/** * @private */Ext.define('Ext.event.publisher.Dom', { extend: 'Ext.event.publisher.Publisher', requires: [ 'Ext.event.Event' ], type: 'dom', /** * @property {Array} handledDomEvents * An array of DOM events that this publisher handles. Events specified in this array * will be added as global listeners on the {@link #target} */ handledDomEvents: [], reEnterCount: 0, // The following events do not bubble, but can still be "captured" at the top of // the DOM, For these events, when the delegated event model is used, we attach a // single listener on the window object using the "useCapture" option. captureEvents: { animationstart: 1, animationend: 1, resize: 1, focus: 1, blur: 1 }, // The following events do not bubble, and cannot be "captured". The only way to // listen for these events is via a listener attached directly to the target element directEvents: { mouseenter: 1, mouseleave: 1, pointerenter: 1, pointerleave: 1, MSPointerEnter: 1, MSPointerLeave: 1, load: 1, unload: 1, beforeunload: 1, error: 1, DOMContentLoaded: 1, DOMFrameContentLoaded: 1, hashchange: 1, // Scroll can be captured, but it is listed here as one of directEvents instead of // captureEvents because in some browsers capturing the scroll event does not work // if the window object itself fired the scroll event. scroll: 1, online: 1, offline: 1 }, /** * In browsers that implement pointerevents when a pointerdown is triggered by touching * the screen, pointerover and pointerenter events will be fired immmediately before * the pointerdown. Also pointerout and pointerleave will be fired immediately after * pointerup when triggered using touch input. For a consistent cross-browser * experience on touch-screens we block pointerover, pointerout, pointerenter, and * pointerleave when triggered by touch input, since in most cases pointerover/pointerenter * behavior is not desired when touching the screen. Note: this should only affect * events with pointerType === 'touch' or pointerType === 'pen', we do NOT want to * block these events when triggered using a mouse. * See also: * http://www.w3.org/TR/pointerevents/#the-pointerdown-event * http://www.w3.org/TR/pointerevents/#the-pointerenter-event * @private */ blockedPointerEvents: { pointerover: 1, pointerout: 1, pointerenter: 1, pointerleave: 1, MSPointerOver: 1, MSPointerOut: 1, MSPointerEnter: 1, MSPointerLeave: 1 }, /** * Browsers with pointer events may implement "compatibility" mouse events: * http://www.w3.org/TR/pointerevents/#compatibility-mapping-with-mouse-events * The behavior implemented in handlers for mouse over/out/enter/leave is not typically * desired when touching the screen, so we map all of these events to their pointer * counterparts in Ext.Element event translation code, so that they can be blocked * via "blockedPointerEvents". The only scenario where this breaks down is in IE10 * with mouseenter/mouseleave, since MSPointerEnter/MSPointerLeave were not implemented * in IE10. For these 2 events we have to resort to a different method - capturing * the timestamp of the last pointer event that has pointerType == 'touch', and if the * mouse event occurred within a certain threshold we can reasonably assume it occurred * because of a touch on the screen (see isEventBlocked) * @private */ blockedCompatibilityMouseEvents: { mouseenter: 1, mouseleave: 1 }, constructor: function() { var me = this, supportsPassive = Ext.supports.PassiveEventListener; me.listenerOptions = supportsPassive ? { passive: false } : false; me.captureOptions = supportsPassive ? { passive: false, capture: true } : true; me.bubbleSubscribers = {}; me.captureSubscribers = {}; me.directSubscribers = {}; me.directCaptureSubscribers = {}; // this map tracks all the names of the events that currently have a delegated // event listener attached so that they can be removed from the dom when the // publisher is destroyed me.delegatedListeners = {}; me.initHandlers(); Ext.onInternalReady(me.onReady, me); me.callParent(); me.registerDomEvents(); }, registerDomEvents: function() { var me = this, publishersByEvent = Ext.event.publisher.Publisher.publishersByEvent, domEvents = me.handledDomEvents, ln = domEvents.length, i, eventName; for (i = 0; i < ln; i++) { eventName = domEvents[i]; me.handles[eventName] = 1; publishersByEvent[eventName] = me; } }, onReady: function() { var me = this, domEvents = me.handledDomEvents, ln, i; if (domEvents) { // If the publisher has handledDomEvents we attach delegated listeners up front // for those events. Dom publisher does not have a list of event names, but // attaches listeners dynamically as subscribers are subscribed. This allows it // to handle all DOM events that are not explicitly handled by another publisher. // Subclasses such as Gesture must explicitly list their handledDomEvents. for (i = 0, ln = domEvents.length; i < ln; i++) { me.addDelegatedListener(domEvents[i]); } } // DOM publishers should be the last thing to go since they are used // to remove any element listeners which is typically part // of the unload destroy process. Ext.getWin().on('unload', me.destroy, me, { priority: -10000 }); }, initHandlers: function() { var me = this; me.onDelegatedEvent = Ext.bind(me.onDelegatedEvent, me); me.onDirectEvent = Ext.bind(me.onDirectEvent, me); me.onDirectCaptureEvent = Ext.bind(me.onDirectCaptureEvent, me); }, addDelegatedListener: function(eventName) { var me = this; me.delegatedListeners[eventName] = 1; me.target.addEventListener( eventName, me.onDelegatedEvent, me.captureEvents[eventName] ? me.captureOptions : me.listenerOptions ); }, removeDelegatedListener: function(eventName) { var me = this; delete me.delegatedListeners[eventName]; me.target.removeEventListener( eventName, me.onDelegatedEvent, me.captureEvents[eventName] ? me.captureOptions : me.listenerOptions ); }, addDirectListener: function(eventName, element, capture) { var me = this; element.dom.addEventListener( eventName, capture ? me.onDirectCaptureEvent : me.onDirectEvent, capture ? me.captureOptions : me.listenerOptions ); }, removeDirectListener: function(eventName, element, capture) { var me = this; element.dom.removeEventListener( eventName, capture ? me.onDirectCaptureEvent : me.onDirectEvent, capture ? me.captureOptions : me.listenerOptions ); }, subscribe: function(element, eventName, delegated, capture) { var me = this, subscribers, id; if (delegated && !me.directEvents[eventName]) { // delegated listeners subscribers = capture ? me.captureSubscribers : me.bubbleSubscribers; if (!me.handles[eventName] && !me.delegatedListeners[eventName]) { // First time we've attached a listener for this eventName - need to begin // listening at the dom level me.addDelegatedListener(eventName); } if (subscribers[eventName]) { ++subscribers[eventName]; } else { subscribers[eventName] = 1; } } else { subscribers = capture ? me.directCaptureSubscribers : me.directSubscribers; id = element.id; // Direct subscribers are tracked by eventName first and by element id second. // This allows the element id key to be deleted when there are no more subscribers // so that this map does not grow indefinitely (it can only grow to a finite // set of event names) - see unsubscribe subscribers = subscribers[eventName] || (subscribers[eventName] = {}); if (subscribers[id]) { ++subscribers[id]; } else { subscribers[id] = 1; me.addDirectListener(eventName, element, capture); } } }, unsubscribe: function(element, eventName, delegated, capture) { var me = this, captureSubscribers, bubbleSubscribers, subscribers, id; if (delegated && !me.directEvents[eventName]) { captureSubscribers = me.captureSubscribers; bubbleSubscribers = me.bubbleSubscribers; subscribers = capture ? captureSubscribers : bubbleSubscribers; if (subscribers[eventName]) { --subscribers[eventName]; } if (!me.handles[eventName] && !bubbleSubscribers[eventName] && !captureSubscribers[eventName]) { // decremented subscribers back to 0 - and the event is not in "handledEvents" // no longer need to listen at the dom level this.removeDelegatedListener(eventName); } } else { subscribers = capture ? me.directCaptureSubscribers : me.directSubscribers; id = element.id; subscribers = subscribers[eventName]; if (subscribers[id]) { --subscribers[id]; } if (!subscribers[id]) { // no more direct subscribers for this element/id/capture, so we can safely // remove the dom listener delete subscribers[id]; me.removeDirectListener(eventName, element, capture); } } }, getPropagatingTargets: function(target) { var currentNode = target, targets = [], parentNode; while (currentNode) { targets.push(currentNode); parentNode = currentNode.parentNode; if (!parentNode) { // If the node has no parentNode it means one of two things - either it is // not in the dom, or we have looped all the way up to the document object. // If the latter is the case we need to add the window object to the targets // to ensure that our propagation mimics browser propagation where events // can bubble from the document to the window. parentNode = currentNode.defaultView; } currentNode = parentNode; } return targets; }, /** * * @param e {Ext.event.Event/Ext.event.Event[]} An event to publish. Can also be an * array of events. Gesture publisher passes an array so that gesture events and * the dom events from which they were synthesized can propagate together. * @param [targets] {HTMLElement[]} propagation targets. Required if `e` is an array. * @param {Boolean} [claimed=false] pass true if we are re-entering publish() to * publish gesture cancellation events that are being fired as a result of something * being claimed. This ensures that cancellation events cannot be claimed. * @protected */ publish: function(e, targets, claimed) { var me = this, hasCaptureSubscribers = false, hasBubbleSubscribers = false, events, type, target, el, i, ln, j, eLn; claimed = claimed || false; // Gesture publisher passes an already created array of propagating targets. // For all other events we need to compute the targets for propagation now. if (!targets) { //<debug> if (e instanceof Array) { Ext.raise("Propagation targets must be supplied when publishing " + "an array of events."); } //</debug> // No targets passed, assume that e is not an array. target = e.target; if (me.captureEvents[e.type]) { el = Ext.cache[target.id]; targets = el ? [el] : []; } else { targets = me.getPropagatingTargets(target); } } // "e" may be either a single event (as is the case for events fired by dom publisher) // or it could be an array of events containing a dom event and its recognized // gesture events. events = Ext.Array.from(e); ln = targets.length; eLn = events.length; for (i = 0; i < eLn; i++) { type = events[i].type; if (!hasCaptureSubscribers && me.captureSubscribers[type]) { hasCaptureSubscribers = true; } if (!hasBubbleSubscribers && me.bubbleSubscribers[type]) { hasBubbleSubscribers = true; } } // We will now proceed to fire events in both capture and bubble phases. You // may notice that we are looping all potential targets both times, and only // firing on the target if there is an Ext.Element wrapper in the cache. This is // done (vs. eliminating non-cached targets from the array up front) because // event handlers can add listeners to other elements during propagation. Looping // all the potential targets ensures that these dynamically added listeners // are fired. See https://sencha.jira.com/browse/EXTJS-15953 // capture phase (top-down event propagation). if (hasCaptureSubscribers) { for (i = ln; i--;) { el = Ext.cache[targets[i].id]; if (el) { for (j = 0; j < eLn; j++) { e = events[j]; me.fire(el, e.type, e, false, true); if (!claimed && e.claimed) { claimed = true; j = me.filterClaimed(events, e); eLn = events.length; // filterClaimed may remove items } if (e.stopped) { events.splice(j, 1); j--; eLn--; } } } } } // bubble phase (bottom-up event propagation). // stopPropagation during capture phase cancels entire bubble phase if (hasBubbleSubscribers && !e.stopped) { for (i = 0; i < ln; i++) { el = Ext.cache[targets[i].id]; if (el) { for (j = 0; j < eLn; j++) { e = events[j]; me.fire(el, e.type, e, false, false); if (!claimed && e.claimed && me.filterClaimed) { claimed = true; j = me.filterClaimed(events, e); eLn = events.length; // filterClaimed may remove items } if (e.stopped) { events.splice(j, 1); j--; eLn--; } } } } } }, /** * Hook for gesture publisher to override and perform gesture recognition * @param {Ext.event.Event} e */ publishDelegatedDomEvent: function(e) { this.publish(e); }, fire: function(element, eventName, e, direct, capture) { var event; if (element.hasListeners[eventName]) { event = element.events[eventName]; if (event) { if (capture && direct) { event = event.directCaptures; } else if (capture) { event = event.captures; } else if (direct) { event = event.directs; } // yes, this second null check for event is necessary - one of the // above assignments might have resulted in undefined if (event) { e.setCurrentTarget(element.dom); event.fire(e, e.target); } } } }, onDelegatedEvent: function(e) { if (Ext.elevateFunction) { // using [e] is faster than using arguments in most browsers // http://jsperf.com/passing-arguments Ext.elevateFunction(this.doDelegatedEvent, this, [e]); } else { this.doDelegatedEvent(e); } }, doDelegatedEvent: function(e) { var me = this, timeStamp; e = new Ext.event.Event(e); timeStamp = e.time; if (!me.isEventBlocked(e)) { me.beforeEvent(e); Ext.frameStartTime = timeStamp; me.reEnterCountAdjusted = false; me.reEnterCount++; me.publishDelegatedDomEvent(e); // Gesture publisher deals with exceptions in recognizers if (!me.reEnterCountAdjusted) { me.reEnterCount--; } me.afterEvent(e); } }, /** * Handler for directly-attached (non-delegated) dom events * @param {Event} e * @private */ onDirectEvent: function(e) { if (Ext.elevateFunction) { // using [e] is faster than using arguments in most browsers // http://jsperf.com/passing-arguments Ext.elevateFunction(this.doDirectEvent, this, [e, false]); } else { this.doDirectEvent(e, false); } }, // When eventPhase is AT_TARGET there's no way to know if we are handling a capture // or bubble listener, hence the need for this separate handler fn onDirectCaptureEvent: function(e) { if (Ext.elevateFunction) { // using [e] is faster than using arguments in most browsers // http://jsperf.com/passing-arguments Ext.elevateFunction(this.doDirectEvent, this, [e, true]); } else { this.doDirectEvent(e, true); } }, doDirectEvent: function(e, capture) { var me = this, currentTarget = e.currentTarget, timeStamp, el; e = new Ext.event.Event(e); timeStamp = e.time; if (me.isEventBlocked(e)) { return; } me.beforeEvent(e); Ext.frameStartTime = timeStamp; el = Ext.cache[currentTarget.id]; // Element can be removed from the cache by this time, with the node // still lingering for some reason. This can happen for example when // load event is fired on an iframe that we constructed when submitting // a form for file uploads. if (el) { // Since natural DOM propagation has occurred, no emulated propagation is needed. // Simply dispatch the event on the currentTarget element me.reEnterCountAdjusted = false; me.reEnterCount++; me.fire(el, e.type, e, true, capture); // Gesture publisher deals with exceptions in recognizers if (!me.reEnterCountAdjusted) { me.reEnterCount--; } } me.afterEvent(e); }, beforeEvent: function(e) { var browserEvent = e.browserEvent, // use full class name, not me.self, so that Dom and Gesture publishers will // both place flags on the same object. self = Ext.event.publisher.Dom, touches, touch; if (browserEvent.type === 'touchstart') { touches = browserEvent.touches; if (touches.length === 1) { // capture the coordinates of the first touchstart event so we can use // them to eliminate duplicate mouse events if needed, (see isEventBlocked). touch = touches[0]; self.lastTouchStartX = touch.pageX; self.lastTouchStartY = touch.pageY; } } }, afterEvent: function(e) { var browserEvent = e.browserEvent, type = browserEvent.type, // use full class name, not me.self, so that Dom and Gesture publishers will // both place flags on the same object. self = Ext.event.publisher.Dom, GlobalEvents = Ext.GlobalEvents; // It is important that the following time stamps are captured after the handlers // have been invoked because they need to represent the "exit" time, so that they // can be compared against the next "entry" time into onDelegatedEvent or // onDirectEvent to detect the time lapse in between the firing of the 2 events. // We set these flags on "this.self" so that they can be shared between Dom // publisher and subclasses if (e.self.pointerEvents[type] && e.pointerType !== 'mouse') { // track the last time a pointer event was fired as a result of interaction // with the screen, pointerType === 'touch' most likely but could also be // pointerType === 'pen' hence the reason we use !== 'mouse', This is used // to eliminate potential duplicate "compatibility" mouse events // (see isEventBlocked) self.lastScreenPointerEventTime = Ext.now(); } if (type === 'touchend') { // Capture a time stamp so we can use it to eliminate potential duplicate // emulated mouse events on multi-input devices that have touch events, // e.g. Chrome on Window8 with touch-screen (see isEventBlocked). self.lastTouchEndTime = Ext.now(); } if (!this.reEnterCount && !GlobalEvents.idleEventMask[type]) { Ext.fireIdle(); } }, /** * Detects if the given event should be blocked from firing because it is an emulated * "compatibility" mouse event triggered by a touch on the screen. * @param {Ext.event.Event} e * @return {Boolean} * @private */ isEventBlocked: function(e) { var me = this, type = e.type, // use full class name, not me.self, so that Dom and Gesture publishers will // both look for flags on the same object. self = Ext.event.publisher.Dom, now = Ext.now(); // Gecko has a bug where right clicking will trigger both a contextmenu // and click event. This only occurs when delegating the event onto the window // object like we do by default for delegated events. // This is not possible to feature detect using synthetic events. // Ticket logged: https://bugzilla.mozilla.org/show_bug.cgi?id=1156023 if (Ext.isGecko && e.type === 'click' && e.button === 2) { return true; } // prevent emulated pointerover, pointerout, pointerenter, and pointerleave // events from firing when triggered by touching the screen. return (me.blockedPointerEvents[type] && e.pointerType !== 'mouse') || // prevent compatibility mouse events from firing on devices with pointer // events - see comment on blockedCompatibilityMouseEvents for more details // The time from when the last pointer event fired until when compatibility // events are received varies depending on the browser, device, and application // so we use 1 second to be safe (me.blockedCompatibilityMouseEvents[type] && (now - self.lastScreenPointerEventTime < 1000)) || (Ext.supports.TouchEvents && e.self.mouseEvents[e.type] && // some browsers (e.g. webkit on Windows 8 with touch screen) emulate mouse // events after touch events have fired. This only seems to happen when there // is no movement present, so, for example, a touchstart followed immediately // by a touchend would result in the following sequence of events: // "touchstart, touchend, mousemove, mousedown, mouseup" // yes, you read that right, the emulated mousemove fires before mousedown. // However, touch events with movement (touchstart, touchmove, then touchend) // do not trigger the emulated mouse events. // The side effect of this behavior is that single-touch gestures that expect // no movement (e.g. tap) can double-fire - once when the touchstart/touchend // occurs, and then again when the emulated mousedown/up occurs. // We cannot solve the problem by only listening for touch events and ignoring // mouse events, since we may be on a multi-input device that supports both // touch and mouse events and we want gestures to respond to both kinds of // events. Instead we have to detect if the mouse event is a "dupe" by // checking if its coordinates are near the last touchstart's coordinates, // and if it's timestamp is within a certain threshold of the last touchend // event's timestamp. This is because when dealing with multi-touch events, // the emulated mousedown event (when it does fire) will fire with approximately // the same coordinates as the first touchstart, but within a short time after // the last touchend. We use 15px as the distance threshold, to be on the safe // side because the difference in coordinates can sometimes be up to 6px. Math.abs(e.pageX - self.lastTouchStartX) < 15 && Math.abs(e.pageY - self.lastTouchStartY) < 15 && // in the majority of cases, the emulated mousedown is observed within // 5ms of touchend, however, to be certain we avoid a situation where a // gesture handler gets executed twice we use a threshold of 1000ms. The // side effect of this is that if a user touches the screen and then quickly // clicks screen in the same spot, the mousedown/mouseup sequence that // ensues will not trigger any gesture recognizers. (Ext.now() - self.lastTouchEndTime) < 1000); }, destroy: function() { var GC = Ext.dom['GarbageCollector'], // eslint-disable-line dot-notation eventName; for (eventName in this.delegatedListeners) { this.removeDelegatedListener(eventName); } // We are wired to the unload event, so we ensure cleanup of low-level stuff // like the Reaper and the GarbageCollector. Ext.Reaper.flush(); if (GC) { GC.collect(); } this.callParent(); }, /** * Resets the internal state of the Dom publisher. Internally the Dom publisher * keeps track of timing and coordinates of events for eliminating browser duplicates * (e.g. emulated mousedown after pointerdown etc.). This method resets all this * cached data to a state similar to when the publisher was first instantiated. * * Applications will not typically need to use this method, but it is useful for * Unit-testing situations where a clean slate is required for each test. */ reset: function() { // use full class name, not me.self, so that Dom and Gesture publishers will // both reset flags on the same object. var self = Ext.event.publisher.Dom; this.reEnterCount = 0; // set to undefined, not null, because that is the initial state of these vars and // undefined/null return different results when used in math operations // (see isEventBlocked) self.lastScreenPointerEventTime = self.lastTouchEndTime = self.lastTouchStartX = self.lastTouchStartY = undefined; }}, function(Dom) { var doc = document, defaultView = doc.defaultView, prototype = Dom.prototype; if ((Ext.os.is.iOS && Ext.os.version.getMajor() < 5) || Ext.browser.is.AndroidStock || !(defaultView && defaultView.addEventListener)) { // Delegated listeners will get attached to the document object because // attaching to the window object will not work. In IE8 this is needed because // events do not bubble up to the window - bubbling stops at the document // object. The iOS < 5 check was carried forward from Sencha Touch 2.3 - // Not sure why it was needed. The check for (defaultView && defaultView.addEventListener) // was carried forward as well - it may be required for older mobile browsers. // see also TOUCH-5408 prototype.target = doc; } else { /** * @member Ext.event.publisher.Dom * @property {Object} target the DOM target to which listeners are attached for * delegated events. * @private */ prototype.target = defaultView; } Dom.instance = new Dom();});