]> git.proxmox.com Git - sencha-touch.git/blob - src/src/event/publisher/TouchGesture.js
import Sencha Touch 2.4.2 source
[sencha-touch.git] / src / src / event / publisher / TouchGesture.js
1 /**
2 * @private
3 */
4 Ext.define('Ext.event.publisher.TouchGesture', {
5
6 extend: 'Ext.event.publisher.Dom',
7
8 requires: [
9 'Ext.util.Point',
10 'Ext.event.Touch',
11 'Ext.AnimationQueue'
12 ],
13
14 isNotPreventable: /^(select|a)$/i,
15
16 handledEvents: ['touchstart', 'touchmove', 'touchend', 'touchcancel'],
17
18 mouseToTouchMap: {
19 mousedown: 'touchstart',
20 mousemove: 'touchmove',
21 mouseup: 'touchend'
22 },
23
24 lastEventType: null,
25
26 config: {
27 moveThrottle: 0,
28 recognizers: {}
29 },
30
31 constructor: function(config) {
32 var me = this;
33
34 this.eventProcessors = {
35 touchstart: this.onTouchStart,
36 touchmove: this.onTouchMove,
37 touchend: this.onTouchEnd,
38 touchcancel: this.onTouchEnd
39 };
40
41 this.eventToRecognizerMap = {};
42
43 this.activeRecognizers = [];
44
45 this.touchesMap = {};
46
47 this.currentIdentifiers = [];
48
49 if (Ext.browser.is.Chrome && Ext.os.is.Android) {
50 this.screenPositionRatio = Ext.browser.version.gt('18') ? 1 : 1 / window.devicePixelRatio;
51 }
52 else if (Ext.browser.is.AndroidStock4) {
53 this.screenPositionRatio = 1;
54 }
55 else if (Ext.os.is.BlackBerry) {
56 this.screenPositionRatio = 1 / window.devicePixelRatio;
57 }
58 else if (Ext.browser.engineName == 'WebKit' && Ext.os.is.Desktop) {
59 this.screenPositionRatio = 1;
60 }
61 else {
62 this.screenPositionRatio = window.innerWidth / window.screen.width;
63 }
64 this.initConfig(config);
65
66 if (Ext.feature.has.Touch) {
67 // bind handlers that are only invoked when the browser has touchevents
68 me.onTargetTouchMove = me.onTargetTouchMove.bind(me);
69 me.onTargetTouchEnd = me.onTargetTouchEnd.bind(me);
70 }
71
72 return this.callSuper();
73 },
74
75 applyRecognizers: function(recognizers) {
76 var i, recognizer;
77
78 for (i in recognizers) {
79 if (recognizers.hasOwnProperty(i)) {
80 recognizer = recognizers[i];
81
82 if (recognizer) {
83 this.registerRecognizer(recognizer);
84 }
85 }
86 }
87
88 return recognizers;
89 },
90
91 handles: function(eventName) {
92 return this.callSuper(arguments) || this.eventToRecognizerMap.hasOwnProperty(eventName);
93 },
94
95 doesEventBubble: function() {
96 // All touch events bubble
97 return true;
98 },
99 onEvent: function(e) {
100 var type = e.type,
101 lastEventType = this.lastEventType,
102 touchList = [e];
103
104 if (this.eventProcessors[type]) {
105 this.eventProcessors[type].call(this, e);
106 return;
107 }
108
109 if ('button' in e && e.button > 0) {
110 return;
111 }
112 else {
113 // Temporary fix for a recent Chrome bugs where events don't seem to bubble up to document
114 // when the element is being animated with webkit-transition (2 mousedowns without any mouseup)
115 if (type === 'mousedown' && lastEventType && lastEventType !== 'mouseup') {
116 var fixedEvent = document.createEvent("MouseEvent");
117 fixedEvent.initMouseEvent('mouseup', e.bubbles, e.cancelable,
118 document.defaultView, e.detail, e.screenX, e.screenY, e.clientX,
119 e.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, e.metaKey,
120 e.button, e.relatedTarget);
121
122 this.onEvent(fixedEvent);
123 }
124
125 if (type !== 'mousemove') {
126 this.lastEventType = type;
127 }
128
129 e.identifier = 1;
130 e.touches = (type !== 'mouseup') ? touchList : [];
131 e.targetTouches = (type !== 'mouseup') ? touchList : [];
132 e.changedTouches = touchList;
133
134 this.eventProcessors[this.mouseToTouchMap[type]].call(this, e);
135 }
136 },
137
138 registerRecognizer: function(recognizer) {
139 var map = this.eventToRecognizerMap,
140 activeRecognizers = this.activeRecognizers,
141 handledEvents = recognizer.getHandledEvents(),
142 i, ln, eventName;
143
144 recognizer.setOnRecognized(this.onRecognized);
145 recognizer.setCallbackScope(this);
146
147 for (i = 0,ln = handledEvents.length; i < ln; i++) {
148 eventName = handledEvents[i];
149
150 map[eventName] = recognizer;
151 }
152
153 activeRecognizers.push(recognizer);
154
155 return this;
156 },
157
158 onRecognized: function(eventName, e, touches, info) {
159 var targetGroups = [],
160 ln = touches.length,
161 targets, i, touch;
162
163 if (ln === 1) {
164 return this.publish(eventName, touches[0].targets, e, info);
165 }
166
167 for (i = 0; i < ln; i++) {
168 touch = touches[i];
169 targetGroups.push(touch.targets);
170 }
171
172 targets = this.getCommonTargets(targetGroups);
173
174 this.publish(eventName, targets, e, info);
175 },
176
177 publish: function(eventName, targets, event, info) {
178 event.set(info);
179 return this.callSuper([eventName, targets, event]);
180 },
181
182 getCommonTargets: function(targetGroups) {
183 var firstTargetGroup = targetGroups[0],
184 ln = targetGroups.length;
185
186 if (ln === 1) {
187 return firstTargetGroup;
188 }
189
190 var commonTargets = [],
191 i = 1,
192 target, targets, j;
193
194 while (true) {
195 target = firstTargetGroup[firstTargetGroup.length - i];
196
197 if (!target) {
198 return commonTargets;
199 }
200
201 for (j = 1; j < ln; j++) {
202 targets = targetGroups[j];
203
204 if (targets[targets.length - i] !== target) {
205 return commonTargets;
206 }
207 }
208
209 commonTargets.unshift(target);
210 i++;
211 }
212
213 return commonTargets;
214 },
215
216 invokeRecognizers: function(methodName, e) {
217 var recognizers = this.activeRecognizers,
218 ln = recognizers.length,
219 i, recognizer;
220
221 if (methodName === 'onStart') {
222 for (i = 0; i < ln; i++) {
223 recognizers[i].isActive = true;
224 }
225 }
226
227 for (i = 0; i < ln; i++) {
228 recognizer = recognizers[i];
229 if (recognizer.isActive && recognizer[methodName].call(recognizer, e) === false) {
230 recognizer.isActive = false;
231 }
232 }
233 },
234
235 getActiveRecognizers: function() {
236 return this.activeRecognizers;
237 },
238
239 updateTouch: function(touch) {
240 var identifier = touch.identifier,
241 currentTouch = this.touchesMap[identifier],
242 target, x, y;
243
244 if (!currentTouch) {
245 target = this.getElementTarget(touch.target);
246
247 this.touchesMap[identifier] = currentTouch = {
248 identifier: identifier,
249 target: target,
250 targets: this.getBubblingTargets(target)
251 };
252
253 this.currentIdentifiers.push(identifier);
254 }
255
256 x = touch.pageX;
257 y = touch.pageY;
258
259 if (x === currentTouch.pageX && y === currentTouch.pageY) {
260 return false;
261 }
262
263 currentTouch.pageX = x;
264 currentTouch.pageY = y;
265 currentTouch.timeStamp = touch.timeStamp;
266 currentTouch.point = new Ext.util.Point(x, y);
267
268 return currentTouch;
269 },
270
271 updateTouches: function(touches) {
272 var i, ln, touch,
273 changedTouches = [];
274
275 for (i = 0, ln = touches.length; i < ln; i++) {
276 touch = this.updateTouch(touches[i]);
277 if (touch) {
278 changedTouches.push(touch);
279 }
280 }
281
282 return changedTouches;
283 },
284
285 syncTouches: function (touches) {
286 var touchIDs = [], len = touches.length,
287 i, id, touch, ghostTouches;
288
289 // Collect the actual touch IDs that exist
290 for (i = 0; i < len; i++) {
291 touch = touches[i];
292 touchIDs.push(touch.identifier);
293 }
294
295 // Compare actual IDs to cached IDs
296 // Remove any that are not real anymore
297 ghostTouches = Ext.Array.difference(this.currentIdentifiers, touchIDs);
298 len = ghostTouches.length;
299
300 for (i = 0; i < len; i++) {
301 id = ghostTouches[i];
302 Ext.Array.remove(this.currentIdentifiers, id);
303 delete this.touchesMap[id];
304 }
305 },
306
307 factoryEvent: function(e) {
308 return new Ext.event.Touch(e, null, this.touchesMap, this.currentIdentifiers);
309 },
310
311 onTouchStart: function(e) {
312 var changedTouches = e.changedTouches,
313 target = e.target,
314 touches = e.touches,
315 ln = changedTouches.length,
316 isNotPreventable = this.isNotPreventable,
317 isTouch = (e.type === 'touchstart'),
318 me = this,
319 i, touch, parent;
320
321 // Potentially sync issue from various reasons.
322 // example: ios8 does not dispatch touchend on audio element play/pause tap.
323 if (touches && touches.length < this.currentIdentifiers.length + 1) {
324 this.syncTouches(touches);
325 }
326
327 this.updateTouches(changedTouches);
328
329 e = this.factoryEvent(e);
330 changedTouches = e.changedTouches;
331
332 // TOUCH-3934
333 // Android event system will not dispatch touchend for any multitouch
334 // event that has not been preventDefaulted.
335 if(Ext.browser.is.AndroidStock && this.currentIdentifiers.length >= 2) {
336 e.preventDefault();
337 }
338
339 // If targets are destroyed while touches are active on them
340 // we need these listeners to sync up our internal TouchesMap
341 if (isTouch) {
342 target.addEventListener('touchmove', me.onTargetTouchMove);
343 target.addEventListener('touchend', me.onTargetTouchEnd);
344 target.addEventListener('touchcancel', me.onTargetTouchEnd);
345 }
346
347 for (i = 0; i < ln; i++) {
348 touch = changedTouches[i];
349 this.publish('touchstart', touch.targets, e, {touch: touch});
350 }
351
352 if (!this.isStarted) {
353 this.isStarted = true;
354 this.invokeRecognizers('onStart', e);
355 }
356
357 this.invokeRecognizers('onTouchStart', e);
358
359 parent = target.parentNode || {};
360 },
361
362 onTouchMove: function(e) {
363 if (!this.isStarted) {
364 return;
365 }
366
367 if (!this.animationQueued) {
368 this.animationQueued = true;
369 Ext.AnimationQueue.start('onAnimationFrame', this);
370 }
371
372 this.lastMoveEvent = e;
373 },
374
375 onAnimationFrame: function() {
376 var event = this.lastMoveEvent;
377
378 if (event) {
379 this.lastMoveEvent = null;
380 this.doTouchMove(event);
381 }
382 },
383
384 doTouchMove: function(e) {
385 var changedTouches, i, ln, touch;
386
387 changedTouches = this.updateTouches(e.changedTouches);
388
389 ln = changedTouches.length;
390
391 e = this.factoryEvent(e);
392
393 for (i = 0; i < ln; i++) {
394 touch = changedTouches[i];
395 this.publish('touchmove', touch.targets, e, {touch: touch});
396 }
397
398 if (ln > 0) {
399 this.invokeRecognizers('onTouchMove', e);
400 }
401 },
402
403 onTouchEnd: function(e) {
404 if (!this.isStarted) {
405 return;
406 }
407
408 if (this.lastMoveEvent) {
409 this.onAnimationFrame();
410 }
411
412 var touchesMap = this.touchesMap,
413 currentIdentifiers = this.currentIdentifiers,
414 changedTouches = e.changedTouches,
415 ln = changedTouches.length,
416 identifier, i, touch;
417
418 this.updateTouches(changedTouches);
419
420 changedTouches = e.changedTouches;
421
422 for (i = 0; i < ln; i++) {
423 Ext.Array.remove(currentIdentifiers, changedTouches[i].identifier);
424 }
425
426 e = this.factoryEvent(e);
427
428 for (i = 0; i < ln; i++) {
429 identifier = changedTouches[i].identifier;
430 touch = touchesMap[identifier];
431 delete touchesMap[identifier];
432 this.publish('touchend', touch.targets, e, {touch: touch});
433 }
434
435 this.invokeRecognizers('onTouchEnd', e);
436
437 // This previously was set to e.touches.length === 1 to catch errors in syncing
438 // this has since been addressed to keep proper sync and now this is a catch for
439 // a sync error in touches to reset our internal maps
440 if (e.touches && e.touches.length === 0 && currentIdentifiers.length) {
441 currentIdentifiers.length = 0;
442 this.touchesMap = {};
443 }
444
445 if (currentIdentifiers.length === 0) {
446 this.isStarted = false;
447 this.invokeRecognizers('onEnd', e);
448 if (this.animationQueued) {
449 this.animationQueued = false;
450 Ext.AnimationQueue.stop('onAnimationFrame', this);
451 }
452 }
453 },
454
455 onTargetTouchMove: function(e) {
456 if (!Ext.getBody().contains(e.target)) {
457 this.onTouchMove(e);
458 }
459 },
460
461 onTargetTouchEnd: function(e) {
462 var me = this,
463 target = e.target,
464 touchCount=0,
465 touchTarget;
466
467 // Determine how many active touches there are on this target
468 for (identifier in this.touchesMap) {
469 touchTarget = this.touchesMap[identifier].target;
470 if (touchTarget === target ) {
471 touchCount++;
472 }
473 }
474
475 // If this is the last active touch on the target remove the target listeners
476 if (touchCount <= 1) {
477 target.removeEventListener('touchmove', me.onTargetTouchMove);
478 target.removeEventListener('touchend', me.onTargetTouchEnd);
479 target.removeEventListener('touchcancel', me.onTargetTouchEnd);
480 }
481
482 if (!Ext.getBody().contains(target)) {
483 me.onTouchEnd(e);
484 }
485 }
486
487 }, function() {
488 if (Ext.feature.has.Pointer) {
489 this.override({
490 pointerToTouchMap: {
491 MSPointerDown: 'touchstart',
492 MSPointerMove: 'touchmove',
493 MSPointerUp: 'touchend',
494 MSPointerCancel: 'touchcancel',
495 pointerdown: 'touchstart',
496 pointermove: 'touchmove',
497 pointerup: 'touchend',
498 pointercancel: 'touchcancel'
499 },
500
501 touchToPointerMap: {
502 touchstart: 'MSPointerDown',
503 touchmove: 'MSPointerMove',
504 touchend: 'MSPointerUp',
505 touchcancel: 'MSPointerCancel'
506 },
507
508 attachListener: function(eventName, doc) {
509 eventName = this.touchToPointerMap[eventName];
510
511 if (!eventName) {
512 return;
513 }
514
515 return this.callOverridden([eventName, doc]);
516 },
517
518 onEvent: function(e) {
519 var type = e.type;
520 if (
521 this.currentIdentifiers.length === 0 &&
522 // This is for IE 10 and IE 11
523 (e.pointerType === e.MSPOINTER_TYPE_TOUCH || e.pointerType === "touch") &&
524 // This is for IE 10 and IE 11
525 (type === "MSPointerMove" || type === "pointermove")
526 ) {
527 type = "MSPointerDown";
528 }
529
530 if ('button' in e && e.button > 0) {
531 return;
532 }
533
534 type = this.pointerToTouchMap[type];
535 e.identifier = e.pointerId;
536 e.changedTouches = [e];
537
538 this.eventProcessors[type].call(this, e);
539 }
540 });
541 }
542 else if (!Ext.browser.is.Ripple && (Ext.os.is.ChromeOS || !Ext.feature.has.Touch)) {
543 this.override({
544 handledEvents: ['touchstart', 'touchmove', 'touchend', 'touchcancel', 'mousedown', 'mousemove', 'mouseup']
545 });
546 }
547 });