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 `