PageRenderTime 307ms CodeModel.GetById 2ms app.highlight 173ms RepoModel.GetById 1ms app.codeStats 3ms

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

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