/**
* @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;
// In case of iOS if it is in iFrame then it should enter if condition
if ((Ext.os.is.iOS && window.self !== window.top) || 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();
});