From d706a35ab722cbdc7028f445d2b79ee219d07fd9 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 22 Apr 2013 17:35:04 -0700 Subject: [PATCH] Backgrid added --- Gruntfile.js | 4 +- UI/Controller.js | 4 +- UI/Index.html | 2 +- UI/JsLibraries/backbone.backgrid.js | 2223 +++++++++++++++++ ...kbone-pageable.js => backbone.pageable.js} | 0 UI/JsLibraries/backgrid.js | 1290 ---------- UI/MainMenuView.js | 1 + UI/Series/Index/EmptySeriesIndexView.js | 8 + UI/Series/Index/List/CollectionTemplate.html | 5 + UI/Series/Index/List/CollectionView.js | 17 + .../ItemTemplate.html} | 0 .../{SeriesItemView.js => List/ItemView.js} | 15 +- UI/Series/Index/SeriesIndexCollectionView.js | 126 - UI/Series/Index/SeriesIndexLayout.js | 142 ++ ...te.html => SeriesIndexLayoutTemplate.html} | 4 +- UI/Series/Index/SeriesIndexTemplate.html | 28 - UI/Series/Index/SeriesItemTemplate.html | 18 - UI/app.js | 52 +- 18 files changed, 2447 insertions(+), 1492 deletions(-) create mode 100644 UI/JsLibraries/backbone.backgrid.js rename UI/JsLibraries/{backbone-pageable.js => backbone.pageable.js} (100%) delete mode 100644 UI/JsLibraries/backgrid.js create mode 100644 UI/Series/Index/EmptySeriesIndexView.js create mode 100644 UI/Series/Index/List/CollectionTemplate.html create mode 100644 UI/Series/Index/List/CollectionView.js rename UI/Series/Index/{SeriesGridItemTemplate.html => List/ItemTemplate.html} (100%) rename UI/Series/Index/{SeriesItemView.js => List/ItemView.js} (68%) delete mode 100644 UI/Series/Index/SeriesIndexCollectionView.js create mode 100644 UI/Series/Index/SeriesIndexLayout.js rename UI/Series/Index/{SeriesIndexGridTemplate.html => SeriesIndexLayoutTemplate.html} (74%) delete mode 100644 UI/Series/Index/SeriesIndexTemplate.html delete mode 100644 UI/Series/Index/SeriesItemTemplate.html diff --git a/Gruntfile.js b/Gruntfile.js index 1d8a15f72..257bb44e6 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -22,8 +22,8 @@ module.exports = function (grunt) { 'UI/JsLibraries/require.js' : 'http://raw.github.com/jrburke/requirejs/master/require.js', 'UI/JsLibraries/sugar.js' : 'http://raw.github.com/andrewplummer/Sugar/master/release/sugar-full.development.js', 'UI/JsLibraries/underscore.js' : 'http://underscorejs.org/underscore.js', - 'UI/JsLibraries/backbone-pageable.js' : 'https://raw.github.com/wyuenho/backbone-pageable/master/lib/backbone-pageable.js', - 'UI/JsLibraries/backgrid.js' : 'https://raw.github.com/wyuenho/backbone-pageable/master/lib/backbone-pageable.js' + 'UI/JsLibraries/backbone.pageable.js' : 'https://raw.github.com/wyuenho/backbone-pageable/master/lib/backbone-pageable.js', + 'UI/JsLibraries/backbone.backgrid.js' : 'https://raw.github.com/wyuenho/backgrid/master/lib/backgrid.js' }, uglify: { diff --git a/UI/Controller.js b/UI/Controller.js index da9cbbfdc..d8f84d8ee 100644 --- a/UI/Controller.js +++ b/UI/Controller.js @@ -1,6 +1,6 @@ "use strict"; define(['app', 'Shared/ModalRegion', 'AddSeries/AddSeriesLayout', - 'Series/Index/SeriesIndexCollectionView', 'Upcoming/UpcomingCollectionView', + 'Series/Index/SeriesIndexLayout', 'Upcoming/UpcomingCollectionView', 'Calendar/CalendarCollectionView', 'Shared/NotificationView', 'Shared/NotFoundView', 'MainMenuView', 'Series/Details/SeriesDetailsView', 'Series/EpisodeCollection', @@ -11,7 +11,7 @@ define(['app', 'Shared/ModalRegion', 'AddSeries/AddSeriesLayout', series: function () { this._setTitle('NzbDrone'); - NzbDrone.mainRegion.show(new NzbDrone.Series.Index.SeriesIndexCollectionView()); + NzbDrone.mainRegion.show(new NzbDrone.Series.Index.SeriesIndexLayout()); }, seriesDetails: function (query) { diff --git a/UI/Index.html b/UI/Index.html index b872ebcf0..27872d6d8 100644 --- a/UI/Index.html +++ b/UI/Index.html @@ -89,7 +89,7 @@ - + diff --git a/UI/JsLibraries/backbone.backgrid.js b/UI/JsLibraries/backbone.backgrid.js new file mode 100644 index 000000000..5d146b2b2 --- /dev/null +++ b/UI/JsLibraries/backbone.backgrid.js @@ -0,0 +1,2223 @@ +/* + backgrid + http://github.com/wyuenho/backgrid + + Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors + Licensed under the MIT @license. +*/ +(function (root, $, _, Backbone) { + + "use strict"; + +/* + backgrid + http://github.com/wyuenho/backgrid + + Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors + Licensed under the MIT @license. +*/ + +var window = root; + +var Backgrid = root.Backgrid = { + VERSION: "0.2.0", + 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 = _.map(name.split('-'), function (e) { return capitalize(e); }).join('') + suffix; + var klass = Backgrid[key] || Backgrid.Extension[key]; + if (_.isUndefined(klass)) { + throw new ReferenceError("Class '" + key + "' not found"); + } + return klass; + } + + return name; +} +/* + backgrid + http://github.com/wyuenho/backgrid + + Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors + Licensed under the MIT @license. +*/ + +/** + Just a convenient class for interested parties to subclass. + + The default Cell classes don't require the formatter to be a subclass of + Formatter as long as the fromRaw(rawData) and toRaw(formattedData) methods + are defined. + + @abstract + @class Backgrid.CellFormatter + @constructor +*/ +var CellFormatter = Backgrid.CellFormatter = function () {}; +_.extend(CellFormatter.prototype, { + + /** + Takes a raw value from a model and returns an optionally formatted string + for display. The default implementation simply returns the supplied value + as is without any type conversion. + + @member Backgrid.CellFormatter + @param {*} rawData + @return {*} + */ + fromRaw: function (rawData) { + return rawData; + }, + + /** + Takes a formatted string, usually from user input, and returns a + appropriately typed value for persistence in the model. + + If the user input is invalid or unable to be converted to a raw value + suitable for persistence in the model, toRaw must return `undefined`. + + @member Backgrid.CellFormatter + @param {string} formattedData + @return {*|undefined} + */ + toRaw: function (formattedData) { + return formattedData; + } + +}); + +/** + A floating point number formatter. Doesn't understand notation at the moment. + + @class Backgrid.NumberFormatter + @extends Backgrid.CellFormatter + @constructor + @throws {RangeError} If decimals < 0 or > 20. +*/ +var NumberFormatter = Backgrid.NumberFormatter = function (options) { + options = options ? _.clone(options) : {}; + _.extend(this, this.defaults, options); + + if (this.decimals < 0 || this.decimals > 20) { + throw new RangeError("decimals must be between 0 and 20"); + } +}; +NumberFormatter.prototype = new CellFormatter(); +_.extend(NumberFormatter.prototype, { + + /** + @member Backgrid.NumberFormatter + @cfg {Object} options + + @cfg {number} [options.decimals=2] Number of decimals to display. Must be an integer. + + @cfg {string} [options.decimalSeparator='.'] The separator to use when + displaying decimals. + + @cfg {string} [options.orderSeparator=','] The separator to use to + separator thousands. May be an empty string. + */ + defaults: { + decimals: 2, + decimalSeparator: '.', + orderSeparator: ',' + }, + + HUMANIZED_NUM_RE: /(\d)(?=(?:\d{3})+$)/g, + + /** + Takes a floating point number and convert it to a formatted string where + every thousand is separated by `orderSeparator`, with a `decimal` number of + decimals separated by `decimalSeparator`. The number returned is rounded + the usual way. + + @member Backgrid.NumberFormatter + @param {number} number + @return {string} + */ + fromRaw: function (number) { + if (_.isNull(number) || _.isUndefined(number)) return ''; + + number = number.toFixed(~~this.decimals); + + var parts = number.split('.'); + var integerPart = parts[0]; + var decimalPart = parts[1] ? (this.decimalSeparator || '.') + parts[1] : ''; + + return integerPart.replace(this.HUMANIZED_NUM_RE, '$1' + this.orderSeparator) + decimalPart; + }, + + /** + Takes a string, possibly formatted with `orderSeparator` and/or + `decimalSeparator`, and convert it back to a number. + + @member Backgrid.NumberFormatter + @param {string} formattedData + @return {number|undefined} Undefined if the string cannot be converted to + a number. + */ + toRaw: function (formattedData) { + var rawData = ''; + + var thousands = formattedData.trim().split(this.orderSeparator); + for (var i = 0; i < thousands.length; i++) { + rawData += thousands[i]; + } + + var decimalParts = rawData.split(this.decimalSeparator); + rawData = ''; + for (var i = 0; i < decimalParts.length; i++) { + rawData = rawData + decimalParts[i] + '.'; + } + + if (rawData[rawData.length - 1] === '.') { + rawData = rawData.slice(0, rawData.length - 1); + } + + var result = (rawData * 1).toFixed(~~this.decimals) * 1; + if (_.isNumber(result) && !_.isNaN(result)) return result; + } + +}); + +/** + Formatter to converts between various datetime 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) { + 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) { + if (_.isNull(rawData) || _.isUndefined(rawData)) return ''; + return this._convert(rawData); + }, + + /** + Converts an ISO-8601 formatted datetime string to a datetime string, date + string or a time string. The timezone is ignored if supplied. This method + parses the input values exactly the same way as + Backgrid.Extension.MomentFormatter#fromRaw(), in addition to doing some + sanity checks. + + @member Backgrid.DatetimeFormatter + @param {string} formattedData + @return {string|undefined} ISO-8601 string in UTC. Undefined if a date is + found when `includeDate` is false, or a time is found when `includeTime` is + false, or if `includeDate` is true and a date is not found, or if + `includeTime` is true and a time is not found. + */ + toRaw: function (formattedData) { + return this._convert(formattedData, true); + } + +}); + +/** + Formatter to convert any value to string. + + @class Backgrid.StringFormatter + @extends Backgrid.CellFormatter + @constructor + */ +var StringFormatter = Backgrid.StringFormatter = function () {}; +StringFormatter.prototype = new CellFormatter(); +_.extend(StringFormatter.prototype, { + /** + Converts any value to a string using Ecmascript's implicit type + conversion. If the given value is `null` or `undefined`, an empty string is + returned instead. + + @member Backgrid.StringFormatter + @param {*} rawValue + @return {string} + */ + fromRaw: function (rawValue) { + if (_.isUndefined(rawValue) || _.isNull(rawValue)) return ''; + return rawValue + ''; + } +}); + +/** + Simple email validation formatter. + + @class Backgrid.EmailFormatter + @extends Backgrid.CellFormatter + @constructor + */ +var EmailFormatter = Backgrid.EmailFormatter = function () {}; +EmailFormatter.prototype = new CellFormatter(); +_.extend(EmailFormatter.prototype, { + /** + Return the input if it is a string that contains an '@' character and if + the strings before and after '@' are non-empty. If the input does not + validate, `undefined` is returned. + + @member Backgrid.EmailFormatter + @param {*} formattedData + @return {string|undefined} + */ + toRaw: function (formattedData) { + var parts = formattedData.trim().split("@"); + if (parts.length === 2 && _.all(parts)) { + return formattedData; + } + } +}); +/* + 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 {*} 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, "backgrid:editing", this.postRender); + } + + this.listenTo(this, "backgrid:done", this.remove); + }, + + /** + 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); + } + }, + + /** + 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 `backgrid:done` event when successful. `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; + + // enter or tab or blur + if (e.keyCode === 13 || e.keyCode === 9 || e.type === "blur") { + e.preventDefault(); + var newValue = formatter.toRaw(this.$el.val()); + if (_.isUndefined(newValue) || + !model.set(column.get("name"), newValue, {validate: true})) { + this.trigger("backgrid:error", this); + + if (e.type === "blur") { + var self = this; + var timeout = window.setTimeout(function () { + self.$el.focus(); + window.clearTimeout(timeout); + }, 1); + } + } + else { + this.trigger("backgrid:done", this); + } + } + // esc + else if (e.keyCode === 27) { + // undo + e.stopPropagation(); + this.trigger("backgrid:done", this); + } + }, + + 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(); + this.$el.text(this.formatter.fromRaw(this.model.get(this.column.get("name")))); + this.delegateEvents(); + return this; + }, + + /** + If this column is editable, a new CellEditor instance is instantiated with + its required parameters and listens on the editor's `backgrid:done` and + `backgrid:error` events. When the editor is `done`, edit mode is + exited. When the editor triggers an `backgrid: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. + + This method triggers a Backbone `backgrid:edit` event 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. The cell and the constructed cell + instance are also sent as parameters in the event. + */ + enterEditMode: function () { + if (this.column.get("editable")) { + + this.currentEditor = new this.editor({ + parent: this, + column: this.column, + model: this.model, + formatter: this.formatter + }); + + this.trigger("backgrid:edit", this, this.currentEditor); + + this.listenTo(this.currentEditor, "backgrid:done", this.exitEditMode); + this.listenTo(this.currentEditor, "backgrid:error", this.renderError); + + this.$el.empty(); + this.undelegateEvents(); + this.$el.append(this.currentEditor.$el); + this.currentEditor.render(); + this.$el.addClass("editor"); + + this.trigger("backgrid: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.stopListening(this.currentEditor); + 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", + + formatter: new StringFormatter() + +}); + +/** + UriCell renders an HTML `` anchor for the value and accepts URIs as user + input values. No type conversion or URL validation is done by the formatter + of this cell. Users who need URL validation are encourage to subclass UriCell + to take advantage of the parsing capabilities of the HTMLAnchorElement + available on HTML5-capable browsers or using a third-party library like + [URI.js](https://github.com/medialize/URI.js). + + @class Backgrid.UriCell + @extends Backgrid.Cell +*/ +var UriCell = Backgrid.UriCell = Cell.extend({ + + /** @property */ + className: "uri-cell", + + render: function () { + this.$el.empty(); + var formattedValue = this.formatter.fromRaw(this.model.get(this.column.get("name"))); + this.$el.append($("", { + href: formattedValue, + title: formattedValue, + target: "_blank" + }).text(formattedValue)); + this.delegateEvents(); + 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.StringCell +*/ +var EmailCell = Backgrid.EmailCell = StringCell.extend({ + + /** @property */ + className: "email-cell", + + formatter: new EmailFormatter(), + + render: function () { + this.$el.empty(); + var formattedValue = this.formatter.fromRaw(this.model.get(this.column.get("name"))); + this.$el.append($("", { + href: "mailto:" + formattedValue, + title: formattedValue + }).text(formattedValue)); + this.delegateEvents(); + return this; + } + +}); + +/** + NumberCell is a generic cell that renders all numbers. Numbers are formatted + using a Backgrid.NumberFormatter. + + @class Backgrid.NumberCell + @extends Backgrid.Cell +*/ +var NumberCell = Backgrid.NumberCell = Cell.extend({ + + /** @property */ + className: "number-cell", + + /** + @property {number} [decimals=2] Must be an integer. + */ + decimals: NumberFormatter.prototype.defaults.decimals, + + /** @property {string} [decimalSeparator='.'] */ + decimalSeparator: NumberFormatter.prototype.defaults.decimalSeparator, + + /** @property {string} [orderSeparator=','] */ + orderSeparator: NumberFormatter.prototype.defaults.orderSeparator, + + /** @property {Backgrid.CellFormatter} [formatter=Backgrid.NumberFormatter] */ + formatter: NumberFormatter, + + /** + Initializes this cell and the number formatter. + + @param {Object} options + @param {Backbone.Model} options.model + @param {Backgrid.Column} options.column + */ + initialize: function (options) { + Cell.prototype.initialize.apply(this, arguments); + this.formatter = new this.formatter({ + decimals: this.decimals, + decimalSeparator: this.decimalSeparator, + orderSeparator: this.orderSeparator + }); + } + +}); + +/** + An IntegerCell is just a Backgrid.NumberCell with 0 decimals. If a floating + point number is supplied, the number is simply rounded the usual way when + displayed. + + @class Backgrid.IntegerCell + @extends Backgrid.NumberCell +*/ +var IntegerCell = Backgrid.IntegerCell = NumberCell.extend({ + + /** @property */ + className: "integer-cell", + + /** + @property {number} decimals Must be an integer. + */ + decimals: 0 +}); + +/** + DatetimeCell is a basic cell that accepts datetime string values in RFC-2822 + or W3C's subset of ISO-8601 and displays them in ISO-8601 format. For a much + more sophisticated date time cell with better datetime formatting, take a + look at the Backgrid.Extension.MomentCell extension. + + @class Backgrid.DatetimeCell + @extends Backgrid.Cell + + See: + + - Backgrid.Extension.MomentCell + - Backgrid.DatetimeFormatter +*/ +var DatetimeCell = Backgrid.DatetimeCell = Cell.extend({ + + /** @property */ + className: "datetime-cell", + + /** + @property {boolean} [includeDate=true] + */ + includeDate: DatetimeFormatter.prototype.defaults.includeDate, + + /** + @property {boolean} [includeTime=true] + */ + includeTime: DatetimeFormatter.prototype.defaults.includeTime, + + /** + @property {boolean} [includeMilli=false] + */ + includeMilli: DatetimeFormatter.prototype.defaults.includeMilli, + + /** @property {Backgrid.CellFormatter} [formatter=Backgrid.DatetimeFormatter] */ + formatter: DatetimeFormatter, + + /** + Initializes this cell and the datetime formatter. + + @param {Object} options + @param {Backbone.Model} options.model + @param {Backgrid.Column} options.column + */ + initialize: function (options) { + Cell.prototype.initialize.apply(this, arguments); + this.formatter = new this.formatter({ + includeDate: this.includeDate, + includeTime: this.includeTime, + includeMilli: this.includeMilli + }); + + var placeholder = this.includeDate ? "YYYY-MM-DD" : ""; + placeholder += (this.includeDate && this.includeTime) ? "T" : ""; + placeholder += this.includeTime ? "HH:mm:ss" : ""; + placeholder += (this.includeTime && this.includeMilli) ? ".SSS" : ""; + + this.editor = this.editor.extend({ + attributes: _.extend({}, this.editor.prototype.attributes, this.editor.attributes, { + placeholder: placeholder + }) + }); + } + +}); + +/** + DateCell is a Backgrid.DatetimeCell without the time part. + + @class Backgrid.DateCell + @extends Backgrid.DatetimeCell +*/ +var DateCell = Backgrid.DateCell = DatetimeCell.extend({ + + /** @property */ + className: "date-cell", + + /** @property */ + includeTime: false + +}); + +/** + TimeCell is a Backgrid.DatetimeCell without the date part. + + @class Backgrid.TimeCell + @extends Backgrid.DatetimeCell +*/ +var TimeCell = Backgrid.TimeCell = DatetimeCell.extend({ + + /** @property */ + className: "time-cell", + + /** @property */ + includeDate: false + +}); + +/** + 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); + this.delegateEvents(); + 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 `