PageRenderTime 2752ms CodeModel.GetById 2414ms app.highlight 199ms RepoModel.GetById 0ms app.codeStats 1ms

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

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