blob: e383489226a06838e950ffb33e43b5e9b2aa893b [file] [log] [blame]
// 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;
}));