AMBARI-16095. Storm Ambari View (Sriharsha Chintalapani via srimanth)

Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/529ef7f7
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/529ef7f7
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/529ef7f7

Branch: refs/heads/trunk
Commit: 529ef7f7042764a060d782719cfe5e72cd636f83
Parents: 5fea541
Author: Srimanth Gunturi <sgunt...@hortonworks.com>
Authored: Tue May 3 17:01:32 2016 -0700
Committer: Srimanth Gunturi <sgunt...@hortonworks.com>
Committed: Tue May 3 17:01:32 2016 -0700

----------------------------------------------------------------------
 contrib/views/storm/src/main/resources/404.html |   169 -
 .../views/storm/src/main/resources/index.html   |    39 +-
 .../js/backbone-paginator.min.js                |  1325 ++
 .../main/resources/libs/Backbone/js/Backbone.js |  1920 ++
 .../libs/Bootstrap/css/bootstrap-editable.css   |   663 +
 .../libs/Bootstrap/css/bootstrap-slider.min.css |    28 +
 .../libs/Bootstrap/css/bootstrap-switch.min.css |    22 +
 .../resources/libs/Bootstrap/css/bootstrap.css  |  5959 +++++
 .../fonts/glyphicons-halflings-regular.svg      |   288 +
 .../libs/Bootstrap/js/bootstrap-editable.min.js |     7 +
 .../libs/Bootstrap/js/bootstrap-notify.min.js   |     1 +
 .../libs/Bootstrap/js/bootstrap-slider.min.js   |    29 +
 .../libs/Bootstrap/js/bootstrap-switch.min.js   |    22 +
 .../libs/Bootstrap/js/bootstrap.min.js          |     7 +
 .../libs/Font-Awesome/css/font-awesome.min.css  |     4 +
 .../Font-Awesome/fonts/fontawesome-webfont.svg  |   655 +
 .../resources/libs/Underscore/js/Underscore.js  |  1548 ++
 .../resources/libs/bootbox/js/bootbox.min.js    |     6 +
 .../bower/backbone-forms/js/backbone-forms.js   |  2446 --
 .../libs/bower/backbone-forms/js/list.js        |   650 -
 .../js/backbone.babysitter.js                   |   190 -
 .../js/backbone.marionette.js                   |  3128 ---
 .../bower/backbone.wreqr/js/backbone.wreqr.js   |   435 -
 .../libs/bower/backbone/js/backbone.js          |  1608 --
 .../libs/bower/backgrid/css/backgrid.min.css    |     1 -
 .../libs/bower/backgrid/js/backgrid.js          |  2887 ---
 .../resources/libs/bower/bootbox/js/bootbox.js  |   894 -
 .../libs/bower/bootstrap/css/bootstrap.min.css  |     5 -
 .../fonts/glyphicons-halflings-regular.svg      |   288 -
 .../bootstrap/js/bootstrap-filestyle.min.js     |     1 -
 .../libs/bower/bootstrap/js/bootstrap-notify.js |    97 -
 .../libs/bower/bootstrap/js/bootstrap.js        |  2306 --
 .../bower/font-awesome/css/font-awesome.min.css |     4 -
 .../font-awesome/fonts/fontawesome-webfont.svg  |   565 -
 .../libs/bower/globalize/js/globalize.js        |  1586 --
 .../jquery-fileupload/js/jquery.fileupload.js   |  1201 -
 .../libs/bower/jquery-ui/css/jquery-ui.min.css  |     5 -
 .../jquery-ui/js/jquery-ui-1.10.3.custom.js     |    12 -
 .../libs/bower/jquery-ui/js/jquery-ui-slider.js |   655 -
 .../bower/jquery-ui/js/jquery.ui.widget.min.js  |     5 -
 .../resources/libs/bower/jquery/js/jquery.js    |  9205 --------
 .../libs/bower/jquery/js/jquery.min.js          |     5 -
 .../libs/bower/jquery/js/jquery.min.map         |     1 -
 .../require-handlebars-plugin/js/handlebars.js  |  2220 --
 .../bower/require-handlebars-plugin/js/hbs.js   |   492 -
 .../js/i18nprecompile.js                        |    57 -
 .../bower/require-handlebars-plugin/js/json2.js |   365 -
 .../libs/bower/requirejs-text/js/text.js        |   390 -
 .../libs/bower/requirejs/js/require.js          |  2083 --
 .../libs/bower/underscore/js/underscore.js      |  1415 --
 .../src/main/resources/libs/d3/js/d3-tip.min.js |     1 +
 .../src/main/resources/libs/d3/js/d3.min.js     |     5 +
 .../libs/jQuery/js/jquery-2.2.3.min.js          |     4 +
 .../main/resources/libs/jsx/JSXTransformer.js   | 15201 ++++++++++++
 .../storm/src/main/resources/libs/jsx/jsx.js    |    75 +
 .../main/resources/libs/other/arbor-graphics.js |    51 -
 .../main/resources/libs/other/arbor-tween.js    |    81 -
 .../src/main/resources/libs/other/arbor.js      |    67 -
 .../main/resources/libs/react/js/react-dom.js   |    42 +
 .../libs/react/js/react-with-addons.js          | 20775 +++++++++++++++++
 .../resources/libs/require-js/js/require.min.js |    36 +
 .../main/resources/libs/require-text/js/text.js |   390 +
 .../storm/src/main/resources/scripts/App.js     |    41 -
 .../scripts/collection/BaseCollection.js        |    61 -
 .../scripts/collection/VTopologyList.js         |    42 -
 .../scripts/collections/BaseCollection.js       |   175 +
 .../scripts/collections/VNimbusConfigList.js    |    52 +
 .../scripts/collections/VNimbusList.js          |    52 +
 .../scripts/collections/VSupervisorList.js      |    52 +
 .../scripts/collections/VTopologyConfigList.js  |    49 +
 .../scripts/collections/VTopologyList.js        |    52 +
 .../scripts/components/Breadcrumbs.jsx          |    47 +
 .../main/resources/scripts/components/Modal.jsx |    54 +
 .../scripts/components/RadialChart.jsx          |   118 +
 .../resources/scripts/components/SpoutGraph.jsx |   136 +
 .../main/resources/scripts/components/Table.jsx |    92 +
 .../scripts/components/TopologyGraph.jsx        |   270 +
 .../scripts/containers/ClusterSummary.jsx       |   122 +
 .../scripts/containers/NimbusConfigSummary.jsx  |   103 +
 .../scripts/containers/NimbusSummary.jsx        |   127 +
 .../scripts/containers/SupervisorSummary.jsx    |   137 +
 .../containers/TopologyConfiguration.jsx        |    90 +
 .../scripts/containers/TopologyDetailGraph.jsx  |    62 +
 .../scripts/containers/TopologyListing.jsx      |   176 +
 .../resources/scripts/globalize/message/en.js   |   181 -
 .../storm/src/main/resources/scripts/main.js    |   193 +-
 .../main/resources/scripts/models/BaseModel.js  |    52 +-
 .../main/resources/scripts/models/Cluster.js    |    42 -
 .../src/main/resources/scripts/models/VBolt.js  |    42 -
 .../main/resources/scripts/models/VCluster.js   |    42 +
 .../src/main/resources/scripts/models/VError.js |    42 -
 .../main/resources/scripts/models/VExecutor.js  |    42 -
 .../src/main/resources/scripts/models/VModel.js |    42 -
 .../main/resources/scripts/models/VNimbus.js    |    28 +-
 .../resources/scripts/models/VNimbusConfig.js   |    30 +-
 .../resources/scripts/models/VOutputStat.js     |    42 -
 .../src/main/resources/scripts/models/VSpout.js |    44 -
 .../resources/scripts/models/VSupervisor.js     |    32 +-
 .../main/resources/scripts/models/VTopology.js  |    62 +-
 .../resources/scripts/models/VTopologyConfig.js |    34 +-
 .../main/resources/scripts/modules/Helpers.js   |   157 -
 .../scripts/modules/Table/PageableTable.jsx     |    47 +
 .../scripts/modules/Table/Pagination.jsx        |   158 +
 .../src/main/resources/scripts/modules/Vent.js  |    22 -
 .../src/main/resources/scripts/router/Router.js |   142 +-
 .../src/main/resources/scripts/utils/Globals.js |    40 +-
 .../main/resources/scripts/utils/LangSupport.js |   116 -
 .../main/resources/scripts/utils/Overrides.js   |   328 +-
 .../main/resources/scripts/utils/TableLayout.js |   106 -
 .../src/main/resources/scripts/utils/Utils.js   |   279 +-
 .../scripts/views/Cluster/ClusterSummary.js     |   353 -
 .../scripts/views/ComponentDetailView.jsx       |   507 +
 .../main/resources/scripts/views/Dashboard.jsx  |    65 +
 .../src/main/resources/scripts/views/Footer.jsx |    48 +
 .../scripts/views/NimbusSummaryView.jsx         |    65 +
 .../resources/scripts/views/ProfilingView.jsx   |   203 +
 .../resources/scripts/views/RebalanceView.jsx   |   216 +
 .../scripts/views/Spout/SpoutCollectionView.js  |    53 -
 .../scripts/views/Spout/SpoutItemView.js        |   355 -
 .../scripts/views/SupervisorSummaryView.jsx     |    65 +
 .../scripts/views/Topology/RebalanceForm.js     |   129 -
 .../scripts/views/Topology/TopologyDetail.js    |   736 -
 .../scripts/views/Topology/TopologyForm.js      |   102 -
 .../scripts/views/Topology/TopologyGraphView.js |   423 -
 .../scripts/views/Topology/TopologySummary.js   |   301 -
 .../scripts/views/TopologyDetailView.jsx        |   806 +
 .../scripts/views/TopologyListingView.jsx       |    65 +
 .../main/resources/scripts/views/site/Header.js |    99 -
 .../storm/src/main/resources/styles/default.css |   328 -
 .../storm/src/main/resources/styles/style.css   |   497 +
 .../templates/cluster/clusterSummary.html       |    83 -
 .../main/resources/templates/site/header.html   |    29 -
 .../templates/spout/spoutItemView.html          |    46 -
 .../templates/topology/rebalanceForm.html       |    26 -
 .../templates/topology/topologyDetail.html      |   108 -
 .../templates/topology/topologyForm.html        |     6 -
 .../templates/topology/topologySummary.html     |    23 -
 contrib/views/storm/src/main/resources/view.xml |    11 +-
 138 files changed, 54268 insertions(+), 40557 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/529ef7f7/contrib/views/storm/src/main/resources/404.html
