couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From kxe...@apache.org
Subject [06/19] fauxton commit: updated refs/heads/import-master to 4ab2cde
Date Thu, 17 Apr 2014 08:37:06 GMT
Fauxton: Improved pagination

This is a new version of pagination in Fauxton using skip. It uses a
PagingCollection that has the main algorithm for pagination and exposes
a nice api.

This is an intermediate step as this is a much better pagination than we
have at the moment. However using just skip for pagination is not
optimal as there are two cases where skip pagination fails - For very
large skips and for when documents that a user have paginated past have
been deleted.

The next step once this has landed will be to add in a startkey_docid
pagination as well. The PagingCollection would then decided which method
to use to paginate for an index.


Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/0f2c148a
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/0f2c148a
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/0f2c148a

Branch: refs/heads/import-master
Commit: 0f2c148a9a796750f0e70badb68e526fa2bbe8f1
Parents: becb46e
Author: Garren Smith <garren.smith@gmail.com>
Authored: Thu Mar 20 09:46:59 2014 +0200
Committer: Garren Smith <garren.smith@gmail.com>
Committed: Thu Apr 10 12:04:52 2014 +0200

----------------------------------------------------------------------
 app/addons/databases/views.js                   |   2 +-
 app/addons/documents/resources.js               | 181 ++-------------
 app/addons/documents/routes.js                  |  81 +++----
 .../documents/templates/advanced_options.html   |   5 +-
 app/addons/documents/views.js                   |  14 +-
 app/addons/fauxton/components.js                |  16 +-
 app/config.js                                   |   3 +-
 assets/js/plugins/cloudant.pagingcollection.js  | 224 +++++++++++++++++++
 8 files changed, 288 insertions(+), 238 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/0f2c148a/app/addons/databases/views.js
