PageRenderTime 91ms CodeModel.GetById 1ms app.highlight 75ms RepoModel.GetById 1ms app.codeStats 0ms

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

https://gitlab.com/alidz1982/cdnjs
JavaScript | 1293 lines | 622 code | 154 blank | 517 comment | 231 complexity | 4a067a702318014874a3f4de64137805 MD5 | raw file
   1/*
   2  backbone-pageable
   3  http://github.com/wyuenho/backbone-pageable
   4
   5  Copyright (c) 2013 Jimmy Yuen Ho Wong
   6  Licensed under the MIT @license.
   7*/
   8
   9(function (factory) {
  10
  11  // CommonJS
  12  if (typeof exports == "object") {
  13    module.exports = factory(require("underscore"), require("backbone"));
  14  }
  15  // AMD
  16  else if (typeof define == "function" && define.amd) {
  17    define(["underscore", "backbone"], factory);
  18  }
  19  // Browser
  20  else if (typeof _ !== "undefined" && typeof Backbone !== "undefined") {
  21    var oldPageableCollection = Backbone.PageableCollection;
  22    var PageableCollection = Backbone.PageableCollection = factory(_, Backbone);
  23
  24    /**
  25       __BROWSER ONLY__
  26
  27       If you already have an object named `PageableCollection` attached to the
  28       `Backbone` module, you can use this to return a local reference to this
  29       Backbone.PageableCollection class and reset the name
  30       Backbone.PageableCollection to its previous definition.
  31
  32           // The left hand side gives you a reference to this
  33           // Backbone.PageableCollection implementation, the right hand side
  34           // resets Backbone.PageableCollection to your other
  35           // Backbone.PageableCollection.
  36           var PageableCollection = Backbone.PageableCollection.noConflict();
  37
  38       @static
  39       @member Backbone.PageableCollection
  40       @return {Backbone.PageableCollection}
  41    */
  42    Backbone.PageableCollection.noConflict = function () {
  43      Backbone.PageableCollection = oldPageableCollection;
  44      return PageableCollection;
  45    };
  46  }
  47
  48}(function (_, Backbone) {
  49
  50  "use strict";
  51
  52  var _extend = _.extend;
  53  var _omit = _.omit;
  54  var _clone = _.clone;
  55  var _each = _.each;
  56  var _pick = _.pick;
  57  var _contains = _.contains;
  58  var _isEmpty = _.isEmpty;
  59  var _pairs = _.pairs;
  60  var _invert = _.invert;
  61  var _isArray = _.isArray;
  62  var _isFunction = _.isFunction;
  63  var _keys = _.keys;
  64  var _isUndefined = _.isUndefined;
  65  var _result = _.result;
  66  var _bind = _.bind;
  67  var ceil = Math.ceil;
  68  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] = prop;
 369        }
 370      }
 371
 372      return fullCollection;
 373    },
 374
 375    /**
 376       Factory method that returns a Backbone event handler that responses to
 377       the `all` event. The returned event handler will synchronize the current
 378       page collection and the full collection's models.
 379
 380       @private
 381
 382       @param {Backbone.PageableCollection} pageCol
 383       @param {Backbone.Collection} fullCol
 384
 385       @return {function(string, Backbone.Model, Backbone.Collection, Object)}
 386       Collection event handler
 387    */
 388    _makeCollectionEventHandler: function (pageCol, fullCol) {
 389
 390      return function collectionEventHandler (event, model, collection, options) {
 391
 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 linkHeader = options.xhr.getResponseHeader("Link");
 906      var relations = ["first", "prev", "previous", "next", "last"];
 907      var links = {};
 908      _each(linkHeader.split(","), function (linkValue) {
 909        var linkParts = linkValue.split(";");
 910        var url = linkParts[0].replace(URL_TRIM_RE, '');
 911        var params = linkParts.slice(1);
 912        _each(params, function (param) {
 913          var paramParts = param.split("=");
 914          var key = paramParts[0].replace(PARAM_TRIM_RE, '');
 915          var value = paramParts[1].replace(PARAM_TRIM_RE, '');
 916          if (key == "rel" && _contains(relations, value)) {
 917            if (value == "previous") links.prev = url;
 918            else links[value] = url;
 919          }
 920        });
 921      });
 922
 923      var last = links.last || '', qsi, qs;
 924      if (qs = (qsi = last.indexOf('?')) ? last.slice(qsi + 1) : '') {
 925        var params = queryStringToParams(qs);
 926
 927        var state = _clone(this.state);
 928        var queryParams = this.queryParams;
 929        var pageSize = state.pageSize;
 930
 931        var totalRecords = params[queryParams.totalRecords] * 1;
 932        var pageNum = params[queryParams.currentPage] * 1;
 933        var totalPages = params[queryParams.totalPages];
 934
 935        if (!totalRecords) {
 936          if (pageNum) totalRecords = (state.firstPage === 0 ?
 937                                       pageNum + 1 :
 938                                       pageNum) * pageSize;
 939          else if (totalPages) totalRecords = totalPages * pageSize;
 940        }
 941
 942        if (totalRecords) state.totalRecords = totalRecords;
 943
 944        this.state = this._checkState(state);
 945      }
 946
 947      delete links.last;
 948
 949      return links;
 950    },
 951
 952    /**
 953       Parse server response data.
 954
 955       This default implementation assumes the response data is in one of two
 956       structures:
 957
 958           [
 959             {}, // Your new pagination state
 960             [{}, ...] // An array of JSON objects
 961           ]
 962
 963       Or,
 964
 965           [{}] // An array of JSON objects
 966
 967       The first structure is the preferred form because the pagination states
 968       may have been updated on the server side, sending them down again allows
 969       this collection to update its states. If the response has a pagination
 970       state object, it is checked for errors.
 971
 972       The second structure is the
 973       [Backbone.Collection#parse](http://backbonejs.org/#Collection-parse)
 974       default.
 975
 976       **Note:** this method has been further simplified since 1.1.7. While
 977       existing #parse implementations will continue to work, new code is
 978       encouraged to override #parseState and #parseRecords instead.
 979
 980       @param {Object} resp The deserialized response data from the server.
 981
 982       @return {Array.<Object>} An array of model objects
 983    */
 984    parse: function (resp) {
 985      var newState = this.parseState(resp, _clone(this.queryParams), _clone(this.state));
 986      if (newState) this.state = this._checkState(_extend({}, this.state, newState));
 987      return this.parseRecords(resp);
 988    },
 989
 990    /**
 991       Parse server response for server pagination state updates.
 992
 993       This default implementation first checks whether the response has any
 994       state object as documented in #parse. If it exists, a state object is
 995       returned by mapping the server state keys to this pageable collection
 996       instance's query parameter keys using `queryParams`.
 997
 998       It is __NOT__ neccessary to return a full state object complete with all
 999       the mappings defined in #queryParams. Any state object resulted is merged
1000       with a copy of the current pageable collection state and checked for
1001       sanity before actually updating. Most of the time, simply providing a new
1002       `totalRecords` value is enough to trigger a full pagination state
1003       recalculation.
1004
1005           parseState: function (resp, queryParams, state) {
1006             return {totalRecords: resp.total_entries};
1007           }
1008
1009       __Note__: `totalRecords` cannot be set to 0 for compatibility reasons,
1010       use `null` instead of 0 for all cases where you would like to set it to
1011       0. You can do this either on the server-side or in your overridden #parseState
1012       method.
1013
1014       This method __MUST__ return a new state object instead of directly
1015       modifying the #state object. The behavior of directly modifying #state is
1016       undefined.
1017
1018       @param {Object} resp The deserialized response data from the server.
1019       @param {Object} queryParams A copy of #queryParams.
1020       @param {Object} state A copy of #state.
1021
1022       @return {Object} A new (partial) state object.
1023     */
1024    parseState: function (resp, queryParams, state) {
1025      if (resp && resp.length === 2 && _.isObject(resp[0]) && _isArray(resp[1])) {
1026
1027        var newState = _clone(state);
1028        var serverState = resp[0];
1029
1030        _each(_pairs(_omit(queryParams, "directions")), function (kvp) {
1031          var k = kvp[0], v = kvp[1];
1032          var serverVal = serverState[v];
1033          if (!_isUndefined(serverVal) && !_.isNull(serverVal)) newState[k] = serverState[v];
1034        });
1035
1036        if (serverState.order) {
1037          newState.order = _invert(queryParams.directions)[serverState.order] * 1;
1038        }
1039
1040        return newState;
1041      }
1042    },
1043
1044    /**
1045       Parse server response for an array of model objects.
1046
1047       This default implementation first checks whether the response has any
1048       state object as documented in #parse. If it exists, the array of model
1049       objects is assumed to be the second element, otherwise the entire
1050       response is returned directly.
1051
1052       @param {Object} resp The deserialized response data from the server.
1053
1054       @return {Array.<Object>} An array of model objects
1055     */
1056    parseRecords: function (resp) {
1057      if (resp && resp.length === 2 && _.isObject(resp[0]) && _isArray(resp[1])) {
1058        return resp[1];
1059      }
1060
1061      return resp;
1062    },
1063
1064    /**
1065       Fetch a page from the server in server mode, or all the pages in client
1066       mode. Under infinite mode, the current page is refetched by default and
1067       then reset.
1068
1069       The query string is constructed by translating the current pagination
1070       state to your server API query parameter using #queryParams.  The current
1071       page will reset after fetch.
1072
1073       @param {Object} [options] Accepts all
1074       [Backbone.Collection#fetch](http://backbonejs.org/#Collection-fetch)
1075       options.
1076
1077       @return {XMLHttpRequest}
1078    */
1079    fetch: function (options) {
1080
1081      options = options || {};
1082
1083      var state = this._checkState(this.state);
1084
1085      var mode = this.mode;
1086
1087      if (mode == "infinite" && !options.url) {
1088        options.url = this.links[state.currentPage];
1089      }
1090
1091      var data = options.data || {};
1092
1093      // dedup query params
1094      var url = _result(options, "url") || _result(this, "url") || '';
1095      var qsi = url.indexOf('?');
1096      if (qsi != -1) {
1097        _extend(data, queryStringToParams(url.slice(qsi + 1)));
1098        url = url.slice(0, qsi);
1099      }
1100
1101      options.url = url;
1102      options.data = data;
1103
1104      // map params except directions
1105      var queryParams = this.mode == "client" ?
1106        _pick(this.queryParams, "sortKey", "order") :
1107        _omit(_pick(this.queryParams, _keys(PageableProto.queryParams)),
1108              "directions");
1109
1110      var i, kvp, k, v, kvps = _pairs(queryParams), thisCopy = _clone(this);
1111      for (i = 0; i < kvps.length; i++) {
1112        kvp = kvps[i], k = kvp[0], v = kvp[1];
1113        v = _isFunction(v) ? v.call(thisCopy) : v;
1114        if (state[k] != null && v != null) {
1115          data[v] = state[k];
1116        }
1117      }
1118
1119      // fix up sorting parameters
1120      if (state.sortKey && state.order) {
1121        data[queryParams.order] = this.queryParams.directions[state.order + ""];
1122      }
1123      else if (!state.sortKey) delete data[queryParams.order];
1124
1125      // map extra query parameters
1126      var extraKvps = _pairs(_omit(this.queryParams,
1127                                   _keys(PageableProto.queryParams)));
1128      for (i = 0; i < extraKvps.length; i++) {
1129        kvp = extraKvps[i];
1130        v = kvp[1];
1131        v = _isFunction(v) ? v.call(thisCopy) : v;
1132        data[kvp[0]] = v;
1133      }
1134
1135      var fullCollection = this.fullCollection, links = this.links;
1136
1137      if (mode != "server") {
1138
1139        var self = this;
1140        var success = options.success;
1141        options.success = function (col, resp, opts) {
1142
1143          // make sure the caller's intent is obeyed
1144          opts = opts || {};
1145          if (_isUndefined(options.silent)) delete opts.silent;
1146          else opts.silent = options.silent;
1147
1148          var models = col.models;
1149          var currentPage = state.currentPage;
1150
1151          if (mode == "client") resetQuickly(fullCollection, models, opts);
1152          else if (links[currentPage]) { // refetching a page
1153            var pageSize = state.pageSize;
1154            var pageStart = (state.firstPage === 0 ?
1155                             currentPage :
1156                             currentPage - 1) * pageSize;
1157            var fullModels = fullCollection.models;
1158            var head = fullModels.slice(0, pageStart);
1159            var tail = fullModels.slice(pageStart + pageSize);
1160            fullModels = head.concat(models).concat(tail);
1161            fullCollection.update(fullModels,
1162                                  _extend({silent: true, sort: false}, opts));
1163            if (fullCollection.comparator) fullCollection.sort();
1164            fullCollection.trigger("reset", fullCollection, opts);
1165          }
1166          else { // fetching new page
1167            fullCollection.add(models, _extend({at: fullCollection.length,
1168                                                silent: true}, opts));
1169            fullCollection.trigger("reset", fullCollection, opts);
1170          }
1171
1172          if (success) success(col, resp, opts);
1173        };
1174
1175        // silent the first reset from backbone
1176        return BBColProto.fetch.call(self, _extend({}, options, {silent: true}));
1177      }
1178
1179      return BBColProto.fetch.call(this, options);
1180    },
1181
1182    /**
1183       Convenient method for making a `comparator` sorted by a model attribute
1184       identified by `sortKey` and ordered by `order`.
1185
1186       Like a Backbone.Collection, a Backbone.PageableCollection will maintain
1187       the __current page__ in sorted order on the client side if a `comparator`
1188       is attached to it. If the collection is in client mode, you can attach a
1189       comparator to #fullCollection to have all the pages reflect the global
1190       sorting order by specifying an option `full` to `true`. You __must__ call
1191       `sort` manually or #fullCollection.sort after calling this method to
1192       force a resort.
1193
1194       While you can use this method to sort the current page in server mode,
1195       the sorting order may not reflect the global sorting order due to the
1196       additions or removals of the records on the server since the last
1197       fetch. If you want the most updated page in a global sorting order, it is
1198       recommended that you set #state.sortKey and optionally #state.order, and
1199       then call #fetch.
1200
1201       @protected
1202
1203       @param {string} [sortKey=this.state.sortKey] See `state.sortKey`.
1204       @param {number} [order=this.state.order] See `state.order`.
1205
1206       See [Backbone.Collection.comparator](http://backbonejs.org/#Collection-comparator).
1207    */
1208    _makeComparator: function (sortKey, order) {
1209
1210      var state = this.state;
1211
1212      sortKey = sortKey || state.sortKey;
1213      order = order || state.order;
1214
1215      if (!sortKey || !order) return;
1216
1217      return function (left, right) {
1218        var l = left.get(sortKey), r = right.get(sortKey), t;
1219        if (order === 1) t = l, l = r, r = t;
1220        if (l === r) return 0;
1221        else if (l < r) return -1;
1222        return 1;
1223      };
1224    },
1225
1226    /**
1227       Adjusts the sorting for this pageable collection.
1228
1229       Given a `sortKey` and an `order`, sets `state.sortKey` and
1230       `state.order`. A comparator can be applied on the client side to sort in
1231       the order defined if `options.side` is `"client"`. By default the
1232       comparator is applied to the #fullCollection. Set `options.full` to
1233       `false` to apply a comparator to the current page under any mode. Setting
1234       `sortKey` to `null` removes the comparator from both the current page and
1235       the full collection.
1236
1237       @chainable
1238
1239       @param {string} sortKey See `state.sortKey`.
1240       @param {number} [order=this.state.order] See `state.order`.
1241       @param {Object} [options]
1242       @param {"server"|"client"} [options.side] By default, `"client"` if
1243       `mode` is `"client"`, `"server"` otherwise.
1244       @param {boolean} [options.full=true]
1245    */
1246    setSorting: function (sortKey, order, options) {
1247
1248      var state = this.state;
1249
1250      state.sortKey = sortKey;
1251      state.order = order = order || state.order;
1252
1253      var fullCollection = this.fullCollection;
1254
1255      var delComp = false, delFullComp = false;
1256
1257      if (!sortKey) delComp = delFullComp = true;
1258
1259      var mode = this.mode;
1260      options = _extend({side: mode == "client" ? mode : "server", full: true},
1261                        options);
1262
1263      var comparator = this._makeComparator(sortKey, order);
1264
1265      var full = options.full, side = options.side;
1266
1267      if (side == "client") {
1268        if (full) {
1269          if (fullCollection) fullCollection.comparator = comparator;
1270          delComp = true;
1271        }
1272        else {
1273          this.comparator = comparator;
1274          delFullComp = true;
1275        }
1276      }
1277      else if (side == "server" && !full) {
1278        this.comparator = comparator;
1279      }
1280
1281      if (delComp) delete this.comparator;
1282      if (delFullComp && fullCollection) delete fullCollection.comparator;
1283
1284      return this;
1285    }
1286
1287  });
1288
1289  var PageableProto = PageableCollection.prototype;
1290
1291  return PageableCollection;
1292
1293}));