----------------------------------------------------------------------
diff --git a/contrib/views/storm/src/main/resources/404.html 
b/contrib/views/storm/src/main/resources/404.html
deleted file mode 100644
index 4f16be5..0000000
--- a/contrib/views/storm/src/main/resources/404.html
+++ /dev/null
@@ -1,169 +0,0 @@
-<!--
-Licensed to the Apache Software Foundation (ASF) under one or more
-contributor license agreements. See the NOTICE file distributed with
-this work for additional information regarding copyright ownership.
-The ASF licenses this file to You under the Apache License, Version 2.0
-(the "License"); you may not use this file except in compliance with
-the License. You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License. Kerberos, LDAP, Custom. Binary/Htt
--->
-<!DOCTYPE html>
-<html lang="en">
-    <head>
-        <meta charset="utf-8">
-        <title>Page Not Found :(</title>
-        <style>
-            ::-moz-selection {
-                background: #b3d4fc;
-                text-shadow: none;
-            }
-
-            ::selection {
-                background: #b3d4fc;
-                text-shadow: none;
-            }
-
-            html {
-                padding: 30px 10px;
-                font-size: 20px;
-                line-height: 1.4;
-                color: #737373;
-                background: #f0f0f0;
-                -webkit-text-size-adjust: 100%;
-                -ms-text-size-adjust: 100%;
-            }
-
-            html,
-            input {
-                font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
-            }
-
-            body {
-                max-width: 500px;
-                _width: 500px;
-                padding: 30px 20px 50px;
-                border: 1px solid #b3b3b3;
-                border-radius: 4px;
-                margin: 0 auto;
-                box-shadow: 0 1px 10px #a7a7a7, inset 0 1px 0 #fff;
-                background: #fcfcfc;
-            }
-
-            h1 {
-                margin: 0 10px;
-                font-size: 50px;
-                text-align: center;
-            }
-
-            h1 span {
-                color: #bbb;
-            }
-
-            h3 {
-                margin: 1.5em 0 0.5em;
-            }
-
-            p {
-                margin: 1em 0;
-            }
-
-            ul {
-                padding: 0 0 0 40px;
-                margin: 1em 0;
-            }
-
-            .container {
-                max-width: 380px;
-                _width: 380px;
-                margin: 0 auto;
-            }
-
-            /* google search */
-
-            #goog-fixurl ul {
-                list-style: none;
-                padding: 0;
-                margin: 0;
-            }
-
-            #goog-fixurl form {
-                margin: 0;
-            }
-
-            #goog-wm-qt,
-            #goog-wm-sb {
-                border: 1px solid #bbb;
-                font-size: 16px;
-                line-height: normal;
-                vertical-align: top;
-                color: #444;
-                border-radius: 2px;
-            }
-
-            #goog-wm-qt {
-                width: 220px;
-                height: 20px;
-                padding: 5px;
-                margin: 5px 10px 0 0;
-                box-shadow: inset 0 1px 1px #ccc;
-            }
-
-            #goog-wm-sb {
-                display: inline-block;
-                height: 32px;
-                padding: 0 10px;
-                margin: 5px 0 0;
-                white-space: nowrap;
-                cursor: pointer;
-                background-color: #f5f5f5;
-                background-image: -webkit-linear-gradient(rgba(255,255,255,0), 
#f1f1f1);
-                background-image: -moz-linear-gradient(rgba(255,255,255,0), 
#f1f1f1);
-                background-image: -ms-linear-gradient(rgba(255,255,255,0), 
#f1f1f1);
-                background-image: -o-linear-gradient(rgba(255,255,255,0), 
#f1f1f1);
-                -webkit-appearance: none;
-                -moz-appearance: none;
-                appearance: none;
-                *overflow: visible;
-                *display: inline;
-                *zoom: 1;
-            }
-
-            #goog-wm-sb:hover,
-            #goog-wm-sb:focus {
-                border-color: #aaa;
-                box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
-                background-color: #f8f8f8;
-            }
-
-            #goog-wm-qt:hover,
-            #goog-wm-qt:focus {
-                border-color: #105cb6;
-                outline: 0;
-                color: #222;
-            }
-
-            input::-moz-focus-inner {
-                padding: 0;
-                border: 0;
-            }
-        </style>
-    </head>
-    <body>
-        <div class="container">
-            <h1>Not found <span>:(</span></h1>
-            <p>Sorry, but the page you were trying to view does not exist.</p>
-            <p>It looks like this was the result of either:</p>
-            <ul>
-                <li>a mistyped address</li>
-                <li>an out-of-date link</li>
-            </ul>
-        </div>
-    </body>
-</html>

http://git-wip-us.apache.org/repos/asf/ambari/blob/529ef7f7/contrib/views/storm/src/main/resources/index.html
----------------------------------------------------------------------
diff --git a/contrib/views/storm/src/main/resources/index.html 
b/contrib/views/storm/src/main/resources/index.html
index 0f59153..df94a76 100644
--- a/contrib/views/storm/src/main/resources/index.html
+++ b/contrib/views/storm/src/main/resources/index.html
@@ -22,35 +22,24 @@ limitations under the License. Kerberos, LDAP, Custom. 
Binary/Htt
     <head>
         <meta charset="utf-8">
         <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
-        <title>Storm Monitoring</title>
+        <title>Apache Storm</title>
         <meta name="description" content="">
         <meta name="viewport" content="width=device-width">
-        <!-- Place favicon.ico and apple-touch-icon.png in the root directory 
-->
-        <link rel="stylesheet" href="libs/bower/backgrid/css/backgrid.min.css">
-        <link rel="stylesheet" 
href="libs/bower/bootstrap/css/bootstrap.min.css">
-        <link rel="stylesheet" 
href="libs/bower/font-awesome/css/font-awesome.min.css">
-        <link rel="stylesheet" 
href="libs/bower/jquery-ui/css/jquery-ui.min.css">
-        <link rel="stylesheet" href="styles/default.css">
+        <link 
href='https://fonts.googleapis.com/css?family=Lato:400,400italic,300italic,300,700,700italic'
 rel='stylesheet' type='text/css'>
+        <link rel="stylesheet" type="text/css" 
href="libs/Bootstrap/css/bootstrap.css">
+        <link rel="stylesheet" type="text/css" 
href="libs/Bootstrap/css/bootstrap-switch.min.css">
+        <link rel="stylesheet" type="text/css" 
href="libs/Bootstrap/css/bootstrap-editable.css">
+        <link rel="stylesheet" type="text/css" 
href="libs/Bootstrap/css/bootstrap-slider.min.css">
+        <link rel="stylesheet" type="text/css" 
href="libs/Font-Awesome/css/font-awesome.min.css">
+        <link rel="stylesheet" type="text/css" href="styles/style.css">
     </head>
 
     <body>
-    <div class="loading"></div>
-        <div class='container-fluid'>
-            <div class="row">
-                <div class="col-md-12">
-                 <section id="header"></section>
-                 <section id="content">
-                    <div class="notifications top-right"></div>
-                    <div class="tab-content">
-                        <div role="sv_tabpanel" class="tab-pane active" 
id="topology"></div>
-                        <div role="sv_tabpanel" class="tab-pane" 
id="cluster"></div>
-                    </div>
-                 </section>
-                </div>
-            </div>
+        <div class="loader"></div>
+        <div class="container-fluid">
+            <section id="container"></section>
+            <footer id="footer"></footer>    
         </div>
-        <!-- build:js scripts/main.js -->
-        <script data-main="scripts/main" 
src="libs/bower/requirejs/js/require.js"></script>
-        <!-- endbuild -->
+        <script data-main="scripts/main" 
src="libs/require-js/js/require.min.js"></script>
     </body>
-</html>
+</html>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/529ef7f7/contrib/views/storm/src/main/resources/libs/Backbone-Paginator/js/backbone-paginator.min.js
----------------------------------------------------------------------
diff --git 
a/contrib/views/storm/src/main/resources/libs/Backbone-Paginator/js/backbone-paginator.min.js
 
b/contrib/views/storm/src/main/resources/libs/Backbone-Paginator/js/backbone-paginator.min.js
new file mode 100644
index 0000000..d8ccc65
--- /dev/null
+++ 
b/contrib/views/storm/src/main/resources/libs/Backbone-Paginator/js/backbone-paginator.min.js
@@ -0,0 +1,1325 @@
+/*
+  backbone.paginator 2.0.0
+  http://github.com/backbone-paginator/backbone.paginator
+
+  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
+  Licensed under the MIT @license.
+*/
+
+(function (factory) {
+
+  // CommonJS
+  if (typeof exports == "object") {
+    module.exports = factory(require("underscore"), require("backbone"));
+  }
+  // AMD
+  else if (typeof define == "function" && define.amd) {
+    define(["underscore", "backbone"], factory);
+  }
+  // Browser
+  else if (typeof _ !== "undefined" && typeof Backbone !== "undefined") {
+    var oldPageableCollection = Backbone.PageableCollection;
+    var PageableCollection = factory(_, Backbone);
+
+    /**
+       __BROWSER ONLY__
+
+       If you already have an object named `PageableCollection` attached to the
+       `Backbone` module, you can use this to return a local reference to this
+       Backbone.PageableCollection class and reset the name
+       Backbone.PageableCollection to its previous definition.
+
+           // The left hand side gives you a reference to this
+           // Backbone.PageableCollection implementation, the right hand side
+           // resets Backbone.PageableCollection to your other
+           // Backbone.PageableCollection.
+           var PageableCollection = Backbone.PageableCollection.noConflict();
+
+       @static
+       @member Backbone.PageableCollection
+       @return {Backbone.PageableCollection}
+    */
+    Backbone.PageableCollection.noConflict = function () {
+      Backbone.PageableCollection = oldPageableCollection;
+      return PageableCollection;
+    };
+  }
+
+}(function (_, Backbone) {
+
+  "use strict";
+
+  var _extend = _.extend;
+  var _omit = _.omit;
+  var _clone = _.clone;
+  var _each = _.each;
+  var _pick = _.pick;
+  var _contains = _.contains;
+  var _isEmpty = _.isEmpty;
+  var _pairs = _.pairs;
+  var _invert = _.invert;
+  var _isArray = _.isArray;
+  var _isFunction = _.isFunction;
+  var _isObject = _.isObject;
+  var _keys = _.keys;
+  var _isUndefined = _.isUndefined;
+  var ceil = Math.ceil;
+  var floor = Math.floor;
+  var max = Math.max;
+
+  var BBColProto = Backbone.Collection.prototype;
+
+  function finiteInt (val, name) {
+    if (!_.isNumber(val) || _.isNaN(val) || !_.isFinite(val) || ~~val !== val) 
{
+      throw new TypeError("`" + name + "` must be a finite integer");
+    }
+    return val;
+  }
+
+  function queryStringToParams (qs) {
+    var kvp, k, v, ls, params = {}, decode = decodeURIComponent;
+    var kvps = qs.split('&');
+    for (var i = 0, l = kvps.length; i < l; i++) {
+      var param = kvps[i];
+      kvp = param.split('='), k = kvp[0], v = kvp[1] || true;
+      k = decode(k), v = decode(v), ls = params[k];
+      if (_isArray(ls)) ls.push(v);
+      else if (ls) params[k] = [ls, v];
+      else params[k] = v;
+    }
+    return params;
+  }
+
+  // hack to make sure the whatever event handlers for this event is run
+  // before func is, and the event handlers that func will trigger.
+  function runOnceAtLastHandler (col, event, func) {
+    var eventHandlers = col._events[event];
+    if (eventHandlers && eventHandlers.length) {
+      var lastHandler = eventHandlers[eventHandlers.length - 1];
+      var oldCallback = lastHandler.callback;
+      lastHandler.callback = function () {
+        try {
+          oldCallback.apply(this, arguments);
+          func();
+        }
+        catch (e) {
+          throw e;
+        }
+        finally {
+          lastHandler.callback = oldCallback;
+        }
+      };
+    }
+    else func();
+  }
+
+  var PARAM_TRIM_RE = /[\s'"]/g;
+  var URL_TRIM_RE = /[<>\s'"]/g;
+
+  /**
+     Drop-in replacement for Backbone.Collection. Supports server-side and
+     client-side pagination and sorting. Client-side mode also support fully
+     multi-directional synchronization of changes between pages.
+
+     @class Backbone.PageableCollection
+     @extends Backbone.Collection
+  */
+  var PageableCollection = Backbone.PageableCollection = 
Backbone.Collection.extend({
+
+    /**
+       The container object to store all pagination states.
+
+       You can override the default state by extending this class or specifying
+       them in an `options` hash to the constructor.
+
+       @property {Object} state
+
+       @property {0|1} [state.firstPage=1] The first page index. Set to 0 if
+       your server API uses 0-based indices. You should only override this 
value
+       during extension, initialization or reset by the server after
+       fetching. This value should be read only at other times.
+
+       @property {number} [state.lastPage=null] The last page index. This value
+       is __read only__ and it's calculated based on whether `firstPage` is 0 
or
+       1, during bootstrapping, fetching and resetting. Please don't change 
this
+       value under any circumstances.
+
+       @property {number} [state.currentPage=null] The current page index. You
+       should only override this value during extension, initialization or 
reset
+       by the server after fetching. This value should be read only at other
+       times. Can be a 0-based or 1-based index, depending on whether
+       `firstPage` is 0 or 1. If left as default, it will be set to `firstPage`
+       on initialization.
+
+       @property {number} [state.pageSize=25] How many records to show per
+       page. This value is __read only__ after initialization, if you want to
+       change the page size after initialization, you must call #setPageSize.
+
+       @property {number} [state.totalPages=null] How many pages there are. 
This
+       value is __read only__ and it is calculated from `totalRecords`.
+
+       @property {number} [state.totalRecords=null] How many records there
+       are. This value is __required__ under server mode. This value is 
optional
+       for client mode as the number will be the same as the number of models
+       during bootstrapping and during fetching, either supplied by the server
+       in the metadata, or calculated from the size of the response.
+
+       @property {string} [state.sortKey=null] The model attribute to use for
+       sorting.
+
+       @property {-1|0|1} [state.order=-1] The order to use for sorting. 
Specify
+       -1 for ascending order or 1 for descending order. If 0, no client side
+       sorting will be done and the order query parameter will not be sent to
+       the server during a fetch.
+    */
+    state: {
+      firstPage: 1,
+      lastPage: null,
+      currentPage: null,
+      pageSize: 25,
+      totalPages: null,
+      totalRecords: null,
+      sortKey: null,
+      order: -1
+    },
+
+    /**
+       @property {"server"|"client"|"infinite"} [mode="server"] The mode of
+       operations for this collection. `"server"` paginates on the server-side,
+       `"client"` paginates on the client-side and `"infinite"` paginates on 
the
+       server-side for APIs that do not support `totalRecords`.
+    */
+    mode: "server",
+
+    /**
+       A translation map to convert Backbone.PageableCollection state 
attributes
+       to the query parameters accepted by your server API.
+
+       You can override the default state by extending this class or specifying
+       them in `options.queryParams` object hash to the constructor.
+
+       @property {Object} queryParams
+       @property {string} [queryParams.currentPage="page"]
+       @property {string} [queryParams.pageSize="per_page"]
+       @property {string} [queryParams.totalPages="total_pages"]
+       @property {string} [queryParams.totalRecords="total_entries"]
+       @property {string} [queryParams.sortKey="sort_by"]
+       @property {string} [queryParams.order="order"]
+       @property {string} [queryParams.directions={"-1": "asc", "1": "desc"}] A
+       map for translating a Backbone.PageableCollection#state.order constant 
to
+       the ones your server API accepts.
+    */
+    queryParams: {
+      currentPage: "page",
+      pageSize: "per_page",
+      totalPages: "total_pages",
+      totalRecords: "total_entries",
+      sortKey: "sort_by",
+      order: "order",
+      directions: {
+        "-1": "asc",
+        "1": "desc"
+      }
+    },
+
+    /**
+       __CLIENT MODE ONLY__
+
+       This collection is the internal storage for the bootstrapped or fetched
+       models. You can use this if you want to operate on all the pages.
+
+       @property {Backbone.Collection} fullCollection
+    */
+
+    /**
+       Given a list of models or model attributues, bootstraps the full
+       collection in client mode or infinite mode, or just the page you want in
+       server mode.
+
+       If you want to initialize a collection to a different state than the
+       default, you can specify them in `options.state`. Any state parameters
+       supplied will be merged with the default. If you want to change the
+       default mapping from #state keys to your server API's query parameter
+       names, you can specifiy an object hash in `option.queryParams`. 
Likewise,
+       any mapping provided will be merged with the default. Lastly, all
+       Backbone.Collection constructor options are also accepted.
+
+       See:
+
+       - Backbone.PageableCollection#state
+       - Backbone.PageableCollection#queryParams
+       - 
[Backbone.Collection#initialize](http://backbonejs.org/#Collection-constructor)
+
+       @param {Array.<Object>} [models]
+
+       @param {Object} [options]
+
+       @param {function(*, *): number} [options.comparator] If specified, this
+       comparator is set to the current page under server mode, or the 
#fullCollection
+       otherwise.
+
+       @param {boolean} [options.full] If `false` and either a
+       `options.comparator` or `sortKey` is defined, the comparator is attached
+       to the current page. Default is `true` under client or infinite mode and
+       the comparator will be attached to the #fullCollection.
+
+       @param {Object} [options.state] The state attributes overriding the 
defaults.
+
+       @param {string} [options.state.sortKey] The model attribute to use for
+       sorting. If specified instead of `options.comparator`, a comparator will
+       be automatically created using this value, and optionally a sorting 
order
+       specified in `options.state.order`. The comparator is then attached to
+       the new collection instance.
+
+       @param {-1|1} [options.state.order] The order to use for sorting. 
Specify
+       -1 for ascending order and 1 for descending order.
+
+       @param {Object} [options.queryParam]
+    */
+    constructor: function (models, options) {
+
+      BBColProto.constructor.apply(this, arguments);
+
+      options = options || {};
+
+      var mode = this.mode = options.mode || this.mode || PageableProto.mode;
+
+      var queryParams = _extend({}, PageableProto.queryParams, 
this.queryParams,
+                                options.queryParams || {});
+
+      queryParams.directions = _extend({},
+                                       PageableProto.queryParams.directions,
+                                       this.queryParams.directions,
+                                       queryParams.directions || {});
+
+      this.queryParams = queryParams;
+
+      var state = this.state = _extend({}, PageableProto.state, this.state,
+                                       options.state || {});
+
+      state.currentPage = state.currentPage == null ?
+        state.firstPage :
+        state.currentPage;
+
+      if (!_isArray(models)) models = models ? [models] : [];
+      models = models.slice();
+
+      if (mode != "server" && state.totalRecords == null && !_isEmpty(models)) 
{
+        state.totalRecords = models.length;
+      }
+
+      this.switchMode(mode, _extend({fetch: false,
+                                     resetState: false,
+                                     models: models}, options));
+
+      var comparator = options.comparator;
+
+      if (state.sortKey && !comparator) {
+        this.setSorting(state.sortKey, state.order, options);
+      }
+
+      if (mode != "server") {
+        var fullCollection = this.fullCollection;
+
+        if (comparator && options.full) {
+          this.comparator = null;
+          fullCollection.comparator = comparator;
+        }
+
+        if (options.full) fullCollection.sort();
+
+        // make sure the models in the current page and full collection have 
the
+        // same references
+        if (models && !_isEmpty(models)) {
+          this.reset(models, _extend({silent: true}, options));
+          this.getPage(state.currentPage);
+          models.splice.apply(models, [0, models.length].concat(this.models));
+        }
+      }
+
+      this._initState = _clone(this.state);
+    },
+
+    /**
+       Makes a Backbone.Collection that contains all the pages.
+
+       @private
+       @param {Array.<Object|Backbone.Model>} models
+       @param {Object} options Options for Backbone.Collection constructor.
+       @return {Backbone.Collection}
+    */
+    _makeFullCollection: function (models, options) {
+
+      var properties = ["url", "model", "sync", "comparator"];
+      var thisProto = this.constructor.prototype;
+      var i, length, prop;
+
+      var proto = {};
+      for (i = 0, length = properties.length; i < length; i++) {
+        prop = properties[i];
+        if (!_isUndefined(thisProto[prop])) {
+          proto[prop] = thisProto[prop];
+        }
+      }
+
+      var fullCollection = new (Backbone.Collection.extend(proto))(models, 
options);
+
+      for (i = 0, length = properties.length; i < length; i++) {
+        prop = properties[i];
+        if (this[prop] !== thisProto[prop]) {
+          fullCollection[prop] = this[prop];
+        }
+      }
+
+      return fullCollection;
+    },
+
+    /**
+       Factory method that returns a Backbone event handler that responses to
+       the `add`, `remove`, `reset`, and the `sort` events. The returned event
+       handler will synchronize the current page collection and the full
+       collection's models.
+
+       @private
+
+       @param {Backbone.PageableCollection} pageCol
+       @param {Backbone.Collection} fullCol
+
+       @return {function(string, Backbone.Model, Backbone.Collection, Object)}
+       Collection event handler
+    */
+    _makeCollectionEventHandler: function (pageCol, fullCol) {
+
+      return function collectionEventHandler (event, model, collection, 
options) {
+
+        var handlers = pageCol._handlers;
+        _each(_keys(handlers), function (event) {
+          var handler = handlers[event];
+          pageCol.off(event, handler);
+          fullCol.off(event, handler);
+        });
+
+        var state = _clone(pageCol.state);
+        var firstPage = state.firstPage;
+        var currentPage = firstPage === 0 ?
+          state.currentPage :
+          state.currentPage - 1;
+        var pageSize = state.pageSize;
+        var pageStart = currentPage * pageSize, pageEnd = pageStart + pageSize;
+
+        if (event == "add") {
+          var pageIndex, fullIndex, addAt, colToAdd, options = options || {};
+          if (collection == fullCol) {
+            fullIndex = fullCol.indexOf(model);
+            if (fullIndex >= pageStart && fullIndex < pageEnd) {
+              colToAdd = pageCol;
+              pageIndex = addAt = fullIndex - pageStart;
+            }
+          }
+          else {
+            pageIndex = pageCol.indexOf(model);
+            fullIndex = pageStart + pageIndex;
+            colToAdd = fullCol;
+            var addAt = !_isUndefined(options.at) ?
+              options.at + pageStart :
+              fullIndex;
+          }
+
+          if (!options.onRemove) {
+            ++state.totalRecords;
+            delete options.onRemove;
+          }
+
+          pageCol.state = pageCol._checkState(state);
+
+          if (colToAdd) {
+            colToAdd.add(model, _extend({}, options || {}, {at: addAt}));
+            var modelToRemove = pageIndex >= pageSize ?
+              model :
+              !_isUndefined(options.at) && addAt < pageEnd && pageCol.length > 
pageSize ?
+              pageCol.at(pageSize) :
+              null;
+            if (modelToRemove) {
+              runOnceAtLastHandler(collection, event, function () {
+                pageCol.remove(modelToRemove, {onAdd: true});
+              });
+            }
+          }
+        }
+
+        // remove the model from the other collection as well
+        if (event == "remove") {
+          if (!options.onAdd) {
+            // decrement totalRecords and update totalPages and lastPage
+            if (!--state.totalRecords) {
+              state.totalRecords = null;
+              state.totalPages = null;
+            }
+            else {
+              var totalPages = state.totalPages = ceil(state.totalRecords / 
pageSize);
+              state.lastPage = firstPage === 0 ? totalPages - 1 : totalPages 
|| firstPage;
+              if (state.currentPage > totalPages) state.currentPage = 
state.lastPage;
+            }
+            pageCol.state = pageCol._checkState(state);
+
+            var nextModel, removedIndex = options.index;
+            if (collection == pageCol) {
+              if (nextModel = fullCol.at(pageEnd)) {
+                runOnceAtLastHandler(pageCol, event, function () {
+                  pageCol.push(nextModel, {onRemove: true});
+                });
+              }
+              else if (!pageCol.length && state.totalRecords) {
+                pageCol.reset(fullCol.models.slice(pageStart - pageSize, 
pageEnd - pageSize),
+                              _extend({}, options, {parse: false}));
+              }
+              fullCol.remove(model);
+            }
+            else if (removedIndex >= pageStart && removedIndex < pageEnd) {
+              if (nextModel = fullCol.at(pageEnd - 1)) {
+                runOnceAtLastHandler(pageCol, event, function() {
+                  pageCol.push(nextModel, {onRemove: true});
+                });
+              }
+              pageCol.remove(model);
+              if (!pageCol.length && state.totalRecords) {
+                pageCol.reset(fullCol.models.slice(pageStart - pageSize, 
pageEnd - pageSize),
+                              _extend({}, options, {parse: false}));
+              }
+            }
+          }
+          else delete options.onAdd;
+        }
+
+        if (event == "reset") {
+          options = collection;
+          collection = model;
+
+          // Reset that's not a result of getPage
+          if (collection == pageCol && options.from == null &&
+              options.to == null) {
+            var head = fullCol.models.slice(0, pageStart);
+            var tail = fullCol.models.slice(pageStart + pageCol.models.length);
+            fullCol.reset(head.concat(pageCol.models).concat(tail), options);
+          }
+          else if (collection == fullCol) {
+            if (!(state.totalRecords = fullCol.models.length)) {
+              state.totalRecords = null;
+              state.totalPages = null;
+            }
+            if (pageCol.mode == "client") {
+              state.lastPage = state.currentPage = state.firstPage;
+            }
+            pageCol.state = pageCol._checkState(state);
+            pageCol.reset(fullCol.models.slice(pageStart, pageEnd),
+                          _extend({}, options, {parse: false}));
+          }
+        }
+
+        if (event == "sort") {
+          options = collection;
+          collection = model;
+          if (collection === fullCol) {
+            pageCol.reset(fullCol.models.slice(pageStart, pageEnd),
+                          _extend({}, options, {parse: false}));
+          }
+        }
+
+        _each(_keys(handlers), function (event) {
+          var handler = handlers[event];
+          _each([pageCol, fullCol], function (col) {
+            col.on(event, handler);
+            var callbacks = col._events[event] || [];
+            callbacks.unshift(callbacks.pop());
+          });
+        });
+      };
+    },
+
+    /**
+       Sanity check this collection's pagination states. Only perform checks
+       when all the required pagination state values are defined and not null.
+       If `totalPages` is undefined or null, it is set to `totalRecords` /
+       `pageSize`. `lastPage` is set according to whether `firstPage` is 0 or 1
+       when no error occurs.
+
+       @private
+
+       @throws {TypeError} If `totalRecords`, `pageSize`, `currentPage` or
+       `firstPage` is not a finite integer.
+
+       @throws {RangeError} If `pageSize`, `currentPage` or `firstPage` is out
+       of bounds.
+
+       @return {Object} Returns the `state` object if no error was found.
+    */
+    _checkState: function (state) {
+
+      var mode = this.mode;
+      var links = this.links;
+      var totalRecords = state.totalRecords;
+      var pageSize = state.pageSize;
+      var currentPage = state.currentPage;
+      var firstPage = state.firstPage;
+      var totalPages = state.totalPages;
+
+      if (totalRecords != null && pageSize != null && currentPage != null &&
+          firstPage != null && (mode == "infinite" ? links : true)) {
+
+        totalRecords = finiteInt(totalRecords, "totalRecords");
+        pageSize = finiteInt(pageSize, "pageSize");
+        currentPage = finiteInt(currentPage, "currentPage");
+        firstPage = finiteInt(firstPage, "firstPage");
+
+        if (pageSize < 1) {
+          throw new RangeError("`pageSize` must be >= 1");
+        }
+
+        totalPages = state.totalPages = ceil(totalRecords / pageSize);
+
+        if (firstPage < 0 || firstPage > 1) {
+          throw new RangeError("`firstPage must be 0 or 1`");
+        }
+
+        state.lastPage = firstPage === 0 ? max(0, totalPages - 1) : totalPages 
|| firstPage;
+
+        if (mode == "infinite") {
+          if (!links[currentPage + '']) {
+            throw new RangeError("No link found for page " + currentPage);
+          }
+        }
+        else if (currentPage < firstPage ||
+                 (totalPages > 0 &&
+                  (firstPage ? currentPage > totalPages : currentPage >= 
totalPages))) {
+          throw new RangeError("`currentPage` must be firstPage <= currentPage 
" +
+                               (firstPage ? ">" : ">=") +
+                               " totalPages if " + firstPage + "-based. Got " +
+                               currentPage + '.');
+        }
+      }
+
+      return state;
+    },
+
+    /**
+       Change the page size of this collection.
+
+       Under most if not all circumstances, you should call this method to
+       change the page size of a pageable collection because it will keep the
+       pagination state sane. By default, the method will recalculate the
+       current page number to one that will retain the current page's models
+       when increasing the page size. When decreasing the page size, this 
method
+       will retain the last models to the current page that will fit into the
+       smaller page size.
+
+       If `options.first` is true, changing the page size will also reset the
+       current page back to the first page instead of trying to be smart.
+
+       For server mode operations, changing the page size will trigger a #fetch
+       and subsequently a `reset` event.
+
+       For client mode operations, changing the page size will `reset` the
+       current page by recalculating the current page boundary on the client
+       side.
+
+       If `options.fetch` is true, a fetch can be forced if the collection is 
in
+       client mode.
+
+       @param {number} pageSize The new page size to set to #state.
+       @param {Object} [options] {@link #fetch} options.
+       @param {boolean} [options.first=false] Reset the current page number to
+       the first page if `true`.
+       @param {boolean} [options.fetch] If `true`, force a fetch in client 
mode.
+
+       @throws {TypeError} If `pageSize` is not a finite integer.
+       @throws {RangeError} If `pageSize` is less than 1.
+
+       @chainable
+       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
+       from fetch or this.
+    */
+    setPageSize: function (pageSize, options) {
+      pageSize = finiteInt(pageSize, "pageSize");
+
+      options = options || {first: false};
+
+      var state = this.state;
+      var totalPages = ceil(state.totalRecords / pageSize);
+      var currentPage = totalPages ?
+          max(state.firstPage, floor(totalPages * state.currentPage / 
state.totalPages)) :
+        state.firstPage;
+
+      state = this.state = this._checkState(_extend({}, state, {
+        pageSize: pageSize,
+        currentPage: options.first ? state.firstPage : currentPage,
+        totalPages: totalPages
+      }));
+
+      return this.getPage(state.currentPage, _omit(options, ["first"]));
+    },
+
+    /**
+       Switching between client, server and infinite mode.
+
+       If switching from client to server mode, the #fullCollection is emptied
+       first and then deleted and a fetch is immediately issued for the current
+       page from the server. Pass `false` to `options.fetch` to skip fetching.
+
+       If switching to infinite mode, and if `options.models` is given for an
+       array of models, #links will be populated with a URL per page, using the
+       default URL for this collection.
+
+       If switching from server to client mode, all of the pages are 
immediately
+       refetched. If you have too many pages, you can pass `false` to
+       `options.fetch` to skip fetching.
+
+       If switching to any mode from infinite mode, the #links will be deleted.
+
+       @param {"server"|"client"|"infinite"} [mode] The mode to switch to.
+
+       @param {Object} [options]
+
+       @param {boolean} [options.fetch=true] If `false`, no fetching is done.
+
+       @param {boolean} [options.resetState=true] If 'false', the state is not
+       reset, but checked for sanity instead.
+
+       @chainable
+       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
+       from fetch or this if `options.fetch` is `false`.
+    */
+    switchMode: function (mode, options) {
+
+      if (!_contains(["server", "client", "infinite"], mode)) {
+        throw new TypeError('`mode` must be one of "server", "client" or 
"infinite"');
+      }
+
+      options = options || {fetch: true, resetState: true};
+
+      var state = this.state = options.resetState ?
+        _clone(this._initState) :
+        this._checkState(_extend({}, this.state));
+
+      this.mode = mode;
+
+      var self = this;
+      var fullCollection = this.fullCollection;
+      var handlers = this._handlers = this._handlers || {}, handler;
+      if (mode != "server" && !fullCollection) {
+        fullCollection = this._makeFullCollection(options.models || [], 
options);
+        fullCollection.pageableCollection = this;
+        this.fullCollection = fullCollection;
+        var allHandler = this._makeCollectionEventHandler(this, 
fullCollection);
+        _each(["add", "remove", "reset", "sort"], function (event) {
+          handlers[event] = handler = _.bind(allHandler, {}, event);
+          self.on(event, handler);
+          fullCollection.on(event, handler);
+        });
+        fullCollection.comparator = this._fullComparator;
+      }
+      else if (mode == "server" && fullCollection) {
+        _each(_keys(handlers), function (event) {
+          handler = handlers[event];
+          self.off(event, handler);
+          fullCollection.off(event, handler);
+        });
+        delete this._handlers;
+        this._fullComparator = fullCollection.comparator;
+        delete this.fullCollection;
+      }
+
+      if (mode == "infinite") {
+        var links = this.links = {};
+        var firstPage = state.firstPage;
+        var totalPages = ceil(state.totalRecords / state.pageSize);
+        var lastPage = firstPage === 0 ? max(0, totalPages - 1) : totalPages 
|| firstPage;
+        for (var i = state.firstPage; i <= lastPage; i++) {
+          links[i] = this.url;
+        }
+      }
+      else if (this.links) delete this.links;
+
+      return options.fetch ?
+        this.fetch(_omit(options, "fetch", "resetState")) :
+        this;
+    },
+
+    /**
+       @return {boolean} `true` if this collection can page backward, `false`
+       otherwise.
+    */
+    hasPreviousPage: function () {
+      var state = this.state;
+      var currentPage = state.currentPage;
+      if (this.mode != "infinite") return currentPage > state.firstPage;
+      return !!this.links[currentPage - 1];
+    },
+
+    /**
+       @return {boolean} `true` if this collection can page forward, `false`
+       otherwise.
+    */
+    hasNextPage: function () {
+      var state = this.state;
+      var currentPage = this.state.currentPage;
+      if (this.mode != "infinite") return currentPage < state.lastPage;
+      return !!this.links[currentPage + 1];
+    },
+
+    /**
+       Fetch the first page in server mode, or reset the current page of this
+       collection to the first page in client or infinite mode.
+
+       @param {Object} options {@link #getPage} options.
+
+       @chainable
+       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
+       from fetch or this.
+    */
+    getFirstPage: function (options) {
+      return this.getPage("first", options);
+    },
+
+    /**
+       Fetch the previous page in server mode, or reset the current page of 
this
+       collection to the previous page in client or infinite mode.
+
+       @param {Object} options {@link #getPage} options.
+
+       @chainable
+       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
+       from fetch or this.
+    */
+    getPreviousPage: function (options) {
+      return this.getPage("prev", options);
+    },
+
+    /**
+       Fetch the next page in server mode, or reset the current page of this
+       collection to the next page in client mode.
+
+       @param {Object} options {@link #getPage} options.
+
+       @chainable
+       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
+       from fetch or this.
+    */
+    getNextPage: function (options) {
+      return this.getPage("next", options);
+    },
+
+    /**
+       Fetch the last page in server mode, or reset the current page of this
+       collection to the last page in client mode.
+
+       @param {Object} options {@link #getPage} options.
+
+       @chainable
+       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
+       from fetch or this.
+    */
+    getLastPage: function (options) {
+      return this.getPage("last", options);
+    },
+
+    /**
+       Given a page index, set #state.currentPage to that index. If this
+       collection is in server mode, fetch the page using the updated state,
+       otherwise, reset the current page of this collection to the page
+       specified by `index` in client mode. If `options.fetch` is true, a fetch
+       can be forced in client mode before resetting the current page. Under
+       infinite mode, if the index is less than the current page, a reset is
+       done as in client mode. If the index is greater than the current page
+       number, a fetch is made with the results **appended** to 
#fullCollection.
+       The current page will then be reset after fetching.
+
+       @param {number|string} index The page index to go to, or the page name 
to
+       look up from #links in infinite mode.
+       @param {Object} [options] {@link #fetch} options or
+       [reset](http://backbonejs.org/#Collection-reset) options for client mode
+       when `options.fetch` is `false`.
+       @param {boolean} [options.fetch=false] If true, force a {@link #fetch} 
in
+       client mode.
+
+       @throws {TypeError} If `index` is not a finite integer under server or
+       client mode, or does not yield a URL from #links under infinite mode.
+
+       @throws {RangeError} If `index` is out of bounds.
+
+       @chainable
+       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
+       from fetch or this.
+    */
+    getPage: function (index, options) {
+
+      var mode = this.mode, fullCollection = this.fullCollection;
+
+      options = options || {fetch: false};
+
+      var state = this.state,
+      firstPage = state.firstPage,
+      currentPage = state.currentPage,
+      lastPage = state.lastPage,
+      pageSize = state.pageSize;
+
+      var pageNum = index;
+      switch (index) {
+        case "first": pageNum = firstPage; break;
+        case "prev": pageNum = currentPage - 1; break;
+        case "next": pageNum = currentPage + 1; break;
+        case "last": pageNum = lastPage; break;
+        default: pageNum = finiteInt(index, "index");
+      }
+
+      this.state = this._checkState(_extend({}, state, {currentPage: 
pageNum}));
+
+      options.from = currentPage, options.to = pageNum;
+
+      var pageStart = (firstPage === 0 ? pageNum : pageNum - 1) * pageSize;
+      var pageModels = fullCollection && fullCollection.length ?
+        fullCollection.models.slice(pageStart, pageStart + pageSize) :
+        [];
+      if ((mode == "client" || (mode == "infinite" && !_isEmpty(pageModels))) 
&&
+          !options.fetch) {
+        this.reset(pageModels, _omit(options, "fetch"));
+        return this;
+      }
+
+      if (mode == "infinite") options.url = this.links[pageNum];
+
+      return this.fetch(_omit(options, "fetch"));
+    },
+
+    /**
+       Fetch the page for the provided item offset in server mode, or reset 
the current page of this
+       collection to the page for the provided item offset in client mode.
+
+       @param {Object} options {@link #getPage} options.
+
+       @chainable
+       @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest
+       from fetch or this.
+    */
+    getPageByOffset: function (offset, options) {
+      if (offset < 0) {
+        throw new RangeError("`offset must be > 0`");
+      }
+      offset = finiteInt(offset);
+
+      var page = floor(offset / this.state.pageSize);
+      if (this.state.firstPage !== 0) page++;
+      if (page > this.state.lastPage) page = this.state.lastPage;
+      return this.getPage(page, options);
+    },
+
+    /**
+       Overidden to make `getPage` compatible with Zepto.
+
+       @param {string} method
+       @param {Backbone.Model|Backbone.Collection} model
+       @param {Object} [options]
+
+       @return {XMLHttpRequest}
+    */
+    sync: function (method, model, options) {
+      var self = this;
+      if (self.mode == "infinite") {
+        var success = options.success;
+        var currentPage = self.state.currentPage;
+        options.success = function (resp, status, xhr) {
+          var links = self.links;
+          var newLinks = self.parseLinks(resp, _extend({xhr: xhr}, options));
+          if (newLinks.first) links[self.state.firstPage] = newLinks.first;
+          if (newLinks.prev) links[currentPage - 1] = newLinks.prev;
+          if (newLinks.next) links[currentPage + 1] = newLinks.next;
+          if (success) success(resp, status, xhr);
+        };
+      }
+
+      return (BBColProto.sync || Backbone.sync).call(self, method, model, 
options);
+    },
+
+    /**
+       Parse pagination links from the server response. Only valid under
+       infinite mode.
+
+       Given a response body and a XMLHttpRequest object, extract pagination
+       links from them for infinite paging.
+
+       This default implementation parses the RFC 5988 `Link` header and 
extract
+       3 links from it - `first`, `prev`, `next`. Any subclasses overriding 
this
+       method __must__ return an object hash having only the keys
+       above. However, simply returning a `next` link or an empty hash if there
+       are no more links should be enough for most implementations.
+
+       @param {*} resp The deserialized response body.
+       @param {Object} [options]
+       @param {XMLHttpRequest} [options.xhr] The XMLHttpRequest object for this
+       response.
+       @return {Object}
+    */
+    parseLinks: function (resp, options) {
+      var links = {};
+      var linkHeader = options.xhr.getResponseHeader("Link");
+      if (linkHeader) {
+        var relations = ["first", "prev", "next"];
+        _each(linkHeader.split(","), function (linkValue) {
+          var linkParts = linkValue.split(";");
+          var url = linkParts[0].replace(URL_TRIM_RE, '');
+          var params = linkParts.slice(1);
+          _each(params, function (param) {
+            var paramParts = param.split("=");
+            var key = paramParts[0].replace(PARAM_TRIM_RE, '');
+            var value = paramParts[1].replace(PARAM_TRIM_RE, '');
+            if (key == "rel" && _contains(relations, value)) links[value] = 
url;
+          });
+        });
+      }
+
+      return links;
+    },
+
+    /**
+       Parse server response data.
+
+       This default implementation assumes the response data is in one of two
+       structures:
+
+           [
+             {}, // Your new pagination state
+             [{}, ...] // An array of JSON objects
+           ]
+
+       Or,
+
+           [{}] // An array of JSON objects
+
+       The first structure is the preferred form because the pagination states
+       may have been updated on the server side, sending them down again allows
+       this collection to update its states. If the response has a pagination
+       state object, it is checked for errors.
+
+       The second structure is the
+       [Backbone.Collection#parse](http://backbonejs.org/#Collection-parse)
+       default.
+
+       **Note:** this method has been further simplified since 1.1.7. While
+       existing #parse implementations will continue to work, new code is
+       encouraged to override #parseState and #parseRecords instead.
+
+       @param {Object} resp The deserialized response data from the server.
+       @param {Object} the options for the ajax request
+
+       @return {Array.<Object>} An array of model objects
+    */
+    parse: function (resp, options) {
+      var newState = this.parseState(resp, _clone(this.queryParams), 
_clone(this.state), options);
+      if (newState) this.state = this._checkState(_extend({}, this.state, 
newState));
+      return this.parseRecords(resp, options);
+    },
+
+    /**
+       Parse server response for server pagination state updates. Not 
applicable
+       under infinite mode.
+
+       This default implementation first checks whether the response has any
+       state object as documented in #parse. If it exists, a state object is
+       returned by mapping the server state keys to this pageable collection
+       instance's query parameter keys using `queryParams`.
+
+       It is __NOT__ neccessary to return a full state object complete with all
+       the mappings defined in #queryParams. Any state object resulted is 
merged
+       with a copy of the current pageable collection state and checked for
+       sanity before actually updating. Most of the time, simply providing a 
new
+       `totalRecords` value is enough to trigger a full pagination state
+       recalculation.
+
+           parseState: function (resp, queryParams, state, options) {
+             return {totalRecords: resp.total_entries};
+           }
+
+       If you want to use header fields use:
+
+           parseState: function (resp, queryParams, state, options) {
+               return {totalRecords: options.xhr.getResponseHeader("X-total")};
+           }
+
+       This method __MUST__ return a new state object instead of directly
+       modifying the #state object. The behavior of directly modifying #state 
is
+       undefined.
+
+       @param {Object} resp The deserialized response data from the server.
+       @param {Object} queryParams A copy of #queryParams.
+       @param {Object} state A copy of #state.
+       @param {Object} [options] The options passed through from
+       `parse`. (backbone >= 0.9.10 only)
+
+       @return {Object} A new (partial) state object.
+     */
+    parseState: function (resp, queryParams, state, options) {
+      if (resp && resp.length === 2 && _isObject(resp[0]) && 
_isArray(resp[1])) {
+
+        var newState = _clone(state);
+        var serverState = resp[0];
+
+        _each(_pairs(_omit(queryParams, "directions")), function (kvp) {
+          var k = kvp[0], v = kvp[1];
+          var serverVal = serverState[v];
+          if (!_isUndefined(serverVal) && !_.isNull(serverVal)) newState[k] = 
serverState[v];
+        });
+
+        if (serverState.order) {
+          newState.order = _invert(queryParams.directions)[serverState.order] 
* 1;
+        }
+
+        return newState;
+      }
+    },
+
+    /**
+       Parse server response for an array of model objects.
+
+       This default implementation first checks whether the response has any
+       state object as documented in #parse. If it exists, the array of model
+       objects is assumed to be the second element, otherwise the entire
+       response is returned directly.
+
+       @param {Object} resp The deserialized response data from the server.
+       @param {Object} [options] The options passed through from the
+       `parse`. (backbone >= 0.9.10 only)
+
+       @return {Array.<Object>} An array of model objects
+     */
+    parseRecords: function (resp, options) {
+      if (resp && resp.length === 2 && _isObject(resp[0]) && 
_isArray(resp[1])) {
+        return resp[1];
+      }
+
+      return resp;
+    },
+
+    /**
+       Fetch a page from the server in server mode, or all the pages in client
+       mode. Under infinite mode, the current page is refetched by default and
+       then reset.
+
+       The query string is constructed by translating the current pagination
+       state to your server API query parameter using #queryParams. The current
+       page will reset after fetch.
+
+       @param {Object} [options] Accepts all
+       [Backbone.Collection#fetch](http://backbonejs.org/#Collection-fetch)
+       options.
+
+       @return {XMLHttpRequest}
+    */
+    fetch: function (options) {
+
+      options = options || {};
+
+      var state = this._checkState(this.state);
+
+      var mode = this.mode;
+
+      if (mode == "infinite" && !options.url) {
+        options.url = this.links[state.currentPage];
+      }
+
+      var data = options.data || {};
+
+      // dedup query params
+      var url = options.url || this.url || "";
+      if (_isFunction(url)) url = url.call(this);
+      var qsi = url.indexOf('?');
+      if (qsi != -1) {
+        _extend(data, queryStringToParams(url.slice(qsi + 1)));
+        url = url.slice(0, qsi);
+      }
+
+      options.url = url;
+      options.data = data;
+
+      // map params except directions
+      var queryParams = this.mode == "client" ?
+        _pick(this.queryParams, "sortKey", "order") :
+        _omit(_pick(this.queryParams, _keys(PageableProto.queryParams)),
+              "directions");
+
+      var i, kvp, k, v, kvps = _pairs(queryParams), thisCopy = _clone(this);
+      for (i = 0; i < kvps.length; i++) {
+        kvp = kvps[i], k = kvp[0], v = kvp[1];
+        v = _isFunction(v) ? v.call(thisCopy) : v;
+        if (state[k] != null && v != null) {
+          data[v] = state[k];
+        }
+      }
+
+      // fix up sorting parameters
+      if (state.sortKey && state.order) {
+        var o = _isFunction(queryParams.order) ?
+          queryParams.order.call(thisCopy) :
+          queryParams.order;
+        data[o] = this.queryParams.directions[state.order + ""];
+      }
+      else if (!state.sortKey) delete data[queryParams.order];
+
+      // map extra query parameters
+      var extraKvps = _pairs(_omit(this.queryParams,
+                                   _keys(PageableProto.queryParams)));
+      for (i = 0; i < extraKvps.length; i++) {
+        kvp = extraKvps[i];
+        v = kvp[1];
+        v = _isFunction(v) ? v.call(thisCopy) : v;
+        if (v != null) data[kvp[0]] = v;
+      }
+
+      if (mode != "server") {
+        var self = this, fullCol = this.fullCollection;
+        var success = options.success;
+        options.success = function (col, resp, opts) {
+
+          // make sure the caller's intent is obeyed
+          opts = opts || {};
+          if (_isUndefined(options.silent)) delete opts.silent;
+          else opts.silent = options.silent;
+
+          var models = col.models;
+          if (mode == "client") fullCol.reset(models, opts);
+          else {
+            fullCol.add(models, _extend({at: fullCol.length},
+                                        _extend(opts, {parse: false})));
+            self.trigger("reset", self, opts);
+          }
+
+          if (success) success(col, resp, opts);
+        };
+
+        // silent the first reset from backbone
+        return BBColProto.fetch.call(this, _extend({}, options, {silent: 
true}));
+      }
+
+      return BBColProto.fetch.call(this, options);
+    },
+
+    /**
+       Convenient method for making a `comparator` sorted by a model attribute
+       identified by `sortKey` and ordered by `order`.
+
+       Like a Backbone.Collection, a Backbone.PageableCollection will maintain
+       the __current page__ in sorted order on the client side if a 
`comparator`
+       is attached to it. If the collection is in client mode, you can attach a
+       comparator to #fullCollection to have all the pages reflect the global
+       sorting order by specifying an option `full` to `true`. You __must__ 
call
+       `sort` manually or #fullCollection.sort after calling this method to
+       force a resort.
+
+       While you can use this method to sort the current page in server mode,
+       the sorting order may not reflect the global sorting order due to the
+       additions or removals of the records on the server since the last
+       fetch. If you want the most updated page in a global sorting order, it 
is
+       recommended that you set #state.sortKey and optionally #state.order, and
+       then call #fetch.
+
+       @protected
+
+       @param {string} [sortKey=this.state.sortKey] See `state.sortKey`.
+       @param {number} [order=this.state.order] See `state.order`.
+       @param {(function(Backbone.Model, string): Object) | string} 
[sortValue] See #setSorting.
+
+       See 
[Backbone.Collection.comparator](http://backbonejs.org/#Collection-comparator).
+    */
+    _makeComparator: function (sortKey, order, sortValue) {
+      var state = this.state;
+
+      sortKey = sortKey || state.sortKey;
+      order = order || state.order;
+
+      if (!sortKey || !order) return;
+
+      if (!sortValue) sortValue = function (model, attr) {
+        return model.get(attr);
+      };
+
+      return function (left, right) {
+        var l = sortValue(left, sortKey), r = sortValue(right, sortKey), t;
+        if (order === 1) t = l, l = r, r = t;
+        if (l === r) return 0;
+        else if (l < r) return -1;
+        return 1;
+      };
+    },
+
+    /**
+       Adjusts the sorting for this pageable collection.
+
+       Given a `sortKey` and an `order`, sets `state.sortKey` and
+       `state.order`. A comparator can be applied on the client side to sort in
+       the order defined if `options.side` is `"client"`. By default the
+       comparator is applied to the #fullCollection. Set `options.full` to
+       `false` to apply a comparator to the current page under any mode. 
Setting
+       `sortKey` to `null` removes the comparator from both the current page 
and
+       the full collection.
+
+       If a `sortValue` function is given, it will be passed the `(model,
+       sortKey)` arguments and is used to extract a value from the model during
+       comparison sorts. If `sortValue` is not given, `model.get(sortKey)` is
+       used for sorting.
+
+       @chainable
+
+       @param {string} sortKey See `state.sortKey`.
+       @param {number} [order=this.state.order] See `state.order`.
+       @param {Object} [options]
+       @param {"server"|"client"} [options.side] By default, `"client"` if
+       `mode` is `"client"`, `"server"` otherwise.
+       @param {boolean} [options.full=true]
+       @param {(function(Backbone.Model, string): Object) | string} 
[options.sortValue]
+    */
+    setSorting: function (sortKey, order, options) {
+
+      var state = this.state;
+
+      state.sortKey = sortKey;
+      state.order = order = order || state.order;
+
+      var fullCollection = this.fullCollection;
+
+      var delComp = false, delFullComp = false;
+
+      if (!sortKey) delComp = delFullComp = true;
+
+      var mode = this.mode;
+      options = _extend({side: mode == "client" ? mode : "server", full: true},
+                        options);
+
+      var comparator = this._makeComparator(sortKey, order, options.sortValue);
+
+      var full = options.full, side = options.side;
+
+      if (side == "client") {
+        if (full) {
+          if (fullCollection) fullCollection.comparator = comparator;
+          delComp = true;
+        }
+        else {
+          this.comparator = comparator;
+          delFullComp = true;
+        }
+      }
+      else if (side == "server" && !full) {
+        this.comparator = comparator;
+      }
+
+      if (delComp) this.comparator = null;
+      if (delFullComp && fullCollection) fullCollection.comparator = null;
+
+      return this;
+    }
+
+  });
+
+  var PageableProto = PageableCollection.prototype;
+
+  return PageableCollection;
+
+}));

Reply via email to