PageRenderTime 225ms CodeModel.GetById 80ms app.highlight 96ms RepoModel.GetById 1ms app.codeStats 1ms

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

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