2013-04-22 02:40:39 +00:00
|
|
|
/*
|
2013-05-29 05:43:10 +00:00
|
|
|
backbone-pageable 1.3.0
|
2013-04-22 02:40:39 +00:00
|
|
|
http://github.com/wyuenho/backbone-pageable
|
|
|
|
|
|
|
|
Copyright (c) 2013 Jimmy Yuen Ho Wong
|
|
|
|
Licensed under the MIT @license.
|
|
|
|
*/
|
|
|
|
|
|
|
|
(function (factory) {
|
|
|
|
|
|
|
|
// CommonJS
|
|
|
|
if (typeof exports == "object") {
|
|
|
|
module.exports = factory(require("underscore"), require("backbone"));
|
|
|
|
}
|
|
|
|
// AMD
|
|
|
|
else if (typeof define == "function" && define.amd) {
|
|
|
|
define(["underscore", "backbone"], factory);
|
|
|
|
}
|
|
|
|
// Browser
|
|
|
|
else if (typeof _ !== "undefined" && typeof Backbone !== "undefined") {
|
|
|
|
var oldPageableCollection = Backbone.PageableCollection;
|
|
|
|
var PageableCollection = Backbone.PageableCollection = factory(_, Backbone);
|
|
|
|
|
|
|
|
/**
|
|
|
|
__BROWSER ONLY__
|
|
|
|
|
|
|
|
If you already have an object named `PageableCollection` attached to the
|
|
|
|
`Backbone` module, you can use this to return a local reference to this
|
|
|
|
Backbone.PageableCollection class and reset the name
|
|
|
|
Backbone.PageableCollection to its previous definition.
|
|
|
|
|
|
|
|
// The left hand side gives you a reference to this
|
|
|
|
// Backbone.PageableCollection implementation, the right hand side
|
|
|
|
// resets Backbone.PageableCollection to your other
|
|
|
|
// Backbone.PageableCollection.
|
|
|
|
var PageableCollection = Backbone.PageableCollection.noConflict();
|
|
|
|
|
|
|
|
@static
|
|
|
|
@member Backbone.PageableCollection
|
|
|
|
@return {Backbone.PageableCollection}
|
|
|
|
*/
|
|
|
|
Backbone.PageableCollection.noConflict = function () {
|
|
|
|
Backbone.PageableCollection = oldPageableCollection;
|
|
|
|
return PageableCollection;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
}(function (_, Backbone) {
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
var _extend = _.extend;
|
|
|
|
var _omit = _.omit;
|
|
|
|
var _clone = _.clone;
|
|
|
|
var _each = _.each;
|
|
|
|
var _pick = _.pick;
|
|
|
|
var _contains = _.contains;
|
|
|
|
var _isEmpty = _.isEmpty;
|
|
|
|
var _pairs = _.pairs;
|
|
|
|
var _invert = _.invert;
|
|
|
|
var _isArray = _.isArray;
|
|
|
|
var _isFunction = _.isFunction;
|
|
|
|
var _isObject = _.isObject;
|
|
|
|
var _keys = _.keys;
|
|
|
|
var _isUndefined = _.isUndefined;
|
|
|
|
var _result = _.result;
|
|
|
|
var ceil = Math.ceil;
|
2013-05-12 15:22:54 +00:00
|
|
|
var floor = Math.floor;
|
2013-04-22 02:40:39 +00:00
|
|
|
var max = Math.max;
|
|
|
|
|
|
|
|
var BBColProto = Backbone.Collection.prototype;
|
|
|
|
|
|
|
|
function finiteInt (val, name) {
|
|
|
|
if (!_.isNumber(val) || _.isNaN(val) || !_.isFinite(val) || ~~val !== val) {
|
|
|
|
throw new TypeError("`" + name + "` must be a finite integer");
|
|
|
|
}
|
|
|
|
return val;
|
|
|
|
}
|
|
|
|
|
|
|
|
function queryStringToParams (qs) {
|
|
|
|
var kvp, k, v, ls, params = {}, decode = decodeURIComponent;
|
|
|
|
var kvps = qs.split('&');
|
|
|
|
for (var i = 0, l = kvps.length; i < l; i++) {
|
|
|
|
var param = kvps[i];
|
|
|
|
kvp = param.split('='), k = kvp[0], v = kvp[1] || true;
|
|
|
|
k = decode(k), ls = params[k];
|
|
|
|
if (_isArray(ls)) ls.push(v);
|
|
|
|
else if (ls) params[k] = [ls, v];
|
|
|
|
else params[k] = v;
|
|
|
|
}
|
|
|
|
return params;
|
|
|
|
}
|
|
|
|
|
|
|
|
var PARAM_TRIM_RE = /[\s'"]/g;
|
|
|
|
var URL_TRIM_RE = /[<>\s'"]/g;
|
|
|
|
|
|
|
|
/**
|
|
|
|
Drop-in replacement for Backbone.Collection. Supports server-side and
|
|
|
|
client-side pagination and sorting. Client-side mode also support fully
|
|
|
|
multi-directional synchronization of changes between pages.
|
|
|
|
|
|
|
|
@class Backbone.PageableCollection
|
|
|
|
@extends Backbone.Collection
|
|
|
|
*/
|
|
|
|
var PageableCollection = Backbone.Collection.extend({
|
|
|
|
|
|
|
|
/**
|
|
|
|
The container object to store all pagination states.
|
|
|
|
|
|
|
|
You can override the default state by extending this class or specifying
|
|
|
|
them in an `options` hash to the constructor.
|
|
|
|
|
|
|
|
@property {Object} state
|
|
|
|
|
|
|
|
@property {0|1} [state.firstPage=1] The first page index. Set to 0 if
|
|
|
|
your server API uses 0-based indices. You should only override this value
|
|
|
|
during extension, initialization or reset by the server after
|
|
|
|
fetching. This value should be read only at other times.
|
|
|
|
|
|
|
|
@property {number} [state.lastPage=null] The last page index. This value
|
|
|
|
is __read only__ and it's calculated based on whether `firstPage` is 0 or
|
|
|
|
1, during bootstrapping, fetching and resetting. Please don't change this
|
|
|
|
value under any circumstances.
|
|
|
|
|
|
|
|
@property {number} [state.currentPage=null] The current page index. You
|
|
|
|
should only override this value during extension, initialization or reset
|
|
|
|
by the server after fetching. This value should be read only at other
|
|
|
|
times. Can be a 0-based or 1-based index, depending on whether
|
|
|
|
`firstPage` is 0 or 1. If left as default, it will be set to `firstPage`
|
|
|
|
on initialization.
|
|
|
|
|
|
|
|
@property {number} [state.pageSize=25] How many records to show per
|
|
|
|
page. This value is __read only__ after initialization, if you want to
|
|
|
|
change the page size after initialization, you must call #setPageSize.
|
|
|
|
|
|
|
|
@property {number} [state.totalPages=null] How many pages there are. This
|
|
|
|
value is __read only__ and it is calculated from `totalRecords`.
|
|
|
|
|
|
|
|
@property {number} [state.totalRecords=null] How many records there
|
|
|
|
are. This value is __required__ under server mode. This value is optional
|
|
|
|
for client mode as the number will be the same as the number of models
|
|
|
|
during bootstrapping and during fetching, either supplied by the server
|
|
|
|
in the metadata, or calculated from the size of the response.
|
|
|
|
|
|
|
|
@property {string} [state.sortKey=null] The model attribute to use for
|
|
|
|
sorting.
|
|
|
|
|
|
|
|
@property {-1|0|1} [state.order=-1] The order to use for sorting. Specify
|
|
|
|
-1 for ascending order or 1 for descending order. If 0, no client side
|
|
|
|
sorting will be done and the order query parameter will not be sent to
|
|
|
|
the server during a fetch.
|
|
|
|
*/
|
|
|
|
state: {
|
|
|
|
firstPage: 1,
|
|
|
|
lastPage: null,
|
|
|
|
currentPage: null,
|
|
|
|
pageSize: 25,
|
|
|
|
totalPages: null,
|
|
|
|
totalRecords: null,
|
|
|
|
sortKey: null,
|
|
|
|
order: -1
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
@property {"server"|"client"|"infinite"} [mode="server"] The mode of
|
|
|
|
operations for this collection. `"server"` paginates on the server-side,
|
|
|
|
`"client"` paginates on the client-side and `"infinite"` paginates on the
|
|
|
|
server-side for APIs that do not support `totalRecords`.
|
|
|
|
*/
|
|
|
|
mode: "server",
|
|
|
|
|
|
|
|
/**
|
|
|
|
A translation map to convert Backbone.PageableCollection state attributes
|
|
|
|
to the query parameters accepted by your server API.
|
|
|
|
|
|
|
|
You can override the default state by extending this class or specifying
|
|
|
|
them in `options.queryParams` object hash to the constructor.
|
|
|
|
|
|
|
|
@property {Object} queryParams
|
|
|
|
@property {string} [queryParams.currentPage="page"]
|
|
|
|
@property {string} [queryParams.pageSize="per_page"]
|
|
|
|
@property {string} [queryParams.totalPages="total_pages"]
|
|
|
|
@property {string} [queryParams.totalRecords="total_entries"]
|
|
|
|
@property {string} [queryParams.sortKey="sort_by"]
|
|
|
|
@property {string} [queryParams.order="order"]
|
|
|
|
@property {string} [queryParams.directions={"-1": "asc", "1": "desc"}] A
|
|
|
|
map for translating a Backbone.PageableCollection#state.order constant to
|
|
|
|
the ones your server API accepts.
|
|
|
|
*/
|
|
|
|
queryParams: {
|
|
|
|
currentPage: "page",
|
|
|
|
pageSize: "per_page",
|
|
|
|
totalPages: "total_pages",
|
|
|
|
totalRecords: "total_entries",
|
|
|
|
sortKey: "sort_by",
|
|
|
|
order: "order",
|
|
|
|
directions: {
|
|
|
|
"-1": "asc",
|
|
|
|
"1": "desc"
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
__CLIENT MODE ONLY__
|
|
|
|
|
|
|
|
This collection is the internal storage for the bootstrapped or fetched
|
|
|
|
models. You can use this if you want to operate on all the pages.
|
|
|
|
|
|
|
|
@property {Backbone.Collection} fullCollection
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
Given a list of models or model attributues, bootstraps the full
|
|
|
|
collection in client mode or infinite mode, or just the page you want in
|
|
|
|
server mode.
|
|
|
|
|
|
|
|
If you want to initialize a collection to a different state than the
|
|
|
|
default, you can specify them in `options.state`. Any state parameters
|
|
|
|
supplied will be merged with the default. If you want to change the
|
|
|
|
default mapping from #state keys to your server API's query parameter
|
|
|
|
names, you can specifiy an object hash in `option.queryParams`. Likewise,
|
|
|
|
any mapping provided will be merged with the default. Lastly, all
|
|
|
|
Backbone.Collection constructor options are also accepted.
|
|
|
|
|
|
|
|
See:
|
|
|
|
|
|
|
|
- Backbone.PageableCollection#state
|
|
|
|
- Backbone.PageableCollection#queryParams
|
|
|
|
- [Backbone.Collection#initialize](http://backbonejs.org/#Collection-constructor)
|
|
|
|
|
|
|
|
@param {Array.<Object>} [models]
|
|
|
|
|
|
|
|
@param {Object} [options]
|
|
|
|
|
|
|
|
@param {function(*, *): number} [options.comparator] If specified, this
|
2013-05-16 22:49:57 +00:00
|
|
|
comparator is set to the current page under server mode, or the #fullCollection
|
|
|
|
otherwise.
|
2013-04-22 02:40:39 +00:00
|
|
|
|
|
|
|
@param {boolean} [options.full] If `false` and either a
|
|
|
|
`options.comparator` or `sortKey` is defined, the comparator is attached
|
|
|
|
to the current page. Default is `true` under client or infinite mode and
|
|
|
|
the comparator will be attached to the #fullCollection.
|
|
|
|
|
|
|
|
@param {Object} [options.state] The state attributes overriding the defaults.
|
|
|
|
|
|
|
|
@param {string} [options.state.sortKey] The model attribute to use for
|
|
|
|
sorting. If specified instead of `options.comparator`, a comparator will
|
|
|
|
be automatically created using this value, and optionally a sorting order
|
|
|
|
specified in `options.state.order`. The comparator is then attached to
|
|
|
|
the new collection instance.
|
|
|
|
|
|
|
|
@param {-1|1} [options.state.order] The order to use for sorting. Specify
|
|
|
|
-1 for ascending order and 1 for descending order.
|
|
|
|
|
|
|
|
@param {Object} [options.queryParam]
|
|
|
|
*/
|
2013-05-16 22:49:57 +00:00
|
|
|
constructor: function (models, options) {
|
|
|
|
|
|
|
|
Backbone.Collection.apply(this, arguments);
|
2013-04-22 02:40:39 +00:00
|
|
|
|
|
|
|
options = options || {};
|
|
|
|
|
|
|
|
var mode = this.mode = options.mode || this.mode || PageableProto.mode;
|
|
|
|
|
|
|
|
var queryParams = _extend({}, PageableProto.queryParams, this.queryParams,
|
|
|
|
options.queryParams || {});
|
|
|
|
|
|
|
|
queryParams.directions = _extend({},
|
|
|
|
PageableProto.queryParams.directions,
|
|
|
|
this.queryParams.directions,
|
|
|
|
queryParams.directions || {});
|
|
|
|
|
|
|
|
this.queryParams = queryParams;
|
|
|
|
|
|
|
|
var state = this.state = _extend({}, PageableProto.state, this.state,
|
|
|
|
options.state || {});
|
|
|
|
|
|
|
|
state.currentPage = state.currentPage == null ?
|
|
|
|
state.firstPage :
|
|
|
|
state.currentPage;
|
|
|
|
|
2013-05-12 15:22:54 +00:00
|
|
|
if (!_isArray(models)) models = models ? [models] : [];
|
|
|
|
|
2013-04-22 02:40:39 +00:00
|
|
|
if (mode != "server" && state.totalRecords == null && !_isEmpty(models)) {
|
|
|
|
state.totalRecords = models.length;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.switchMode(mode, _extend({fetch: false,
|
|
|
|
resetState: false,
|
|
|
|
models: models}, options));
|
|
|
|
|
|
|
|
var comparator = options.comparator;
|
|
|
|
|
|
|
|
if (state.sortKey && !comparator) {
|
|
|
|
this.setSorting(state.sortKey, state.order, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mode != "server") {
|
2013-05-29 05:43:10 +00:00
|
|
|
var fullCollection = this.fullCollection;
|
2013-04-22 02:40:39 +00:00
|
|
|
|
|
|
|
if (comparator && options.full) {
|
|
|
|
delete this.comparator;
|
|
|
|
fullCollection.comparator = comparator;
|
|
|
|
}
|
|
|
|
|
2013-05-29 05:43:10 +00:00
|
|
|
if (options.full) fullCollection.sort();
|
|
|
|
|
2013-04-22 02:40:39 +00:00
|
|
|
// make sure the models in the current page and full collection have the
|
|
|
|
// same references
|
|
|
|
if (models && !_isEmpty(models)) {
|
|
|
|
this.getPage(state.currentPage);
|
|
|
|
models.splice.apply(models, [0, models.length].concat(this.models));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this._initState = _clone(this.state);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
Makes a Backbone.Collection that contains all the pages.
|
|
|
|
|
|
|
|
@private
|
|
|
|
@param {Array.<Object|Backbone.Model>} models
|
|
|
|
@param {Object} options Options for Backbone.Collection constructor.
|
|
|
|
@return {Backbone.Collection}
|
|
|
|
*/
|
|
|
|
_makeFullCollection: function (models, options) {
|
|
|
|
|
|
|
|
var properties = ["url", "model", "sync", "comparator"];
|
|
|
|
var thisProto = this.constructor.prototype;
|
|
|
|
var i, length, prop;
|
|
|
|
|
|
|
|
var proto = {};
|
|
|
|
for (i = 0, length = properties.length; i < length; i++) {
|
|
|
|
prop = properties[i];
|
|
|
|
if (!_isUndefined(thisProto[prop])) {
|
|
|
|
proto[prop] = thisProto[prop];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var fullCollection = new (Backbone.Collection.extend(proto))(models, options);
|
|
|
|
|
|
|
|
for (i = 0, length = properties.length; i < length; i++) {
|
|
|
|
prop = properties[i];
|
|
|
|
if (this[prop] !== thisProto[prop]) {
|
|
|
|
fullCollection[prop] = this[prop];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return fullCollection;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
Factory method that returns a Backbone event handler that responses to
|
|
|
|
the `add`, `remove`, `reset`, and the `sort` events. The returned event
|
|
|
|
handler will synchronize the current page collection and the full
|
|
|
|
collection's models.
|
|
|
|
|
|
|
|
@private
|
|
|
|
|
|
|
|
@param {Backbone.PageableCollection} pageCol
|
|
|
|
@param {Backbone.Collection} fullCol
|
|
|
|
|
|
|
|
@return {function(string, Backbone.Model, Backbone.Collection, Object)}
|
|
|
|
Collection event handler
|
|
|
|
*/
|
|
|
|
_makeCollectionEventHandler: function (pageCol, fullCol) {
|
|
|
|
|
|
|
|
return function collectionEventHandler (event, model, collection, options) {
|
2013-05-29 05:43:10 +00:00
|
|
|
|
2013-04-22 02:40:39 +00:00
|
|
|
var handlers = pageCol._handlers;
|
|
|
|
_each(_keys(handlers), function (event) {
|
|
|
|
var handler = handlers[event];
|
|
|
|
pageCol.off(event, handler);
|
|
|
|
fullCol.off(event, handler);
|
|
|
|
});
|
|
|
|
|
|
|
|
var state = _clone(pageCol.state);
|
|
|
|
var firstPage = state.firstPage;
|
|
|
|
var currentPage = firstPage === 0 ?
|
|
|
|
state.currentPage :
|
|
|
|
state.currentPage - 1;
|
|
|
|
var pageSize = state.pageSize;
|
|
|
|
var pageStart = currentPage * pageSize, pageEnd = pageStart + pageSize;
|
|
|
|
|
|
|
|
if (event == "add") {
|
|
|
|
var pageIndex, fullIndex, addAt, colToAdd, options = options || {};
|
|
|
|
if (collection == fullCol) {
|
|
|
|
fullIndex = fullCol.indexOf(model);
|
|
|
|
if (fullIndex >= pageStart && fullIndex < pageEnd) {
|
|
|
|
colToAdd = pageCol;
|
|
|
|
pageIndex = addAt = fullIndex - pageStart;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
pageIndex = pageCol.indexOf(model);
|
|
|
|
fullIndex = pageStart + pageIndex;
|
|
|
|
colToAdd = fullCol;
|
|
|
|
var addAt = !_isUndefined(options.at) ?
|
|
|
|
options.at + pageStart :
|
|
|
|
fullIndex;
|
|
|
|
}
|
|
|
|
|
|
|
|
++state.totalRecords;
|
|
|
|
pageCol.state = pageCol._checkState(state);
|
|
|
|
|
|
|
|
if (colToAdd) {
|
|
|
|
colToAdd.add(model, _extend({}, options || {}, {at: addAt}));
|
|
|
|
var modelToRemove = pageIndex >= pageSize ?
|
|
|
|
model :
|
|
|
|
!_isUndefined(options.at) && addAt < pageEnd && pageCol.length > pageSize ?
|
|
|
|
pageCol.at(pageSize) :
|
|
|
|
null;
|
|
|
|
if (modelToRemove) {
|
|
|
|
var addHandlers = collection._events.add || [],
|
|
|
|
popOptions = {onAdd: true};
|
|
|
|
if (addHandlers.length) {
|
|
|
|
var lastAddHandler = addHandlers[addHandlers.length - 1];
|
|
|
|
var oldCallback = lastAddHandler.callback;
|
|
|
|
lastAddHandler.callback = function () {
|
|
|
|
try {
|
|
|
|
oldCallback.apply(this, arguments);
|
|
|
|
pageCol.remove(modelToRemove, popOptions);
|
|
|
|
}
|
|
|
|
finally {
|
|
|
|
lastAddHandler.callback = oldCallback;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
else pageCol.remove(modelToRemove, popOptions);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// remove the model from the other collection as well
|
|
|
|
if (event == "remove") {
|
|
|
|
if (!options.onAdd) {
|
|
|
|
// decrement totalRecords and update totalPages and lastPage
|
|
|
|
if (!--state.totalRecords) {
|
|
|
|
state.totalRecords = null;
|
|
|
|
state.totalPages = null;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
var totalPages = state.totalPages = ceil(state.totalRecords / pageSize);
|
|
|
|
state.lastPage = firstPage === 0 ? totalPages - 1 : totalPages;
|
|
|
|
if (state.currentPage > totalPages) state.currentPage = state.lastPage;
|
|
|
|
}
|
|
|
|
pageCol.state = pageCol._checkState(state);
|
|
|
|
|
|
|
|
var nextModel, removedIndex = options.index;
|
|
|
|
if (collection == pageCol) {
|
|
|
|
if (nextModel = fullCol.at(pageEnd)) pageCol.push(nextModel);
|
|
|
|
fullCol.remove(model);
|
|
|
|
}
|
|
|
|
else if (removedIndex >= pageStart && removedIndex < pageEnd) {
|
|
|
|
pageCol.remove(model);
|
|
|
|
nextModel = fullCol.at(currentPage * (pageSize + removedIndex));
|
|
|
|
if (nextModel) pageCol.push(nextModel);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else delete options.onAdd;
|
|
|
|
}
|
|
|
|
|
2013-05-29 05:43:10 +00:00
|
|
|
if (event == "reset") {
|
2013-04-22 02:40:39 +00:00
|
|
|
options = collection;
|
|
|
|
collection = model;
|
|
|
|
|
2013-05-29 05:43:10 +00:00
|
|
|
// Reset that's not a result of getPage
|
|
|
|
if (collection === pageCol && options.from == null &&
|
|
|
|
options.to == null) {
|
2013-04-22 02:40:39 +00:00
|
|
|
var head = fullCol.models.slice(0, pageStart);
|
|
|
|
var tail = fullCol.models.slice(pageStart + pageCol.models.length);
|
2013-05-29 05:43:10 +00:00
|
|
|
fullCol.reset(head.concat(pageCol.models).concat(tail), options);
|
2013-04-22 02:40:39 +00:00
|
|
|
}
|
2013-05-29 05:43:10 +00:00
|
|
|
else if (collection === fullCol) {
|
2013-04-22 02:40:39 +00:00
|
|
|
if (!(state.totalRecords = fullCol.models.length)) {
|
|
|
|
state.totalRecords = null;
|
|
|
|
state.totalPages = null;
|
2013-05-29 05:43:10 +00:00
|
|
|
}
|
|
|
|
if (pageCol.mode == "client") {
|
2013-04-22 02:40:39 +00:00
|
|
|
state.lastPage = state.currentPage = state.firstPage;
|
|
|
|
}
|
|
|
|
pageCol.state = pageCol._checkState(state);
|
2013-05-29 05:43:10 +00:00
|
|
|
pageCol.reset(fullCol.models.slice(pageStart, pageEnd),
|
|
|
|
_extend({}, options, {parse: false}));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event == "sort") {
|
|
|
|
options = collection;
|
|
|
|
collection = model;
|
|
|
|
if (collection === fullCol) {
|
|
|
|
pageCol.reset(fullCol.models.slice(pageStart, pageEnd),
|
|
|
|
_extend({}, options, {parse: false}));
|
2013-04-22 02:40:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_each(_keys(handlers), function (event) {
|
|
|
|
var handler = handlers[event];
|
|
|
|
_each([pageCol, fullCol], function (col) {
|
|
|
|
col.on(event, handler);
|
|
|
|
var callbacks = col._events[event] || [];
|
|
|
|
callbacks.unshift(callbacks.pop());
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
Sanity check this collection's pagination states. Only perform checks
|
|
|
|
when all the required pagination state values are defined and not null.
|
|
|
|
If `totalPages` is undefined or null, it is set to `totalRecords` /
|
|
|
|
`pageSize`. `lastPage` is set according to whether `firstPage` is 0 or 1
|
|
|
|
when no error occurs.
|
|
|
|
|
|
|
|
@private
|
|
|
|
|
|
|
|
@throws {TypeError} If `totalRecords`, `pageSize`, `currentPage` or
|
|
|
|
`firstPage` is not a finite integer.
|
|
|
|
|
|
|
|
@throws {RangeError} If `pageSize`, `currentPage` or `firstPage` is out
|
|
|
|
of bounds.
|
|
|
|
|
|
|
|
@return {Object} Returns the `state` object if no error was found.
|
|
|
|
*/
|
|
|
|
_checkState: function (state) {
|
|
|
|
|
|
|
|
var mode = this.mode;
|
|
|
|
var links = this.links;
|
|
|
|
var totalRecords = state.totalRecords;
|
|
|
|
var pageSize = state.pageSize;
|
|
|
|
var currentPage = state.currentPage;
|
|
|
|
var firstPage = state.firstPage;
|
|
|
|
var totalPages = state.totalPages;
|
|
|
|
|
|
|
|
if (totalRecords != null && pageSize != null && currentPage != null &&
|
|
|
|
firstPage != null && (mode == "infinite" ? links : true)) {
|
|
|
|
|
|
|
|
totalRecords = finiteInt(totalRecords, "totalRecords");
|
|
|
|
pageSize = finiteInt(pageSize, "pageSize");
|
|
|
|
currentPage = finiteInt(currentPage, "currentPage");
|
|
|
|
firstPage = finiteInt(firstPage, "firstPage");
|
|
|
|
|
|
|
|
if (pageSize < 1) {
|
|
|
|
throw new RangeError("`pageSize` must be >= 1");
|
|
|
|
}
|
|
|
|
|
|
|
|
totalPages = state.totalPages = ceil(totalRecords / pageSize);
|
|
|
|
|
|
|
|
if (firstPage < 0 || firstPage > 1) {
|
|
|
|
throw new RangeError("`firstPage must be 0 or 1`");
|
|
|
|
}
|
|
|
|
|
|
|
|
state.lastPage = firstPage === 0 ? max(0, totalPages - 1) : totalPages;
|
|
|
|
|
|
|
|
if (mode == "infinite") {
|
|
|
|
if (!links[currentPage + '']) {
|
|
|
|
throw new RangeError("No link found for page " + currentPage);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (currentPage < firstPage ||
|
|
|
|
(totalPages > 0 &&
|
|
|
|
(firstPage ? currentPage > totalPages : currentPage >= totalPages))) {
|
|
|
|
throw new RangeError("`currentPage` must be firstPage <= currentPage " +
|
|
|
|
(firstPage ? ">" : ">=") +
|
|
|
|
" totalPages if " + firstPage + "-based. Got " +
|
|
|
|
currentPage + '.');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return state;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
Change the page size of this collection.
|
|
|
|
|
|
|
|
For server mode operations, changing the page size will trigger a #fetch
|
|
|
|
and subsequently a `reset` event.
|
|
|
|
|
|
|
|
For client mode operations, changing the page size will `reset` the
|
|
|
|
current page by recalculating the current page boundary on the client
|
|
|
|
side.
|
|
|
|
|
|
|
|
If `options.fetch` is true, a fetch can be forced if the collection is in
|
|
|
|
client mode.
|
|
|
|
|
|
|
|
@param {number} pageSize The new page size to set to #state.
|
|
|
|
@param {Object} [options] {@link #fetch} options.
|
|
|
|
@param {boolean} [options.fetch] If `true`, force a fetch in client mode.
|
|
|
|
|
|
|
|
@throws {TypeError} If `pageSize` is not a finite integer.
|
|
|
|
@throws {RangeError} If `pageSize` is less than 1.
|
|
|
|
|
|
|
|
@chainable
|
|
|
|
@return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
|
|
|
|
from fetch or this.
|
|
|
|
*/
|
|
|
|
setPageSize: function (pageSize, options) {
|
|
|
|
pageSize = finiteInt(pageSize, "pageSize");
|
|
|
|
|
|
|
|
options = options || {};
|
|
|
|
|
|
|
|
this.state = this._checkState(_extend({}, this.state, {
|
|
|
|
pageSize: pageSize,
|
|
|
|
totalPages: ceil(this.state.totalRecords / pageSize)
|
|
|
|
}));
|
|
|
|
|
|
|
|
return this.getPage(this.state.currentPage, options);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
Switching between client, server and infinite mode.
|
|
|
|
|
|
|
|
If switching from client to server mode, the #fullCollection is emptied
|
|
|
|
first and then deleted and a fetch is immediately issued for the current
|
|
|
|
page from the server. Pass `false` to `options.fetch` to skip fetching.
|
|
|
|
|
|
|
|
If switching to infinite mode, and if `options.models` is given for an
|
|
|
|
array of models, #links will be populated with a URL per page, using the
|
|
|
|
default URL for this collection.
|
|
|
|
|
|
|
|
If switching from server to client mode, all of the pages are immediately
|
|
|
|
refetched. If you have too many pages, you can pass `false` to
|
|
|
|
`options.fetch` to skip fetching.
|
|
|
|
|
|
|
|
If switching to any mode from infinite mode, the #links will be deleted.
|
|
|
|
|
|
|
|
@param {"server"|"client"|"infinite"} [mode] The mode to switch to.
|
|
|
|
|
|
|
|
@param {Object} [options]
|
|
|
|
|
|
|
|
@param {boolean} [options.fetch=true] If `false`, no fetching is done.
|
|
|
|
|
|
|
|
@param {boolean} [options.resetState=true] If 'false', the state is not
|
|
|
|
reset, but checked for sanity instead.
|
|
|
|
|
|
|
|
@chainable
|
|
|
|
@return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
|
|
|
|
from fetch or this if `options.fetch` is `false`.
|
|
|
|
*/
|
|
|
|
switchMode: function (mode, options) {
|
|
|
|
|
|
|
|
if (!_contains(["server", "client", "infinite"], mode)) {
|
|
|
|
throw new TypeError('`mode` must be one of "server", "client" or "infinite"');
|
|
|
|
}
|
|
|
|
|
|
|
|
options = options || {fetch: true, resetState: true};
|
|
|
|
|
|
|
|
var state = this.state = options.resetState ?
|
|
|
|
_clone(this._initState) :
|
|
|
|
this._checkState(_extend({}, this.state));
|
|
|
|
|
|
|
|
this.mode = mode;
|
|
|
|
|
|
|
|
var self = this;
|
|
|
|
var fullCollection = this.fullCollection;
|
|
|
|
var handlers = this._handlers = this._handlers || {}, handler;
|
|
|
|
if (mode != "server" && !fullCollection) {
|
|
|
|
fullCollection = this._makeFullCollection(options.models || []);
|
|
|
|
fullCollection.pageableCollection = this;
|
|
|
|
this.fullCollection = fullCollection;
|
|
|
|
var allHandler = this._makeCollectionEventHandler(this, fullCollection);
|
|
|
|
_each(["add", "remove", "reset", "sort"], function (event) {
|
|
|
|
handlers[event] = handler = _.bind(allHandler, {}, event);
|
|
|
|
self.on(event, handler);
|
|
|
|
fullCollection.on(event, handler);
|
|
|
|
});
|
|
|
|
fullCollection.comparator = this._fullComparator;
|
|
|
|
}
|
|
|
|
else if (mode == "server" && fullCollection) {
|
|
|
|
_each(_keys(handlers), function (event) {
|
|
|
|
handler = handlers[event];
|
|
|
|
self.off(event, handler);
|
|
|
|
fullCollection.off(event, handler);
|
|
|
|
});
|
|
|
|
delete this._handlers;
|
|
|
|
this._fullComparator = fullCollection.comparator;
|
|
|
|
delete this.fullCollection;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mode == "infinite") {
|
|
|
|
var links = this.links = {};
|
|
|
|
var firstPage = state.firstPage;
|
|
|
|
var totalPages = ceil(state.totalRecords / state.pageSize);
|
|
|
|
var lastPage = firstPage === 0 ? max(0, totalPages - 1) : totalPages || firstPage;
|
|
|
|
for (var i = state.firstPage; i <= lastPage; i++) {
|
|
|
|
links[i] = this.url;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (this.links) delete this.links;
|
|
|
|
|
|
|
|
return options.fetch ?
|
|
|
|
this.fetch(_omit(options, "fetch", "resetState")) :
|
|
|
|
this;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
@return {boolean} `true` if this collection can page backward, `false`
|
|
|
|
otherwise.
|
|
|
|
*/
|
|
|
|
hasPrevious: function () {
|
|
|
|
var state = this.state;
|
|
|
|
var currentPage = state.currentPage;
|
|
|
|
if (this.mode != "infinite") return currentPage > state.firstPage;
|
|
|
|
return !!this.links[currentPage - 1];
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
@return {boolean} `true` if this collection can page forward, `false`
|
|
|
|
otherwise.
|
|
|
|
*/
|
|
|
|
hasNext: function () {
|
|
|
|
var state = this.state;
|
|
|
|
var currentPage = this.state.currentPage;
|
|
|
|
if (this.mode != "infinite") return currentPage < state.lastPage;
|
|
|
|
return !!this.links[currentPage + 1];
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
Fetch the first page in server mode, or reset the current page of this
|
|
|
|
collection to the first page in client or infinite mode.
|
|
|
|
|
|
|
|
@param {Object} options {@link #getPage} options.
|
|
|
|
|
|
|
|
@chainable
|
|
|
|
@return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
|
|
|
|
from fetch or this.
|
|
|
|
*/
|
|
|
|
getFirstPage: function (options) {
|
|
|
|
return this.getPage("first", options);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
Fetch the previous page in server mode, or reset the current page of this
|
|
|
|
collection to the previous page in client or infinite mode.
|
|
|
|
|
|
|
|
@param {Object} options {@link #getPage} options.
|
|
|
|
|
|
|
|
@chainable
|
|
|
|
@return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
|
|
|
|
from fetch or this.
|
|
|
|
*/
|
|
|
|
getPreviousPage: function (options) {
|
|
|
|
return this.getPage("prev", options);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
Fetch the next page in server mode, or reset the current page of this
|
|
|
|
collection to the next page in client mode.
|
|
|
|
|
|
|
|
@param {Object} options {@link #getPage} options.
|
|
|
|
|
|
|
|
@chainable
|
|
|
|
@return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
|
|
|
|
from fetch or this.
|
|
|
|
*/
|
|
|
|
getNextPage: function (options) {
|
|
|
|
return this.getPage("next", options);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
Fetch the last page in server mode, or reset the current page of this
|
|
|
|
collection to the last page in client mode.
|
|
|
|
|
|
|
|
@param {Object} options {@link #getPage} options.
|
|
|
|
|
|
|
|
@chainable
|
|
|
|
@return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
|
|
|
|
from fetch or this.
|
|
|
|
*/
|
|
|
|
getLastPage: function (options) {
|
|
|
|
return this.getPage("last", options);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
Given a page index, set #state.currentPage to that index. If this
|
|
|
|
collection is in server mode, fetch the page using the updated state,
|
|
|
|
otherwise, reset the current page of this collection to the page
|
|
|
|
specified by `index` in client mode. If `options.fetch` is true, a fetch
|
|
|
|
can be forced in client mode before resetting the current page. Under
|
|
|
|
infinite mode, if the index is less than the current page, a reset is
|
|
|
|
done as in client mode. If the index is greater than the current page
|
|
|
|
number, a fetch is made with the results **appended** to
|
|
|
|
#fullCollection. The current page will then be reset after fetching.
|
|
|
|
|
|
|
|
@param {number|string} index The page index to go to, or the page name to
|
|
|
|
look up from #links in infinite mode.
|
|
|
|
@param {Object} [options] {@link #fetch} options or
|
|
|
|
[reset](http://backbonejs.org/#Collection-reset) options for client mode
|
|
|
|
when `options.fetch` is `false`.
|
|
|
|
@param {boolean} [options.fetch=false] If true, force a {@link #fetch} in
|
|
|
|
client mode.
|
|
|
|
|
|
|
|
@throws {TypeError} If `index` is not a finite integer under server or
|
|
|
|
client mode, or does not yield a URL from #links under infinite mode.
|
|
|
|
|
|
|
|
@throws {RangeError} If `index` is out of bounds.
|
|
|
|
|
|
|
|
@chainable
|
|
|
|
@return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
|
|
|
|
from fetch or this.
|
|
|
|
*/
|
|
|
|
getPage: function (index, options) {
|
|
|
|
|
|
|
|
var mode = this.mode, fullCollection = this.fullCollection;
|
|
|
|
|
|
|
|
options = options || {fetch: false};
|
|
|
|
|
|
|
|
var state = this.state,
|
|
|
|
firstPage = state.firstPage,
|
|
|
|
currentPage = state.currentPage,
|
|
|
|
lastPage = state.lastPage,
|
|
|
|
pageSize = state.pageSize;
|
|
|
|
|
|
|
|
var pageNum = index;
|
|
|
|
switch (index) {
|
|
|
|
case "first": pageNum = firstPage; break;
|
|
|
|
case "prev": pageNum = currentPage - 1; break;
|
|
|
|
case "next": pageNum = currentPage + 1; break;
|
|
|
|
case "last": pageNum = lastPage; break;
|
|
|
|
default: pageNum = finiteInt(index, "index");
|
|
|
|
}
|
|
|
|
|
|
|
|
this.state = this._checkState(_extend({}, state, {currentPage: pageNum}));
|
|
|
|
|
2013-05-29 05:43:10 +00:00
|
|
|
options.from = currentPage, options.to = pageNum;
|
|
|
|
|
2013-04-22 02:40:39 +00:00
|
|
|
var pageStart = (firstPage === 0 ? pageNum : pageNum - 1) * pageSize;
|
|
|
|
var pageModels = fullCollection && fullCollection.length ?
|
|
|
|
fullCollection.models.slice(pageStart, pageStart + pageSize) :
|
|
|
|
[];
|
|
|
|
if ((mode == "client" || (mode == "infinite" && !_isEmpty(pageModels))) &&
|
|
|
|
!options.fetch) {
|
2013-05-29 05:43:10 +00:00
|
|
|
return this.reset(pageModels, _omit(options, "fetch"));
|
2013-04-22 02:40:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (mode == "infinite") options.url = this.links[pageNum];
|
|
|
|
|
|
|
|
return this.fetch(_omit(options, "fetch"));
|
|
|
|
},
|
|
|
|
|
2013-05-12 15:22:54 +00:00
|
|
|
/**
|
|
|
|
Fetch the page for the provided item offset in server mode, or reset the current page of this
|
|
|
|
collection to the page for the provided item offset in client mode.
|
|
|
|
|
|
|
|
@param {Object} options {@link #getPage} options.
|
|
|
|
|
|
|
|
@chainable
|
|
|
|
@return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
|
|
|
|
from fetch or this.
|
|
|
|
*/
|
|
|
|
getPageByOffset: function (offset, options) {
|
|
|
|
if (offset < 0) {
|
|
|
|
throw new RangeError("`offset must be > 0`");
|
|
|
|
}
|
|
|
|
offset = finiteInt(offset);
|
|
|
|
|
|
|
|
var page = floor(offset / this.state.pageSize);
|
|
|
|
if (this.state.firstPage !== 0) page++;
|
|
|
|
if (page > this.state.lastPage) page = this.state.lastPage;
|
|
|
|
return this.getPage(page, options);
|
|
|
|
},
|
|
|
|
|
2013-04-22 02:40:39 +00:00
|
|
|
/**
|
|
|
|
Overidden to make `getPage` compatible with Zepto.
|
|
|
|
|
|
|
|
@param {string} method
|
|
|
|
@param {Backbone.Model|Backbone.Collection} model
|
|
|
|
@param {Object} [options]
|
|
|
|
|
|
|
|
@return {XMLHttpRequest}
|
|
|
|
*/
|
|
|
|
sync: function (method, model, options) {
|
|
|
|
var self = this;
|
|
|
|
if (self.mode == "infinite") {
|
|
|
|
var success = options.success;
|
|
|
|
var currentPage = self.state.currentPage;
|
|
|
|
options.success = function (resp, status, xhr) {
|
|
|
|
var links = self.links;
|
|
|
|
var newLinks = self.parseLinks(resp, _extend({xhr: xhr}, options));
|
|
|
|
if (newLinks.first) links[self.state.firstPage] = newLinks.first;
|
|
|
|
if (newLinks.prev) links[currentPage - 1] = newLinks.prev;
|
|
|
|
if (newLinks.next) links[currentPage + 1] = newLinks.next;
|
|
|
|
if (success) success(resp, status, xhr);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return (BBColProto.sync || Backbone.sync).call(self, method, model, options);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
Parse pagination links from the server response. Only valid under
|
|
|
|
infinite mode.
|
|
|
|
|
|
|
|
Given a response body and a XMLHttpRequest object, extract pagination
|
|
|
|
links from them for infinite paging.
|
|
|
|
|
|
|
|
This default implementation parses the RFC 5988 `Link` header and extract
|
|
|
|
3 links from it - `first`, `prev`, `next`. If a `previous` link is found,
|
|
|
|
it will be found in the `prev` key in the returned object hash. Any
|
|
|
|
subclasses overriding this method __must__ return an object hash having
|
|
|
|
only the keys above. If `first` is missing, the collection's default URL
|
|
|
|
is assumed to be the `first` URL. If `prev` or `next` is missing, it is
|
|
|
|
assumed to be `null`. An empty object hash must be returned if there are
|
|
|
|
no links found. If either the response or the header contains information
|
|
|
|
pertaining to the total number of records on the server,
|
|
|
|
#state.totalRecords must be set to that number. The default
|
|
|
|
implementation uses the `last` link from the header to calculate it.
|
|
|
|
|
|
|
|
@param {*} resp The deserialized response body.
|
|
|
|
@param {Object} [options]
|
|
|
|
@param {XMLHttpRequest} [options.xhr] The XMLHttpRequest object for this
|
|
|
|
response.
|
|
|
|
@return {Object}
|
|
|
|
*/
|
|
|
|
parseLinks: function (resp, options) {
|
|
|
|
var links = {};
|
|
|
|
var linkHeader = options.xhr.getResponseHeader("Link");
|
|
|
|
if (linkHeader) {
|
|
|
|
var relations = ["first", "prev", "previous", "next", "last"];
|
|
|
|
_each(linkHeader.split(","), function (linkValue) {
|
|
|
|
var linkParts = linkValue.split(";");
|
|
|
|
var url = linkParts[0].replace(URL_TRIM_RE, '');
|
|
|
|
var params = linkParts.slice(1);
|
|
|
|
_each(params, function (param) {
|
|
|
|
var paramParts = param.split("=");
|
|
|
|
var key = paramParts[0].replace(PARAM_TRIM_RE, '');
|
|
|
|
var value = paramParts[1].replace(PARAM_TRIM_RE, '');
|
|
|
|
if (key == "rel" && _contains(relations, value)) {
|
|
|
|
if (value == "previous") links.prev = url;
|
|
|
|
else links[value] = url;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
var last = links.last || '', qsi, qs;
|
|
|
|
if (qs = (qsi = last.indexOf('?')) ? last.slice(qsi + 1) : '') {
|
|
|
|
var params = queryStringToParams(qs);
|
|
|
|
|
|
|
|
var state = _clone(this.state);
|
|
|
|
var queryParams = this.queryParams;
|
|
|
|
var pageSize = state.pageSize;
|
|
|
|
|
|
|
|
var totalRecords = params[queryParams.totalRecords] * 1;
|
|
|
|
var pageNum = params[queryParams.currentPage] * 1;
|
|
|
|
var totalPages = params[queryParams.totalPages];
|
|
|
|
|
|
|
|
if (!totalRecords) {
|
|
|
|
if (pageNum) totalRecords = (state.firstPage === 0 ?
|
|
|
|
pageNum + 1 :
|
|
|
|
pageNum) * pageSize;
|
|
|
|
else if (totalPages) totalRecords = totalPages * pageSize;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (totalRecords) state.totalRecords = totalRecords;
|
|
|
|
|
|
|
|
this.state = this._checkState(state);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
delete links.last;
|
|
|
|
|
|
|
|
return links;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
Parse server response data.
|
|
|
|
|
|
|
|
This default implementation assumes the response data is in one of two
|
|
|
|
structures:
|
|
|
|
|
|
|
|
[
|
|
|
|
{}, // Your new pagination state
|
|
|
|
[{}, ...] // An array of JSON objects
|
|
|
|
]
|
|
|
|
|
|
|
|
Or,
|
|
|
|
|
|
|
|
[{}] // An array of JSON objects
|
|
|
|
|
|
|
|
The first structure is the preferred form because the pagination states
|
|
|
|
may have been updated on the server side, sending them down again allows
|
|
|
|
this collection to update its states. If the response has a pagination
|
|
|
|
state object, it is checked for errors.
|
|
|
|
|
|
|
|
The second structure is the
|
|
|
|
[Backbone.Collection#parse](http://backbonejs.org/#Collection-parse)
|
|
|
|
default.
|
|
|
|
|
|
|
|
**Note:** this method has been further simplified since 1.1.7. While
|
|
|
|
existing #parse implementations will continue to work, new code is
|
|
|
|
encouraged to override #parseState and #parseRecords instead.
|
|
|
|
|
|
|
|
@param {Object} resp The deserialized response data from the server.
|
|
|
|
|
|
|
|
@return {Array.<Object>} An array of model objects
|
|
|
|
*/
|
|
|
|
parse: function (resp) {
|
|
|
|
var newState = this.parseState(resp, _clone(this.queryParams), _clone(this.state));
|
|
|
|
if (newState) this.state = this._checkState(_extend({}, this.state, newState));
|
|
|
|
return this.parseRecords(resp);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
Parse server response for server pagination state updates.
|
|
|
|
|
|
|
|
This default implementation first checks whether the response has any
|
|
|
|
state object as documented in #parse. If it exists, a state object is
|
|
|
|
returned by mapping the server state keys to this pageable collection
|
|
|
|
instance's query parameter keys using `queryParams`.
|
|
|
|
|
|
|
|
It is __NOT__ neccessary to return a full state object complete with all
|
|
|
|
the mappings defined in #queryParams. Any state object resulted is merged
|
|
|
|
with a copy of the current pageable collection state and checked for
|
|
|
|
sanity before actually updating. Most of the time, simply providing a new
|
|
|
|
`totalRecords` value is enough to trigger a full pagination state
|
|
|
|
recalculation.
|
|
|
|
|
|
|
|
parseState: function (resp, queryParams, state) {
|
|
|
|
return {totalRecords: resp.total_entries};
|
|
|
|
}
|
|
|
|
|
|
|
|
This method __MUST__ return a new state object instead of directly
|
|
|
|
modifying the #state object. The behavior of directly modifying #state is
|
|
|
|
undefined.
|
|
|
|
|
|
|
|
@param {Object} resp The deserialized response data from the server.
|
|
|
|
@param {Object} queryParams A copy of #queryParams.
|
|
|
|
@param {Object} state A copy of #state.
|
|
|
|
|
|
|
|
@return {Object} A new (partial) state object.
|
|
|
|
*/
|
|
|
|
parseState: function (resp, queryParams, state) {
|
|
|
|
if (resp && resp.length === 2 && _isObject(resp[0]) && _isArray(resp[1])) {
|
|
|
|
|
|
|
|
var newState = _clone(state);
|
|
|
|
var serverState = resp[0];
|
|
|
|
|
|
|
|
_each(_pairs(_omit(queryParams, "directions")), function (kvp) {
|
|
|
|
var k = kvp[0], v = kvp[1];
|
|
|
|
var serverVal = serverState[v];
|
|
|
|
if (!_isUndefined(serverVal) && !_.isNull(serverVal)) newState[k] = serverState[v];
|
|
|
|
});
|
|
|
|
|
|
|
|
if (serverState.order) {
|
|
|
|
newState.order = _invert(queryParams.directions)[serverState.order] * 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return newState;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
Parse server response for an array of model objects.
|
|
|
|
|
|
|
|
This default implementation first checks whether the response has any
|
|
|
|
state object as documented in #parse. If it exists, the array of model
|
|
|
|
objects is assumed to be the second element, otherwise the entire
|
|
|
|
response is returned directly.
|
|
|
|
|
|
|
|
@param {Object} resp The deserialized response data from the server.
|
|
|
|
|
|
|
|
@return {Array.<Object>} An array of model objects
|
|
|
|
*/
|
|
|
|
parseRecords: function (resp) {
|
|
|
|
if (resp && resp.length === 2 && _isObject(resp[0]) && _isArray(resp[1])) {
|
|
|
|
return resp[1];
|
|
|
|
}
|
|
|
|
|
|
|
|
return resp;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
Fetch a page from the server in server mode, or all the pages in client
|
|
|
|
mode. Under infinite mode, the current page is refetched by default and
|
|
|
|
then reset.
|
|
|
|
|
|
|
|
The query string is constructed by translating the current pagination
|
|
|
|
state to your server API query parameter using #queryParams. The current
|
|
|
|
page will reset after fetch.
|
|
|
|
|
|
|
|
@param {Object} [options] Accepts all
|
|
|
|
[Backbone.Collection#fetch](http://backbonejs.org/#Collection-fetch)
|
|
|
|
options.
|
|
|
|
|
|
|
|
@return {XMLHttpRequest}
|
|
|
|
*/
|
|
|
|
fetch: function (options) {
|
|
|
|
|
|
|
|
options = options || {};
|
|
|
|
|
|
|
|
var state = this._checkState(this.state);
|
|
|
|
|
|
|
|
var mode = this.mode;
|
|
|
|
|
|
|
|
if (mode == "infinite" && !options.url) {
|
|
|
|
options.url = this.links[state.currentPage];
|
|
|
|
}
|
|
|
|
|
|
|
|
var data = options.data || {};
|
|
|
|
|
|
|
|
// dedup query params
|
|
|
|
var url = _result(options, "url") || _result(this, "url") || '';
|
|
|
|
var qsi = url.indexOf('?');
|
|
|
|
if (qsi != -1) {
|
|
|
|
_extend(data, queryStringToParams(url.slice(qsi + 1)));
|
|
|
|
url = url.slice(0, qsi);
|
|
|
|
}
|
|
|
|
|
|
|
|
options.url = url;
|
|
|
|
options.data = data;
|
|
|
|
|
|
|
|
// map params except directions
|
|
|
|
var queryParams = this.mode == "client" ?
|
|
|
|
_pick(this.queryParams, "sortKey", "order") :
|
|
|
|
_omit(_pick(this.queryParams, _keys(PageableProto.queryParams)),
|
|
|
|
"directions");
|
|
|
|
|
|
|
|
var i, kvp, k, v, kvps = _pairs(queryParams), thisCopy = _clone(this);
|
|
|
|
for (i = 0; i < kvps.length; i++) {
|
|
|
|
kvp = kvps[i], k = kvp[0], v = kvp[1];
|
|
|
|
v = _isFunction(v) ? v.call(thisCopy) : v;
|
|
|
|
if (state[k] != null && v != null) {
|
|
|
|
data[v] = state[k];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// fix up sorting parameters
|
|
|
|
if (state.sortKey && state.order) {
|
|
|
|
data[queryParams.order] = this.queryParams.directions[state.order + ""];
|
|
|
|
}
|
|
|
|
else if (!state.sortKey) delete data[queryParams.order];
|
|
|
|
|
|
|
|
// map extra query parameters
|
|
|
|
var extraKvps = _pairs(_omit(this.queryParams,
|
|
|
|
_keys(PageableProto.queryParams)));
|
|
|
|
for (i = 0; i < extraKvps.length; i++) {
|
|
|
|
kvp = extraKvps[i];
|
|
|
|
v = kvp[1];
|
|
|
|
v = _isFunction(v) ? v.call(thisCopy) : v;
|
|
|
|
data[kvp[0]] = v;
|
|
|
|
}
|
|
|
|
|
2013-05-29 05:43:10 +00:00
|
|
|
var fullCol = this.fullCollection, links = this.links;
|
2013-04-22 02:40:39 +00:00
|
|
|
|
|
|
|
if (mode != "server") {
|
|
|
|
|
|
|
|
var self = this;
|
|
|
|
var success = options.success;
|
|
|
|
options.success = function (col, resp, opts) {
|
|
|
|
|
|
|
|
// make sure the caller's intent is obeyed
|
|
|
|
opts = opts || {};
|
|
|
|
if (_isUndefined(options.silent)) delete opts.silent;
|
|
|
|
else opts.silent = options.silent;
|
|
|
|
|
|
|
|
var models = col.models;
|
|
|
|
var currentPage = state.currentPage;
|
|
|
|
|
2013-05-29 05:43:10 +00:00
|
|
|
if (mode == "client") fullCol.reset(models, opts);
|
2013-04-22 02:40:39 +00:00
|
|
|
else if (links[currentPage]) { // refetching a page
|
|
|
|
var pageSize = state.pageSize;
|
|
|
|
var pageStart = (state.firstPage === 0 ?
|
|
|
|
currentPage :
|
|
|
|
currentPage - 1) * pageSize;
|
2013-05-29 05:43:10 +00:00
|
|
|
var fullModels = fullCol.models;
|
2013-04-22 02:40:39 +00:00
|
|
|
var head = fullModels.slice(0, pageStart);
|
|
|
|
var tail = fullModels.slice(pageStart + pageSize);
|
|
|
|
fullModels = head.concat(models).concat(tail);
|
2013-05-29 05:43:10 +00:00
|
|
|
var updateFunc = fullCol.set || fullCol.update;
|
|
|
|
// Must silent update and trigger reset later because the event
|
|
|
|
// sychronization handler is temporarily taken out during either add
|
|
|
|
// or remove, which Collection#set does, so the pageable collection
|
|
|
|
// will be out of sync if not silenced because adding will trigger
|
|
|
|
// the sychonization event handler
|
|
|
|
updateFunc.call(fullCol, fullModels, _extend({silent: true}, opts));
|
|
|
|
fullCol.trigger("reset", fullCol, opts);
|
2013-04-22 02:40:39 +00:00
|
|
|
}
|
2013-05-29 05:43:10 +00:00
|
|
|
// fetching new page
|
|
|
|
else fullCol.add(models, _extend({at: fullCol.length}, opts));
|
2013-04-22 02:40:39 +00:00
|
|
|
|
|
|
|
if (success) success(col, resp, opts);
|
|
|
|
};
|
|
|
|
|
|
|
|
// silent the first reset from backbone
|
|
|
|
return BBColProto.fetch.call(self, _extend({}, options, {silent: true}));
|
|
|
|
}
|
|
|
|
|
|
|
|
return BBColProto.fetch.call(this, options);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
Convenient method for making a `comparator` sorted by a model attribute
|
|
|
|
identified by `sortKey` and ordered by `order`.
|
|
|
|
|
|
|
|
Like a Backbone.Collection, a Backbone.PageableCollection will maintain
|
|
|
|
the __current page__ in sorted order on the client side if a `comparator`
|
|
|
|
is attached to it. If the collection is in client mode, you can attach a
|
|
|
|
comparator to #fullCollection to have all the pages reflect the global
|
|
|
|
sorting order by specifying an option `full` to `true`. You __must__ call
|
|
|
|
`sort` manually or #fullCollection.sort after calling this method to
|
|
|
|
force a resort.
|
|
|
|
|
|
|
|
While you can use this method to sort the current page in server mode,
|
|
|
|
the sorting order may not reflect the global sorting order due to the
|
|
|
|
additions or removals of the records on the server since the last
|
|
|
|
fetch. If you want the most updated page in a global sorting order, it is
|
|
|
|
recommended that you set #state.sortKey and optionally #state.order, and
|
|
|
|
then call #fetch.
|
|
|
|
|
|
|
|
@protected
|
|
|
|
|
|
|
|
@param {string} [sortKey=this.state.sortKey] See `state.sortKey`.
|
|
|
|
@param {number} [order=this.state.order] See `state.order`.
|
|
|
|
|
|
|
|
See [Backbone.Collection.comparator](http://backbonejs.org/#Collection-comparator).
|
|
|
|
*/
|
|
|
|
_makeComparator: function (sortKey, order) {
|
|
|
|
|
|
|
|
var state = this.state;
|
|
|
|
|
|
|
|
sortKey = sortKey || state.sortKey;
|
|
|
|
order = order || state.order;
|
|
|
|
|
|
|
|
if (!sortKey || !order) return;
|
|
|
|
|
|
|
|
return function (left, right) {
|
|
|
|
var l = left.get(sortKey), r = right.get(sortKey), t;
|
|
|
|
if (order === 1) t = l, l = r, r = t;
|
|
|
|
if (l === r) return 0;
|
|
|
|
else if (l < r) return -1;
|
|
|
|
return 1;
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
Adjusts the sorting for this pageable collection.
|
|
|
|
|
|
|
|
Given a `sortKey` and an `order`, sets `state.sortKey` and
|
|
|
|
`state.order`. A comparator can be applied on the client side to sort in
|
|
|
|
the order defined if `options.side` is `"client"`. By default the
|
|
|
|
comparator is applied to the #fullCollection. Set `options.full` to
|
|
|
|
`false` to apply a comparator to the current page under any mode. Setting
|
|
|
|
`sortKey` to `null` removes the comparator from both the current page and
|
|
|
|
the full collection.
|
|
|
|
|
|
|
|
@chainable
|
|
|
|
|
|
|
|
@param {string} sortKey See `state.sortKey`.
|
|
|
|
@param {number} [order=this.state.order] See `state.order`.
|
|
|
|
@param {Object} [options]
|
|
|
|
@param {"server"|"client"} [options.side] By default, `"client"` if
|
|
|
|
`mode` is `"client"`, `"server"` otherwise.
|
|
|
|
@param {boolean} [options.full=true]
|
|
|
|
*/
|
|
|
|
setSorting: function (sortKey, order, options) {
|
|
|
|
|
|
|
|
var state = this.state;
|
|
|
|
|
|
|
|
state.sortKey = sortKey;
|
|
|
|
state.order = order = order || state.order;
|
|
|
|
|
|
|
|
var fullCollection = this.fullCollection;
|
|
|
|
|
|
|
|
var delComp = false, delFullComp = false;
|
|
|
|
|
|
|
|
if (!sortKey) delComp = delFullComp = true;
|
|
|
|
|
|
|
|
var mode = this.mode;
|
|
|
|
options = _extend({side: mode == "client" ? mode : "server", full: true},
|
|
|
|
options);
|
|
|
|
|
|
|
|
var comparator = this._makeComparator(sortKey, order);
|
|
|
|
|
|
|
|
var full = options.full, side = options.side;
|
|
|
|
|
|
|
|
if (side == "client") {
|
|
|
|
if (full) {
|
|
|
|
if (fullCollection) fullCollection.comparator = comparator;
|
|
|
|
delComp = true;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
this.comparator = comparator;
|
|
|
|
delFullComp = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (side == "server" && !full) {
|
|
|
|
this.comparator = comparator;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (delComp) delete this.comparator;
|
|
|
|
if (delFullComp && fullCollection) delete fullCollection.comparator;
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
var PageableProto = PageableCollection.prototype;
|
|
|
|
|
|
|
|
return PageableCollection;
|
|
|
|
|
|
|
|
}));
|