PageRenderTime 110ms CodeModel.GetById 22ms app.highlight 77ms RepoModel.GetById 1ms app.codeStats 0ms

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

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