/* backgrid-paginator http://github.com/wyuenho/backgrid Copyright (c) 2013-present Cloudflare, Inc and contributors Licensed under the MIT @license. */ (function (root, factory) { // CommonJS if (typeof exports == "object") { module.exports = factory(require("underscore"), require("backbone"), require("backgrid"), require("backbone.paginator")); } // AMD. Register as an anonymous module. else if (typeof define == "function" && define.amd) { define(["underscore", "backbone", "backgrid", "backbone.paginator"], factory); } // Browser else { factory(root._, root.Backbone, root.Backgrid); } }(this, function (_, Backbone, Backgrid) { "use strict"; /** PageHandle is a class that renders the actual page handles and reacts to click events for pagination. This class acts in two modes - control or discrete page handle modes. If one of the `is*` flags is `true`, an instance of this class is under control page handle mode. Setting a `pageIndex` to an instance of this class under control mode has no effect and the correct page index will always be inferred from the `is*` flag. Only one of the `is*` flags should be set to `true` at a time. For example, an instance of this class cannot simultaneously be a rewind control and a fast forward control. A `label` and a `title` function or a string are required to be passed to the constuctor under this mode. If a `title` function is provided, it __MUST__ accept a hash parameter `data`, which contains a key `label`. Its result will be used to render the generated anchor's title attribute. If all of the `is*` flags is set to `false`, which is the default, an instance of this class will be in discrete page handle mode. An instance under this mode requires the `pageIndex` to be passed from the constructor as an option and it __MUST__ be a 0-based index of the list of page numbers to render. The constuctor will normalize the base to the same base the underlying PageableCollection collection instance uses. A `label` is not required under this mode, which will default to the equivalent 1-based page index calculated from `pageIndex` and the underlying PageableCollection instance. A provided `label` will still be honored however. The `title` parameter is also not required under this mode, in which case the default `title` function will be used. You are encouraged to provide your own `title` function however if you wish to localize the title strings. If this page handle represents the current page, an `active` class will be placed on the root list element. If this page handle is at the border of the list of pages, a `disabled` class will be placed on the root list element. Only page handles that are neither `active` nor `disabled` will respond to click events and triggers pagination. @class Backgrid.Extension.PageHandle */ var PageHandle = Backgrid.Extension.PageHandle = Backbone.View.extend({ /** @property */ tagName: "li", /** @property */ events: { "click a": "changePage" }, /** @property {string|function(Object.): string} title The title to use for the `title` attribute of the generated page handle anchor elements. It can be a string or a function that takes a `data` parameter, which contains a mandatory `label` key which provides the label value to be displayed. */ title: function (data) { return 'Page ' + data.label; }, /** @property {boolean} isRewind Whether this handle represents a rewind control */ isRewind: false, /** @property {boolean} isBack Whether this handle represents a back control */ isBack: false, /** @property {boolean} isForward Whether this handle represents a forward control */ isForward: false, /** @property {boolean} isFastForward Whether this handle represents a fast forward control */ isFastForward: false, /** Initializer. @param {Object} options @param {Backbone.Collection} options.collection @param {number} pageIndex 0-based index of the page number this handle handles. This parameter will be normalized to the base the underlying PageableCollection uses. @param {string} [options.label] If provided it is used to render the anchor text, otherwise the normalized pageIndex will be used instead. Required if any of the `is*` flags is set to `true`. @param {string} [options.title] @param {boolean} [options.isRewind=false] @param {boolean} [options.isBack=false] @param {boolean} [options.isForward=false] @param {boolean} [options.isFastForward=false] */ initialize: function (options) { var collection = this.collection; var state = collection.state; var currentPage = state.currentPage; var firstPage = state.firstPage; var lastPage = state.lastPage; _.extend(this, _.pick(options, ["isRewind", "isBack", "isForward", "isFastForward"])); var pageIndex; if (this.isRewind) pageIndex = firstPage; else if (this.isBack) pageIndex = Math.max(firstPage, currentPage - 1); else if (this.isForward) pageIndex = Math.min(lastPage, currentPage + 1); else if (this.isFastForward) pageIndex = lastPage; else { pageIndex = +options.pageIndex; pageIndex = (firstPage ? pageIndex + 1 : pageIndex); } this.pageIndex = pageIndex; this.label = (options.label || (firstPage ? pageIndex : pageIndex + 1)) + ''; var title = options.title || this.title; this.title = _.isFunction(title) ? title({label: this.label}) : title; }, /** Renders a clickable anchor element under a list item. */ render: function () { this.$el.empty(); var anchor = document.createElement("a"); anchor.href = '#'; if (this.title) anchor.title = this.title; anchor.innerHTML = this.label; this.el.appendChild(anchor); var collection = this.collection; var state = collection.state; var currentPage = state.currentPage; var pageIndex = this.pageIndex; if (this.isRewind && currentPage == state.firstPage || this.isBack && !collection.hasPreviousPage() || this.isForward && !collection.hasNextPage() || this.isFastForward && (currentPage == state.lastPage || state.totalPages < 1)) { this.$el.addClass("disabled"); } else if (!(this.isRewind || this.isBack || this.isForward || this.isFastForward) && state.currentPage == pageIndex) { this.$el.addClass("active"); } this.delegateEvents(); return this; }, /** jQuery click event handler. Goes to the page this PageHandle instance represents. No-op if this page handle is currently active or disabled. */ changePage: function (e) { e.preventDefault(); var $el = this.$el, col = this.collection; if (!$el.hasClass("active") && !$el.hasClass("disabled")) { if (this.isRewind) col.getFirstPage({reset: true}); else if (this.isBack) col.getPreviousPage({reset: true}); else if (this.isForward) col.getNextPage({reset: true}); else if (this.isFastForward) col.getLastPage({reset: true}); else col.getPage(this.pageIndex, {reset: true}); } return this; } }); /** Paginator is a Backgrid extension that renders a series of configurable pagination handles. This extension is best used for splitting a large data set across multiple pages. If the number of pages is larger then a threshold, which is set to 10 by default, the page handles are rendered within a sliding window, plus the rewind, back, forward and fast forward control handles. The individual control handles can be turned off. @class Backgrid.Extension.Paginator */ var Paginator = Backgrid.Extension.Paginator = Backbone.View.extend({ /** @property */ className: "backgrid-paginator", /** @property */ windowSize: 10, /** @property {number} slideScale the number used by #slideHowMuch to scale `windowSize` to yield the number of pages to slide. For example, the default windowSize(10) * slideScale(0.5) yields 5, which means the window will slide forward 5 pages as soon as you've reached page 6. The smaller the scale factor the less pages to slide, and vice versa. Also See: - #slideMaybe - #slideHowMuch */ slideScale: 0.5, /** @property {Object.>} controls You can disable specific control handles by setting the keys in question to null. The defaults will be merged with your controls object, with your changes taking precedent. */ controls: { rewind: { label: "《", title: "First" }, back: { label: "〈", title: "Previous" }, forward: { label: "〉", title: "Next" }, fastForward: { label: "》", title: "Last" } }, /** @property */ renderIndexedPageHandles: true, /** @property renderMultiplePagesOnly. Determines if the paginator should show in cases where the collection has more than one page. Default is false for backwards compatibility. */ renderMultiplePagesOnly: false, /** @property {Backgrid.Extension.PageHandle} pageHandle. The PageHandle class to use for rendering individual handles */ pageHandle: PageHandle, /** @property */ goBackFirstOnSort: true, /** Initializer. @param {Object} options @param {Backbone.Collection} options.collection @param {boolean} [options.controls] @param {boolean} [options.pageHandle=Backgrid.Extension.PageHandle] @param {boolean} [options.goBackFirstOnSort=true] @param {boolean} [options.renderMultiplePagesOnly=false] */ initialize: function (options) { var self = this; self.controls = _.defaults(options.controls || {}, self.controls, Paginator.prototype.controls); _.extend(self, _.pick(options || {}, "windowSize", "pageHandle", "slideScale", "goBackFirstOnSort", "renderIndexedPageHandles", "renderMultiplePagesOnly")); var col = self.collection; self.listenTo(col, "add", self.render); self.listenTo(col, "remove", self.render); self.listenTo(col, "reset", self.render); self.listenTo(col, "backgrid:sorted", function () { if (self.goBackFirstOnSort && col.state.currentPage !== col.state.firstPage) col.getFirstPage({reset: true}); }); }, /** Decides whether the window should slide. This method should return 1 if sliding should occur and 0 otherwise. The default is sliding should occur if half of the pages in a window has been reached. __Note__: All the parameters have been normalized to be 0-based. @param {number} firstPage @param {number} lastPage @param {number} currentPage @param {number} windowSize @param {number} slideScale @return {0|1} */ slideMaybe: function (firstPage, lastPage, currentPage, windowSize, slideScale) { return Math.round(currentPage % windowSize / windowSize); }, /** Decides how many pages to slide when sliding should occur. The default simply scales the `windowSize` to arrive at a fraction of the `windowSize` to increment. __Note__: All the parameters have been normalized to be 0-based. @param {number} firstPage @param {number} lastPage @param {number} currentPage @param {number} windowSize @param {number} slideScale @return {number} */ slideThisMuch: function (firstPage, lastPage, currentPage, windowSize, slideScale) { return ~~(windowSize * slideScale); }, _calculateWindow: function () { var collection = this.collection; var state = collection.state; // convert all indices to 0-based here var firstPage = state.firstPage; var lastPage = +state.lastPage; lastPage = Math.max(0, firstPage ? lastPage - 1 : lastPage); var currentPage = Math.max(state.currentPage, state.firstPage); currentPage = firstPage ? currentPage - 1 : currentPage; var windowSize = this.windowSize; var slideScale = this.slideScale; var windowStart = Math.floor(currentPage / windowSize) * windowSize; if (currentPage <= lastPage - this.slideThisMuch()) { windowStart += (this.slideMaybe(firstPage, lastPage, currentPage, windowSize, slideScale) * this.slideThisMuch(firstPage, lastPage, currentPage, windowSize, slideScale)); } var windowEnd = Math.min(lastPage + 1, windowStart + windowSize); return [windowStart, windowEnd]; }, /** Creates a list of page handle objects for rendering. @return {Array.} an array of page handle objects hashes */ makeHandles: function () { var handles = []; var collection = this.collection; var window = this._calculateWindow(); var winStart = window[0], winEnd = window[1]; if (this.renderIndexedPageHandles) { for (var i = winStart; i < winEnd; i++) { handles.push(new this.pageHandle({ collection: collection, pageIndex: i })); } } var controls = this.controls; _.each(["back", "rewind", "forward", "fastForward"], function (key) { var value = controls[key]; if (value) { var handleCtorOpts = { collection: collection, title: value.title, label: value.label }; handleCtorOpts["is" + key.slice(0, 1).toUpperCase() + key.slice(1)] = true; var handle = new this.pageHandle(handleCtorOpts); if (key == "rewind" || key == "back") handles.unshift(handle); else handles.push(handle); } }, this); return handles; }, /** Render the paginator handles inside an unordered list. */ render: function () { this.$el.empty(); var totalPages = this.collection.state.totalPages; // Don't render if collection is empty if(this.renderMultiplePagesOnly && totalPages <= 1) { return this; } if (this.handles) { for (var i = 0, l = this.handles.length; i < l; i++) { this.handles[i].remove(); } } var handles = this.handles = this.makeHandles(); var ul = document.createElement("ul"); for (var i = 0; i < handles.length; i++) { ul.appendChild(handles[i].render().el); } this.el.appendChild(ul); return this; } }); }));