AMBARI-21955. Update React version to 15.6.2 to get MIT license. (Sanket Shah via yusaku)
(cherry picked from commit 810488abcb0979cd36743d8fe1b7fccc6e5dc0d4) Change-Id: If0bcae949aa82068821f0a5814440d494ad56dad Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/e3931cc2 Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/e3931cc2 Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/e3931cc2 Branch: refs/heads/AMBARI-2.6.0.0 Commit: e3931cc2dd1e22bd891b8b0321042863fe95a218 Parents: 913bc84 Author: Yusaku Sako <yus...@hortonworks.com> Authored: Mon Oct 23 10:22:39 2017 -0700 Committer: Yusaku Sako <yus...@hortonworks.com> Committed: Mon Oct 23 13:48:02 2017 -0700 ---------------------------------------------------------------------- .gitignore | 3 + contrib/views/storm/pom.xml | 103 +- .../src/main/resources/images/icon-bolt.png | Bin 2157 -> 0 bytes .../src/main/resources/images/icon-spout.png | Bin 2620 -> 0 bytes .../storm/src/main/resources/images/loader.gif | Bin 15017 -> 0 bytes .../views/storm/src/main/resources/index.html | 45 - .../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.eot | Bin 20127 -> 0 bytes .../fonts/glyphicons-halflings-regular.svg | 288 - .../fonts/glyphicons-halflings-regular.ttf | Bin 45404 -> 0 bytes .../fonts/glyphicons-halflings-regular.woff | Bin 23424 -> 0 bytes .../fonts/glyphicons-halflings-regular.woff2 | Bin 18028 -> 0 bytes .../resources/libs/Bootstrap/images/clear.png | Bin 509 -> 0 bytes .../resources/libs/Bootstrap/images/loading.gif | Bin 1849 -> 0 bytes .../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 - .../libs/Font-Awesome/fonts/FontAwesome.otf | Bin 109688 -> 0 bytes .../Font-Awesome/fonts/fontawesome-webfont.eot | Bin 70807 -> 0 bytes .../Font-Awesome/fonts/fontawesome-webfont.svg | 655 - .../Font-Awesome/fonts/fontawesome-webfont.ttf | Bin 142072 -> 0 bytes .../Font-Awesome/fonts/fontawesome-webfont.woff | Bin 83588 -> 0 bytes .../fonts/fontawesome-webfont.woff2 | Bin 66624 -> 0 bytes .../resources/libs/Underscore/js/Underscore.js | 1548 -- .../resources/libs/bootbox/js/bootbox.min.js | 6 - .../src/main/resources/libs/d3/js/d3-tip.min.js | 1 - .../src/main/resources/libs/d3/js/d3.min.js | 5 - .../resources/libs/dagre-d3/dagre-d3.min.js | 28 - .../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/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 - .../scripts/collections/BaseCollection.js | 197 - .../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 - .../resources/scripts/components/BarChart.jsx | 402 - .../scripts/components/Breadcrumbs.jsx | 50 - .../main/resources/scripts/components/Modal.jsx | 60 - .../scripts/components/RadialChart.jsx | 127 - .../resources/scripts/components/SearchLogs.jsx | 89 - .../main/resources/scripts/components/Table.jsx | 101 - .../scripts/components/TopologyGraph.jsx | 199 - .../scripts/containers/ClusterSummary.jsx | 122 - .../scripts/containers/NimbusConfigSummary.jsx | 103 - .../scripts/containers/NimbusSummary.jsx | 139 - .../scripts/containers/SupervisorSummary.jsx | 155 - .../containers/TopologyConfiguration.jsx | 93 - .../scripts/containers/TopologyDetailGraph.jsx | 66 - .../scripts/containers/TopologyListing.jsx | 188 - .../storm/src/main/resources/scripts/main.js | 98 - .../main/resources/scripts/models/BaseModel.js | 83 - .../main/resources/scripts/models/VCluster.js | 42 - .../main/resources/scripts/models/VNimbus.js | 42 - .../resources/scripts/models/VNimbusConfig.js | 42 - .../resources/scripts/models/VSupervisor.js | 42 - .../main/resources/scripts/models/VTopology.js | 90 - .../resources/scripts/models/VTopologyConfig.js | 34 - .../scripts/modules/Table/PageableTable.jsx | 47 - .../scripts/modules/Table/Pagination.jsx | 161 - .../src/main/resources/scripts/router/Router.js | 123 - .../src/main/resources/scripts/utils/Globals.js | 30 - .../main/resources/scripts/utils/Overrides.js | 30 - .../src/main/resources/scripts/utils/Utils.js | 113 - .../scripts/views/ComponentDetailView.jsx | 534 - .../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 | 214 - .../resources/scripts/views/RebalanceView.jsx | 223 - .../scripts/views/SupervisorSummaryView.jsx | 65 - .../scripts/views/TopologyDetailView.jsx | 1039 - .../scripts/views/TopologyListingView.jsx | 65 - .../storm/src/main/resources/styles/style.css | 579 - .../views/storm/src/main/resources/ui/.babelrc | 25 + .../src/main/resources/ui/.eslintignore.js | 3 + .../storm/src/main/resources/ui/.eslintrc.js | 58 + .../src/main/resources/ui/app/scripts/app.js | 40 + .../ui/app/scripts/components/BarChart.jsx | 429 + .../ui/app/scripts/components/Breadcrumbs.jsx | 45 + .../app/scripts/components/CommonExpanded.jsx | 30 + .../scripts/components/CommonNotification.jsx | 69 + .../app/scripts/components/CommonPagination.jsx | 56 + .../components/CommonSwitchComponent.jsx | 41 + .../scripts/components/CommonWindowPanel.jsx | 99 + .../scripts/components/CustomToastContainer.jsx | 41 + .../ui/app/scripts/components/Editable.jsx | 127 + .../ui/app/scripts/components/FSModel.jsx | 149 + .../ui/app/scripts/components/FSReactToastr.jsx | 37 + .../ui/app/scripts/components/Footer.jsx | 28 + .../scripts/components/LogLevelComponent.jsx | 236 + .../ui/app/scripts/components/ProfilingView.jsx | 168 + .../ui/app/scripts/components/RadialChart.jsx | 134 + .../scripts/components/RebalanceTopology.jsx | 152 + .../ui/app/scripts/components/SearchLogs.jsx | 84 + .../ui/app/scripts/components/TopologyGraph.jsx | 208 + .../ui/app/scripts/containers/BaseContainer.jsx | 50 + .../app/scripts/containers/ClusterSummary.jsx | 125 + .../scripts/containers/ComponentDetailView.jsx | 714 + .../ui/app/scripts/containers/Dashboard.jsx | 52 + .../scripts/containers/NimbusConfigSummary.jsx | 126 + .../ui/app/scripts/containers/NimbusSummary.jsx | 150 + .../scripts/containers/SupervisorSummary.jsx | 165 + .../scripts/containers/TopologyDetailView.jsx | 862 + .../app/scripts/containers/TopologyListing.jsx | 222 + .../src/main/resources/ui/app/scripts/main.js | 46 + .../ui/app/scripts/rest/TopologyREST.js | 118 + .../resources/ui/app/scripts/routers/routes.jsx | 68 + .../resources/ui/app/scripts/utils/Constants.js | 45 + .../resources/ui/app/scripts/utils/Utils.js | 51 + .../resources/ui/app/styles/css/bootstrap.css | 6757 ++++++ .../ui/app/styles/css/font-awesome.min.css | 4 + .../main/resources/ui/app/styles/css/style.css | 836 + .../resources/ui/app/styles/css/toastr.min.css | 1 + .../ui/app/styles/fonts/fontawesome-webfont.eot | Bin 0 -> 76518 bytes .../ui/app/styles/fonts/fontawesome-webfont.svg | 685 + .../ui/app/styles/fonts/fontawesome-webfont.ttf | Bin 0 -> 152796 bytes .../app/styles/fonts/fontawesome-webfont.woff | Bin 0 -> 90412 bytes .../app/styles/fonts/fontawesome-webfont.woff2 | Bin 0 -> 71896 bytes .../fonts/glyphicons-halflings-regular.eot | Bin 0 -> 20127 bytes .../fonts/glyphicons-halflings-regular.svg | 288 + .../fonts/glyphicons-halflings-regular.ttf | Bin 0 -> 45404 bytes .../fonts/glyphicons-halflings-regular.woff | Bin 0 -> 23424 bytes .../fonts/glyphicons-halflings-regular.woff2 | Bin 0 -> 18028 bytes .../resources/ui/app/styles/img/icon-bolt.png | Bin 0 -> 2157 bytes .../resources/ui/app/styles/img/icon-spout.png | Bin 0 -> 2620 bytes .../main/resources/ui/app/styles/img/loader.gif | Bin 0 -> 15017 bytes .../resources/ui/config/webpack.config.base.js | 101 + .../ui/config/webpack.config.development.js | 64 + .../ui/config/webpack.config.production.js | 131 + .../storm/src/main/resources/ui/dev-server.js | 101 + .../storm/src/main/resources/ui/index.html | 47 + .../storm/src/main/resources/ui/package.json | 110 + pom.xml | 7 +- 147 files changed, 14285 insertions(+), 55314 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/e3931cc2/.gitignore ---------------------------------------------------------------------- diff --git a/.gitignore b/.gitignore index a40e61a..77aadb7 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ rebel.xml rebel-remote.xml out createDDL.jdbc +/contrib/views/storm/src/main/resources/ui/node_modules/ +/contrib/views/storm/src/main/resources/ui/public/ +/contrib/views/storm/src/main/resources/ui/npm-debug.log http://git-wip-us.apache.org/repos/asf/ambari/blob/e3931cc2/contrib/views/storm/pom.xml ---------------------------------------------------------------------- diff --git a/contrib/views/storm/pom.xml b/contrib/views/storm/pom.xml index cf0e303..0eb1cfb 100644 --- a/contrib/views/storm/pom.xml +++ b/contrib/views/storm/pom.xml @@ -23,6 +23,7 @@ </parent> <properties> <ambari.dir>${project.parent.parent.parent.basedir}</ambari.dir> + <ui.directory>${basedir}/src/main/resources/ui</ui.directory> </properties> <modelVersion>4.0.0</modelVersion> <groupId>org.apache.ambari.contrib.views</groupId> @@ -55,14 +56,17 @@ </execution> </executions> </plugin> + <plugin> <groupId>org.apache.rat</groupId> <artifactId>apache-rat-plugin</artifactId> <configuration> <excludes> - <exclude>src/main/resources/libs/**</exclude> - <exclude>src/main/resources/styles/**</exclude> - <exclude>src/main/resources/templates/**</exclude> + <exclude>src/main/resources/ui/.*</exclude> + <exclude>src/main/resources/ui/node_modules/**</exclude> + <exclude>src/main/resources/ui/public/**</exclude> + <exclude>src/main/resources/ui/app/styles/**</exclude> + <exclude>src/main/resources/ui/package.json</exclude> </excludes> </configuration> <executions> @@ -74,6 +78,52 @@ </execution> </executions> </plugin> + + <!-- Building frontend --> + <plugin> + <groupId>com.github.eirslett</groupId> + <artifactId>frontend-maven-plugin</artifactId> + <version>1.4</version> + <configuration> + <workingDirectory>src/main/resources/ui/</workingDirectory> + <installDirectory>target</installDirectory> + <npmInheritsProxyConfigFromMaven>false</npmInheritsProxyConfigFromMaven> + </configuration> + <executions> + <execution> + <id>install node and npm</id> + <phase>generate-sources</phase> + <goals> + <goal>install-node-and-npm</goal> + </goals> + <configuration> + <nodeVersion>v5.6.0</nodeVersion> + <npmVersion>3.6.0</npmVersion> + </configuration> + </execution> + <execution> + <id>npm install</id> + <goals> + <goal>npm</goal> + </goals> + <phase>generate-sources</phase> + <configuration> + <arguments>install</arguments> + </configuration> + </execution> + <execution> + <id>npm run build</id> + <goals> + <goal>npm</goal> + </goals> + <phase>generate-sources</phase> + <configuration> + <arguments>run build</arguments> + </configuration> + </execution> + </executions> + </plugin> + <plugin> <artifactId>maven-dependency-plugin</artifactId> <executions> @@ -98,6 +148,27 @@ </executions> </plugin> </plugins> + <resources> + <resource> + <directory>src/main/resources/ui/public</directory> + <filtering>false</filtering> + </resource> + + <resource> + <directory>src/main/resources/WEB-INF</directory> + <targetPath>WEB-INF</targetPath> + <filtering>false</filtering> + </resource> + + <resource> + <directory>src/main/resources/</directory> + <filtering>false</filtering> + <includes> + <include>view.xml</include> + <include>view.log4j.properties</include> + </includes> + </resource> + </resources> </build> <dependencies> <dependency> @@ -125,4 +196,30 @@ </dependency> </dependencies> + <profiles> + <profile> + <id>windows</id> + <activation> + <os> + <family>win</family> + </os> + </activation> + <properties> + <node.executable>node.exe</node.executable> + <skip.nodegyp.chmod>true</skip.nodegyp.chmod> + </properties> + </profile> + <profile> + <id>linux</id> + <activation> + <os> + <family>unix</family> + </os> + </activation> + <properties> + <node.executable>node</node.executable> + <skip.nodegyp.chmod>false</skip.nodegyp.chmod> + </properties> + </profile> + </profiles> </project> http://git-wip-us.apache.org/repos/asf/ambari/blob/e3931cc2/contrib/views/storm/src/main/resources/images/icon-bolt.png ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/images/icon-bolt.png b/contrib/views/storm/src/main/resources/images/icon-bolt.png deleted file mode 100644 index f3eb3db..0000000 Binary files a/contrib/views/storm/src/main/resources/images/icon-bolt.png and /dev/null differ http://git-wip-us.apache.org/repos/asf/ambari/blob/e3931cc2/contrib/views/storm/src/main/resources/images/icon-spout.png ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/images/icon-spout.png b/contrib/views/storm/src/main/resources/images/icon-spout.png deleted file mode 100644 index 9153d26..0000000 Binary files a/contrib/views/storm/src/main/resources/images/icon-spout.png and /dev/null differ http://git-wip-us.apache.org/repos/asf/ambari/blob/e3931cc2/contrib/views/storm/src/main/resources/images/loader.gif ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/images/loader.gif b/contrib/views/storm/src/main/resources/images/loader.gif deleted file mode 100644 index 766329d..0000000 Binary files a/contrib/views/storm/src/main/resources/images/loader.gif and /dev/null differ http://git-wip-us.apache.org/repos/asf/ambari/blob/e3931cc2/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 deleted file mode 100644 index df94a76..0000000 --- a/contrib/views/storm/src/main/resources/index.html +++ /dev/null @@ -1,45 +0,0 @@ -<!doctype html> -<!-- -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 ---> -<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]--> -<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]--> -<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]--> -<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]--> - <head> - <meta charset="utf-8"> - <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> - <title>Apache Storm</title> - <meta name="description" content=""> - <meta name="viewport" content="width=device-width"> - <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="loader"></div> - <div class="container-fluid"> - <section id="container"></section> - <footer id="footer"></footer> - </div> - <script data-main="scripts/main" src="libs/require-js/js/require.min.js"></script> - </body> -</html> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ambari/blob/e3931cc2/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 deleted file mode 100644 index d8ccc65..0000000 --- a/contrib/views/storm/src/main/resources/libs/Backbone-Paginator/js/backbone-paginator.min.js +++ /dev/null @@ -1,1325 +0,0 @@ -/* - 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; - -}));