1026 lines
29 KiB
JavaScript
1026 lines
29 KiB
JavaScript
/*
|
|
backgrid
|
|
http://github.com/wyuenho/backgrid
|
|
|
|
Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
|
|
Licensed under the MIT license.
|
|
*/
|
|
|
|
/**
|
|
Generic cell editor base class. Only defines an initializer for a number of
|
|
required parameters.
|
|
|
|
@abstract
|
|
@class Backgrid.CellEditor
|
|
@extends Backbone.View
|
|
*/
|
|
var CellEditor = Backgrid.CellEditor = Backbone.View.extend({
|
|
|
|
/**
|
|
Initializer.
|
|
|
|
@param {Object} options
|
|
@param {Backgrid.CellFormatter} options.formatter
|
|
@param {Backgrid.Column} options.column
|
|
@param {Backbone.Model} options.model
|
|
|
|
@throws {TypeError} If `formatter` is not a formatter instance, or when
|
|
`model` or `column` are undefined.
|
|
*/
|
|
initialize: function (options) {
|
|
this.formatter = options.formatter;
|
|
this.column = options.column;
|
|
if (!(this.column instanceof Column)) {
|
|
this.column = new Column(this.column);
|
|
}
|
|
|
|
this.listenTo(this.model, "backgrid:editing", this.postRender);
|
|
},
|
|
|
|
/**
|
|
Post-rendering setup and initialization. Focuses the cell editor's `el` in
|
|
this default implementation. **Should** be called by Cell classes after
|
|
calling Backgrid.CellEditor#render.
|
|
*/
|
|
postRender: function (model, column) {
|
|
if (column == null || column.get("name") == this.column.get("name")) {
|
|
this.$el.focus();
|
|
}
|
|
return this;
|
|
}
|
|
|
|
});
|
|
|
|
/**
|
|
InputCellEditor the cell editor type used by most core cell types. This cell
|
|
editor renders a text input box as its editor. The input will render a
|
|
placeholder if the value is empty on supported browsers.
|
|
|
|
@class Backgrid.InputCellEditor
|
|
@extends Backgrid.CellEditor
|
|
*/
|
|
var InputCellEditor = Backgrid.InputCellEditor = CellEditor.extend({
|
|
|
|
/** @property */
|
|
tagName: "input",
|
|
|
|
/** @property */
|
|
attributes: {
|
|
type: "text"
|
|
},
|
|
|
|
/** @property */
|
|
events: {
|
|
"blur": "saveOrCancel",
|
|
"keydown": "saveOrCancel"
|
|
},
|
|
|
|
/**
|
|
Initializer. Removes this `el` from the DOM when a `done` event is
|
|
triggered.
|
|
|
|
@param {Object} options
|
|
@param {Backgrid.CellFormatter} options.formatter
|
|
@param {Backgrid.Column} options.column
|
|
@param {Backbone.Model} options.model
|
|
@param {string} [options.placeholder]
|
|
*/
|
|
initialize: function (options) {
|
|
InputCellEditor.__super__.initialize.apply(this, arguments);
|
|
|
|
if (options.placeholder) {
|
|
this.$el.attr("placeholder", options.placeholder);
|
|
}
|
|
},
|
|
|
|
/**
|
|
Renders a text input with the cell value formatted for display, if it
|
|
exists.
|
|
*/
|
|
render: function () {
|
|
var model = this.model;
|
|
this.$el.val(this.formatter.fromRaw(model.get(this.column.get("name")), model));
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
If the key pressed is `enter`, `tab`, `up`, or `down`, converts the value
|
|
in the editor to a raw value for saving into the model using the formatter.
|
|
|
|
If the key pressed is `esc` the changes are undone.
|
|
|
|
If the editor goes out of focus (`blur`) but the value is invalid, the
|
|
event is intercepted and cancelled so the cell remains in focus pending for
|
|
further action. The changes are saved otherwise.
|
|
|
|
Triggers a Backbone `backgrid:edited` event from the model when successful,
|
|
and `backgrid:error` if the value cannot be converted. Classes listening to
|
|
the `error` event, usually the Cell classes, should respond appropriately,
|
|
usually by rendering some kind of error feedback.
|
|
|
|
@param {Event} e
|
|
*/
|
|
saveOrCancel: function (e) {
|
|
|
|
var formatter = this.formatter;
|
|
var model = this.model;
|
|
var column = this.column;
|
|
|
|
var command = new Command(e);
|
|
var blurred = e.type === "blur";
|
|
|
|
if (command.moveUp() || command.moveDown() || command.moveLeft() || command.moveRight() ||
|
|
command.save() || blurred) {
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
var val = this.$el.val();
|
|
var newValue = formatter.toRaw(val, model);
|
|
if (_.isUndefined(newValue)) {
|
|
model.trigger("backgrid:error", model, column, val);
|
|
}
|
|
else {
|
|
model.set(column.get("name"), newValue);
|
|
model.trigger("backgrid:edited", model, column, command);
|
|
}
|
|
}
|
|
// esc
|
|
else if (command.cancel()) {
|
|
// undo
|
|
e.stopPropagation();
|
|
model.trigger("backgrid:edited", model, column, command);
|
|
}
|
|
},
|
|
|
|
postRender: function (model, column) {
|
|
if (column == null || column.get("name") == this.column.get("name")) {
|
|
// move the cursor to the end on firefox if text is right aligned
|
|
if (this.$el.css("text-align") === "right") {
|
|
var val = this.$el.val();
|
|
this.$el.focus().val(null).val(val);
|
|
}
|
|
else this.$el.focus();
|
|
}
|
|
return this;
|
|
}
|
|
|
|
});
|
|
|
|
/**
|
|
The super-class for all Cell types. By default, this class renders a plain
|
|
table cell with the model value converted to a string using the
|
|
formatter. The table cell is clickable, upon which the cell will go into
|
|
editor mode, which is rendered by a Backgrid.InputCellEditor instance by
|
|
default. Upon encountering any formatting errors, this class will add an
|
|
`error` CSS class to the table cell.
|
|
|
|
@abstract
|
|
@class Backgrid.Cell
|
|
@extends Backbone.View
|
|
*/
|
|
var Cell = Backgrid.Cell = Backbone.View.extend({
|
|
|
|
/** @property */
|
|
tagName: "td",
|
|
|
|
/**
|
|
@property {Backgrid.CellFormatter|Object|string} [formatter=CellFormatter]
|
|
*/
|
|
formatter: CellFormatter,
|
|
|
|
/**
|
|
@property {Backgrid.CellEditor} [editor=Backgrid.InputCellEditor] The
|
|
default editor for all cell instances of this class. This value must be a
|
|
class, it will be automatically instantiated upon entering edit mode.
|
|
|
|
See Backgrid.CellEditor
|
|
*/
|
|
editor: InputCellEditor,
|
|
|
|
/** @property */
|
|
events: {
|
|
"click": "enterEditMode"
|
|
},
|
|
|
|
/**
|
|
Initializer.
|
|
|
|
@param {Object} options
|
|
@param {Backbone.Model} options.model
|
|
@param {Backgrid.Column} options.column
|
|
|
|
@throws {ReferenceError} If formatter is a string but a formatter class of
|
|
said name cannot be found in the Backgrid module.
|
|
*/
|
|
initialize: function (options) {
|
|
this.column = options.column;
|
|
if (!(this.column instanceof Column)) {
|
|
this.column = new Column(this.column);
|
|
}
|
|
|
|
var column = this.column, model = this.model, $el = this.$el;
|
|
|
|
var formatter = Backgrid.resolveNameToClass(column.get("formatter") ||
|
|
this.formatter, "Formatter");
|
|
|
|
if (!_.isFunction(formatter.fromRaw) && !_.isFunction(formatter.toRaw)) {
|
|
formatter = new formatter();
|
|
}
|
|
|
|
this.formatter = formatter;
|
|
|
|
this.editor = Backgrid.resolveNameToClass(this.editor, "CellEditor");
|
|
|
|
this.listenTo(model, "change:" + column.get("name"), function () {
|
|
if (!$el.hasClass("editor")) this.render();
|
|
});
|
|
|
|
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 (Backgrid.callByNeed(column.editable(), column, model)) $el.addClass("editable");
|
|
if (Backgrid.callByNeed(column.sortable(), column, model)) $el.addClass("sortable");
|
|
if (Backgrid.callByNeed(column.renderable(), column, model)) $el.addClass("renderable");
|
|
},
|
|
|
|
/**
|
|
Render a text string in a table cell. The text is converted from the
|
|
model's raw value for this cell's column.
|
|
*/
|
|
render: function () {
|
|
this.$el.empty();
|
|
var model = this.model;
|
|
this.$el.text(this.formatter.fromRaw(model.get(this.column.get("name")), model));
|
|
this.delegateEvents();
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
If this column is editable, a new CellEditor instance is instantiated with
|
|
its required parameters. An `editor` CSS class is added to the cell upon
|
|
entering edit mode.
|
|
|
|
This method triggers a Backbone `backgrid:edit` event from the model when
|
|
the cell is entering edit mode and an editor instance has been constructed,
|
|
but before it is rendered and inserted into the DOM. The cell and the
|
|
constructed cell editor instance are sent as event parameters when this
|
|
event is triggered.
|
|
|
|
When this cell has finished switching to edit mode, a Backbone
|
|
`backgrid:editing` event is triggered from the model. The cell and the
|
|
constructed cell instance are also sent as parameters in the event.
|
|
|
|
When the model triggers a `backgrid:error` event, it means the editor is
|
|
unable to convert the current user input to an apprpriate value for the
|
|
model's column, and an `error` CSS class is added to the cell accordingly.
|
|
*/
|
|
enterEditMode: function () {
|
|
var model = this.model;
|
|
var column = this.column;
|
|
|
|
var editable = Backgrid.callByNeed(column.editable(), column, model);
|
|
if (editable) {
|
|
|
|
this.currentEditor = new this.editor({
|
|
column: this.column,
|
|
model: this.model,
|
|
formatter: this.formatter
|
|
});
|
|
|
|
model.trigger("backgrid:edit", model, column, this, this.currentEditor);
|
|
|
|
// Need to redundantly undelegate events for Firefox
|
|
this.undelegateEvents();
|
|
this.$el.empty();
|
|
this.$el.append(this.currentEditor.$el);
|
|
this.currentEditor.render();
|
|
this.$el.addClass("editor");
|
|
|
|
model.trigger("backgrid:editing", model, column, this, this.currentEditor);
|
|
}
|
|
},
|
|
|
|
/**
|
|
Put an `error` CSS class on the table cell.
|
|
*/
|
|
renderError: function (model, column) {
|
|
if (column == null || column.get("name") == this.column.get("name")) {
|
|
this.$el.addClass("error");
|
|
}
|
|
},
|
|
|
|
/**
|
|
Removes the editor and re-render in display mode.
|
|
*/
|
|
exitEditMode: function () {
|
|
this.$el.removeClass("error");
|
|
this.currentEditor.remove();
|
|
this.stopListening(this.currentEditor);
|
|
delete this.currentEditor;
|
|
this.$el.removeClass("editor");
|
|
this.render();
|
|
},
|
|
|
|
/**
|
|
Clean up this cell.
|
|
|
|
@chainable
|
|
*/
|
|
remove: function () {
|
|
if (this.currentEditor) {
|
|
this.currentEditor.remove.apply(this.currentEditor, arguments);
|
|
delete this.currentEditor;
|
|
}
|
|
return Cell.__super__.remove.apply(this, arguments);
|
|
}
|
|
|
|
});
|
|
|
|
/**
|
|
StringCell displays HTML escaped strings and accepts anything typed in.
|
|
|
|
@class Backgrid.StringCell
|
|
@extends Backgrid.Cell
|
|
*/
|
|
var StringCell = Backgrid.StringCell = Cell.extend({
|
|
|
|
/** @property */
|
|
className: "string-cell",
|
|
|
|
formatter: StringFormatter
|
|
|
|
});
|
|
|
|
/**
|
|
UriCell renders an HTML `<a>` anchor for the value and accepts URIs as user
|
|
input values. No type conversion or URL validation is done by the formatter
|
|
of this cell. Users who need URL validation are encourage to subclass UriCell
|
|
to take advantage of the parsing capabilities of the HTMLAnchorElement
|
|
available on HTML5-capable browsers or using a third-party library like
|
|
[URI.js](https://github.com/medialize/URI.js).
|
|
|
|
@class Backgrid.UriCell
|
|
@extends Backgrid.Cell
|
|
*/
|
|
var UriCell = Backgrid.UriCell = Cell.extend({
|
|
|
|
/** @property */
|
|
className: "uri-cell",
|
|
|
|
/**
|
|
@property {string} [title] The title attribute of the generated anchor. It
|
|
uses the display value formatted by the `formatter.fromRaw` by default.
|
|
*/
|
|
title: null,
|
|
|
|
/**
|
|
@property {string} [target="_blank"] The target attribute of the generated
|
|
anchor.
|
|
*/
|
|
target: "_blank",
|
|
|
|
initialize: function (options) {
|
|
UriCell.__super__.initialize.apply(this, arguments);
|
|
this.title = options.title || this.title;
|
|
this.target = options.target || this.target;
|
|
},
|
|
|
|
render: function () {
|
|
this.$el.empty();
|
|
var rawValue = this.model.get(this.column.get("name"));
|
|
var formattedValue = this.formatter.fromRaw(rawValue, this.model);
|
|
this.$el.append($("<a>", {
|
|
tabIndex: -1,
|
|
href: rawValue,
|
|
title: this.title || formattedValue,
|
|
target: this.target
|
|
}).text(formattedValue));
|
|
this.delegateEvents();
|
|
return this;
|
|
}
|
|
|
|
});
|
|
|
|
/**
|
|
Like Backgrid.UriCell, EmailCell renders an HTML `<a>` anchor for the
|
|
value. The `href` in the anchor is prefixed with `mailto:`. EmailCell will
|
|
complain if the user enters a string that doesn't contain the `@` sign.
|
|
|
|
@class Backgrid.EmailCell
|
|
@extends Backgrid.StringCell
|
|
*/
|
|
var EmailCell = Backgrid.EmailCell = StringCell.extend({
|
|
|
|
/** @property */
|
|
className: "email-cell",
|
|
|
|
formatter: EmailFormatter,
|
|
|
|
render: function () {
|
|
this.$el.empty();
|
|
var model = this.model;
|
|
var formattedValue = this.formatter.fromRaw(model.get(this.column.get("name")), model);
|
|
this.$el.append($("<a>", {
|
|
tabIndex: -1,
|
|
href: "mailto:" + formattedValue,
|
|
title: formattedValue
|
|
}).text(formattedValue));
|
|
this.delegateEvents();
|
|
return this;
|
|
}
|
|
|
|
});
|
|
|
|
/**
|
|
NumberCell is a generic cell that renders all numbers. Numbers are formatted
|
|
using a Backgrid.NumberFormatter.
|
|
|
|
@class Backgrid.NumberCell
|
|
@extends Backgrid.Cell
|
|
*/
|
|
var NumberCell = Backgrid.NumberCell = Cell.extend({
|
|
|
|
/** @property */
|
|
className: "number-cell",
|
|
|
|
/**
|
|
@property {number} [decimals=2] Must be an integer.
|
|
*/
|
|
decimals: NumberFormatter.prototype.defaults.decimals,
|
|
|
|
/** @property {string} [decimalSeparator='.'] */
|
|
decimalSeparator: NumberFormatter.prototype.defaults.decimalSeparator,
|
|
|
|
/** @property {string} [orderSeparator=','] */
|
|
orderSeparator: NumberFormatter.prototype.defaults.orderSeparator,
|
|
|
|
/** @property {Backgrid.CellFormatter} [formatter=Backgrid.NumberFormatter] */
|
|
formatter: NumberFormatter,
|
|
|
|
/**
|
|
Initializes this cell and the number formatter.
|
|
|
|
@param {Object} options
|
|
@param {Backbone.Model} options.model
|
|
@param {Backgrid.Column} options.column
|
|
*/
|
|
initialize: function (options) {
|
|
NumberCell.__super__.initialize.apply(this, arguments);
|
|
var formatter = this.formatter;
|
|
formatter.decimals = this.decimals;
|
|
formatter.decimalSeparator = this.decimalSeparator;
|
|
formatter.orderSeparator = this.orderSeparator;
|
|
}
|
|
|
|
});
|
|
|
|
/**
|
|
An IntegerCell is just a Backgrid.NumberCell with 0 decimals. If a floating
|
|
point number is supplied, the number is simply rounded the usual way when
|
|
displayed.
|
|
|
|
@class Backgrid.IntegerCell
|
|
@extends Backgrid.NumberCell
|
|
*/
|
|
var IntegerCell = Backgrid.IntegerCell = NumberCell.extend({
|
|
|
|
/** @property */
|
|
className: "integer-cell",
|
|
|
|
/**
|
|
@property {number} decimals Must be an integer.
|
|
*/
|
|
decimals: 0
|
|
});
|
|
|
|
/**
|
|
A PercentCell is another Backgrid.NumberCell that takes a floating number,
|
|
optionally multiplied by a multiplier and display it as a percentage.
|
|
|
|
@class Backgrid.PercentCell
|
|
@extends Backgrid.NumberCell
|
|
*/
|
|
var PercentCell = Backgrid.PercentCell = NumberCell.extend({
|
|
|
|
/** @property */
|
|
className: "percent-cell",
|
|
|
|
/** @property {number} [multiplier=1] */
|
|
multiplier: PercentFormatter.prototype.defaults.multiplier,
|
|
|
|
/** @property {string} [symbol='%'] */
|
|
symbol: PercentFormatter.prototype.defaults.symbol,
|
|
|
|
/** @property {Backgrid.CellFormatter} [formatter=Backgrid.PercentFormatter] */
|
|
formatter: PercentFormatter,
|
|
|
|
/**
|
|
Initializes this cell and the percent formatter.
|
|
|
|
@param {Object} options
|
|
@param {Backbone.Model} options.model
|
|
@param {Backgrid.Column} options.column
|
|
*/
|
|
initialize: function () {
|
|
PercentCell.__super__.initialize.apply(this, arguments);
|
|
var formatter = this.formatter;
|
|
formatter.multiplier = this.multiplier;
|
|
formatter.symbol = this.symbol;
|
|
}
|
|
|
|
});
|
|
|
|
/**
|
|
DatetimeCell is a basic cell that accepts datetime string values in RFC-2822
|
|
or W3C's subset of ISO-8601 and displays them in ISO-8601 format. For a much
|
|
more sophisticated date time cell with better datetime formatting, take a
|
|
look at the Backgrid.Extension.MomentCell extension.
|
|
|
|
@class Backgrid.DatetimeCell
|
|
@extends Backgrid.Cell
|
|
|
|
See:
|
|
|
|
- Backgrid.Extension.MomentCell
|
|
- Backgrid.DatetimeFormatter
|
|
*/
|
|
var DatetimeCell = Backgrid.DatetimeCell = Cell.extend({
|
|
|
|
/** @property */
|
|
className: "datetime-cell",
|
|
|
|
/**
|
|
@property {boolean} [includeDate=true]
|
|
*/
|
|
includeDate: DatetimeFormatter.prototype.defaults.includeDate,
|
|
|
|
/**
|
|
@property {boolean} [includeTime=true]
|
|
*/
|
|
includeTime: DatetimeFormatter.prototype.defaults.includeTime,
|
|
|
|
/**
|
|
@property {boolean} [includeMilli=false]
|
|
*/
|
|
includeMilli: DatetimeFormatter.prototype.defaults.includeMilli,
|
|
|
|
/** @property {Backgrid.CellFormatter} [formatter=Backgrid.DatetimeFormatter] */
|
|
formatter: DatetimeFormatter,
|
|
|
|
/**
|
|
Initializes this cell and the datetime formatter.
|
|
|
|
@param {Object} options
|
|
@param {Backbone.Model} options.model
|
|
@param {Backgrid.Column} options.column
|
|
*/
|
|
initialize: function (options) {
|
|
DatetimeCell.__super__.initialize.apply(this, arguments);
|
|
var formatter = this.formatter;
|
|
formatter.includeDate = this.includeDate;
|
|
formatter.includeTime = this.includeTime;
|
|
formatter.includeMilli = this.includeMilli;
|
|
|
|
var placeholder = this.includeDate ? "YYYY-MM-DD" : "";
|
|
placeholder += (this.includeDate && this.includeTime) ? "T" : "";
|
|
placeholder += this.includeTime ? "HH:mm:ss" : "";
|
|
placeholder += (this.includeTime && this.includeMilli) ? ".SSS" : "";
|
|
|
|
this.editor = this.editor.extend({
|
|
attributes: _.extend({}, this.editor.prototype.attributes, this.editor.attributes, {
|
|
placeholder: placeholder
|
|
})
|
|
});
|
|
}
|
|
|
|
});
|
|
|
|
/**
|
|
DateCell is a Backgrid.DatetimeCell without the time part.
|
|
|
|
@class Backgrid.DateCell
|
|
@extends Backgrid.DatetimeCell
|
|
*/
|
|
var DateCell = Backgrid.DateCell = DatetimeCell.extend({
|
|
|
|
/** @property */
|
|
className: "date-cell",
|
|
|
|
/** @property */
|
|
includeTime: false
|
|
|
|
});
|
|
|
|
/**
|
|
TimeCell is a Backgrid.DatetimeCell without the date part.
|
|
|
|
@class Backgrid.TimeCell
|
|
@extends Backgrid.DatetimeCell
|
|
*/
|
|
var TimeCell = Backgrid.TimeCell = DatetimeCell.extend({
|
|
|
|
/** @property */
|
|
className: "time-cell",
|
|
|
|
/** @property */
|
|
includeDate: false
|
|
|
|
});
|
|
|
|
/**
|
|
BooleanCellEditor renders a checkbox as its editor.
|
|
|
|
@class Backgrid.BooleanCellEditor
|
|
@extends Backgrid.CellEditor
|
|
*/
|
|
var BooleanCellEditor = Backgrid.BooleanCellEditor = CellEditor.extend({
|
|
|
|
/** @property */
|
|
tagName: "input",
|
|
|
|
/** @property */
|
|
attributes: {
|
|
tabIndex: -1,
|
|
type: "checkbox"
|
|
},
|
|
|
|
/** @property */
|
|
events: {
|
|
"mousedown": function () {
|
|
this.mouseDown = true;
|
|
},
|
|
"blur": "enterOrExitEditMode",
|
|
"mouseup": function () {
|
|
this.mouseDown = false;
|
|
},
|
|
"change": "saveOrCancel",
|
|
"keydown": "saveOrCancel"
|
|
},
|
|
|
|
/**
|
|
Renders a checkbox and check it if the model value of this column is true,
|
|
uncheck otherwise.
|
|
*/
|
|
render: function () {
|
|
var model = this.model;
|
|
var val = this.formatter.fromRaw(model.get(this.column.get("name")), model);
|
|
this.$el.prop("checked", val);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
Event handler. Hack to deal with the case where `blur` is fired before
|
|
`change` and `click` on a checkbox.
|
|
*/
|
|
enterOrExitEditMode: function (e) {
|
|
if (!this.mouseDown) {
|
|
var model = this.model;
|
|
model.trigger("backgrid:edited", model, this.column, new Command(e));
|
|
}
|
|
},
|
|
|
|
/**
|
|
Event handler. Save the value into the model if the event is `change` or
|
|
one of the keyboard navigation key presses. Exit edit mode without saving
|
|
if `escape` was pressed.
|
|
*/
|
|
saveOrCancel: function (e) {
|
|
var model = this.model;
|
|
var column = this.column;
|
|
var formatter = this.formatter;
|
|
var command = new Command(e);
|
|
// skip ahead to `change` when space is pressed
|
|
if (command.passThru() && e.type != "change") return true;
|
|
if (command.cancel()) {
|
|
e.stopPropagation();
|
|
model.trigger("backgrid:edited", model, column, command);
|
|
}
|
|
|
|
var $el = this.$el;
|
|
if (command.save() || command.moveLeft() || command.moveRight() || command.moveUp() ||
|
|
command.moveDown()) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
var val = formatter.toRaw($el.prop("checked"), model);
|
|
model.set(column.get("name"), val);
|
|
model.trigger("backgrid:edited", model, column, command);
|
|
}
|
|
else if (e.type == "change") {
|
|
var val = formatter.toRaw($el.prop("checked"), model);
|
|
model.set(column.get("name"), val);
|
|
$el.focus();
|
|
}
|
|
}
|
|
|
|
});
|
|
|
|
/**
|
|
BooleanCell renders a checkbox both during display mode and edit mode. The
|
|
checkbox is checked if the model value is true, unchecked otherwise.
|
|
|
|
@class Backgrid.BooleanCell
|
|
@extends Backgrid.Cell
|
|
*/
|
|
var BooleanCell = Backgrid.BooleanCell = Cell.extend({
|
|
|
|
/** @property */
|
|
className: "boolean-cell",
|
|
|
|
/** @property */
|
|
editor: BooleanCellEditor,
|
|
|
|
/** @property */
|
|
events: {
|
|
"click": "enterEditMode"
|
|
},
|
|
|
|
/**
|
|
Renders a checkbox and check it if the model value of this column is true,
|
|
uncheck otherwise.
|
|
*/
|
|
render: function () {
|
|
this.$el.empty();
|
|
var model = this.model, column = this.column;
|
|
var editable = Backgrid.callByNeed(column.editable(), column, model);
|
|
this.$el.append($("<input>", {
|
|
tabIndex: -1,
|
|
type: "checkbox",
|
|
checked: this.formatter.fromRaw(model.get(column.get("name")), model),
|
|
disabled: !editable
|
|
}));
|
|
this.delegateEvents();
|
|
return this;
|
|
}
|
|
|
|
});
|
|
|
|
/**
|
|
SelectCellEditor renders an HTML `<select>` fragment as the editor.
|
|
|
|
@class Backgrid.SelectCellEditor
|
|
@extends Backgrid.CellEditor
|
|
*/
|
|
var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({
|
|
|
|
/** @property */
|
|
tagName: "select",
|
|
|
|
/** @property */
|
|
events: {
|
|
"change": "save",
|
|
"blur": "close",
|
|
"keydown": "close"
|
|
},
|
|
|
|
/** @property {function(Object, ?Object=): string} template */
|
|
template: _.template('<option value="<%- value %>" <%= selected ? \'selected="selected"\' : "" %>><%- text %></option>', null, {variable: null}),
|
|
|
|
setOptionValues: function (optionValues) {
|
|
this.optionValues = optionValues;
|
|
this.optionValues = _.result(this, "optionValues");
|
|
},
|
|
|
|
setMultiple: function (multiple) {
|
|
this.multiple = multiple;
|
|
this.$el.prop("multiple", multiple);
|
|
},
|
|
|
|
_renderOptions: function (nvps, selectedValues) {
|
|
var options = '';
|
|
for (var i = 0; i < nvps.length; i++) {
|
|
options = options + this.template({
|
|
text: nvps[i][0],
|
|
value: nvps[i][1],
|
|
selected: _.indexOf(selectedValues, nvps[i][1]) > -1
|
|
});
|
|
}
|
|
return options;
|
|
},
|
|
|
|
/**
|
|
Renders the options if `optionValues` is a list of name-value pairs. The
|
|
options are contained inside option groups if `optionValues` is a list of
|
|
object hashes. The name is rendered at the option text and the value is the
|
|
option value. If `optionValues` is a function, it is called without a
|
|
parameter.
|
|
*/
|
|
render: function () {
|
|
this.$el.empty();
|
|
|
|
var optionValues = _.result(this, "optionValues");
|
|
var model = this.model;
|
|
var selectedValues = this.formatter.fromRaw(model.get(this.column.get("name")), model);
|
|
|
|
if (!_.isArray(optionValues)) throw new TypeError("optionValues must be an array");
|
|
|
|
var optionValue = null;
|
|
var optionText = null;
|
|
var optionValue = null;
|
|
var optgroupName = null;
|
|
var optgroup = null;
|
|
|
|
for (var i = 0; i < optionValues.length; i++) {
|
|
var optionValue = optionValues[i];
|
|
|
|
if (_.isArray(optionValue)) {
|
|
optionText = optionValue[0];
|
|
optionValue = optionValue[1];
|
|
|
|
this.$el.append(this.template({
|
|
text: optionText,
|
|
value: optionValue,
|
|
selected: _.indexOf(selectedValues, optionValue) > -1
|
|
}));
|
|
}
|
|
else if (_.isObject(optionValue)) {
|
|
optgroupName = optionValue.name;
|
|
optgroup = $("<optgroup></optgroup>", { label: optgroupName });
|
|
optgroup.append(this._renderOptions.call(this, optionValue.values, selectedValues));
|
|
this.$el.append(optgroup);
|
|
}
|
|
else {
|
|
throw new TypeError("optionValues elements must be a name-value pair or an object hash of { name: 'optgroup label', value: [option name-value pairs] }");
|
|
}
|
|
}
|
|
|
|
this.delegateEvents();
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
Saves the value of the selected option to the model attribute.
|
|
*/
|
|
save: function (e) {
|
|
var model = this.model;
|
|
var column = this.column;
|
|
model.set(column.get("name"), this.formatter.toRaw(this.$el.val(), model));
|
|
},
|
|
|
|
/**
|
|
Triggers a `backgrid:edited` event from the model so the body can close
|
|
this editor.
|
|
*/
|
|
close: function (e) {
|
|
var model = this.model;
|
|
var column = this.column;
|
|
var command = new Command(e);
|
|
if (command.cancel()) {
|
|
e.stopPropagation();
|
|
model.trigger("backgrid:edited", model, column, new Command(e));
|
|
}
|
|
else if (command.save() || command.moveLeft() || command.moveRight() ||
|
|
command.moveUp() || command.moveDown() || e.type == "blur") {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.save(e);
|
|
model.trigger("backgrid:edited", model, column, new Command(e));
|
|
}
|
|
}
|
|
|
|
});
|
|
|
|
/**
|
|
SelectCell is also a different kind of cell in that upon going into edit mode
|
|
the cell renders a list of options to pick from, as opposed to an input box.
|
|
|
|
SelectCell cannot be referenced by its string name when used in a column
|
|
definition because it requires an `optionValues` class attribute to be
|
|
defined. `optionValues` can either be a list of name-value pairs, to be
|
|
rendered as options, or a list of object hashes which consist of a key *name*
|
|
which is the option group name, and a key *values* which is a list of
|
|
name-value pairs to be rendered as options under that option group.
|
|
|
|
In addition, `optionValues` can also be a parameter-less function that
|
|
returns one of the above. If the options are static, it is recommended the
|
|
returned values to be memoized. `_.memoize()` is a good function to help with
|
|
that.
|
|
|
|
During display mode, the default formatter will normalize the raw model value
|
|
to an array of values whether the raw model value is a scalar or an
|
|
array. Each value is compared with the `optionValues` values using
|
|
Ecmascript's implicit type conversion rules. When exiting edit mode, no type
|
|
conversion is performed when saving into the model. This behavior is not
|
|
always desirable when the value type is anything other than string. To
|
|
control type conversion on the client-side, you should subclass SelectCell to
|
|
provide a custom formatter or provide the formatter to your column
|
|
definition.
|
|
|
|
See:
|
|
[$.fn.val()](http://api.jquery.com/val/)
|
|
|
|
@class Backgrid.SelectCell
|
|
@extends Backgrid.Cell
|
|
*/
|
|
var SelectCell = Backgrid.SelectCell = Cell.extend({
|
|
|
|
/** @property */
|
|
className: "select-cell",
|
|
|
|
/** @property */
|
|
editor: SelectCellEditor,
|
|
|
|
/** @property */
|
|
multiple: false,
|
|
|
|
/** @property */
|
|
formatter: SelectFormatter,
|
|
|
|
/**
|
|
@property {Array.<Array>|Array.<{name: string, values: Array.<Array>}>} optionValues
|
|
*/
|
|
optionValues: undefined,
|
|
|
|
/** @property */
|
|
delimiter: ', ',
|
|
|
|
/**
|
|
Initializer.
|
|
|
|
@param {Object} options
|
|
@param {Backbone.Model} options.model
|
|
@param {Backgrid.Column} options.column
|
|
|
|
@throws {TypeError} If `optionsValues` is undefined.
|
|
*/
|
|
initialize: function (options) {
|
|
SelectCell.__super__.initialize.apply(this, arguments);
|
|
this.listenTo(this.model, "backgrid:edit", function (model, column, cell, editor) {
|
|
if (column.get("name") == this.column.get("name")) {
|
|
editor.setOptionValues(this.optionValues);
|
|
editor.setMultiple(this.multiple);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
Renders the label using the raw value as key to look up from `optionValues`.
|
|
|
|
@throws {TypeError} If `optionValues` is malformed.
|
|
*/
|
|
render: function () {
|
|
this.$el.empty();
|
|
|
|
var optionValues = _.result(this, "optionValues");
|
|
var model = this.model;
|
|
var rawData = this.formatter.fromRaw(model.get(this.column.get("name")), model);
|
|
|
|
var selectedText = [];
|
|
|
|
try {
|
|
if (!_.isArray(optionValues) || _.isEmpty(optionValues)) throw new TypeError;
|
|
|
|
for (var k = 0; k < rawData.length; k++) {
|
|
var rawDatum = rawData[k];
|
|
|
|
for (var i = 0; i < optionValues.length; i++) {
|
|
var optionValue = optionValues[i];
|
|
|
|
if (_.isArray(optionValue)) {
|
|
var optionText = optionValue[0];
|
|
var optionValue = optionValue[1];
|
|
|
|
if (optionValue == rawDatum) selectedText.push(optionText);
|
|
}
|
|
else if (_.isObject(optionValue)) {
|
|
var optionGroupValues = optionValue.values;
|
|
|
|
for (var j = 0; j < optionGroupValues.length; j++) {
|
|
var optionGroupValue = optionGroupValues[j];
|
|
if (optionGroupValue[1] == rawDatum) {
|
|
selectedText.push(optionGroupValue[0]);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
throw new TypeError;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.$el.append(selectedText.join(this.delimiter));
|
|
}
|
|
catch (ex) {
|
|
if (ex instanceof TypeError) {
|
|
throw new TypeError("'optionValues' must be of type {Array.<Array>|Array.<{name: string, values: Array.<Array>}>}");
|
|
}
|
|
throw ex;
|
|
}
|
|
|
|
this.delegateEvents();
|
|
|
|
return this;
|
|
}
|
|
|
|
});
|