PageRenderTime 175ms CodeModel.GetById 0ms app.highlight 122ms RepoModel.GetById 31ms app.codeStats 0ms

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

https://gitlab.com/Blueprint-Marketing/cdnjs
JavaScript | 1325 lines | 625 code | 152 blank | 548 comment | 230 complexity | f860317610755ed637a5fb30583d4536 MD5 | raw file
   1/*
   2  backbone-pageable 1.3.2
   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 = 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.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 = totalPages ?
 619        max(state.firstPage,
 620            floor(totalPages *
 621                  (state.firstPage ?
 622                   state.currentPage :
 623                   state.currentPage + 1) /
 624                  state.totalPages)) :
 625        state.firstPage;
 626
 627      state = this.state = this._checkState(_extend({}, state, {
 628        pageSize: pageSize,
 629        currentPage: options.first ? state.firstPage : currentPage,
 630        totalPages: totalPages
 631      }));
 632
 633      return this.getPage(state.currentPage, _omit(options, ["first"]));
 634    },
 635
 636    /**
 637       Switching between client, server and infinite mode.
 638
 639       If switching from client to server mode, the #fullCollection is emptied
 640       first and then deleted and a fetch is immediately issued for the current
 641       page from the server. Pass `false` to `options.fetch` to skip fetching.
 642
 643       If switching to infinite mode, and if `options.models` is given for an
 644       array of models, #links will be populated with a URL per page, using the
 645       default URL for this collection.
 646
 647       If switching from server to client mode, all of the pages are immediately
 648       refetched. If you have too many pages, you can pass `false` to
 649       `options.fetch` to skip fetching.
 650
 651       If switching to any mode from infinite mode, the #links will be deleted.
 652
 653       @param {"server"|"client"|"infinite"} [mode] The mode to switch to.
 654
 655       @param {Object} [options]
 656
 657       @param {boolean} [options.fetch=true] If `false`, no fetching is done.
 658
 659       @param {boolean} [options.resetState=true] If 'false', the state is not
 660       reset, but checked for sanity instead.
 661
 662       @chainable
 663       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
 664       from fetch or this if `options.fetch` is `false`.
 665    */
 666    switchMode: function (mode, options) {
 667
 668      if (!_contains(["server", "client", "infinite"], mode)) {
 669        throw new TypeError('`mode` must be one of "server", "client" or "infinite"');
 670      }
 671
 672      options = options || {fetch: true, resetState: true};
 673
 674      var state = this.state = options.resetState ?
 675        _clone(this._initState) :
 676        this._checkState(_extend({}, this.state));
 677
 678      this.mode = mode;
 679
 680      var self = this;
 681      var fullCollection = this.fullCollection;
 682      var handlers = this._handlers = this._handlers || {}, handler;
 683      if (mode != "server" && !fullCollection) {
 684        fullCollection = this._makeFullCollection(options.models || []);
 685        fullCollection.pageableCollection = this;
 686        this.fullCollection = fullCollection;
 687        var allHandler = this._makeCollectionEventHandler(this, fullCollection);
 688        _each(["add", "remove", "reset", "sort"], function (event) {
 689          handlers[event] = handler = _.bind(allHandler, {}, event);
 690          self.on(event, handler);
 691          fullCollection.on(event, handler);
 692        });
 693        fullCollection.comparator = this._fullComparator;
 694      }
 695      else if (mode == "server" && fullCollection) {
 696        _each(_keys(handlers), function (event) {
 697          handler = handlers[event];
 698          self.off(event, handler);
 699          fullCollection.off(event, handler);
 700        });
 701        delete this._handlers;
 702        this._fullComparator = fullCollection.comparator;
 703        delete this.fullCollection;
 704      }
 705
 706      if (mode == "infinite") {
 707        var links = this.links = {};
 708        var firstPage = state.firstPage;
 709        var totalPages = ceil(state.totalRecords / state.pageSize);
 710        var lastPage = firstPage === 0 ? max(0, totalPages - 1) : totalPages || firstPage;
 711        for (var i = state.firstPage; i <= lastPage; i++) {
 712          links[i] = this.url;
 713        }
 714      }
 715      else if (this.links) delete this.links;
 716
 717      return options.fetch ?
 718        this.fetch(_omit(options, "fetch", "resetState")) :
 719        this;
 720    },
 721
 722    /**
 723       @return {boolean} `true` if this collection can page backward, `false`
 724       otherwise.
 725    */
 726    hasPrevious: function () {
 727      var state = this.state;
 728      var currentPage = state.currentPage;
 729      if (this.mode != "infinite") return currentPage > state.firstPage;
 730      return !!this.links[currentPage - 1];
 731    },
 732
 733    /**
 734       @return {boolean} `true` if this collection can page forward, `false`
 735       otherwise.
 736    */
 737    hasNext: function () {
 738      var state = this.state;
 739      var currentPage = this.state.currentPage;
 740      if (this.mode != "infinite") return currentPage < state.lastPage;
 741      return !!this.links[currentPage + 1];
 742    },
 743
 744    /**
 745       Fetch the first page in server mode, or reset the current page of this
 746       collection to the first page in client or infinite mode.
 747
 748       @param {Object} options {@link #getPage} options.
 749
 750       @chainable
 751       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
 752       from fetch or this.
 753    */
 754    getFirstPage: function (options) {
 755      return this.getPage("first", options);
 756    },
 757
 758    /**
 759       Fetch the previous page in server mode, or reset the current page of this
 760       collection to the previous page in client or infinite mode.
 761
 762       @param {Object} options {@link #getPage} options.
 763
 764       @chainable
 765       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
 766       from fetch or this.
 767    */
 768    getPreviousPage: function (options) {
 769      return this.getPage("prev", options);
 770    },
 771
 772    /**
 773       Fetch the next page in server mode, or reset the current page of this
 774       collection to the next page in client mode.
 775
 776       @param {Object} options {@link #getPage} options.
 777
 778       @chainable
 779       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
 780       from fetch or this.
 781    */
 782    getNextPage: function (options) {
 783      return this.getPage("next", options);
 784    },
 785
 786    /**
 787       Fetch the last page in server mode, or reset the current page of this
 788       collection to the last page in client mode.
 789
 790       @param {Object} options {@link #getPage} options.
 791
 792       @chainable
 793       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
 794       from fetch or this.
 795    */
 796    getLastPage: function (options) {
 797      return this.getPage("last", options);
 798    },
 799
 800    /**
 801       Given a page index, set #state.currentPage to that index. If this
 802       collection is in server mode, fetch the page using the updated state,
 803       otherwise, reset the current page of this collection to the page
 804       specified by `index` in client mode. If `options.fetch` is true, a fetch
 805       can be forced in client mode before resetting the current page. Under
 806       infinite mode, if the index is less than the current page, a reset is
 807       done as in client mode. If the index is greater than the current page
 808       number, a fetch is made with the results **appended** to #fullCollection.
 809       The current page will then be reset after fetching.
 810
 811       @param {number|string} index The page index to go to, or the page name to
 812       look up from #links in infinite mode.
 813       @param {Object} [options] {@link #fetch} options or
 814       [reset](http://backbonejs.org/#Collection-reset) options for client mode
 815       when `options.fetch` is `false`.
 816       @param {boolean} [options.fetch=false] If true, force a {@link #fetch} in
 817       client mode.
 818
 819       @throws {TypeError} If `index` is not a finite integer under server or
 820       client mode, or does not yield a URL from #links under infinite mode.
 821
 822       @throws {RangeError} If `index` is out of bounds.
 823
 824       @chainable
 825       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
 826       from fetch or this.
 827    */
 828    getPage: function (index, options) {
 829
 830      var mode = this.mode, fullCollection = this.fullCollection;
 831
 832      options = options || {fetch: false};
 833
 834      var state = this.state,
 835      firstPage = state.firstPage,
 836      currentPage = state.currentPage,
 837      lastPage = state.lastPage,
 838      pageSize = state.pageSize;
 839
 840      var pageNum = index;
 841      switch (index) {
 842        case "first": pageNum = firstPage; break;
 843        case "prev": pageNum = currentPage - 1; break;
 844        case "next": pageNum = currentPage + 1; break;
 845        case "last": pageNum = lastPage; break;
 846        default: pageNum = finiteInt(index, "index");
 847      }
 848
 849      this.state = this._checkState(_extend({}, state, {currentPage: pageNum}));
 850
 851      options.from = currentPage, options.to = pageNum;
 852
 853      var pageStart = (firstPage === 0 ? pageNum : pageNum - 1) * pageSize;
 854      var pageModels = fullCollection && fullCollection.length ?
 855        fullCollection.models.slice(pageStart, pageStart + pageSize) :
 856        [];
 857      if ((mode == "client" || (mode == "infinite" && !_isEmpty(pageModels))) &&
 858          !options.fetch) {
 859        return this.reset(pageModels, _omit(options, "fetch"));
 860      }
 861
 862      if (mode == "infinite") options.url = this.links[pageNum];
 863
 864      return this.fetch(_omit(options, "fetch"));
 865    },
 866
 867    /**
 868       Fetch the page for the provided item offset in server mode, or reset the current page of this
 869       collection to the page for the provided item offset in client mode.
 870
 871       @param {Object} options {@link #getPage} options.
 872
 873       @chainable
 874       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
 875       from fetch or this.
 876    */
 877    getPageByOffset: function (offset, options) {
 878      if (offset < 0) {
 879        throw new RangeError("`offset must be > 0`");
 880      }
 881      offset = finiteInt(offset);
 882
 883      var page = floor(offset / this.state.pageSize);
 884      if (this.state.firstPage !== 0) page++;
 885      if (page > this.state.lastPage) page = this.state.lastPage;
 886      return this.getPage(page, options);
 887    },
 888
 889    /**
 890       Overidden to make `getPage` compatible with Zepto.
 891
 892       @param {string} method
 893       @param {Backbone.Model|Backbone.Collection} model
 894       @param {Object} [options]
 895
 896       @return {XMLHttpRequest}
 897    */
 898    sync: function (method, model, options) {
 899      var self = this;
 900      if (self.mode == "infinite") {
 901        var success = options.success;
 902        var currentPage = self.state.currentPage;
 903        options.success = function (resp, status, xhr) {
 904          var links = self.links;
 905          var newLinks = self.parseLinks(resp, _extend({xhr: xhr}, options));
 906          if (newLinks.first) links[self.state.firstPage] = newLinks.first;
 907          if (newLinks.prev) links[currentPage - 1] = newLinks.prev;
 908          if (newLinks.next) links[currentPage + 1] = newLinks.next;
 909          if (success) success(resp, status, xhr);
 910        };
 911      }
 912
 913      return (BBColProto.sync || Backbone.sync).call(self, method, model, options);
 914    },
 915
 916    /**
 917       Parse pagination links from the server response. Only valid under
 918       infinite mode.
 919
 920       Given a response body and a XMLHttpRequest object, extract pagination
 921       links from them for infinite paging.
 922
 923       This default implementation parses the RFC 5988 `Link` header and extract
 924       3 links from it - `first`, `prev`, `next`. If a `previous` link is found,
 925       it will be found in the `prev` key in the returned object hash. Any
 926       subclasses overriding this method __must__ return an object hash having
 927       only the keys above. If `first` is missing, the collection's default URL
 928       is assumed to be the `first` URL. If `prev` or `next` is missing, it is
 929       assumed to be `null`. An empty object hash must be returned if there are
 930       no links found. If either the response or the header contains information
 931       pertaining to the total number of records on the server, #state.totalRecords
 932       must be set to that number. The default implementation uses the `last`
 933       link from the header to calculate it.
 934
 935       @param {*} resp The deserialized response body.
 936       @param {Object} [options]
 937       @param {XMLHttpRequest} [options.xhr] The XMLHttpRequest object for this
 938       response.
 939       @return {Object}
 940    */
 941    parseLinks: function (resp, options) {
 942      var links = {};
 943      var linkHeader = options.xhr.getResponseHeader("Link");
 944      if (linkHeader) {
 945        var relations = ["first", "prev", "previous", "next", "last"];
 946        _each(linkHeader.split(","), function (linkValue) {
 947          var linkParts = linkValue.split(";");
 948          var url = linkParts[0].replace(URL_TRIM_RE, '');
 949          var params = linkParts.slice(1);
 950          _each(params, function (param) {
 951            var paramParts = param.split("=");
 952            var key = paramParts[0].replace(PARAM_TRIM_RE, '');
 953            var value = paramParts[1].replace(PARAM_TRIM_RE, '');
 954            if (key == "rel" && _contains(relations, value)) {
 955              if (value == "previous") links.prev = url;
 956              else links[value] = url;
 957            }
 958          });
 959        });
 960
 961        var last = links.last || '', qsi, qs;
 962        if (qs = (qsi = last.indexOf('?')) ? last.slice(qsi + 1) : '') {
 963          var params = queryStringToParams(qs);
 964
 965          var state = _clone(this.state);
 966          var queryParams = this.queryParams;
 967          var pageSize = state.pageSize;
 968
 969          var totalRecords = params[queryParams.totalRecords] * 1;
 970          var pageNum = params[queryParams.currentPage] * 1;
 971          var totalPages = params[queryParams.totalPages];
 972
 973          if (!totalRecords) {
 974            if (pageNum) totalRecords = (state.firstPage === 0 ?
 975                                         pageNum + 1 :
 976                                         pageNum) * pageSize;
 977            else if (totalPages) totalRecords = totalPages * pageSize;
 978          }
 979
 980          if (totalRecords) state.totalRecords = totalRecords;
 981
 982          this.state = this._checkState(state);
 983        }
 984      }
 985
 986      delete links.last;
 987
 988      return links;
 989    },
 990
 991    /**
 992       Parse server response data.
 993
 994       This default implementation assumes the response data is in one of two
 995       structures:
 996
 997           [
 998             {}, // Your new pagination state
 999             [{}, ...] // An array of JSON objects
1000           ]
1001
1002       Or,
1003
1004           [{}] // An array of JSON objects
1005
1006       The first structure is the preferred form because the pagination states
1007       may have been updated on the server side, sending them down again allows
1008       this collection to update its states. If the response has a pagination
1009       state object, it is checked for errors.
1010
1011       The second structure is the
1012       [Backbone.Collection#parse](http://backbonejs.org/#Collection-parse)
1013       default.
1014
1015       **Note:** this method has been further simplified since 1.1.7. While
1016       existing #parse implementations will continue to work, new code is
1017       encouraged to override #parseState and #parseRecords instead.
1018
1019       @param {Object} resp The deserialized response data from the server.
1020       @param {Object} the options for the ajax request
1021
1022       @return {Array.<Object>} An array of model objects
1023    */
1024    parse: function (resp, options) {
1025      var newState = this.parseState(resp, _clone(this.queryParams), _clone(this.state), options);
1026      if (newState) this.state = this._checkState(_extend({}, this.state, newState));
1027      return this.parseRecords(resp, options);
1028    },
1029
1030    /**
1031       Parse server response for server pagination state updates.
1032
1033       This default implementation first checks whether the response has any
1034       state object as documented in #parse. If it exists, a state object is
1035       returned by mapping the server state keys to this pageable collection
1036       instance's query parameter keys using `queryParams`.
1037
1038       It is __NOT__ neccessary to return a full state object complete with all
1039       the mappings defined in #queryParams. Any state object resulted is merged
1040       with a copy of the current pageable collection state and checked for
1041       sanity before actually updating. Most of the time, simply providing a new
1042       `totalRecords` value is enough to trigger a full pagination state
1043       recalculation.
1044
1045           parseState: function (resp, queryParams, state, options) {
1046             return {totalRecords: resp.total_entries};
1047           }
1048
1049       If you want to use header fields use:
1050
1051           parseState: function (resp, queryParams, state, options) {
1052               return {totalRecords: options.xhr.getResponseHeader("X-total")};
1053           }
1054
1055       This method __MUST__ return a new state object instead of directly
1056       modifying the #state object. The behavior of directly modifying #state is
1057       undefined.
1058
1059       @param {Object} resp The deserialized response data from the server.
1060       @param {Object} queryParams A copy of #queryParams.
1061       @param {Object} state A copy of #state.
1062       @param {Object} [options] The options passed through from
1063       `parse`. (backbone >= 0.9.10 only)
1064
1065       @return {Object} A new (partial) state object.
1066     */
1067    parseState: function (resp, queryParams, state, options) {
1068      if (resp && resp.length === 2 && _isObject(resp[0]) && _isArray(resp[1])) {
1069
1070        var newState = _clone(state);
1071        var serverState = resp[0];
1072
1073        _each(_pairs(_omit(queryParams, "directions")), function (kvp) {
1074          var k = kvp[0], v = kvp[1];
1075          var serverVal = serverState[v];
1076          if (!_isUndefined(serverVal) && !_.isNull(serverVal)) newState[k] = serverState[v];
1077        });
1078
1079        if (serverState.order) {
1080          newState.order = _invert(queryParams.directions)[serverState.order] * 1;
1081        }
1082
1083        return newState;
1084      }
1085    },
1086
1087    /**
1088       Parse server response for an array of model objects.
1089
1090       This default implementation first checks whether the response has any
1091       state object as documented in #parse. If it exists, the array of model
1092       objects is assumed to be the second element, otherwise the entire
1093       response is returned directly.
1094
1095       @param {Object} resp The deserialized response data from the server.
1096       @param {Object} [options] The options passed through from the
1097       `parse`. (backbone >= 0.9.10 only)
1098
1099       @return {Array.<Object>} An array of model objects
1100     */
1101    parseRecords: function (resp, options) {
1102      if (resp && resp.length === 2 && _isObject(resp[0]) && _isArray(resp[1])) {
1103        return resp[1];
1104      }
1105
1106      return resp;
1107    },
1108
1109    /**
1110       Fetch a page from the server in server mode, or all the pages in client
1111       mode. Under infinite mode, the current page is refetched by default and
1112       then reset.
1113
1114       The query string is constructed by translating the current pagination
1115       state to your server API query parameter using #queryParams.  The current
1116       page will reset after fetch.
1117
1118       @param {Object} [options] Accepts all
1119       [Backbone.Collection#fetch](http://backbonejs.org/#Collection-fetch)
1120       options.
1121
1122       @return {XMLHttpRequest}
1123    */
1124    fetch: function (options) {
1125
1126      options = options || {};
1127
1128      var state = this._checkState(this.state);
1129
1130      var mode = this.mode;
1131
1132      if (mode == "infinite" && !options.url) {
1133        options.url = this.links[state.currentPage];
1134      }
1135
1136      var data = options.data || {};
1137
1138      // dedup query params
1139      var url = _result(options, "url") || _result(this, "url") || '';
1140      var qsi = url.indexOf('?');
1141      if (qsi != -1) {
1142        _extend(data, queryStringToParams(url.slice(qsi + 1)));
1143        url = url.slice(0, qsi);
1144      }
1145
1146      options.url = url;
1147      options.data = data;
1148
1149      // map params except directions
1150      var queryParams = this.mode == "client" ?
1151        _pick(this.queryParams, "sortKey", "order") :
1152        _omit(_pick(this.queryParams, _keys(PageableProto.queryParams)),
1153              "directions");
1154
1155      var i, kvp, k, v, kvps = _pairs(queryParams), thisCopy = _clone(this);
1156      for (i = 0; i < kvps.length; i++) {
1157        kvp = kvps[i], k = kvp[0], v = kvp[1];
1158        v = _isFunction(v) ? v.call(thisCopy) : v;
1159        if (state[k] != null && v != null) {
1160          data[v] = state[k];
1161        }
1162      }
1163
1164      // fix up sorting parameters
1165      if (state.sortKey && state.order) {
1166        data[queryParams.order] = this.queryParams.directions[state.order + ""];
1167      }
1168      else if (!state.sortKey) delete data[queryParams.order];
1169
1170      // map extra query parameters
1171      var extraKvps = _pairs(_omit(this.queryParams,
1172                                   _keys(PageableProto.queryParams)));
1173      for (i = 0; i < extraKvps.length; i++) {
1174        kvp = extraKvps[i];
1175        v = kvp[1];
1176        v = _isFunction(v) ? v.call(thisCopy) : v;
1177        if (v != null) data[kvp[0]] = v;
1178      }
1179
1180      if (mode != "server") {
1181        var self = this, fullCol = this.fullCollection;
1182        var success = options.success;
1183        options.success = function (col, resp, opts) {
1184
1185          // make sure the caller's intent is obeyed
1186          opts = opts || {};
1187          if (_isUndefined(options.silent)) delete opts.silent;
1188          else opts.silent = options.silent;
1189
1190          var models = col.models;
1191          if (mode == "client") fullCol.reset(models, opts);
1192          else fullCol.add(models, _extend({at: fullCol.length}, opts));
1193
1194          if (success) success(col, resp, opts);
1195        };
1196
1197        // silent the first reset from backbone
1198        return BBColProto.fetch.call(self, _extend({}, options, {silent: true}));
1199      }
1200
1201      return BBColProto.fetch.call(this, options);
1202    },
1203
1204    /**
1205       Convenient method for making a `comparator` sorted by a model attribute
1206       identified by `sortKey` and ordered by `order`.
1207
1208       Like a Backbone.Collection, a Backbone.PageableCollection will maintain
1209       the __current page__ in sorted order on the client side if a `comparator`
1210       is attached to it. If the collection is in client mode, you can attach a
1211       comparator to #fullCollection to have all the pages reflect the global
1212       sorting order by specifying an option `full` to `true`. You __must__ call
1213       `sort` manually or #fullCollection.sort after calling this method to
1214       force a resort.
1215
1216       While you can use this method to sort the current page in server mode,
1217       the sorting order may not reflect the global sorting order due to the
1218       additions or removals of the records on the server since the last
1219       fetch. If you want the most updated page in a global sorting order, it is
1220       recommended that you set #state.sortKey and optionally #state.order, and
1221       then call #fetch.
1222
1223       @protected
1224
1225       @param {string} [sortKey=this.state.sortKey] See `state.sortKey`.
1226       @param {number} [order=this.state.order] See `state.order`.
1227       @param {(function(Backbone.Model, string): Object) | string} [sortValue] See #setSorting.
1228
1229       See [Backbone.Collection.comparator](http://backbonejs.org/#Collection-comparator).
1230    */
1231    _makeComparator: function (sortKey, order, sortValue) {
1232      var state = this.state;
1233
1234      sortKey = sortKey || state.sortKey;
1235      order = order || state.order;
1236
1237      if (!sortKey || !order) return;
1238
1239      if (!sortValue) sortValue = function (model, attr) {
1240        return model.get(attr);
1241      };
1242
1243      return function (left, right) {
1244        var l = sortValue(left, sortKey), r = sortValue(right, sortKey), t;
1245        if (order === 1) t = l, l = r, r = t;
1246        if (l === r) return 0;
1247        else if (l < r) return -1;
1248        return 1;
1249      };
1250    },
1251
1252    /**
1253       Adjusts the sorting for this pageable collection.
1254
1255       Given a `sortKey` and an `order`, sets `state.sortKey` and
1256       `state.order`. A comparator can be applied on the client side to sort in
1257       the order defined if `options.side` is `"client"`. By default the
1258       comparator is applied to the #fullCollection. Set `options.full` to
1259       `false` to apply a comparator to the current page under any mode. Setting
1260       `sortKey` to `null` removes the comparator from both the current page and
1261       the full collection.
1262
1263       If a `sortValue` function is given, it will be passed the `(model,
1264       sortKey)` arguments and is used to extract a value from the model during
1265       comparison sorts. If `sortValue` is not given, `model.get(sortKey)` is
1266       used for sorting.
1267
1268       @chainable
1269
1270       @param {string} sortKey See `state.sortKey`.
1271       @param {number} [order=this.state.order] See `state.order`.
1272       @param {Object} [options]
1273       @param {"server"|"client"} [options.side] By default, `"client"` if
1274       `mode` is `"client"`, `"server"` otherwise.
1275       @param {boolean} [options.full=true]
1276       @param {(function(Backbone.Model, string): Object) | string} [options.sortValue]
1277    */
1278    setSorting: function (sortKey, order, options) {
1279
1280      var state = this.state;
1281
1282      state.sortKey = sortKey;
1283      state.order = order = order || state.order;
1284
1285      var fullCollection = this.fullCollection;
1286
1287      var delComp = false, delFullComp = false;
1288
1289      if (!sortKey) delComp = delFullComp = true;
1290
1291      var mode = this.mode;
1292      options = _extend({side: mode == "client" ? mode : "server", full: true},
1293                        options);
1294
1295      var comparator = this._makeComparator(sortKey, order, options.sortValue);
1296
1297      var full = options.full, side = options.side;
1298
1299      if (side == "client") {
1300        if (full) {
1301          if (fullCollection) fullCollection.comparator = comparator;
1302          delComp = true;
1303        }
1304        else {
1305          this.comparator = comparator;
1306          delFullComp = true;
1307        }
1308      }
1309      else if (side == "server" && !full) {
1310        this.comparator = comparator;
1311      }
1312
1313      if (delComp) delete this.comparator;
1314      if (delFullComp && fullCollection) delete fullCollection.comparator;
1315
1316      return this;
1317    }
1318
1319  });
1320
1321  var PageableProto = PageableCollection.prototype;
1322
1323  return PageableCollection;
1324
1325}));