----------------------------------------------------------------------
diff --git a/app/addons/databases/views.js b/app/addons/databases/views.js
index d632486..0806b92 100644
--- a/app/addons/databases/views.js
+++ b/app/addons/databases/views.js
@@ -81,7 +81,7 @@ function(app, Components, FauxtonAPI, Databases) {
           // TODO: switch to using a model, or Databases.databaseUrl()
           // Neither of which are in scope right now
           // var db = new Database.Model({id: dbname});
-          var url = ["/database/", app.utils.safeURLName(dbname), "/_all_docs?limit=" + Databases.DocLimit].join('');
+          var url = ["/database/", app.utils.safeURLName(dbname), "/_all_docs"].join('');
           FauxtonAPI.navigate(url);
       } else {
         FauxtonAPI.addNotification({

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/0f2c148a/app/addons/documents/resources.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/resources.js b/app/addons/documents/resources.js
index e47bd61..a787f0d 100644
--- a/app/addons/documents/resources.js
+++ b/app/addons/documents/resources.js
@@ -12,10 +12,11 @@
 
 define([
   "app",
-  "api"
+  "api",
+  "cloudant.pagingcollection"
 ],
 
-function(app, FauxtonAPI) {
+function(app, FauxtonAPI, PagingCollection) {
   var Documents = FauxtonAPI.addon();
 
   Documents.QueryParams = (function () {
@@ -40,70 +41,7 @@ function(app, FauxtonAPI) {
     };
   })();
 
-  Documents.paginate = {
-    history: [],
-    calculate: function (doc, defaultParams, currentParams, _isAllDocs) {
-      var docId = '',
-          lastId = '',
-          isView = !!!_isAllDocs,
-          key;
-
-      if (currentParams.keys) {
-        throw "Cannot paginate when keys is specfied";
-      }
-
-      if (_.isUndefined(doc)) {
-        throw "Require docs to paginate";
-      }
-
-      // defaultParams should always override the user-specified parameters
-      _.extend(currentParams, defaultParams);
-
-      lastId = doc.id || doc._id;
-
-      // If we are paginating on a view, we need to set a ``key`` and a ``docId``
-      // and expect that they are different values.
-      if (isView) {
-        key = doc.key;
-        docId = lastId;
-      } else {
-        docId = key = lastId;
-      }
-
-      // Set parameters to paginate
-      if (isView) {
-        currentParams.startkey_docid = docId;
-        currentParams.startkey = key;
-      } else if (currentParams.startkey) {
-        currentParams.startkey = key;
-      } else {
-        currentParams.startkey_docid = docId;
-      }
-
-      return currentParams;
-    },
-
-    next: function (docs, currentParams, perPage, _isAllDocs) {
-      var params = {limit: perPage, skip: 1},
-          doc = _.last(docs);
-
-      this.history.push(_.clone(currentParams));
-      return this.calculate(doc, params, currentParams, _isAllDocs);
-    },
-
-    previous: function (docs, currentParams, perPage, _isAllDocs) {
-      var params = this.history.pop(),
-          doc = _.first(docs);
-
-      params.limit = perPage;
-      return params;
-    },
-
-    reset: function () {
-      this.history = [];
-    }
-  };
-
+  
   Documents.Doc = FauxtonAPI.Model.extend({
     idAttribute: "_id",
     documentation: function(){
@@ -357,25 +295,8 @@ function(app, FauxtonAPI) {
 
   });
 
-  var DefaultParametersMixin = function() {
-    // keep this variable private
-    var defaultParams;
-
-    return {
-      saveDefaultParameters: function() {
-        // store the default parameters so we can reset to the first page
-        defaultParams = _.clone(this.params);
-      },
-
-      restoreDefaultParameters: function() {
-        this.params = _.clone(defaultParams);
-      }
-    };
-  };
-
-  Documents.AllDocs = FauxtonAPI.Collection.extend(_.extend({}, DefaultParametersMixin(),
{
+  Documents.AllDocs = PagingCollection.extend({
     model: Documents.Doc,
-    isAllDocs: true,
     documentation: function(){
       return "docs";
     },
@@ -389,11 +310,9 @@ function(app, FauxtonAPI) {
       if (!this.params.limit) {
         this.params.limit = this.perPageLimit;
       }
-
-      this.saveDefaultParameters();
     },
 
-    url: function(context, params) {
+    urlRef: function(context, params) {
       var query = "";
 
       if (params) {
@@ -415,6 +334,10 @@ function(app, FauxtonAPI) {
       }
     },
 
+    url: function () {
+      return this.urlRef.apply(this, arguments);
+    },
+
     simple: function () {
       var docs = this.map(function (item) {
         return {
@@ -429,15 +352,6 @@ function(app, FauxtonAPI) {
       });
     },
 
-    updateLimit: function (limit) {
-      this.perPageLimit = limit;
-      this.params.limit = limit;
-    },
-
-    updateParams: function (params) {
-      this.params = params;
-    },
-
     totalRows: function() {
       return this.viewMeta.total_rows || "unknown";
     },
@@ -456,37 +370,17 @@ function(app, FauxtonAPI) {
     parse: function(resp) {
       var rows = resp.rows;
 
-      this.viewMeta = {
-        total_rows: resp.total_rows,
-        offset: resp.offset,
-        update_seq: resp.update_seq
-      };
-
-      //Paginating, don't show first item as it was the last
-      //item in the previous page
-      if (this.skipFirstItem) {
-        rows = rows.splice(1);
-      }
-
       // remove any query errors that may return without doc info
       // important for when querying keys on all docs
-      var noQueryErrors = _.filter(rows, function(row){
+      resp.rows = _.filter(rows, function(row){
         return row.value;
       });
 
-      return _.map(noQueryErrors, function(row) {
-          return {
-            _id: row.id,
-            _rev: row.value.rev,
-            value: row.value,
-            key: row.key,
-            doc: row.doc || undefined
-          };
-      });
+      return PagingCollection.prototype.parse.call(this, resp);
     }
-  }));
+  });
 
-  Documents.IndexCollection = FauxtonAPI.Collection.extend(_.extend({}, DefaultParametersMixin(),
{
+  Documents.IndexCollection = PagingCollection.extend({
     model: Documents.ViewRow,
     documentation: function(){
       return "docs";
@@ -498,17 +392,14 @@ function(app, FauxtonAPI) {
       this.idxType = "_view";
       this.view = options.view;
       this.design = options.design.replace('_design/','');
-      this.skipFirstItem = false;
       this.perPageLimit = options.perPageLimit || 20;
 
       if (!this.params.limit) {
         this.params.limit = this.perPageLimit;
       }
-
-      this.saveDefaultParameters();
     },
 
-    url: function(context, params) {
+    urlRef: function(context, params) {
       var query = "";
       if (params) {
         if (!_.isEmpty(params)) {
@@ -533,18 +424,8 @@ function(app, FauxtonAPI) {
       return url.join("/") + query;
     },
 
-    updateParams: function (params) {
-      this.params = params;
-    },
-
-    updateLimit: function (limit) {
-      if (this.params.startkey_docid && this.params.startkey) {
-        //we are paginating so set limit + 1
-        this.params.limit = limit + 1;
-        return;
-      }
-
-      this.params.limit = limit;
+    url: function () {
+      return this.urlRef.apply(this, arguments);
     },
 
     totalRows: function() {
@@ -579,23 +460,7 @@ function(app, FauxtonAPI) {
       this.endTime = new Date().getTime();
       this.requestDuration = (this.endTime - this.startTime);
 
-      if (this.skipFirstItem) {
-        rows = rows.splice(1);
-      }
-
-      this.viewMeta = {
-        total_rows: resp.total_rows,
-        offset: resp.offset,
-        update_seq: resp.update_seq
-      };
-      return _.map(rows, function(row) {
-        return {
-          value: row.value,
-          key: row.key,
-          doc: row.doc,
-          id: row.id
-        };
-      });
+      return PagingCollection.prototype.parse.apply(this, arguments);
     },
 
     buildAllDocs: function(){
@@ -606,7 +471,7 @@ function(app, FauxtonAPI) {
     // we can get the request duration
     fetch: function () {
       this.startTime = new Date().getTime();
-      return FauxtonAPI.Collection.prototype.fetch.call(this);
+      return PagingCollection.prototype.fetch.call(this);
     },
 
     allDocs: function(){
@@ -645,10 +510,10 @@ function(app, FauxtonAPI) {
       return timeString;
     }
 
-  }));
+  });
 
 
-  Documents.PouchIndexCollection = FauxtonAPI.Collection.extend(_.extend({}, DefaultParametersMixin(),
{
+  Documents.PouchIndexCollection = PagingCollection.extend({
     model: Documents.ViewRow,
     documentation: function(){
       return "docs";
@@ -661,8 +526,6 @@ function(app, FauxtonAPI) {
       this.params = _.extend({limit: 20, reduce: false}, options.params);
 
       this.idxType = "_view";
-
-      this.saveDefaultParameters();
     },
 
     url: function () {
@@ -717,7 +580,7 @@ function(app, FauxtonAPI) {
     allDocs: function(){
       return this.models;
     }
-  }));
+  });
 
 
 

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/0f2c148a/app/addons/documents/routes.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/routes.js b/app/addons/documents/routes.js
index 699a496..5e8834f 100644
--- a/app/addons/documents/routes.js
+++ b/app/addons/documents/routes.js
@@ -168,10 +168,14 @@ function(app, FauxtonAPI, Documents, Databases) {
 
       this.data.designDocs = new Documents.AllDocs(null, {
         database: this.data.database,
+        paging: {
+          pageSize: 500
+        },
         params: {
-          startkey: '"_design"',
-          endkey: '"_design1"',
-          include_docs: true
+          startkey: '_design',
+          endkey: '_design1',
+          include_docs: true,
+          limit: 500
         }
       });
 
@@ -182,11 +186,11 @@ function(app, FauxtonAPI, Documents, Databases) {
     },
 
     establish: function () {
-      return this.data.designDocs.fetch();
+      return this.data.designDocs.fetch({reset: true});
     },
 
     createParams: function (options) {
-      var urlParams = app.getParams(options);
+      var urlParams = Documents.QueryParams.parse(app.getParams(options));
       return {
         urlParams: urlParams,
         docParams: _.extend(_.clone(urlParams), {limit: this.getDocPerPageLimit(urlParams,
20)})
@@ -223,6 +227,8 @@ function(app, FauxtonAPI, Documents, Databases) {
         collection: this.data.database.allDocs
       }));
 
+      this.data.database.allDocs.paging.pageSize = this.getDocPerPageLimit(urlParams, parseInt(docParams.limit,
10));
+
       this.setView("#dashboard-upper-content", new Documents.Views.AllDocsLayout({
         database: this.data.database,
         collection: this.data.database.allDocs,
@@ -240,9 +246,7 @@ function(app, FauxtonAPI, Documents, Databases) {
         {"name": this.data.database.id, "link": Databases.databaseUrl(this.data.database)}
       ];
 
-      this.apiUrl = [this.data.database.allDocs.url("apiurl", urlParams), this.data.database.allDocs.documentation()
];
-      //reset the pagination history - the history is used for pagination.previous
-      Documents.paginate.reset();
+      this.apiUrl = [this.data.database.allDocs.urlRef("apiurl", urlParams), this.data.database.allDocs.documentation()
];
     },
 
     viewFn: function (databaseName, ddoc, view) {
@@ -257,7 +261,10 @@ function(app, FauxtonAPI, Documents, Databases) {
         database: this.data.database,
         design: decodeDdoc,
         view: view,
-        params: docParams
+        params: docParams,
+        paging: {
+          pageSize: this.getDocPerPageLimit(urlParams, parseInt(docParams.limit, 10))
+        }
       });
      
       this.viewEditor = this.setView("#dashboard-upper-content", new Documents.Views.ViewEditor({
@@ -290,8 +297,7 @@ function(app, FauxtonAPI, Documents, Databases) {
         ];
       };
 
-      this.apiUrl = [this.data.indexedDocs.url("apiurl", urlParams), "docs"];
-      Documents.paginate.reset();
+      this.apiUrl = [this.data.indexedDocs.urlRef("apiurl", urlParams), "docs"];
     },
 
     ddocInfo: function (designDoc, designDocs, view) {
@@ -344,22 +350,27 @@ function(app, FauxtonAPI, Documents, Databases) {
           urlParams = params.urlParams,
           docParams = params.docParams,
           ddoc = event.ddoc,
+          pageSize,
           collection;
 
-      docParams.limit = this.getDocPerPageLimit(urlParams, this.documentsView.perPage());
+      docParams.limit = pageSize = this.getDocPerPageLimit(urlParams, this.documentsView.perPage());
       this.documentsView.forceRender();
 
       if (event.allDocs) {
         this.eventAllDocs = true; // this is horrible. But I cannot get the trigger not to
fire the route!
         this.data.database.buildAllDocs(docParams);
         collection = this.data.database.allDocs;
+        collection.paging.pageSize = pageSize;
 
       } else {
         collection = this.data.indexedDocs = new Documents.IndexCollection(null, {
           database: this.data.database,
           design: ddoc,
           view: view,
-          params: docParams
+          params: docParams,
+          paging: {
+            pageSize: pageSize
+          }
         });
 
         if (!this.documentsView) {
@@ -378,8 +389,7 @@ function(app, FauxtonAPI, Documents, Databases) {
       this.documentsView.setCollection(collection);
       this.documentsView.setParams(docParams, urlParams);
 
-      this.apiUrl = [collection.url("apiurl", urlParams), "docs"];
-      Documents.paginate.reset();
+      this.apiUrl = [collection.urlRef("apiurl", urlParams), "docs"];
     },
 
     updateAllDocsFromPreview: function (event) {
@@ -405,47 +415,18 @@ function(app, FauxtonAPI, Documents, Databases) {
     perPageChange: function (perPage) {
       // We need to restore the collection parameters to the defaults (1st page)
       // and update the page size
-      var params = this.documentsView.collection.restoreDefaultParameters();
       this.perPage = perPage;
-      this.documentsView.updatePerPage(perPage);
       this.documentsView.forceRender();
-      this.documentsView.collection.params.limit = perPage;
+      this.documentsView.collection.pageSizeReset(perPage, {fetch: false});
       this.setDocPerPageLimit(perPage);
     },
 
     paginate: function (options) {
-      var params = {},
-          urlParams = app.getParams(),
-          collection = this.documentsView.collection;
+      var collection = this.documentsView.collection;
 
       this.documentsView.forceRender();
-
-      // this is really ugly. But we basically need to make sure that
-      // all parameters are in the correct state and have been parsed before we
-      // calculate how to paginate the collection
-      collection.params = Documents.QueryParams.parse(collection.params);
-      urlParams = Documents.QueryParams.parse(urlParams);
-
-      if (options.direction === 'next') {
-          params = Documents.paginate.next(collection.toJSON(), 
-                                           collection.params,
-                                           options.perPage, 
-                                           !!collection.isAllDocs);
-      } else {
-          params = Documents.paginate.previous(collection.toJSON(), 
-                                               collection.params, 
-                                               options.perPage, 
-                                               !!collection.isAllDocs);
-      }
-
-      // use the perPage sent from IndexPagination as it calculates how many
-      // docs to fetch for next page
-      params.limit = options.perPage;
-
-      // again not pretty but need to make sure all the parameters can be correctly
-      // built into a query
-      params = Documents.QueryParams.stringify(params);
-      collection.updateParams(params);
+      collection.paging.pageSize = options.perPage;
+      var promise = collection[options.direction]({fetch: false});
     },
 
     reloadDesignDocs: function (event) {
@@ -476,9 +457,9 @@ function(app, FauxtonAPI, Documents, Databases) {
       } 
 
       if (!urlParams.limit || urlParams.limit > storedPerPage) {
-        return storedPerPage;
+        return parseInt(storedPerPage, 10);
       } else {
-        return urlParams.limit;
+        return parseInt(urlParams.limit, 10);
       }
     }
 

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/0f2c148a/app/addons/documents/templates/advanced_options.html
----------------------------------------------------------------------
diff --git a/app/addons/documents/templates/advanced_options.html b/app/addons/documents/templates/advanced_options.html
index d8d57cd..55c5946 100644
--- a/app/addons/documents/templates/advanced_options.html
+++ b/app/addons/documents/templates/advanced_options.html
@@ -50,13 +50,14 @@ the License.
       <label class="drop-down inline">
         Limit:
         <select name="limit" class="input-small">
+          <option selected="selected">None</option>
           <option>5</option>
           <option>10</option>
-          <option selected="selected">20</option>
+          <option>20</option>
           <option>30</option>
           <option>50</option>
           <option>100</option>
-		  <option>500</option>
+		      <option>500</option>
         </select>
       </label>
       <label for="skipRows" class="inline drop-down">

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/0f2c148a/app/addons/documents/views.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/views.js b/app/addons/documents/views.js
index cc23e19..351b2b0 100644
--- a/app/addons/documents/views.js
+++ b/app/addons/documents/views.js
@@ -681,8 +681,6 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColum
     },
 
     addPagination: function () {
-      var collection = this.collection;
-
       this.pagination = new Components.IndexPagination({
         collection: this.collection,
         scrollToSelector: '#dashboard-content',
@@ -703,9 +701,7 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb, resizeColum
         this.addPagination();
       }
 
-      if (!this.params.keys) { //cannot paginate with keys
-        this.insertView('#documents-pagination', this.pagination);
-      }
+      this.insertView('#documents-pagination', this.pagination);
 
       if (!this.allDocsNumber) {
         this.allDocsNumber = new Views.AllDocsNumber({
@@ -749,10 +745,6 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb,
resizeColum
 
     perPage: function () {
       return this.allDocsNumber.perPage();
-    },
-
-    updatePerPage: function (newPerPage) {
-      this.collection.updateLimit(newPerPage);
     }
   });
 
@@ -1741,9 +1733,8 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb,
resizeColum
         this.ddocID = this.model.id;
       } else {
         var ddocDecode = decodeURIComponent(this.ddocID);
-        this.model = this.ddocs.get(ddocDecode).dDocModel();
+        this.model = this.ddocs.get(this.ddocID).dDocModel();
         this.reduceFunStr = this.model.viewHasReduce(this.viewName);
-        
       }
 
       this.designDocSelector = this.setView('.design-doc-group', new Views.DesignDocSelector({
@@ -1752,7 +1743,6 @@ function(app, FauxtonAPI, Components, Documents, Databases, pouchdb,
resizeColum
         database: this.database
       }));
 
-
       if (!this.newView) {
         this.eventer = _.extend({}, Backbone.Events);
 

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/0f2c148a/app/addons/fauxton/components.js
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/components.js b/app/addons/fauxton/components.js
index 25f623c..47f4726 100644
--- a/app/addons/fauxton/components.js
+++ b/app/addons/fauxton/components.js
@@ -84,28 +84,18 @@ function(app, FauxtonAPI, ace, spin) {
     },
 
     canShowPreviousfn: function () {
-      if (this._pageStart === 1 || !this.enabled) {
-        return false;
-      }
-      return true;
+      if (!this.enabled) { return this.enabled; }
+      return this.collection.hasPrevious();
     },
 
     canShowNextfn: function () {
       if (!this.enabled) { return this.enabled; }
 
-      if (this.collection.length < (this.perPage -1)) {
-        return false;
-      }
-
       if ((this.pageStart() + this.perPage) >= this.docLimit) {
         return false;
       }
 
-      if (this.collection.viewMeta && this.collection.viewMeta.total_rows <= this.pageStart()
+ this.perPage) {
-        return false;
-      }
-
-      return true;
+      return this.collection.hasNext();
     },
 
     previousClicked: function (event) {

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/0f2c148a/app/config.js
----------------------------------------------------------------------
diff --git a/app/config.js b/app/config.js
index 4a2f136..edcd9a2 100644
--- a/app/config.js
+++ b/app/config.js
@@ -30,7 +30,8 @@ require.config({
     spin: "../assets/js/libs/spin.min",
     d3: "../assets/js/libs/d3",
     "nv.d3": "../assets/js/libs/nv.d3",
-    "ace":"../assets/js/libs/ace"
+    "ace":"../assets/js/libs/ace",
+    "cloudant.pagingcollection": "../assets/js/plugins/cloudant.pagingcollection"
   },
 
   baseUrl: '/',

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/0f2c148a/assets/js/plugins/cloudant.pagingcollection.js
----------------------------------------------------------------------
diff --git a/assets/js/plugins/cloudant.pagingcollection.js b/assets/js/plugins/cloudant.pagingcollection.js
new file mode 100644
index 0000000..2ab5eaf
--- /dev/null
+++ b/assets/js/plugins/cloudant.pagingcollection.js
@@ -0,0 +1,224 @@
+// Licensed 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.
+
+(function(root, factory) {
+  "use strict";
+  // start with AMD, so paginate could then be used by ``var paginate = require('paginate');``
+  if (typeof define === 'function' && define.amd) {
+    define(['underscore', 'backbone', 'jquery'], function(_, Backbone, $) {
+      // Export global even in AMD case in case this script is loaded with
+      // others that may still expect a global paginate.
+      return factory(root, null, _, Backbone, $.param);
+    });
+
+  // Next check for Node.js or CommonJS. Also look to see if either
+  // underscore or lodash are the modules being used
+  } else if (typeof exports !== 'undefined') {
+    var Backbone = require('Backbone'),
+        $param = require('querystring').stringify,
+        _;
+    try {
+      _ = require('underscore');
+    } catch(e) {
+      _ = require('lodash');
+    }
+    factory(root, exports, _, Backbone, $param);
+
+  // Finally, register as a browser global.
+  } else {
+    root.PagingCollection = factory(root, {}, root._, root.Backbone, root.$.param);
+  }
+
+}(this, function(root, exports, _, Backbone, $param) {
+  "use strict";
+
+  //PagingCollection
+  //----------------
+
+  // A PagingCollection knows how to build appropriate requests to the
+  // CouchDB-like server and how to fetch. The Collection will always contain a
+  // single page of documents.
+
+  var PagingCollection = Backbone.Collection.extend({
+
+    // initialize parameters and page size
+    constructor: function() {
+      Backbone.Collection.apply(this, arguments);
+      this.configure.apply(this, arguments);
+    },
+
+    configure: function(collections, options) {
+      var querystring = _.result(this, "url").split("?")[1] || "";
+      this.paging = _.defaults((options.paging || {}), {
+        defaultParams: _.defaults({}, options.params, this._parseQueryString(querystring)),
+        hasNext: false,
+        hasPrevious: false,
+        params: {},
+        pageSize: 20,
+        direction: undefined
+      });
+
+      this.paging.params = _.clone(this.paging.defaultParams);
+      this.updateUrlQuery(this.paging.defaultParams);
+    },
+
+    calculateParams: function(currentParams, skipIncrement, limitIncrement) {
+
+      var params = _.clone(currentParams);
+      params.skip = (parseInt(currentParams.skip, 10) || 0) + skipIncrement;
+
+      // guard against hard limits
+      if(this.paging.defaultParams.limit) {
+        params.limit = Math.min(this.paging.defaultParams.limit, params.limit);
+      }
+      // request an extra row so we know that there are more results
+      params.limit = limitIncrement + 1;
+      // prevent illegal skip values
+      params.skip = Math.max(params.skip, 0);
+
+      return params;
+    },
+
+    pageSizeReset: function(pageSize, opts) {
+      var options = _.defaults((opts || {}), {fetch: true});
+      this.paging.direction = undefined;
+      this.paging.pageSize = pageSize;
+      this.paging.params = this.paging.defaultParams;
+      this.paging.params.limit = pageSize;
+      this.updateUrlQuery(this.paging.params);
+      if (options.fetch) {
+        return this.fetch();
+      }
+    },
+
+    _parseQueryString: function(uri) {
+      var queryString = decodeURI(uri).split(/&/);
+
+      return _.reduce(queryString, function (parsedQuery, item) {
+          var nameValue = item.split(/=/);
+          if (nameValue.length === 2) {
+            parsedQuery[nameValue[0]] = nameValue[1];
+          }
+
+          return parsedQuery;
+      }, {});
+    },
+
+    _iterate: function(offset, opts) {
+      var options = _.defaults((opts || {}), {fetch: true});
+
+      this.paging.params = this.calculateParams(this.paging.params, offset, this.paging.pageSize);
+
+      // Fetch the next page of documents
+      this.updateUrlQuery(this.paging.params);
+      if (options.fetch) {
+        return this.fetch({reset: true});
+      }
+    },
+
+    // `next` is called with the number of items for the next page.
+    // It returns the fetch promise.
+    next: function(options){
+      this.paging.direction = "next";
+      return this._iterate(this.paging.pageSize, options);
+    },
+
+    // `previous` is called with the number of items for the previous page.
+    // It returns the fetch promise.
+    previous: function(options){
+      this.paging.direction = "previous";
+      return this._iterate(0 - this.paging.pageSize, options);
+    },
+
+    shouldStringify: function (val) {
+      try {
+        JSON.parse(val);
+        return false;
+      } catch(e) {
+        return true;
+      }
+    },
+
+    // Encodes the parameters so that couchdb will understand them
+    // and then sets the url with the new url.
+    updateUrlQuery: function (params) {
+      var url = _.result(this, "url").split("?")[0];
+
+      _.each(['startkey', 'endkey', 'key'], function (key) {
+        if (_.has(params, key) && this.shouldStringify(params[key])) {
+          params[key] = JSON.stringify(params[key]);
+        }
+      }, this);
+
+      this.url = url + '?' + $param(params);
+    },
+
+    fetch: function () {
+      // if this is a fetch for the first time, fetch one extra to see if there is a next
+      if (!this.paging.direction && this.paging.params.limit > 0) {
+        this.paging.direction = 'fetch';
+        this.paging.params.limit = this.paging.params.limit + 1;
+        this.updateUrlQuery(this.paging.params);
+      }
+
+      return Backbone.Collection.prototype.fetch.apply(this, arguments);
+    },
+
+    parse: function (resp) {
+      var rows = resp.rows;
+
+      this.paging.hasNext = this.paging.hasPrevious = false;
+
+      this.viewMeta = {
+        total_rows: resp.total_rows,
+        offset: resp.offset,
+        update_seq: resp.update_seq
+      };
+
+      var skipLimit = this.paging.defaultParams.skip || 0;
+      if(this.paging.params.skip > skipLimit) {
+        this.paging.hasPrevious = true;
+      }
+
+      if(rows.length === this.paging.pageSize + 1) {
+        this.paging.hasNext = true;
+
+        // remove the next page marker result
+        rows.pop();
+        this.viewMeta.total_rows = this.viewMeta.total_rows - 1;
+      }
+      return rows;
+    },
+
+    hasNext: function() {
+      return this.paging.hasNext;
+    },
+
+    hasPrevious: function() {
+      return this.paging.hasPrevious;
+    }
+  });
+
+
+  if (exports) {
+    // Overload the Backbone.ajax method, this allows PagingCollection to be able to
+    // work in node.js
+    exports.setAjax = function (ajax) {
+      Backbone.ajax = ajax;
+    };
+
+    exports.PagingCollection = PagingCollection;
+  }
+
+  return PagingCollection;
+}));
+


Mime
View raw message