]>
Commit | Line | Data |
---|---|---|
6527f429 DM |
1 | /**\r |
2 | * @private\r | |
3 | */\r | |
4 | Ext.define('Ext.event.publisher.Dom', {\r | |
5 | extend: 'Ext.event.publisher.Publisher',\r | |
6 | requires: [\r | |
7 | 'Ext.event.Event'\r | |
8 | ],\r | |
9 | \r | |
10 | type: 'dom',\r | |
11 | \r | |
12 | /**\r | |
13 | * @property {Array} handledDomEvents\r | |
14 | * An array of DOM events that this publisher handles. Events specified in this array\r | |
15 | * will be added as global listeners on the {@link #target}\r | |
16 | */\r | |
17 | handledDomEvents: [],\r | |
18 | \r | |
19 | reEnterCount: 0,\r | |
20 | \r | |
21 | // The following events do not bubble, but can still be "captured" at the top of\r | |
22 | // the DOM, For these events, when the delegated event model is used, we attach a\r | |
23 | // single listener on the window object using the "useCapture" option.\r | |
24 | captureEvents: {\r | |
25 | animationstart: 1,\r | |
26 | animationend: 1,\r | |
27 | resize: 1,\r | |
28 | focus: 1,\r | |
29 | blur: 1,\r | |
30 | scroll: 1\r | |
31 | },\r | |
32 | \r | |
33 | // The following events do not bubble, and cannot be "captured". The only way to\r | |
34 | // listen for these events is via a listener attached directly to the target element\r | |
35 | directEvents: {\r | |
36 | mouseenter: 1,\r | |
37 | mouseleave: 1,\r | |
38 | pointerenter: 1,\r | |
39 | pointerleave: 1,\r | |
40 | MSPointerEnter: 1,\r | |
41 | MSPointerLeave: 1,\r | |
42 | load: 1,\r | |
43 | unload: 1,\r | |
44 | beforeunload: 1,\r | |
45 | error: 1,\r | |
46 | DOMContentLoaded: 1,\r | |
47 | DOMFrameContentLoaded: 1,\r | |
48 | hashchange: 1\r | |
49 | },\r | |
50 | \r | |
51 | /**\r | |
52 | * In browsers that implement pointerevents when a pointerdown is triggered by touching\r | |
53 | * the screen, pointerover and pointerenter events will be fired immmediately before\r | |
54 | * the pointerdown. Also pointerout and pointerleave will be fired immediately after\r | |
55 | * pointerup when triggered using touch input. For a consistent cross-browser\r | |
56 | * experience on touch-screens we block pointerover, pointerout, pointerenter, and\r | |
57 | * pointerleave when triggered by touch input, since in most cases pointerover/pointerenter\r | |
58 | * behavior is not desired when touching the screen. Note: this should only affect\r | |
59 | * events with pointerType === 'touch' or pointerType === 'pen', we do NOT want to\r | |
60 | * block these events when triggered using a mouse.\r | |
61 | * See also:\r | |
62 | * http://www.w3.org/TR/pointerevents/#the-pointerdown-event\r | |
63 | * http://www.w3.org/TR/pointerevents/#the-pointerenter-event\r | |
64 | * @private\r | |
65 | */\r | |
66 | blockedPointerEvents: {\r | |
67 | pointerover: 1,\r | |
68 | pointerout: 1,\r | |
69 | pointerenter: 1,\r | |
70 | pointerleave: 1,\r | |
71 | MSPointerOver: 1,\r | |
72 | MSPointerOut: 1,\r | |
73 | MSPointerEnter: 1,\r | |
74 | MSPointerLeave: 1\r | |
75 | },\r | |
76 | \r | |
77 | /**\r | |
78 | * Browsers with pointer events may implement "compatibility" mouse events:\r | |
79 | * http://www.w3.org/TR/pointerevents/#compatibility-mapping-with-mouse-events\r | |
80 | * The behavior implemented in handlers for mouse over/out/enter/leave is not typically\r | |
81 | * desired when touching the screen, so we map all of these events to their pointer\r | |
82 | * counterparts in Ext.Element event translation code, so that they can be blocked\r | |
83 | * via "blockedPointerEvents". The only scenario where this breaks down is in IE10\r | |
84 | * with mouseenter/mouseleave, since MSPointerEnter/MSPointerLeave were not implemented\r | |
85 | * in IE10. For these 2 events we have to resort to a different method - capturing\r | |
86 | * the timestamp of the last pointer event that has pointerType == 'touch', and if the\r | |
87 | * mouse event occurred within a certain threshold we can reasonably assume it occurred\r | |
88 | * because of a touch on the screen (see isEventBlocked)\r | |
89 | * @private\r | |
90 | */\r | |
91 | blockedCompatibilityMouseEvents: {\r | |
92 | mouseenter: 1,\r | |
93 | mouseleave: 1\r | |
94 | },\r | |
95 | \r | |
96 | constructor: function() {\r | |
97 | var me = this;\r | |
98 | \r | |
99 | me.bubbleSubscribers = {};\r | |
100 | me.captureSubscribers = {};\r | |
101 | me.directSubscribers = {};\r | |
102 | me.directCaptureSubscribers = {};\r | |
103 | \r | |
104 | // this map tracks all the names of the events that currently have a delegated\r | |
105 | // event listener attached so that they can be removed from the dom when the\r | |
106 | // publisher is destroyed\r | |
107 | me.delegatedListeners = {};\r | |
108 | \r | |
109 | me.initHandlers();\r | |
110 | \r | |
111 | Ext.onInternalReady(me.onReady, me);\r | |
112 | \r | |
113 | me.callParent();\r | |
114 | },\r | |
115 | \r | |
116 | registerEvents: function() {\r | |
117 | var me = this,\r | |
118 | publishersByEvent = Ext.event.publisher.Publisher.publishersByEvent,\r | |
119 | domEvents = me.handledDomEvents,\r | |
120 | ln = domEvents.length,\r | |
121 | i = 0,\r | |
122 | eventName;\r | |
123 | \r | |
124 | for (; i < ln; i++) {\r | |
125 | eventName = domEvents[i];\r | |
126 | me.handles[eventName] = 1;\r | |
127 | publishersByEvent[eventName] = me;\r | |
128 | }\r | |
129 | \r | |
130 | this.callParent();\r | |
131 | },\r | |
132 | \r | |
133 | onReady: function() {\r | |
134 | var me = this,\r | |
135 | domEvents = me.handledDomEvents,\r | |
136 | ln, i;\r | |
137 | \r | |
138 | if (domEvents) {\r | |
139 | // If the publisher has handledDomEvents we attach delegated listeners up front\r | |
140 | // for those events. Dom publisher does not have a list of event names, but\r | |
141 | // attaches listeners dynamically as subscribers are subscribed. This allows it\r | |
142 | // to handle all DOM events that are not explicitly handled by another publisher.\r | |
143 | // Subclasses such as Gesture must explicitly list their handledDomEvents.\r | |
144 | for (i = 0, ln = domEvents.length; i < ln; i++) {\r | |
145 | me.addDelegatedListener(domEvents[i]);\r | |
146 | }\r | |
147 | }\r | |
148 | \r | |
149 | Ext.getWin().on('unload', me.destroy, me);\r | |
150 | },\r | |
151 | \r | |
152 | initHandlers: function() {\r | |
153 | var me = this;\r | |
154 | \r | |
155 | me.onDelegatedEvent = Ext.bind(me.onDelegatedEvent, me);\r | |
156 | me.onDirectEvent = Ext.bind(me.onDirectEvent, me);\r | |
157 | me.onDirectCaptureEvent = Ext.bind(me.onDirectCaptureEvent, me);\r | |
158 | },\r | |
159 | \r | |
160 | addDelegatedListener: function(eventName) {\r | |
161 | this.delegatedListeners[eventName] = 1;\r | |
162 | this.target.addEventListener(\r | |
163 | eventName, this.onDelegatedEvent, !!this.captureEvents[eventName]\r | |
164 | );\r | |
165 | },\r | |
166 | \r | |
167 | removeDelegatedListener: function(eventName) {\r | |
168 | delete this.delegatedListeners[eventName];\r | |
169 | this.target.removeEventListener(\r | |
170 | eventName, this.onDelegatedEvent, !!this.captureEvents[eventName]\r | |
171 | );\r | |
172 | },\r | |
173 | \r | |
174 | addDirectListener: function(eventName, element, capture) {\r | |
175 | element.dom.addEventListener(\r | |
176 | eventName,\r | |
177 | capture ? this.onDirectCaptureEvent : this.onDirectEvent,\r | |
178 | capture\r | |
179 | );\r | |
180 | },\r | |
181 | \r | |
182 | removeDirectListener: function(eventName, element, capture) {\r | |
183 | element.dom.removeEventListener(\r | |
184 | eventName,\r | |
185 | capture ? this.onDirectCaptureEvent : this.onDirectEvent,\r | |
186 | capture\r | |
187 | );\r | |
188 | },\r | |
189 | \r | |
190 | subscribe: function(element, eventName, delegated, capture) {\r | |
191 | var me = this,\r | |
192 | subscribers, id;\r | |
193 | \r | |
194 | if (delegated && !me.directEvents[eventName]) {\r | |
195 | // delegated listeners\r | |
196 | subscribers = capture ? me.captureSubscribers : me.bubbleSubscribers;\r | |
197 | if (!me.handles[eventName] && !me.delegatedListeners[eventName]) {\r | |
198 | // First time we've attached a listener for this eventName - need to begin\r | |
199 | // listening at the dom level\r | |
200 | me.addDelegatedListener(eventName);\r | |
201 | }\r | |
202 | \r | |
203 | if (subscribers[eventName]) {\r | |
204 | ++subscribers[eventName];\r | |
205 | } else {\r | |
206 | subscribers[eventName] = 1;\r | |
207 | }\r | |
208 | } else {\r | |
209 | subscribers = capture ? me.directCaptureSubscribers : me.directSubscribers;\r | |
210 | \r | |
211 | id = element.id;\r | |
212 | // Direct subscribers are tracked by eventName first and by element id second.\r | |
213 | // This allows the element id key to be deleted when there are no more subscribers\r | |
214 | // so that this map does not grow indefinitely (it can only grow to a finite\r | |
215 | // set of event names) - see unsubscribe\r | |
216 | subscribers = subscribers[eventName] || (subscribers[eventName] = {});\r | |
217 | if (subscribers[id]) {\r | |
218 | ++subscribers[id];\r | |
219 | } else {\r | |
220 | subscribers[id] = 1;\r | |
221 | me.addDirectListener(eventName, element, capture);\r | |
222 | }\r | |
223 | }\r | |
224 | },\r | |
225 | \r | |
226 | unsubscribe: function(element, eventName, delegated, capture) {\r | |
227 | var me = this,\r | |
228 | captureSubscribers, bubbleSubscribers, subscribers, id;\r | |
229 | \r | |
230 | if (delegated && !me.directEvents[eventName]) {\r | |
231 | captureSubscribers = me.captureSubscribers;\r | |
232 | bubbleSubscribers = me.bubbleSubscribers;\r | |
233 | subscribers = capture ? captureSubscribers : bubbleSubscribers;\r | |
234 | \r | |
235 | if (subscribers[eventName]) {\r | |
236 | --subscribers[eventName];\r | |
237 | }\r | |
238 | \r | |
239 | if (!me.handles[eventName] && !bubbleSubscribers[eventName] && !captureSubscribers[eventName]) {\r | |
240 | // decremented subscribers back to 0 - and the event is not in "handledEvents"\r | |
241 | // no longer need to listen at the dom level\r | |
242 | this.removeDelegatedListener(eventName);\r | |
243 | }\r | |
244 | } else {\r | |
245 | subscribers = capture ? me.directCaptureSubscribers : me.directSubscribers;\r | |
246 | \r | |
247 | id = element.id;\r | |
248 | subscribers = subscribers[eventName];\r | |
249 | if (subscribers[id]) {\r | |
250 | --subscribers[id];\r | |
251 | }\r | |
252 | \r | |
253 | if (!subscribers[id]) {\r | |
254 | // no more direct subscribers for this element/id/capture, so we can safely\r | |
255 | // remove the dom listener\r | |
256 | delete subscribers[id];\r | |
257 | me.removeDirectListener(eventName, element, capture);\r | |
258 | }\r | |
259 | }\r | |
260 | },\r | |
261 | \r | |
262 | getPropagatingTargets: function(target) {\r | |
263 | var currentNode = target,\r | |
264 | targets = [],\r | |
265 | parentNode;\r | |
266 | \r | |
267 | while (currentNode) {\r | |
268 | targets.push(currentNode);\r | |
269 | parentNode = currentNode.parentNode;\r | |
270 | \r | |
271 | if (!parentNode) {\r | |
272 | // If the node has no parentNode it means one of two things - either it is\r | |
273 | // not in the dom, or we have looped all the way up to the document object.\r | |
274 | // If the latter is the case we need to add the window object to the targets\r | |
275 | // to ensure that our propagation mimics browser propagation where events\r | |
276 | // can bubble from the document to the window.\r | |
277 | parentNode = currentNode.defaultView;\r | |
278 | }\r | |
279 | \r | |
280 | currentNode = parentNode;\r | |
281 | }\r | |
282 | \r | |
283 | return targets;\r | |
284 | },\r | |
285 | \r | |
286 | publish: function(eventName, target, e) {\r | |
287 | var me = this,\r | |
288 | targets, el, i, ln;\r | |
289 | \r | |
290 | if (Ext.isArray(target)) {\r | |
291 | // Gesture publisher passes an already created array of propagating targets\r | |
292 | targets = target;\r | |
293 | } else if (me.captureEvents[eventName]) {\r | |
294 | el = Ext.cache[target.id];\r | |
295 | targets = el ? [el] : [];\r | |
296 | } else {\r | |
297 | targets = me.getPropagatingTargets(target);\r | |
298 | }\r | |
299 | \r | |
300 | ln = targets.length;\r | |
301 | \r | |
302 | // We will now proceed to fire events in both capture and bubble phases. You\r | |
303 | // may notice that we are looping all potential targets both times, and only\r | |
304 | // firing on the target if there is an Ext.Element wrapper in the cache. This is\r | |
305 | // done (vs. eliminating non-cached targets from the array up front) because\r | |
306 | // event handlers can add listeners to other elements during propagation. Looping\r | |
307 | // all the potential targets ensures that these dynamically added listeners\r | |
308 | // are fired. See https://sencha.jira.com/browse/EXTJS-15953\r | |
309 | \r | |
310 | // capture phase (top-down event propagation).\r | |
311 | if (me.captureSubscribers[eventName]) {\r | |
312 | for (i = ln; i--;) {\r | |
313 | el = Ext.cache[targets[i].id];\r | |
314 | if (el) {\r | |
315 | me.fire(el, eventName, e, false, true);\r | |
316 | if (e.isStopped) {\r | |
317 | break;\r | |
318 | }\r | |
319 | }\r | |
320 | }\r | |
321 | }\r | |
322 | \r | |
323 | // bubble phase (bottom-up event propagation).\r | |
324 | // stopPropagation during capture phase cancels entire bubble phase\r | |
325 | if (!e.isStopped && me.bubbleSubscribers[eventName]) {\r | |
326 | for (i = 0; i < ln; i++) {\r | |
327 | el = Ext.cache[targets[i].id];\r | |
328 | if (el) {\r | |
329 | me.fire(el, eventName, e, false, false);\r | |
330 | if (e.isStopped) {\r | |
331 | break;\r | |
332 | }\r | |
333 | }\r | |
334 | }\r | |
335 | }\r | |
336 | },\r | |
337 | \r | |
338 | fire: function(element, eventName, e, direct, capture) {\r | |
339 | var event;\r | |
340 | \r | |
341 | if (element.hasListeners[eventName]) {\r | |
342 | event = element.events[eventName];\r | |
343 | \r | |
344 | if (event) {\r | |
345 | if (capture && direct) {\r | |
346 | event = event.directCaptures;\r | |
347 | } else if (capture) {\r | |
348 | event = event.captures;\r | |
349 | } else if (direct) {\r | |
350 | event = event.directs;\r | |
351 | }\r | |
352 | \r | |
353 | // yes, this second null check for event is necessary - one of the\r | |
354 | // above assignments might have resulted in undefined\r | |
355 | if (event) {\r | |
356 | e.setCurrentTarget(element.dom);\r | |
357 | event.fire(e, e.target);\r | |
358 | }\r | |
359 | }\r | |
360 | }\r | |
361 | },\r | |
362 | \r | |
363 | onDelegatedEvent: function(e) {\r | |
364 | if (Ext.elevateFunction) {\r | |
365 | // using [e] is faster than using arguments in most browsers\r | |
366 | // http://jsperf.com/passing-arguments\r | |
367 | Ext.elevateFunction(this.doDelegatedEvent, this, [e]);\r | |
368 | } else {\r | |
369 | this.doDelegatedEvent(e);\r | |
370 | }\r | |
371 | },\r | |
372 | \r | |
373 | doDelegatedEvent: function(e, invokeAfter) {\r | |
374 | var me = this,\r | |
375 | timeStamp = e.timeStamp;\r | |
376 | \r | |
377 | e = new Ext.event.Event(e);\r | |
378 | \r | |
379 | if (me.isEventBlocked(e)) {\r | |
380 | return false;\r | |
381 | }\r | |
382 | \r | |
383 | me.beforeEvent(e);\r | |
384 | \r | |
385 | Ext.frameStartTime = timeStamp;\r | |
386 | \r | |
387 | me.reEnterCount++;\r | |
388 | me.publish(e.type, e.target, e);\r | |
389 | me.reEnterCount--;\r | |
390 | \r | |
391 | if (invokeAfter !== false) {\r | |
392 | me.afterEvent(e);\r | |
393 | }\r | |
394 | \r | |
395 | return e;\r | |
396 | },\r | |
397 | \r | |
398 | /**\r | |
399 | * Handler for directly-attached (non-delegated) dom events\r | |
400 | * @param {Event} e\r | |
401 | * @private\r | |
402 | */\r | |
403 | onDirectEvent: function(e) {\r | |
404 | if (Ext.elevateFunction) {\r | |
405 | // using [e] is faster than using arguments in most browsers\r | |
406 | // http://jsperf.com/passing-arguments\r | |
407 | Ext.elevateFunction(this.doDirectEvent, this, [e, false]);\r | |
408 | } else {\r | |
409 | this.doDirectEvent(e, false);\r | |
410 | }\r | |
411 | },\r | |
412 | \r | |
413 | // When eventPhase is AT_TARGET there's no way to know if we are handling a capture\r | |
414 | // or bubble listener, hence the need for this separate handler fn\r | |
415 | onDirectCaptureEvent: function(e) {\r | |
416 | if (Ext.elevateFunction) {\r | |
417 | // using [e] is faster than using arguments in most browsers\r | |
418 | // http://jsperf.com/passing-arguments\r | |
419 | Ext.elevateFunction(this.doDirectEvent, this, [e, true]);\r | |
420 | } else {\r | |
421 | this.doDirectEvent(e, true);\r | |
422 | }\r | |
423 | },\r | |
424 | \r | |
425 | doDirectEvent: function(e, capture) {\r | |
426 | var me = this,\r | |
427 | currentTarget = e.currentTarget,\r | |
428 | timeStamp = e.timeStamp,\r | |
429 | el;\r | |
430 | \r | |
431 | e = new Ext.event.Event(e);\r | |
432 | \r | |
433 | if (me.isEventBlocked(e)) {\r | |
434 | return;\r | |
435 | }\r | |
436 | \r | |
437 | me.beforeEvent(e);\r | |
438 | \r | |
439 | Ext.frameStartTime = timeStamp;\r | |
440 | \r | |
441 | el = Ext.cache[currentTarget.id];\r | |
442 | \r | |
443 | // Element can be removed from the cache by this time, with the node\r | |
444 | // still lingering for some reason. This can happen for example when\r | |
445 | // load event is fired on an iframe that we constructed when submitting\r | |
446 | // a form for file uploads.\r | |
447 | if (el) {\r | |
448 | // Since natural DOM propagation has occurred, no emulated propagation is needed.\r | |
449 | // Simply dispatch the event on the currentTarget element\r | |
450 | me.reEnterCount++;\r | |
451 | me.fire(el, e.type, e, true, capture);\r | |
452 | me.reEnterCount--;\r | |
453 | }\r | |
454 | \r | |
455 | me.afterEvent(e);\r | |
456 | },\r | |
457 | \r | |
458 | beforeEvent: function(e) {\r | |
459 | var browserEvent = e.browserEvent,\r | |
460 | // use full class name, not me.self, so that Dom and Gesture publishers will\r | |
461 | // both place flags on the same object.\r | |
462 | self = Ext.event.publisher.Dom,\r | |
463 | touches, touch;\r | |
464 | \r | |
465 | if (browserEvent.type === 'touchstart') {\r | |
466 | touches = browserEvent.touches;\r | |
467 | \r | |
468 | if (touches.length === 1) {\r | |
469 | // capture the coordinates of the first touchstart event so we can use\r | |
470 | // them to eliminate duplicate mouse events if needed, (see isEventBlocked).\r | |
471 | touch = touches[0];\r | |
472 | self.lastTouchStartX = touch.pageX;\r | |
473 | self.lastTouchStartY = touch.pageY;\r | |
474 | }\r | |
475 | }\r | |
476 | },\r | |
477 | \r | |
478 | afterEvent: function(e) {\r | |
479 | var browserEvent = e.browserEvent,\r | |
480 | type = browserEvent.type,\r | |
481 | // use full class name, not me.self, so that Dom and Gesture publishers will\r | |
482 | // both place flags on the same object.\r | |
483 | self = Ext.event.publisher.Dom,\r | |
484 | GlobalEvents = Ext.GlobalEvents;\r | |
485 | \r | |
486 | // It is important that the following time stamps are captured after the handlers\r | |
487 | // have been invoked because they need to represent the "exit" time, so that they\r | |
488 | // can be compared against the next "entry" time into onDelegatedEvent or\r | |
489 | // onDirectEvent to detect the time lapse in between the firing of the 2 events.\r | |
490 | // We set these flags on "this.self" so that they can be shared between Dom\r | |
491 | // publisher and subclasses\r | |
492 | \r | |
493 | if (e.self.pointerEvents[type] && e.pointerType !== 'mouse') {\r | |
494 | // track the last time a pointer event was fired as a result of interaction\r | |
495 | // with the screen, pointerType === 'touch' most likely but could also be\r | |
496 | // pointerType === 'pen' hence the reason we use !== 'mouse', This is used\r | |
497 | // to eliminate potential duplicate "compatibility" mouse events\r | |
498 | // (see isEventBlocked)\r | |
499 | self.lastScreenPointerEventTime = Ext.now();\r | |
500 | }\r | |
501 | \r | |
502 | if (type === 'touchend') {\r | |
503 | // Capture a time stamp so we can use it to eliminate potential duplicate\r | |
504 | // emulated mouse events on multi-input devices that have touch events,\r | |
505 | // e.g. Chrome on Window8 with touch-screen (see isEventBlocked).\r | |
506 | self.lastTouchEndTime = Ext.now();\r | |
507 | }\r | |
508 | \r | |
509 | if (!this.reEnterCount && GlobalEvents.hasListeners.idle && !GlobalEvents.idleEventMask[type]) {\r | |
510 | GlobalEvents.fireEvent('idle');\r | |
511 | }\r | |
512 | },\r | |
513 | \r | |
514 | /**\r | |
515 | * Detects if the given event should be blocked from firing because it is an emulated\r | |
516 | * "compatibility" mouse event triggered by a touch on the screen.\r | |
517 | * @param {Ext.event.Event} e\r | |
518 | * @return {Boolean}\r | |
519 | * @private\r | |
520 | */\r | |
521 | isEventBlocked: function(e) {\r | |
522 | var me = this,\r | |
523 | type = e.type,\r | |
524 | // use full class name, not me.self, so that Dom and Gesture publishers will\r | |
525 | // both look for flags on the same object.\r | |
526 | self = Ext.event.publisher.Dom,\r | |
527 | now = Ext.now();\r | |
528 | \r | |
529 | // Gecko has a bug where right clicking will trigger both a contextmenu\r | |
530 | // and click event. This only occurs when delegating the event onto the window\r | |
531 | // object like we do by default for delegated events.\r | |
532 | // This is not possible to feature detect using synthetic events.\r | |
533 | // Ticket logged: https://bugzilla.mozilla.org/show_bug.cgi?id=1156023\r | |
534 | if (Ext.isGecko && e.type === 'click' && e.button === 2) {\r | |
535 | return true;\r | |
536 | }\r | |
537 | \r | |
538 | // prevent emulated pointerover, pointerout, pointerenter, and pointerleave\r | |
539 | // events from firing when triggered by touching the screen.\r | |
540 | return (me.blockedPointerEvents[type] && e.pointerType !== 'mouse') ||\r | |
541 | // prevent compatibility mouse events from firing on devices with pointer\r | |
542 | // events - see comment on blockedCompatibilityMouseEvents for more details\r | |
543 | // The time from when the last pointer event fired until when compatibility\r | |
544 | // events are received varies depending on the browser, device, and application\r | |
545 | // so we use 1 second to be safe\r | |
546 | (me.blockedCompatibilityMouseEvents[type] &&\r | |
547 | (now - self.lastScreenPointerEventTime < 1000)) ||\r | |
548 | \r | |
549 | (Ext.supports.TouchEvents && e.self.mouseEvents[e.type] &&\r | |
550 | // some browsers (e.g. webkit on Windows 8 with touch screen) emulate mouse\r | |
551 | // events after touch events have fired. This only seems to happen when there\r | |
552 | // is no movement present, so, for example, a touchstart followed immediately\r | |
553 | // by a touchend would result in the following sequence of events:\r | |
554 | // "touchstart, touchend, mousemove, mousedown, mouseup"\r | |
555 | // yes, you read that right, the emulated mousemove fires before mousedown.\r | |
556 | // However, touch events with movement (touchstart, touchmove, then touchend)\r | |
557 | // do not trigger the emulated mouse events.\r | |
558 | // The side effect of this behavior is that single-touch gestures that expect\r | |
559 | // no movement (e.g. tap) can double-fire - once when the touchstart/touchend\r | |
560 | // occurs, and then again when the emulated mousedown/up occurs.\r | |
561 | // We cannot solve the problem by only listening for touch events and ignoring\r | |
562 | // mouse events, since we may be on a multi-input device that supports both\r | |
563 | // touch and mouse events and we want gestures to respond to both kinds of\r | |
564 | // events. Instead we have to detect if the mouse event is a "dupe" by\r | |
565 | // checking if its coordinates are near the last touchstart's coordinates,\r | |
566 | // and if it's timestamp is within a certain threshold of the last touchend\r | |
567 | // event's timestamp. This is because when dealing with multi-touch events,\r | |
568 | // the emulated mousedown event (when it does fire) will fire with approximately\r | |
569 | // the same coordinates as the first touchstart, but within a short time after\r | |
570 | // the last touchend. We use 15px as the distance threshold, to be on the safe\r | |
571 | // side because the difference in coordinates can sometimes be up to 6px.\r | |
572 | Math.abs(e.pageX - self.lastTouchStartX) < 15 &&\r | |
573 | Math.abs(e.pageY - self.lastTouchStartY) < 15 &&\r | |
574 | \r | |
575 | // in the majority of cases, the emulated mousedown is observed within\r | |
576 | // 5ms of touchend, however, to be certain we avoid a situation where a\r | |
577 | // gesture handler gets executed twice we use a threshold of 1000ms. The\r | |
578 | // side effect of this is that if a user touches the screen and then quickly\r | |
579 | // clicks screen in the same spot, the mousedown/mouseup sequence that\r | |
580 | // ensues will not trigger any gesture recognizers.\r | |
581 | (Ext.now() - self.lastTouchEndTime) < 1000);\r | |
582 | },\r | |
583 | \r | |
584 | destroy: function() {\r | |
585 | var eventName;\r | |
586 | \r | |
587 | for (eventName in this.delegatedListeners) {\r | |
588 | this.removeDelegatedListener(eventName);\r | |
589 | }\r | |
590 | this.callParent();\r | |
591 | },\r | |
592 | \r | |
593 | /**\r | |
594 | * Resets the internal state of the Dom publisher. Internally the Dom publisher\r | |
595 | * keeps track of timing and coordinates of events for eliminating browser duplicates\r | |
596 | * (e.g. emulated mousedown after pointerdown etc.). This method resets all this\r | |
597 | * cached data to a state similar to when the publisher was first instantiated.\r | |
598 | *\r | |
599 | * Applications will not typically need to use this method, but it is useful for\r | |
600 | * Unit-testing situations where a clean slate is required for each test.\r | |
601 | */\r | |
602 | reset: function() {\r | |
603 | // use full class name, not me.self, so that Dom and Gesture publishers will\r | |
604 | // both reset flags on the same object.\r | |
605 | var self = Ext.event.publisher.Dom;\r | |
606 | \r | |
607 | // set to undefined, not null, because that is the initial state of these vars and\r | |
608 | // undefined/null return different results when used in math operations\r | |
609 | // (see isEventBlocked)\r | |
610 | self.lastScreenPointerEventTime = self.lastTouchEndTime = self.lastTouchStartX =\r | |
611 | self.lastTouchStartY = undefined;\r | |
612 | }\r | |
613 | }, function(Dom) {\r | |
614 | var doc = document,\r | |
615 | defaultView = doc.defaultView,\r | |
616 | prototype = Dom.prototype;\r | |
617 | \r | |
618 | if ((Ext.os.is.iOS && Ext.os.version.getMajor() < 5) || Ext.browser.is.AndroidStock ||\r | |
619 | !(defaultView && defaultView.addEventListener)) {\r | |
620 | // Delegated listeners will get attached to the document object because\r | |
621 | // attaching to the window object will not work. In IE8 this is needed because\r | |
622 | // events do not bubble up to the window - bubbling stops at the document\r | |
623 | // object. The iOS < 5 check was carried forward from Sencha Touch 2.3 -\r | |
624 | // Not sure why it was needed. The check for (defaultView && defaultView.addEventListener)\r | |
625 | // was carried forward as well - it may be required for older mobile browsers.\r | |
626 | // see also TOUCH-5408\r | |
627 | prototype.target = doc;\r | |
628 | } else {\r | |
629 | /**\r | |
630 | * @property {Object} target the DOM target to which listeners are attached for\r | |
631 | * delegated events.\r | |
632 | * @private\r | |
633 | */\r | |
634 | prototype.target = defaultView;\r | |
635 | }\r | |
636 | \r | |
637 | Dom.instance = new Dom();\r | |
638 | });\r |