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