PageRenderTime 145ms CodeModel.GetById 24ms app.highlight 105ms RepoModel.GetById 1ms app.codeStats 0ms

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

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