updated backgrid

This commit is contained in:
kay.one 2013-06-20 22:37:37 -07:00
parent 467c88f711
commit 8fd7def9dd
4 changed files with 543 additions and 265 deletions

View File

@ -6,10 +6,12 @@
Licensed under the MIT @license.
*/
(function ($, _, Backbone, Backgrid, lunr) {
(function (root) {
"use strict";
var Backbone = root.Backbone, Backgrid = root.Backgrid, lunr = root.lunr;
/**
ServerSideFilter is a search form widget that submits a query to the server
for filtering the current collection.
@ -36,14 +38,17 @@
/** @property {string} [name='q'] Query key */
name: "q",
/** @property The HTML5 placeholder to appear beneath the search box. */
/**
@property {string} [placeholder] The HTML5 placeholder to appear beneath
the search box.
*/
placeholder: null,
/**
@param {Object} options
@param {Backbone.Collection} options.collection
@param {String} [options.name]
@param {String} [options.placeholder]
@param {string} [options.name]
@param {string} [options.placeholder]
*/
initialize: function (options) {
Backgrid.requireOptions(options, ["collection"]);
@ -51,16 +56,21 @@
this.name = options.name || this.name;
this.placeholder = options.placeholder || this.placeholder;
// Persist the query on pagination
var collection = this.collection, self = this;
if (Backbone.PageableCollection &&
collection instanceof Backbone.PageableCollection &&
collection.mode == "server") {
collection.queryParams[this.name] = function () {
return self.$el.find("input[type=text]").val();
return self.searchBox().val() || null;
};
}
},
searchBox: function () {
return this.$el.find("input[type=text]");
},
/**
Upon search form submission, this event handler constructs a query
parameter object and pass it to Collection#fetch for server-side
@ -68,9 +78,22 @@
*/
search: function (e) {
if (e) e.preventDefault();
var data = {};
data[this.name] = this.$el.find("input[type=text]").val();
this.collection.fetch({data: data});
// go back to the first page on search
var collection = this.collection;
if (Backbone.PageableCollection &&
collection instanceof Backbone.PageableCollection &&
collection.mode == "server") {
collection.state.currentPage = 1;
}
else {
var query = this.searchBox().val();
if (query) data[this.name] = query;
}
collection.fetch({data: data, reset: true});
},
/**
@ -79,8 +102,8 @@
*/
clear: function (e) {
if (e) e.preventDefault();
this.$("input[type=text]").val(null);
this.collection.fetch();
this.searchBox().val(null);
this.collection.fetch({reset: true});
},
/**
@ -115,8 +138,7 @@
e.preventDefault();
this.clear();
},
"change input[type=text]": "search",
"keyup input[type=text]": "search",
"keydown input[type=text]": "search",
"submit": function (e) {
e.preventDefault();
this.search();
@ -124,14 +146,14 @@
},
/**
@property {?Array.<string>} A list of model field names to search
for matches. If null, all of the fields will be searched.
@property {?Array.<string>} [fields] A list of model field names to
search for matches. If null, all of the fields will be searched.
*/
fields: null,
/**
@property wait The time in milliseconds to wait since for since the last
change to the search box's value before searching. This value can be
@property [wait=149] The time in milliseconds to wait since for since the
last change to the search box's value before searching. This value can be
adjusted depending on how often the search box is used and how large the
search index is.
*/
@ -143,9 +165,9 @@
@param {Object} options
@param {Backbone.Collection} options.collection
@param {String} [options.placeholder]
@param {String} [options.fields]
@param {String} [options.wait=149]
@param {string} [options.placeholder]
@param {string} [options.fields]
@param {string} [options.wait=149]
*/
initialize: function (options) {
ServerSideFilter.prototype.initialize.apply(this, arguments);
@ -155,11 +177,8 @@
this._debounceMethods(["search", "clear"]);
var collection = this.collection;
var collection = this.collection = this.collection.fullCollection || this.collection;
var shadowCollection = this.shadowCollection = collection.clone();
shadowCollection.url = collection.url;
shadowCollection.sync = collection.sync;
shadowCollection.parse = collection.parse;
this.listenTo(collection, "add", function (model, collection, options) {
shadowCollection.add(model, options);
@ -167,9 +186,15 @@
this.listenTo(collection, "remove", function (model, collection, options) {
shadowCollection.remove(model, options);
});
this.listenTo(collection, "sort reset", function (collection, options) {
this.listenTo(collection, "sort", function (col) {
if (!this.searchBox().val()) shadowCollection.reset(col.models);
});
this.listenTo(collection, "reset", function (col, options) {
options = _.extend({reindex: true}, options || {});
if (options.reindex) shadowCollection.reset(collection.models);
if (options.reindex && col === collection &&
options.from == null && options.to == null) {
shadowCollection.reset(col.models);
}
});
},
@ -218,7 +243,9 @@
when all the matches have been found.
*/
search: function () {
var matcher = _.bind(this.makeMatcher(this.$("input[type=text]").val()), this);
var matcher = _.bind(this.makeMatcher(this.searchBox().val()), this);
var col = this.collection;
if (col.pageableCollection) col.pageableCollection.getFirstPage({silent: true});
this.collection.reset(this.shadowCollection.filter(matcher), {reindex: false});
},
@ -226,7 +253,7 @@
Clears the search box and reset the collection to its original.
*/
clear: function () {
this.$("input[type=text]").val(null);
this.searchBox().val(null);
this.collection.reset(this.shadowCollection.models, {reindex: false});
}
@ -262,7 +289,7 @@
@param {Object} options
@param {Backbone.Collection} options.collection
@param {String} [options.placeholder]
@param {string} [options.placeholder]
@param {string} [options.ref] lunrjs` document reference attribute name.
@param {Object} [options.fields] A hash of `lunrjs` index field names and
boost value.
@ -273,7 +300,7 @@
this.ref = options.ref || this.ref;
var collection = this.collection;
var collection = this.collection = this.collection.fullCollection || this.collection;
this.listenTo(collection, "add", this.addToIndex);
this.listenTo(collection, "remove", this.removeFromIndex);
this.listenTo(collection, "reset", this.resetIndex);
@ -351,15 +378,17 @@
query answer.
*/
search: function () {
var searchResults = this.index.search(this.$("input[type=text]").val());
var searchResults = this.index.search(this.searchBox().val());
var models = [];
for (var i = 0; i < searchResults.length; i++) {
var result = searchResults[i];
models.push(this.shadowCollection.get(result.ref));
}
this.collection.reset(models, {reindex: false});
var col = this.collection;
if (col.pageableCollection) col.pageableCollection.getFirstPage({silent: true});
col.reset(models, {reindex: false});
}
});
}(jQuery, _, Backbone, Backgrid, lunr));
}(this));

View File

@ -41,10 +41,6 @@ if (!String.prototype.trim || ws.trim()) {
};
}
function capitalize(s) {
return String.fromCharCode(s.charCodeAt(0) - 32) + s.slice(1);
}
function lpad(str, length, padstr) {
var paddingLen = length - (str + '').length;
paddingLen = paddingLen < 0 ? 0 : paddingLen;
@ -72,7 +68,9 @@ var Backgrid = root.Backgrid = {
resolveNameToClass: function (name, suffix) {
if (_.isString(name)) {
var key = _.map(name.split('-'), function (e) { return capitalize(e); }).join('') + suffix;
var key = _.map(name.split('-'), function (e) {
return e.slice(0, 1).toUpperCase() + e.slice(1);
}).join('') + suffix;
var klass = Backgrid[key] || Backgrid.Extension[key];
if (_.isUndefined(klass)) {
throw new ReferenceError("Class '" + key + "' not found");
@ -81,7 +79,17 @@ var Backgrid = root.Backgrid = {
}
return name;
},
callByNeed: function () {
var value = arguments[0];
if (!_.isFunction(value)) return value;
var context = arguments[1];
var args = [].slice.call(arguments, 2);
return value.apply(context, !!(args + '') ? args : void 0);
}
};
_.extend(Backgrid, Backbone.Events);
@ -99,7 +107,7 @@ _.extend(Backgrid, Backbone.Events);
var Command = Backgrid.Command = function (evt) {
_.extend(this, {
altKey: !!evt.altKey,
char: evt.char,
"char": evt["char"],
charCode: evt.charCode,
ctrlKey: !!evt.ctrlKey,
key: evt.key,
@ -737,12 +745,33 @@ var Cell = Backgrid.Cell = Backbone.View.extend({
if (!(this.column instanceof Column)) {
this.column = new Column(this.column);
}
this.formatter = Backgrid.resolveNameToClass(this.column.get("formatter") || this.formatter, "Formatter");
var column = this.column, model = this.model, $el = this.$el;
this.formatter = Backgrid.resolveNameToClass(column.get("formatter") ||
this.formatter, "Formatter");
this.editor = Backgrid.resolveNameToClass(this.editor, "CellEditor");
this.listenTo(this.model, "change:" + this.column.get("name"), function () {
if (!this.$el.hasClass("editor")) this.render();
this.listenTo(model, "change:" + column.get("name"), function () {
if (!$el.hasClass("editor")) this.render();
});
this.listenTo(this.model, "backgrid:error", this.renderError);
this.listenTo(model, "backgrid:error", this.renderError);
this.listenTo(column, "change:editable change:sortable change:renderable",
function (column) {
var changed = column.changedAttributes();
for (var key in changed) {
if (changed.hasOwnProperty(key)) {
$el.toggleClass(key, changed[key]);
}
}
});
if (column.get("editable")) $el.addClass("editable");
if (column.get("sortable")) $el.addClass("sortable");
if (column.get("renderable")) $el.addClass("renderable");
},
/**
@ -779,7 +808,8 @@ var Cell = Backgrid.Cell = Backbone.View.extend({
var model = this.model;
var column = this.column;
if (column.get("editable")) {
var editable = Backgrid.callByNeed(column.get("editable"), column, model);
if (editable) {
this.currentEditor = new this.editor({
column: this.column,
@ -828,7 +858,7 @@ var Cell = Backgrid.Cell = Backbone.View.extend({
*/
remove: function () {
if (this.currentEditor) {
this.currentEditor.remove.apply(this, arguments);
this.currentEditor.remove.apply(this.currentEditor, arguments);
delete this.currentEditor;
}
return Backbone.View.prototype.remove.apply(this, arguments);
@ -1483,6 +1513,7 @@ var Column = Backgrid.Column = Backbone.Model.extend({
editable: true,
renderable: true,
formatter: undefined,
sortValue: undefined,
cell: undefined,
headerCell: undefined
},
@ -1491,22 +1522,36 @@ var Column = Backgrid.Column = Backbone.Model.extend({
Initializes this Column instance.
@param {Object} attrs Column attributes.
@param {string} attrs.name The name of the model attribute.
@param {string|Backgrid.Cell} attrs.cell The cell type.
If this is a string, the capitalized form will be used to look up a
cell class in Backbone, i.e.: string => StringCell. If a Cell subclass
is supplied, it is initialized with a hash of parameters. If a Cell
instance is supplied, it is used directly.
@param {string|Backgrid.HeaderCell} [attrs.headerCell] The header cell type.
@param {string} [attrs.label] The label to show in the header.
@param {boolean} [attrs.sortable=true]
@param {boolean} [attrs.editable=true]
@param {boolean} [attrs.renderable=true]
@param {boolean|string} [attrs.sortable=true]
@param {boolean|string} [attrs.editable=true]
@param {boolean|string} [attrs.renderable=true]
@param {Backgrid.CellFormatter | Object | string} [attrs.formatter] The
formatter to use to convert between raw model values and user input.
@param {(function(Backbone.Model, string): Object) | string} [sortValue] The
function to use to extract a value from the model for comparison during
sorting. If this value is a string, a method with the same name will be
looked up from the column instance.
@throws {TypeError} If attrs.cell or attrs.options are not supplied.
@throws {ReferenceError} If attrs.cell is a string but a cell class of
@throws {ReferenceError} If formatter is a string but a formatter class of
said name cannot be found in the Backgrid module.
See:
@ -1522,8 +1567,32 @@ var Column = Backgrid.Column = Backbone.Model.extend({
}
var headerCell = Backgrid.resolveNameToClass(this.get("headerCell"), "HeaderCell");
var cell = Backgrid.resolveNameToClass(this.get("cell"), "Cell");
this.set({ cell: cell, headerCell: headerCell }, { silent: true });
var sortValue = this.get("sortValue");
if (sortValue == null) sortValue = function (model, colName) {
return model.get(colName);
};
else if (_.isString(sortValue)) sortValue = this[sortValue];
var sortable = this.get("sortable");
if (_.isString(sortable)) sortable = this[sortable];
var editable = this.get("editable");
if (_.isString(editable)) editable = this[editable];
var renderable = this.get("renderable");
if (_.isString(renderable)) renderable = this[renderable];
this.set({
cell: cell,
headerCell: headerCell,
sortable: sortable,
editable: editable,
renderable: renderable,
sortValue: sortValue
}, { silent: true });
}
});
@ -1587,22 +1656,11 @@ var Row = Backgrid.Row = Backbone.View.extend({
cells.push(this.makeCell(columns.at(i), options));
}
this.listenTo(columns, "change:renderable", function (column, renderable) {
for (var i = 0; i < cells.length; i++) {
var cell = cells[i];
if (cell.column.get("name") == column.get("name")) {
if (renderable) cell.$el.show(); else cell.$el.hide();
}
}
});
this.listenTo(columns, "add", function (column, columns) {
var i = columns.indexOf(column);
var cell = this.makeCell(column, options);
cells.splice(i, 0, cell);
if (!cell.column.get("renderable")) cell.$el.hide();
var $el = this.$el;
if (i === 0) {
$el.prepend(cell.render().$el);
@ -1646,11 +1704,8 @@ var Row = Backgrid.Row = Backbone.View.extend({
this.$el.empty();
var fragment = document.createDocumentFragment();
for (var i = 0; i < this.cells.length; i++) {
var cell = this.cells[i];
fragment.appendChild(cell.render().el);
if (!cell.column.get("renderable")) cell.$el.hide();
fragment.appendChild(this.cells[i].render().el);
}
this.el.appendChild(fragment);
@ -1766,7 +1821,24 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({
if (!(this.column instanceof Column)) {
this.column = new Column(this.column);
}
this.listenTo(this.collection, "backgrid:sort", this._resetCellDirection);
var column = this.column, $el = this.$el;
this.listenTo(column, "change:editable change:sortable change:renderable",
function (column) {
var changed = column.changedAttributes();
for (var key in changed) {
if (changed.hasOwnProperty(key)) {
$el.toggleClass(key, changed[key]);
}
}
});
if (column.get("editable")) $el.addClass("editable");
if (column.get("sortable")) $el.addClass("sortable");
if (column.get("renderable")) $el.addClass("renderable");
},
/**
@ -1793,9 +1865,9 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({
@private
*/
_resetCellDirection: function (sortByColName, direction, comparator, collection) {
_resetCellDirection: function (columnToSort, direction, comparator, collection) {
if (collection == this.collection) {
if (sortByColName !== this.column.get("name")) this.direction(null);
if (columnToSort !== this.column) this.direction(null);
else this.direction(direction);
}
},
@ -1808,34 +1880,12 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({
onClick: function (e) {
e.preventDefault();
var columnName = this.column.get("name");
if (this.column.get("sortable")) {
if (this.direction() === "ascending") {
this.sort(columnName, "descending", function (left, right) {
var leftVal = left.get(columnName);
var rightVal = right.get(columnName);
if (leftVal === rightVal) {
return 0;
}
else if (leftVal > rightVal) { return -1; }
return 1;
});
}
else if (this.direction() === "descending") {
this.sort(columnName, null);
}
else {
this.sort(columnName, "ascending", function (left, right) {
var leftVal = left.get(columnName);
var rightVal = right.get(columnName);
if (leftVal === rightVal) {
return 0;
}
else if (leftVal < rightVal) { return -1; }
return 1;
});
}
var column = this.column;
var sortable = Backgrid.callByNeed(column.get("sortable"), column, this.model);
if (sortable) {
if (this.direction() === "ascending") this.sort(column, "descending");
else if (this.direction() === "descending") this.sort(column, null);
else this.sort(column, "ascending");
}
},
@ -1852,31 +1902,37 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({
and the current page will then be returned.
Triggers a Backbone `backgrid:sort` event from the collection when done
with the column name, direction, comparator and a reference to the
collection.
with the column, direction, comparator and a reference to the collection.
@param {string} columnName
@param {Backgrid.Column} column
@param {null|"ascending"|"descending"} direction
@param {function(*, *): number} [comparator]
See [Backbone.Collection#comparator](http://backbonejs.org/#Collection-comparator)
*/
sort: function (columnName, direction, comparator) {
comparator = comparator || this._cidComparator;
sort: function (column, direction) {
var collection = this.collection;
if (Backbone.PageableCollection && collection instanceof Backbone.PageableCollection) {
var order;
if (direction === "ascending") order = -1;
else if (direction === "descending") order = 1;
else order = null;
collection.setSorting(order ? columnName : null, order);
var comparator = this.makeComparator(column.get("name"), order,
order ?
column.get("sortValue") :
function (model) {
return model.cid;
});
if (Backbone.PageableCollection &&
collection instanceof Backbone.PageableCollection) {
collection.setSorting(order && column.get("name"), order,
{sortValue: column.get("sortValue")});
if (collection.mode == "client") {
if (!collection.fullCollection.comparator) {
if (collection.fullCollection.comparator == null) {
collection.fullCollection.comparator = comparator;
}
collection.fullCollection.sort();
@ -1888,26 +1944,24 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({
collection.sort();
}
this.collection.trigger("backgrid:sort", columnName, direction, comparator, this.collection);
this.collection.trigger("backgrid:sort", column, direction, comparator,
this.collection);
},
/**
Default comparator for Backbone.Collections. Sorts cids in ascending
order. The cids of the models are assumed to be in insertion order.
makeComparator: function (attr, order, func) {
@private
@param {*} left
@param {*} right
*/
_cidComparator: function (left, right) {
var lcid = left.cid, rcid = right.cid;
if (!_.isUndefined(lcid) && !_.isUndefined(rcid)) {
lcid = lcid.slice(1) * 1, rcid = rcid.slice(1) * 1;
if (lcid < rcid) return -1;
else if (lcid > rcid) return 1;
}
return function (left, right) {
// extract the values from the models
var l = func(left, attr), r = func(right, attr), t;
return 0;
// if descending order, swap left and right
if (order === 1) t = l, l = r, r = t;
// compare as usual
if (l === r) return 0;
else if (l < r) return -1;
return 1;
};
},
/**
@ -1915,7 +1969,9 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({
*/
render: function () {
this.$el.empty();
var $label = $("<a>").text(this.column.get("label")).append("<b class='sort-caret'></b>");
var $label = $("<a>").text(this.column.get("label"));
var sortable = Backgrid.callByNeed(this.column.get("sortable"), this.column, this.model);
if (sortable) $label.append("<b class='sort-caret'></b>");
this.$el.append($label);
this.delegateEvents();
return this;
@ -2259,6 +2315,9 @@ var Body = Backgrid.Body = Backbone.View.extend({
moveToNextCell: function (model, column, command) {
var i = this.collection.indexOf(model);
var j = this.columns.indexOf(column);
var cell, renderable, editable;
this.rows[i].cells[j].exitEditMode();
if (command.moveUp() || command.moveDown() || command.moveLeft() ||
command.moveRight() || command.save()) {
@ -2267,7 +2326,12 @@ var Body = Backgrid.Body = Backbone.View.extend({
if (command.moveUp() || command.moveDown()) {
var row = this.rows[i + (command.moveUp() ? -1 : 1)];
if (row) row.cells[j].enterEditMode();
if (row) {
cell = row.cells[j];
if (Backgrid.callByNeed(cell.column.get("editable"), cell.column, model)) {
cell.enterEditMode();
}
}
}
else if (command.moveLeft() || command.moveRight()) {
var right = command.moveRight();
@ -2276,16 +2340,16 @@ var Body = Backgrid.Body = Backbone.View.extend({
right ? offset++ : offset--) {
var m = ~~(offset / l);
var n = offset - m * l;
var cell = this.rows[m].cells[n];
if (cell.column.get("renderable") && cell.column.get("editable")) {
cell = this.rows[m].cells[n];
renderable = Backgrid.callByNeed(cell.column.get("renderable"), cell.column, cell.model);
editable = Backgrid.callByNeed(cell.column.get("editable"), cell.column, model);
if (renderable && editable) {
cell.enterEditMode();
break;
}
}
}
}
this.rows[i].cells[j].exitEditMode();
}
});
/*

View File

@ -6,18 +6,186 @@
Licensed under the MIT @license.
*/
(function ($, _, Backbone, Backgrid) {
(function (_, Backbone, Backgrid) {
"use strict";
/**
PageHandle is a class that renders the actual page handles and reacts to
click events for pagination.
This class acts in two modes - control or discrete page handle modes. If
one of the `is*` flags is `true`, an instance of this class is under
control page handle mode. Setting a `pageIndex` to an instance of this
class under control mode has no effect and the correct page index will
always be inferred from the `is*` flag. Only one of the `is*` flags should
be set to `true` at a time. For example, an instance of this class cannot
simultaneously be a rewind control and a fast forward control. A `label`
and a `title` template or a string are required to be passed to the
constuctor under this mode. If a `title` template is provided, it __MUST__
accept a parameter `label`. When the `label` is provided to the `title`
template function, its result will be used to render the generated anchor's
title attribute.
If all of the `is*` flags is set to `false`, which is the default, an
instance of this class will be in discrete page handle mode. An instance
under this mode requires the `pageIndex` to be passed from the constructor
as an option and it __MUST__ be a 0-based index of the list of page numbers
to render. The constuctor will normalize the base to the same base the
underlying PageableCollection collection instance uses. A `label` is not
required under this mode, which will default to the equivalent 1-based page
index calculated from `pageIndex` and the underlying PageableCollection
instance. A provided `label` will still be honored however. The `title`
parameter is also not required under this mode, in which case the default
`title` template will be used. You are encouraged to provide your own
`title` template however if you wish to localize the title strings.
If this page handle represents the current page, an `active` class will be
placed on the root list element.
if this page handle is at the border of the list of pages, a `disabled`
class will be placed on the root list element.
Only page handles that are neither `active` nor `disabled` will respond to
click events and triggers pagination.
@class Backgrid.Extension.PageHandle
*/
var PageHandle = Backgrid.Extension.PageHandle = Backbone.View.extend({
/** @property */
tagName: "li",
/** @property */
events: {
"click a": "changePage"
},
/**
@property {string|function(Object.<string, string>): string} title
The title to use for the `title` attribute of the generated page handle
anchor elements. It can be a string or an Underscore template function
that takes a mandatory `label` parameter.
*/
title: _.template('Page <%- label %>'),
/**
@property {boolean} isRewind Whether this handle represents a rewind
control
*/
isRewind: false,
/**
@property {boolean} isBack Whether this handle represents a back
control
*/
isBack: false,
/**
@property {boolean} isForward Whether this handle represents a forward
control
*/
isForward: false,
/**
@property {boolean} isFastForward Whether this handle represents a fast
forward control
*/
isFastForward: false,
/**
Initializer.
@param {Object} options
@param {Backbone.Collection} options.collection
@param {number} pageIndex 0-based index of the page number this handle
handles. This parameter will be normalized to the base the underlying
PageableCollection uses.
@param {string} [options.label] If provided it is used to render the
anchor text, otherwise the normalized pageIndex will be used
instead. Required if any of the `is*` flags is set to `true`.
@param {string} [options.title]
@param {boolean} [options.isRewind=false]
@param {boolean} [options.isBack=false]
@param {boolean} [options.isForward=false]
@param {boolean} [options.isFastForward=false]
*/
initialize: function (options) {
Backbone.View.prototype.initialize.apply(this, arguments);
var collection = this.collection;
var state = collection.state;
var currentPage = state.currentPage;
var firstPage = state.firstPage;
var lastPage = state.lastPage;
_.extend(this, _.pick(options,
["isRewind", "isBack", "isForward", "isFastForward"]));
var pageIndex;
if (this.isRewind) pageIndex = firstPage;
else if (this.isBack) pageIndex = Math.max(firstPage, currentPage - 1);
else if (this.isForward) pageIndex = Math.min(lastPage, currentPage + 1);
else if (this.isFastForward) pageIndex = lastPage;
else {
pageIndex = +options.pageIndex;
pageIndex = (firstPage ? pageIndex + 1 : pageIndex);
}
this.pageIndex = pageIndex;
if (((this.isRewind || this.isBack) && currentPage == firstPage) ||
((this.isForward || this.isFastForward) && currentPage == lastPage)) {
this.$el.addClass("disabled");
}
else if (!(this.isRewind ||
this.isBack ||
this.isForward ||
this.isFastForward) &&
currentPage == pageIndex) {
this.$el.addClass("active");
}
this.label = (options.label || (firstPage ? pageIndex : pageIndex + 1)) + '';
var title = options.title || this.title;
this.title = _.isFunction(title) ? title({label: this.label}) : title;
},
/**
Renders a clickable anchor element under a list item.
*/
render: function () {
this.$el.empty();
var anchor = document.createElement("a");
anchor.href = '#';
if (this.title) anchor.title = this.title;
anchor.innerHTML = this.label;
this.el.appendChild(anchor);
this.delegateEvents();
return this;
},
/**
jQuery click event handler. Goes to the page this PageHandle instance
represents. No-op if this page handle is currently active or disabled.
*/
changePage: function (e) {
e.preventDefault();
var $el = this.$el;
if (!$el.hasClass("active") && !$el.hasClass("disabled")) {
this.collection.getPage(this.pageIndex);
}
return this;
}
});
/**
Paginator is a Backgrid extension that renders a series of configurable
pagination handles. This extension is best used for splitting a large data
set across multiple pages. If the number of pages is larger then a
threshold, which is set to 10 by default, the page handles are rendered
within a sliding window, plus the fast forward, fast backward, previous and
next page handles. The fast forward, fast backward, previous and next page
handles can be turned off.
within a sliding window, plus the rewind, back, forward and fast forward
control handles. The individual control handles can be turned off.
@class Backgrid.Extension.Paginator
*/
@ -30,97 +198,65 @@
windowSize: 10,
/**
@property {Object} fastForwardHandleLabels You can disable specific
handles by setting its value to `null`.
@property {Object.<string, Object.<string, string>>} controls You can
disable specific control handles by omitting certain keys.
*/
fastForwardHandleLabels: {
first: "《",
prev: "〈",
next: "〉",
last: "》"
controls: {
rewind: {
label: "《",
title: "First"
},
back: {
label: "〈",
title: "Previous"
},
forward: {
label: "〉",
title: "Next"
},
fastForward: {
label: "》",
title: "Last"
}
},
/** @property */
template: _.template('<ul><% _.each(handles, function (handle) { %><li <% if (handle.className) { %>class="<%= handle.className %>"<% } %>><a href="#" <% if (handle.title) {%> title="<%= handle.title %>"<% } %>><%= handle.label %></a></li><% }); %></ul>'),
/**
@property {Backgrid.Extension.PageHandle} pageHandle. The PageHandle
class to use for rendering individual handles
*/
pageHandle: PageHandle,
/** @property */
events: {
"click a": "changePage"
},
goBackFirstOnSort: true,
/**
Initializer.
@param {Object} options
@param {Backbone.Collection} options.collection
@param {boolean} [options.fastForwardHandleLabels] Whether to render fast forward buttons.
@param {boolean} [options.controls]
@param {boolean} [options.pageHandle=Backgrid.Extension.PageHandle]
@param {boolean} [options.goBackFirstOnSort=true]
*/
initialize: function (options) {
Backgrid.requireOptions(options, ["collection"]);
this.controls = options.controls || this.controls;
this.pageHandle = options.pageHandle || this.pageHandle;
var collection = this.collection;
var fullCollection = collection.fullCollection;
if (fullCollection) {
this.listenTo(fullCollection, "add", this.render);
this.listenTo(fullCollection, "remove", this.render);
this.listenTo(fullCollection, "reset", this.render);
}
else {
this.listenTo(collection, "add", this.render);
this.listenTo(collection, "remove", this.render);
this.listenTo(collection, "reset", this.render);
}
},
/**
jQuery event handler for the page handlers. Goes to the right page upon
clicking.
@param {Event} e
*/
changePage: function (e) {
e.preventDefault();
var $li = $(e.target).parent();
if (!$li.hasClass("active") && !$li.hasClass("disabled")) {
var label = $(e.target).text();
var ffLabels = this.fastForwardHandleLabels;
var collection = this.collection;
if (ffLabels) {
switch (label) {
case ffLabels.first:
if ((options.goBackFirstOnSort || this.goBackFirstOnSort) &&
collection.fullCollection) {
this.listenTo(collection.fullCollection, "sort", function () {
collection.getFirstPage();
return;
case ffLabels.prev:
collection.getPreviousPage();
return;
case ffLabels.next:
collection.getNextPage();
return;
case ffLabels.last:
collection.getLastPage();
return;
}
}
var state = collection.state;
var pageIndex = +label;
collection.getPage(state.firstPage === 0 ? pageIndex - 1 : pageIndex);
});
}
},
/**
Internal method to create a list of page handle objects for the template
to render them.
@return {Array.<Object>} an array of page handle objects hashes
*/
makeHandles: function () {
var handles = [];
_calculateWindow: function () {
var collection = this.collection;
var state = collection.state;
@ -132,48 +268,44 @@
currentPage = firstPage ? currentPage - 1 : currentPage;
var windowStart = Math.floor(currentPage / this.windowSize) * this.windowSize;
var windowEnd = Math.min(lastPage + 1, windowStart + this.windowSize);
return [windowStart, windowEnd];
},
if (collection.mode !== "infinite") {
for (var i = windowStart; i < windowEnd; i++) {
handles.push({
label: i + 1,
title: "No. " + (i + 1),
className: currentPage === i ? "active" : undefined
/**
Creates a list of page handle objects for rendering.
@return {Array.<Object>} an array of page handle objects hashes
*/
makeHandles: function () {
var handles = [];
var collection = this.collection;
var window = this._calculateWindow();
var winStart = window[0], winEnd = window[1];
for (var i = winStart; i < winEnd; i++) {
handles.push(new PageHandle({
collection: collection,
pageIndex: i
}));
}
var controls = this.controls;
_.each(["back", "rewind", "forward", "fastForward"], function (key) {
var value = controls[key];
if (value) {
var handleCtorOpts = {
collection: collection,
title: value.title,
label: value.label
};
handleCtorOpts["is" + key.slice(0, 1).toUpperCase() + key.slice(1)] = true;
var handle = new PageHandle(handleCtorOpts);
if (key == "rewind" || key == "back") handles.unshift(handle);
else handles.push(handle);
}
});
}
}
var ffLabels = this.fastForwardHandleLabels;
if (ffLabels) {
if (ffLabels.prev) {
handles.unshift({
label: ffLabels.prev,
className: collection.hasPrevious() ? void 0 : "disabled"
});
}
if (ffLabels.first) {
handles.unshift({
label: ffLabels.first,
className: collection.hasPrevious() ? void 0 : "disabled"
});
}
if (ffLabels.next) {
handles.push({
label: ffLabels.next,
className: collection.hasNext() ? void 0 : "disabled"
});
}
if (ffLabels.last) {
handles.push({
label: ffLabels.last,
className: collection.hasNext() ? void 0 : "disabled"
});
}
}
return handles;
},
@ -184,15 +316,24 @@
render: function () {
this.$el.empty();
this.$el.append(this.template({
handles: this.makeHandles()
}));
if (this.handles) {
for (var i = 0, l = this.handles.length; i < l; i++) {
this.handles[i].remove();
}
}
this.delegateEvents();
var handles = this.handles = this.makeHandles();
var ul = document.createElement("ul");
for (var i = 0; i < handles.length; i++) {
ul.appendChild(handles[i].render().el);
}
this.el.appendChild(ul);
return this;
}
});
}(jQuery, _, Backbone, Backgrid));
}(_, Backbone, Backgrid));

View File

@ -1,5 +1,5 @@
/*
backbone-pageable 1.3.0
backbone-pageable 1.3.1
http://github.com/wyuenho/backbone-pageable
Copyright (c) 2013 Jimmy Yuen Ho Wong
@ -574,6 +574,17 @@
/**
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.
@ -586,6 +597,8 @@
@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.
@ -598,14 +611,24 @@
setPageSize: function (pageSize, options) {
pageSize = finiteInt(pageSize, "pageSize");
options = options || {};
options = options || {first: false};
this.state = this._checkState(_extend({}, this.state, {
var state = this.state;
var totalPages = ceil(state.totalRecords / pageSize);
var currentPage = max(state.firstPage,
floor(totalPages *
(state.firstPage ?
state.currentPage :
state.currentPage + 1) /
state.totalPages));
state = this.state = this._checkState(_extend({}, state, {
pageSize: pageSize,
totalPages: ceil(this.state.totalRecords / pageSize)
currentPage: options.first ? state.firstPage : currentPage,
totalPages: totalPages
}));
return this.getPage(this.state.currentPage, options);
return this.getPage(state.currentPage, _omit(options, ["first"]));
},
/**
@ -992,13 +1015,14 @@
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) {
var newState = this.parseState(resp, _clone(this.queryParams), _clone(this.state));
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);
return this.parseRecords(resp, options);
},
/**
@ -1016,10 +1040,16 @@
`totalRecords` value is enough to trigger a full pagination state
recalculation.
parseState: function (resp, queryParams, state) {
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.
@ -1027,10 +1057,12 @@
@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) {
parseState: function (resp, queryParams, state, options) {
if (resp && resp.length === 2 && _isObject(resp[0]) && _isArray(resp[1])) {
var newState = _clone(state);
@ -1059,10 +1091,12 @@
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) {
parseRecords: function (resp, options) {
if (resp && resp.length === 2 && _isObject(resp[0]) && _isArray(resp[1])) {
return resp[1];
}
@ -1138,7 +1172,7 @@
kvp = extraKvps[i];
v = kvp[1];
v = _isFunction(v) ? v.call(thisCopy) : v;
data[kvp[0]] = v;
if (v != null) data[kvp[0]] = v;
}
var fullCol = this.fullCollection, links = this.links;
@ -1212,11 +1246,11 @@
@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) {
_makeComparator: function (sortKey, order, sortValue) {
var state = this.state;
sortKey = sortKey || state.sortKey;
@ -1224,8 +1258,12 @@
if (!sortKey || !order) return;
if (!sortValue) sortValue = function (model, attr) {
return model.get(attr);
};
return function (left, right) {
var l = left.get(sortKey), r = right.get(sortKey), t;
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;
@ -1244,6 +1282,11 @@
`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`.
@ -1252,6 +1295,7 @@
@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) {
@ -1270,7 +1314,7 @@
options = _extend({side: mode == "client" ? mode : "server", full: true},
options);
var comparator = this._makeComparator(sortKey, order);
var comparator = this._makeComparator(sortKey, order, options.sortValue);
var full = options.full, side = options.side;