/**
 * 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',
 
    uses: [
        'Ext.scroll.NativeScroller',
        'Ext.scroll.VirtualScroller'
    ],
 
    mixins: [
        'Ext.mixin.Factoryable',
        'Ext.mixin.Bufferable'
    ],
 
    isScroller: true,
 
    factoryConfig: {
        defaultType: 'native'
    },
 
    bufferableMethods: {
        onScrollEnd: 100
    },
 
    /**
     * @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 since the last scrollstart event
     * @param {Number} deltaY The change in y value since the last scrollstart event
     */
 
    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: null,
 
        /**
         * @cfg {Object} 
         * The size of the scrollable content expressed as an object with x and y properties
         * @private
         * @readonly
         */
        size: 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 {Object} touchAction for the scroller's {@link #element}.
         *
         * For more details see {@link Ext.dom.Element#setTouchAction}
         */
        touchAction: null,
 
        /**
         * @private
         */
        translatable: 'scrollposition'
    },
 
    constructor: function(config) {
        var me = this;
 
        me.position = { x: 0, y: 0 };
 
        me.configuredSize = { x: false, y: false };
 
        me.callParent([config]);
    },
 
    destroy: function() {
        var me = this;
 
        me.destroying = true;
        me.destroy = Ext.emptyFn;
 
        me.doDestroy();
 
        me.callParent();
 
        // This just makes it hard to ask "was destroy() called?":
        // me.destroying = false; // removed in 7.0
    },
 
    doDestroy: function() {
        var me = this,
            partners, id;
 
        me.component = null;
 
        partners = me._partners;
 
        if (partners) {
            for (id in partners) {
                me.removePartner(partners[id].scroller);
            }
        }
 
        me._partners = null;
 
        me.setElement(null);
        me.setTranslatable(null);
    },
 
    /**
     * 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,
            called: false,
            calledPrimary: false
        };
 
        otherPartners[me.getId()] = {
            scroller: me,
            axes: axis,
            called: false,
            calledPrimary: false
        };
    },
 
    // hook for rtl mode to convert an x coordinate to RTL space.
    convertX: function(x) {
        return x;
    },
 
    /**
     * Ensures a descendant element of the scroller is visible by scrolling to it.
     *
     * @param {Object/String/HTMLElement/Ext.dom.Element} 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;
    },
 
    /**
     * @method getClientSize
     * Gets the `clientWidth` and `clientHeight` of the {@link #element} for this scroller.
     * @return {Object} An object with `x` and `y` properties.
     */
 
    /**
     * @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
     */
 
    /**
     * @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
     */
 
    /**
     * @method getPosition
     * Returns the current scroll position
     * @return {Object} An object with `x` and `y` properties.
     */
    getPosition: function() {
        return this.position;
    },
 
    /**
     * @method getScrollbarSize
     * 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.
     */
 
    /**
     * 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
     * @param {Boolean} [contains=true] `false` to skip checking if the scroller contains
     * the passed element in the dom.  When `false` the element is considered to be
     * "in view" if its location on the page is within the scroller's client region.
     * Passing `false` improves performance when the element is already known to be
     * contained by this scroller.
     * @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, contains) {
        var me = this,
            c = me.component,
            result = {
                x: false,
                y: false
            },
            myEl = me.getElement(),
            elRegion, myElRegion;
 
        if (el && (contains === false || 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;
    },
 
    /**
     * 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() {
        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);
    },
 
    /**
     * 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
        });
    },
 
    /**
     * 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 me = this,
            maxPosition;
 
        if (x) {
            if (Ext.isArray(x)) {
                animation = y;
                y = x[1];
                x = x[0];
            }
            else if (typeof x !== 'number') {
                animation = y;
                y = x.y;
                x = x.x;
            }
        }
 
        if (< 0 || y < 0) {
            maxPosition = me.getMaxPosition();
 
            if (< 0) {
                x += maxPosition.x;
            }
 
            if (< 0) {
                y += maxPosition.y;
            }
        }
 
        return me.doScrollTo(x, y, animation);
    },
 
    //-------------------------
    // Public Configs
 
    // element
 
    applyElement: function(element) {
        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>
 
            if (!element.isElement) {
                element = Ext.get(element);
            }
        }
 
        return element;
    },
 
    updateElement: function(element, oldElement) {
        var me = this,
            scrollerCls = me.scrollerCls,
            touchAction;
 
        if (oldElement && !oldElement.destroyed) {
            oldElement.removeCls(scrollerCls);
        }
 
        if (element) {
            if (!this.isConfiguring) {
                touchAction = this.getTouchAction();
 
                if (touchAction) {
                    element.setTouchAction(touchAction);
                }
            }
 
            element.addCls(scrollerCls);
        }
    },
 
    // direction
 
    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);
        }
    },
 
    // size
 
    applySize: function(size, oldSize) {
        var num = 'number',
            configuredSize = this.configuredSize,
            x, y;
 
        if (size == null || typeof size === num) {
            x = y = size;
        }
        else if (size) {
            x = size.x;
            y = size.y;
        }
 
        if (typeof x === num) {
            configuredSize.x = true;
        }
        else if (=== null) {
            configuredSize.x = false;
        }
 
        if (typeof y === num) {
            configuredSize.y = true;
        }
        else if (=== null) {
            configuredSize.y = false;
        }
 
        if (=== undefined) {
            x = (oldSize ? oldSize.x : null);
        }
 
        if (=== undefined) {
            y = (oldSize ? oldSize.y : null);
        }
 
        return (oldSize && x === oldSize.x && y === oldSize.y) ? oldSize : { x: x, y: y };
    },
 
    // touchAction
 
    updateTouchAction: function(touchAction) {
        var element = this.getElement();
 
        if (element) {
            element.setTouchAction(touchAction);
        }
    },
 
    //-----------------------------------------------------------------------
 
    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);
        },
 
        /**
         * @private
         */
        getScrollingElement: function() {
            return Ext.get(document.scrollingElement || document.documentElement);
        }
    },
 
    privates: {
        scrollerCls: Ext.baseCSSPrefix + 'scroller',
 
        axisConfigs: {
            x: { x: 1, y: 0 },
            y: { x: 0, y: 1 },
            both: { x: 1, y: 1 }
        },
 
        callPartners: function(method, scrollX, scrollY) {
            var me = this,
                partners = me._partners,
                axes, id, partner, pos, scroller, x, y;
 
            if (!me.suspendSync) {
                for (id in partners) {
                    partner = partners[id];
                    scroller = partner.scroller;
 
                    if (!scroller.isPrimary && !partner.called) {
                        partner.called = true; // this flag avoids infinite recursion
                        axes = partners[id].axes;
                        pos = scroller.position;
 
                        x = (!axes.x || scrollX === undefined) ? pos.x : scrollX;
                        y = (!axes.y || scrollY === undefined) ? pos.y : scrollY;
 
                        scroller[method](x, y, (- pos.x) || 0, (- pos.y) || 0);
 
                        scroller.callPartners(method, x, y);
 
                        partner.called = false;
                    }
                }
            }
        },
 
        /**
         * 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.
         * @private
         */
        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;
        },
 
        fireScrollStart: function(x, y, deltaX, deltaY) {
            var me = this,
                component = me.component;
 
            me.startX = x - deltaX;
            me.startY = y - deltaY;
 
            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, deltaX, deltaY) {
            var me = this,
                component = me.component;
 
            if (me.hasListeners.scroll) {
                me.fireEvent('scroll', me, x, y, deltaX, deltaY);
            }
 
            if (component && component.onScrollMove) {
                component.onScrollMove(x, y, deltaX, deltaY);
            }
 
            Ext.fireEvent('scroll', me, x, y, deltaX, deltaY);
        },
 
        fireScrollEnd: function(x, y) {
            var me = this,
                component = me.component,
                deltaX = x - me.startX,
                deltaY = y - me.startY;
 
            if (me.hasListeners.scrollend) {
                me.fireEvent('scrollend', me, x, y, deltaX, deltaY);
            }
 
            if (component && component.onScrollEnd) {
                component.onScrollEnd(x, y, deltaX, deltaY);
            }
 
            Ext.fireEvent('scrollend', me, x, y, deltaX, deltaY);
        },
 
        /**
         * @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;
        },
 
        onPartnerScroll: function(x, y) {
            this.doScrollTo(x, y);
        },
 
        onPartnerScrollStart: function(x, y, deltaX, deltaY) {
            this.isScrolling = true;
 
            if (deltaX || deltaY) {
                this.doScrollTo(x, y);
            }
        },
 
        onPartnerScrollEnd: function() {
            this.isScrolling = false;
        },
 
        /**
         * In IE8, IE9, and IE10 when using native scrolling the scroll position is reset
         * to 0,0 when the scrolling element is hidden.  This method may be called to restore
         * the scroll after hiding and showing the element.
         */
        restoreState: Ext.privateFn,
 
        resumePartnerSync: function(syncNow) {
            var me = this,
                position, x, y;
 
            if (me.suspendSync) {
                me.suspendSync--;
            }
 
            if (!me.suspendSync && syncNow) {
                position = me.position;
                x = position.x;
                y = position.y;
 
                me.callPartners('onPartnerScrollStart', undefined, undefined);
                me.callPartners('fireScrollStart', undefined, undefined);
 
                me.callPartners('onPartnerScroll', x, y);
                me.callPartners('fireScroll', x, y);
 
                me.callPartners('onPartnerScrollEnd');
                me.callPartners('fireScrollEnd', x, y);
            }
        },
 
        /**
         * Sets this scroller as the "primary" scroller in a partnership.  When true
         * sets a `isPrimary` property to true on the primary scroller and recursively sets
         * the same property to `false` on the partners
         * @param {Boolean} isPrimary 
         * @private
         */
        setPrimary: function(isPrimary) {
            var me = this,
                partners = me._partners,
                partner, scroller, id;
 
            if (isPrimary) {
                for (id in partners) {
                    partner = partners[id];
                    scroller = partner.scroller;
 
                    if (!partner.calledPrimary) {
                        partner.calledPrimary = true; // this flag avoids infinite recursion
                        scroller.setPrimary(false);
                        partner.calledPrimary = false;
                    }
                }
 
                me.isScrolling = Ext.isScrolling = true;
            }
            else if (me.isScrolling) {
                me.cancelOnScrollEnd();
                me.doOnScrollEnd();
            }
 
            me.isPrimary = isPrimary;
        },
 
        suspendPartnerSync: function() {
            this.suspendSync = (this.suspendSync || 0) + 1;
        },
 
        /**
         * @private
         * May be called when a Component is rendered 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,
                position = me.position,
                id, axes, xAxis, yAxis, partner, partnerPosition, x, y, deltaX, deltaY;
 
            for (id in partners) {
                axes = partners[id].axes;
                xAxis = axes.x;
                yAxis = axes.y;
 
                partner = partners[id].scroller;
                partnerPosition = partner.position;
 
                partner.setPrimary(true);
 
                x = xAxis ? position.x : null;
                y = yAxis ? position.y : null;
 
                deltaX = deltaY = 0;
 
                me.onPartnerScrollStart(x, y, 0, 0);
 
                me.fireScrollStart(x, y, 0, 0);
 
                x = xAxis ? partnerPosition.x : null;
                y = yAxis ? partnerPosition.y : null;
 
                deltaX = (=== null) ? 0 : (- position.x);
                deltaY = (=== null) ? 0 : (- position.y);
 
                me.onPartnerScroll(x, y);
                me.fireScroll(x, y, deltaX, deltaY);
 
                me.onPartnerScrollEnd();
                me.fireScrollEnd(x, y);
 
                partner.setPrimary(null);
            }
        },
 
        //-------------------------
        // Private Configs
 
        // translatable
 
        applyTranslatable: function(translatable, oldTranslatable) {
            return Ext.Factory.translatable.update(oldTranslatable, translatable, this,
                                                   'createTranslatable');
        }
    },
 
    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);
                }
            }
        }
    }
});