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. Licensed under the MIT @license.
*/ */
(function ($, _, Backbone, Backgrid, lunr) { (function (root) {
"use strict"; "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 ServerSideFilter is a search form widget that submits a query to the server
for filtering the current collection. for filtering the current collection.
@ -36,14 +38,17 @@
/** @property {string} [name='q'] Query key */ /** @property {string} [name='q'] Query key */
name: "q", 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, placeholder: null,
/** /**
@param {Object} options @param {Object} options
@param {Backbone.Collection} options.collection @param {Backbone.Collection} options.collection
@param {String} [options.name] @param {string} [options.name]
@param {String} [options.placeholder] @param {string} [options.placeholder]
*/ */
initialize: function (options) { initialize: function (options) {
Backgrid.requireOptions(options, ["collection"]); Backgrid.requireOptions(options, ["collection"]);
@ -51,16 +56,21 @@
this.name = options.name || this.name; this.name = options.name || this.name;
this.placeholder = options.placeholder || this.placeholder; this.placeholder = options.placeholder || this.placeholder;
// Persist the query on pagination
var collection = this.collection, self = this; var collection = this.collection, self = this;
if (Backbone.PageableCollection && if (Backbone.PageableCollection &&
collection instanceof Backbone.PageableCollection && collection instanceof Backbone.PageableCollection &&
collection.mode == "server") { collection.mode == "server") {
collection.queryParams[this.name] = function () { 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 Upon search form submission, this event handler constructs a query
parameter object and pass it to Collection#fetch for server-side parameter object and pass it to Collection#fetch for server-side
@ -68,9 +78,22 @@
*/ */
search: function (e) { search: function (e) {
if (e) e.preventDefault(); if (e) e.preventDefault();
var data = {}; 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) { clear: function (e) {
if (e) e.preventDefault(); if (e) e.preventDefault();
this.$("input[type=text]").val(null); this.searchBox().val(null);
this.collection.fetch(); this.collection.fetch({reset: true});
}, },
/** /**
@ -115,8 +138,7 @@
e.preventDefault(); e.preventDefault();
this.clear(); this.clear();
}, },
"change input[type=text]": "search", "keydown input[type=text]": "search",
"keyup input[type=text]": "search",
"submit": function (e) { "submit": function (e) {
e.preventDefault(); e.preventDefault();
this.search(); this.search();
@ -124,14 +146,14 @@
}, },
/** /**
@property {?Array.<string>} A list of model field names to search @property {?Array.<string>} [fields] A list of model field names to
for matches. If null, all of the fields will be searched. search for matches. If null, all of the fields will be searched.
*/ */
fields: null, fields: null,
/** /**
@property wait The time in milliseconds to wait since for since the last @property [wait=149] The time in milliseconds to wait since for since the
change to the search box's value before searching. This value can be 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 adjusted depending on how often the search box is used and how large the
search index is. search index is.
*/ */
@ -143,9 +165,9 @@
@param {Object} options @param {Object} options
@param {Backbone.Collection} options.collection @param {Backbone.Collection} options.collection
@param {String} [options.placeholder] @param {string} [options.placeholder]
@param {String} [options.fields] @param {string} [options.fields]
@param {String} [options.wait=149] @param {string} [options.wait=149]
*/ */
initialize: function (options) { initialize: function (options) {
ServerSideFilter.prototype.initialize.apply(this, arguments); ServerSideFilter.prototype.initialize.apply(this, arguments);
@ -155,11 +177,8 @@
this._debounceMethods(["search", "clear"]); this._debounceMethods(["search", "clear"]);
var collection = this.collection; var collection = this.collection = this.collection.fullCollection || this.collection;
var shadowCollection = this.shadowCollection = collection.clone(); 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) { this.listenTo(collection, "add", function (model, collection, options) {
shadowCollection.add(model, options); shadowCollection.add(model, options);
@ -167,9 +186,15 @@
this.listenTo(collection, "remove", function (model, collection, options) { this.listenTo(collection, "remove", function (model, collection, options) {
shadowCollection.remove(model, 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 || {}); 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. when all the matches have been found.
*/ */
search: function () { 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}); this.collection.reset(this.shadowCollection.filter(matcher), {reindex: false});
}, },
@ -226,7 +253,7 @@
Clears the search box and reset the collection to its original. Clears the search box and reset the collection to its original.
*/ */
clear: function () { clear: function () {
this.$("input[type=text]").val(null); this.searchBox().val(null);
this.collection.reset(this.shadowCollection.models, {reindex: false}); this.collection.reset(this.shadowCollection.models, {reindex: false});
} }
@ -262,7 +289,7 @@
@param {Object} options @param {Object} options
@param {Backbone.Collection} options.collection @param {Backbone.Collection} options.collection
@param {String} [options.placeholder] @param {string} [options.placeholder]
@param {string} [options.ref] lunrjs` document reference attribute name. @param {string} [options.ref] lunrjs` document reference attribute name.
@param {Object} [options.fields] A hash of `lunrjs` index field names and @param {Object} [options.fields] A hash of `lunrjs` index field names and
boost value. boost value.
@ -273,7 +300,7 @@
this.ref = options.ref || this.ref; 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, "add", this.addToIndex);
this.listenTo(collection, "remove", this.removeFromIndex); this.listenTo(collection, "remove", this.removeFromIndex);
this.listenTo(collection, "reset", this.resetIndex); this.listenTo(collection, "reset", this.resetIndex);
@ -351,15 +378,17 @@
query answer. query answer.
*/ */
search: function () { search: function () {
var searchResults = this.index.search(this.$("input[type=text]").val()); var searchResults = this.index.search(this.searchBox().val());
var models = []; var models = [];
for (var i = 0; i < searchResults.length; i++) { for (var i = 0; i < searchResults.length; i++) {
var result = searchResults[i]; var result = searchResults[i];
models.push(this.shadowCollection.get(result.ref)); 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) { function lpad(str, length, padstr) {
var paddingLen = length - (str + '').length; var paddingLen = length - (str + '').length;
paddingLen = paddingLen < 0 ? 0 : paddingLen; paddingLen = paddingLen < 0 ? 0 : paddingLen;
@ -72,7 +68,9 @@ var Backgrid = root.Backgrid = {
resolveNameToClass: function (name, suffix) { resolveNameToClass: function (name, suffix) {
if (_.isString(name)) { 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]; var klass = Backgrid[key] || Backgrid.Extension[key];
if (_.isUndefined(klass)) { if (_.isUndefined(klass)) {
throw new ReferenceError("Class '" + key + "' not found"); throw new ReferenceError("Class '" + key + "' not found");
@ -81,7 +79,17 @@ var Backgrid = root.Backgrid = {
} }
return name; 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); _.extend(Backgrid, Backbone.Events);
@ -99,7 +107,7 @@ _.extend(Backgrid, Backbone.Events);
var Command = Backgrid.Command = function (evt) { var Command = Backgrid.Command = function (evt) {
_.extend(this, { _.extend(this, {
altKey: !!evt.altKey, altKey: !!evt.altKey,
char: evt.char, "char": evt["char"],
charCode: evt.charCode, charCode: evt.charCode,
ctrlKey: !!evt.ctrlKey, ctrlKey: !!evt.ctrlKey,
key: evt.key, key: evt.key,
@ -737,12 +745,33 @@ var Cell = Backgrid.Cell = Backbone.View.extend({
if (!(this.column instanceof Column)) { if (!(this.column instanceof Column)) {
this.column = new Column(this.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.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 model = this.model;
var column = this.column; var column = this.column;
if (column.get("editable")) { var editable = Backgrid.callByNeed(column.get("editable"), column, model);
if (editable) {
this.currentEditor = new this.editor({ this.currentEditor = new this.editor({
column: this.column, column: this.column,
@ -828,7 +858,7 @@ var Cell = Backgrid.Cell = Backbone.View.extend({
*/ */
remove: function () { remove: function () {
if (this.currentEditor) { if (this.currentEditor) {
this.currentEditor.remove.apply(this, arguments); this.currentEditor.remove.apply(this.currentEditor, arguments);
delete this.currentEditor; delete this.currentEditor;
} }
return Backbone.View.prototype.remove.apply(this, arguments); return Backbone.View.prototype.remove.apply(this, arguments);
@ -1483,6 +1513,7 @@ var Column = Backgrid.Column = Backbone.Model.extend({
editable: true, editable: true,
renderable: true, renderable: true,
formatter: undefined, formatter: undefined,
sortValue: undefined,
cell: undefined, cell: undefined,
headerCell: undefined headerCell: undefined
}, },
@ -1491,22 +1522,36 @@ var Column = Backgrid.Column = Backbone.Model.extend({
Initializes this Column instance. Initializes this Column instance.
@param {Object} attrs Column attributes. @param {Object} attrs Column attributes.
@param {string} attrs.name The name of the model attribute. @param {string} attrs.name The name of the model attribute.
@param {string|Backgrid.Cell} attrs.cell The cell type. @param {string|Backgrid.Cell} attrs.cell The cell type.
If this is a string, the capitalized form will be used to look up a 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 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 is supplied, it is initialized with a hash of parameters. If a Cell
instance is supplied, it is used directly. instance is supplied, it is used directly.
@param {string|Backgrid.HeaderCell} [attrs.headerCell] The header cell type. @param {string|Backgrid.HeaderCell} [attrs.headerCell] The header cell type.
@param {string} [attrs.label] The label to show in the header. @param {string} [attrs.label] The label to show in the header.
@param {boolean} [attrs.sortable=true]
@param {boolean} [attrs.editable=true] @param {boolean|string} [attrs.sortable=true]
@param {boolean} [attrs.renderable=true]
@param {Backgrid.CellFormatter|Object|string} [attrs.formatter] The @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. 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 {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. said name cannot be found in the Backgrid module.
See: See:
@ -1522,8 +1567,32 @@ var Column = Backgrid.Column = Backbone.Model.extend({
} }
var headerCell = Backgrid.resolveNameToClass(this.get("headerCell"), "HeaderCell"); var headerCell = Backgrid.resolveNameToClass(this.get("headerCell"), "HeaderCell");
var cell = Backgrid.resolveNameToClass(this.get("cell"), "Cell"); 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)); 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) { this.listenTo(columns, "add", function (column, columns) {
var i = columns.indexOf(column); var i = columns.indexOf(column);
var cell = this.makeCell(column, options); var cell = this.makeCell(column, options);
cells.splice(i, 0, cell); cells.splice(i, 0, cell);
if (!cell.column.get("renderable")) cell.$el.hide();
var $el = this.$el; var $el = this.$el;
if (i === 0) { if (i === 0) {
$el.prepend(cell.render().$el); $el.prepend(cell.render().$el);
@ -1646,11 +1704,8 @@ var Row = Backgrid.Row = Backbone.View.extend({
this.$el.empty(); this.$el.empty();
var fragment = document.createDocumentFragment(); var fragment = document.createDocumentFragment();
for (var i = 0; i < this.cells.length; i++) { for (var i = 0; i < this.cells.length; i++) {
var cell = this.cells[i]; fragment.appendChild(this.cells[i].render().el);
fragment.appendChild(cell.render().el);
if (!cell.column.get("renderable")) cell.$el.hide();
} }
this.el.appendChild(fragment); this.el.appendChild(fragment);
@ -1766,7 +1821,24 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({
if (!(this.column instanceof Column)) { if (!(this.column instanceof Column)) {
this.column = new Column(this.column); this.column = new Column(this.column);
} }
this.listenTo(this.collection, "backgrid:sort", this._resetCellDirection); 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 @private
*/ */
_resetCellDirection: function (sortByColName, direction, comparator, collection) { _resetCellDirection: function (columnToSort, direction, comparator, collection) {
if (collection == this.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); else this.direction(direction);
} }
}, },
@ -1808,34 +1880,12 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({
onClick: function (e) { onClick: function (e) {
e.preventDefault(); e.preventDefault();
var columnName = this.column.get("name"); var column = this.column;
var sortable = Backgrid.callByNeed(column.get("sortable"), column, this.model);
if (this.column.get("sortable")) { if (sortable) {
if (this.direction() === "ascending") { if (this.direction() === "ascending") this.sort(column, "descending");
this.sort(columnName, "descending", function (left, right) { else if (this.direction() === "descending") this.sort(column, null);
var leftVal = left.get(columnName); else this.sort(column, "ascending");
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;
});
}
} }
}, },
@ -1852,31 +1902,37 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({
and the current page will then be returned. and the current page will then be returned.
Triggers a Backbone `backgrid:sort` event from the collection when done Triggers a Backbone `backgrid:sort` event from the collection when done
with the column name, direction, comparator and a reference to the with the column, direction, comparator and a reference to the collection.
collection.
@param {string} columnName @param {Backgrid.Column} column
@param {null|"ascending"|"descending"} direction @param {null|"ascending"|"descending"} direction
@param {function(*, *): number} [comparator]
See [Backbone.Collection#comparator](http://backbonejs.org/#Collection-comparator) See [Backbone.Collection#comparator](http://backbonejs.org/#Collection-comparator)
*/ */
sort: function (columnName, direction, comparator) { sort: function (column, direction) {
comparator = comparator || this._cidComparator;
var collection = this.collection; var collection = this.collection;
if (Backbone.PageableCollection && collection instanceof Backbone.PageableCollection) { var order;
var order; if (direction === "ascending") order = -1;
if (direction === "ascending") order = -1; else if (direction === "descending") order = 1;
else if (direction === "descending") order = 1; else order = null;
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.mode == "client") {
if (!collection.fullCollection.comparator) { if (collection.fullCollection.comparator == null) {
collection.fullCollection.comparator = comparator; collection.fullCollection.comparator = comparator;
} }
collection.fullCollection.sort(); collection.fullCollection.sort();
@ -1888,26 +1944,24 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({
collection.sort(); collection.sort();
} }
this.collection.trigger("backgrid:sort", columnName, direction, comparator, this.collection); this.collection.trigger("backgrid:sort", column, direction, comparator,
this.collection);
}, },
/** makeComparator: function (attr, order, func) {
Default comparator for Backbone.Collections. Sorts cids in ascending
order. The cids of the models are assumed to be in insertion order.
@private return function (left, right) {
@param {*} left // extract the values from the models
@param {*} right var l = func(left, attr), r = func(right, attr), t;
*/
_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 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 () { render: function () {
this.$el.empty(); 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.$el.append($label);
this.delegateEvents(); this.delegateEvents();
return this; return this;
@ -2259,6 +2315,9 @@ var Body = Backgrid.Body = Backbone.View.extend({
moveToNextCell: function (model, column, command) { moveToNextCell: function (model, column, command) {
var i = this.collection.indexOf(model); var i = this.collection.indexOf(model);
var j = this.columns.indexOf(column); var j = this.columns.indexOf(column);
var cell, renderable, editable;
this.rows[i].cells[j].exitEditMode();
if (command.moveUp() || command.moveDown() || command.moveLeft() || if (command.moveUp() || command.moveDown() || command.moveLeft() ||
command.moveRight() || command.save()) { command.moveRight() || command.save()) {
@ -2267,7 +2326,12 @@ var Body = Backgrid.Body = Backbone.View.extend({
if (command.moveUp() || command.moveDown()) { if (command.moveUp() || command.moveDown()) {
var row = this.rows[i + (command.moveUp() ? -1 : 1)]; 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()) { else if (command.moveLeft() || command.moveRight()) {
var right = command.moveRight(); var right = command.moveRight();
@ -2276,16 +2340,16 @@ var Body = Backgrid.Body = Backbone.View.extend({
right ? offset++ : offset--) { right ? offset++ : offset--) {
var m = ~~(offset / l); var m = ~~(offset / l);
var n = offset - m * l; var n = offset - m * l;
var cell = this.rows[m].cells[n]; cell = this.rows[m].cells[n];
if (cell.column.get("renderable") && cell.column.get("editable")) { 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(); cell.enterEditMode();
break; break;
} }
} }
} }
} }
this.rows[i].cells[j].exitEditMode();
} }
}); });
/* /*

View File

@ -6,18 +6,186 @@
Licensed under the MIT @license. Licensed under the MIT @license.
*/ */
(function ($, _, Backbone, Backgrid) { (function (_, Backbone, Backgrid) {
"use strict"; "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 Paginator is a Backgrid extension that renders a series of configurable
pagination handles. This extension is best used for splitting a large data 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 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 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 within a sliding window, plus the rewind, back, forward and fast forward
next page handles. The fast forward, fast backward, previous and next page control handles. The individual control handles can be turned off.
handles can be turned off.
@class Backgrid.Extension.Paginator @class Backgrid.Extension.Paginator
*/ */
@ -30,97 +198,65 @@
windowSize: 10, windowSize: 10,
/** /**
@property {Object} fastForwardHandleLabels You can disable specific @property {Object.<string, Object.<string, string>>} controls You can
handles by setting its value to `null`. disable specific control handles by omitting certain keys.
*/ */
fastForwardHandleLabels: { controls: {
first: "《", rewind: {
prev: "〈", label: "《",
next: "〉", title: "First"
last: "》" },
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 */ /** @property */
events: { goBackFirstOnSort: true,
"click a": "changePage"
},
/** /**
Initializer. Initializer.
@param {Object} options @param {Object} options
@param {Backbone.Collection} options.collection @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) { initialize: function (options) {
Backgrid.requireOptions(options, ["collection"]); Backgrid.requireOptions(options, ["collection"]);
this.controls = options.controls || this.controls;
this.pageHandle = options.pageHandle || this.pageHandle;
var collection = this.collection; var collection = this.collection;
var fullCollection = collection.fullCollection; this.listenTo(collection, "add", this.render);
if (fullCollection) { this.listenTo(collection, "remove", this.render);
this.listenTo(fullCollection, "add", this.render); this.listenTo(collection, "reset", this.render);
this.listenTo(fullCollection, "remove", this.render); if ((options.goBackFirstOnSort || this.goBackFirstOnSort) &&
this.listenTo(fullCollection, "reset", this.render); collection.fullCollection) {
} this.listenTo(collection.fullCollection, "sort", function () {
else { collection.getFirstPage();
this.listenTo(collection, "add", this.render); });
this.listenTo(collection, "remove", this.render);
this.listenTo(collection, "reset", this.render);
} }
}, },
/** _calculateWindow: function () {
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:
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 = [];
var collection = this.collection; var collection = this.collection;
var state = collection.state; var state = collection.state;
@ -132,48 +268,44 @@
currentPage = firstPage ? currentPage - 1 : currentPage; currentPage = firstPage ? currentPage - 1 : currentPage;
var windowStart = Math.floor(currentPage / this.windowSize) * this.windowSize; var windowStart = Math.floor(currentPage / this.windowSize) * this.windowSize;
var windowEnd = Math.min(lastPage + 1, windowStart + 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++) { Creates a list of page handle objects for rendering.
handles.push({
label: i + 1, @return {Array.<Object>} an array of page handle objects hashes
title: "No. " + (i + 1), */
className: currentPage === i ? "active" : undefined 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 ffLabels = this.fastForwardHandleLabels; var controls = this.controls;
if (ffLabels) { _.each(["back", "rewind", "forward", "fastForward"], function (key) {
var value = controls[key];
if (ffLabels.prev) { if (value) {
handles.unshift({ var handleCtorOpts = {
label: ffLabels.prev, collection: collection,
className: collection.hasPrevious() ? void 0 : "disabled" 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);
} }
});
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; return handles;
}, },
@ -184,15 +316,24 @@
render: function () { render: function () {
this.$el.empty(); this.$el.empty();
this.$el.append(this.template({ if (this.handles) {
handles: this.makeHandles() 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; 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 http://github.com/wyuenho/backbone-pageable
Copyright (c) 2013 Jimmy Yuen Ho Wong Copyright (c) 2013 Jimmy Yuen Ho Wong
@ -574,6 +574,17 @@
/** /**
Change the page size of this collection. 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 For server mode operations, changing the page size will trigger a #fetch
and subsequently a `reset` event. and subsequently a `reset` event.
@ -586,6 +597,8 @@
@param {number} pageSize The new page size to set to #state. @param {number} pageSize The new page size to set to #state.
@param {Object} [options] {@link #fetch} options. @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. @param {boolean} [options.fetch] If `true`, force a fetch in client mode.
@throws {TypeError} If `pageSize` is not a finite integer. @throws {TypeError} If `pageSize` is not a finite integer.
@ -598,14 +611,24 @@
setPageSize: function (pageSize, options) { setPageSize: function (pageSize, options) {
pageSize = finiteInt(pageSize, "pageSize"); 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, 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. encouraged to override #parseState and #parseRecords instead.
@param {Object} resp The deserialized response data from the server. @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 @return {Array.<Object>} An array of model objects
*/ */
parse: function (resp) { parse: function (resp, options) {
var newState = this.parseState(resp, _clone(this.queryParams), _clone(this.state)); var newState = this.parseState(resp, _clone(this.queryParams), _clone(this.state), options);
if (newState) this.state = this._checkState(_extend({}, this.state, newState)); 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 `totalRecords` value is enough to trigger a full pagination state
recalculation. recalculation.
parseState: function (resp, queryParams, state) { parseState: function (resp, queryParams, state, options) {
return {totalRecords: resp.total_entries}; 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 This method __MUST__ return a new state object instead of directly
modifying the #state object. The behavior of directly modifying #state is modifying the #state object. The behavior of directly modifying #state is
undefined. undefined.
@ -1027,10 +1057,12 @@
@param {Object} resp The deserialized response data from the server. @param {Object} resp The deserialized response data from the server.
@param {Object} queryParams A copy of #queryParams. @param {Object} queryParams A copy of #queryParams.
@param {Object} state A copy of #state. @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. @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])) { if (resp && resp.length === 2 && _isObject(resp[0]) && _isArray(resp[1])) {
var newState = _clone(state); var newState = _clone(state);
@ -1059,10 +1091,12 @@
response is returned directly. response is returned directly.
@param {Object} resp The deserialized response data from the server. @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 @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])) { if (resp && resp.length === 2 && _isObject(resp[0]) && _isArray(resp[1])) {
return resp[1]; return resp[1];
} }
@ -1138,7 +1172,7 @@
kvp = extraKvps[i]; kvp = extraKvps[i];
v = kvp[1]; v = kvp[1];
v = _isFunction(v) ? v.call(thisCopy) : v; 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; var fullCol = this.fullCollection, links = this.links;
@ -1212,11 +1246,11 @@
@param {string} [sortKey=this.state.sortKey] See `state.sortKey`. @param {string} [sortKey=this.state.sortKey] See `state.sortKey`.
@param {number} [order=this.state.order] See `state.order`. @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). See [Backbone.Collection.comparator](http://backbonejs.org/#Collection-comparator).
*/ */
_makeComparator: function (sortKey, order) { _makeComparator: function (sortKey, order, sortValue) {
var state = this.state; var state = this.state;
sortKey = sortKey || state.sortKey; sortKey = sortKey || state.sortKey;
@ -1224,8 +1258,12 @@
if (!sortKey || !order) return; if (!sortKey || !order) return;
if (!sortValue) sortValue = function (model, attr) {
return model.get(attr);
};
return function (left, right) { 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 (order === 1) t = l, l = r, r = t;
if (l === r) return 0; if (l === r) return 0;
else if (l < r) return -1; else if (l < r) return -1;
@ -1244,6 +1282,11 @@
`sortKey` to `null` removes the comparator from both the current page and `sortKey` to `null` removes the comparator from both the current page and
the full collection. 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 @chainable
@param {string} sortKey See `state.sortKey`. @param {string} sortKey See `state.sortKey`.
@ -1252,6 +1295,7 @@
@param {"server"|"client"} [options.side] By default, `"client"` if @param {"server"|"client"} [options.side] By default, `"client"` if
`mode` is `"client"`, `"server"` otherwise. `mode` is `"client"`, `"server"` otherwise.
@param {boolean} [options.full=true] @param {boolean} [options.full=true]
@param {(function(Backbone.Model, string): Object) | string} [options.sortValue]
*/ */
setSorting: function (sortKey, order, options) { setSorting: function (sortKey, order, options) {
@ -1270,7 +1314,7 @@
options = _extend({side: mode == "client" ? mode : "server", full: true}, options = _extend({side: mode == "client" ? mode : "server", full: true},
options); options);
var comparator = this._makeComparator(sortKey, order); var comparator = this._makeComparator(sortKey, order, options.sortValue);
var full = options.full, side = options.side; var full = options.full, side = options.side;