2531 lines
71 KiB
JavaScript
2531 lines
71 KiB
JavaScript
/*
|
|
backgrid
|
|
http://github.com/wyuenho/backgrid
|
|
|
|
Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
|
|
Licensed under the MIT @license.
|
|
*/
|
|
(function (root, $, _, Backbone) {
|
|
|
|
"use strict";
|
|
/*
|
|
backgrid
|
|
http://github.com/wyuenho/backgrid
|
|
|
|
Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
|
|
Licensed under the MIT @license.
|
|
*/
|
|
|
|
var window = root;
|
|
|
|
// Copyright 2009, 2010 Kristopher Michael Kowal
|
|
// https://github.com/kriskowal/es5-shim
|
|
// ES5 15.5.4.20
|
|
// http://es5.github.com/#x15.5.4.20
|
|
var ws = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003" +
|
|
"\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028" +
|
|
"\u2029\uFEFF";
|
|
if (!String.prototype.trim || ws.trim()) {
|
|
// http://blog.stevenlevithan.com/archives/faster-trim-javascript
|
|
// http://perfectionkills.com/whitespace-deviations/
|
|
ws = "[" + ws + "]";
|
|
var trimBeginRegexp = new RegExp("^" + ws + ws + "*"),
|
|
trimEndRegexp = new RegExp(ws + ws + "*$");
|
|
String.prototype.trim = function trim() {
|
|
if (this === undefined || this === null) {
|
|
throw new TypeError("can't convert " + this + " to object");
|
|
}
|
|
return String(this)
|
|
.replace(trimBeginRegexp, "")
|
|
.replace(trimEndRegexp, "");
|
|
};
|
|
}
|
|
|
|
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;
|
|
var padding = '';
|
|
for (var i = 0; i < paddingLen; i++) {
|
|
padding = padding + padstr;
|
|
}
|
|
return padding + str;
|
|
}
|
|
|
|
var Backgrid = root.Backgrid = {
|
|
|
|
VERSION: "0.2.6",
|
|
|
|
Extension: {},
|
|
|
|
requireOptions: function (options, requireOptionKeys) {
|
|
for (var i = 0; i < requireOptionKeys.length; i++) {
|
|
var key = requireOptionKeys[i];
|
|
if (_.isUndefined(options[key])) {
|
|
throw new TypeError("'" + key + "' is required");
|
|
}
|
|
}
|
|
},
|
|
|
|
resolveNameToClass: function (name, suffix) {
|
|
if (_.isString(name)) {
|
|
var key = _.map(name.split('-'), function (e) { return capitalize(e); }).join('') + suffix;
|
|
var klass = Backgrid[key] || Backgrid.Extension[key];
|
|
if (_.isUndefined(klass)) {
|
|
throw new ReferenceError("Class '" + key + "' not found");
|
|
}
|
|
return klass;
|
|
}
|
|
|
|
return name;
|
|
}
|
|
};
|
|
_.extend(Backgrid, Backbone.Events);
|
|
|
|
/**
|
|
Command translates a DOM Event into commands that Backgrid
|
|
recognizes. Interested parties can listen on selected Backgrid events that
|
|
come with an instance of this class and act on the commands.
|
|
|
|
It is also possible to globally rebind the keyboard shortcuts by replacing
|
|
the methods in this class' prototype.
|
|
|
|
@class Backgrid.Command
|
|
@constructor
|
|
*/
|
|
var Command = Backgrid.Command = function (evt) {
|
|
_.extend(this, {
|
|
altKey: !!evt.altKey,
|
|
char: evt.char,
|
|
charCode: evt.charCode,
|
|
ctrlKey: !!evt.ctrlKey,
|
|
key: evt.key,
|
|
keyCode: evt.keyCode,
|
|
locale: evt.locale,
|
|
location: evt.location,
|
|
metaKey: !!evt.metaKey,
|
|
repeat: !!evt.repeat,
|
|
shiftKey: !!evt.shiftKey,
|
|
which: evt.which
|
|
});
|
|
};
|
|
_.extend(Command.prototype, {
|
|
/**
|
|
Up Arrow
|
|
|
|
@member Backgrid.Command
|
|
*/
|
|
moveUp: function () { return this.keyCode == 38; },
|
|
/**
|
|
Down Arrow
|
|
|
|
@member Backgrid.Command
|
|
*/
|
|
moveDown: function () { return this.keyCode === 40; },
|
|
/**
|
|
Shift Tab
|
|
|
|
@member Backgrid.Command
|
|
*/
|
|
moveLeft: function () { return this.shiftKey && this.keyCode === 9; },
|
|
/**
|
|
Tab
|
|
|
|
@member Backgrid.Command
|
|
*/
|
|
moveRight: function () { return !this.shiftKey && this.keyCode === 9; },
|
|
/**
|
|
Enter
|
|
|
|
@member Backgrid.Command
|
|
*/
|
|
save: function () { return this.keyCode === 13; },
|
|
/**
|
|
Esc
|
|
|
|
@member Backgrid.Command
|
|
*/
|
|
cancel: function () { return this.keyCode === 27; },
|
|
/**
|
|
None of the above.
|
|
|
|
@member Backgrid.Command
|
|
*/
|
|
passThru: function () {
|
|
return !(this.moveUp() || this.moveDown() || this.moveLeft() ||
|
|
this.moveRight() || this.save() || this.cancel());
|
|
}
|
|
});
|
|
|
|
/*
|
|
backgrid
|
|
http://github.com/wyuenho/backgrid
|
|
|
|
Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
|
|
Licensed under the MIT @license.
|
|
*/
|
|
|
|
/**
|
|
Just a convenient class for interested parties to subclass.
|
|
|
|
The default Cell classes don't require the formatter to be a subclass of
|
|
Formatter as long as the fromRaw(rawData) and toRaw(formattedData) methods
|
|
are defined.
|
|
|
|
@abstract
|
|
@class Backgrid.CellFormatter
|
|
@constructor
|
|
*/
|
|
var CellFormatter = Backgrid.CellFormatter = function () {};
|
|
_.extend(CellFormatter.prototype, {
|
|
|
|
/**
|
|
Takes a raw value from a model and returns an optionally formatted string
|
|
for display. The default implementation simply returns the supplied value
|
|
as is without any type conversion.
|
|
|
|
@member Backgrid.CellFormatter
|
|
@param {*} rawData
|
|
@return {*}
|
|
*/
|
|
fromRaw: function (rawData) {
|
|
return rawData;
|
|
},
|
|
|
|
/**
|
|
Takes a formatted string, usually from user input, and returns a
|
|
appropriately typed value for persistence in the model.
|
|
|
|
If the user input is invalid or unable to be converted to a raw value
|
|
suitable for persistence in the model, toRaw must return `undefined`.
|
|
|
|
@member Backgrid.CellFormatter
|
|
@param {string} formattedData
|
|
@return {*|undefined}
|
|
*/
|
|
toRaw: function (formattedData) {
|
|
return formattedData;
|
|
}
|
|
|
|
});
|
|
|
|
/**
|
|
A floating point number formatter. Doesn't understand notation at the moment.
|
|
|
|
@class Backgrid.NumberFormatter
|
|
@extends Backgrid.CellFormatter
|
|
@constructor
|
|
@throws {RangeError} If decimals < 0 or > 20.
|
|
*/
|
|
var NumberFormatter = Backgrid.NumberFormatter = function (options) {
|
|
options = options ? _.clone(options) : {};
|
|
_.extend(this, this.defaults, options);
|
|
|
|
if (this.decimals < 0 || this.decimals > 20) {
|
|
throw new RangeError("decimals must be between 0 and 20");
|
|
}
|
|
};
|
|
NumberFormatter.prototype = new CellFormatter();
|
|
_.extend(NumberFormatter.prototype, {
|
|
|
|
/**
|
|
@member Backgrid.NumberFormatter
|
|
@cfg {Object} options
|
|
|
|
@cfg {number} [options.decimals=2] Number of decimals to display. Must be an integer.
|
|
|
|
@cfg {string} [options.decimalSeparator='.'] The separator to use when
|
|
displaying decimals.
|
|
|
|
@cfg {string} [options.orderSeparator=','] The separator to use to
|
|
separator thousands. May be an empty string.
|
|
*/
|
|
defaults: {
|
|
decimals: 2,
|
|
decimalSeparator: '.',
|
|
orderSeparator: ','
|
|
},
|
|
|
|
HUMANIZED_NUM_RE: /(\d)(?=(?:\d{3})+$)/g,
|
|
|
|
/**
|
|
Takes a floating point number and convert it to a formatted string where
|
|
every thousand is separated by `orderSeparator`, with a `decimal` number of
|
|
decimals separated by `decimalSeparator`. The number returned is rounded
|
|
the usual way.
|
|
|
|
@member Backgrid.NumberFormatter
|
|
@param {number} number
|
|
@return {string}
|
|
*/
|
|
fromRaw: function (number) {
|
|
if (_.isNull(number) || _.isUndefined(number)) return '';
|
|
|
|
number = number.toFixed(~~this.decimals);
|
|
|
|
var parts = number.split('.');
|
|
var integerPart = parts[0];
|
|
var decimalPart = parts[1] ? (this.decimalSeparator || '.') + parts[1] : '';
|
|
|
|
return integerPart.replace(this.HUMANIZED_NUM_RE, '$1' + this.orderSeparator) + decimalPart;
|
|
},
|
|
|
|
/**
|
|
Takes a string, possibly formatted with `orderSeparator` and/or
|
|
`decimalSeparator`, and convert it back to a number.
|
|
|
|
@member Backgrid.NumberFormatter
|
|
@param {string} formattedData
|
|
@return {number|undefined} Undefined if the string cannot be converted to
|
|
a number.
|
|
*/
|
|
toRaw: function (formattedData) {
|
|
var rawData = '';
|
|
|
|
var thousands = formattedData.trim().split(this.orderSeparator);
|
|
for (var i = 0; i < thousands.length; i++) {
|
|
rawData += thousands[i];
|
|
}
|
|
|
|
var decimalParts = rawData.split(this.decimalSeparator);
|
|
rawData = '';
|
|
for (var i = 0; i < decimalParts.length; i++) {
|
|
rawData = rawData + decimalParts[i] + '.';
|
|
}
|
|
|
|
if (rawData[rawData.length - 1] === '.') {
|
|
rawData = rawData.slice(0, rawData.length - 1);
|
|
}
|
|
|
|
var result = (rawData * 1).toFixed(~~this.decimals) * 1;
|
|
if (_.isNumber(result) && !_.isNaN(result)) return result;
|
|
}
|
|
|
|
});
|
|
|
|
/**
|
|
Formatter to converts between various datetime formats.
|
|
|
|
This class only understands ISO-8601 formatted datetime strings and UNIX
|
|
offset (number of milliseconds since UNIX Epoch). See
|
|
Backgrid.Extension.MomentFormatter if you need a much more flexible datetime
|
|
formatter.
|
|
|
|
@class Backgrid.DatetimeFormatter
|
|
@extends Backgrid.CellFormatter
|
|
@constructor
|
|
@throws {Error} If both `includeDate` and `includeTime` are false.
|
|
*/
|
|
var DatetimeFormatter = Backgrid.DatetimeFormatter = function (options) {
|
|
options = options ? _.clone(options) : {};
|
|
_.extend(this, this.defaults, options);
|
|
|
|
if (!this.includeDate && !this.includeTime) {
|
|
throw new Error("Either includeDate or includeTime must be true");
|
|
}
|
|
};
|
|
DatetimeFormatter.prototype = new CellFormatter();
|
|
_.extend(DatetimeFormatter.prototype, {
|
|
|
|
/**
|
|
@member Backgrid.DatetimeFormatter
|
|
|
|
@cfg {Object} options
|
|
|
|
@cfg {boolean} [options.includeDate=true] Whether the values include the
|
|
date part.
|
|
|
|
@cfg {boolean} [options.includeTime=true] Whether the values include the
|
|
time part.
|
|
|
|
@cfg {boolean} [options.includeMilli=false] If `includeTime` is true,
|
|
whether to include the millisecond part, if it exists.
|
|
*/
|
|
defaults: {
|
|
includeDate: true,
|
|
includeTime: true,
|
|
includeMilli: false
|
|
},
|
|
|
|
DATE_RE: /^([+\-]?\d{4})-(\d{2})-(\d{2})$/,
|
|
TIME_RE: /^(\d{2}):(\d{2}):(\d{2})(\.(\d{3}))?$/,
|
|
ISO_SPLITTER_RE: /T|Z| +/,
|
|
|
|
_convert: function (data, validate) {
|
|
var date, time = null;
|
|
if (_.isNumber(data)) {
|
|
var jsDate = new Date(data);
|
|
date = lpad(jsDate.getUTCFullYear(), 4, 0) + '-' + lpad(jsDate.getUTCMonth() + 1, 2, 0) + '-' + lpad(jsDate.getUTCDate(), 2, 0);
|
|
time = lpad(jsDate.getUTCHours(), 2, 0) + ':' + lpad(jsDate.getUTCMinutes(), 2, 0) + ':' + lpad(jsDate.getUTCSeconds(), 2, 0);
|
|
}
|
|
else {
|
|
data = data.trim();
|
|
var parts = data.split(this.ISO_SPLITTER_RE) || [];
|
|
date = this.DATE_RE.test(parts[0]) ? parts[0] : '';
|
|
time = date && parts[1] ? parts[1] : this.TIME_RE.test(parts[0]) ? parts[0] : '';
|
|
}
|
|
|
|
var YYYYMMDD = this.DATE_RE.exec(date) || [];
|
|
var HHmmssSSS = this.TIME_RE.exec(time) || [];
|
|
|
|
if (validate) {
|
|
if (this.includeDate && _.isUndefined(YYYYMMDD[0])) return;
|
|
if (this.includeTime && _.isUndefined(HHmmssSSS[0])) return;
|
|
if (!this.includeDate && date) return;
|
|
if (!this.includeTime && time) return;
|
|
}
|
|
|
|
var jsDate = new Date(Date.UTC(YYYYMMDD[1] * 1 || 0,
|
|
YYYYMMDD[2] * 1 - 1 || 0,
|
|
YYYYMMDD[3] * 1 || 0,
|
|
HHmmssSSS[1] * 1 || null,
|
|
HHmmssSSS[2] * 1 || null,
|
|
HHmmssSSS[3] * 1 || null,
|
|
HHmmssSSS[5] * 1 || null));
|
|
|
|
var result = '';
|
|
|
|
if (this.includeDate) {
|
|
result = lpad(jsDate.getUTCFullYear(), 4, 0) + '-' + lpad(jsDate.getUTCMonth() + 1, 2, 0) + '-' + lpad(jsDate.getUTCDate(), 2, 0);
|
|
}
|
|
|
|
if (this.includeTime) {
|
|
result = result + (this.includeDate ? 'T' : '') + lpad(jsDate.getUTCHours(), 2, 0) + ':' + lpad(jsDate.getUTCMinutes(), 2, 0) + ':' + lpad(jsDate.getUTCSeconds(), 2, 0);
|
|
|
|
if (this.includeMilli) {
|
|
result = result + '.' + lpad(jsDate.getUTCMilliseconds(), 3, 0);
|
|
}
|
|
}
|
|
|
|
if (this.includeDate && this.includeTime) {
|
|
result += "Z";
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
Converts an ISO-8601 formatted datetime string to a datetime string, date
|
|
string or a time string. The timezone is ignored if supplied.
|
|
|
|
@member Backgrid.DatetimeFormatter
|
|
@param {string} rawData
|
|
@return {string|null|undefined} ISO-8601 string in UTC. Null and undefined
|
|
values are returned as is.
|
|
*/
|
|
fromRaw: function (rawData) {
|
|
if (_.isNull(rawData) || _.isUndefined(rawData)) return '';
|
|
return this._convert(rawData);
|
|
},
|
|
|
|
/**
|
|
Converts an ISO-8601 formatted datetime string to a datetime string, date
|
|
string or a time string. The timezone is ignored if supplied. This method
|
|
parses the input values exactly the same way as
|
|
Backgrid.Extension.MomentFormatter#fromRaw(), in addition to doing some
|
|
sanity checks.
|
|
|
|
@member Backgrid.DatetimeFormatter
|
|
@param {string} formattedData
|
|
@return {string|undefined} ISO-8601 string in UTC. Undefined if a date is
|
|
found when `includeDate` is false, or a time is found when `includeTime` is
|
|
false, or if `includeDate` is true and a date is not found, or if
|
|
`includeTime` is true and a time is not found.
|
|
*/
|
|
toRaw: function (formattedData) {
|
|
return this._convert(formattedData, true);
|
|
}
|
|
|
|
});
|
|
|
|
/**
|
|
Formatter to convert any value to string.
|
|
|
|
@class Backgrid.StringFormatter
|
|
@extends Backgrid.CellFormatter
|
|
@constructor
|
|
*/
|
|
var StringFormatter = Backgrid.StringFormatter = function () {};
|
|
StringFormatter.prototype = new CellFormatter();
|
|
_.extend(StringFormatter.prototype, {
|
|
/**
|
|
Converts any value to a string using Ecmascript's implicit type
|
|
conversion. If the given value is `null` or `undefined`, an empty string is
|
|
returned instead.
|
|
|
|
@member Backgrid.StringFormatter
|
|
@param {*} rawValue
|
|
@return {string}
|
|
*/
|
|
fromRaw: function (rawValue) {
|
|
if (_.isUndefined(rawValue) || _.isNull(rawValue)) return '';
|
|
return rawValue + '';
|
|
}
|
|
});
|
|
|
|
/**
|
|
Simple email validation formatter.
|
|
|
|
@class Backgrid.EmailFormatter
|
|
@extends Backgrid.CellFormatter
|
|
@constructor
|
|
*/
|
|
var EmailFormatter = Backgrid.EmailFormatter = function () {};
|
|
EmailFormatter.prototype = new CellFormatter();
|
|
_.extend(EmailFormatter.prototype, {
|
|
/**
|
|
Return the input if it is a string that contains an '@' character and if
|
|
the strings before and after '@' are non-empty. If the input does not
|
|
validate, `undefined` is returned.
|
|
|
|
@member Backgrid.EmailFormatter
|
|
@param {*} formattedData
|
|
@return {string|undefined}
|
|
*/
|
|
toRaw: function (formattedData) {
|
|
var parts = formattedData.trim().split("@");
|
|
if (parts.length === 2 && _.all(parts)) {
|
|
return formattedData;
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
Formatter for SelectCell.
|
|
|
|
@class Backgrid.SelectFormatter
|
|
@extends Backgrid.CellFormatter
|
|
@constructor
|
|
*/
|
|
var SelectFormatter = Backgrid.SelectFormatter = function () {};
|
|
SelectFormatter.prototype = new CellFormatter();
|
|
_.extend(SelectFormatter.prototype, {
|
|
|
|
/**
|
|
Normalizes raw scalar or array values to an array.
|
|
|
|
@member Backgrid.SelectFormatter
|
|
@param {*} rawValue
|
|
@return {Array.<*>}
|
|
*/
|
|
fromRaw: function (rawValue) {
|
|
return _.isArray(rawValue) ? rawValue : rawValue != null ? [rawValue] : [];
|
|
}
|
|
});
|
|
|
|
/*
|
|
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) {
|
|
Backgrid.requireOptions(options, ["formatter", "column", "model"]);
|
|
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) {
|
|
CellEditor.prototype.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 () {
|
|
this.$el.val(this.formatter.fromRaw(this.model.get(this.column.get("name"))));
|
|
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);
|
|
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=new CellFormatter()]
|
|
*/
|
|
formatter: new 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) {
|
|
Backgrid.requireOptions(options, ["model", "column"]);
|
|
this.column = options.column;
|
|
if (!(this.column instanceof Column)) {
|
|
this.column = new Column(this.column);
|
|
}
|
|
this.formatter = Backgrid.resolveNameToClass(this.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(this.model, "backgrid:error", this.renderError);
|
|
},
|
|
|
|
/**
|
|
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();
|
|
this.$el.text(this.formatter.fromRaw(this.model.get(this.column.get("name"))));
|
|
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;
|
|
|
|
if (column.get("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, arguments);
|
|
delete this.currentEditor;
|
|
}
|
|
return Backbone.View.prototype.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: new 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",
|
|
|
|
render: function () {
|
|
this.$el.empty();
|
|
var formattedValue = this.formatter.fromRaw(this.model.get(this.column.get("name")));
|
|
this.$el.append($("<a>", {
|
|
tabIndex: -1,
|
|
href: formattedValue,
|
|
title: formattedValue,
|
|
target: "_blank"
|
|
}).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: new EmailFormatter(),
|
|
|
|
render: function () {
|
|
this.$el.empty();
|
|
var formattedValue = this.formatter.fromRaw(this.model.get(this.column.get("name")));
|
|
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) {
|
|
Cell.prototype.initialize.apply(this, arguments);
|
|
this.formatter = new this.formatter({
|
|
decimals: this.decimals,
|
|
decimalSeparator: this.decimalSeparator,
|
|
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
|
|
});
|
|
|
|
/**
|
|
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) {
|
|
Cell.prototype.initialize.apply(this, arguments);
|
|
this.formatter = new this.formatter({
|
|
includeDate: this.includeDate,
|
|
includeTime: this.includeTime,
|
|
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 val = this.formatter.fromRaw(this.model.get(this.column.get("name")));
|
|
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.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.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();
|
|
this.$el.append($("<input>", {
|
|
tabIndex: -1,
|
|
type: "checkbox",
|
|
checked: this.formatter.fromRaw(this.model.get(this.column.get("name")))
|
|
}));
|
|
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>'),
|
|
|
|
setOptionValues: function (optionValues) {
|
|
this.optionValues = 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: selectedValues.indexOf(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 selectedValues = this.formatter.fromRaw(this.model.get(this.column.get("name")));
|
|
|
|
if (!_.isArray(optionValues)) throw 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: selectedValues.indexOf(optionValue) > -1
|
|
}));
|
|
}
|
|
else if (_.isObject(optionValue)) {
|
|
optgroupName = optionValue.name;
|
|
optgroup = $("<optgroup></optgroup>", { label: optgroupName });
|
|
optgroup.append(this._renderOptions(optionValue.values, selectedValues));
|
|
this.$el.append(optgroup);
|
|
}
|
|
else {
|
|
throw 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. Triggers a
|
|
`backgrid:edited` Backbone event from the model.
|
|
*/
|
|
save: function (e) {
|
|
var model = this.model;
|
|
var column = this.column;
|
|
model.set(column.get("name"), this.formatter.toRaw(this.$el.val()));
|
|
model.trigger("backgrid:edited", model, column, new Command(e));
|
|
},
|
|
|
|
/**
|
|
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();
|
|
if (e.type == "blur" && this.$el.find("option").length === 1) {
|
|
model.set(column.get("name"), this.formatter.toRaw(this.$el.val()));
|
|
}
|
|
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: new 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) {
|
|
Cell.prototype.initialize.apply(this, arguments);
|
|
Backgrid.requireOptions(this, ["optionValues"]);
|
|
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 = this.optionValues;
|
|
var rawData = this.formatter.fromRaw(this.model.get(this.column.get("name")));
|
|
|
|
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 TypeError("'optionValues' must be of type {Array.<Array>|Array.<{name: string, values: Array.<Array>}>}");
|
|
}
|
|
throw ex;
|
|
}
|
|
|
|
this.delegateEvents();
|
|
|
|
return this;
|
|
}
|
|
|
|
});
|
|
/*
|
|
backgrid
|
|
http://github.com/wyuenho/backgrid
|
|
|
|
Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
|
|
Licensed under the MIT @license.
|
|
*/
|
|
|
|
/**
|
|
A Column is a placeholder for column metadata.
|
|
|
|
You usually don't need to create an instance of this class yourself as a
|
|
collection of column instances will be created for you from a list of column
|
|
attributes in the Backgrid.js view class constructors.
|
|
|
|
@class Backgrid.Column
|
|
@extends Backbone.Model
|
|
*/
|
|
var Column = Backgrid.Column = Backbone.Model.extend({
|
|
|
|
defaults: {
|
|
name: undefined,
|
|
label: undefined,
|
|
sortable: true,
|
|
editable: true,
|
|
renderable: true,
|
|
formatter: undefined,
|
|
cell: undefined,
|
|
headerCell: undefined
|
|
},
|
|
|
|
/**
|
|
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 {Backgrid.CellFormatter|Object|string} [attrs.formatter] The
|
|
formatter to use to convert between raw model values and user input.
|
|
|
|
@throws {TypeError} If attrs.cell or attrs.options are not supplied.
|
|
@throws {ReferenceError} If attrs.cell is a string but a cell class of
|
|
said name cannot be found in the Backgrid module.
|
|
|
|
See:
|
|
|
|
- Backgrid.Cell
|
|
- Backgrid.CellFormatter
|
|
*/
|
|
initialize: function (attrs) {
|
|
Backgrid.requireOptions(attrs, ["cell", "name"]);
|
|
|
|
if (!this.has("label")) {
|
|
this.set({ label: this.get("name") }, { silent: true });
|
|
}
|
|
|
|
var headerCell = Backgrid.resolveNameToClass(this.get("headerCell"), "HeaderCell");
|
|
var cell = Backgrid.resolveNameToClass(this.get("cell"), "Cell");
|
|
this.set({ cell: cell, headerCell: headerCell }, { silent: true });
|
|
}
|
|
|
|
});
|
|
|
|
/**
|
|
A Backbone collection of Column instances.
|
|
|
|
@class Backgrid.Columns
|
|
@extends Backbone.Collection
|
|
*/
|
|
var Columns = Backgrid.Columns = Backbone.Collection.extend({
|
|
|
|
/**
|
|
@property {Backgrid.Column} model
|
|
*/
|
|
model: Column
|
|
});
|
|
/*
|
|
backgrid
|
|
http://github.com/wyuenho/backgrid
|
|
|
|
Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
|
|
Licensed under the MIT @license.
|
|
*/
|
|
|
|
/**
|
|
Row is a simple container view that takes a model instance and a list of
|
|
column metadata describing how each of the model's attribute is to be
|
|
rendered, and apply the appropriate cell to each attribute.
|
|
|
|
@class Backgrid.Row
|
|
@extends Backbone.View
|
|
*/
|
|
var Row = Backgrid.Row = Backbone.View.extend({
|
|
|
|
/** @property */
|
|
tagName: "tr",
|
|
|
|
requiredOptions: ["columns", "model"],
|
|
|
|
/**
|
|
Initializes a row view instance.
|
|
|
|
@param {Object} options
|
|
@param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata.
|
|
@param {Backbone.Model} options.model The model instance to render.
|
|
|
|
@throws {TypeError} If options.columns or options.model is undefined.
|
|
*/
|
|
initialize: function (options) {
|
|
|
|
Backgrid.requireOptions(options, this.requiredOptions);
|
|
|
|
var columns = this.columns = options.columns;
|
|
if (!(columns instanceof Backbone.Collection)) {
|
|
columns = this.columns = new Columns(columns);
|
|
}
|
|
|
|
var cells = this.cells = [];
|
|
for (var i = 0; i < columns.length; i++) {
|
|
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);
|
|
}
|
|
else if (i === columns.length - 1) {
|
|
$el.append(cell.render().$el);
|
|
}
|
|
else {
|
|
$el.children().eq(i).before(cell.render().$el);
|
|
}
|
|
});
|
|
|
|
this.listenTo(columns, "remove", function (column, columns, opts) {
|
|
cells[opts.index].remove();
|
|
cells.splice(opts.index, 1);
|
|
});
|
|
},
|
|
|
|
/**
|
|
Factory method for making a cell. Used by #initialize internally. Override
|
|
this to provide an appropriate cell instance for a custom Row subclass.
|
|
|
|
@protected
|
|
|
|
@param {Backgrid.Column} column
|
|
@param {Object} options The options passed to #initialize.
|
|
|
|
@return {Backgrid.Cell}
|
|
*/
|
|
makeCell: function (column) {
|
|
return new (column.get("cell"))({
|
|
column: column,
|
|
model: this.model
|
|
});
|
|
},
|
|
|
|
/**
|
|
Renders a row of cells for this row's model.
|
|
*/
|
|
render: function () {
|
|
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();
|
|
}
|
|
|
|
this.el.appendChild(fragment);
|
|
|
|
this.delegateEvents();
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
Clean up this row and its cells.
|
|
|
|
@chainable
|
|
*/
|
|
remove: function () {
|
|
for (var i = 0; i < this.cells.length; i++) {
|
|
var cell = this.cells[i];
|
|
cell.remove.apply(cell, arguments);
|
|
}
|
|
return Backbone.View.prototype.remove.apply(this, arguments);
|
|
}
|
|
|
|
});
|
|
|
|
/**
|
|
EmptyRow is a simple container view that takes a list of column and render a
|
|
row with a single column.
|
|
|
|
@class Backgrid.EmptyRow
|
|
@extends Backbone.View
|
|
*/
|
|
var EmptyRow = Backgrid.EmptyRow = Backbone.View.extend({
|
|
|
|
/** @property */
|
|
tagName: "tr",
|
|
|
|
/** @property */
|
|
emptyText: null,
|
|
|
|
/**
|
|
Initializer.
|
|
|
|
@param {Object} options
|
|
@param {string} options.emptyText
|
|
@param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata.
|
|
*/
|
|
initialize: function (options) {
|
|
Backgrid.requireOptions(options, ["emptyText", "columns"]);
|
|
|
|
this.emptyText = options.emptyText;
|
|
this.columns = options.columns;
|
|
},
|
|
|
|
/**
|
|
Renders an empty row.
|
|
*/
|
|
render: function () {
|
|
this.$el.empty();
|
|
|
|
var td = document.createElement("td");
|
|
td.setAttribute("colspan", this.columns.length);
|
|
td.textContent = this.emptyText;
|
|
|
|
this.el.setAttribute("class", "empty");
|
|
this.el.appendChild(td);
|
|
|
|
return this;
|
|
}
|
|
});
|
|
/*
|
|
backgrid
|
|
http://github.com/wyuenho/backgrid
|
|
|
|
Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
|
|
Licensed under the MIT @license.
|
|
*/
|
|
|
|
/**
|
|
HeaderCell is a special cell class that renders a column header cell. If the
|
|
column is sortable, a sorter is also rendered and will trigger a table
|
|
refresh after sorting.
|
|
|
|
@class Backgrid.HeaderCell
|
|
@extends Backbone.View
|
|
*/
|
|
var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({
|
|
|
|
/** @property */
|
|
tagName: "th",
|
|
|
|
/** @property */
|
|
events: {
|
|
"click a": "onClick"
|
|
},
|
|
|
|
/**
|
|
@property {null|"ascending"|"descending"} _direction The current sorting
|
|
direction of this column.
|
|
*/
|
|
_direction: null,
|
|
|
|
/**
|
|
Initializer.
|
|
|
|
@param {Object} options
|
|
@param {Backgrid.Column|Object} options.column
|
|
|
|
@throws {TypeError} If options.column or options.collection is undefined.
|
|
*/
|
|
initialize: function (options) {
|
|
Backgrid.requireOptions(options, ["column", "collection"]);
|
|
this.column = options.column;
|
|
if (!(this.column instanceof Column)) {
|
|
this.column = new Column(this.column);
|
|
}
|
|
this.listenTo(this.collection, "backgrid:sort", this._resetCellDirection);
|
|
},
|
|
|
|
/**
|
|
Gets or sets the direction of this cell. If called directly without
|
|
parameters, returns the current direction of this cell, otherwise sets
|
|
it. If a `null` is given, sets this cell back to the default order.
|
|
|
|
@param {null|"ascending"|"descending"} dir
|
|
@return {null|string} The current direction or the changed direction.
|
|
*/
|
|
direction: function (dir) {
|
|
if (arguments.length) {
|
|
if (this._direction) this.$el.removeClass(this._direction);
|
|
if (dir) this.$el.addClass(dir);
|
|
this._direction = dir;
|
|
}
|
|
|
|
return this._direction;
|
|
},
|
|
|
|
/**
|
|
Event handler for the Backbone `backgrid:sort` event. Resets this cell's
|
|
direction to default if sorting is being done on another column.
|
|
|
|
@private
|
|
*/
|
|
_resetCellDirection: function (sortByColName, direction, comparator, collection) {
|
|
if (collection == this.collection) {
|
|
if (sortByColName !== this.column.get("name")) this.direction(null);
|
|
else this.direction(direction);
|
|
}
|
|
},
|
|
|
|
/**
|
|
Event handler for the `click` event on the cell's anchor. If the column is
|
|
sortable, clicking on the anchor will cycle through 3 sorting orderings -
|
|
`ascending`, `descending`, and default.
|
|
*/
|
|
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;
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
If the underlying collection is a Backbone.PageableCollection in
|
|
server-mode or infinite-mode, a page of models is fetched after sorting is
|
|
done on the server.
|
|
|
|
If the underlying collection is a Backbone.PageableCollection in
|
|
client-mode, or any
|
|
[Backbone.Collection](http://backbonejs.org/#Collection) instance, sorting
|
|
is done on the client side. If the collection is an instance of a
|
|
Backbone.PageableCollection, sorting will be done globally on all the pages
|
|
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.
|
|
|
|
@param {string} columnName
|
|
@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;
|
|
|
|
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);
|
|
|
|
if (collection.mode == "client") {
|
|
if (!collection.fullCollection.comparator) {
|
|
collection.fullCollection.comparator = comparator;
|
|
}
|
|
collection.fullCollection.sort();
|
|
}
|
|
else collection.fetch({reset: true});
|
|
}
|
|
else {
|
|
collection.comparator = comparator;
|
|
collection.sort();
|
|
}
|
|
|
|
this.collection.trigger("backgrid:sort", columnName, 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.
|
|
|
|
@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 0;
|
|
},
|
|
|
|
/**
|
|
Renders a header cell with a sorter and a label.
|
|
*/
|
|
render: function () {
|
|
this.$el.empty();
|
|
var $label = $("<a>").text(this.column.get("label")).append("<b class='sort-caret'></b>");
|
|
this.$el.append($label);
|
|
this.delegateEvents();
|
|
return this;
|
|
}
|
|
|
|
});
|
|
|
|
/**
|
|
HeaderRow is a controller for a row of header cells.
|
|
|
|
@class Backgrid.HeaderRow
|
|
@extends Backgrid.Row
|
|
*/
|
|
var HeaderRow = Backgrid.HeaderRow = Backgrid.Row.extend({
|
|
|
|
requiredOptions: ["columns", "collection"],
|
|
|
|
/**
|
|
Initializer.
|
|
|
|
@param {Object} options
|
|
@param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns
|
|
@param {Backgrid.HeaderCell} [options.headerCell] Customized default
|
|
HeaderCell for all the columns. Supply a HeaderCell class or instance to a
|
|
the `headerCell` key in a column definition for column-specific header
|
|
rendering.
|
|
|
|
@throws {TypeError} If options.columns or options.collection is undefined.
|
|
*/
|
|
initialize: function () {
|
|
Backgrid.Row.prototype.initialize.apply(this, arguments);
|
|
},
|
|
|
|
makeCell: function (column, options) {
|
|
var headerCell = column.get("headerCell") || options.headerCell || HeaderCell;
|
|
headerCell = new headerCell({
|
|
column: column,
|
|
collection: this.collection
|
|
});
|
|
return headerCell;
|
|
}
|
|
|
|
});
|
|
|
|
/**
|
|
Header is a special structural view class that renders a table head with a
|
|
single row of header cells.
|
|
|
|
@class Backgrid.Header
|
|
@extends Backbone.View
|
|
*/
|
|
var Header = Backgrid.Header = Backbone.View.extend({
|
|
|
|
/** @property */
|
|
tagName: "thead",
|
|
|
|
/**
|
|
Initializer. Initializes this table head view to contain a single header
|
|
row view.
|
|
|
|
@param {Object} options
|
|
@param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata.
|
|
@param {Backbone.Model} options.model The model instance to render.
|
|
|
|
@throws {TypeError} If options.columns or options.model is undefined.
|
|
*/
|
|
initialize: function (options) {
|
|
Backgrid.requireOptions(options, ["columns", "collection"]);
|
|
|
|
this.columns = options.columns;
|
|
if (!(this.columns instanceof Backbone.Collection)) {
|
|
this.columns = new Columns(this.columns);
|
|
}
|
|
|
|
this.row = new Backgrid.HeaderRow({
|
|
columns: this.columns,
|
|
collection: this.collection
|
|
});
|
|
},
|
|
|
|
/**
|
|
Renders this table head with a single row of header cells.
|
|
*/
|
|
render: function () {
|
|
this.$el.append(this.row.render().$el);
|
|
this.delegateEvents();
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
Clean up this header and its row.
|
|
|
|
@chainable
|
|
*/
|
|
remove: function () {
|
|
this.row.remove.apply(this.row, arguments);
|
|
return Backbone.View.prototype.remove.apply(this, arguments);
|
|
}
|
|
|
|
});
|
|
/*
|
|
backgrid
|
|
http://github.com/wyuenho/backgrid
|
|
|
|
Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
|
|
Licensed under the MIT @license.
|
|
*/
|
|
|
|
/**
|
|
Body is the table body which contains the rows inside a table. Body is
|
|
responsible for refreshing the rows after sorting, insertion and removal.
|
|
|
|
@class Backgrid.Body
|
|
@extends Backbone.View
|
|
*/
|
|
var Body = Backgrid.Body = Backbone.View.extend({
|
|
|
|
/** @property */
|
|
tagName: "tbody",
|
|
|
|
/**
|
|
Initializer.
|
|
|
|
@param {Object} options
|
|
@param {Backbone.Collection} options.collection
|
|
@param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns
|
|
Column metadata.
|
|
@param {Backgrid.Row} [options.row=Backgrid.Row] The Row class to use.
|
|
@param {string} [options.emptyText] The text to display in the empty row.
|
|
|
|
@throws {TypeError} If options.columns or options.collection is undefined.
|
|
|
|
See Backgrid.Row.
|
|
*/
|
|
initialize: function (options) {
|
|
Backgrid.requireOptions(options, ["columns", "collection"]);
|
|
|
|
this.columns = options.columns;
|
|
if (!(this.columns instanceof Backbone.Collection)) {
|
|
this.columns = new Columns(this.columns);
|
|
}
|
|
|
|
this.row = options.row || Row;
|
|
this.rows = this.collection.map(function (model) {
|
|
var row = new this.row({
|
|
columns: this.columns,
|
|
model: model
|
|
});
|
|
|
|
return row;
|
|
}, this);
|
|
|
|
this.emptyText = options.emptyText;
|
|
this._unshiftEmptyRowMayBe();
|
|
|
|
var collection = this.collection;
|
|
this.listenTo(collection, "add", this.insertRow);
|
|
this.listenTo(collection, "remove", this.removeRow);
|
|
this.listenTo(collection, "sort", this.refresh);
|
|
this.listenTo(collection, "reset", this.refresh);
|
|
this.listenTo(collection, "backgrid:edited", this.moveToNextCell);
|
|
},
|
|
|
|
_unshiftEmptyRowMayBe: function () {
|
|
if (this.rows.length === 0 && this.emptyText != null) {
|
|
this.rows.unshift(new EmptyRow({
|
|
emptyText: this.emptyText,
|
|
columns: this.columns
|
|
}));
|
|
}
|
|
},
|
|
|
|
/**
|
|
This method can be called either directly or as a callback to a
|
|
[Backbone.Collecton#add](http://backbonejs.org/#Collection-add) event.
|
|
|
|
When called directly, it accepts a model or an array of models and an
|
|
option hash just like
|
|
[Backbone.Collection#add](http://backbonejs.org/#Collection-add) and
|
|
delegates to it. Once the model is added, a new row is inserted into the
|
|
body and automatically rendered.
|
|
|
|
When called as a callback of an `add` event, splices a new row into the
|
|
body and renders it.
|
|
|
|
@param {Backbone.Model} model The model to render as a row.
|
|
@param {Backbone.Collection} collection When called directly, this
|
|
parameter is actually the options to
|
|
[Backbone.Collection#add](http://backbonejs.org/#Collection-add).
|
|
@param {Object} options When called directly, this must be null.
|
|
|
|
See:
|
|
|
|
- [Backbone.Collection#add](http://backbonejs.org/#Collection-add)
|
|
*/
|
|
insertRow: function (model, collection, options) {
|
|
|
|
if (this.rows[0] instanceof EmptyRow) this.rows.pop().remove();
|
|
|
|
// insertRow() is called directly
|
|
if (!(collection instanceof Backbone.Collection) && !options) {
|
|
this.collection.add(model, (options = collection));
|
|
return;
|
|
}
|
|
|
|
options = _.extend({render: true}, options || {});
|
|
|
|
var row = new this.row({
|
|
columns: this.columns,
|
|
model: model
|
|
});
|
|
|
|
var index = collection.indexOf(model);
|
|
this.rows.splice(index, 0, row);
|
|
|
|
var $el = this.$el;
|
|
var $children = $el.children();
|
|
var $rowEl = row.render().$el;
|
|
|
|
if (options.render) {
|
|
if (index >= $children.length) {
|
|
$el.append($rowEl);
|
|
}
|
|
else {
|
|
$children.eq(index).before($rowEl);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
The method can be called either directly or as a callback to a
|
|
[Backbone.Collection#remove](http://backbonejs.org/#Collection-remove)
|
|
event.
|
|
|
|
When called directly, it accepts a model or an array of models and an
|
|
option hash just like
|
|
[Backbone.Collection#remove](http://backbonejs.org/#Collection-remove) and
|
|
delegates to it. Once the model is removed, a corresponding row is removed
|
|
from the body.
|
|
|
|
When called as a callback of a `remove` event, splices into the rows and
|
|
removes the row responsible for rendering the model.
|
|
|
|
@param {Backbone.Model} model The model to remove from the body.
|
|
@param {Backbone.Collection} collection When called directly, this
|
|
parameter is actually the options to
|
|
[Backbone.Collection#remove](http://backbonejs.org/#Collection-remove).
|
|
@param {Object} options When called directly, this must be null.
|
|
|
|
See:
|
|
|
|
- [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove)
|
|
*/
|
|
removeRow: function (model, collection, options) {
|
|
|
|
// removeRow() is called directly
|
|
if (!options) {
|
|
this.collection.remove(model, (options = collection));
|
|
this._unshiftEmptyRowMayBe();
|
|
return;
|
|
}
|
|
|
|
if (_.isUndefined(options.render) || options.render) {
|
|
this.rows[options.index].remove();
|
|
}
|
|
|
|
this.rows.splice(options.index, 1);
|
|
this._unshiftEmptyRowMayBe();
|
|
},
|
|
|
|
/**
|
|
Reinitialize all the rows inside the body and re-render them. Triggers a
|
|
Backbone `backgrid:refresh` event from the collection along with the body
|
|
instance as its sole parameter when done.
|
|
*/
|
|
refresh: function () {
|
|
for (var i = 0; i < this.rows.length; i++) {
|
|
this.rows[i].remove();
|
|
}
|
|
|
|
this.rows = this.collection.map(function (model) {
|
|
var row = new this.row({
|
|
columns: this.columns,
|
|
model: model
|
|
});
|
|
|
|
return row;
|
|
}, this);
|
|
this._unshiftEmptyRowMayBe();
|
|
|
|
this.render();
|
|
|
|
this.collection.trigger("backgrid:refresh", this);
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
Renders all the rows inside this body. If the collection is empty and
|
|
`options.emptyText` is defined and not null in the constructor, an empty
|
|
row is rendered, otherwise no row is rendered.
|
|
*/
|
|
render: function () {
|
|
this.$el.empty();
|
|
|
|
var fragment = document.createDocumentFragment();
|
|
for (var i = 0; i < this.rows.length; i++) {
|
|
var row = this.rows[i];
|
|
fragment.appendChild(row.render().el);
|
|
}
|
|
|
|
this.el.appendChild(fragment);
|
|
|
|
this.delegateEvents();
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
Clean up this body and it's rows.
|
|
|
|
@chainable
|
|
*/
|
|
remove: function () {
|
|
for (var i = 0; i < this.rows.length; i++) {
|
|
var row = this.rows[i];
|
|
row.remove.apply(row, arguments);
|
|
}
|
|
return Backbone.View.prototype.remove.apply(this, arguments);
|
|
},
|
|
|
|
/**
|
|
Moves focus to the next renderable and editable cell and return the
|
|
currently editing cell to display mode.
|
|
|
|
@param {Backbone.Model} model The originating model
|
|
@param {Backgrid.Column} column The originating model column
|
|
@param {Backgrid.Command} command The Command object constructed from a DOM
|
|
Event
|
|
*/
|
|
moveToNextCell: function (model, column, command) {
|
|
var i = this.collection.indexOf(model);
|
|
var j = this.columns.indexOf(column);
|
|
|
|
if (command.moveUp() || command.moveDown() || command.moveLeft() ||
|
|
command.moveRight() || command.save()) {
|
|
var l = this.columns.length;
|
|
var maxOffset = l * this.collection.length;
|
|
|
|
if (command.moveUp() || command.moveDown()) {
|
|
var row = this.rows[i + (command.moveUp() ? -1 : 1)];
|
|
if (row) row.cells[j].enterEditMode();
|
|
}
|
|
else if (command.moveLeft() || command.moveRight()) {
|
|
var right = command.moveRight();
|
|
for (var offset = i * l + j + (right ? 1 : -1);
|
|
offset >= 0 && offset < maxOffset;
|
|
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.enterEditMode();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
this.rows[i].cells[j].exitEditMode();
|
|
}
|
|
});
|
|
/*
|
|
backgrid
|
|
http://github.com/wyuenho/backgrid
|
|
|
|
Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
|
|
Licensed under the MIT @license.
|
|
*/
|
|
|
|
/**
|
|
A Footer is a generic class that only defines a default tag `tfoot` and
|
|
number of required parameters in the initializer.
|
|
|
|
@abstract
|
|
@class Backgrid.Footer
|
|
@extends Backbone.View
|
|
*/
|
|
var Footer = Backgrid.Footer = Backbone.View.extend({
|
|
|
|
/** @property */
|
|
tagName: "tfoot",
|
|
|
|
/**
|
|
Initializer.
|
|
|
|
@param {Object} options
|
|
@param {*} options.parent The parent view class of this footer.
|
|
@param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns
|
|
Column metadata.
|
|
@param {Backbone.Collection} options.collection
|
|
|
|
@throws {TypeError} If options.columns or options.collection is undefined.
|
|
*/
|
|
initialize: function (options) {
|
|
Backgrid.requireOptions(options, ["columns", "collection"]);
|
|
this.columns = options.columns;
|
|
if (!(this.columns instanceof Backbone.Collection)) {
|
|
this.columns = new Backgrid.Columns(this.columns);
|
|
}
|
|
}
|
|
|
|
});
|
|
/*
|
|
backgrid
|
|
http://github.com/wyuenho/backgrid
|
|
|
|
Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
|
|
Licensed under the MIT @license.
|
|
*/
|
|
|
|
/**
|
|
Grid represents a data grid that has a header, body and an optional footer.
|
|
|
|
By default, a Grid treats each model in a collection as a row, and each
|
|
attribute in a model as a column. To render a grid you must provide a list of
|
|
column metadata and a collection to the Grid constructor. Just like any
|
|
Backbone.View class, the grid is rendered as a DOM node fragment when you
|
|
call render().
|
|
|
|
var grid = Backgrid.Grid({
|
|
columns: [{ name: "id", label: "ID", type: "string" },
|
|
// ...
|
|
],
|
|
collections: books
|
|
});
|
|
|
|
$("#table-container").append(grid.render().el);
|
|
|
|
Optionally, if you want to customize the rendering of the grid's header and
|
|
footer, you may choose to extend Backgrid.Header and Backgrid.Footer, and
|
|
then supply that class or an instance of that class to the Grid constructor.
|
|
See the documentation for Header and Footer for further details.
|
|
|
|
var grid = Backgrid.Grid({
|
|
columns: [{ name: "id", label: "ID", type: "string" }],
|
|
collections: books,
|
|
header: Backgrid.Header.extend({
|
|
//...
|
|
}),
|
|
footer: Backgrid.Paginator
|
|
});
|
|
|
|
Finally, if you want to override how the rows are rendered in the table body,
|
|
you can supply a Body subclass as the `body` attribute that uses a different
|
|
Row class.
|
|
|
|
@class Backgrid.Grid
|
|
@extends Backbone.View
|
|
|
|
See:
|
|
|
|
- Backgrid.Column
|
|
- Backgrid.Header
|
|
- Backgrid.Body
|
|
- Backgrid.Row
|
|
- Backgrid.Footer
|
|
*/
|
|
var Grid = Backgrid.Grid = Backbone.View.extend({
|
|
|
|
/** @property */
|
|
tagName: "table",
|
|
|
|
/** @property */
|
|
className: "backgrid",
|
|
|
|
/** @property */
|
|
header: Header,
|
|
|
|
/** @property */
|
|
body: Body,
|
|
|
|
/** @property */
|
|
footer: null,
|
|
|
|
/**
|
|
Initializes a Grid instance.
|
|
|
|
@param {Object} options
|
|
@param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata.
|
|
@param {Backbone.Collection} options.collection The collection of tabular model data to display.
|
|
@param {Backgrid.Header} [options.header=Backgrid.Header] An optional Header class to override the default.
|
|
@param {Backgrid.Body} [options.body=Backgrid.Body] An optional Body class to override the default.
|
|
@param {Backgrid.Row} [options.row=Backgrid.Row] An optional Row class to override the default.
|
|
@param {Backgrid.Footer} [options.footer=Backgrid.Footer] An optional Footer class.
|
|
*/
|
|
initialize: function (options) {
|
|
Backgrid.requireOptions(options, ["columns", "collection"]);
|
|
|
|
// Convert the list of column objects here first so the subviews don't have
|
|
// to.
|
|
if (!(options.columns instanceof Backbone.Collection)) {
|
|
options.columns = new Columns(options.columns);
|
|
}
|
|
this.columns = options.columns;
|
|
|
|
var passedThruOptions = _.omit(options, ["el", "id", "attributes",
|
|
"className", "tagName", "events"]);
|
|
|
|
this.header = options.header || this.header;
|
|
this.header = new this.header(passedThruOptions);
|
|
|
|
this.body = options.body || this.body;
|
|
this.body = new this.body(passedThruOptions);
|
|
|
|
this.footer = options.footer || this.footer;
|
|
if (this.footer) {
|
|
this.footer = new this.footer(passedThruOptions);
|
|
}
|
|
|
|
this.listenTo(this.columns, "reset", function () {
|
|
this.header = new (this.header.remove().constructor)(passedThruOptions);
|
|
this.body = new (this.body.remove().constructor)(passedThruOptions);
|
|
if (this.footer) {
|
|
this.footer = new (this.footer.remove().constructor)(passedThruOptions);
|
|
}
|
|
this.render();
|
|
});
|
|
},
|
|
|
|
/**
|
|
Delegates to Backgrid.Body#insertRow.
|
|
*/
|
|
insertRow: function (model, collection, options) {
|
|
return this.body.insertRow(model, collection, options);
|
|
},
|
|
|
|
/**
|
|
Delegates to Backgrid.Body#removeRow.
|
|
*/
|
|
removeRow: function (model, collection, options) {
|
|
return this.body.removeRow(model, collection, options);
|
|
},
|
|
|
|
/**
|
|
Delegates to Backgrid.Columns#add for adding a column. Subviews can listen
|
|
to the `add` event from their internal `columns` if rerendering needs to
|
|
happen.
|
|
|
|
@param {Object} [options] Options for `Backgrid.Columns#add`.
|
|
@param {boolean} [options.render=true] Whether to render the column
|
|
immediately after insertion.
|
|
|
|
@chainable
|
|
*/
|
|
insertColumn: function (column, options) {
|
|
options = options || {render: true};
|
|
this.columns.add(column, options);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
Delegates to Backgrid.Columns#remove for removing a column. Subviews can
|
|
listen to the `remove` event from the internal `columns` if rerendering
|
|
needs to happen.
|
|
|
|
@param {Object} [options] Options for `Backgrid.Columns#remove`.
|
|
|
|
@chainable
|
|
*/
|
|
removeColumn: function (column, options) {
|
|
this.columns.remove(column, options);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
Renders the grid's header, then footer, then finally the body. Triggers a
|
|
Backbone `backgrid:rendered` event along with a reference to the grid when
|
|
the it has successfully been rendered.
|
|
*/
|
|
render: function () {
|
|
this.$el.empty();
|
|
|
|
this.$el.append(this.header.render().$el);
|
|
|
|
if (this.footer) {
|
|
this.$el.append(this.footer.render().$el);
|
|
}
|
|
|
|
this.$el.append(this.body.render().$el);
|
|
|
|
this.delegateEvents();
|
|
|
|
this.trigger("backgrid:rendered", this);
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
Clean up this grid and its subviews.
|
|
|
|
@chainable
|
|
*/
|
|
remove: function () {
|
|
this.header.remove.apply(this.header, arguments);
|
|
this.body.remove.apply(this.body, arguments);
|
|
this.footer && this.footer.remove.apply(this.footer, arguments);
|
|
return Backbone.View.prototype.remove.apply(this, arguments);
|
|
}
|
|
|
|
});
|
|
|
|
}(this, jQuery, _, Backbone)); |