Ext.define('Ext.event.publisher.TouchGesture', { extend: 'Ext.event.publisher.Dom', requires: [ 'Ext.util.Point', 'Ext.event.Touch' ], handledEvents: ['touchstart', 'touchmove', 'touchend', 'touchcancel'], moveEventName: 'touchmove', config: { moveThrottle: 1, buffering: { enabled: false, interval: 10 }, recognizers: {} }, currentTouchesCount: 0, constructor: function(config) { this.processEvents = Ext.Function.bind(this.processEvents, this); this.eventProcessors = { touchstart: this.onTouchStart, touchmove: this.onTouchMove, touchend: this.onTouchEnd, touchcancel: this.onTouchEnd }; this.eventToRecognizerMap = {}; this.activeRecognizers = []; this.currentRecognizers = []; this.currentTargets = {}; this.currentTouches = {}; this.buffer = []; this.initConfig(config); return this.callParent(); }, applyBuffering: function(buffering) { if (buffering.enabled === true) { this.bufferTimer = setInterval(this.processEvents, buffering.interval); } else { clearInterval(this.bufferTimer); } return buffering; }, applyRecognizers: function(recognizers) { var i, recognizer; for (i in recognizers) { if (recognizers.hasOwnProperty(i)) { recognizer = recognizers[i]; this.registerRecognizer(recognizer); } } return recognizers; }, handles: function(eventName) { return this.callParent(arguments) || this.eventToRecognizerMap.hasOwnProperty(eventName); }, doesEventBubble: function() { // All touch events bubble return true; }, eventLogs: [], onEvent: function(e) { var buffering = this.getBuffering(); e = new Ext.event.Touch(e); if (buffering.enabled) { this.buffer.push(e); } else { this.processEvent(e); } }, processEvents: function() { var buffer = this.buffer, ln = buffer.length, moveEvents = [], events, event, i; if (ln > 0) { events = buffer.slice(0); buffer.length = 0; for (i = 0; i < ln; i++) { event = events[i]; if (event.type === this.moveEventName) { moveEvents.push(event); } else { if (moveEvents.length > 0) { this.processEvent(this.mergeEvents(moveEvents)); moveEvents.length = 0; } this.processEvent(event); } } if (moveEvents.length > 0) { this.processEvent(this.mergeEvents(moveEvents)); moveEvents.length = 0; } } }, mergeEvents: function(events) { var changedTouchesLists = [], ln = events.length, i, event, targetEvent; targetEvent = events[ln - 1]; if (ln === 1) { return targetEvent; } for (i = 0; i < ln; i++) { event = events[i]; changedTouchesLists.push(event.changedTouches); } targetEvent.changedTouches = this.mergeTouchLists(changedTouchesLists); return targetEvent; }, mergeTouchLists: function(touchLists) { var touches = {}, list = [], i, ln, touchList, j, subLn, touch, identifier; for (i = 0,ln = touchLists.length; i < ln; i++) { touchList = touchLists[i]; for (j = 0,subLn = touchList.length; j < subLn; j++) { touch = touchList[j]; identifier = touch.identifier; touches[identifier] = touch; } } for (identifier in touches) { if (touches.hasOwnProperty(identifier)) { list.push(touches[identifier]); } } return list; }, registerRecognizer: function(recognizer) { var map = this.eventToRecognizerMap, activeRecognizers = this.activeRecognizers, handledEvents = recognizer.getHandledEvents(), i, ln, eventName; recognizer.setOnRecognized(this.onRecognized); recognizer.setCallbackScope(this); for (i = 0,ln = handledEvents.length; i < ln; i++) { eventName = handledEvents[i]; map[eventName] = recognizer; } activeRecognizers.push(recognizer); return this; }, onRecognized: function(eventName, e, touches, info) { var targetGroups = [], ln = touches.length, targets, i, touch; if (ln === 1) { return this.publish(eventName, touches[0].targets, e, info); } for (i = 0; i < ln; i++) { touch = touches[i]; targetGroups.push(touch.targets); } targets = this.getCommonTargets(targetGroups); this.publish(eventName, targets, e, info); }, publish: function(eventName, targets, event, info) { event.set(info); return this.callParent([eventName, targets, event]); }, getCommonTargets: function(targetGroups) { var firstTargetGroup = targetGroups[0], ln = targetGroups.length; if (ln === 1) { return firstTargetGroup; } var commonTargets = [], i = 1, target, targets, j; while (true) { target = firstTargetGroup[firstTargetGroup.length - i]; if (!target) { return commonTargets; } for (j = 1; j < ln; j++) { targets = targetGroups[j]; if (targets[targets.length - i] !== target) { return commonTargets; } } commonTargets.unshift(target); i++; } return commonTargets; }, invokeRecognizers: function(methodName, e) { var recognizers = this.activeRecognizers, ln = recognizers.length, i, recognizer; if (methodName === 'onStart') { for (i = 0; i < ln; i++) { recognizers[i].isActive = true; } } for (i = 0; i < ln; i++) { recognizer = recognizers[i]; if (recognizer.isActive && recognizer[methodName].call(recognizer, e) === false) { recognizer.isActive = false; } } }, getActiveRecognizers: function() { return this.activeRecognizers; }, processEvent: function(e) { this.eventProcessors[e.type].call(this, e); }, onTouchStart: function(e) { var currentTargets = this.currentTargets, currentTouches = this.currentTouches, currentTouchesCount = this.currentTouchesCount, changedTouches = e.changedTouches, touches = e.touches, touchesLn = touches.length, currentIdentifiers = {}, ln = changedTouches.length, i, touch, identifier, fakeEndEvent; currentTouchesCount += ln; if (currentTouchesCount > touchesLn) { for (i = 0; i < touchesLn; i++) { touch = touches[i]; identifier = touch.identifier; currentIdentifiers[identifier] = true; } for (identifier in currentTouches) { if (currentTouches.hasOwnProperty(identifier)) { if (!currentIdentifiers[identifier]) { currentTouchesCount--; fakeEndEvent = e.clone(); touch = currentTouches[identifier]; touch.targets = this.getBubblingTargets(this.getElementTarget(touch.target)); fakeEndEvent.changedTouches = [touch]; this.onTouchEnd(fakeEndEvent); } } } // Fix for a bug found in Motorola Droid X (Gingerbread) and similar // where there are 2 touchstarts but just one touchend if (currentTouchesCount > touchesLn) { return; } } for (i = 0; i < ln; i++) { touch = changedTouches[i]; identifier = touch.identifier; if (!currentTouches.hasOwnProperty(identifier)) { this.currentTouchesCount++; } currentTouches[identifier] = touch; currentTargets[identifier] = this.getBubblingTargets(this.getElementTarget(touch.target)); } e.setTargets(currentTargets); for (i = 0; i < ln; i++) { touch = changedTouches[i]; this.publish('touchstart', touch.targets, e, {touch: touch}); } if (!this.isStarted) { this.isStarted = true; this.invokeRecognizers('onStart', e); } this.invokeRecognizers('onTouchStart', e); }, onTouchMove: function(e) { if (!this.isStarted) { return; } var currentTargets = this.currentTargets, currentTouches = this.currentTouches, moveThrottle = this.getMoveThrottle(), changedTouches = e.changedTouches, stillTouchesCount = 0, i, ln, touch, point, oldPoint, identifier; e.setTargets(currentTargets); for (i = 0,ln = changedTouches.length; i < ln; i++) { touch = changedTouches[i]; identifier = touch.identifier; point = touch.point; oldPoint = currentTouches[identifier].point; if (moveThrottle && point.isCloseTo(oldPoint, moveThrottle)) { stillTouchesCount++; continue; } currentTouches[identifier] = touch; this.publish('touchmove', touch.targets, e, {touch: touch}); } if (stillTouchesCount < ln) { this.invokeRecognizers('onTouchMove', e); } }, onTouchEnd: function(e) { if (!this.isStarted) { return; } var currentTargets = this.currentTargets, currentTouches = this.currentTouches, changedTouches = e.changedTouches, ln = changedTouches.length, isEnded, identifier, i, touch; e.setTargets(currentTargets); this.currentTouchesCount -= ln; isEnded = (this.currentTouchesCount === 0); if (isEnded) { this.isStarted = false; } for (i = 0; i < ln; i++) { touch = changedTouches[i]; identifier = touch.identifier; delete currentTouches[identifier]; delete currentTargets[identifier]; this.publish('touchend', touch.targets, e, {touch: touch}); } this.invokeRecognizers('onTouchEnd', e); if (isEnded) { this.invokeRecognizers('onEnd', e); } } }, function() { if (!Ext.feature.has.Touch) { this.override({ moveEventName: 'mousemove', map: { mouseToTouch: { mousedown: 'touchstart', mousemove: 'touchmove', mouseup: 'touchend' }, touchToMouse: { touchstart: 'mousedown', touchmove: 'mousemove', touchend: 'mouseup' } }, attachListener: function(eventName) { eventName = this.map.touchToMouse[eventName]; if (!eventName) { return; } return this.callOverridden([eventName]); }, lastEventType: null, onEvent: function(e) { if ('button' in e && e.button !== 0) { return; } var type = e.type, touchList = [e]; // Temporary fix for a recent Chrome bugs where events don't seem to bubble up to document // when the element is being animated // with webkit-transition (2 mousedowns without any mouseup) if (type === 'mousedown' && this.lastEventType && this.lastEventType !== 'mouseup') { var fixedEvent = document.createEvent("MouseEvent"); fixedEvent.initMouseEvent('mouseup', e.bubbles, e.cancelable, document.defaultView, e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, e.metaKey, e.button, e.relatedTarget); this.onEvent(fixedEvent); } if (type !== 'mousemove') { this.lastEventType = type; } e.identifier = 1; e.touches = (type !== 'mouseup') ? touchList : []; e.targetTouches = (type !== 'mouseup') ? touchList : []; e.changedTouches = touchList; return this.callOverridden([e]); }, processEvent: function(e) { this.eventProcessors[this.map.mouseToTouch[e.type]].call(this, e); } }); } });