PageRenderTime 198ms CodeModel.GetById 59ms app.highlight 126ms RepoModel.GetById 0ms app.codeStats 1ms

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

https://gitlab.com/Blueprint-Marketing/cdnjs
JavaScript | 1293 lines | 622 code | 153 blank | 518 comment | 234 complexity | 19fc0bb357ee03f35af04b5a5ab1f504 MD5 | raw file
   1/*
   2  backbone-pageable 1.2.0
   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 ceil = Math.ceil;
  67  var max = Math.max;
  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] = this[prop];
 368        }
 369      }
 370
 371      return fullCollection;
 372    },
 373
 374    /**
 375       Factory method that returns a Backbone event handler that responses to
 376       the `add`, `remove`, `reset`, and the `sort` events. The returned event
 377       handler will synchronize the current page collection and the full
 378       collection's models.
 379
 380       @private
 381
 382       @param {Backbone.PageableCollection} pageCol
 383       @param {Backbone.Collection} fullCol
 384
 385       @return {function(string, Backbone.Model, Backbone.Collection, Object)}
 386       Collection event handler
 387    */
 388    _makeCollectionEventHandler: function (pageCol, fullCol) {
 389
 390      return function collectionEventHandler (event, model, collection, options) {
 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 ? max(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 && totalPages > 0))) {
 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 ? max(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          var serverVal = serverState[v];
1032          if (!_isUndefined(serverVal) && !_.isNull(serverVal)) newState[k] = serverState[v];
1033        });
1034
1035        if (serverState.order) {
1036          newState.order = _invert(queryParams.directions)[serverState.order] * 1;
1037        }
1038
1039        return newState;
1040      }
1041    },
1042
1043    /**
1044       Parse server response for an array of model objects.
1045
1046       This default implementation first checks whether the response has any
1047       state object as documented in #parse. If it exists, the array of model
1048       objects is assumed to be the second element, otherwise the entire
1049       response is returned directly.
1050
1051       @param {Object} resp The deserialized response data from the server.
1052
1053       @return {Array.<Object>} An array of model objects
1054     */
1055    parseRecords: function (resp) {
1056      if (resp && resp.length === 2 && _.isObject(resp[0]) && _isArray(resp[1])) {
1057        return resp[1];
1058      }
1059
1060      return resp;
1061    },
1062
1063    /**
1064       Fetch a page from the server in server mode, or all the pages in client
1065       mode. Under infinite mode, the current page is refetched by default and
1066       then reset.
1067
1068       The query string is constructed by translating the current pagination
1069       state to your server API query parameter using #queryParams.  The current
1070       page will reset after fetch.
1071
1072       @param {Object} [options] Accepts all
1073       [Backbone.Collection#fetch](http://backbonejs.org/#Collection-fetch)
1074       options.
1075
1076       @return {XMLHttpRequest}
1077    */
1078    fetch: function (options) {
1079
1080      options = options || {};
1081
1082      var state = this._checkState(this.state);
1083
1084      var mode = this.mode;
1085
1086      if (mode == "infinite" && !options.url) {
1087        options.url = this.links[state.currentPage];
1088      }
1089
1090      var data = options.data || {};
1091
1092      // dedup query params
1093      var url = _result(options, "url") || _result(this, "url") || '';
1094      var qsi = url.indexOf('?');
1095      if (qsi != -1) {
1096        _extend(data, queryStringToParams(url.slice(qsi + 1)));
1097        url = url.slice(0, qsi);
1098      }
1099
1100      options.url = url;
1101      options.data = data;
1102
1103      // map params except directions
1104      var queryParams = this.mode == "client" ?
1105        _pick(this.queryParams, "sortKey", "order") :
1106        _omit(_pick(this.queryParams, _keys(PageableProto.queryParams)),
1107              "directions");
1108
1109      var i, kvp, k, v, kvps = _pairs(queryParams), thisCopy = _clone(this);
1110      for (i = 0; i < kvps.length; i++) {
1111        kvp = kvps[i], k = kvp[0], v = kvp[1];
1112        v = _isFunction(v) ? v.call(thisCopy) : v;
1113        if (state[k] != null && v != null) {
1114          data[v] = state[k];
1115        }
1116      }
1117
1118      // fix up sorting parameters
1119      if (state.sortKey && state.order) {
1120        data[queryParams.order] = this.queryParams.directions[state.order + ""];
1121      }
1122      else if (!state.sortKey) delete data[queryParams.order];
1123
1124      // map extra query parameters
1125      var extraKvps = _pairs(_omit(this.queryParams,
1126                                   _keys(PageableProto.queryParams)));
1127      for (i = 0; i < extraKvps.length; i++) {
1128        kvp = extraKvps[i];
1129        v = kvp[1];
1130        v = _isFunction(v) ? v.call(thisCopy) : v;
1131        data[kvp[0]] = v;
1132      }
1133
1134      var fullCollection = this.fullCollection, links = this.links;
1135
1136      if (mode != "server") {
1137
1138        var self = this;
1139        var success = options.success;
1140        options.success = function (col, resp, opts) {
1141
1142          // make sure the caller's intent is obeyed
1143          opts = opts || {};
1144          if (_isUndefined(options.silent)) delete opts.silent;
1145          else opts.silent = options.silent;
1146
1147          var models = col.models;
1148          var currentPage = state.currentPage;
1149
1150          if (mode == "client") resetQuickly(fullCollection, models, opts);
1151          else if (links[currentPage]) { // refetching a page
1152            var pageSize = state.pageSize;
1153            var pageStart = (state.firstPage === 0 ?
1154                             currentPage :
1155                             currentPage - 1) * pageSize;
1156            var fullModels = fullCollection.models;
1157            var head = fullModels.slice(0, pageStart);
1158            var tail = fullModels.slice(pageStart + pageSize);
1159            fullModels = head.concat(models).concat(tail);
1160            var updateFunc = fullCollection.set || fullCollection.update;
1161            updateFunc.call(fullCollection, fullModels,
1162                            _extend({silent: true, sort: false}, opts));
1163            if (fullCollection.comparator) fullCollection.sort();
1164            fullCollection.trigger("reset", fullCollection, opts);
1165          }
1166          else { // fetching new page
1167            fullCollection.add(models, _extend({at: fullCollection.length,
1168                                                silent: true}, opts));
1169            fullCollection.trigger("reset", fullCollection, opts);
1170          }
1171
1172          if (success) success(col, resp, opts);
1173        };
1174
1175        // silent the first reset from backbone
1176        return BBColProto.fetch.call(self, _extend({}, options, {silent: true}));
1177      }
1178
1179      return BBColProto.fetch.call(this, options);
1180    },
1181
1182    /**
1183       Convenient method for making a `comparator` sorted by a model attribute
1184       identified by `sortKey` and ordered by `order`.
1185
1186       Like a Backbone.Collection, a Backbone.PageableCollection will maintain
1187       the __current page__ in sorted order on the client side if a `comparator`
1188       is attached to it. If the collection is in client mode, you can attach a
1189       comparator to #fullCollection to have all the pages reflect the global
1190       sorting order by specifying an option `full` to `true`. You __must__ call
1191       `sort` manually or #fullCollection.sort after calling this method to
1192       force a resort.
1193
1194       While you can use this method to sort the current page in server mode,
1195       the sorting order may not reflect the global sorting order due to the
1196       additions or removals of the records on the server since the last
1197       fetch. If you want the most updated page in a global sorting order, it is
1198       recommended that you set #state.sortKey and optionally #state.order, and
1199       then call #fetch.
1200
1201       @protected
1202
1203       @param {string} [sortKey=this.state.sortKey] See `state.sortKey`.
1204       @param {number} [order=this.state.order] See `state.order`.
1205
1206       See [Backbone.Collection.comparator](http://backbonejs.org/#Collection-comparator).
1207    */
1208    _makeComparator: function (sortKey, order) {
1209
1210      var state = this.state;
1211
1212      sortKey = sortKey || state.sortKey;
1213      order = order || state.order;
1214
1215      if (!sortKey || !order) return;
1216
1217      return function (left, right) {
1218        var l = left.get(sortKey), r = right.get(sortKey), t;
1219        if (order === 1) t = l, l = r, r = t;
1220        if (l === r) return 0;
1221        else if (l < r) return -1;
1222        return 1;
1223      };
1224    },
1225
1226    /**
1227       Adjusts the sorting for this pageable collection.
1228
1229       Given a `sortKey` and an `order`, sets `state.sortKey` and
1230       `state.order`. A comparator can be applied on the client side to sort in
1231       the order defined if `options.side` is `"client"`. By default the
1232       comparator is applied to the #fullCollection. Set `options.full` to
1233       `false` to apply a comparator to the current page under any mode. Setting
1234       `sortKey` to `null` removes the comparator from both the current page and
1235       the full collection.
1236
1237       @chainable
1238
1239       @param {string} sortKey See `state.sortKey`.
1240       @param {number} [order=this.state.order] See `state.order`.
1241       @param {Object} [options]
1242       @param {"server"|"client"} [options.side] By default, `"client"` if
1243       `mode` is `"client"`, `"server"` otherwise.
1244       @param {boolean} [options.full=true]
1245    */
1246    setSorting: function (sortKey, order, options) {
1247
1248      var state = this.state;
1249
1250      state.sortKey = sortKey;
1251      state.order = order = order || state.order;
1252
1253      var fullCollection = this.fullCollection;
1254
1255      var delComp = false, delFullComp = false;
1256
1257      if (!sortKey) delComp = delFullComp = true;
1258
1259      var mode = this.mode;
1260      options = _extend({side: mode == "client" ? mode : "server", full: true},
1261                        options);
1262
1263      var comparator = this._makeComparator(sortKey, order);
1264
1265      var full = options.full, side = options.side;
1266
1267      if (side == "client") {
1268        if (full) {
1269          if (fullCollection) fullCollection.comparator = comparator;
1270          delComp = true;
1271        }
1272        else {
1273          this.comparator = comparator;
1274          delFullComp = true;
1275        }
1276      }
1277      else if (side == "server" && !full) {
1278        this.comparator = comparator;
1279      }
1280
1281      if (delComp) delete this.comparator;
1282      if (delFullComp && fullCollection) delete fullCollection.comparator;
1283
1284      return this;
1285    }
1286
1287  });
1288
1289  var PageableProto = PageableCollection.prototype;
1290
1291  return PageableCollection;
1292
1293}));