Version 3.17.2
Show:

File: scrollview/js/paginator-plugin.js

/**
 * Provides a plugin that adds pagination support to ScrollView instances
 *
 * @module scrollview-paginator
 */
var getClassName = Y.ClassNameManager.getClassName,
    SCROLLVIEW = 'scrollview',
    CLASS_HIDDEN = getClassName(SCROLLVIEW, 'hidden'),
    CLASS_PAGED = getClassName(SCROLLVIEW, 'paged'),
    UI = (Y.ScrollView) ? Y.ScrollView.UI_SRC : 'ui',
    INDEX = 'index',
    SCROLL_X = 'scrollX',
    SCROLL_Y = 'scrollY',
    TOTAL = 'total',
    DISABLED = 'disabled',
    HOST = 'host',
    SELECTOR = 'selector',
    AXIS = 'axis',
    DIM_X = 'x',
    DIM_Y = 'y';

/**
 * Scrollview plugin that adds support for paging
 *
 * @class ScrollViewPaginator
 * @namespace Plugin
 * @extends Plugin.Base
 * @constructor
 */
function PaginatorPlugin() {
    PaginatorPlugin.superclass.constructor.apply(this, arguments);
}

Y.extend(PaginatorPlugin, Y.Plugin.Base, {

    /**
     * Designated initializer
     *
     * @method initializer
     * @param {Object} Configuration object for the plugin
     */
    initializer: function (config) {
        var paginator = this,
            host = paginator.get(HOST);

        // Initialize & default
        paginator._pageDims = [];
        paginator._pageBuffer = 1;
        paginator._optimizeMemory = false;

        // Cache some values
        paginator._host = host;
        paginator._bb = host._bb;
        paginator._cb = host._cb;
        paginator._cIndex = paginator.get(INDEX);
        paginator._cAxis = paginator.get(AXIS);

        // Apply configs
        if (config._optimizeMemory) {
            paginator._optimizeMemory = config._optimizeMemory;
        }

        if (config._pageBuffer) {
            paginator._pageBuffer = config._pageBuffer;
        }

        // Attach event bindings
        paginator._bindAttrs();
    },

    /**
     *
     *
     * @method _bindAttrs
     * @private
     */
    _bindAttrs: function () {
        var paginator = this;

        // Event listeners
        paginator.after({
            'indexChange': paginator._afterIndexChange,
            'axisChange': paginator._afterAxisChange
        });

        // Host method listeners
        paginator.beforeHostMethod('scrollTo', paginator._beforeHostScrollTo);
        paginator.beforeHostMethod('_mousewheel', paginator._beforeHostMousewheel);
        paginator.beforeHostMethod('_flick', paginator._beforeHostFlick);
        paginator.afterHostMethod('_onGestureMoveEnd', paginator._afterHostGestureMoveEnd);
        paginator.afterHostMethod('_uiDimensionsChange', paginator._afterHostUIDimensionsChange);
        paginator.afterHostMethod('syncUI', paginator._afterHostSyncUI);

        // Host event listeners
        paginator.afterHostEvent('render', paginator._afterHostRender);
        paginator.afterHostEvent('scrollEnd', paginator._afterHostScrollEnded);
    },

    /**
     * After host render
     *
     * @method _afterHostRender
     * @param e {EventFacade} The event facade
     * @protected
     */
    _afterHostRender: function () {
        var paginator = this,
            bb = paginator._bb,
            host = paginator._host,
            index = paginator._cIndex,
            paginatorAxis = paginator._cAxis,
            pageNodes = paginator._getPageNodes(),
            size = pageNodes.size(),
            pageDim = paginator._pageDims[index];

        if (paginatorAxis[DIM_Y]) {
            host._maxScrollX = pageDim.maxScrollX;
        }
        else if (paginatorAxis[DIM_X]) {
            host._maxScrollY = pageDim.maxScrollY;
        }

        // Set the page count
        paginator.set(TOTAL, size);

        // Jump to the index
        if (index !== 0) {
            paginator.scrollToIndex(index, 0);
        }

        // Add the paginator class
        bb.addClass(CLASS_PAGED);

        // Trigger the optimization process
        paginator._optimize();
    },

    /**
     * After host syncUI
     *
     * @method _afterHostSyncUI
     * @param e {EventFacade} The event facade
     * @protected
     */
    _afterHostSyncUI: function () {
        var paginator = this,
            host = paginator._host,
            pageNodes = paginator._getPageNodes(),
            size = pageNodes.size();

        // Set the page count
        paginator.set(TOTAL, size);

        // If paginator's 'axis' property is to be automatically determined, inherit host's property
        if (paginator._cAxis === undefined) {
            paginator._set(AXIS, host.get(AXIS));
        }
    },

    /**
     * After host _uiDimensionsChange
     *
     * @method _afterHostUIDimensionsChange
     * @param e {EventFacade} The event facade
     * @protected
     */
    _afterHostUIDimensionsChange: function () {

        var paginator = this,
            host = paginator._host,
            dims = host._getScrollDims(),
            widgetWidth = dims.offsetWidth,
            widgetHeight = dims.offsetHeight,
            pageNodes = paginator._getPageNodes();

        // Inefficient. Should not reinitialize every page every syncUI
        pageNodes.each(function (node, i) {
            var scrollWidth = node.get('scrollWidth'),
                scrollHeight = node.get('scrollHeight'),
                maxScrollX = Math.max(0, scrollWidth - widgetWidth), // Math.max to ensure we don't set it to a negative value
                maxScrollY = Math.max(0, scrollHeight - widgetHeight);

            // Don't initialize any page _pageDims that already have been.
            if (!paginator._pageDims[i]) {

                paginator._pageDims[i] = {

                    // Current scrollX & scrollY positions (default to 0)
                    scrollX: 0,
                    scrollY: 0,

                    // Maximum scrollable values
                    maxScrollX: maxScrollX,
                    maxScrollY: maxScrollY,

                    // Height & width of the page
                    width: scrollWidth,
                    height: scrollHeight
                };

            } else {
                paginator._pageDims[i].maxScrollX = maxScrollX;
                paginator._pageDims[i].maxScrollY = maxScrollY;
            }

        });
    },

    /**
     * Executed before host.scrollTo
     *
     * @method _beforeHostScrollTo
     * @param x {Number} The x-position to scroll to. (null for no movement)
     * @param y {Number} The y-position to scroll to. (null for no movement)
     * @param {Number} [duration] Duration, in ms, of the scroll animation (default is 0)
     * @param {String} [easing] An easing equation if duration is set
     * @param {String} [node] The node to move
     * @protected
     */
    _beforeHostScrollTo: function (x, y, duration, easing, node) {
        var paginator = this,
            host = paginator._host,
            gesture = host._gesture,
            index = paginator._cIndex,
            paginatorAxis = paginator._cAxis,
            pageNodes = paginator._getPageNodes(),
            gestureAxis;

        if (gesture) {
            gestureAxis = gesture.axis;

            // Null the opposite axis so it won't be modified by host.scrollTo
            if (gestureAxis === DIM_Y) {
                x = null;
            } else {
                y = null;
            }

            // If they are scrolling against the specified axis, pull out the page's node to have its own offset
            if (paginatorAxis[gestureAxis] === false) {
                node = pageNodes.item(index);
            }

        }

        // Return the modified argument list
        return new Y.Do.AlterArgs("new args", [x, y, duration, easing, node]);
    },

    /**
     * Executed after host._gestureMoveEnd
     * Determines if the gesture should page prev or next (if at all)
     *
     * @method _afterHostGestureMoveEnd
     * @param e {EventFacade} The event facade
     * @protected
     */
    _afterHostGestureMoveEnd: function () {

        // This was a flick, so we don't need to do anything here
        if (this._host._gesture.flick) {
            return;
        }

        var paginator = this,
            host = paginator._host,
            gesture = host._gesture,
            index = paginator._cIndex,
            paginatorAxis = paginator._cAxis,
            gestureAxis = gesture.axis,
            isHorizontal = (gestureAxis === DIM_X),
            delta = gesture[(isHorizontal ? 'deltaX' : 'deltaY')],
            isForward = (delta > 0),
            pageDims = paginator._pageDims[index],
            halfway = pageDims[(isHorizontal ? 'width' : 'height')] / 2,
            isHalfway = (Math.abs(delta) >= halfway),
            canScroll = paginatorAxis[gestureAxis],
            rtl = host.rtl;

        if (canScroll) {
            if (isHalfway) { // TODO: This condition should probably be configurable
                // Fire next()/prev()
                paginator[(rtl === isForward ? 'prev' : 'next')]();
            }
            // Scrollback
            else {
                paginator.scrollToIndex(paginator.get(INDEX));
            }
        }
    },

    /**
     * Executed before host._mousewheel
     * Prevents mousewheel events in some conditions
     *
     * @method _beforeHostMousewheel
     * @param e {EventFacade} The event facade
     * @protected
     */
    _beforeHostMousewheel: function (e) {
        var paginator = this,
            host = paginator._host,
            bb = host._bb,
            isForward = (e.wheelDelta < 0),
            paginatorAxis = paginator._cAxis;

        // Only if the mousewheel event occurred on a DOM node inside the BB
        if (bb.contains(e.target) && paginatorAxis[DIM_Y]) {

            // Fire next()/prev()
            paginator[(isForward ? 'next' : 'prev')]();

            // prevent browser default behavior on mousewheel
            e.preventDefault();

            // Block host._mousewheel from running
            return new Y.Do.Prevent();
        }
    },

    /**
     * Executed before host._flick
     * Prevents flick events in some conditions
     *
     * @method _beforeHostFlick
     * @param e {EventFacade} The event facade
     * @protected
     */
    _beforeHostFlick: function (e) {

        // If the widget is disabled
        if (this._host.get(DISABLED)) {
            return false;
        }

        // The drag was out of bounds, so do nothing (which will cause a snapback)
        if (this._host._isOutOfBounds()){
            return new Y.Do.Prevent();
        }

        var paginator = this,
            host = paginator._host,
            gesture = host._gesture,
            paginatorAxis = paginator.get(AXIS),
            flick = e.flick,
            velocity = flick.velocity,
            flickAxis = flick.axis || false,
            isForward = (velocity < 0),
            canScroll = paginatorAxis[flickAxis],
            rtl = host.rtl;

        // Store the flick data in the this._host._gesture object so it knows this was a flick
        if (gesture) {
            gesture.flick = flick;
        }

        // Can we scroll along this axis?
        if (canScroll) {

            // Fire next()/prev()
            paginator[(rtl === isForward ? 'prev' : 'next')]();

            // Prevent flicks on the paginated axis
            if (paginatorAxis[flickAxis]) {
                return new Y.Do.Prevent();
            }
        }
    },

    /**
     * Executes after host's 'scrollEnd' event
     * Runs cleanup operations
     *
     * @method _afterHostScrollEnded
     * @param e {EventFacade} The event facade
     * @protected
     */
    _afterHostScrollEnded: function () {
        var paginator = this,
            host = paginator._host,
            index = paginator._cIndex,
            scrollX = host.get(SCROLL_X),
            scrollY = host.get(SCROLL_Y),
            paginatorAxis = paginator._cAxis;

        if (paginatorAxis[DIM_Y]) {
            paginator._pageDims[index].scrollX = scrollX;
        } else {
            paginator._pageDims[index].scrollY = scrollY;
        }

        paginator._optimize();
    },

    /**
     * index attr change handler
     *
     * @method _afterIndexChange
     * @param e {EventFacade} The event facade
     * @protected
     */
    _afterIndexChange: function (e) {
        var paginator = this,
            host = paginator._host,
            index = e.newVal,
            pageDims = paginator._pageDims[index],
            hostAxis = host._cAxis,
            paginatorAxis = paginator._cAxis;

        // Cache the new index value
        paginator._cIndex = index;

        // For dual-axis instances, we need to hack some host properties to the
        // current page's max height/width and current stored offset
        if (hostAxis[DIM_X] && hostAxis[DIM_Y]) {
            if (paginatorAxis[DIM_Y]) {
                host._maxScrollX = pageDims.maxScrollX;
                host.set(SCROLL_X, pageDims.scrollX, { src: UI });
            }
            else if (paginatorAxis[DIM_X]) {
                host._maxScrollY = pageDims.maxScrollY;
                host.set(SCROLL_Y, pageDims.scrollY, { src: UI });
            }
        }

        if (e.src !== UI) {
            paginator.scrollToIndex(index);
        }
    },

    /**
     * Optimization: Hides the pages not near the viewport
     *
     * @method _optimize
     * @protected
     */
    _optimize: function () {

        if (!this._optimizeMemory) {
            return false;
        }

        var paginator = this,
            currentIndex = paginator._cIndex,
            pageNodes = paginator._getStage(currentIndex);

        // Show the pages in/near the viewport & hide the rest
        paginator._showNodes(pageNodes.visible);
        paginator._hideNodes(pageNodes.hidden);
    },

    /**
     * Optimization: Determines which nodes should be visible, and which should be hidden.
     *
     * @method _getStage
     * @param index {Number} The page index # intended to be in focus.
     * @return {object}
     * @protected
     */
    _getStage: function (index) {
        var paginator = this,
            pageBuffer = paginator._pageBuffer,
            pageCount = paginator.get(TOTAL),
            pageNodes = paginator._getPageNodes(),
            start = Math.max(0, index - pageBuffer),
            end = Math.min(pageCount, index + 1 + pageBuffer); // noninclusive

        return {
            visible: pageNodes.splice(start, end - start),
            hidden: pageNodes
        };
    },

    /**
     * A utility method to show node(s)
     *
     * @method _showNodes
     * @param nodeList {Object} The list of nodes to show
     * @protected
     */
    _showNodes: function (nodeList) {
        if (nodeList) {
            nodeList.removeClass(CLASS_HIDDEN).setStyle('visibility', '');
        }
    },

    /**
     * A utility method to hide node(s)
     *
     * @method _hideNodes
     * @param nodeList {Object} The list of nodes to hide
     * @protected
     */
    _hideNodes: function (nodeList) {
        if (nodeList) {
            nodeList.addClass(CLASS_HIDDEN).setStyle('visibility', 'hidden');
        }
    },

    /**
     * Gets a nodeList for the "pages"
     *
     * @method _getPageNodes
     * @protected
     * @return {nodeList}
     */
    _getPageNodes: function () {
        var paginator = this,
            host = paginator._host,
            cb = host._cb,
            pageSelector = paginator.get(SELECTOR),
            pageNodes = (pageSelector ? cb.all(pageSelector) : cb.get('children'));

        return pageNodes;
    },

    /**
     * Scroll to the next page, with animation
     *
     * @method next
     */
    next: function () {
        var paginator = this,
            scrollview = paginator._host,
            index = paginator._cIndex,
            target = index + 1,
            total = paginator.get(TOTAL);

        // If the widget is disabled, ignore
        if (scrollview.get(DISABLED)) {
            return;
        }

        // If the target index is greater than the page count, ignore
        if (target >= total) {
            return;
        }

        // Update the index
        paginator.set(INDEX, target);
    },

    /**
     * Scroll to the previous page, with animation
     *
     * @method prev
     */
    prev: function () {
        var paginator = this,
            scrollview = paginator._host,
            index = paginator._cIndex,
            target = index - 1;

        // If the widget is disabled, ignore
        if (scrollview.get(DISABLED)) {
            return;
        }

        // If the target index is before the first page, ignore
        if (target < 0) {
            return;
        }

        // Update the index
        paginator.set(INDEX, target);
    },

    /**
     * Deprecated for 3.7.0.
     * @method scrollTo
     * @deprecated
     */
    scrollTo: function () {
        return this.scrollToIndex.apply(this, arguments);
    },

    /**
     * Scroll to a given page in the scrollview
     *
     * @method scrollToIndex
     * @since 3.7.0
     * @param index {Number} The index of the page to scroll to
     * @param {Number} [duration] The number of ms the animation should last
     * @param {String} [easing] The timing function to use in the animation
     */
    scrollToIndex: function (index, duration, easing) {
        var paginator = this,
            host = paginator._host,
            pageNode = paginator._getPageNodes().item(index),
            scrollAxis = (paginator._cAxis[DIM_X] ? SCROLL_X : SCROLL_Y),
            scrollOffset = pageNode.get(scrollAxis === SCROLL_X ? 'offsetLeft' : 'offsetTop');

        duration = (duration !== undefined) ? duration : PaginatorPlugin.TRANSITION.duration;
        easing = (easing !== undefined) ? easing : PaginatorPlugin.TRANSITION.easing;

        // Set the index ATTR to the specified index value
        paginator.set(INDEX, index, { src: UI });

        // Makes sure the viewport nodes are visible
        paginator._showNodes(pageNode);

        // Scroll to the offset
        host.set(scrollAxis, scrollOffset, {
            duration: duration,
            easing: easing
        });
    },

    /**
     * Setter for 'axis' attribute
     *
     * @method _axisSetter
     * @param val {Mixed} A string ('x', 'y', 'xy') to specify which axis/axes to allow scrolling on
     * @param name {String} The attribute name
     * @return {Object} An object to specify scrollability on the x & y axes
     *
     * @protected
     */
    _axisSetter: function (val) {

        // Turn a string into an axis object
        if (Y.Lang.isString(val)) {
            return {
                x: (val.match(/x/i) ? true : false),
                y: (val.match(/y/i) ? true : false)
            };
        }
    },


    /**
     * After listener for the axis attribute
     *
     * @method _afterAxisChange
     * @param e {EventFacade} The event facade
     * @protected
     */
    _afterAxisChange: function (e) {
        this._cAxis = e.newVal;
    }

    // End prototype properties

}, {

    // Static properties

    /**
     * The identity of the plugin
     *
     * @property NAME
     * @type String
     * @default 'pluginScrollViewPaginator'
     * @readOnly
     * @protected
     * @static
     */
    NAME: 'pluginScrollViewPaginator',

    /**
     * The namespace on which the plugin will reside
     *
     * @property NS
     * @type String
     * @default 'pages'
     * @static
     */
    NS: 'pages',

    /**
     * The default attribute configuration for the plugin
     *
     * @property ATTRS
     * @type {Object}
     * @static
     */
    ATTRS: {

        /**
         * Specifies ability to scroll on x, y, or x and y axis/axes.
         *  If unspecified, it inherits from the host instance.
         *
         * @attribute axis
         * @type String
         */
        axis: {
            setter: '_axisSetter',
            writeOnce: 'initOnly'
        },

        /**
         * CSS selector for a page inside the scrollview. The scrollview
         * will snap to the closest page.
         *
         * @attribute selector
         * @type {String}
         * @default null
         */
        selector: {
            value: null
        },

        /**
         * The active page number for a paged scrollview
         *
         * @attribute index
         * @type {Number}
         * @default 0
         */
        index: {
            value: 0
        },

        /**
         * The total number of pages
         *
         * @attribute total
         * @type {Number}
         * @default 0
         */
        total: {
            value: 0
        }
    },

    /**
     * The default snap to current duration and easing values used on scroll end.
     *
     * @property SNAP_TO_CURRENT
     * @static
     */
    TRANSITION: {
        duration: 300,
        easing: 'ease-out'
    }

    // End static properties

});

Y.namespace('Plugin').ScrollViewPaginator = PaginatorPlugin;