backbone-rails-pageable 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MzJhNGJiMWVmMjRiMjUzM2Q3MTQ4ZWRkNTg4ZDMyZTJlMjg1MTZmYQ==
5
+ data.tar.gz: !binary |-
6
+ ZTc1MWM3NTlmYTkyMzRiNDcxNzRhMjRhYmIyNTM3M2ZiODY1NDQyNw==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ MjE3YWE5MmQ0MTgzM2E4MTNjM2I4MTdhYjdlZjY0NDhkZTQ3ZGFhMTJjZmY5
10
+ OWExZjlhMTdjNzFmZWJhMjE5Zjg4NWNlNjIwNGZiY2ViMDM2YzAwNzZlZTEy
11
+ MmZlNmI0YjY4ZGE5ZmM4YzM1N2M5MTYxMTRhNDBhYTgyNGNhNGE=
12
+ data.tar.gz: !binary |-
13
+ ZWRmMDBjMDA1ZmFkNzkwMDdhZTJmMmM4OWM1YTU1ZjZiNmJjMWI4Y2IzMTZj
14
+ NTc4N2JjYjdjMDU1NjhlNWRhODFhOTc0NWIxYmE2NjRkNjBiMTJhMTM3ZTZm
15
+ OTQ1YjRhZTMwZWE4MTc4ZjdlZDI2MTMxNjBhZDNmZTllYmM4Yzc=
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2013 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,17 @@
1
+ = BackboneRailsPageable
2
+
3
+ Simple gem that vendors backbone-pageable into the rails asset pipeline.
4
+
5
+ To install add
6
+
7
+ gem 'backbone-rails-pageable'
8
+
9
+ and add
10
+
11
+ //= require backbone-pageable
12
+
13
+ to app/assets/javascripts/application.js
14
+
15
+ Will match the version number of backbone-pageable itself
16
+
17
+ This project rocks and uses MIT-LICENSE.
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'BackboneRailsPageable'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+
24
+
25
+
26
+ Bundler::GemHelper.install_tasks
27
+
@@ -0,0 +1,5 @@
1
+ module BackboneRailsPageable
2
+ module Rails
3
+ require 'backbone-rails-pageable/engine'
4
+ end
5
+ end
@@ -0,0 +1,2 @@
1
+ module BackboneRailsPageable
2
+ end
@@ -0,0 +1,8 @@
1
+ require 'rails'
2
+
3
+ module BackboneRailsPageable
4
+ module Rails
5
+ class Engine < ::Rails::Engine
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ require 'rails'
2
+
3
+ module BackboneRailsPageable
4
+ module Rails
5
+ class Engine < ::Rails::Engine
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ module BackboneRailsPageable
2
+ VERSION = "1.3.1"
3
+ end
@@ -0,0 +1,3 @@
1
+ module BackboneRailsPageable
2
+ VERSION = "1.3.1"
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :backbone-rails-pageable do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,1347 @@
1
+ /*
2
+ backbone-pageable 1.3.1
3
+ http://github.com/wyuenho/backbone-pageable
4
+
5
+ Copyright (c) 2013 Jimmy Yuen Ho Wong
6
+ Licensed under the MIT @license.
7
+ */
8
+
9
+ (function (factory) {
10
+
11
+ // CommonJS
12
+ if (typeof exports == "object") {
13
+ module.exports = factory(require("underscore"), require("backbone"));
14
+ }
15
+ // AMD
16
+ else if (typeof define == "function" && define.amd) {
17
+ define(["underscore", "backbone"], factory);
18
+ }
19
+ // Browser
20
+ else if (typeof _ !== "undefined" && typeof Backbone !== "undefined") {
21
+ var oldPageableCollection = Backbone.PageableCollection;
22
+ var PageableCollection = Backbone.PageableCollection = factory(_, Backbone);
23
+
24
+ /**
25
+ __BROWSER ONLY__
26
+
27
+ If you already have an object named `PageableCollection` attached to the
28
+ `Backbone` module, you can use this to return a local reference to this
29
+ Backbone.PageableCollection class and reset the name
30
+ Backbone.PageableCollection to its previous definition.
31
+
32
+ // The left hand side gives you a reference to this
33
+ // Backbone.PageableCollection implementation, the right hand side
34
+ // resets Backbone.PageableCollection to your other
35
+ // Backbone.PageableCollection.
36
+ var PageableCollection = Backbone.PageableCollection.noConflict();
37
+
38
+ @static
39
+ @member Backbone.PageableCollection
40
+ @return {Backbone.PageableCollection}
41
+ */
42
+ Backbone.PageableCollection.noConflict = function () {
43
+ Backbone.PageableCollection = oldPageableCollection;
44
+ return PageableCollection;
45
+ };
46
+ }
47
+
48
+ }(function (_, Backbone) {
49
+
50
+ "use strict";
51
+
52
+ var _extend = _.extend;
53
+ var _omit = _.omit;
54
+ var _clone = _.clone;
55
+ var _each = _.each;
56
+ var _pick = _.pick;
57
+ var _contains = _.contains;
58
+ var _isEmpty = _.isEmpty;
59
+ var _pairs = _.pairs;
60
+ var _invert = _.invert;
61
+ var _isArray = _.isArray;
62
+ var _isFunction = _.isFunction;
63
+ var _isObject = _.isObject;
64
+ var _keys = _.keys;
65
+ var _isUndefined = _.isUndefined;
66
+ var _result = _.result;
67
+ var ceil = Math.ceil;
68
+ var floor = Math.floor;
69
+ var max = Math.max;
70
+
71
+ var BBColProto = Backbone.Collection.prototype;
72
+
73
+ function finiteInt (val, name) {
74
+ if (!_.isNumber(val) || _.isNaN(val) || !_.isFinite(val) || ~~val !== val) {
75
+ throw new TypeError("`" + name + "` must be a finite integer");
76
+ }
77
+ return val;
78
+ }
79
+
80
+ function queryStringToParams (qs) {
81
+ var kvp, k, v, ls, params = {}, decode = decodeURIComponent;
82
+ var kvps = qs.split('&');
83
+ for (var i = 0, l = kvps.length; i < l; i++) {
84
+ var param = kvps[i];
85
+ kvp = param.split('='), k = kvp[0], v = kvp[1] || true;
86
+ k = decode(k), ls = params[k];
87
+ if (_isArray(ls)) ls.push(v);
88
+ else if (ls) params[k] = [ls, v];
89
+ else params[k] = v;
90
+ }
91
+ return params;
92
+ }
93
+
94
+ var PARAM_TRIM_RE = /[\s'"]/g;
95
+ var URL_TRIM_RE = /[<>\s'"]/g;
96
+
97
+ /**
98
+ Drop-in replacement for Backbone.Collection. Supports server-side and
99
+ client-side pagination and sorting. Client-side mode also support fully
100
+ multi-directional synchronization of changes between pages.
101
+
102
+ @class Backbone.PageableCollection
103
+ @extends Backbone.Collection
104
+ */
105
+ var PageableCollection = Backbone.Collection.extend({
106
+
107
+ /**
108
+ The container object to store all pagination states.
109
+
110
+ You can override the default state by extending this class or specifying
111
+ them in an `options` hash to the constructor.
112
+
113
+ @property {Object} state
114
+
115
+ @property {0|1} [state.firstPage=1] The first page index. Set to 0 if
116
+ your server API uses 0-based indices. You should only override this value
117
+ during extension, initialization or reset by the server after
118
+ fetching. This value should be read only at other times.
119
+
120
+ @property {number} [state.lastPage=null] The last page index. This value
121
+ is __read only__ and it's calculated based on whether `firstPage` is 0 or
122
+ 1, during bootstrapping, fetching and resetting. Please don't change this
123
+ value under any circumstances.
124
+
125
+ @property {number} [state.currentPage=null] The current page index. You
126
+ should only override this value during extension, initialization or reset
127
+ by the server after fetching. This value should be read only at other
128
+ times. Can be a 0-based or 1-based index, depending on whether
129
+ `firstPage` is 0 or 1. If left as default, it will be set to `firstPage`
130
+ on initialization.
131
+
132
+ @property {number} [state.pageSize=25] How many records to show per
133
+ page. This value is __read only__ after initialization, if you want to
134
+ change the page size after initialization, you must call #setPageSize.
135
+
136
+ @property {number} [state.totalPages=null] How many pages there are. This
137
+ value is __read only__ and it is calculated from `totalRecords`.
138
+
139
+ @property {number} [state.totalRecords=null] How many records there
140
+ are. This value is __required__ under server mode. This value is optional
141
+ for client mode as the number will be the same as the number of models
142
+ during bootstrapping and during fetching, either supplied by the server
143
+ in the metadata, or calculated from the size of the response.
144
+
145
+ @property {string} [state.sortKey=null] The model attribute to use for
146
+ sorting.
147
+
148
+ @property {-1|0|1} [state.order=-1] The order to use for sorting. Specify
149
+ -1 for ascending order or 1 for descending order. If 0, no client side
150
+ sorting will be done and the order query parameter will not be sent to
151
+ the server during a fetch.
152
+ */
153
+ state: {
154
+ firstPage: 1,
155
+ lastPage: null,
156
+ currentPage: null,
157
+ pageSize: 25,
158
+ totalPages: null,
159
+ totalRecords: null,
160
+ sortKey: null,
161
+ order: -1
162
+ },
163
+
164
+ /**
165
+ @property {"server"|"client"|"infinite"} [mode="server"] The mode of
166
+ operations for this collection. `"server"` paginates on the server-side,
167
+ `"client"` paginates on the client-side and `"infinite"` paginates on the
168
+ server-side for APIs that do not support `totalRecords`.
169
+ */
170
+ mode: "server",
171
+
172
+ /**
173
+ A translation map to convert Backbone.PageableCollection state attributes
174
+ to the query parameters accepted by your server API.
175
+
176
+ You can override the default state by extending this class or specifying
177
+ them in `options.queryParams` object hash to the constructor.
178
+
179
+ @property {Object} queryParams
180
+ @property {string} [queryParams.currentPage="page"]
181
+ @property {string} [queryParams.pageSize="per_page"]
182
+ @property {string} [queryParams.totalPages="total_pages"]
183
+ @property {string} [queryParams.totalRecords="total_entries"]
184
+ @property {string} [queryParams.sortKey="sort_by"]
185
+ @property {string} [queryParams.order="order"]
186
+ @property {string} [queryParams.directions={"-1": "asc", "1": "desc"}] A
187
+ map for translating a Backbone.PageableCollection#state.order constant to
188
+ the ones your server API accepts.
189
+ */
190
+ queryParams: {
191
+ currentPage: "page",
192
+ pageSize: "per_page",
193
+ totalPages: "total_pages",
194
+ totalRecords: "total_entries",
195
+ sortKey: "sort_by",
196
+ order: "order",
197
+ directions: {
198
+ "-1": "asc",
199
+ "1": "desc"
200
+ }
201
+ },
202
+
203
+ /**
204
+ __CLIENT MODE ONLY__
205
+
206
+ This collection is the internal storage for the bootstrapped or fetched
207
+ models. You can use this if you want to operate on all the pages.
208
+
209
+ @property {Backbone.Collection} fullCollection
210
+ */
211
+
212
+ /**
213
+ Given a list of models or model attributues, bootstraps the full
214
+ collection in client mode or infinite mode, or just the page you want in
215
+ server mode.
216
+
217
+ If you want to initialize a collection to a different state than the
218
+ default, you can specify them in `options.state`. Any state parameters
219
+ supplied will be merged with the default. If you want to change the
220
+ default mapping from #state keys to your server API's query parameter
221
+ names, you can specifiy an object hash in `option.queryParams`. Likewise,
222
+ any mapping provided will be merged with the default. Lastly, all
223
+ Backbone.Collection constructor options are also accepted.
224
+
225
+ See:
226
+
227
+ - Backbone.PageableCollection#state
228
+ - Backbone.PageableCollection#queryParams
229
+ - [Backbone.Collection#initialize](http://backbonejs.org/#Collection-constructor)
230
+
231
+ @param {Array.<Object>} [models]
232
+
233
+ @param {Object} [options]
234
+
235
+ @param {function(*, *): number} [options.comparator] If specified, this
236
+ comparator is set to the current page under server mode, or the #fullCollection
237
+ otherwise.
238
+
239
+ @param {boolean} [options.full] If `false` and either a
240
+ `options.comparator` or `sortKey` is defined, the comparator is attached
241
+ to the current page. Default is `true` under client or infinite mode and
242
+ the comparator will be attached to the #fullCollection.
243
+
244
+ @param {Object} [options.state] The state attributes overriding the defaults.
245
+
246
+ @param {string} [options.state.sortKey] The model attribute to use for
247
+ sorting. If specified instead of `options.comparator`, a comparator will
248
+ be automatically created using this value, and optionally a sorting order
249
+ specified in `options.state.order`. The comparator is then attached to
250
+ the new collection instance.
251
+
252
+ @param {-1|1} [options.state.order] The order to use for sorting. Specify
253
+ -1 for ascending order and 1 for descending order.
254
+
255
+ @param {Object} [options.queryParam]
256
+ */
257
+ constructor: function (models, options) {
258
+
259
+ Backbone.Collection.apply(this, arguments);
260
+
261
+ options = options || {};
262
+
263
+ var mode = this.mode = options.mode || this.mode || PageableProto.mode;
264
+
265
+ var queryParams = _extend({}, PageableProto.queryParams, this.queryParams,
266
+ options.queryParams || {});
267
+
268
+ queryParams.directions = _extend({},
269
+ PageableProto.queryParams.directions,
270
+ this.queryParams.directions,
271
+ queryParams.directions || {});
272
+
273
+ this.queryParams = queryParams;
274
+
275
+ var state = this.state = _extend({}, PageableProto.state, this.state,
276
+ options.state || {});
277
+
278
+ state.currentPage = state.currentPage == null ?
279
+ state.firstPage :
280
+ state.currentPage;
281
+
282
+ if (!_isArray(models)) models = models ? [models] : [];
283
+
284
+ if (mode != "server" && state.totalRecords == null && !_isEmpty(models)) {
285
+ state.totalRecords = models.length;
286
+ }
287
+
288
+ this.switchMode(mode, _extend({fetch: false,
289
+ resetState: false,
290
+ models: models}, options));
291
+
292
+ var comparator = options.comparator;
293
+
294
+ if (state.sortKey && !comparator) {
295
+ this.setSorting(state.sortKey, state.order, options);
296
+ }
297
+
298
+ if (mode != "server") {
299
+ var fullCollection = this.fullCollection;
300
+
301
+ if (comparator && options.full) {
302
+ delete this.comparator;
303
+ fullCollection.comparator = comparator;
304
+ }
305
+
306
+ if (options.full) fullCollection.sort();
307
+
308
+ // make sure the models in the current page and full collection have the
309
+ // same references
310
+ if (models && !_isEmpty(models)) {
311
+ this.getPage(state.currentPage);
312
+ models.splice.apply(models, [0, models.length].concat(this.models));
313
+ }
314
+ }
315
+
316
+ this._initState = _clone(this.state);
317
+ },
318
+
319
+ /**
320
+ Makes a Backbone.Collection that contains all the pages.
321
+
322
+ @private
323
+ @param {Array.<Object|Backbone.Model>} models
324
+ @param {Object} options Options for Backbone.Collection constructor.
325
+ @return {Backbone.Collection}
326
+ */
327
+ _makeFullCollection: function (models, options) {
328
+
329
+ var properties = ["url", "model", "sync", "comparator"];
330
+ var thisProto = this.constructor.prototype;
331
+ var i, length, prop;
332
+
333
+ var proto = {};
334
+ for (i = 0, length = properties.length; i < length; i++) {
335
+ prop = properties[i];
336
+ if (!_isUndefined(thisProto[prop])) {
337
+ proto[prop] = thisProto[prop];
338
+ }
339
+ }
340
+
341
+ var fullCollection = new (Backbone.Collection.extend(proto))(models, options);
342
+
343
+ for (i = 0, length = properties.length; i < length; i++) {
344
+ prop = properties[i];
345
+ if (this[prop] !== thisProto[prop]) {
346
+ fullCollection[prop] = this[prop];
347
+ }
348
+ }
349
+
350
+ return fullCollection;
351
+ },
352
+
353
+ /**
354
+ Factory method that returns a Backbone event handler that responses to
355
+ the `add`, `remove`, `reset`, and the `sort` events. The returned event
356
+ handler will synchronize the current page collection and the full
357
+ collection's models.
358
+
359
+ @private
360
+
361
+ @param {Backbone.PageableCollection} pageCol
362
+ @param {Backbone.Collection} fullCol
363
+
364
+ @return {function(string, Backbone.Model, Backbone.Collection, Object)}
365
+ Collection event handler
366
+ */
367
+ _makeCollectionEventHandler: function (pageCol, fullCol) {
368
+
369
+ return function collectionEventHandler (event, model, collection, options) {
370
+
371
+ var handlers = pageCol._handlers;
372
+ _each(_keys(handlers), function (event) {
373
+ var handler = handlers[event];
374
+ pageCol.off(event, handler);
375
+ fullCol.off(event, handler);
376
+ });
377
+
378
+ var state = _clone(pageCol.state);
379
+ var firstPage = state.firstPage;
380
+ var currentPage = firstPage === 0 ?
381
+ state.currentPage :
382
+ state.currentPage - 1;
383
+ var pageSize = state.pageSize;
384
+ var pageStart = currentPage * pageSize, pageEnd = pageStart + pageSize;
385
+
386
+ if (event == "add") {
387
+ var pageIndex, fullIndex, addAt, colToAdd, options = options || {};
388
+ if (collection == fullCol) {
389
+ fullIndex = fullCol.indexOf(model);
390
+ if (fullIndex >= pageStart && fullIndex < pageEnd) {
391
+ colToAdd = pageCol;
392
+ pageIndex = addAt = fullIndex - pageStart;
393
+ }
394
+ }
395
+ else {
396
+ pageIndex = pageCol.indexOf(model);
397
+ fullIndex = pageStart + pageIndex;
398
+ colToAdd = fullCol;
399
+ var addAt = !_isUndefined(options.at) ?
400
+ options.at + pageStart :
401
+ fullIndex;
402
+ }
403
+
404
+ ++state.totalRecords;
405
+ pageCol.state = pageCol._checkState(state);
406
+
407
+ if (colToAdd) {
408
+ colToAdd.add(model, _extend({}, options || {}, {at: addAt}));
409
+ var modelToRemove = pageIndex >= pageSize ?
410
+ model :
411
+ !_isUndefined(options.at) && addAt < pageEnd && pageCol.length > pageSize ?
412
+ pageCol.at(pageSize) :
413
+ null;
414
+ if (modelToRemove) {
415
+ var addHandlers = collection._events.add || [],
416
+ popOptions = {onAdd: true};
417
+ if (addHandlers.length) {
418
+ var lastAddHandler = addHandlers[addHandlers.length - 1];
419
+ var oldCallback = lastAddHandler.callback;
420
+ lastAddHandler.callback = function () {
421
+ try {
422
+ oldCallback.apply(this, arguments);
423
+ pageCol.remove(modelToRemove, popOptions);
424
+ }
425
+ finally {
426
+ lastAddHandler.callback = oldCallback;
427
+ }
428
+ };
429
+ }
430
+ else pageCol.remove(modelToRemove, popOptions);
431
+ }
432
+ }
433
+ }
434
+
435
+ // remove the model from the other collection as well
436
+ if (event == "remove") {
437
+ if (!options.onAdd) {
438
+ // decrement totalRecords and update totalPages and lastPage
439
+ if (!--state.totalRecords) {
440
+ state.totalRecords = null;
441
+ state.totalPages = null;
442
+ }
443
+ else {
444
+ var totalPages = state.totalPages = ceil(state.totalRecords / pageSize);
445
+ state.lastPage = firstPage === 0 ? totalPages - 1 : totalPages;
446
+ if (state.currentPage > totalPages) state.currentPage = state.lastPage;
447
+ }
448
+ pageCol.state = pageCol._checkState(state);
449
+
450
+ var nextModel, removedIndex = options.index;
451
+ if (collection == pageCol) {
452
+ if (nextModel = fullCol.at(pageEnd)) pageCol.push(nextModel);
453
+ fullCol.remove(model);
454
+ }
455
+ else if (removedIndex >= pageStart && removedIndex < pageEnd) {
456
+ pageCol.remove(model);
457
+ nextModel = fullCol.at(currentPage * (pageSize + removedIndex));
458
+ if (nextModel) pageCol.push(nextModel);
459
+ }
460
+ }
461
+ else delete options.onAdd;
462
+ }
463
+
464
+ if (event == "reset") {
465
+ options = collection;
466
+ collection = model;
467
+
468
+ // Reset that's not a result of getPage
469
+ if (collection === pageCol && options.from == null &&
470
+ options.to == null) {
471
+ var head = fullCol.models.slice(0, pageStart);
472
+ var tail = fullCol.models.slice(pageStart + pageCol.models.length);
473
+ fullCol.reset(head.concat(pageCol.models).concat(tail), options);
474
+ }
475
+ else if (collection === fullCol) {
476
+ if (!(state.totalRecords = fullCol.models.length)) {
477
+ state.totalRecords = null;
478
+ state.totalPages = null;
479
+ }
480
+ if (pageCol.mode == "client") {
481
+ state.lastPage = state.currentPage = state.firstPage;
482
+ }
483
+ pageCol.state = pageCol._checkState(state);
484
+ pageCol.reset(fullCol.models.slice(pageStart, pageEnd),
485
+ _extend({}, options, {parse: false}));
486
+ }
487
+ }
488
+
489
+ if (event == "sort") {
490
+ options = collection;
491
+ collection = model;
492
+ if (collection === fullCol) {
493
+ pageCol.reset(fullCol.models.slice(pageStart, pageEnd),
494
+ _extend({}, options, {parse: false}));
495
+ }
496
+ }
497
+
498
+ _each(_keys(handlers), function (event) {
499
+ var handler = handlers[event];
500
+ _each([pageCol, fullCol], function (col) {
501
+ col.on(event, handler);
502
+ var callbacks = col._events[event] || [];
503
+ callbacks.unshift(callbacks.pop());
504
+ });
505
+ });
506
+ };
507
+ },
508
+
509
+ /**
510
+ Sanity check this collection's pagination states. Only perform checks
511
+ when all the required pagination state values are defined and not null.
512
+ If `totalPages` is undefined or null, it is set to `totalRecords` /
513
+ `pageSize`. `lastPage` is set according to whether `firstPage` is 0 or 1
514
+ when no error occurs.
515
+
516
+ @private
517
+
518
+ @throws {TypeError} If `totalRecords`, `pageSize`, `currentPage` or
519
+ `firstPage` is not a finite integer.
520
+
521
+ @throws {RangeError} If `pageSize`, `currentPage` or `firstPage` is out
522
+ of bounds.
523
+
524
+ @return {Object} Returns the `state` object if no error was found.
525
+ */
526
+ _checkState: function (state) {
527
+
528
+ var mode = this.mode;
529
+ var links = this.links;
530
+ var totalRecords = state.totalRecords;
531
+ var pageSize = state.pageSize;
532
+ var currentPage = state.currentPage;
533
+ var firstPage = state.firstPage;
534
+ var totalPages = state.totalPages;
535
+
536
+ if (totalRecords != null && pageSize != null && currentPage != null &&
537
+ firstPage != null && (mode == "infinite" ? links : true)) {
538
+
539
+ totalRecords = finiteInt(totalRecords, "totalRecords");
540
+ pageSize = finiteInt(pageSize, "pageSize");
541
+ currentPage = finiteInt(currentPage, "currentPage");
542
+ firstPage = finiteInt(firstPage, "firstPage");
543
+
544
+ if (pageSize < 1) {
545
+ throw new RangeError("`pageSize` must be >= 1");
546
+ }
547
+
548
+ totalPages = state.totalPages = ceil(totalRecords / pageSize);
549
+
550
+ if (firstPage < 0 || firstPage > 1) {
551
+ throw new RangeError("`firstPage must be 0 or 1`");
552
+ }
553
+
554
+ state.lastPage = firstPage === 0 ? max(0, totalPages - 1) : totalPages;
555
+
556
+ if (mode == "infinite") {
557
+ if (!links[currentPage + '']) {
558
+ throw new RangeError("No link found for page " + currentPage);
559
+ }
560
+ }
561
+ else if (currentPage < firstPage ||
562
+ (totalPages > 0 &&
563
+ (firstPage ? currentPage > totalPages : currentPage >= totalPages))) {
564
+ throw new RangeError("`currentPage` must be firstPage <= currentPage " +
565
+ (firstPage ? ">" : ">=") +
566
+ " totalPages if " + firstPage + "-based. Got " +
567
+ currentPage + '.');
568
+ }
569
+ }
570
+
571
+ return state;
572
+ },
573
+
574
+ /**
575
+ Change the page size of this collection.
576
+
577
+ Under most if not all circumstances, you should call this method to
578
+ change the page size of a pageable collection because it will keep the
579
+ pagination state sane. By default, the method will recalculate the
580
+ current page number to one that will retain the current page's models
581
+ when increasing the page size. When decreasing the page size, this method
582
+ will retain the last models to the current page that will fit into the
583
+ smaller page size.
584
+
585
+ If `options.first` is true, changing the page size will also reset the
586
+ current page back to the first page instead of trying to be smart.
587
+
588
+ For server mode operations, changing the page size will trigger a #fetch
589
+ and subsequently a `reset` event.
590
+
591
+ For client mode operations, changing the page size will `reset` the
592
+ current page by recalculating the current page boundary on the client
593
+ side.
594
+
595
+ If `options.fetch` is true, a fetch can be forced if the collection is in
596
+ client mode.
597
+
598
+ @param {number} pageSize The new page size to set to #state.
599
+ @param {Object} [options] {@link #fetch} options.
600
+ @param {boolean} [options.first=false] Reset the current page number to
601
+ the first page if `true`.
602
+ @param {boolean} [options.fetch] If `true`, force a fetch in client mode.
603
+
604
+ @throws {TypeError} If `pageSize` is not a finite integer.
605
+ @throws {RangeError} If `pageSize` is less than 1.
606
+
607
+ @chainable
608
+ @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
609
+ from fetch or this.
610
+ */
611
+ setPageSize: function (pageSize, options) {
612
+ pageSize = finiteInt(pageSize, "pageSize");
613
+
614
+ options = options || {first: false};
615
+
616
+ var state = this.state;
617
+ var totalPages = ceil(state.totalRecords / pageSize);
618
+ var currentPage = max(state.firstPage,
619
+ floor(totalPages *
620
+ (state.firstPage ?
621
+ state.currentPage :
622
+ state.currentPage + 1) /
623
+ state.totalPages));
624
+
625
+ state = this.state = this._checkState(_extend({}, state, {
626
+ pageSize: pageSize,
627
+ currentPage: options.first ? state.firstPage : currentPage,
628
+ totalPages: totalPages
629
+ }));
630
+
631
+ return this.getPage(state.currentPage, _omit(options, ["first"]));
632
+ },
633
+
634
+ /**
635
+ Switching between client, server and infinite mode.
636
+
637
+ If switching from client to server mode, the #fullCollection is emptied
638
+ first and then deleted and a fetch is immediately issued for the current
639
+ page from the server. Pass `false` to `options.fetch` to skip fetching.
640
+
641
+ If switching to infinite mode, and if `options.models` is given for an
642
+ array of models, #links will be populated with a URL per page, using the
643
+ default URL for this collection.
644
+
645
+ If switching from server to client mode, all of the pages are immediately
646
+ refetched. If you have too many pages, you can pass `false` to
647
+ `options.fetch` to skip fetching.
648
+
649
+ If switching to any mode from infinite mode, the #links will be deleted.
650
+
651
+ @param {"server"|"client"|"infinite"} [mode] The mode to switch to.
652
+
653
+ @param {Object} [options]
654
+
655
+ @param {boolean} [options.fetch=true] If `false`, no fetching is done.
656
+
657
+ @param {boolean} [options.resetState=true] If 'false', the state is not
658
+ reset, but checked for sanity instead.
659
+
660
+ @chainable
661
+ @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
662
+ from fetch or this if `options.fetch` is `false`.
663
+ */
664
+ switchMode: function (mode, options) {
665
+
666
+ if (!_contains(["server", "client", "infinite"], mode)) {
667
+ throw new TypeError('`mode` must be one of "server", "client" or "infinite"');
668
+ }
669
+
670
+ options = options || {fetch: true, resetState: true};
671
+
672
+ var state = this.state = options.resetState ?
673
+ _clone(this._initState) :
674
+ this._checkState(_extend({}, this.state));
675
+
676
+ this.mode = mode;
677
+
678
+ var self = this;
679
+ var fullCollection = this.fullCollection;
680
+ var handlers = this._handlers = this._handlers || {}, handler;
681
+ if (mode != "server" && !fullCollection) {
682
+ fullCollection = this._makeFullCollection(options.models || []);
683
+ fullCollection.pageableCollection = this;
684
+ this.fullCollection = fullCollection;
685
+ var allHandler = this._makeCollectionEventHandler(this, fullCollection);
686
+ _each(["add", "remove", "reset", "sort"], function (event) {
687
+ handlers[event] = handler = _.bind(allHandler, {}, event);
688
+ self.on(event, handler);
689
+ fullCollection.on(event, handler);
690
+ });
691
+ fullCollection.comparator = this._fullComparator;
692
+ }
693
+ else if (mode == "server" && fullCollection) {
694
+ _each(_keys(handlers), function (event) {
695
+ handler = handlers[event];
696
+ self.off(event, handler);
697
+ fullCollection.off(event, handler);
698
+ });
699
+ delete this._handlers;
700
+ this._fullComparator = fullCollection.comparator;
701
+ delete this.fullCollection;
702
+ }
703
+
704
+ if (mode == "infinite") {
705
+ var links = this.links = {};
706
+ var firstPage = state.firstPage;
707
+ var totalPages = ceil(state.totalRecords / state.pageSize);
708
+ var lastPage = firstPage === 0 ? max(0, totalPages - 1) : totalPages || firstPage;
709
+ for (var i = state.firstPage; i <= lastPage; i++) {
710
+ links[i] = this.url;
711
+ }
712
+ }
713
+ else if (this.links) delete this.links;
714
+
715
+ return options.fetch ?
716
+ this.fetch(_omit(options, "fetch", "resetState")) :
717
+ this;
718
+ },
719
+
720
+ /**
721
+ @return {boolean} `true` if this collection can page backward, `false`
722
+ otherwise.
723
+ */
724
+ hasPrevious: function () {
725
+ var state = this.state;
726
+ var currentPage = state.currentPage;
727
+ if (this.mode != "infinite") return currentPage > state.firstPage;
728
+ return !!this.links[currentPage - 1];
729
+ },
730
+
731
+ /**
732
+ @return {boolean} `true` if this collection can page forward, `false`
733
+ otherwise.
734
+ */
735
+ hasNext: function () {
736
+ var state = this.state;
737
+ var currentPage = this.state.currentPage;
738
+ if (this.mode != "infinite") return currentPage < state.lastPage;
739
+ return !!this.links[currentPage + 1];
740
+ },
741
+
742
+ /**
743
+ Fetch the first page in server mode, or reset the current page of this
744
+ collection to the first page in client or infinite mode.
745
+
746
+ @param {Object} options {@link #getPage} options.
747
+
748
+ @chainable
749
+ @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
750
+ from fetch or this.
751
+ */
752
+ getFirstPage: function (options) {
753
+ return this.getPage("first", options);
754
+ },
755
+
756
+ /**
757
+ Fetch the previous page in server mode, or reset the current page of this
758
+ collection to the previous page in client or infinite mode.
759
+
760
+ @param {Object} options {@link #getPage} options.
761
+
762
+ @chainable
763
+ @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
764
+ from fetch or this.
765
+ */
766
+ getPreviousPage: function (options) {
767
+ return this.getPage("prev", options);
768
+ },
769
+
770
+ /**
771
+ Fetch the next page in server mode, or reset the current page of this
772
+ collection to the next page in client mode.
773
+
774
+ @param {Object} options {@link #getPage} options.
775
+
776
+ @chainable
777
+ @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
778
+ from fetch or this.
779
+ */
780
+ getNextPage: function (options) {
781
+ return this.getPage("next", options);
782
+ },
783
+
784
+ /**
785
+ Fetch the last page in server mode, or reset the current page of this
786
+ collection to the last page in client mode.
787
+
788
+ @param {Object} options {@link #getPage} options.
789
+
790
+ @chainable
791
+ @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
792
+ from fetch or this.
793
+ */
794
+ getLastPage: function (options) {
795
+ return this.getPage("last", options);
796
+ },
797
+
798
+ /**
799
+ Given a page index, set #state.currentPage to that index. If this
800
+ collection is in server mode, fetch the page using the updated state,
801
+ otherwise, reset the current page of this collection to the page
802
+ specified by `index` in client mode. If `options.fetch` is true, a fetch
803
+ can be forced in client mode before resetting the current page. Under
804
+ infinite mode, if the index is less than the current page, a reset is
805
+ done as in client mode. If the index is greater than the current page
806
+ number, a fetch is made with the results **appended** to
807
+ #fullCollection. The current page will then be reset after fetching.
808
+
809
+ @param {number|string} index The page index to go to, or the page name to
810
+ look up from #links in infinite mode.
811
+ @param {Object} [options] {@link #fetch} options or
812
+ [reset](http://backbonejs.org/#Collection-reset) options for client mode
813
+ when `options.fetch` is `false`.
814
+ @param {boolean} [options.fetch=false] If true, force a {@link #fetch} in
815
+ client mode.
816
+
817
+ @throws {TypeError} If `index` is not a finite integer under server or
818
+ client mode, or does not yield a URL from #links under infinite mode.
819
+
820
+ @throws {RangeError} If `index` is out of bounds.
821
+
822
+ @chainable
823
+ @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
824
+ from fetch or this.
825
+ */
826
+ getPage: function (index, options) {
827
+
828
+ var mode = this.mode, fullCollection = this.fullCollection;
829
+
830
+ options = options || {fetch: false};
831
+
832
+ var state = this.state,
833
+ firstPage = state.firstPage,
834
+ currentPage = state.currentPage,
835
+ lastPage = state.lastPage,
836
+ pageSize = state.pageSize;
837
+
838
+ var pageNum = index;
839
+ switch (index) {
840
+ case "first": pageNum = firstPage; break;
841
+ case "prev": pageNum = currentPage - 1; break;
842
+ case "next": pageNum = currentPage + 1; break;
843
+ case "last": pageNum = lastPage; break;
844
+ default: pageNum = finiteInt(index, "index");
845
+ }
846
+
847
+ this.state = this._checkState(_extend({}, state, {currentPage: pageNum}));
848
+
849
+ options.from = currentPage, options.to = pageNum;
850
+
851
+ var pageStart = (firstPage === 0 ? pageNum : pageNum - 1) * pageSize;
852
+ var pageModels = fullCollection && fullCollection.length ?
853
+ fullCollection.models.slice(pageStart, pageStart + pageSize) :
854
+ [];
855
+ if ((mode == "client" || (mode == "infinite" && !_isEmpty(pageModels))) &&
856
+ !options.fetch) {
857
+ return this.reset(pageModels, _omit(options, "fetch"));
858
+ }
859
+
860
+ if (mode == "infinite") options.url = this.links[pageNum];
861
+
862
+ return this.fetch(_omit(options, "fetch"));
863
+ },
864
+
865
+ /**
866
+ Fetch the page for the provided item offset in server mode, or reset the current page of this
867
+ collection to the page for the provided item offset in client mode.
868
+
869
+ @param {Object} options {@link #getPage} options.
870
+
871
+ @chainable
872
+ @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
873
+ from fetch or this.
874
+ */
875
+ getPageByOffset: function (offset, options) {
876
+ if (offset < 0) {
877
+ throw new RangeError("`offset must be > 0`");
878
+ }
879
+ offset = finiteInt(offset);
880
+
881
+ var page = floor(offset / this.state.pageSize);
882
+ if (this.state.firstPage !== 0) page++;
883
+ if (page > this.state.lastPage) page = this.state.lastPage;
884
+ return this.getPage(page, options);
885
+ },
886
+
887
+ /**
888
+ Overidden to make `getPage` compatible with Zepto.
889
+
890
+ @param {string} method
891
+ @param {Backbone.Model|Backbone.Collection} model
892
+ @param {Object} [options]
893
+
894
+ @return {XMLHttpRequest}
895
+ */
896
+ sync: function (method, model, options) {
897
+ var self = this;
898
+ if (self.mode == "infinite") {
899
+ var success = options.success;
900
+ var currentPage = self.state.currentPage;
901
+ options.success = function (resp, status, xhr) {
902
+ var links = self.links;
903
+ var newLinks = self.parseLinks(resp, _extend({xhr: xhr}, options));
904
+ if (newLinks.first) links[self.state.firstPage] = newLinks.first;
905
+ if (newLinks.prev) links[currentPage - 1] = newLinks.prev;
906
+ if (newLinks.next) links[currentPage + 1] = newLinks.next;
907
+ if (success) success(resp, status, xhr);
908
+ };
909
+ }
910
+
911
+ return (BBColProto.sync || Backbone.sync).call(self, method, model, options);
912
+ },
913
+
914
+ /**
915
+ Parse pagination links from the server response. Only valid under
916
+ infinite mode.
917
+
918
+ Given a response body and a XMLHttpRequest object, extract pagination
919
+ links from them for infinite paging.
920
+
921
+ This default implementation parses the RFC 5988 `Link` header and extract
922
+ 3 links from it - `first`, `prev`, `next`. If a `previous` link is found,
923
+ it will be found in the `prev` key in the returned object hash. Any
924
+ subclasses overriding this method __must__ return an object hash having
925
+ only the keys above. If `first` is missing, the collection's default URL
926
+ is assumed to be the `first` URL. If `prev` or `next` is missing, it is
927
+ assumed to be `null`. An empty object hash must be returned if there are
928
+ no links found. If either the response or the header contains information
929
+ pertaining to the total number of records on the server,
930
+ #state.totalRecords must be set to that number. The default
931
+ implementation uses the `last` link from the header to calculate it.
932
+
933
+ @param {*} resp The deserialized response body.
934
+ @param {Object} [options]
935
+ @param {XMLHttpRequest} [options.xhr] The XMLHttpRequest object for this
936
+ response.
937
+ @return {Object}
938
+ */
939
+ parseLinks: function (resp, options) {
940
+ var links = {};
941
+ var linkHeader = options.xhr.getResponseHeader("Link");
942
+ if (linkHeader) {
943
+ var relations = ["first", "prev", "previous", "next", "last"];
944
+ _each(linkHeader.split(","), function (linkValue) {
945
+ var linkParts = linkValue.split(";");
946
+ var url = linkParts[0].replace(URL_TRIM_RE, '');
947
+ var params = linkParts.slice(1);
948
+ _each(params, function (param) {
949
+ var paramParts = param.split("=");
950
+ var key = paramParts[0].replace(PARAM_TRIM_RE, '');
951
+ var value = paramParts[1].replace(PARAM_TRIM_RE, '');
952
+ if (key == "rel" && _contains(relations, value)) {
953
+ if (value == "previous") links.prev = url;
954
+ else links[value] = url;
955
+ }
956
+ });
957
+ });
958
+
959
+ var last = links.last || '', qsi, qs;
960
+ if (qs = (qsi = last.indexOf('?')) ? last.slice(qsi + 1) : '') {
961
+ var params = queryStringToParams(qs);
962
+
963
+ var state = _clone(this.state);
964
+ var queryParams = this.queryParams;
965
+ var pageSize = state.pageSize;
966
+
967
+ var totalRecords = params[queryParams.totalRecords] * 1;
968
+ var pageNum = params[queryParams.currentPage] * 1;
969
+ var totalPages = params[queryParams.totalPages];
970
+
971
+ if (!totalRecords) {
972
+ if (pageNum) totalRecords = (state.firstPage === 0 ?
973
+ pageNum + 1 :
974
+ pageNum) * pageSize;
975
+ else if (totalPages) totalRecords = totalPages * pageSize;
976
+ }
977
+
978
+ if (totalRecords) state.totalRecords = totalRecords;
979
+
980
+ this.state = this._checkState(state);
981
+ }
982
+ }
983
+
984
+ delete links.last;
985
+
986
+ return links;
987
+ },
988
+
989
+ /**
990
+ Parse server response data.
991
+
992
+ This default implementation assumes the response data is in one of two
993
+ structures:
994
+
995
+ [
996
+ {}, // Your new pagination state
997
+ [{}, ...] // An array of JSON objects
998
+ ]
999
+
1000
+ Or,
1001
+
1002
+ [{}] // An array of JSON objects
1003
+
1004
+ The first structure is the preferred form because the pagination states
1005
+ may have been updated on the server side, sending them down again allows
1006
+ this collection to update its states. If the response has a pagination
1007
+ state object, it is checked for errors.
1008
+
1009
+ The second structure is the
1010
+ [Backbone.Collection#parse](http://backbonejs.org/#Collection-parse)
1011
+ default.
1012
+
1013
+ **Note:** this method has been further simplified since 1.1.7. While
1014
+ existing #parse implementations will continue to work, new code is
1015
+ encouraged to override #parseState and #parseRecords instead.
1016
+
1017
+ @param {Object} resp The deserialized response data from the server.
1018
+ @param {Object} the options for the ajax request
1019
+
1020
+ @return {Array.<Object>} An array of model objects
1021
+ */
1022
+ parse: function (resp, options) {
1023
+ var newState = this.parseState(resp, _clone(this.queryParams), _clone(this.state), options);
1024
+ if (newState) this.state = this._checkState(_extend({}, this.state, newState));
1025
+ return this.parseRecords(resp, options);
1026
+ },
1027
+
1028
+ /**
1029
+ Parse server response for server pagination state updates.
1030
+
1031
+ This default implementation first checks whether the response has any
1032
+ state object as documented in #parse. If it exists, a state object is
1033
+ returned by mapping the server state keys to this pageable collection
1034
+ instance's query parameter keys using `queryParams`.
1035
+
1036
+ It is __NOT__ neccessary to return a full state object complete with all
1037
+ the mappings defined in #queryParams. Any state object resulted is merged
1038
+ with a copy of the current pageable collection state and checked for
1039
+ sanity before actually updating. Most of the time, simply providing a new
1040
+ `totalRecords` value is enough to trigger a full pagination state
1041
+ recalculation.
1042
+
1043
+ parseState: function (resp, queryParams, state, options) {
1044
+ return {totalRecords: resp.total_entries};
1045
+ }
1046
+
1047
+ If you want to use header fields use:
1048
+
1049
+ parseState: function (resp, queryParams, state, options) {
1050
+ return {totalRecords: options.xhr.getResponseHeader("X-total")};
1051
+ }
1052
+
1053
+ This method __MUST__ return a new state object instead of directly
1054
+ modifying the #state object. The behavior of directly modifying #state is
1055
+ undefined.
1056
+
1057
+ @param {Object} resp The deserialized response data from the server.
1058
+ @param {Object} queryParams A copy of #queryParams.
1059
+ @param {Object} state A copy of #state.
1060
+ @param {Object} [options] The options passed through from
1061
+ `parse`. (backbone >= 0.9.10 only)
1062
+
1063
+ @return {Object} A new (partial) state object.
1064
+ */
1065
+ parseState: function (resp, queryParams, state, options) {
1066
+ if (resp && resp.length === 2 && _isObject(resp[0]) && _isArray(resp[1])) {
1067
+
1068
+ var newState = _clone(state);
1069
+ var serverState = resp[0];
1070
+
1071
+ _each(_pairs(_omit(queryParams, "directions")), function (kvp) {
1072
+ var k = kvp[0], v = kvp[1];
1073
+ var serverVal = serverState[v];
1074
+ if (!_isUndefined(serverVal) && !_.isNull(serverVal)) newState[k] = serverState[v];
1075
+ });
1076
+
1077
+ if (serverState.order) {
1078
+ newState.order = _invert(queryParams.directions)[serverState.order] * 1;
1079
+ }
1080
+
1081
+ return newState;
1082
+ }
1083
+ },
1084
+
1085
+ /**
1086
+ Parse server response for an array of model objects.
1087
+
1088
+ This default implementation first checks whether the response has any
1089
+ state object as documented in #parse. If it exists, the array of model
1090
+ objects is assumed to be the second element, otherwise the entire
1091
+ response is returned directly.
1092
+
1093
+ @param {Object} resp The deserialized response data from the server.
1094
+ @param {Object} [options] The options passed through from the
1095
+ `parse`. (backbone >= 0.9.10 only)
1096
+
1097
+ @return {Array.<Object>} An array of model objects
1098
+ */
1099
+ parseRecords: function (resp, options) {
1100
+ if (resp && resp.length === 2 && _isObject(resp[0]) && _isArray(resp[1])) {
1101
+ return resp[1];
1102
+ }
1103
+
1104
+ return resp;
1105
+ },
1106
+
1107
+ /**
1108
+ Fetch a page from the server in server mode, or all the pages in client
1109
+ mode. Under infinite mode, the current page is refetched by default and
1110
+ then reset.
1111
+
1112
+ The query string is constructed by translating the current pagination
1113
+ state to your server API query parameter using #queryParams. The current
1114
+ page will reset after fetch.
1115
+
1116
+ @param {Object} [options] Accepts all
1117
+ [Backbone.Collection#fetch](http://backbonejs.org/#Collection-fetch)
1118
+ options.
1119
+
1120
+ @return {XMLHttpRequest}
1121
+ */
1122
+ fetch: function (options) {
1123
+
1124
+ options = options || {};
1125
+
1126
+ var state = this._checkState(this.state);
1127
+
1128
+ var mode = this.mode;
1129
+
1130
+ if (mode == "infinite" && !options.url) {
1131
+ options.url = this.links[state.currentPage];
1132
+ }
1133
+
1134
+ var data = options.data || {};
1135
+
1136
+ // dedup query params
1137
+ var url = _result(options, "url") || _result(this, "url") || '';
1138
+ var qsi = url.indexOf('?');
1139
+ if (qsi != -1) {
1140
+ _extend(data, queryStringToParams(url.slice(qsi + 1)));
1141
+ url = url.slice(0, qsi);
1142
+ }
1143
+
1144
+ options.url = url;
1145
+ options.data = data;
1146
+
1147
+ // map params except directions
1148
+ var queryParams = this.mode == "client" ?
1149
+ _pick(this.queryParams, "sortKey", "order") :
1150
+ _omit(_pick(this.queryParams, _keys(PageableProto.queryParams)),
1151
+ "directions");
1152
+
1153
+ var i, kvp, k, v, kvps = _pairs(queryParams), thisCopy = _clone(this);
1154
+ for (i = 0; i < kvps.length; i++) {
1155
+ kvp = kvps[i], k = kvp[0], v = kvp[1];
1156
+ v = _isFunction(v) ? v.call(thisCopy) : v;
1157
+ if (state[k] != null && v != null) {
1158
+ data[v] = state[k];
1159
+ }
1160
+ }
1161
+
1162
+ // fix up sorting parameters
1163
+ if (state.sortKey && state.order) {
1164
+ data[queryParams.order] = this.queryParams.directions[state.order + ""];
1165
+ }
1166
+ else if (!state.sortKey) delete data[queryParams.order];
1167
+
1168
+ // map extra query parameters
1169
+ var extraKvps = _pairs(_omit(this.queryParams,
1170
+ _keys(PageableProto.queryParams)));
1171
+ for (i = 0; i < extraKvps.length; i++) {
1172
+ kvp = extraKvps[i];
1173
+ v = kvp[1];
1174
+ v = _isFunction(v) ? v.call(thisCopy) : v;
1175
+ if (v != null) data[kvp[0]] = v;
1176
+ }
1177
+
1178
+ var fullCol = this.fullCollection, links = this.links;
1179
+
1180
+ if (mode != "server") {
1181
+
1182
+ var self = this;
1183
+ var success = options.success;
1184
+ options.success = function (col, resp, opts) {
1185
+
1186
+ // make sure the caller's intent is obeyed
1187
+ opts = opts || {};
1188
+ if (_isUndefined(options.silent)) delete opts.silent;
1189
+ else opts.silent = options.silent;
1190
+
1191
+ var models = col.models;
1192
+ var currentPage = state.currentPage;
1193
+
1194
+ if (mode == "client") fullCol.reset(models, opts);
1195
+ else if (links[currentPage]) { // refetching a page
1196
+ var pageSize = state.pageSize;
1197
+ var pageStart = (state.firstPage === 0 ?
1198
+ currentPage :
1199
+ currentPage - 1) * pageSize;
1200
+ var fullModels = fullCol.models;
1201
+ var head = fullModels.slice(0, pageStart);
1202
+ var tail = fullModels.slice(pageStart + pageSize);
1203
+ fullModels = head.concat(models).concat(tail);
1204
+ var updateFunc = fullCol.set || fullCol.update;
1205
+ // Must silent update and trigger reset later because the event
1206
+ // sychronization handler is temporarily taken out during either add
1207
+ // or remove, which Collection#set does, so the pageable collection
1208
+ // will be out of sync if not silenced because adding will trigger
1209
+ // the sychonization event handler
1210
+ updateFunc.call(fullCol, fullModels, _extend({silent: true}, opts));
1211
+ fullCol.trigger("reset", fullCol, opts);
1212
+ }
1213
+ // fetching new page
1214
+ else fullCol.add(models, _extend({at: fullCol.length}, opts));
1215
+
1216
+ if (success) success(col, resp, opts);
1217
+ };
1218
+
1219
+ // silent the first reset from backbone
1220
+ return BBColProto.fetch.call(self, _extend({}, options, {silent: true}));
1221
+ }
1222
+
1223
+ return BBColProto.fetch.call(this, options);
1224
+ },
1225
+
1226
+ /**
1227
+ Convenient method for making a `comparator` sorted by a model attribute
1228
+ identified by `sortKey` and ordered by `order`.
1229
+
1230
+ Like a Backbone.Collection, a Backbone.PageableCollection will maintain
1231
+ the __current page__ in sorted order on the client side if a `comparator`
1232
+ is attached to it. If the collection is in client mode, you can attach a
1233
+ comparator to #fullCollection to have all the pages reflect the global
1234
+ sorting order by specifying an option `full` to `true`. You __must__ call
1235
+ `sort` manually or #fullCollection.sort after calling this method to
1236
+ force a resort.
1237
+
1238
+ While you can use this method to sort the current page in server mode,
1239
+ the sorting order may not reflect the global sorting order due to the
1240
+ additions or removals of the records on the server since the last
1241
+ fetch. If you want the most updated page in a global sorting order, it is
1242
+ recommended that you set #state.sortKey and optionally #state.order, and
1243
+ then call #fetch.
1244
+
1245
+ @protected
1246
+
1247
+ @param {string} [sortKey=this.state.sortKey] See `state.sortKey`.
1248
+ @param {number} [order=this.state.order] See `state.order`.
1249
+ @param {(function(Backbone.Model, string): Object) | string} [sortValue] See #setSorting.
1250
+
1251
+ See [Backbone.Collection.comparator](http://backbonejs.org/#Collection-comparator).
1252
+ */
1253
+ _makeComparator: function (sortKey, order, sortValue) {
1254
+ var state = this.state;
1255
+
1256
+ sortKey = sortKey || state.sortKey;
1257
+ order = order || state.order;
1258
+
1259
+ if (!sortKey || !order) return;
1260
+
1261
+ if (!sortValue) sortValue = function (model, attr) {
1262
+ return model.get(attr);
1263
+ };
1264
+
1265
+ return function (left, right) {
1266
+ var l = sortValue(left, sortKey), r = sortValue(right, sortKey), t;
1267
+ if (order === 1) t = l, l = r, r = t;
1268
+ if (l === r) return 0;
1269
+ else if (l < r) return -1;
1270
+ return 1;
1271
+ };
1272
+ },
1273
+
1274
+ /**
1275
+ Adjusts the sorting for this pageable collection.
1276
+
1277
+ Given a `sortKey` and an `order`, sets `state.sortKey` and
1278
+ `state.order`. A comparator can be applied on the client side to sort in
1279
+ the order defined if `options.side` is `"client"`. By default the
1280
+ comparator is applied to the #fullCollection. Set `options.full` to
1281
+ `false` to apply a comparator to the current page under any mode. Setting
1282
+ `sortKey` to `null` removes the comparator from both the current page and
1283
+ the full collection.
1284
+
1285
+ If a `sortValue` function is given, it will be passed the `(model,
1286
+ sortKey)` arguments and is used to extract a value from the model during
1287
+ comparison sorts. If `sortValue` is not given, `model.get(sortKey)` is
1288
+ used for sorting.
1289
+
1290
+ @chainable
1291
+
1292
+ @param {string} sortKey See `state.sortKey`.
1293
+ @param {number} [order=this.state.order] See `state.order`.
1294
+ @param {Object} [options]
1295
+ @param {"server"|"client"} [options.side] By default, `"client"` if
1296
+ `mode` is `"client"`, `"server"` otherwise.
1297
+ @param {boolean} [options.full=true]
1298
+ @param {(function(Backbone.Model, string): Object) | string} [options.sortValue]
1299
+ */
1300
+ setSorting: function (sortKey, order, options) {
1301
+
1302
+ var state = this.state;
1303
+
1304
+ state.sortKey = sortKey;
1305
+ state.order = order = order || state.order;
1306
+
1307
+ var fullCollection = this.fullCollection;
1308
+
1309
+ var delComp = false, delFullComp = false;
1310
+
1311
+ if (!sortKey) delComp = delFullComp = true;
1312
+
1313
+ var mode = this.mode;
1314
+ options = _extend({side: mode == "client" ? mode : "server", full: true},
1315
+ options);
1316
+
1317
+ var comparator = this._makeComparator(sortKey, order, options.sortValue);
1318
+
1319
+ var full = options.full, side = options.side;
1320
+
1321
+ if (side == "client") {
1322
+ if (full) {
1323
+ if (fullCollection) fullCollection.comparator = comparator;
1324
+ delComp = true;
1325
+ }
1326
+ else {
1327
+ this.comparator = comparator;
1328
+ delFullComp = true;
1329
+ }
1330
+ }
1331
+ else if (side == "server" && !full) {
1332
+ this.comparator = comparator;
1333
+ }
1334
+
1335
+ if (delComp) delete this.comparator;
1336
+ if (delFullComp && fullCollection) delete fullCollection.comparator;
1337
+
1338
+ return this;
1339
+ }
1340
+
1341
+ });
1342
+
1343
+ var PageableProto = PageableCollection.prototype;
1344
+
1345
+ return PageableCollection;
1346
+
1347
+ }));