PageRenderTime 449ms CodeModel.GetById 121ms app.highlight 195ms RepoModel.GetById 117ms app.codeStats 1ms

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

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