/* backgrid http://github.com/wyuenho/backgrid Copyright (c) 2013 Jimmy Yuen Ho Wong Licensed under the MIT @license. */ (function (root, $, _, Backbone) { "use strict"; /* backgrid http://github.com/wyuenho/backgrid Copyright (c) 2013 Jimmy Yuen Ho Wong Licensed under the MIT @license. */ var window = root; var Backgrid = root.Backgrid = { VERSION: "0.1.4", Extension: {} }; // 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; } function requireOptions(options, requireOptionKeys) { for (var i = 0; i < requireOptionKeys.length; i++) { var key = requireOptionKeys[i]; if (_.isUndefined(options[key])) { throw new TypeError("'" + key + "' is required"); } } } function resolveNameToClass(name, suffix) { if (_.isString(name)) { var key = capitalize(name) + suffix; var klass = Backgrid[key] || Backgrid.Extension[key]; if (_.isUndefined(klass)) { throw new ReferenceError("Class '" + key + "' not found"); } return klass; } return name; } /* backgrid http://github.com/wyuenho/backgrid Copyright (c) 2013 Jimmy Yuen Ho Wong 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 a formatted string for display. @member Backgrid.CellFormatter @param {*} rawData @return {string} */ 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 (isNaN(number) || number === null) 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 string formats. This class only understands ISO-8601 formatted datetime strings. 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) { if (_.isNull(data) || _.isUndefined(data)) return data; data = data.trim(); var parts = data.split(this.ISO_SPLITTER_RE) || []; var date = this.DATE_RE.test(parts[0]) ? parts[0] : ''; var 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) { 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 `includeDate` is false, or a time is found if `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); } }); /* backgrid http://github.com/wyuenho/backgrid Copyright (c) 2013 Jimmy Yuen Ho Wong 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 {*} options.parent @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) { requireOptions(options, ["formatter", "column", "model"]); this.parent = options.parent; this.formatter = options.formatter; this.column = options.column; if (!(this.column instanceof Column)) { this.column = new Column(this.column); } if (this.parent && _.isFunction(this.parent.on)) { this.listenTo(this.parent, "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 () { 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); } this.listenTo(this, "done", this.remove); }, /** 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` or `tab`, converts the value in the editor to a raw value for the model using the formatter. If the key pressed is `esc` the changes are undone. If the editor's value was changed and goes out of focus (`blur`), the event is intercepted, cancelled so the cell remains in focus pending for further action. Triggers a Backbone `done` event when successful. `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) { if (e.type === "keydown") { // enter or tab if (e.keyCode === 13 || e.keyCode === 9) { e.preventDefault(); var valueToSet = this.formatter.toRaw(this.$el.val()); if (_.isUndefined(valueToSet) || !this.model.set(this.column.get("name"), valueToSet, {validate: true})) { this.trigger("error"); } else { this.trigger("done"); } } // esc else if (e.keyCode === 27) { // undo e.stopPropagation(); this.trigger("done"); } } else if (e.type === "blur") { if (this.formatter.fromRaw(this.model.get(this.column.get("name"))) === this.$el.val()) { this.trigger("done"); } else { var self = this; var timeout = window.setTimeout(function () { self.$el.focus(); window.clearTimeout(timeout); }, 1); } } }, postRender: function () { // 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 any formatting errors, this class will add a `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) { requireOptions(options, ["model", "column"]); this.column = options.column; if (!(this.column instanceof Column)) { this.column = new Column(this.column); } this.formatter = resolveNameToClass(this.formatter, "Formatter"); this.editor = resolveNameToClass(this.editor, "CellEditor"); this.listenTo(this.model, "change:" + this.column.get("name"), function () { if (!this.$el.hasClass("editor")) this.render(); }); }, /** 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().text(this.formatter.fromRaw(this.model.get(this.column.get("name")))); return this; }, /** If this column is editable, a new CellEditor instance is instantiated with its required parameters and listens on the editor's `done` and `error` events. When the editor is `done`, edit mode is exited. When the editor triggers an `error` event, it means the editor is unable to convert the current user input to an apprpriate value for the model's column. An `editor` CSS class is added to the cell upon entering edit mode. */ enterEditMode: function (e) { if (this.column.get("editable")) { this.currentEditor = new this.editor({ parent: this, column: this.column, model: this.model, formatter: this.formatter }); /** Backbone Event. Fired when a cell is entering edit mode and an editor instance has been constructed, but before it is rendered and inserted into the DOM. @event edit @param {Backgrid.Cell} cell This cell instance. @param {Backgrid.CellEditor} editor The cell editor constructed. */ this.trigger("edit", this, this.currentEditor); this.listenTo(this.currentEditor, "done", this.exitEditMode); this.listenTo(this.currentEditor, "error", this.renderError); this.$el.empty(); this.undelegateEvents(); this.$el.append(this.currentEditor.$el); this.currentEditor.render(); this.$el.addClass("editor"); /** Backbone Event. Fired when a cell has finished switching to edit mode. @event editing @param {Backgrid.Cell} cell This cell instance. @param {Backgrid.CellEditor} editor The cell editor constructed. */ this.trigger("editing", this, this.currentEditor); } }, /** Put an `error` CSS class on the table cell. */ renderError: function () { this.$el.addClass("error"); }, /** Removes the editor and re-render in display mode. */ exitEditMode: function () { this.$el.removeClass("error"); this.currentEditor.off(null, null, this); this.currentEditor.remove(); delete this.currentEditor; this.$el.removeClass("editor"); this.render(); this.delegateEvents(); }, /** 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" // No formatter needed. Strings call auto-escaped by jQuery on insertion. }); /** UriCell renders an HTML `` anchor for the value and accepts URIs as user input values. A URI input is URI encoded using `encodeURI()` before writing to the underlying model. @class Backgrid.UriCell @extends Backgrid.Cell */ var UriCell = Backgrid.UriCell = Cell.extend({ /** @property */ className: "uri-cell", formatter: { fromRaw: function (rawData) { return rawData; }, toRaw: function (formattedData) { var result = encodeURI(formattedData); return result === "undefined" ? undefined : result; } }, render: function () { this.$el.empty(); var formattedValue = this.formatter.fromRaw(this.model.get(this.column.get("name"))); this.$el.append($("", { href: formattedValue, title: formattedValue, target: "_blank" }).text(formattedValue)); return this; } }); /** Like Backgrid.UriCell, EmailCell renders an HTML `` 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.Cell */ var EmailCell = Backgrid.EmailCell = Cell.extend({ /** @property */ className: "email-cell", formatter: { fromRaw: function (rawData) { return rawData; }, toRaw: function (formattedData) { var parts = formattedData.split("@"); if (parts.length === 2 && _.all(parts)) { return formattedData; } } }, render: function () { this.$el.empty(); var formattedValue = this.formatter.fromRaw(this.model.get(this.column.get("name"))); this.$el.append($("", { href: "mailto:" + formattedValue, title: formattedValue }).text(formattedValue)); 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 }); /** BooleanCell is a different kind of cell in that there's no difference between display mode and edit mode and this cell type always renders a checkbox for selection. @class Backgrid.BooleanCell @extends Backgrid.Cell */ var BooleanCell = Backgrid.BooleanCell = Cell.extend({ /** @property */ className: "boolean-cell", /** BooleanCell simple uses a default HTML checkbox template instead of a CellEditor instance. @property {function(Object, ?Object=): string} editor The Underscore.js template to render the editor. */ editor: _.template(" />'"), /** Since the editor is not an instance of a CellEditor subclass, more things need to be done in BooleanCell class to listen to editor mode events. */ events: { "click": "enterEditMode", "blur input[type=checkbox]": "exitEditMode", "change input[type=checkbox]": "save" }, /** Renders a checkbox and check it if the model value of this column is true, uncheck otherwise. */ render: function () { this.$el.empty(); this.currentEditor = $(this.editor({ checked: this.formatter.fromRaw(this.model.get(this.column.get("name"))) })); this.$el.append(this.currentEditor); return this; }, /** Simple focuses the checkbox and add an `editor` CSS class to the cell. */ enterEditMode: function (e) { this.$el.addClass("editor"); this.currentEditor.focus(); }, /** Removed the `editor` CSS class from the cell. */ exitEditMode: function (e) { this.$el.removeClass("editor"); }, /** Set true to the model attribute if the checkbox is checked, false otherwise. */ save: function (e) { var val = this.formatter.toRaw(this.currentEditor.prop("checked")); this.model.set(this.column.get("name"), val); } }); /** SelectCellEditor renders an HTML `