/** * 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;