/**
 * Ext.scroll.Scroller allows any element to have scrollable content, both on desktop and
 * touch-screen devices, and defines a set of useful methods for manipulating the scroll
 * position and controlling the scrolling behavior.
 */
Ext.define('Ext.scroll.Scroller', {
    extend: 'Ext.Evented',
    alias: 'scroller.scroller',
 
    mixins: [
        'Ext.mixin.Factoryable',
        'Ext.mixin.Bufferable'
    ],
 
    requires: [
        'Ext.util.CSS',
        'Ext.util.translatable.ScrollPosition',
        'Ext.Deferred'
    ],
 
    factoryConfig: {
        defaultType: 'scroller'
    },
 
    bufferableMethods: {
        onDomScrollEnd: 100
    },
 
    isScroller: true,
 
    /**
     * @event refresh
     * Fires whenever the Scroller is refreshed.
     * @param {Ext.scroll.Scroller} this 
     */
 
    /**
     * @event scrollstart
     * Fires whenever the scrolling is started.
     * @param {Ext.scroll.Scroller} this 
     * @param {Number} x The current x position.
     * @param {Number} y The current y position.
     */
 
    /**
     * @event scrollend
     * Fires whenever the scrolling is ended.
     * @param {Ext.scroll.Scroller} this 
     * @param {Number} x The current x position.
     * @param {Number} y The current y position.
     * @param {Number} deltaX The change in x value.
     * @param {Number} deltaY The change in y value.
     */
 
    /**
     * @event scroll
     * Fires whenever the Scroller is scrolled.
     * @param {Ext.scroll.Scroller} this 
     * @param {Number} x The new x position.
     * @param {Number} y The new y position.
     * @param {Number} deltaX The change in x value.
     * @param {Number} deltaY The change in y value.
     */
 
    config: {
        /**
         * @cfg {'auto'/'vertical'/'horizontal'/'both'} [direction='auto']
         * @deprecated 5.1.0 use {@link #x} and {@link #y} instead
         */
        direction: undefined, // undefined because we need the updater to always run
 
        /**
         * @cfg {String/HTMLElement/Ext.dom.Element}
         * The element to make scrollable.
         */
        element: undefined,
 
        /**
         * @cfg {Boolean} [scrollbars=true]
         * `false` to hide scrollbars on browsers where it is possible via CSS,
         * Currently Webkit, Chrome, and IE10+
         * @private
         */
        scrollbars: null,
 
        /**
         * @cfg {String} A CSS selector that identifies items inside this scroller that
         * should be snapped into position when user scrolling ends.  By default the items
         * top/left will be aligned with the top/left of the container.  This alignment
         * can be changed using {@link #snapOffset}
         *
         * This api is highly experimental as it is based on bleeding-edge CSS implementations
         * that may change in the near future.  Do not rely on it in your applications.
         *
         * @private
         */
        snapSelector: null,
 
        /**
         * @cfg {Object} An object with x and y properties for offsetting the currently
         * snapped item from the top/left of the container.
         *
         * This api is highly experimental as it is based on bleeding-edge CSS implementations
         * that may change in the near future.  Do not rely on it in your applications.
         *
         * @private
         */
        snapOffset: null,
 
        /**
         * @cfg {Object} an object with x and y properties that specifies the size of the
         * snap points on the x and y axes.  For IE10+/Edge only, since those browsers do
         * not support the newer CSS properties for snapping to element boundaries.
         *
         * This config is experimental and may be removed in a future version of the framework.
         *
         * @private
         */
        msSnapInterval: null,
 
        /**
         * @cfg {Boolean/String}
         * - `true` or `'auto'` to enable horizontal auto-scrolling. In auto-scrolling mode
         * scrolling is only enabled when the {@link #element} has overflowing content.
         * - `false` to disable horizontal scrolling
         * - `'scroll'` to always enable horizontal scrolling regardless of content size.
         */
        x: true,
 
        /**
         * @cfg {Boolean/String}
         * - `true` or `'auto'` to enable vertical auto-scrolling. In auto-scrolling mode
         * scrolling is only enabled when the {@link #element} has overflowing content.
         * - `false` to disable vertical scrolling
         * - `'scroll'` to always enable vertical scrolling regardless of content size.
         */
        y: true,
 
        /**
         * @cfg {Ext.dom.Element} scrollElement
         * The element to read the scrollTop/scrollLeft from. This is used to
         * work around cross browser issues where WebKit/Blink require reading/writing
         * scrollTop/scrollLeft on the document.body, even if the documentElement is
         * the thing overflowing. In future this can be removed once document.scrollingElement
         * becomes a standard across all supported browsers.
         *
         * Note that scroll(Width/Height) and other dimensions can be read from the
         * documentElement without issue.
         * @private
         */
        scrollElement: null,
 
        /**
         * @cfg {Object} 
         * The size of the scrollable content expressed as an object with x and y properties
         * @private
         * @readonly
         */
        size: null,
 
        spacerXY: null,
 
        /**
         * @cfg {Object} touchAction for the scroller's {@link #element}.
         *
         * For more details see {@link Ext.dom.Element#setTouchAction}
         */
        touchAction: null
    },
 
    snappableCls: Ext.baseCSSPrefix + 'scroller-snappable',
    elementCls: Ext.baseCSSPrefix + 'scroller',
    spacerCls: Ext.baseCSSPrefix + 'scroller-spacer',
    noScrollbarsCls: Ext.baseCSSPrefix + 'no-scrollbars',
 
    statics: {
        /**
         * Creates and returns an appropriate Scroller instance for the current device.
         * @param {Object} config Configuration options for the Scroller
         * @param type
         * @return {Ext.scroll.Scroller} 
         */
        create: function(config, type) {
            return Ext.Factory.scroller(config, type);
        },
 
        /**
         * Get the scrolling element for the document based on feature detection.
         * See: https://dev.opera.com/articles/fixing-the-scrolltop-bug/
         * 
         * @return {HTMLElement} 
         *
         * @private
         */
        getScrollingElement: function() {
            var doc = document,
                standard = this.$standardScrollElement,
                el = doc.scrollingElement,
                iframe, frameDoc;
 
            // Normalize the scrollElement we need to read/write from
            // First attempt to detect the newer standard for viewport
            // scrolling
            if (el) {
                return el;
            }
 
            // The newer standard doesn't exist, let the scroller
            // decide via feature detection.
            if (standard === undefined) {
                iframe = document.createElement('iframe');
 
                iframe.style.height = '1px';
                document.body.appendChild(iframe);
                frameDoc = iframe.contentWindow.document;
                frameDoc.write('<!DOCTYPE html><div style="height:9999em">x</div>');
                frameDoc.close();
                standard = frameDoc.documentElement.scrollHeight > frameDoc.body.scrollHeight;
                iframe.parentNode.removeChild(iframe);
 
                this.$standardScrollElement = standard;
            }
            return standard ? doc.documentElement : doc.body;
        },
        
        /**
         * @private
         */
        initViewportScroller: function() {
            var scroller = Ext.getViewportScroller();
 
            if (!scroller.getElement()) {
                // if the viewport component has already claimed the viewport scroller
                // it will have already set its overflow element as the scroller element,
                // otherwise, the element is always the body.
                scroller.setElement(Ext.getBody());
            }
        }
    },
 
    constructor: function(config) {
        var me = this;
 
        me.position = { x: 0, y: 0 };
 
        me.callParent([config]);
    },
 
    destroy: function() {
        var me = this,
            partners = me._partners,
            key;
 
        Ext.undefer(me.restoreTimer);
 
        // Clear any overflow styles
        me.setX(Ext.emptyString);
        me.setY(Ext.emptyString);
        
        if (me._spacer) {
            me._spacer.destroy();
        }
        
        if (me.scrollListener) {
            me.scrollListener.destroy();
        }
 
        if (partners) {
            for (key in partners) {
                me.removePartner(partners[key].scroller);
            }
        }
 
        // Remove element listeners, this will cause scrollElement to
        // be cleared also.
        me.setElement(null);
        me._partners = me.component = null;
 
        if (me.translatable) {
            me.translatable.destroy();
            me.translatable = null;
        }
 
        me.removeSnapStylesheet();
 
        me.callParent();
    },
 
    /**
     * Adds a "partner" scroller.  Partner scrollers reflect each other's scroll position
     * at all times - if either scroller is scrolled, the scroll position of its partner
     * will be be automatically synchronized.
     *
     * A scroller may have multiple partners.
     *
     * @param {Ext.scroll.Scroller} partner 
     * @param {String} [axis='both'] The axis to synchronize (`'x'`, '`y`', or '`both`')
     */
    addPartner: function(partner, axis) {
        var me = this,
            partners = me._partners || (me._partners = {}),
            otherPartners = partner._partners || (partner._partners = {});
 
        // Translate to boolean flags. {x:<boolean>,y:<boolean>}
        axis = me.axisConfigs[axis || 'both'];
 
        partners[partner.getId()] = {
            scroller: partner,
            axes: axis
        };
 
        otherPartners[me.getId()] = {
            scroller: me,
            axes: axis
        };
    },
 
    applyElement: function(element, oldElement) {
        var me = this;
 
        if (oldElement && me.scrollListener) {
            me.scrollListener.destroy();
        }
 
        if (element) {
            //<debug>
            if (typeof element === 'string' && !Ext.get(element)) {
                Ext.raise("Cannot create Ext.scroll.Scroller instance. " +
                    "Element with id '" + element + "' not found.");
            }
            //</debug>
            element = Ext.get(element);
        }
        return element;
    },
 
    applySize: function(size, oldSize) {
        var x, y;
 
        if (size === null || typeof size === 'number') {
            x = y = size;
        } 
        else if (size) {
            x = size.x;
            y = size.y;
        }
 
        if (=== null) {
            x = 0;
        } 
        else if (=== undefined) {
            x = (oldSize ? oldSize.x : 0);
        }
 
        if (=== null) {
            y = 0;
        } 
        else if (=== undefined) {
            y = (oldSize ? oldSize.y : 0);
        }
        if (!oldSize || x !== oldSize.x || y !== oldSize.y) {
            return { x: x, y: y };
        }
    },
 
    /**
     * Gets the `clientWidth` and `clientHeight` of the {@link #element} for this scroller.
     * @return {Object} An object with `x` and `y` properties.
     */
    getClientSize: function() {
        var dom = this.getElement().dom;
        return {
            x: dom.clientWidth,
            y: dom.clientHeight
        };
    },
 
    /**
     * Returns the amount of space consumed by scrollbars in the DOM
     * @return {Object} size An object containing the scrollbar sizes.
     * @return {Number} return.width The width of the vertical scrollbar.
     * @return {Number} return.height The height of the horizontal scrollbar.
     */
    getScrollbarSize: function() {
        var me = this,
            width = 0,
            height = 0,
            element = me.getElement(),
            dom, x, y, hasXScroll, hasYScroll, scrollbarSize;
 
        if (element && !element.destroyed) {
            x = me.getX();
            y = me.getY();
            dom = element.dom;
 
            if (|| y) {
                scrollbarSize = Ext.getScrollbarSize();
            }
 
            if (=== 'scroll') {
                hasXScroll = true;
            } 
            else if (x) {
                hasXScroll = dom.scrollWidth > dom.clientWidth;
            }
 
            if (=== 'scroll') {
                hasYScroll = true;
            } 
            else if (y) {
                hasYScroll = dom.scrollHeight > dom.clientHeight;
            }
 
            if (hasXScroll) {
                height = scrollbarSize.height;
            }
 
            if (hasYScroll) {
                width = scrollbarSize.width;
            }
        }
 
        return {
            width: width,
            height: height
        };
    },
 
    /**
     * @method getPosition
     * Returns the current scroll position
     * @return {Object} An object with `x` and `y` properties.
     */
    getPosition: function() {
        var me = this;
        if (me.positionDirty) {
            me.updateDomScrollPosition();
        }
        return me.position;
    },
 
    /**
     * @method getSize
     * Returns the size of the scrollable content
     * @return {Object} size
     * @return {Number} return.x The width of the scrollable content
     * @return {Number} return.y The height of the scrollable content
     */
    getSize: function() {
        var element = this.getElement(),
            size, dom;
 
        if (element && !element.destroyed) {
            dom = element.dom;
            size = {
                x: dom.scrollWidth,
                y: dom.scrollHeight
            };
        } 
        else {
            size = {
                x: 0,
                y: 0
            };
        }
 
        return size;
    },
 
    /**
     * @method getMaxPosition
     * Returns the maximum scroll position for this scroller
     * @return {Object} position
     * @return {Number} return.x The maximum scroll position on the x axis
     * @return {Number} return.y The maximum scroll position on the y axis
     */
    getMaxPosition: function() {
        var element = this.getElement(),
            x = 0,
            y = 0,
            dom;
 
        if (element && !element.destroyed) {
            dom = element.dom;
            x = dom.scrollWidth - dom.clientWidth;
            y = dom.scrollHeight - dom.clientHeight;
        }
 
 
        return {
            x: x,
            y: y
        };
    },
 
    /**
     * @method getMaxUserPosition
     * Returns the maximum scroll position for this scroller for scrolling that is initiated
     * by the user via mouse or touch.  This differs from getMaxPosition in that getMaxPosition
     * returns the true maximum scroll position regardless of which axes are enabled for
     * user scrolling.
     * @return {Object} position
     * @return {Number} return.x The maximum scroll position on the x axis
     * @return {Number} return.y The maximum scroll position on the y axis
     */
    getMaxUserPosition: function() {
        var me = this,
            element = me.getElement(),
            x = 0,
            y = 0,
            dom;
 
        if (element && !element.destroyed) {
            dom = element.dom;
            if (me.getX()) {
                x = dom.scrollWidth - dom.clientWidth;
            }
            if (me.getY()) {
                y = dom.scrollHeight - dom.clientHeight;
            }
        }
 
        return {
            x: x,
            y: y
        };
    },
 
    /**
     * Refreshes the scroller size and maxPosition.
     * @param {Boolean} immediate `true` to refresh immediately. By default refreshes
     * are deferred until the next {@link Ext.GlobalEvents#event-idle idle} event to
     * ensure any pending writes have been flushed to the dom and any reflows have
     * taken place.
     * @return {Ext.scroll.Scroller} this
     * @chainable
     */
    refresh: function() {
        // Element size has changed.
        // Our position property may need refreshing from the DOM
        this.positionDirty = true;
 
        this.fireEvent('refresh', this);
        return this;
    },
 
    /**
     * Removes a partnership that was created via {@link #addPartner}
     * @param {Ext.scroll.Scroller} partner 
     * @private
     */
    removePartner: function(partner) {
        var partners = this._partners,
            otherPartners = partner._partners;
 
        if (partners) {
            delete partners[partner.getId()];
        }
 
        if (otherPartners) {
            delete(otherPartners[this.getId()]);
        }
    },
 
    /**
     * Scrolls by the passed delta values, optionally animating.
     *
     * All of the following are equivalent:
     *
     *      scroller.scrollBy(10, 10, true);
     *      scroller.scrollBy([10, 10], true);
     *      scroller.scrollBy({ x: 10, y: 10 }, true);
     *
     * A null value for either `x` or `y` will result in no scrolling on the given axis,
     * for example:
     *
     *     scroller.scrollBy(null, 10);
     *
     * will scroll by 10 on the y axis and leave the x axis at its current scroll position
     *
     * @param {Number/Number[]/Object} deltaX Either the x delta, an Array specifying x
     * and y deltas or an object with "x" and "y" properties.
     * @param {Number/Boolean/Object} deltaY Either the y delta, or an animate flag or
     * config object.
     * @param {Boolean/Object} animate Animate flag/config object if the delta values were
     * passed separately.
     * @return {Ext.Promise} A promise for when the scroll completes.
     */
    scrollBy: function(deltaX, deltaY, animate) {
        var position = this.getPosition();
 
        if (deltaX) {
            if (deltaX.length) { // array
                animate = deltaY;
                deltaY = deltaX[1];
                deltaX = deltaX[0];
            } 
            else if (typeof deltaX !== 'number') { // object
                animate = deltaY;
                deltaY = deltaX.y;
                deltaX = deltaX.x;
            }
        }
 
        deltaX = (typeof deltaX === 'number') ? deltaX + position.x : null;
        deltaY = (typeof deltaY === 'number') ? deltaY + position.y : null;
 
        return this.doScrollTo(deltaX, deltaY, animate);
    },
 
    /**
     * Ensures a descendant element of the scroller is visible by scrolling to it.
     *
     * @param {String/HTMLElement/Ext.dom.Element/Object} el
     * The descendant element to scroll into view. May also be the options object with
     * the `element` key defining the descendant element.
     *
     * @param {Object} [options] An object containing options to modify the operation.
     *
     * @param {Object} [options.align] The alignment for the scroll.
     * @param {'start'/'center'/'end'} [options.align.x] The alignment of the x scroll. If not
     * specified, the minimum will be done to make the element visible. The behavior is `undefined`
     * if the request cannot be honored. If the alignment is suffixed with a `?`, the alignment will
     * only take place if the item is not already in the visible area.
     * @param {'start'/'center'/'end'} [options.align.y] The alignment of the y scroll. If not
     * specified, the minimum will be done to make the element visible. The behavior is `undefined`
     * if the request cannot be honored. If the alignment is suffixed with a `?`, the alignment will
     * only take place if the item is not already in the visible area.
     *
     * @param {Boolean} [options.animation] Pass `true` to animate the row into view.
     *
     * @param {Boolean} [options.highlight=false] Pass `true` to highlight the row with a glow
     * animation when it is in view. Can also be a hex color to use for highlighting
     * (defaults to yellow = '#ffff9c').
     *
     * @param {Boolean} [options.x=true] `false` to disable horizontal scroll.
     * @param {Boolean} [options.y=true] `false` to disable vertical scroll.
     *
     * @return {Ext.Promise} A promise for when the scroll completes.
     * @since 6.5.1
     */
    ensureVisible: function(el, options) {
        var me = this,
            position = me.getPosition(),
            highlight, newPosition, ret;
 
        // Might get called before Component#onBoxReady which is when the Scroller is set up with elements.
        if (el) {
            if (el && el.element && !el.isElement) {
                options = el;
                el = options.element;
            }
 
            options = options || {};
 
            highlight = options.highlight;
            newPosition = me.getEnsureVisibleXY(el, options);
 
            // Only attempt to scroll if it's needed.
            if (newPosition.y !== position.y || newPosition.x !== position.x) {
                if (highlight) {
                    me.on({
                        scrollend: 'doHighlight',
                        scope: me,
                        single: true,
                        args: [el, highlight]
                    });
                }
 
                ret = me.doScrollTo(newPosition.x, newPosition.y, options.animation);
            } 
            else {
                // No scrolling needed, but still honor highlight request
                if (highlight) {
                    me.doHighlight(el, highlight);
                }
 
                // Resolve straight away
                ret = Ext.Deferred.getCachedResolved();
            }
        } 
        else {
            // Can't scroll
            ret = Ext.Deferred.getCachedRejected();
        }
 
        return ret;
    },
 
    /**
     * Scrolls a descendant element of the scroller into view.
     * @param {String/HTMLElement/Ext.dom.Element} el the descendant to scroll into view
     * @param {Boolean} [hscroll=true] False to disable horizontal scroll.
     * @param {Boolean/Object} [animate] true for the default animation or a standard Element
     * animation config object
     * @param {Boolean/String} [highlight=false] true to
     * {@link Ext.dom.Element#highlight} the element when it is in view. Can also be a
     * hex color to use for highlighting (defaults to yellow = '#ffff9c').
     *
     * @deprecated 6.5.1 Use {@link #ensureVisible} instead.
     * @return {Ext.Promise} A promise for when the scroll completes.
     */
    scrollIntoView: function(el, hscroll, animate, highlight) {
        return this.ensureVisible(el, {
            animation: animate,
            highlight: highlight,
            x: hscroll
        });
    },
 
    /**
     * Determines if the passed element is within the visible x and y scroll viewport.
     * @param {String/HTMLElement/Ext.dom.Element} el The dom node, Ext.dom.Element, or 
     * id (string) of the dom element that is to be verified to be in view
     * @return {Object} Which ranges the element is in.
     * @return {Boolean} return.x `true` if the passed element is within the x visible range.
     * @return {Boolean} return.y `true` if the passed element is within the y visible range.
     */
    isInView: function(el) {
        return this.doIsInView(el);
    },
 
    /**
     * Scrolls to the given position.
     *
     * All of the following are equivalent:
     *
     *      scroller.scrollTo(10, 10, true);
     *      scroller.scrollTo([10, 10], true);
     *      scroller.scrollTo({ x: 10, y: 10 }, true);
     *
     * A null value for either `x` or `y` will result in no scrolling on the given axis,
     * for example:
     *
     *     scroller.scrollTo(null, 10);
     *
     * will scroll to 10 on the y axis and leave the x axis at its current scroll position
     *
     * A negative value for either `x` or `y` represents an offset from the maximum scroll
     * position on the given axis:
     *
     *     // scrolls to 10px from the maximum x scroll position and 20px from maximum y
     *     scroller.scrollTo(-10, -20);
     *
     * A value of Infinity on either axis will scroll to the maximum scroll position on
     * that axis:
     *
     *     // scrolls to the maximum position on both axes
     *     scroller.scrollTo(Infinity, Infinity);
     *
     * @param {Number} x The scroll position on the x axis.
     * @param {Number} y The scroll position on the y axis.
     * @param {Boolean/Object} [animation] Whether or not to animate the scrolling to the new position.
     *
     * @return {Ext.Promise} A promise for when the scroll completes.
     */
    scrollTo: function(x, y, animation) {
        var maxPosition;
 
        if (x) {
            if (x.length) { // array
                animation = y;
                y = x[1];
                x = x[0];
            } 
            else if (typeof x !== 'number') { // object
                animation = y;
                y = x.y;
                x = x.x;
            }
        }
 
        if (< 0 || y < 0) {
            maxPosition = this.getMaxPosition();
 
            if (< 0) {
                x += maxPosition.x;
            }
            if (< 0) {
                y += maxPosition.y;
            }
        }
 
        return this.doScrollTo(x, y, animation);
    },
 
    updateDirection: function(direction) {
        var me = this,
            x, y;
 
        if (!direction) {
            // if no direction was configured we set its value based on the values of
            // x and y.  This ensures getDirection() always returns something useful
            // for backward compatibility.
            x = me.getX();
            y = me.getY();
            if (&& y) {
                direction = (=== 'scroll' && x === 'scroll') ? 'both' : 'auto';
            } 
            else if (y) {
                direction = 'vertical';
            } 
            else if (x) {
                direction = 'horizontal';
            }
            // set the _direction property directly to avoid the updater being called
            // and triggering setX/setY calls
            me._direction = direction;
        } 
        else {
            if (direction === 'auto') {
                x = true;
                y = true;
            } 
            else if (direction === 'vertical') {
                x = false;
                y = true;
            } 
            else if (direction === 'horizontal') {
                x = true;
                y = false;
            } 
            else if (direction === 'both') {
                x = 'scroll';
                y = 'scroll';
            }
 
            me.setX(x);
            me.setY(y);
        }
    },
 
    updateScrollbars: function(scrollbars, oldScrollbars) {
        this.syncScrollbarCls();
    },
 
    updateSize: function(size) {
        var me = this,
            element = me.getElement(),
            x = size.x,
            y = size.y,
            spacer;
 
        if (element) {
            me.positionDirty = true;
            spacer = me.getSpacer();
 
            // Typically a dom scroller simply assumes the scroll size dictated by its content.
            // In some cases, however, it is necessary to be able to manipulate this scroll size
            // (infinite lists for example).  This method positions a 1x1 px spacer element
            // within the scroller element to set a specific scroll size.
            if (!&& !y) {
                spacer.hide();
            } 
            else {
                // Subtract spacer size from coordinates (spacer is always 1x1 px in size)
                if (> 0) {
                    x -= 1;
                }
                if (> 0) {
                    y -= 1;
                }
 
                me.setSpacerXY({
                    x: x,
                    y: y
                });
                spacer.show();
            }
        }
    },
 
    updateMsSnapInterval: function() {
        this.initMsSnapInterval();
    },
 
    updateSnapSelector: function() {
        this.initSnap();
    },
 
    updateSnapOffset: function() {
        this.initSnap();
    },
 
    updateTouchAction: function(touchAction) {
        var element = this.getElement();
 
        if (element) {
            element.setTouchAction(touchAction);
        }
    },
 
    updateElement: function(element, oldElement) {
        var me = this,
            touchAction = me.getTouchAction(),
            scrollListener = me.scrollListener,
            elementCls = me.elementCls,
            eventSource, scrollEl;
 
        // If we have a scrollListener, we also have a scrollElement
        if (scrollListener) {
            scrollListener.destroy();
            me.scrollListener = null;
            me.setScrollElement(null);
        }
 
        if (oldElement && !oldElement.destroyed) {
            // TODO: might be nice to have x-scroller-foo classes to map overflow styling
            oldElement.setStyle('overflow', 'hidden');
            oldElement.removeCls(elementCls);
        }
 
        if (element) {
            if (element.dom === document.documentElement || element.dom === document.body) {
                // When the documentElement or body is scrolled, its scroll events are
                // fired via the window object
                eventSource = Ext.getWin();
                scrollEl = Ext.scroll.Scroller.getScrollingElement();
            } 
            else {
                scrollEl = eventSource = element;
            }
            me.setScrollElement(Ext.get(scrollEl));
            me.scrollListener = eventSource.on({
                scroll: me.onDomScroll,
                scope: me,
                destroyable: true
            });
 
            if (touchAction) {
                element.setTouchAction(touchAction);
            }
 
            me.initXStyle();
            me.initYStyle();
            element.addCls(elementCls);
            me.initSnap();
            me.initMsSnapInterval();
            me.syncScrollbarCls();
        }
    },
 
    updateX: function(x) {
        this.initXStyle();
    },
 
    updateY: function(y) {
        this.initYStyle();
    },
 
    deprecated: {
        '5': {
            methods: {
                /**
                 * @method getScroller
                 * Returns this scroller.
                 *
                 * In Sencha Touch 2, access to a Component's Scroller was provided via
                 * a Ext.scroll.View class that was returned from the Component's getScrollable()
                 * method:
                 *
                 *     component.getScrollable().getScroller();
                 *
                 * in 5.0 all the functionality of Ext.scroll.View has been rolled into
                 * Ext.scroll.Scroller, and Ext.scroll.View has been removed.  Component's
                 * getScrollable() method now returns a Ext.scroll.Scroller.  This method is
                 * provided for compatibility.
                 * @deprecated 5.0 This method is deprecated.  Please use Ext.scroll.Scroller's
                 * getScrollable() method instead.
                 */
                getScroller: function() {
                    return this;
                }
            }
        },
        '5.1.0': {
            methods: {
                /**
                 * @method scrollToTop
                 * Scrolls to 0 on both axes
                 * @param {Boolean/Object} animate
                 * @private
                 * @return {Ext.scroll.Scroller} this
                 * @chainable
                 * @deprecated 5.1.0 Use scrollTo instead
                 */
                scrollToTop: function(animate) {
                    return this.scrollTo(0, 0, animate);
                },
 
                /**
                 * @method scrollToEnd
                 * Scrolls to the maximum position on both axes
                 * @param {Boolean/Object} animate
                 * @private
                 * @return {Ext.scroll.Scroller} this
                 * @chainable
                 * @deprecated 5.1.0 Use scrollTo instead
                 */
                scrollToEnd: function(animate) {
                    return this.scrollTo(Infinity, Infinity, animate);
                }
            }
        }
    },
 
    privates: {
        axisConfigs: {
            x: {
                x: true
            },
            y: {
                y: true
            },
            both: {
                x: true,
                y: true
            }
        },
 
        /**
         * @private
         * Gets the x/y coordinates to ensure the element is scrolled into view.
         *
         * @param {String/HTMLElement/Ext.dom.Element/Object} el
         * The descendant element to scroll into view. May also be the options object with
         * the `element` key defining the descendant element.
         *
         * @param {Object} [options] An object containing options to modify the operation.
         *
         * @param {Object/String} [options.align] The alignment for the scroll. If a string, this value
         * will be used for both `x` and `y` alignments.
         * @param {'start'/'center'/'end'} [options.align.x] The alignment of the x scroll. If not
         * specified, the minimum will be done to make the element visible. The behavior is `undefined`
         * if the request cannot be honored. If the alignment is suffixed with a `?`, the alignment will
         * only take place if the item is not already in the visible area.
         * @param {'start'/'center'/'end'} [options.align.y] The alignment of the y scroll. If not
         * specified, the minimum will be done to make the element visible. The behavior is `undefined`
         * if the request cannot be honored. If the alignment is suffixed with a `?`, the alignment will
         * only take place if the item is not already in the visible area.
         *
         * @param {Boolean} [options.x=true] `false` to disable horizontal scroll and `x` align option.
         * @param {Boolean} [options.y=true] `false` to disable vertical scroll and `y` align option.
         * @return {Object} The new position that will be used to scroll the element into view.
         * @since 6.5.1
         */
        getEnsureVisibleXY: function(el, options) {
            var position = this.getPosition(),
                viewport = this.component ? this.component.getScrollableClientRegion() : this.getElement(),
                newPosition, align;
 
            if (el && el.element && !el.isElement) {
                options = el;
                el = options.element;
            }
 
            options = options || {};
            align = options.align;
 
            if (align) {
                if (Ext.isString(align)) {
                    align = {
                        x: options.x === false ? null : align,
                        y: options.y === false ? null : align
                    };
                } 
                else if (Ext.isObject(align)) {
                    if (align.x && options.x === false) {
                        align.x = null;
                    }
 
                    if (align.y && options.y === false) {
                        align.y = null;
                    }
                }
            }
 
            newPosition = Ext.fly(el).getScrollIntoViewXY(viewport, position.x, position.y, align);
 
            newPosition.x = options.x === false ? position.x : newPosition.x;
            newPosition.y = options.y === false ? position.y : newPosition.y;
 
            return newPosition;
        },
 
        getSpacer: function() {
            var me = this,
                spacer = me._spacer,
                element;
 
            // In some cases (e.g. infinite lists) we need to be able to tell the scroller
            // to have a specific size, regardless of its contents.  This creates a spacer
            // element which can then be absolutely positioned to affect the element's
            // scroll size. Must be first element, so it is not translated due to being after
            // the element contrainer el.
            if (!spacer) {
                element = me.getElement();
                spacer = me._spacer = element.createChild({
                    cls: me.spacerCls,
                    role: 'presentation'
                }, element.dom.firstChild);
 
                spacer.setVisibilityMode(2); // 'display' visibilityMode
                spacer.hide();
 
                // make sure the element is positioned if it is not already.  This ensures
                // that the spacer's position will affect the element's scroll size
                element.position();
            }
 
            return spacer;
        },
 
        applySpacerXY: function(pos, oldPos) {
            // Opt out if we have the same value
            if (oldPos && pos.x === oldPos.x && pos.y === oldPos.y) {
                pos = undefined;
            }
            return pos;
        },
 
        updateSpacerXY: function(pos) {
            var me = this,
                spacer = me.getSpacer(),
                sStyle = spacer.dom.style,
                scrollHeight = pos.y,
                shortfall;
 
            sStyle.marginTop = '';
            me.translateSpacer(pos.x, me.constrainScrollRange(scrollHeight));
 
            // Force a synchronous layout to update the scrollHeight.
            // This flip-flops between 0px and 1px
            sStyle.lineHeight = Number(!parseInt(sStyle.lineHeight, 10)) + 'px';
 
            // See if we can get any more scrollHeight from a margin-top
            if (scrollHeight > 1000000) {
                shortfall = scrollHeight - me.getElement().dom.scrollHeight;
                if (shortfall > 0) {
                    sStyle.marginTop = Math.min(shortfall, me.maxSpacerMargin || 0) + 'px';
                }
            }
        },
 
        // rtl hook - rtl version sets right style
        translateSpacer: function(x, y) {
            this.getSpacer().translate(x, y);
        },
 
        doIsInView: function(el, skipCheck) {
            var me = this,
                c = me.component,
                result = {
                    x: false,
                    y: false
                },
                elRegion,
                myEl = me.getElement(),
                myElRegion;
 
            if (el && (skipCheck || (myEl.contains(el) || (&& c.owns(el))))) {
                myElRegion = myEl.getRegion();
                elRegion = Ext.fly(el).getRegion();
 
                result.x = elRegion.right > myElRegion.left && elRegion.left < myElRegion.right;
                result.y = elRegion.bottom > myElRegion.top && elRegion.top < myElRegion.bottom;
            }
            return result;
        },
 
        // Checks if the scroller contains a component by searching up the element hierarchy
        // using components. It uses component navigation as opposed to elements because we
        // want logical ownership.
        contains: function(component) {
            var el = this.getElement(),
                owner = component;
 
            while (owner && owner !== Ext.Viewport) {
                if (el.contains(owner.el)) {
                    return true;
                }
                owner = owner.getRefOwner();
            }
            return false;
        },
 
        constrainScrollRange: function(scrollRange) {
            var maxScrollHeight, tooHigh, tooLow, scrollTest, stretcher, sStyle;
            // Only do the expensive search for the browser limit if they
            // want more than a million pixels.
            if (scrollRange < 1000000) {
                return scrollRange;
            }
 
            if (!this.maxSpacerTranslate) {
                //
                // Find max scroll height which transform: translateY(npx) will support.
                // IE11 appears to have 21,474,834
                // Chrome and Safari have 16,777,216, but additional margin-top of 16777215px allows a scrollHeight of 33,554,431
                // Firefox has 17,895,698
                // IE9-10 1,534,000
                //
                maxScrollHeight = Math.pow(2, 32);
                tooHigh = maxScrollHeight;
                tooLow = 500;
                scrollTest = Ext.getBody().createChild({
                    style: {
                        position: 'absolute',
                        left: '-10000px',
                        top: '0',
                        width: '500px',
                        height: '500px'
                    },
                    cn: {
                        cls: this.spacerCls
                    }
                }, null, true);
                stretcher = Ext.get(scrollTest.firstChild);
                sStyle = stretcher.dom.style;
 
                stretcher.translate(0, maxScrollHeight - 1);
                sStyle.lineHeight = Number(!parseInt(sStyle.lineHeight, 10)) + 'px';
 
                // See what the max translateY is which still stretches the scrollHeight
                while (tooHigh !== tooLow + 1) {
                    stretcher.translate(0, (maxScrollHeight = tooLow + Math.floor((tooHigh - tooLow) / 2)));
 
                    // Force a synchronous layout to update the scrollHeight.
                    // This flip-flops between 0px and 1px
                    sStyle.lineHeight = Number(!parseInt(sStyle.lineHeight, 10)) + 'px';
 
                    if (scrollTest.scrollHeight < maxScrollHeight) {
                        tooHigh = maxScrollHeight;
                    } 
                    else {
                        tooLow = maxScrollHeight;
                    }
                }
                stretcher.translate(0, Ext.scroll.Scroller.prototype.maxSpacerTranslate = tooLow);
 
                // Go through the same steps seeing how far we can push it with margin-top
                tooHigh = tooLow * 2;
                while (tooHigh !== tooLow + 1) {
                    stretcher.dom.style.marginTop = ((maxScrollHeight = tooLow + Math.floor((tooHigh - tooLow) / 2))) + 'px';
 
                    // Force a synchronous layout to update the scrollHeight.
                    // This flip-flops between 0px and 1px
                    sStyle.lineHeight = Number(!parseInt(sStyle.lineHeight, 10)) + 'px';
 
                    if (scrollTest.scrollHeight < maxScrollHeight) {
                        tooHigh = maxScrollHeight;
                    } 
                    else {
                        tooLow = maxScrollHeight;
                    }
                }
                Ext.fly(scrollTest).destroy();
 
                Ext.scroll.Scroller.prototype.maxSpacerMargin = tooLow - Ext.scroll.Scroller.prototype.maxSpacerTranslate;
            }
 
            // The maximum a translateY transform can be pushed to stretch the scrollHeight before
            // it collapses back to offsetHeight
            return Math.min(scrollRange, this.maxSpacerTranslate);
        },
 
        // hook for rtl mode to convert an x coordinate to RTL space.
        convertX: function(x) {
            return x;
        },
 
        // highlights an element after it has been scrolled into view
        doHighlight: function(el, highlight) {
            if (highlight !== true) { // handle hex color
                Ext.fly(el).highlight(highlight);
            } 
            else {
                Ext.fly(el).highlight();
            }
        },
 
        doScrollTo: function(x, y, animate) {
            // There is an IE8 override of this method; when making changes here
            // don't forget to update the override as well
            var me = this,
                element = me.getScrollElement(),
                maxPosition, dom, xInf, yInf,
                ret, translatable, deferred;
 
            if (element && !element.destroyed) {
                dom = element.dom;
 
                xInf = (=== Infinity);
                yInf = (=== Infinity);
 
                if (xInf || yInf) {
                    maxPosition = me.getMaxPosition();
                    if (xInf) {
                        x = maxPosition.x;
                    }
                    if (yInf) {
                        y = maxPosition.y;
                    }
                }
 
                if (!== null) {
                    x = me.convertX(x);
                }
 
                if (animate) {
                    translatable = me.translatable;
                    
                    if (!translatable) {
                        me.translatable = translatable = new Ext.util.translatable.ScrollPosition({ element: element });
                    }
 
                    deferred = new Ext.Deferred();
                    
                    // Use onFrame here to let the scroll complete and animations to fire.
                    translatable.on('animationend', function() {
                        // Check destroyed vs destroying since we're onFrame here
                        if (me.destroyed) {
                            deferred.reject();
                        }
                        else {
                            deferred.resolve();
                        }
                    }, Ext.global, { single: true, onFrame: true });
                    
                    translatable.translate(x, y, animate);
                    
                    ret = deferred.promise;
                }
                else {
                    if (!= null) {
                        dom.scrollTop = y;
                    }
                    if (!= null) {
                        dom.scrollLeft = x;
                    }
                    
                    ret = Ext.Deferred.getCachedResolved();
                }
 
                // Our position object will need refreshing before returning.
                me.positionDirty = true;
            }
            else {
                ret = Ext.Deferred.getCachedRejected();
            }
            
            return ret;
        },
 
        fireScrollStart: function(x, y, xDelta, yDelta) {
            var me = this,
                component = me.component;
 
            me.invokePartners('onPartnerScrollStart', x, y, xDelta, yDelta);
 
            me.startX = x - xDelta;
            me.startY = y - yDelta;
 
            if (me.hasListeners.scrollstart) {
                me.fireEvent('scrollstart', me, x, y);
            }
 
            if (component && component.onScrollStart) {
                component.onScrollStart(x, y);
            }
 
            Ext.GlobalEvents.fireEvent('scrollstart', me, x, y);
        },
 
        fireScroll: function(x, y, xDelta, yDelta) {
            var me = this,
                component = me.component;
 
            me.invokePartners('onPartnerScroll', x, y, xDelta, yDelta);
 
            if (me.hasListeners.scroll) {
                me.fireEvent('scroll', me, x, y, xDelta, yDelta);
            }
 
            if (component && component.onScrollMove) {
                component.onScrollMove(x, y);
            }
 
            Ext.GlobalEvents.fireEvent('scroll', me, x, y, xDelta, yDelta);
        },
 
        fireScrollEnd: function(x, y, xDelta, yDelta) {
            var me = this,
                component = me.component,
                dx = x - me.startX,
                dy = y - me.startY;
 
            me.startX = me.startY = null;
 
            me.invokePartners('onPartnerScrollEnd', x, y, xDelta, yDelta);
 
            if (me.hasListeners.scrollend) {
                me.fireEvent('scrollend', me, x, y, dx, dy);
            }
 
            if (component && component.onScrollEnd) {
                component.onScrollEnd(x, y);
            }
 
            Ext.GlobalEvents.fireEvent('scrollend', me, x, y, dx, dy);
        },
 
        // rtl hook
        getElementScroll: function(element) {
            return element.getScroll();
        },
 
        initSnap: function() {
            var me = this,
                snapOffset = me.getSnapOffset(),
                snapSelector = me.getSnapSelector(),
                element = me.getElement(),
                offsetX, offsetY, snapCoordinate;
 
            if (element && snapSelector) {
                element.addCls(me.snappableCls);
 
                me.removeSnapStylesheet();
 
                if (snapOffset) {
                    offsetX = snapOffset.x || 0;
                    offsetY = snapOffset.y || 0;
 
                    if (offsetX) {
                        offsetX = -offsetX + 'px';
                    }
 
                    if (offsetY) {
                        offsetY = -offsetY + 'px';
                    }
                }
 
                snapCoordinate = offsetX + ' ' + offsetY + ';';
 
                me.snapStylesheet = Ext.util.CSS.createStyleSheet(
                    '#' + element.id + ' ' + snapSelector +
                    '{-webkit-scroll-snap-coordinate:' + snapCoordinate +
                    'scroll-snap-coordinate:' + snapCoordinate + '}'
                );
            }
        },
 
        initMsSnapInterval: function() {
            var element = this.getElement(),
                interval, x, y, style;
 
            if (element) {
                interval = this.getMsSnapInterval();
 
                if (interval) {
                    x = interval.x;
                    y = interval.y;
                    style = element.dom.style;
 
                    if (x) {
                        style['-ms-scroll-snap-points-x'] = 'snapInterval(0px, ' + x + 'px)';
                    }
 
                    if (y) {
                        style['-ms-scroll-snap-points-y'] = 'snapInterval(0px, ' + y + 'px)';
                    }
                }
            }
        },
 
        initXStyle: function() {
            var element = this.getElement(),
                x = this.getX();
 
            // Check that element exists and is not destroyed
            if (element && element.dom) {
                if (!x) {
                    x = 'hidden';
                } 
                else if (=== true) {
                    x = 'auto';
                }
 
                element.setStyle('overflow-x', x);
            }
        },
 
        initYStyle: function() {
            var element = this.getElement(),
                y = this.getY();
 
            // Check that element exists and is not destroyed
            if (element && element.dom) {
                if (!y) {
                    y = 'hidden';
                } 
                else if (=== true) {
                    y = 'auto';
                }
 
                element.setStyle('overflow-y', y);
            }
        },
 
        invokePartners: function(method, x, y, xDelta, yDelta) {
            var me = this,
                partners = me._partners,
                partner,
                id, axes;
 
            if (!me.suspendSync) {
                me.invokingPartners = true;
                for (id in partners) {
                    axes = partners[id].axes;
                    partner = partners[id].scroller;
 
                    // Only pass the scroll on to partners if we are are configured to pass on the scrolled dimension
                    if (!partner.invokingPartners && (xDelta && axes.x || yDelta && axes.y)) {
                        partner[method](me, axes.x ? x : null, axes.y ? y : null, xDelta, yDelta);
                    }
                }
                me.invokingPartners = false;
            }
        },
 
        suspendPartnerSync: function() {
            this.suspendSync = (this.suspendSync || 0) + 1;
        },
 
        resumePartnerSync: function(syncNow) {
            var me = this,
                position;
 
            if (me.suspendSync) {
                me.suspendSync--;
            }
            if (!me.suspendSync && syncNow) {
                position = me.getPosition();
                me.invokePartners('onPartnerScroll', position.x, position.y);
                me.invokePartners('onPartnerScrollEnd', position.x, position.y);
            }
        },
 
        readPosition: function(position) {
            var me = this,
                element = me.getScrollElement(),
                elScroll;
 
            position = position || {};
 
            if (element && !element.destroyed) {
                elScroll = me.getElementScroll(element);
 
                position.x = elScroll.left;
                position.y = elScroll.top;
            }
 
            return position;
        },
 
        updateDomScrollPosition: function(silent) {
            var me = this,
                position = me.position,
                oldX = position.x,
                oldY = position.y,
                x, y, xDelta, yDelta;
 
            me.readPosition(position);
 
            x = position.x;
            y = position.y;
 
            me.positionDirty = false;
 
            if (!silent) {
                xDelta = x - oldX;
                yDelta = y - oldY;
 
                // If we already know about the position. then we've been coerced there by a partner
                // and that will have been firing our event sequence synchronously, so they do not
                // not need to be fire in response to the ensuing scroll event.
 
                if (xDelta || yDelta) {
                    if (!me.isScrolling) {
                        me.isScrolling = Ext.isScrolling = true;
                        me.fireScrollStart(x, y, xDelta, yDelta);
                    }
 
                    me.fireScroll(x, y, xDelta, yDelta);
 
                    me.onDomScrollEnd(x, y, xDelta, yDelta);
                }
            }
 
            return position;
        },
 
        /**
         * @private
         * May be called when a Component is rendererd AFTER some scrolling partner has begun its lifecycle to sync
         * this scroller with partners which may be scrolled anywhere by now.
         */
        syncWithPartners: function() {
            var me = this,
                partners = me._partners,
                id, partner, position;
 
            me.suspendPartnerSync();
            for (id in partners) {
                partner = partners[id].scroller;
                position = partner.getPosition();
                me.onPartnerScroll(partner, position.x, position.y);
            }
            me.resumePartnerSync();
        },
 
        syncScrollbarCls: function() {
            var element = this.getElement();
 
            if (element) {
                element.toggleCls(this.noScrollbarsCls, this.getScrollbars() === false);
            }
        },
 
        onDomScroll: function() {
            var hasTimer = !!this.restoreTimer;
 
            this.updateDomScrollPosition(hasTimer);
 
            if (hasTimer) {
                Ext.undefer(this.onDomScrollEnd.timer);
                return;
            }
        },
 
        doOnDomScrollEnd: function(x, y, xDelta, yDelta) {
            var me = this;
 
            // Could be destroyed by this time
            if (me.destroying || me.destroyed) {
                return;
            }
 
            me.isScrolling = Ext.isScrolling = false;
 
            // if this is being flushed we only need to set the scrolling status to false
            if (=== undefined) {
                return;
            }
            me.trackingScrollLeft = x;
            me.trackingScrollTop = y;
            me.fireScrollEnd(x, y, xDelta, yDelta);
        },
 
        onPartnerScrollStart: function(partner, x, y, xDelta, yDelta) {
            // Pass the signal on immediately to all partners.
            this.isScrolling = true;
            this.fireScrollStart(x, y, xDelta, yDelta);
        },
 
        onPartnerScroll: function(partner, x, y, xDelta, yDelta) {
            this.doScrollTo(x, y, false);
 
            // Update the known scroll position so that when it reacts to its DOM,
            // it will not register a change and so will not invoke partners.
            // All scroll intentions are propagated synchronously.
            // The ensuing multiple scroll events are then ignored.
            this.updateDomScrollPosition(true);
 
            // Pass the signal on immediately to all partners.
            this.fireScroll(x, y, xDelta, yDelta);
        },
 
        onPartnerScrollEnd: function(partner, x, y, xDelta, yDelta) {
            // manually clearing the buffer queue before calling the method without buffering
            this.cancelOnDomScrollEnd();
            // Pass the signal on immediately to all partners.
            // We are called by the onDomScrollEnd of our controller
            // so we must not add another delay and call doOnScrollEnd directly.
            this.doOnDomScrollEnd(x, y, xDelta, yDelta);
        },
 
        removeSnapStylesheet: function() {
            var stylesheet = this.snapStylesheet;
 
            if (stylesheet) {
                Ext.util.CSS.removeStyleSheet(stylesheet);
                this.snapStylesheet = null;
            }
        },
 
        restoreState: function() {
            var me = this,
                el = me.getScrollElement();
 
            if (el) {
 
                // Only restore state if has been previously captured! For example,
                // floaters probably have not been hidden before initially shown.
                if (me.trackingScrollTop !== undefined) {
                    // If we're restoring the scroll position, we don't want to publish
                    // scroll events since the scroll position should not have changed
                    // at all as far as the user is concerned, so just do it silently
                    // while ensuring we maintain the correct internal state. 50ms is
                    // enough to capture the async scroll events, anything after that
                    // we re-enable.
                    if (!me.restoreTimer) {
                        me.restoreTimer = Ext.defer(function() {
                            me.restoreTimer = null;
                        }, 50);
                    }
                    me.doScrollTo(me.trackingScrollLeft, me.trackingScrollTop, false);
 
                    // Do not discard the state.
                    // It may need to be restored again.
                }
            }
        }
    }
}, function(Scroller) {
    /**
     * @private
     * @return {Ext.scroll.Scroller} 
     */
    Ext.getViewportScroller = function() {
        // This method creates the global viewport scroller.  This scroller instance must
        // always exist regardless of whether or not there is a Viewport component in use
        // so that global scroll events will still fire.  Menus and some other floating
        // things use these scroll events to hide themselves.
        var scroller = Scroller.viewport;
        if (!scroller) {
            Scroller.viewport = scroller = new Scroller();
            Scroller.initViewportScroller();
        }
        return scroller;
    };
 
    /**
     * @private
     * @param {Ext.scroll.Scroller} scroller 
     */
    Ext.setViewportScroller = function(scroller) {
        if (Scroller.viewport !== scroller) {
            Ext.destroy(Scroller.viewport);
            if (scroller && !scroller.isScroller) {
                scroller = new Scroller(scroller);
            }
            Scroller.viewport = scroller;
        }
    };
 
    Ext.onReady(function() {
        // The viewport scroller must always exist, but it is deferred so that the
        // viewport component has a chance to call Ext.setViewportScroller() with
        // its own scroller first.
        // We assign the timer to a property to cancel the call while setting up
        // for unit tests. We will call initViewportScroller without waiting for the
        // Viewport to initialize.
        Scroller.initViewportScrollerTimer = Ext.defer(Scroller.initViewportScroller, 100);
    });
});