    /** * @private */Ext.define('Ext.event.publisher.Gesture', {    extend: 'Ext.event.publisher.Dom',     requires: [        'Ext.util.Point',        'Ext.AnimationQueue'    ],     uses: 'Ext.event.gesture.*',     type: 'gesture',     isCancelEvent: {        touchcancel: 1,        pointercancel: 1,        MSPointerCancel: 1    },     handledEvents: [],    handledDomEvents: [],     constructor: function(config) {        var me = this,            handledDomEvents = me.handledDomEvents,            supports = Ext.supports,            supportsTouchEvents = supports.TouchEvents,            onTouchStart = me.onTouchStart,            onTouchMove = me.onTouchMove,            onTouchEnd = me.onTouchEnd;         // set up handlers that do not use requestAnimationFrame for when the useAnimationFrame         // config is set to false         me.handlers = {            touchstart: onTouchStart,            touchmove: onTouchMove,            touchend: onTouchEnd,            touchcancel: onTouchEnd,            pointerdown: onTouchStart,            pointermove: onTouchMove,            pointerup: onTouchEnd,            pointercancel: onTouchEnd,            MSPointerDown: onTouchStart,            MSPointerMove: onTouchMove,            MSPointerUp: onTouchEnd,            MSPointerCancel: onTouchEnd,            mousedown: onTouchStart,            mousemove: onTouchMove,            mouseup: onTouchEnd        };         me.activeTouchesMap = {};        me.activeTouches = [];        me.changedTouches = [];        me.recognizers = [];        me.eventToRecognizer = {};        me.cancelEvents = [];         if (supportsTouchEvents) {            // bind handlers that are only invoked when the browser has touchevents             me.onTargetTouchMove = me.onTargetTouchMove.bind(me);            me.onTargetTouchEnd = me.onTargetTouchEnd.bind(me);        }         if (supports.PointerEvents) {            handledDomEvents.push('pointerdown', 'pointermove', 'pointerup', 'pointercancel');            me.mousePointerType = 'mouse';        } else if (supports.MSPointerEvents) {            // IE10 uses vendor prefixed pointer events, IE11+ use unprefixed names.             handledDomEvents.push('MSPointerDown', 'MSPointerMove', 'MSPointerUp', 'MSPointerCancel');            me.mousePointerType = 4;        } else if (supportsTouchEvents) {            handledDomEvents.push('touchstart', 'touchmove', 'touchend', 'touchcancel');        }         if (!handledDomEvents.length || (supportsTouchEvents && Ext.isWebKit && Ext.os.is.Desktop)) {            // If the browser doesn't have pointer events or touch events we use mouse events             // to trigger gestures.  The exception to this rule is touch enabled webkit             // browsers such as chrome on Windows 8.  These browsers accept both touch and             // mouse input, so we need to listen for both touch events and mouse events.             handledDomEvents.push('mousedown', 'mousemove', 'mouseup');        }         me.initConfig(config);         return me.callParent();    },     onReady: function() {        this.callParent();         Ext.Array.sort(this.recognizers, function(recognizerA, recognizerB) {            var a = recognizerA.priority,                b = recognizerB.priority;             return (a > b) ? 1 : (a < b) ? -1 : 0;        });    },     registerRecognizer: function(recognizer) {        var me = this,            handledEvents = recognizer.handledEvents,            ln = handledEvents.length,            eventName, i;         // The recognizer will call our onRecognized method when it determines that a         // gesture has occurred.         recognizer.setOnRecognized(me.onRecognized);        recognizer.setCallbackScope(me);         // the gesture publishers handledEvents array is derived from the handledEvents         // of all of its recognizers         for (i = 0; i < ln; i++) {            eventName = handledEvents[i];            me.handledEvents.push(eventName);            me.eventToRecognizer[eventName] = recognizer;        }         me.registerEvents(handledEvents);         me.recognizers.push(recognizer);    },     onRecognized: function(recognizer, eventName, e, info, isCancel) {        var me = this,            touches = e.touches,            changedTouches = e.changedTouches,            ln = changedTouches.length,            events = me.events,            queueWasEmpty = !events.length,            cancelEvents = me.cancelEvents,            targetGroups, targets, i, touch;         info = info || {};         // At this point "e" still refers to the originally dispatched Ext.event.Event that         // wraps a native browser event such as "touchend", or "mousemove".  We need to         // dispatch with an an event object that has the correct "recognized" type such         // as "tap", or "drag".  We don't want to change the type of the original event         // object because it may be used asynchronously by event handlers, so we create a         // new object that is chained to the original event.         info.type = eventName;        // Touch events have a handy feature - the original target of a touchstart is         // always the target of successive touchmove/touchend events event if the touch         // is dragged off of the original target.  Pointer events also have this behavior         // via the setPointerCapture method, unless their target is removed from the dom         // mid-gesture, however, we do not currently use setPointerCapture because it         // can change the target of translated mouse events.  Mouse events do not have this         // "capturing" feature at all - the target is always the element that was under         // the mouse at the time the event occurred.  To be safe, and to ensure consistency,         // we just always set the target of recognized events to be the original target         // that was cached when the first "start" event was received.         info.target = changedTouches[0].target;         // reset stopped and claimed just in case the event that we are wrapping had         // stoppedPropagation or claimGesture called         info.stopped = false;        info.claimed = false;        info.isGesture = true;         e = e.chain(info);         if (!me.gestureTargets) {            if (ln > 1) {                targetGroups = [];                for (i = 0; i < ln; i++) {                    touch = changedTouches[i];                    targetGroups.push(touch.targets);                }                 targets = me.getCommonTargets(targetGroups);            } else {                targets = changedTouches[0].targets;            }             // Cache targets so that they only have to be computed once if multiple             // gestures are currently being recognized.             me.gestureTargets = targets;        }         if (isCancel && recognizer.isSingleTouch && (touches.length > 1)) {            // single touch recognizer cancelled by the start of a second touch.             // push into a separate queue which does not use the targets common to all             // touches (this.gestureTargets) as the targets for publishing but rather             // only uses the targets for the initial touch.             e.target = touches[0].target;            cancelEvents.push(e);        } else {            events.push(e);        }         if (queueWasEmpty) {            // if there were no events in the queue previously, it means the dom event             // has already been published, which means a recognizer must have recognized             // a gesture asynchronously (e.g. singletap fires on a timer)             // if this is the case we must publish now, otherwise we wait for the dom             // event handler to publish after it is finished invoking the recognizers             me.publishGestures();        }    },     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.recognizers,            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;            }        }    },     /**     * When a gesture has been claimed this method is invoked to remove gesture events of     * other kinds.  See implementation in Gesture publisher.     * @param {Ext.event.Event[]}events     * @param {String} claimedEvent      * @return {Number} The new index of the claimed event     * @private     */    filterClaimed: function(events, claimedEvent) {        var me = this,            eventToRecognizer = me.eventToRecognizer,            claimedEventType = claimedEvent.type,            claimedRecognizer = eventToRecognizer[claimedEventType],            claimedEventIndex, recognizer, type, i;         for (i = events.length; i--;) {            type = events[i].type;             if (type === claimedEventType) {                claimedEventIndex = i;            } else {                recognizer = eventToRecognizer[type];                // if there is no claimed recognizer it means the user must have invoked                 // claimGesture on a dom event (touchstart, touchmove etc).  If this is the                 // case we need to cease firing all gesture events, otherwise we allow only                 // the "claimed" recognizer to continue to dispatch events.                 if (!claimedRecognizer || (recognizer && (recognizer !== claimedRecognizer))) {                    events.splice(i, 1);                     if (claimedEventIndex) {                        claimedEventIndex--;                    }                }            }        }         me.claimRecognizer(claimedRecognizer, events[0]);         return claimedEventIndex;    },     /**     * Deactivates all recognizers other than the "claimed" recognizer     * @param {Ext.event.gesture.Recognizer} claimedRecognizer     * @param {Ext.event.Event} e     * @private     */    claimRecognizer: function(claimedRecognizer, e) {        var me = this,            recognizers = me.recognizers,            i, ln, recognizer;         for (i = 0, ln = recognizers.length; i < ln; i++) {            recognizer = recognizers[i];             // cancel recognition for all recognizers other than the one that was claimed             if (recognizer !== claimedRecognizer) {                recognizer.isActive = false;                recognizer.cancel(e);            }        }         if (me.events.length) {            // if any recognizers added cancelation events...             me.publishGestures(true);        }    },     publishGestures: function(claimed) {        var me = this,            cancelEvents = me.cancelEvents,            events = me.events,            gestureTargets = me.gestureTargets;         if (cancelEvents.length) {            me.cancelEvents = [];            // Since cancellation events cannot be claimed we pass true here which             // prevents them from being claimed.             me.publish(cancelEvents, me.getPropagatingTargets(cancelEvents[0].target), true);        }         if (events.length) {            // It is important to reset the events property to an empty array before             // publishing since since events may be added to the array during publishing.             // This can happen if an event is claimed, thus triggering "cancel" gesture events.             me.events = [];            me.gestureTargets = null;             me.publish(events, gestureTargets || me.getPropagatingTargets(events[0].target), claimed);        }    },     updateTouches: function(e, isEnd) {        var me = this,            browserEvent = e.browserEvent,            // the touchSource is the object from which we get data about the changed touch             // point or points related to an event object, such as identifier, target, and             // coordinates. For touch event the source is changedTouches, for mouse and             // pointer events it is the event object itself.             touchSources = browserEvent.changedTouches || [browserEvent],            activeTouches = me.activeTouches,            activeTouchesMap = me.activeTouchesMap,            changedTouches = [],            touchSource, identifier, touch, target, i, ln, x, y;         for (i = 0, ln = touchSources.length; i < ln; i++) {            touchSource = touchSources[i];             if ('identifier' in touchSource) {                // touch events have an identifier property on their touches objects.                 // It can be 0, hence the "in" check                 identifier = touchSource.identifier;            } else if ('pointerId' in touchSource) {                // Pointer events have a pointerId on the event object itself                 identifier = touchSource.pointerId;            } else {                // Mouse events don't have an identifier, so we always use 1 since there                 // can only be one mouse touch point active at a time                 identifier = 1;            }             touch = activeTouchesMap[identifier];             if (!touch) {                target = Ext.event.Event.resolveTextNode(touchSource.target);                touch = activeTouchesMap[identifier] = {                    identifier: identifier,                    target: target,                    // There are 2 main advantages to caching the targets here, vs.                     // waiting until onRecognized                     // 1. for "move" events we don't have to construct the targets array                     // for every event - a theoretical performance win                     // 2. if the target is removed from the dom mid-gesture we still                     // want any gestures listeners on elements that were above the                     // target to complete.  This means the propagating targets must reflect                     // the target element's initial hierarchy when the gesture began                     targets: me.getPropagatingTargets(target)                };                activeTouches.push(touch);            }             if (isEnd) {                delete activeTouchesMap[identifier];                Ext.Array.remove(activeTouches, touch);            }             x = touchSource.pageX;            y = touchSource.pageY;             touch.pageX = x;            touch.pageY = y;            // recognizers frequently use Point methods, so go ahead and create a Point.             touch.point = new Ext.util.Point(x, y);            changedTouches.push(touch);        }         // decorate the event object with the touch point info so that it can be used from         // within gesture recognizers (clone touches, just in case event object is used         // asynchronously since this.activeTouches is continuously modified)         e.touches = Ext.Array.clone(activeTouches);        // no need to clone changedTouches since we just created it from scratch         e.changedTouches = changedTouches;    },     publishDelegatedDomEvent: function(e) {        var me = this;         if (!e.button || e.button < 1) {            // mouse gestures (and pointer gestures triggered by a mouse) can only be             // initiated using the left button (0).  button value < 0 is also acceptable             // (e.g. pointermove has a button value of -1)              // Track the event on the instance so it can be fired after gesture recognition             // completes (if any gestures are recognized they will be added to this array)             me.events = [e];             me.handlers[e.type].call(me, e);        } else {            // mouse events *with* button still need to be published.             me.callParent([e]);        }    },     onTouchStart: function(e) {        var me = this,            target = e.target,            touches = e.browserEvent.touches;         if (e.browserEvent.type === 'touchstart') {            // When using touch events, if the target is removed from the dom mid-gesture             // the touchend event cannot be handled normally because it will not bubble             // to the top of the dom since the target el is no longer attached to the dom.             // Add some special handlers to clean everything up. (see onTargetTouchEnd)             // use addEventListener directly so that we don't have to spin up an instance             // of Ext.Element for every event target.             target.addEventListener('touchmove', me.onTargetTouchMove);            target.addEventListener('touchend', me.onTargetTouchEnd);            target.addEventListener('touchcancel', me.onTargetTouchEnd);        }         // There is a bug in IOS8 where touchstart, but not touchend event is         // fired when clicking on controls for audio/video, which can leave         // us in a bad state here.         if (touches && touches.length <= me.activeTouches.length) {            me.removeGhostTouches(touches);        }         me.updateTouches(e);         if (!me.isStarted) {            // Disable garbage collection during gestures so that if the target element             // of a gesture is removed from the dom, it does not get garbage collected             // until the gesture is complete             if (Ext.enableGarbageCollector) {                Ext.dom.GarbageCollector.pause();            }             // this is the first active touch - invoke "onStart" which indicates the             // beginning of a gesture             me.isStarted = true;            me.invokeRecognizers('onStart', e);        }         me.invokeRecognizers('onTouchStart', e);         me.publishGestures();    },     onTouchMove: function(e) {        var me = this,            mousePointerType = me.mousePointerType;         if (me.isStarted) {            // In IE10/11, the corresponding pointerup event is not fired after the pointerdown after             // the mouse is released from the scrollbar. However, it does fire a pointermove event with buttons: 0, so             // we capture that here and ensure the touch end process is completed.             if (mousePointerType && e.browserEvent.pointerType === mousePointerType && e.buttons === 0) {                e.type = Ext.dom.Element.prototype.eventMap.touchend;                e.button = 0;                me.onTouchEnd(e);                return;            }             me.updateTouches(e);             if (e.changedTouches.length > 0) {                me.invokeRecognizers('onTouchMove', e);            }        }         me.publishGestures();    },     // This method serves as the handler for both "end" and "cancel" events.  This is     // because they are handled identically with the exception of the recognizer method     // that is called.     onTouchEnd: function(e) {        var me = this,            touchCount;         if (!me.isStarted) {            me.publishGestures();            return;        }         me.updateTouches(e, true);         touchCount = me.activeTouches.length;         // If an exception is thrown in any of the recognizers, we still need to run         // the cleanup. Otherwise the gesture might get "stuck" and *every* pointer event         // after that will fire the same handlers over and over, potentially spewing         // the same exceptions endlessly. See https://sencha.jira.com/browse/EXTJS-15674.         // We don't want to mask the original exception though, let it propagate.         try {            me.invokeRecognizers(me.isCancelEvent[e.type] ? 'onTouchCancel' : 'onTouchEnd', e);        }        finally {            if (!touchCount) {                // no more active touches - invoke onEnd to indicate the end of the gesture                 me.isStarted = false;                me.invokeRecognizers('onEnd', e);            }             me.publishGestures();             if (!touchCount) {                // Gesture is finished, safe to resume garbage collection so that any target                 // elements destroyed while gesture was in progress can be collected                 if (Ext.enableGarbageCollector) {                    Ext.dom.GarbageCollector.resume();                }            }        }    },     onTargetTouchMove: function(e) {        if (Ext.elevateFunction) {            // using [e] is faster than using arguments in most browsers             // http://jsperf.com/passing-arguments             Ext.elevateFunction(this.doTargetTouchMove, this, [e]);        } else {            this.doTargetTouchMove(e);        }    },     doTargetTouchMove: function(e) {        // handle touchmove if the target el was removed from dom mid-gesture.         // see onTouchStart/onTargetTouchEnd for further explanation         if (!Ext.getBody().contains(e.target)) {            this.onTouchMove(new Ext.event.Event(e));        }    },     onTargetTouchEnd: function(e) {        if (Ext.elevateFunction) {            // using [e] is faster than using arguments in most browsers             // http://jsperf.com/passing-arguments             Ext.elevateFunction(this.doTargetTouchEnd, this, [e]);        } else {            this.doTargetTouchEnd(e);        }    },     doTargetTouchEnd: function(e) {        var me = this,            target = e.target;         target.removeEventListener('touchmove', me.onTargetTouchMove);        target.removeEventListener('touchend', me.onTargetTouchEnd);        target.removeEventListener('touchcancel', me.onTargetTouchEnd);         // if the target el was removed from the dom mid-gesture, then the touchend event,         // when it occurs, will not be handled because it will not bubble to the top of         // the dom. This is because the "target" of the touchend is the removed element.         // If this is the case, go ahead and trigger touchend handling now.         // Detect whether the target was removed from the DOM mid gesture by using Element.contains.         // Originally we attempted to detect this by listening for the DOMNodeRemovedFromDocument         // event, and setting a flag on the element when it was removed, however that         // approach only works when the element is removed using removedChild, and fails         // if the element is removed because some ancestor had innerHTML assigned.         // note: this handling is applicable for actual touchend events, pointer and mouse         // events will fire on whatever element is under the cursor/pointer after the         // original target has been removed.         if (!Ext.getBody().contains(target)) {            me.onTouchEnd(new Ext.event.Event(e));        }    },     /**     * Resets the internal state of the Gesture publisher and all of its recognizers.     * 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.     *     * Calling this method will also reset the state of Ext.event.publisher.Dom     */    reset: function() {        var me = this,            recognizers = me.recognizers,            ln = recognizers.length,            i, recognizer;         me.activeTouchesMap = {};        me.activeTouches = [];        me.changedTouches = [];        me.isStarted = false;        me.gestureTargets = null;        me.events = [];        me.cancelEvents = [];         for (i = 0; i < ln; i++) {            recognizer = recognizers[i];            recognizer.reset();            recognizer.isActive = false;        }         this.callParent();    },     privates: {        removeGhostTouches: function(touches) {            var ids = {},                 len = touches.length,                activeTouches = this.activeTouches,                map = this.activeTouchesMap,                i, id, touch;             // Collect the actual touches             for (i = 0; i < len; ++i) {                ids[touches[i].identifier] = true;            }             i = activeTouches.length;            while (i--) {                touch = activeTouches[i];                id = touch.identifier;                if (!touches[id]) {                    Ext.Array.remove(activeTouches, touch);                    delete map[id];                }            }        }    }}, function(Gesture) {    Gesture.instance = Ext.$gesturePublisher = new Gesture();});