PageRenderTime 122ms CodeModel.GetById 11ms app.highlight 94ms RepoModel.GetById 1ms app.codeStats 1ms

/ajax/libs/backbone-pageable/1.4.6/backbone-pageable.js

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