| /*! |
| backgrid |
| http://github.com/wyuenho/backgrid |
| |
| Copyright (c) 2014 Jimmy Yuen Ho Wong and contributors <wyuenho@gmail.com> |
| Licensed under the MIT license. |
| */ |
| |
| (function (factory) { |
| |
| // CommonJS |
| if (typeof exports == "object") { |
| module.exports = factory(module.exports, |
| require("underscore"), |
| require("backbone")); |
| } |
| // Browser |
| else factory(this, this._, this.Backbone); |
| }(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. |
| */ |
| |
| // 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 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 $ = Backbone.$; |
| |
| var Backgrid = root.Backgrid = { |
| |
| Extension: {}, |
| |
| resolveNameToClass: function (name, suffix) { |
| if (_.isString(name)) { |
| var key = _.map(name.split('-'), function (e) { |
| return e.slice(0, 1).toUpperCase() + e.slice(1); |
| }).join('') + suffix; |
| var klass = Backgrid[key] || Backgrid.Extension[key]; |
| if (_.isUndefined(klass)) { |
| throw new ReferenceError("Class '" + key + "' not found"); |
| } |
| return klass; |
| } |
| |
| return name; |
| }, |
| |
| callByNeed: function () { |
| var value = arguments[0]; |
| if (!_.isFunction(value)) return value; |
| |
| var context = arguments[1]; |
| var args = [].slice.call(arguments, 2); |
| return value.apply(context, !!(args + '') ? args : []); |
| } |
| |
| }; |
| _.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 |
| @param {Backbone.Model} model Used for more complicated formatting |
| @return {*} |
| */ |
| fromRaw: function (rawData, model) { |
| 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 |
| @param {Backbone.Model} model Used for more complicated formatting |
| @return {*|undefined} |
| */ |
| toRaw: function (formattedData, model) { |
| return formattedData; |
| } |
| |
| }); |
| |
| /** |
| A floating point number formatter. Doesn't understand scientific notation at |
| the moment. |
| |
| @class Backgrid.NumberFormatter |
| @extends Backgrid.CellFormatter |
| @constructor |
| @throws {RangeError} If decimals < 0 or > 20. |
| */ |
| var NumberFormatter = Backgrid.NumberFormatter = function (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 |
| @param {Backbone.Model} model Used for more complicated formatting |
| @return {string} |
| */ |
| fromRaw: function (number, model) { |
| 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 |
| @param {Backbone.Model} model Used for more complicated formatting |
| @return {number|undefined} Undefined if the string cannot be converted to |
| a number. |
| */ |
| toRaw: function (formattedData, model) { |
| formattedData = formattedData.trim(); |
| |
| if (formattedData === '') return null; |
| |
| var rawData = ''; |
| |
| var thousands = formattedData.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; |
| } |
| |
| }); |
| |
| /** |
| A number formatter that converts a floating point number, optionally |
| multiplied by a multiplier, to a percentage string and vice versa. |
| |
| @class Backgrid.PercentFormatter |
| @extends Backgrid.NumberFormatter |
| @constructor |
| @throws {RangeError} If decimals < 0 or > 20. |
| */ |
| var PercentFormatter = Backgrid.PercentFormatter = function () { |
| Backgrid.NumberFormatter.apply(this, arguments); |
| }; |
| |
| PercentFormatter.prototype = new Backgrid.NumberFormatter(), |
| |
| _.extend(PercentFormatter.prototype, { |
| |
| /** |
| @member Backgrid.PercentFormatter |
| @cfg {Object} options |
| |
| @cfg {number} [options.multiplier=1] The number used to multiply the model |
| value for display. |
| |
| @cfg {string} [options.symbol='%'] The symbol to append to the percentage |
| string. |
| */ |
| defaults: _.extend({}, NumberFormatter.prototype.defaults, { |
| multiplier: 1, |
| symbol: "%" |
| }), |
| |
| /** |
| Takes a floating point number, where the number is first multiplied by |
| `multiplier`, then converted to a formatted string like |
| NumberFormatter#fromRaw, then finally append `symbol` to the end. |
| |
| @member Backgrid.PercentFormatter |
| @param {number} rawValue |
| @param {Backbone.Model} model Used for more complicated formatting |
| @return {string} |
| */ |
| fromRaw: function (number, model) { |
| var args = [].slice.call(arguments, 1); |
| args.unshift(number * this.multiplier); |
| return (NumberFormatter.prototype.fromRaw.apply(this, args) || "0") + this.symbol; |
| }, |
| |
| /** |
| Takes a string, possibly appended with `symbol` and/or `decimalSeparator`, |
| and convert it back to a number for the model like NumberFormatter#toRaw, |
| and then dividing it by `multiplier`. |
| |
| @member Backgrid.PercentFormatter |
| @param {string} formattedData |
| @param {Backbone.Model} model Used for more complicated formatting |
| @return {number|undefined} Undefined if the string cannot be converted to |
| a number. |
| */ |
| toRaw: function (formattedValue, model) { |
| var tokens = formattedValue.split(this.symbol); |
| if (tokens && tokens[0] && tokens[1] === "" || tokens[1] == null) { |
| var rawValue = NumberFormatter.prototype.toRaw.call(this, tokens[0]); |
| if (_.isUndefined(rawValue)) return rawValue; |
| return rawValue / this.multiplier; |
| } |
| } |
| |
| }); |
| |
| /** |
| 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) { |
| _.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) { |
| if ((data + '').trim() === '') return null; |
| |
| 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 |
| @param {Backbone.Model} model Used for more complicated formatting |
| @return {string|null|undefined} ISO-8601 string in UTC. Null and undefined |
| values are returned as is. |
| */ |
| fromRaw: function (rawData, model) { |
| 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 |
| @param {Backbone.Model} model Used for more complicated formatting |
| @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, model) { |
| 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 |
| @param {Backbone.Model} model Used for more complicated formatting |
| @return {string} |
| */ |
| fromRaw: function (rawValue, model) { |
| 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 |
| @param {Backbone.Model} model Used for more complicated formatting |
| @return {string|undefined} |
| */ |
| toRaw: function (formattedData, model) { |
| var parts = formattedData.trim().split("@"); |
| if (parts.length === 2 && _.all(parts)) { |
| return formattedData; |
| } |
| } |
| }); |
| |
| /** |
| Formatter for SelectCell. |
| |
| If the type of a model value is not a string, it is expected that a subclass |
| of this formatter is provided to the SelectCell, with #toRaw overridden to |
| convert the string value returned from the DOM back to whatever value is |
| expected in the model. |
| |
| @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 |
| @param {Backbone.Model} model Used for more complicated formatting |
| @return {Array.<*>} |
| */ |
| fromRaw: function (rawValue, model) { |
| 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) { |
| this.formatter = options.formatter; |
| this.column = options.column; |
| if (!(this.column instanceof Column)) { |
| this.column = new Column(this.column); |
| } |
| |
| this.listenTo(this.model, "backgrid:editing", this.postRender); |
| }, |
| |
| /** |
| Post-rendering setup and initialization. Focuses the cell editor's `el` in |
| this default implementation. **Should** be called by Cell classes after |
| calling Backgrid.CellEditor#render. |
| */ |
| postRender: function (model, column) { |
| if (column == null || column.get("name") == this.column.get("name")) { |
| this.$el.focus(); |
| } |
| return this; |
| } |
| |
| }); |
| |
| /** |
| InputCellEditor the cell editor type used by most core cell types. This cell |
| editor renders a text input box as its editor. The input will render a |
| placeholder if the value is empty on supported browsers. |
| |
| @class Backgrid.InputCellEditor |
| @extends Backgrid.CellEditor |
| */ |
| var InputCellEditor = Backgrid.InputCellEditor = CellEditor.extend({ |
| |
| /** @property */ |
| tagName: "input", |
| |
| /** @property */ |
| attributes: { |
| type: "text" |
| }, |
| |
| /** @property */ |
| events: { |
| "blur": "saveOrCancel", |
| "keydown": "saveOrCancel" |
| }, |
| |
| /** |
| Initializer. Removes this `el` from the DOM when a `done` event is |
| triggered. |
| |
| @param {Object} options |
| @param {Backgrid.CellFormatter} options.formatter |
| @param {Backgrid.Column} options.column |
| @param {Backbone.Model} options.model |
| @param {string} [options.placeholder] |
| */ |
| initialize: function (options) { |
| InputCellEditor.__super__.initialize.apply(this, arguments); |
| |
| if (options.placeholder) { |
| this.$el.attr("placeholder", options.placeholder); |
| } |
| }, |
| |
| /** |
| Renders a text input with the cell value formatted for display, if it |
| exists. |
| */ |
| render: function () { |
| var model = this.model; |
| this.$el.val(this.formatter.fromRaw(model.get(this.column.get("name")), model)); |
| return this; |
| }, |
| |
| /** |
| If the key pressed is `enter`, `tab`, `up`, or `down`, converts the value |
| in the editor to a raw value for saving into the model using the formatter. |
| |
| If the key pressed is `esc` the changes are undone. |
| |
| If the editor goes out of focus (`blur`) but the value is invalid, the |
| event is intercepted and cancelled so the cell remains in focus pending for |
| further action. The changes are saved otherwise. |
| |
| Triggers a Backbone `backgrid:edited` event from the model when successful, |
| and `backgrid:error` if the value cannot be converted. Classes listening to |
| the `error` event, usually the Cell classes, should respond appropriately, |
| usually by rendering some kind of error feedback. |
| |
| @param {Event} e |
| */ |
| saveOrCancel: function (e) { |
| |
| var formatter = this.formatter; |
| var model = this.model; |
| var column = this.column; |
| |
| var command = new Command(e); |
| var blurred = e.type === "blur"; |
| |
| if (command.moveUp() || command.moveDown() || command.moveLeft() || command.moveRight() || |
| command.save() || blurred) { |
| |
| e.preventDefault(); |
| e.stopPropagation(); |
| |
| var val = this.$el.val(); |
| var newValue = formatter.toRaw(val, model); |
| if (_.isUndefined(newValue)) { |
| model.trigger("backgrid:error", model, column, val); |
| } |
| else { |
| model.set(column.get("name"), newValue); |
| model.trigger("backgrid:edited", model, column, command); |
| } |
| } |
| // esc |
| else if (command.cancel()) { |
| // undo |
| e.stopPropagation(); |
| model.trigger("backgrid:edited", model, column, command); |
| } |
| }, |
| |
| postRender: function (model, column) { |
| if (column == null || column.get("name") == this.column.get("name")) { |
| // move the cursor to the end on firefox if text is right aligned |
| if (this.$el.css("text-align") === "right") { |
| var val = this.$el.val(); |
| this.$el.focus().val(null).val(val); |
| } |
| else this.$el.focus(); |
| } |
| return this; |
| } |
| |
| }); |
| |
| /** |
| The super-class for all Cell types. By default, this class renders a plain |
| table cell with the model value converted to a string using the |
| formatter. The table cell is clickable, upon which the cell will go into |
| editor mode, which is rendered by a Backgrid.InputCellEditor instance by |
| default. Upon encountering any formatting errors, this class will add an |
| `error` CSS class to the table cell. |
| |
| @abstract |
| @class Backgrid.Cell |
| @extends Backbone.View |
| */ |
| var Cell = Backgrid.Cell = Backbone.View.extend({ |
| |
| /** @property */ |
| tagName: "td", |
| |
| /** |
| @property {Backgrid.CellFormatter|Object|string} [formatter=CellFormatter] |
| */ |
| formatter: CellFormatter, |
| |
| /** |
| @property {Backgrid.CellEditor} [editor=Backgrid.InputCellEditor] The |
| default editor for all cell instances of this class. This value must be a |
| class, it will be automatically instantiated upon entering edit mode. |
| |
| See Backgrid.CellEditor |
| */ |
| editor: InputCellEditor, |
| |
| /** @property */ |
| events: { |
| "click": "enterEditMode" |
| }, |
| |
| /** |
| Initializer. |
| |
| @param {Object} options |
| @param {Backbone.Model} options.model |
| @param {Backgrid.Column} options.column |
| |
| @throws {ReferenceError} If formatter is a string but a formatter class of |
| said name cannot be found in the Backgrid module. |
| */ |
| initialize: function (options) { |
| this.column = options.column; |
| if (!(this.column instanceof Column)) { |
| this.column = new Column(this.column); |
| } |
| |
| var column = this.column, model = this.model, $el = this.$el; |
| |
| var formatter = Backgrid.resolveNameToClass(column.get("formatter") || |
| this.formatter, "Formatter"); |
| |
| if (!_.isFunction(formatter.fromRaw) && !_.isFunction(formatter.toRaw)) { |
| formatter = new formatter(); |
| } |
| |
| this.formatter = formatter; |
| |
| this.editor = Backgrid.resolveNameToClass(this.editor, "CellEditor"); |
| |
| this.listenTo(model, "change:" + column.get("name"), function () { |
| if (!$el.hasClass("editor")) this.render(); |
| }); |
| |
| this.listenTo(model, "backgrid:error", this.renderError); |
| |
| this.listenTo(column, "change:editable change:sortable change:renderable", |
| function (column) { |
| var changed = column.changedAttributes(); |
| for (var key in changed) { |
| if (changed.hasOwnProperty(key)) { |
| $el.toggleClass(key, changed[key]); |
| } |
| } |
| }); |
| |
| if (Backgrid.callByNeed(column.editable(), column, model)) $el.addClass("editable"); |
| if (Backgrid.callByNeed(column.sortable(), column, model)) $el.addClass("sortable"); |
| if (Backgrid.callByNeed(column.renderable(), column, model)) $el.addClass("renderable"); |
| }, |
| |
| /** |
| Render a text string in a table cell. The text is converted from the |
| model's raw value for this cell's column. |
| */ |
| render: function () { |
| this.$el.empty(); |
| var model = this.model; |
| this.$el.text(this.formatter.fromRaw(model.get(this.column.get("name")), model)); |
| this.delegateEvents(); |
| return this; |
| }, |
| |
| /** |
| If this column is editable, a new CellEditor instance is instantiated with |
| its required parameters. An `editor` CSS class is added to the cell upon |
| entering edit mode. |
| |
| This method triggers a Backbone `backgrid:edit` event from the model when |
| the cell is entering edit mode and an editor instance has been constructed, |
| but before it is rendered and inserted into the DOM. The cell and the |
| constructed cell editor instance are sent as event parameters when this |
| event is triggered. |
| |
| When this cell has finished switching to edit mode, a Backbone |
| `backgrid:editing` event is triggered from the model. The cell and the |
| constructed cell instance are also sent as parameters in the event. |
| |
| When the model triggers a `backgrid:error` event, it means the editor is |
| unable to convert the current user input to an apprpriate value for the |
| model's column, and an `error` CSS class is added to the cell accordingly. |
| */ |
| enterEditMode: function () { |
| var model = this.model; |
| var column = this.column; |
| |
| var editable = Backgrid.callByNeed(column.editable(), column, model); |
| if (editable) { |
| |
| this.currentEditor = new this.editor({ |
| column: this.column, |
| model: this.model, |
| formatter: this.formatter |
| }); |
| |
| model.trigger("backgrid:edit", model, column, this, this.currentEditor); |
| |
| // Need to redundantly undelegate events for Firefox |
| this.undelegateEvents(); |
| this.$el.empty(); |
| this.$el.append(this.currentEditor.$el); |
| this.currentEditor.render(); |
| this.$el.addClass("editor"); |
| |
| model.trigger("backgrid:editing", model, column, this, this.currentEditor); |
| } |
| }, |
| |
| /** |
| Put an `error` CSS class on the table cell. |
| */ |
| renderError: function (model, column) { |
| if (column == null || column.get("name") == this.column.get("name")) { |
| this.$el.addClass("error"); |
| } |
| }, |
| |
| /** |
| Removes the editor and re-render in display mode. |
| */ |
| exitEditMode: function () { |
| this.$el.removeClass("error"); |
| this.currentEditor.remove(); |
| this.stopListening(this.currentEditor); |
| delete this.currentEditor; |
| this.$el.removeClass("editor"); |
| this.render(); |
| }, |
| |
| /** |
| Clean up this cell. |
| |
| @chainable |
| */ |
| remove: function () { |
| if (this.currentEditor) { |
| this.currentEditor.remove.apply(this.currentEditor, arguments); |
| delete this.currentEditor; |
| } |
| return Cell.__super__.remove.apply(this, arguments); |
| } |
| |
| }); |
| |
| /** |
| StringCell displays HTML escaped strings and accepts anything typed in. |
| |
| @class Backgrid.StringCell |
| @extends Backgrid.Cell |
| */ |
| var StringCell = Backgrid.StringCell = Cell.extend({ |
| |
| /** @property */ |
| className: "string-cell", |
| |
| formatter: StringFormatter |
| |
| }); |
| |
| /** |
| UriCell renders an HTML `<a>` anchor for the value and accepts URIs as user |
| input values. No type conversion or URL validation is done by the formatter |
| of this cell. Users who need URL validation are encourage to subclass UriCell |
| to take advantage of the parsing capabilities of the HTMLAnchorElement |
| available on HTML5-capable browsers or using a third-party library like |
| [URI.js](https://github.com/medialize/URI.js). |
| |
| @class Backgrid.UriCell |
| @extends Backgrid.Cell |
| */ |
| var UriCell = Backgrid.UriCell = Cell.extend({ |
| |
| /** @property */ |
| className: "uri-cell", |
| |
| /** |
| @property {string} [title] The title attribute of the generated anchor. It |
| uses the display value formatted by the `formatter.fromRaw` by default. |
| */ |
| title: null, |
| |
| /** |
| @property {string} [target="_blank"] The target attribute of the generated |
| anchor. |
| */ |
| target: "_blank", |
| |
| initialize: function (options) { |
| UriCell.__super__.initialize.apply(this, arguments); |
| this.title = options.title || this.title; |
| this.target = options.target || this.target; |
| }, |
| |
| render: function () { |
| this.$el.empty(); |
| var rawValue = this.model.get(this.column.get("name")); |
| var formattedValue = this.formatter.fromRaw(rawValue, this.model); |
| this.$el.append($("<a>", { |
| tabIndex: -1, |
| href: rawValue, |
| title: this.title || formattedValue, |
| target: this.target |
| }).text(formattedValue)); |
| this.delegateEvents(); |
| return this; |
| } |
| |
| }); |
| |
| /** |
| Like Backgrid.UriCell, EmailCell renders an HTML `<a>` anchor for the |
| value. The `href` in the anchor is prefixed with `mailto:`. EmailCell will |
| complain if the user enters a string that doesn't contain the `@` sign. |
| |
| @class Backgrid.EmailCell |
| @extends Backgrid.StringCell |
| */ |
| var EmailCell = Backgrid.EmailCell = StringCell.extend({ |
| |
| /** @property */ |
| className: "email-cell", |
| |
| formatter: EmailFormatter, |
| |
| render: function () { |
| this.$el.empty(); |
| var model = this.model; |
| var formattedValue = this.formatter.fromRaw(model.get(this.column.get("name")), model); |
| this.$el.append($("<a>", { |
| tabIndex: -1, |
| href: "mailto:" + formattedValue, |
| title: formattedValue |
| }).text(formattedValue)); |
| this.delegateEvents(); |
| return this; |
| } |
| |
| }); |
| |
| /** |
| NumberCell is a generic cell that renders all numbers. Numbers are formatted |
| using a Backgrid.NumberFormatter. |
| |
| @class Backgrid.NumberCell |
| @extends Backgrid.Cell |
| */ |
| var NumberCell = Backgrid.NumberCell = Cell.extend({ |
| |
| /** @property */ |
| className: "number-cell", |
| |
| /** |
| @property {number} [decimals=2] Must be an integer. |
| */ |
| decimals: NumberFormatter.prototype.defaults.decimals, |
| |
| /** @property {string} [decimalSeparator='.'] */ |
| decimalSeparator: NumberFormatter.prototype.defaults.decimalSeparator, |
| |
| /** @property {string} [orderSeparator=','] */ |
| orderSeparator: NumberFormatter.prototype.defaults.orderSeparator, |
| |
| /** @property {Backgrid.CellFormatter} [formatter=Backgrid.NumberFormatter] */ |
| formatter: NumberFormatter, |
| |
| /** |
| Initializes this cell and the number formatter. |
| |
| @param {Object} options |
| @param {Backbone.Model} options.model |
| @param {Backgrid.Column} options.column |
| */ |
| initialize: function (options) { |
| NumberCell.__super__.initialize.apply(this, arguments); |
| var formatter = this.formatter; |
| formatter.decimals = this.decimals; |
| formatter.decimalSeparator = this.decimalSeparator; |
| formatter.orderSeparator = this.orderSeparator; |
| } |
| |
| }); |
| |
| /** |
| An IntegerCell is just a Backgrid.NumberCell with 0 decimals. If a floating |
| point number is supplied, the number is simply rounded the usual way when |
| displayed. |
| |
| @class Backgrid.IntegerCell |
| @extends Backgrid.NumberCell |
| */ |
| var IntegerCell = Backgrid.IntegerCell = NumberCell.extend({ |
| |
| /** @property */ |
| className: "integer-cell", |
| |
| /** |
| @property {number} decimals Must be an integer. |
| */ |
| decimals: 0 |
| }); |
| |
| /** |
| A PercentCell is another Backgrid.NumberCell that takes a floating number, |
| optionally multiplied by a multiplier and display it as a percentage. |
| |
| @class Backgrid.PercentCell |
| @extends Backgrid.NumberCell |
| */ |
| var PercentCell = Backgrid.PercentCell = NumberCell.extend({ |
| |
| /** @property */ |
| className: "percent-cell", |
| |
| /** @property {number} [multiplier=1] */ |
| multiplier: PercentFormatter.prototype.defaults.multiplier, |
| |
| /** @property {string} [symbol='%'] */ |
| symbol: PercentFormatter.prototype.defaults.symbol, |
| |
| /** @property {Backgrid.CellFormatter} [formatter=Backgrid.PercentFormatter] */ |
| formatter: PercentFormatter, |
| |
| /** |
| Initializes this cell and the percent formatter. |
| |
| @param {Object} options |
| @param {Backbone.Model} options.model |
| @param {Backgrid.Column} options.column |
| */ |
| initialize: function () { |
| PercentCell.__super__.initialize.apply(this, arguments); |
| var formatter = this.formatter; |
| formatter.multiplier = this.multiplier; |
| formatter.symbol = this.symbol; |
| } |
| |
| }); |
| |
| /** |
| DatetimeCell is a basic cell that accepts datetime string values in RFC-2822 |
| or W3C's subset of ISO-8601 and displays them in ISO-8601 format. For a much |
| more sophisticated date time cell with better datetime formatting, take a |
| look at the Backgrid.Extension.MomentCell extension. |
| |
| @class Backgrid.DatetimeCell |
| @extends Backgrid.Cell |
| |
| See: |
| |
| - Backgrid.Extension.MomentCell |
| - Backgrid.DatetimeFormatter |
| */ |
| var DatetimeCell = Backgrid.DatetimeCell = Cell.extend({ |
| |
| /** @property */ |
| className: "datetime-cell", |
| |
| /** |
| @property {boolean} [includeDate=true] |
| */ |
| includeDate: DatetimeFormatter.prototype.defaults.includeDate, |
| |
| /** |
| @property {boolean} [includeTime=true] |
| */ |
| includeTime: DatetimeFormatter.prototype.defaults.includeTime, |
| |
| /** |
| @property {boolean} [includeMilli=false] |
| */ |
| includeMilli: DatetimeFormatter.prototype.defaults.includeMilli, |
| |
| /** @property {Backgrid.CellFormatter} [formatter=Backgrid.DatetimeFormatter] */ |
| formatter: DatetimeFormatter, |
| |
| /** |
| Initializes this cell and the datetime formatter. |
| |
| @param {Object} options |
| @param {Backbone.Model} options.model |
| @param {Backgrid.Column} options.column |
| */ |
| initialize: function (options) { |
| DatetimeCell.__super__.initialize.apply(this, arguments); |
| var formatter = this.formatter; |
| formatter.includeDate = this.includeDate; |
| formatter.includeTime = this.includeTime; |
| formatter.includeMilli = this.includeMilli; |
| |
| var placeholder = this.includeDate ? "YYYY-MM-DD" : ""; |
| placeholder += (this.includeDate && this.includeTime) ? "T" : ""; |
| placeholder += this.includeTime ? "HH:mm:ss" : ""; |
| placeholder += (this.includeTime && this.includeMilli) ? ".SSS" : ""; |
| |
| this.editor = this.editor.extend({ |
| attributes: _.extend({}, this.editor.prototype.attributes, this.editor.attributes, { |
| placeholder: placeholder |
| }) |
| }); |
| } |
| |
| }); |
| |
| /** |
| DateCell is a Backgrid.DatetimeCell without the time part. |
| |
| @class Backgrid.DateCell |
| @extends Backgrid.DatetimeCell |
| */ |
| var DateCell = Backgrid.DateCell = DatetimeCell.extend({ |
| |
| /** @property */ |
| className: "date-cell", |
| |
| /** @property */ |
| includeTime: false |
| |
| }); |
| |
| /** |
| TimeCell is a Backgrid.DatetimeCell without the date part. |
| |
| @class Backgrid.TimeCell |
| @extends Backgrid.DatetimeCell |
| */ |
| var TimeCell = Backgrid.TimeCell = DatetimeCell.extend({ |
| |
| /** @property */ |
| className: "time-cell", |
| |
| /** @property */ |
| includeDate: false |
| |
| }); |
| |
| /** |
| BooleanCellEditor renders a checkbox as its editor. |
| |
| @class Backgrid.BooleanCellEditor |
| @extends Backgrid.CellEditor |
| */ |
| var BooleanCellEditor = Backgrid.BooleanCellEditor = CellEditor.extend({ |
| |
| /** @property */ |
| tagName: "input", |
| |
| /** @property */ |
| attributes: { |
| tabIndex: -1, |
| type: "checkbox" |
| }, |
| |
| /** @property */ |
| events: { |
| "mousedown": function () { |
| this.mouseDown = true; |
| }, |
| "blur": "enterOrExitEditMode", |
| "mouseup": function () { |
| this.mouseDown = false; |
| }, |
| "change": "saveOrCancel", |
| "keydown": "saveOrCancel" |
| }, |
| |
| /** |
| Renders a checkbox and check it if the model value of this column is true, |
| uncheck otherwise. |
| */ |
| render: function () { |
| var model = this.model; |
| var val = this.formatter.fromRaw(model.get(this.column.get("name")), model); |
| this.$el.prop("checked", val); |
| return this; |
| }, |
| |
| /** |
| Event handler. Hack to deal with the case where `blur` is fired before |
| `change` and `click` on a checkbox. |
| */ |
| enterOrExitEditMode: function (e) { |
| if (!this.mouseDown) { |
| var model = this.model; |
| model.trigger("backgrid:edited", model, this.column, new Command(e)); |
| } |
| }, |
| |
| /** |
| Event handler. Save the value into the model if the event is `change` or |
| one of the keyboard navigation key presses. Exit edit mode without saving |
| if `escape` was pressed. |
| */ |
| saveOrCancel: function (e) { |
| var model = this.model; |
| var column = this.column; |
| var formatter = this.formatter; |
| var command = new Command(e); |
| // skip ahead to `change` when space is pressed |
| if (command.passThru() && e.type != "change") return true; |
| if (command.cancel()) { |
| e.stopPropagation(); |
| model.trigger("backgrid:edited", model, column, command); |
| } |
| |
| var $el = this.$el; |
| if (command.save() || command.moveLeft() || command.moveRight() || command.moveUp() || |
| command.moveDown()) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| var val = formatter.toRaw($el.prop("checked"), model); |
| model.set(column.get("name"), val); |
| model.trigger("backgrid:edited", model, column, command); |
| } |
| else if (e.type == "change") { |
| var val = formatter.toRaw($el.prop("checked"), model); |
| model.set(column.get("name"), val); |
| $el.focus(); |
| } |
| } |
| |
| }); |
| |
| /** |
| BooleanCell renders a checkbox both during display mode and edit mode. The |
| checkbox is checked if the model value is true, unchecked otherwise. |
| |
| @class Backgrid.BooleanCell |
| @extends Backgrid.Cell |
| */ |
| var BooleanCell = Backgrid.BooleanCell = Cell.extend({ |
| |
| /** @property */ |
| className: "boolean-cell", |
| |
| /** @property */ |
| editor: BooleanCellEditor, |
| |
| /** @property */ |
| events: { |
| "click": "enterEditMode" |
| }, |
| |
| /** |
| Renders a checkbox and check it if the model value of this column is true, |
| uncheck otherwise. |
| */ |
| render: function () { |
| this.$el.empty(); |
| var model = this.model, column = this.column; |
| var editable = Backgrid.callByNeed(column.editable(), column, model); |
| this.$el.append($("<input>", { |
| tabIndex: -1, |
| type: "checkbox", |
| checked: this.formatter.fromRaw(model.get(column.get("name")), model), |
| disabled: !editable |
| })); |
| this.delegateEvents(); |
| return this; |
| } |
| |
| }); |
| |
| /** |
| SelectCellEditor renders an HTML `<select>` fragment as the editor. |
| |
| @class Backgrid.SelectCellEditor |
| @extends Backgrid.CellEditor |
| */ |
| var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({ |
| |
| /** @property */ |
| tagName: "select", |
| |
| /** @property */ |
| events: { |
| "change": "save", |
| "blur": "close", |
| "keydown": "close" |
| }, |
| |
| /** @property {function(Object, ?Object=): string} template */ |
| template: _.template('<option value="<%- value %>" <%= selected ? \'selected="selected"\' : "" %>><%- text %></option>', null, {variable: null}), |
| |
| setOptionValues: function (optionValues) { |
| this.optionValues = optionValues; |
| this.optionValues = _.result(this, "optionValues"); |
| }, |
| |
| setMultiple: function (multiple) { |
| this.multiple = multiple; |
| this.$el.prop("multiple", multiple); |
| }, |
| |
| _renderOptions: function (nvps, selectedValues) { |
| var options = ''; |
| for (var i = 0; i < nvps.length; i++) { |
| options = options + this.template({ |
| text: nvps[i][0], |
| value: nvps[i][1], |
| selected: _.indexOf(selectedValues, nvps[i][1]) > -1 |
| }); |
| } |
| return options; |
| }, |
| |
| /** |
| Renders the options if `optionValues` is a list of name-value pairs. The |
| options are contained inside option groups if `optionValues` is a list of |
| object hashes. The name is rendered at the option text and the value is the |
| option value. If `optionValues` is a function, it is called without a |
| parameter. |
| */ |
| render: function () { |
| this.$el.empty(); |
| |
| var optionValues = _.result(this, "optionValues"); |
| var model = this.model; |
| var selectedValues = this.formatter.fromRaw(model.get(this.column.get("name")), model); |
| |
| if (!_.isArray(optionValues)) throw new TypeError("optionValues must be an array"); |
| |
| var optionValue = null; |
| var optionText = null; |
| var optionValue = null; |
| var optgroupName = null; |
| var optgroup = null; |
| |
| for (var i = 0; i < optionValues.length; i++) { |
| var optionValue = optionValues[i]; |
| |
| if (_.isArray(optionValue)) { |
| optionText = optionValue[0]; |
| optionValue = optionValue[1]; |
| |
| this.$el.append(this.template({ |
| text: optionText, |
| value: optionValue, |
| selected: _.indexOf(selectedValues, optionValue) > -1 |
| })); |
| } |
| else if (_.isObject(optionValue)) { |
| optgroupName = optionValue.name; |
| optgroup = $("<optgroup></optgroup>", { label: optgroupName }); |
| optgroup.append(this._renderOptions.call(this, optionValue.values, selectedValues)); |
| this.$el.append(optgroup); |
| } |
| else { |
| throw new TypeError("optionValues elements must be a name-value pair or an object hash of { name: 'optgroup label', value: [option name-value pairs] }"); |
| } |
| } |
| |
| this.delegateEvents(); |
| |
| return this; |
| }, |
| |
| /** |
| Saves the value of the selected option to the model attribute. |
| */ |
| save: function (e) { |
| var model = this.model; |
| var column = this.column; |
| model.set(column.get("name"), this.formatter.toRaw(this.$el.val(), model)); |
| }, |
| |
| /** |
| Triggers a `backgrid:edited` event from the model so the body can close |
| this editor. |
| */ |
| close: function (e) { |
| var model = this.model; |
| var column = this.column; |
| var command = new Command(e); |
| if (command.cancel()) { |
| e.stopPropagation(); |
| model.trigger("backgrid:edited", model, column, new Command(e)); |
| } |
| else if (command.save() || command.moveLeft() || command.moveRight() || |
| command.moveUp() || command.moveDown() || e.type == "blur") { |
| e.preventDefault(); |
| e.stopPropagation(); |
| this.save(e); |
| model.trigger("backgrid:edited", model, column, new Command(e)); |
| } |
| } |
| |
| }); |
| |
| /** |
| SelectCell is also a different kind of cell in that upon going into edit mode |
| the cell renders a list of options to pick from, as opposed to an input box. |
| |
| SelectCell cannot be referenced by its string name when used in a column |
| definition because it requires an `optionValues` class attribute to be |
| defined. `optionValues` can either be a list of name-value pairs, to be |
| rendered as options, or a list of object hashes which consist of a key *name* |
| which is the option group name, and a key *values* which is a list of |
| name-value pairs to be rendered as options under that option group. |
| |
| In addition, `optionValues` can also be a parameter-less function that |
| returns one of the above. If the options are static, it is recommended the |
| returned values to be memoized. `_.memoize()` is a good function to help with |
| that. |
| |
| During display mode, the default formatter will normalize the raw model value |
| to an array of values whether the raw model value is a scalar or an |
| array. Each value is compared with the `optionValues` values using |
| Ecmascript's implicit type conversion rules. When exiting edit mode, no type |
| conversion is performed when saving into the model. This behavior is not |
| always desirable when the value type is anything other than string. To |
| control type conversion on the client-side, you should subclass SelectCell to |
| provide a custom formatter or provide the formatter to your column |
| definition. |
| |
| See: |
| [$.fn.val()](http://api.jquery.com/val/) |
| |
| @class Backgrid.SelectCell |
| @extends Backgrid.Cell |
| */ |
| var SelectCell = Backgrid.SelectCell = Cell.extend({ |
| |
| /** @property */ |
| className: "select-cell", |
| |
| /** @property */ |
| editor: SelectCellEditor, |
| |
| /** @property */ |
| multiple: false, |
| |
| /** @property */ |
| formatter: SelectFormatter, |
| |
| /** |
| @property {Array.<Array>|Array.<{name: string, values: Array.<Array>}>} optionValues |
| */ |
| optionValues: undefined, |
| |
| /** @property */ |
| delimiter: ', ', |
| |
| /** |
| Initializer. |
| |
| @param {Object} options |
| @param {Backbone.Model} options.model |
| @param {Backgrid.Column} options.column |
| |
| @throws {TypeError} If `optionsValues` is undefined. |
| */ |
| initialize: function (options) { |
| SelectCell.__super__.initialize.apply(this, arguments); |
| this.listenTo(this.model, "backgrid:edit", function (model, column, cell, editor) { |
| if (column.get("name") == this.column.get("name")) { |
| editor.setOptionValues(this.optionValues); |
| editor.setMultiple(this.multiple); |
| } |
| }); |
| }, |
| |
| /** |
| Renders the label using the raw value as key to look up from `optionValues`. |
| |
| @throws {TypeError} If `optionValues` is malformed. |
| */ |
| render: function () { |
| this.$el.empty(); |
| |
| var optionValues = _.result(this, "optionValues"); |
| var model = this.model; |
| var rawData = this.formatter.fromRaw(model.get(this.column.get("name")), model); |
| |
| var selectedText = []; |
| |
| try { |
| if (!_.isArray(optionValues) || _.isEmpty(optionValues)) throw new TypeError; |
| |
| for (var k = 0; k < rawData.length; k++) { |
| var rawDatum = rawData[k]; |
| |
| for (var i = 0; i < optionValues.length; i++) { |
| var optionValue = optionValues[i]; |
| |
| if (_.isArray(optionValue)) { |
| var optionText = optionValue[0]; |
| var optionValue = optionValue[1]; |
| |
| if (optionValue == rawDatum) selectedText.push(optionText); |
| } |
| else if (_.isObject(optionValue)) { |
| var optionGroupValues = optionValue.values; |
| |
| for (var j = 0; j < optionGroupValues.length; j++) { |
| var optionGroupValue = optionGroupValues[j]; |
| if (optionGroupValue[1] == rawDatum) { |
| selectedText.push(optionGroupValue[0]); |
| } |
| } |
| } |
| else { |
| throw new TypeError; |
| } |
| } |
| } |
| |
| this.$el.append(selectedText.join(this.delimiter)); |
| } |
| catch (ex) { |
| if (ex instanceof TypeError) { |
| throw new TypeError("'optionValues' must be of type {Array.<Array>|Array.<{name: string, values: Array.<Array>}>}"); |
| } |
| throw ex; |
| } |
| |
| this.delegateEvents(); |
| |
| return this; |
| } |
| |
| }); |
| |
| /* |
| 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({ |
| |
| /** |
| @cfg {Object} defaults Column defaults. To override any of these default |
| values, you can either change the prototype directly to override |
| Column.defaults globally or extend Column and supply the custom class to |
| Backgrid.Grid: |
| |
| // Override Column defaults globally |
| Column.prototype.defaults.sortable = false; |
| |
| // Override Column defaults locally |
| var MyColumn = Column.extend({ |
| defaults: _.defaults({ |
| editable: false |
| }, Column.prototype.defaults) |
| }); |
| |
| var grid = new Backgrid.Grid(columns: new Columns([{...}, {...}], { |
| model: MyColumn |
| })); |
| |
| @cfg {string} [defaults.name] The default name of the model attribute. |
| |
| @cfg {string} [defaults.label] The default label to show in the header. |
| |
| @cfg {string|Backgrid.Cell} [defaults.cell] The default 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. |
| |
| @cfg {string|Backgrid.HeaderCell} [defaults.headerCell] The default header |
| cell type. |
| |
| @cfg {boolean|string|function(): boolean} [defaults.sortable=true] Whether |
| this column is sortable. If the value is a string, a method will the same |
| name will be looked up from the column instance to determine whether the |
| column should be sortable. The method's signature must be `function |
| (Backgrid.Column, Backbone.Model): boolean`. |
| |
| @cfg {boolean|string|function(): boolean} [defaults.editable=true] Whether |
| this column is editable. If the value is a string, a method will the same |
| name will be looked up from the column instance to determine whether the |
| column should be editable. The method's signature must be `function |
| (Backgrid.Column, Backbone.Model): boolean`. |
| |
| @cfg {boolean|string|function(): boolean} [defaults.renderable=true] |
| Whether this column is renderable. If the value is a string, a method will |
| the same name will be looked up from the column instance to determine |
| whether the column should be renderable. The method's signature must be |
| `function (Backrid.Column, Backbone.Model): boolean`. |
| |
| @cfg {Backgrid.CellFormatter | Object | string} [defaults.formatter] The |
| formatter to use to convert between raw model values and user input. |
| |
| @cfg {"toggle"|"cycle"} [defaults.sortType="cycle"] Whether sorting will |
| toggle between ascending and descending order, or cycle between insertion |
| order, ascending and descending order. |
| |
| @cfg {(function(Backbone.Model, string): *) | string} [defaults.sortValue] |
| The function to use to extract a value from the model for comparison during |
| sorting. If this value is a string, a method with the same name will be |
| looked up from the column instance. |
| |
| @cfg {"ascending"|"descending"|null} [defaults.direction=null] The initial |
| sorting direction for this column. The default is ordered by |
| Backbone.Model.cid, which usually means the collection is ordered by |
| insertion order. |
| */ |
| defaults: { |
| name: undefined, |
| label: undefined, |
| sortable: true, |
| editable: true, |
| renderable: true, |
| formatter: undefined, |
| sortType: "cycle", |
| sortValue: undefined, |
| direction: null, |
| cell: undefined, |
| headerCell: undefined |
| }, |
| |
| /** |
| Initializes this Column instance. |
| |
| @param {Object} attrs |
| |
| @param {string} attrs.name The model attribute this column is responsible |
| for. |
| |
| @param {string|Backgrid.Cell} attrs.cell The cell type to use to render |
| this column. |
| |
| @param {string} [attrs.label] |
| |
| @param {string|Backgrid.HeaderCell} [attrs.headerCell] |
| |
| @param {boolean|string|function(): boolean} [attrs.sortable=true] |
| |
| @param {boolean|string|function(): boolean} [attrs.editable=true] |
| |
| @param {boolean|string|function(): boolean} [attrs.renderable=true] |
| |
| @param {Backgrid.CellFormatter | Object | string} [attrs.formatter] |
| |
| @param {"toggle"|"cycle"} [attrs.sortType="cycle"] |
| |
| @param {(function(Backbone.Model, string): *) | string} [attrs.sortValue] |
| |
| @throws {TypeError} If attrs.cell or attrs.options are not supplied. |
| |
| @throws {ReferenceError} If formatter is a string but a formatter class of |
| said name cannot be found in the Backgrid module. |
| |
| See: |
| |
| - Backgrid.Column.defaults |
| - Backgrid.Cell |
| - Backgrid.CellFormatter |
| */ |
| initialize: function () { |
| 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 }); |
| }, |
| |
| /** |
| Returns an appropriate value extraction function from a model for sorting. |
| |
| If the column model contains an attribute `sortValue`, if it is a string, a |
| method from the column instance identifified by the `sortValue` string is |
| returned. If it is a function, it it returned as is. If `sortValue` isn't |
| found from the column model's attributes, a default value extraction |
| function is returned which will compare according to the natural order of |
| the value's type. |
| |
| @return {function(Backbone.Model, string): *} |
| */ |
| sortValue: function () { |
| var sortValue = this.get("sortValue"); |
| if (_.isString(sortValue)) return this[sortValue]; |
| else if (_.isFunction(sortValue)) return sortValue; |
| |
| return function (model, colName) { |
| return model.get(colName); |
| }; |
| } |
| |
| /** |
| @member Backgrid.Column |
| @protected |
| @method sortable |
| @return {function(Backgrid.Column, Backbone.Model): boolean | boolean} |
| */ |
| |
| /** |
| @member Backgrid.Column |
| @protected |
| @method editable |
| @return {function(Backgrid.Column, Backbone.Model): boolean | boolean} |
| */ |
| |
| /** |
| @member Backgrid.Column |
| @protected |
| @method renderable |
| @return {function(Backgrid.Column, Backbone.Model): boolean | boolean} |
| */ |
| }); |
| |
| _.each(["sortable", "renderable", "editable"], function (key) { |
| Column.prototype[key] = function () { |
| var value = this.get(key); |
| if (_.isString(value)) return this[value]; |
| else if (_.isFunction(value)) return value; |
| |
| return !!value; |
| }; |
| }); |
| |
| /** |
| 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", |
| |
| /** |
| 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) { |
| |
| 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, "add", function (column, columns) { |
| var i = columns.indexOf(column); |
| var cell = this.makeCell(column, options); |
| cells.splice(i, 0, cell); |
| |
| 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++) { |
| fragment.appendChild(this.cells[i].render().el); |
| } |
| |
| 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 {string|function(): string} */ |
| emptyText: null, |
| |
| /** |
| Initializer. |
| |
| @param {Object} options |
| @param {string|function(): string} options.emptyText |
| @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata. |
| */ |
| initialize: function (options) { |
| 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.appendChild(document.createTextNode(_.result(this, "emptyText"))); |
| |
| this.el.className = "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" |
| }, |
| |
| /** |
| Initializer. |
| |
| @param {Object} options |
| @param {Backgrid.Column|Object} options.column |
| |
| @throws {TypeError} If options.column or options.collection is undefined. |
| */ |
| initialize: function (options) { |
| this.column = options.column; |
| if (!(this.column instanceof Column)) { |
| this.column = new Column(this.column); |
| } |
| |
| var column = this.column, collection = this.collection, $el = this.$el; |
| |
| this.listenTo(column, "change:editable change:sortable change:renderable", |
| function (column) { |
| var changed = column.changedAttributes(); |
| for (var key in changed) { |
| if (changed.hasOwnProperty(key)) { |
| $el.toggleClass(key, changed[key]); |
| } |
| } |
| }); |
| this.listenTo(column, "change:direction", this.setCellDirection); |
| this.listenTo(column, "change:name change:label", this.render); |
| |
| if (Backgrid.callByNeed(column.editable(), column, collection)) $el.addClass("editable"); |
| if (Backgrid.callByNeed(column.sortable(), column, collection)) $el.addClass("sortable"); |
| if (Backgrid.callByNeed(column.renderable(), column, collection)) $el.addClass("renderable"); |
| |
| this.listenTo(collection.fullCollection || collection, "sort", this.removeCellDirection); |
| }, |
| |
| /** |
| Event handler for the collection's `sort` event. Removes all the CSS |
| direction classes. |
| */ |
| removeCellDirection: function () { |
| this.$el.removeClass("ascending").removeClass("descending"); |
| this.column.set("direction", null); |
| }, |
| |
| /** |
| Event handler for the column's `change:direction` event. If this |
| HeaderCell's column is being sorted on, it applies the direction given as a |
| CSS class to the header cell. Removes all the CSS direction classes |
| otherwise. |
| */ |
| setCellDirection: function (column, direction) { |
| this.$el.removeClass("ascending").removeClass("descending"); |
| if (column.cid == this.column.cid) this.$el.addClass(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 column = this.column; |
| var collection = this.collection; |
| var event = "backgrid:sort"; |
| |
| function cycleSort(header, col) { |
| if (column.get("direction") === "ascending") collection.trigger(event, col, "descending"); |
| else if (column.get("direction") === "descending") collection.trigger(event, col, null); |
| else collection.trigger(event, col, "ascending"); |
| } |
| |
| function toggleSort(header, col) { |
| if (column.get("direction") === "ascending") collection.trigger(event, col, "descending"); |
| else collection.trigger(event, col, "ascending"); |
| } |
| |
| var sortable = Backgrid.callByNeed(column.sortable(), column, this.collection); |
| if (sortable) { |
| var sortType = column.get("sortType"); |
| if (sortType === "toggle") toggleSort(this, column); |
| else cycleSort(this, column); |
| } |
| }, |
| |
| /** |
| Renders a header cell with a sorter, a label, and a class name for this |
| column. |
| */ |
| render: function () { |
| this.$el.empty(); |
| var column = this.column; |
| var sortable = Backgrid.callByNeed(column.sortable(), column, this.collection); |
| var label; |
| if(sortable){ |
| label = $("<a>").text(column.get("label")).append("<b class='sort-caret'></b>"); |
| } else { |
| label = document.createTextNode(column.get("label")); |
| } |
| |
| this.$el.append(label); |
| this.$el.addClass(column.get("name")); |
| this.$el.addClass(column.get("direction")); |
| 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) { |
| 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|function(): 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) { |
| |
| 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:sort", this.sort); |
| 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; |
| } |
| |
| 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 (index >= $children.length) { |
| $el.append($rowEl); |
| } |
| else { |
| $children.eq(index).before($rowEl); |
| } |
| |
| return this; |
| }, |
| |
| /** |
| 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(); |
| |
| return this; |
| }, |
| |
| /** |
| 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); |
| }, |
| |
| /** |
| 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:sorted` event from the collection when done |
| with the column, direction and a reference to the collection. |
| |
| @param {Backgrid.Column} column |
| @param {null|"ascending"|"descending"} direction |
| |
| See [Backbone.Collection#comparator](http://backbonejs.org/#Collection-comparator) |
| */ |
| sort: function (column, direction) { |
| |
| if (!_.contains(["ascending", "descending", null], direction)) { |
| throw new RangeError('direction must be one of "ascending", "descending" or `null`'); |
| } |
| |
| if (_.isString(column)) column = this.columns.findWhere({name: column}); |
| |
| var collection = this.collection; |
| |
| var order; |
| if (direction === "ascending") order = -1; |
| else if (direction === "descending") order = 1; |
| else order = null; |
| |
| var comparator = this.makeComparator(column.get("name"), order, |
| order ? |
| column.sortValue() : |
| function (model) { |
| return model.cid.replace('c', '') * 1; |
| }); |
| |
| if (Backbone.PageableCollection && |
| collection instanceof Backbone.PageableCollection) { |
| |
| collection.setSorting(order && column.get("name"), order, |
| {sortValue: column.sortValue()}); |
| |
| if (collection.fullCollection) { |
| // If order is null, pageable will remove the comparator on both sides, |
| // in this case the default insertion order comparator needs to be |
| // attached to get back to the order before sorting. |
| if (collection.fullCollection.comparator == null) { |
| collection.fullCollection.comparator = comparator; |
| } |
| collection.fullCollection.sort(); |
| collection.trigger("backgrid:sorted", column, direction, collection); |
| } |
| else collection.fetch({reset: true, success: function () { |
| collection.trigger("backgrid:sorted", column, direction, collection); |
| }}); |
| } |
| else { |
| collection.comparator = comparator; |
| collection.sort(); |
| collection.trigger("backgrid:sorted", column, direction, collection); |
| } |
| |
| column.set("direction", direction); |
| |
| return this; |
| }, |
| |
| makeComparator: function (attr, order, func) { |
| |
| return function (left, right) { |
| // extract the values from the models |
| var l = func(left, attr), r = func(right, attr), t; |
| |
| // if descending order, swap left and right |
| if (order === 1) t = l, l = r, r = t; |
| |
| // compare as usual |
| if (l === r) return 0; |
| else if (l < r) return -1; |
| return 1; |
| }; |
| }, |
| |
| /** |
| Moves focus to the next renderable and editable cell and return the |
| currently editing cell to display mode. |
| |
| Triggers a `backgrid:next` event on the model with the indices of the row |
| and column the user *intended* to move to, and whether the intended move |
| was going to go out of bounds. Note that *out of bound* always means an |
| attempt to go past the end of the last row. |
| |
| @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); |
| var cell, renderable, editable, m, n; |
| |
| this.rows[i].cells[j].exitEditMode(); |
| |
| 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()) { |
| m = i + (command.moveUp() ? -1 : 1); |
| var row = this.rows[m]; |
| if (row) { |
| cell = row.cells[j]; |
| if (Backgrid.callByNeed(cell.column.editable(), cell.column, model)) { |
| cell.enterEditMode(); |
| model.trigger("backgrid:next", m, j, false); |
| } |
| } |
| else model.trigger("backgrid:next", m, j, true); |
| } |
| 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--) { |
| m = ~~(offset / l); |
| n = offset - m * l; |
| cell = this.rows[m].cells[n]; |
| renderable = Backgrid.callByNeed(cell.column.renderable(), cell.column, cell.model); |
| editable = Backgrid.callByNeed(cell.column.editable(), cell.column, model); |
| if (renderable && editable) { |
| cell.enterEditMode(); |
| model.trigger("backgrid:next", m, n, false); |
| break; |
| } |
| } |
| |
| if (offset == maxOffset) { |
| model.trigger("backgrid:next", ~~(offset / l), offset - m * l, true); |
| } |
| } |
| } |
| |
| return this; |
| } |
| }); |
| |
| /* |
| 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 {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) { |
| 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.Columns>|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) { |
| // 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 filteredOptions = _.omit(options, ["el", "id", "attributes", |
| "className", "tagName", "events"]); |
| |
| // must construct body first so it listens to backgrid:sort first |
| this.body = options.body || this.body; |
| this.body = new this.body(filteredOptions); |
| |
| this.header = options.header || this.header; |
| if (this.header) { |
| this.header = new this.header(filteredOptions); |
| } |
| |
| this.footer = options.footer || this.footer; |
| if (this.footer) { |
| this.footer = new this.footer(filteredOptions); |
| } |
| |
| this.listenTo(this.columns, "reset", function () { |
| if (this.header) { |
| this.header = new (this.header.remove().constructor)(filteredOptions); |
| } |
| this.body = new (this.body.remove().constructor)(filteredOptions); |
| if (this.footer) { |
| this.footer = new (this.footer.remove().constructor)(filteredOptions); |
| } |
| this.render(); |
| }); |
| }, |
| |
| /** |
| Delegates to Backgrid.Body#insertRow. |
| */ |
| insertRow: function () { |
| this.body.insertRow.apply(this.body, arguments); |
| return this; |
| }, |
| |
| /** |
| Delegates to Backgrid.Body#removeRow. |
| */ |
| removeRow: function () { |
| this.body.removeRow.apply(this.body, arguments); |
| return this; |
| }, |
| |
| /** |
| 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`. |
| */ |
| insertColumn: function () { |
| this.columns.add.apply(this.columns, arguments); |
| 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`. |
| */ |
| removeColumn: function () { |
| this.columns.remove.apply(this.columns, arguments); |
| return this; |
| }, |
| |
| /** |
| Delegates to Backgrid.Body#sort. |
| */ |
| sort: function () { |
| this.body.sort.apply(this.body, arguments); |
| 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(); |
| |
| if (this.header) { |
| 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 && 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); |
| } |
| |
| }); |
| return Backgrid; |
| })); |