/**
 * @class Ext.scroll.TouchScroller
 * @private
 * Momentum scrolling is one of the most important parts of the user experience on touch-screen
 * devices.  Depending on the device and browser, Ext JS will select one of several different
 * scrolling implementations for best performance.
 *
 * Scroller settings can be changed using the {@link Ext.Container#scrollable scrollable}
 * configuration on {@link Ext.Component}. Here is a simple example of how to adjust the
 * scroller settings when using a Component (or anything that extends it).
 *
 *     @example
 *     Ext.create('Ext.Component', {
 *         renderTo: Ext.getBody(),
 *         height: 100,
 *         width: 100,
 *         // this component is scrollable vertically but not horizontally
 *         scrollable: 'y',
 *         html: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque convallis lorem et magna tempus fermentum.'
 *     });
 */
Ext.define('Ext.scroll.TouchScroller', {
    extend: 'Ext.scroll.Scroller',
    alias: 'scroller.touch',
 
    requires: [
        'Ext.fx.easing.BoundMomentum',
        'Ext.fx.easing.EaseOut',
        'Ext.util.Translatable',
        'Ext.scroll.Indicator',
        'Ext.GlobalEvents'
    ],
 
    isTouchScroller: true,
 
    config: {
        /**
         * @cfg autoRefresh
         * Accepted values:
         *
         * - `true`: monitors both element and innerElement for resize
         * - `null`: only monitors element for resize
         * - `false`: does not monitor resize of either element or innerElement
         * @private
         */
        autoRefresh: true,
 
        /**
         * @cfg bounceEasing
         * @private
         */
        bounceEasing: {
            duration: 400
        },
 
        /**
         * @cfg
         * @private
         */
        elementSize: undefined,
 
        indicators: true,
 
        /**
         * @cfg fps
         * @private
         */
        fps: 'auto',
 
        /**
         * @cfg maxAbsoluteVelocity
         * @private
         */
        maxAbsoluteVelocity: 6,
 
        /**
         * @cfg {Object} momentumEasing 
         * @inheritdoc
         * The default value is:
         *
         *     {
         *         momentum: {
         *             acceleration: 30,
         *             friction: 0.5
         *         },
         *         bounce: {
         *             acceleration: 30,
         *             springTension: 0.3
         *         }
         *     }
         *
         * Note that supplied object will be recursively merged with the default object. For example, you can simply
         * pass this to change the momentum acceleration only:
         *
         *     {
         *         momentum: {
         *             acceleration: 10
         *         }
         *     }
         */
        momentumEasing: {
            momentum: {
                acceleration: 30,
                friction: 0.5
            },
 
            bounce: {
                acceleration: 30,
                springTension: 0.3
            },
 
            minVelocity: 1
        },
 
        /**
         * @cfg outOfBoundRestrictFactor
         * @private
         */
        outOfBoundRestrictFactor: 0.5,
 
        /**
         * @cfg {Ext.dom.Element}
         * @private
         * The element that wraps the content of {@link #element} and is translated in
         * response to user interaction.  If not configured, one will be automatically
         * generated.
         */
        innerElement: null,
 
        size: undefined,
 
        /**
         * @cfg
         * @private
         */
        slotSnapEasing: {
            duration: 150
        },
 
        /**
         * @cfg slotSnapOffset
         * @private
         */
        slotSnapOffset: {
            x: 0,
            y: 0
        },
 
        /**
         * @cfg startMomentumResetTime
         * @private
         */
        startMomentumResetTime: 300,
 
        /**
         * @cfg translatable
         * @private
         */
        translatable: {
            translationMethod: 'auto',
            useWrapper: false
        }
    },
 
    cls: Ext.baseCSSPrefix + 'scroll-container',
    scrollerCls: Ext.baseCSSPrefix + 'scroll-scroller',
 
    dragStartTime: 0,
 
    dragEndTime: 0,
 
    isDragging: false,
 
    isAnimating: false,
 
    isMouseEvent: {
        mousedown: 1,
        mousemove: 1,
        mouseup: 1
    },
 
    listenerMap: {
        touchstart: 'onTouchStart',
        touchmove: 'onTouchMove',
        dragstart: 'onDragStart',
        drag: 'onDrag',
        dragend: 'onDragEnd'
    },
 
    refreshCounter: 0,
 
    constructor: function(config) {
        var me = this,
            onEvent = 'onEvent';
 
        me.elementListeners = {
            touchstart: onEvent,
            touchmove: onEvent,
            dragstart: onEvent,
            drag: onEvent,
            dragend: onEvent,
            scope: me
        };
 
        me.minPosition = { x: 0, y: 0 };
 
        me.startPosition = { x: 0, y: 0 };
 
        me.velocity = { x: 0, y: 0 };
 
        me.isAxisEnabledFlags = { x: false, y: false };
 
        me.flickStartPosition = { x: 0, y: 0 };
 
        me.flickStartTime = { x: 0, y: 0 };
 
        me.lastDragPosition = { x: 0, y: 0 };
 
        me.dragDirection = { x: 0, y: 0};
 
        me.callParent([config]);
 
        me.refreshAxes();
        
        me.scheduleRefresh = {
            idle: me.doRefresh,
            scope: me,
            single: true,
            destroyable: true
        }
    },
    
    applyBounceEasing: function(easing) {
        var defaultClass = Ext.fx.easing.EaseOut;
 
        return {
            x: Ext.factory(easing, defaultClass),
            y: Ext.factory(easing, defaultClass)
        };
    },
 
    applyElementSize: function(size) {
        var el = this.getElement(),
            dom, x, y;
 
        if (!el) {
            return null;
        }
 
        dom = el.dom;
 
        if (!dom) {
            return;
        }
 
        if (size == null) { // null or undefined 
            x = dom.clientWidth;
            y = dom.clientHeight;
        } else {
            x = size.x;
            y = size.y;
        }
 
        return {
            x: x,
            y: y
        };
    },
 
    applyIndicators: function(indicators, oldIndicators) {
        var me = this,
            xIndicator, yIndicator, x, y;
 
        if (indicators) {
            if (indicators === true) {
                xIndicator = yIndicator = {};
            } else {
                x = indicators.x;
                y = indicators.y;
                if (|| y) {
                    // handle an object with x/y keys for configuring the indicators 
                    // individually.  undfined/null/true are all the same, only false 
                    // can prevent the indicator from being created 
                    xIndicator = (== null || x === true) ? {} : x;
                    yIndicator = (== null || y === true) ? {} : y;
                } else {
                    // not an object with x/y keys, handle as a single indicators config 
                    // that applies to both axes 
                    xIndicator = yIndicator = indicators;
                }
            }
 
            if (oldIndicators) {
                if (xIndicator) {
                    oldIndicators.x.setConfig(xIndicator);
                } else {
                    oldIndicators.x.destroy();
                    oldIndicators.x = null;
                }
                if (yIndicator) {
                    oldIndicators.y.setConfig(yIndicator);
                } else {
                    oldIndicators.y.destroy();
                    oldIndicators.y = null;
                }
                indicators = oldIndicators;
            } else {
                indicators = { x: null, y: null };
                if (xIndicator) {
                    indicators.x = new Ext.scroll.Indicator(Ext.applyIf({
                        axis: 'x',
                        scroller: me
                    }, xIndicator));
                }
                if (yIndicator) {
                    indicators.y = new Ext.scroll.Indicator(Ext.applyIf({
                        axis: 'y',
                        scroller: me
                    }, yIndicator));
                }
            }
        } else if (oldIndicators) {
            if (oldIndicators.x) {
                oldIndicators.x.destroy();
            }
            if (oldIndicators.y) {
                oldIndicators.y.destroy();
            }
            oldIndicators.x = oldIndicators.y = null;
        }
 
        return indicators;
    },
 
    applyMomentumEasing: function(easing) {
        var defaultClass = Ext.fx.easing.BoundMomentum;
 
        return {
            x: Ext.factory(easing, defaultClass),
            y: Ext.factory(easing, defaultClass)
        };
    },
 
    applyInnerElement: function(innerElement) {
        if (innerElement && !innerElement.isElement) {
            innerElement = Ext.get(innerElement);
        }
 
        //<debug> 
        if (this.isConfiguring && !innerElement) {
            Ext.raise("Cannot create Ext.scroll.TouchScroller instance with null innerElement");
        }
        //</debug> 
 
        return innerElement;
    },
 
    applyMaxPosition: function(maxPosition, oldMaxPosition) {
        // If a no-op (generated setter tests object identity), return undefined to abort set. 
        if (oldMaxPosition && maxPosition.x === oldMaxPosition.x && maxPosition.y === oldMaxPosition.y) {
            return;
        }
        var translatable = this.getTranslatable(),
            yEasing;
 
        // If an animation is in flight... 
        if (translatable.isAnimating) {
 
            // Find its Y dimension easing object 
            yEasing = translatable.activeEasingY;
 
            // If it's animating in the -ve direction (scrolling up), and we are 
            // shortening the scroll range, ensure the easing's min point complies 
            // with the new end position. 
            if (yEasing && yEasing.getStartVelocity &&
                yEasing.getStartVelocity() < 0 && maxPosition.y < oldMaxPosition.y) {
                yEasing.setMinMomentumValue(-maxPosition.y);
            }
        }
 
        return maxPosition;
    },
 
    applyMaxUserPosition: function(maxUserPosition, oldMaxUserPosition) {
        // If a no-op (generated setter tests object identity), return undefined to abort set. 
        if (oldMaxUserPosition && maxUserPosition.x === oldMaxUserPosition.x && maxUserPosition.y === oldMaxUserPosition.y) {
            return;
        }
        return maxUserPosition;
    },
 
    applySize: function(size) {
        var el = this.getElement(),
            dom, scrollerDom, x, y;
 
        if (typeof size === 'number') {
            x = size;
            y = size;
        } else if (size) {
            x = size.x;
            y = size.y;
        }
 
        if (el && (== null || y == null)) {
            dom = el.dom;
            scrollerDom = this.getInnerElement().dom;
 
            // using scrollWidth/scrollHeight instead of offsetWidth/offsetHeight ensures 
            // that the size includes any contained absolutely positioned items 
            if (== null) {
                x = Math.max(scrollerDom.scrollWidth, dom.clientWidth);
            }
 
            if (==  null) {
                y = Math.max(scrollerDom.scrollHeight, dom.clientHeight);
            }
        }
 
        return {
            x: x,
            y: y
        };
    },
 
    applySlotSnapOffset: function(snapOffset) {
        if (typeof snapOffset === 'number') {
            snapOffset = {
                x: snapOffset,
                y: snapOffset
            };
        }
 
        return snapOffset;
    },
 
    applySlotSnapSize: function(snapSize) {
        if (typeof snapSize === 'number') {
            snapSize = {
                x: snapSize,
                y: snapSize
            };
        }
 
        return snapSize;
    },
 
    applySlotSnapEasing: function(easing) {
        var defaultClass = Ext.fx.easing.EaseOut;
 
        return {
            x: Ext.factory(easing, defaultClass),
            y: Ext.factory(easing, defaultClass)
        };
    },
 
    applyTranslatable: function(config, translatable) {
        return Ext.factory(config, Ext.util.Translatable, translatable);
    },
 
    destroy: function() {
        var me = this,
            element = me.getElement(),
            innerElement = me.getInnerElement(),
            sizeMonitors = me.sizeMonitors;
 
        if (sizeMonitors) {
            sizeMonitors.element.destroy();
            sizeMonitors.container.destroy();
        }
 
        if (element && !element.destroyed) {
            element.removeCls(me.cls);
        }
 
        if (innerElement && !innerElement.destroyed) {
            innerElement.removeCls(me.scrollerCls);
        }
 
        if (me._isWrapped) {
            if (!element.destroyed) {
                me.unwrapContent();
            }
 
            innerElement.destroy();
        }
 
        me.setElement(null);
        me.setInnerElement(null);
        me.setIndicators(null);
 
        Ext.destroy(me.getTranslatable());
 
        me.callParent();
    },
 
    refresh: function(immediate, /* private */ options) {
        var me = this;
 
        ++me.refreshCounter;
        if (immediate) {
            me.doRefresh(options);
        }
        // Schedule a refresh at the next transition to idle. 
        else if (!me.refreshScheduled) {
            me.scheduleRefresh.args = [options];
            me.refreshScheduled = Ext.on(me.scheduleRefresh);
        }
    },
 
    updateAutoRefresh: function(autoRefresh) {
        this.toggleResizeListeners(autoRefresh);
    },
 
    updateBounceEasing: function(easing) {
        this.getTranslatable().setEasingX(easing.x).setEasingY(easing.y);
    },
 
    updateElementSize: function() {
        if (!this.isConfiguring) {
            // to avoid multiple calls to refreshAxes() during initialization we will 
            // call it once after initConfig has finished. 
            this.refreshAxes();
        }
    },
 
    updateDisabled: function(disabled) {
        // attachment of listeners is handled by updateElement during initial config 
        if (!this.isConfiguring) {
            if (disabled) {
                this.detachListeners();
            } else {
                this.attachListeners();
            }
        }
    },
 
    updateElement: function(element, oldElement) {
        var me = this,
            // first check if the user configured a innerElement 
            innerElement = me.getInnerElement(),
            listeners, autoRefresh;
 
        if (!innerElement) {
            // if no configured scroller element, check if the first child has the 
            // scrollerCls, if so we can assume that the user already wrapped the content 
            // in a scrollerEl (this is true of both Ext and Touch Components). 
            innerElement = element.dom.firstChild;
 
            if (!innerElement || innerElement.nodeType !== 1 ||
                    !Ext.fly(innerElement).hasCls(me.scrollerCls)) {
                // no scrollerEl found, generate one now 
                innerElement = me.wrapContent(element);
            }
 
            me.setInnerElement(innerElement);
        }
 
        element.addCls(me.cls);
 
        if (me.isConfiguring) {
            if (!me.getTranslatable().isScrollParent) {
                // Not using DOM scrolling, clear overflow styles. 
                element.dom.style.overflowX = element.dom.style.overflowY = '';
 
                // If using full virtual scrolling attach a mousewheel listener for moving 
                // the scroll position.  Otherwise we use native scrolling when interacting 
                // using the mouse and so do not want to override the native behavior 
                listeners = me.elementListeners;
                listeners.mousewheel = 'onMouseWheel';
                listeners.scroll = {
                    fn: 'onElementScroll',
                    delegated: false,
                    scope: me
                };
            }
        }
 
        if (!me.getDisabled()) {
            me.attachListeners();
        }
 
        if (!me.isConfiguring) {
            // setting element after initial construction of Scroller 
            // sync up configs that depend on element 
            autoRefresh = me.getAutoRefresh();
 
            if (autoRefresh !== false) {
                me.toggleResizeListeners(autoRefresh);
 
                if (autoRefresh) {
                    me.refresh();
                } else if (autoRefresh === null) {
                    // setting elementSize to null will cause it to be updated from the dom 
                    me.setElementSize(null);
                }
            }
        }
    },
 
    updateFps: function(fps) {
        if (fps !== 'auto') {
            this.getTranslatable().setFps(fps);
        }
    },
 
    updateMaxUserPosition: function() {
        this.snapToBoundary();
    },
 
    updateMinUserPosition: function() {
        this.snapToBoundary();
    },
 
    updateInnerElement: function(innerElement) {
        if (innerElement) {
            innerElement.addCls(this.scrollerCls);
        }
 
        this.getTranslatable().setElement(innerElement);
    },
 
    updateSize: function(size) {
        if (!this.isConfiguring) {
 
            // Base class keeps the spacer el sized to "stretch" a DOM scroll range. 
            if (Ext.supports.touchScroll === 1) {
                this.callParent([size]);
            }
            // to avoid multiple calls to refreshAxes() during initialization we will 
            // call it once after initConfig has finished. 
            this.refreshAxes();
        }
    },
 
    updateTranslatable: function(translatable) {
        translatable.setElement(this.getInnerElement());
 
        // We only need to process scroll in each frame, and scroll end on animation end 
        // if we are using CSS transform. 
        //  
        // If we are using DOM scrollTop/scrollLeft, then 
        // Scroller#onDomScroll will perform these duties. 
        if (!translatable.isScrollParent) {
            translatable.on({
                animationframe: 'onAnimationFrame',
                animationend: 'onAnimationEnd',
                scope: this
            });
        }
    },
 
    updateX: function() {
        if (!this.isConfiguring) {
            // to avoid multiple calls to refreshAxes() during initialization we will 
            // call it once after initConfig has finished. 
            this.refreshAxes();
        }
    },
 
    updateY: function() {
        if (!this.isConfiguring) {
            // to avoid multiple calls to refreshAxes() during initialization we will 
            // call it once after initConfig has finished. 
            this.refreshAxes();
        }
    },
 
    privates: {
        attachListeners: function() {
            this.getElement().on(this.elementListeners);
        },
 
        constrainX: function(x) {
            return Math.min(this.getMaxPosition().x, Math.max(x, 0));
        },
 
        constrainY: function(y) {
            return Math.min(this.getMaxPosition().y, Math.max(y, 0));
        },
 
        // overridden in RTL mode to swap min/max momentum values 
        convertEasingConfig: function(config) {
            return config;
        },
 
        detachListeners: function() {
            this.getElement().un(this.elementListeners);
        },
 
        /**
         * @private
         */
        doRefresh: function(options) {
            var me = this,
                size, elementSize;
 
            if (me.refreshScheduled) {
                me.refreshScheduled = me.refreshScheduled.destroy();
            }
 
            if (me.refreshCounter && me.getElement()) {
                me.stopAnimation();
 
                me.getTranslatable().refresh();
 
                if (options) {
                    // If either size or elementSize were provided in options, do not bother 
                    // to read the DOM to determine sizing info, just set the size given. 
                    size = options.size;
                    elementSize = options.elementSize;
                }
 
                // If size or elementSize are null, they will be read from the DOM 
                me.setSize(size);
                me.setElementSize(elementSize);
 
                me.fireEvent('refresh', me);
                me.refreshCounter = 0;
            }
        },
 
        doScrollTo: function(x, y, animation, /* private */ allowOverscroll) {
            var me = this,
                isDragging = me.isDragging,
                // We only call onScroll if we are programatically CSS translating the scrollable. 
                // If we are using DOM scrollTop/scrollLeft, then 
                // Scroller#onDomScroll will perform this duty. 
                DOMScrolling = me.getTranslatable().isScrollParent,
                // We only fire scrollstart, and scrollend if we are not reflecting, 
                // (and not having this handled by Scroller#onDomScroll) 
                // If we are reflecting, then onPartnerScrollStart and onPartnerScrollEnd 
                // will perform these duties. 
                fireStartEnd = !me.isReflecting && !DOMScrolling;
 
            if (me.destroyed || !me.getElement()) {
                return me;
            }
 
            // Normally the scroll position is constrained to the max scroll position, but 
            // during a drag operation or during reflection the scroller is allowed to overscroll. 
            allowOverscroll = allowOverscroll || me.isDragging;
 
            var translatable = me.getTranslatable(),
                position = me.position,
                positionChanged = false,
                translationX, translationY;
 
            if (!isDragging || me.isAxisEnabled('x')) {
                if (isNaN(x) || typeof x !== 'number') {
                    x = position.x;
                } else {
 
                    if (!allowOverscroll) {
                        x = me.constrainX(x);
                    }
 
                    if (position.x !== x) {
                        position.x = x;
                        positionChanged = true;
                    }
                }
 
                translationX = me.convertX(-x);
            }
 
            if (!isDragging || me.isAxisEnabled('y')) {
                if (isNaN(y) || typeof y !== 'number') {
                    y = position.y;
                } else {
                    if (!allowOverscroll) {
                        y = me.constrainY(y);
                    }
 
                    if (position.y !== y) {
                        position.y = y;
                        positionChanged = true;
                    }
                }
 
                translationY = -y;
            }
 
            if (positionChanged) {
                // If we are using scrollTop/scrollLeft, then DOM events will fire 
                // and Scroller#onDomScroll will perform scroll start processing 
                if (fireStartEnd) {
                    // Invoke scroll start handlers. 
                    // If we are already scrolling, and this movement 
                    // is in response to a subsequent touchmove, the this.isScrolling 
                    // flag will already be set, and this will do nothing. 
                    me.onScrollStart();
                }
                if (animation) {
                    // onAnimationEnd calls onScrollEnd 
                    translatable.translateAnimated(translationX, translationY, animation);
                } else {
                    // If we are using scrollTop/scrollLeft, then DOM events will fire 
                    // and Scroller#onDomScroll will perform scroll processing 
                    if (!DOMScrolling) {
                        me.onScroll();
                    }
                    translatable.translate(translationX, translationY);
                    // If we are using scrollTop/scrollLeft, then DOM events will fire 
                    // and Scroller#onDomScroll will schedule scroll end processing 
                    if (fireStartEnd) {
                        me.onScrollEnd();
                    }
                }
            } else if (animation && animation.callback) {
                animation.callback();
            }
 
            return me;
        },
 
        /**
         * @private
         */
        getAnimationEasing: function(axis, e) {
            if (!this.isAxisEnabled(axis)) {
                return null;
            }
 
            var me = this,
                currentPosition = me.position[axis],
                minPosition = me.getMinUserPosition()[axis],
                maxPosition = me.getMaxUserPosition()[axis],
                maxAbsVelocity = me.getMaxAbsoluteVelocity(),
                boundValue = null,
                dragEndTime = me.dragEndTime,
                velocity = e.flick.velocity[axis],
                isX = axis === 'x',
                easingConfig, easing;
 
            if (currentPosition < minPosition) {
                boundValue = minPosition;
            }
            else if (currentPosition > maxPosition) {
                boundValue = maxPosition;
            }
 
            if (isX) {
                currentPosition = me.convertX(currentPosition);
                boundValue = me.convertX(boundValue);
            }
 
            // Out of bound, to be pulled back 
            if (boundValue !== null) {
                easing = me.getBounceEasing()[axis];
                easing.setConfig({
                    startTime: dragEndTime,
                    startValue: -currentPosition,
                    endValue: -boundValue
                });
 
                return easing;
            }
 
            if (velocity === 0) {
                return null;
            }
 
            if (velocity < -maxAbsVelocity) {
                velocity = -maxAbsVelocity;
            }
            else if (velocity > maxAbsVelocity) {
                velocity = maxAbsVelocity;
            }
 
            easing = me.getMomentumEasing()[axis];
            easingConfig = {
                startTime: dragEndTime,
                startValue: -currentPosition,
                startVelocity: velocity * 1.5,
                minMomentumValue: -maxPosition,
                maxMomentumValue: 0
            };
 
            if (isX) {
                me.convertEasingConfig(easingConfig);
            }
 
            easing.setConfig(easingConfig);
 
            return easing;
        },
 
        /**
         * @private
         * @return {Number/null}
         */
        getSnapPosition: function(axis) {
            var me = this,
                snapSize = me.getSlotSnapSize()[axis],
                snapPosition = null,
                position, snapOffset, maxPosition, mod;
 
            if (snapSize !== 0 && me.isAxisEnabled(axis)) {
                position = me.position[axis];
                snapOffset = me.getSlotSnapOffset()[axis];
                maxPosition = me.getMaxUserPosition()[axis];
 
                mod = Math.floor((position - snapOffset) % snapSize);
 
                if (mod !== 0) {
                    if (position !== maxPosition) {
                        if (Math.abs(mod) > snapSize / 2) {
                            snapPosition = Math.min(maxPosition, position + ((mod > 0) ? snapSize - mod : mod - snapSize));
                        }
                        else {
                            snapPosition = position - mod;
                        }
                    }
                    else {
                        snapPosition = position - mod;
                    }
                }
            }
 
            return snapPosition;
        },
 
        hideIndicators: function() {
            var me = this,
                indicators = me.getIndicators(),
                xIndicator, yIndicator;
 
            if (indicators) {
                if (me.isAxisEnabled('x')) {
                    xIndicator = indicators.x;
                    if (xIndicator) {
                        xIndicator.hide();
                    }
                }
 
                if (me.isAxisEnabled('y')) {
                    yIndicator = indicators.y;
                    if (yIndicator) {
                        yIndicator.hide();
                    }
                }
            }
        },
 
        /**
         * Returns `true` if a specified axis is enabled.
         * @private
         * @param {String} axis The axis to check (`x` or `y`).
         * @return {Boolean} `true` if the axis is enabled.
         */
        isAxisEnabled: function(axis) {
            this.getX();
            this.getY();
 
            return this.isAxisEnabledFlags[axis];
        },
 
        onAnimationEnd: function() {
            this.snapToBoundary();
            this.onScrollEnd();
        },
 
        onAnimationFrame: function(translatable, x, y) {
            var position = this.position;
 
            position.x = this.convertX(-x);
            position.y = -y;
 
            this.onScroll();
        },
 
        onAxisDrag: function(axis, delta) {
            // Nothing to do if no delta, or it's on a disabled axis 
            if (delta && this.isAxisEnabled(axis)) {
                var me = this,
                    flickStartPosition = me.flickStartPosition,
                    flickStartTime = me.flickStartTime,
                    lastDragPosition = me.lastDragPosition,
                    dragDirection = me.dragDirection,
                    old = me.position[axis],
                    min = me.getMinUserPosition()[axis],
                    max = me.getMaxUserPosition()[axis],
                    start = me.startPosition[axis],
                    last = lastDragPosition[axis],
                    current = start - delta,
                    lastDirection = dragDirection[axis],
                    restrictFactor = me.getOutOfBoundRestrictFactor(),
                    startMomentumResetTime = me.getStartMomentumResetTime(),
                    now = Ext.Date.now(),
                    distance;
 
                if (current < min) {
                    current *= restrictFactor;
                }
                else if (current > max) {
                    distance = current - max;
                    current = max + distance * restrictFactor;
                }
 
                if (current > last) {
                    dragDirection[axis] = 1;
                }
                else if (current < last) {
                    dragDirection[axis] = -1;
                }
 
                if ((lastDirection !== 0 && (dragDirection[axis] !== lastDirection)) ||
                        (now - flickStartTime[axis]) > startMomentumResetTime) {
                    flickStartPosition[axis] = old;
                    flickStartTime[axis] = now;
                }
 
                lastDragPosition[axis] = current;
                return true;
            }
        },
 
        // In "hybrid" touch scroll mode where the TouchScroller is used to control the 
        // scroll position of a naturally overflowing element, we need to sync the scroll 
        // position of the TouchScroller when the element is scrolled 
        onDomScroll: function() {
            var me = this,
                dom, position;
 
            if (me.getTranslatable().isScrollParent) {
                dom = me.getElement().dom;
                position = me.position;
 
                position.x = dom.scrollLeft;
                position.y = dom.scrollTop;
            }
            me.callParent();
        },
 
        onDrag: function(e) {
            var me = this,
                lastDragPosition = me.lastDragPosition;
 
            if (!me.isDragging) {
                return;
            }
 
            // If there's any moving to do, then move the content. 
            // Boolean or operator avoids shortcutting the second function call if 
            // first returns true. 
            if (me.onAxisDrag('x', me.convertX(e.deltaX)) | me.onAxisDrag('y', e.deltaY)) {
                me.doScrollTo(lastDragPosition.x, lastDragPosition.y);
            }
        },
 
        onDragEnd: function(e) {
            var me = this,
                easingX, easingY;
 
            if (!me.isDragging) {
                return;
            }
 
            me.dragEndTime = Ext.Date.now();
 
            me.onDrag(e);
 
            me.isDragging = false;
 
            easingX = me.getAnimationEasing('x', e);
            easingY = me.getAnimationEasing('y', e);
 
            if (easingX || easingY) {
                me.getTranslatable().animate(easingX, easingY);
            } else {
                me.onScrollEnd();
            }
        },
 
        onDragStart: function(e) {
            var me = this,
                direction = me.getDirection(),
                absDeltaX = e.absDeltaX,
                absDeltaY = e.absDeltaY,
                directionLock = me.getDirectionLock(),
                startPosition = me.startPosition,
                flickStartPosition = me.flickStartPosition,
                flickStartTime = me.flickStartTime,
                lastDragPosition = me.lastDragPosition,
                currentPosition = me.position,
                dragDirection = me.dragDirection,
                x = currentPosition.x,
                y = currentPosition.y,
                now = Ext.Date.now();
 
            if (directionLock && direction !== 'both') {
                if ((direction === 'horizontal' && absDeltaX > absDeltaY) ||
                    (direction === 'vertical' && absDeltaY > absDeltaX)) {
                    e.stopPropagation();
                }
                else {
                    return;
                }
            }
 
            lastDragPosition.x = x;
            lastDragPosition.y = y;
 
            flickStartPosition.x = x;
            flickStartPosition.y = y;
 
            startPosition.x = x;
            startPosition.y = y;
 
            flickStartTime.x = now;
            flickStartTime.y = now;
 
            dragDirection.x = 0;
            dragDirection.y = 0;
 
            me.dragStartTime = now;
 
            me.isDragging = true;
 
            // Only signal a scroll start if we are not already scrolling. 
            // If the drag is just the user giving another impulse, it is NOT 
            // the start of a drag. 
            if (!me.isScrolling) {
                me.onScrollStart();
            }
        },
 
        onElementResize: function(element, info) {
            this.refresh(true, {
                elementSize: {
                    x: info.contentWidth,
                    y: info.contentHeight
                },
                size: this.getAutoRefresh() ? null : this.getSize()
            });
        },
 
        onElementScroll: function(event, targetEl) {
            targetEl.scrollTop = targetEl.scrollLeft = 0;
        },
 
        onEvent: function(e) {
            // use browserEvent to get the "real" type of DOM event that was fired, not a 
            // potentially translated (or recognized) type 
            var me = this,
                browserEvent = e.browserEvent;
 
            if ((!me.self.isTouching || me.isTouching) && // prevents nested scrolling 
                    // prevents scrolling in response to mouse input on multi-input devices 
                    // such as windows 8 laptops with touch screens. 
                    // Don't bother checking the event type if we are on a device that uses 
                    // full virtual scrolling (!isScrollParent) 
                    // TODO: this should be handled by the event system once EXTJSIV-12840 
                    // is implemented 
                    ((!me.getTranslatable().isScrollParent) || (!me.isMouseEvent[browserEvent.type] &&
                        browserEvent.pointerType !== 'mouse')) &&
                        (me.getY() || me.getX())) {
                me[me.listenerMap[e.type]](e);
            }
        },
 
        onInnerElementResize: function(element, info) {
            this.refresh(true, {
                size: {
                    x: info.width,
                    y: info.height
                }
            });
        },
 
        onMouseWheel: function(e) {
            var me = this,
                delta = e.getWheelDeltas(),
                deltaX = -delta.x,
                deltaY = -delta.y,
                position = me.position,
                maxPosition = me.getMaxUserPosition(),
                minPosition = me.getMinUserPosition(),
                max = Math.max,
                min = Math.min,
                positionX = max(min(position.x + deltaX, maxPosition.x), minPosition.x),
                positionY = max(min(position.y + deltaY, maxPosition.y), minPosition.y);
 
            deltaX = positionX - position.x;
            deltaY = positionY - position.y;
 
            if (!deltaX && !deltaY) {
                return;
            }
            e.stopEvent();
 
            me.onScrollStart();
            me.scrollBy(deltaX, deltaY);
            me.onScroll();
            me.onScrollEnd();
        },
 
        onPartnerScrollEnd: function(x, y) {
            var me = this;
 
            // In "hybrid" touch scroll mode where the TouchScroller is used to control the 
            // scroll position of a naturally overflowing element, we do NOT need to call 
            // fireScrollEnd because we will have been recieving DOM scroll events and will 
            // begin and end scrolling in response to those events. 
            // 
            // If scrollers programatically animate the scroll, then we must take the correct 
            // scroll end action when our partner ends its scroll. 
            if (!me.getTranslatable().isScrollParent) {
                me.fireScrollEnd(x, y);
            }
            me.callParent([x, y]);
            me.isScrolling = false;
            me.hideIndicators();
        },
 
        onPartnerScrollStart: function(x, y) {
            var me = this;
 
            me.isScrolling = true;
 
            // In "hybrid" touch scroll mode where the TouchScroller is used to control the 
            // scroll position of a naturally overflowing element, we do NOT need to call 
            // fireScrollStart because we will soon start recieving DOM scroll events and will 
            // begin and end scrolling in response to those events. 
            // 
            // If scrollers programatically animate the scroll, then we must take the correct 
            // scroll end action when our partner starts its scroll. 
            if (!me.getTranslatable().isScrollParent) {
                me.fireScrollStart(x, y);
            }
            me.showIndicators();
        },
 
        onScroll: function() {
            var me = this,
                position = me.position,
                x = position.x,
                y = position.y,
                indicators = me.getIndicators(),
                xIndicator, yIndicator;
 
            if (indicators) {
                if (me.isAxisEnabled('x')) {
                    xIndicator = indicators.x;
                    if (xIndicator) {
                        xIndicator.setValue(x);
                    }
                }
                if (me.isAxisEnabled('y')) {
                    yIndicator = indicators.y;
                    if (yIndicator) {
                        yIndicator.setValue(y);
                    }
                }
            }
 
            me.fireScroll(x, y);
        },
 
        onScrollEnd: function() {
            var me = this,
                position = me.position;
 
            if (me.isScrolling && !me.isTouching && !me.snapToSlot()) {
                me.hideIndicators();
                me.isScrolling = Ext.isScrolling = false;
                me.fireScrollEnd(position.x, position.y);
            }
        },
 
        onScrollStart: function() {
            var me = this,
                position = me.position;
 
            if (!me.isScrolling) {
                me.showIndicators();
                me.isScrolling = Ext.isScrolling = true;
                me.fireScrollStart(position.x, position.y);
            }
        },
 
        onTouchEnd: function() {
            var me = this;
 
            me.isTouching = me.self.isTouching = false;
 
            if (!me.isDragging && me.snapToSlot()) {
                me.onScrollStart();
            }
        },
 
        onTouchMove: function(e) {
            // Prevents the page from scrolling while an element is being scrolled using 
            // the TouchScroller.  Only needed when inside a page that does not use a 
            // Viewport, since the Viewport already prevents default behavior of touchmove 
            e.preventDefault();
        },
 
        onTouchStart: function() {
            var me = this;
 
            me.isTouching = me.self.isTouching = true;
 
            Ext.getDoc().on({
                touchend: 'onTouchEnd',
                scope: me,
                single: true
            });
 
            me.stopAnimation();
        },
 
        refreshAxes: function() {
            var me = this,
                flags = me.isAxisEnabledFlags,
                size = me.getSize(),
                elementSize = me.getElementSize(),
                indicators = me.getIndicators(),
                maxX, maxY, x, y, xIndicator, yIndicator;
 
            if (!size || !elementSize) {
                return;
            }
 
            maxX = Math.max(0, size.x - elementSize.x);
            maxY = Math.max(0, size.y - elementSize.y);
            x = me.getX();
            y = me.getY();
 
            me.setMaxPosition({
                x: maxX,
                y: maxY
            });
 
            if (=== true || x === 'auto') {
                // auto scroll - axis is only enabled if the content is overflowing in the 
                // same direction 
                flags.x = !!maxX;
            } else if (=== false) {
                flags.x = false;
                xIndicator = indicators && indicators.x;
                if (xIndicator) {
                    // hide the x indicator if the x axis is disabled, just in case we 
                    // are refreshing during a scroll 
                    xIndicator.hide();
                }
            } else if (=== 'scroll') {
                flags.x = true;
            }
 
            if (=== true || y === 'auto') {
                // auto scroll - axis is only enabled if the content is overflowing in the 
                // same direction 
                flags.y = !!maxY;
            } else if (=== false) {
                flags.y = false;
                yIndicator = indicators && indicators.y;
                if (yIndicator) {
                    // hide the y indicator if the y axis is disabled, just in case we 
                    // are refreshing during a scroll 
                    yIndicator.hide();
                }
            } else if (=== 'scroll') {
                flags.y = true;
            }
 
            me.setMaxUserPosition({
                x: flags.x ? maxX : 0,
                y: flags.y ? maxY : 0
            });
 
            // If we are using regular DOM overflow scrolling, sync the element styles. 
            if (Ext.supports.touchScroll === 1) {
                me.initXStyle();
                me.initYStyle();
            }
        },
 
        showIndicators: function() {
            var me = this,
                indicators = me.getIndicators(),
                xIndicator, yIndicator;
 
            if (indicators) {
                if (me.isAxisEnabled('x')) {
                    xIndicator = indicators.x;
                    if (xIndicator) {
                        xIndicator.show();
                    }
                }
 
                if (me.isAxisEnabled('y')) {
                    yIndicator = indicators.y;
                    if (yIndicator) {
                        yIndicator.show();
                    }
                }
            }
        },
 
        snapToBoundary: function() {
            var me = this,
                position = me.getPosition();
 
            // If we haven't scrolled anywhere, we're done. 
            if (me.isConfiguring || !(position.x || position.y)) {
                return;
            }
 
            var minPosition = me.getMinUserPosition(),
                maxPosition = me.getMaxUserPosition(),
                minX = minPosition.x,
                minY = minPosition.y,
                maxX = maxPosition.x,
                maxY = maxPosition.y,
                x = Math.round(position.x),
                y = Math.round(position.y);
 
            if (< minX) {
                x = minX;
            }
            else if (> maxX) {
                x = maxX;
            }
 
            if (< minY) {
                y = minY;
            }
            else if (> maxY) {
                y = maxY;
            }
 
            me.doScrollTo(x, y);
        },
 
        /**
         * @private
         * @return {Boolean}
         */
        snapToSlot: function() {
            var me = this,
                snapX = me.getSnapPosition('x'),
                snapY = me.getSnapPosition('y'),
                easing = me.getSlotSnapEasing();
 
            if (snapX !== null || snapY !== null) {
                me.doScrollTo(snapX, snapY, {
                    easingX: easing.x,
                    easingY: easing.y
                });
 
                return true;
            }
 
            return false;
        },
 
        /**
         * @private
         * Stops the animation of the scroller at any time.
         */
        stopAnimation: function() {
            this.getTranslatable().stopAnimation();
        },
 
        toggleResizeListeners: function(autoRefresh) {
            var me = this,
                element = me.getElement(),
                method, innerMethod,
                innerElement;
 
            if (element) {
                innerElement = me.getInnerElement();
                if (autoRefresh) {
                    method = innerMethod = 'on';
                } else if (autoRefresh === null) {
                    method = 'on';
                    innerMethod = 'un';
                } else {
                    method = innerMethod = 'un';
                }
 
                element[method]('resize', 'onElementResize', me);
                innerElement[innerMethod]('resize', 'onInnerElementResize', me);
            }
        },
 
        unwrapContent: function() {
            var innerDom = this.getInnerElement().dom,
                dom = this.getElement().dom,
                child;
 
            while ((child = innerDom.firstChild)) {
                dom.insertBefore(child, innerDom);
            }
        },
 
        /**
         * Wraps the element's content in a innerElement
         * @param {Ext.dom.Element} element
         * @return {Ext.dom.Element} the innerElement
         * @private
         */
        wrapContent: function(element) {
            var wrap = document.createElement('div'),
                dom = element.dom,
                child;
 
            while (child = dom.lastChild) { // jshint ignore:line 
                wrap.insertBefore(child, wrap.firstChild);
            }
 
            dom.appendChild(wrap);
 
            this.setInnerElement(wrap);
 
            // Set a flag that indiacates the element's content was not already pre-wrapped 
            // when the scroller was instanced.  This means we had to wrap the content 
            // and so must unwrap when we destroy the scroller. 
            this._isWrapped = true;
 
            return this.getInnerElement();
        }
    }
});