PageRenderTime 225ms CodeModel.GetById 21ms app.highlight 154ms RepoModel.GetById 1ms app.codeStats 1ms

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

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