/lib/backgrid.js
JavaScript | 2531 lines | 1117 code | 328 blank | 1086 comment | 238 complexity | 39f78fb6e0e210d7c7dcd6173e5cbdf1 MD5 | raw file
Possible License(s): MIT
- /*
- 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));