backgridjs-rails 0.2.6 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,18 @@
5
5
  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
6
6
  Licensed under the MIT @license.
7
7
  */
8
- (function (window, $, _, Backbone, Backgrid) {
8
+ (function (root, factory) {
9
+
10
+ // CommonJS
11
+ if (typeof exports == "object") {
12
+ module.exports = factory(require("backbone"), require("backgrid"), require("underscore"));
13
+ }
14
+ // Browser
15
+ else factory(root.Backbone, root.Backgrid, root._);
16
+
17
+ }(this, function (Backbone, Backgrid, _) {
18
+
19
+ "use strict";
9
20
 
10
21
  /**
11
22
  Renders a checkbox for row selection.
@@ -23,9 +34,9 @@
23
34
 
24
35
  /** @property */
25
36
  events: {
26
- "keydown :checkbox": "onKeydown",
27
- "change :checkbox": "onChange",
28
- "click :checkbox": "enterEditMode"
37
+ "keydown input[type=checkbox]": "onKeydown",
38
+ "change input[type=checkbox]": "onChange",
39
+ "click input[type=checkbox]": "enterEditMode"
29
40
  },
30
41
 
31
42
  /**
@@ -37,31 +48,43 @@
37
48
  @param {Backbone.Model} options.model
38
49
  */
39
50
  initialize: function (options) {
40
- Backgrid.requireOptions(options, ["model", "column"]);
41
51
 
42
52
  this.column = options.column;
43
53
  if (!(this.column instanceof Backgrid.Column)) {
44
54
  this.column = new Backgrid.Column(this.column);
45
55
  }
46
56
 
47
- this.listenTo(this.model, "backgrid:select", function (model, selected) {
48
- this.$el.find(":checkbox").prop("checked", selected).change();
57
+ var column = this.column, model = this.model, $el = this.$el;
58
+ this.listenTo(column, "change:renderable", function (column, renderable) {
59
+ $el.toggleClass("renderable", renderable);
49
60
  });
50
61
 
62
+ if (Backgrid.callByNeed(column.renderable(), column, model)) $el.addClass("renderable");
63
+
64
+ this.listenTo(model, "backgrid:select", function (model, selected) {
65
+ this.checkbox().prop("checked", selected).change();
66
+ });
67
+ },
68
+
69
+ /**
70
+ Returns the checkbox.
71
+ */
72
+ checkbox: function () {
73
+ return this.$el.find("input[type=checkbox]");
51
74
  },
52
75
 
53
76
  /**
54
77
  Focuses the checkbox.
55
78
  */
56
79
  enterEditMode: function () {
57
- this.$el.find(":checkbox").focus();
80
+ this.checkbox().focus();
58
81
  },
59
82
 
60
83
  /**
61
84
  Unfocuses the checkbox.
62
85
  */
63
86
  exitEditMode: function () {
64
- this.$el.find(":checkbox").blur();
87
+ this.checkbox().blur();
65
88
  },
66
89
 
67
90
  /**
@@ -72,7 +95,7 @@
72
95
  if (command.passThru()) return true; // skip ahead to `change`
73
96
  if (command.cancel()) {
74
97
  e.stopPropagation();
75
- this.$el.find(":checkbox").blur();
98
+ this.checkbox().blur();
76
99
  }
77
100
  else if (command.save() || command.moveLeft() || command.moveRight() ||
78
101
  command.moveUp() || command.moveDown()) {
@@ -87,8 +110,10 @@
87
110
  `backgrid:selected` event with a reference of the model and the
88
111
  checkbox's `checked` value.
89
112
  */
90
- onChange: function (e) {
91
- this.model.trigger("backgrid:selected", this.model, $(e.target).prop("checked"));
113
+ onChange: function () {
114
+ var checked = this.checkbox().prop("checked");
115
+ this.$el.parent().toggleClass("selected", checked);
116
+ this.model.trigger("backgrid:selected", this.model, checked);
92
117
  },
93
118
 
94
119
  /**
@@ -135,7 +160,6 @@
135
160
  @param {Backbone.Collection} options.collection
136
161
  */
137
162
  initialize: function (options) {
138
- Backgrid.requireOptions(options, ["column", "collection"]);
139
163
 
140
164
  this.column = options.column;
141
165
  if (!(this.column instanceof Backgrid.Column)) {
@@ -144,49 +168,86 @@
144
168
 
145
169
  var collection = this.collection;
146
170
  var selectedModels = this.selectedModels = {};
147
- this.listenTo(collection, "backgrid:selected", function (model, selected) {
148
- if (selected) selectedModels[model.id || model.cid] = model;
171
+ this.listenTo(collection.fullCollection || collection,
172
+ "backgrid:selected", function (model, selected) {
173
+ if (selected) selectedModels[model.id || model.cid] = 1;
149
174
  else {
150
175
  delete selectedModels[model.id || model.cid];
151
- this.$el.find(":checkbox").prop("checked", false);
176
+ this.checkbox().prop("checked", false);
177
+ }
178
+ if (_.keys(selectedModels).length === (collection.fullCollection|| collection).length) {
179
+ this.checkbox().prop("checked", true);
152
180
  }
153
181
  });
154
182
 
155
- this.listenTo(collection, "remove", function (model) {
156
- delete selectedModels[model.cid];
183
+ this.listenTo(collection.fullCollection || collection, "remove", function (model) {
184
+ delete selectedModels[model.id || model.cid];
185
+ if ((collection.fullCollection || collection).length === 0) {
186
+ this.checkbox().prop("checked", false);
187
+ }
157
188
  });
158
189
 
159
190
  this.listenTo(collection, "backgrid:refresh", function () {
160
- this.$el.find(":checkbox").prop("checked", false);
161
- for (var i = 0; i < collection.length; i++) {
162
- var model = collection.at(i);
163
- if (selectedModels[model.id || model.cid]) {
164
- model.trigger('backgrid:select', model, true);
191
+ if ((collection.fullCollection || collection).length === 0) {
192
+ this.checkbox().prop("checked", false);
193
+ }
194
+ else {
195
+ var checked = this.checkbox().prop("checked");
196
+ for (var i = 0; i < collection.length; i++) {
197
+ var model = collection.at(i);
198
+ if (checked || selectedModels[model.id || model.cid]) {
199
+ model.trigger("backgrid:select", model, true);
200
+ }
165
201
  }
166
202
  }
167
203
  });
204
+
205
+ var column = this.column, $el = this.$el;
206
+ this.listenTo(column, "change:renderable", function (column, renderable) {
207
+ $el.toggleClass("renderable", renderable);
208
+ });
209
+
210
+ if (Backgrid.callByNeed(column.renderable(), column, collection)) $el.addClass("renderable");
168
211
  },
169
212
 
170
213
  /**
171
- Progagates the checked value of this checkbox to all the models of the
214
+ Propagates the checked value of this checkbox to all the models of the
172
215
  underlying collection by triggering a Backbone `backgrid:select` event on
173
- the models themselves, passing each model and the current `checked` value
174
- of the checkbox in each event.
216
+ the models on the current page, passing each model and the current
217
+ `checked` value of the checkbox in each event.
218
+
219
+ A `backgrid:selected` event will also be triggered with the current
220
+ `checked` value on all the models regardless of whether they are on the
221
+ current page.
222
+
223
+ This method triggers a 'backgrid:select-all' event on the collection
224
+ afterwards.
175
225
  */
176
- onChange: function (e) {
177
- var checked = $(e.target).prop("checked");
226
+ onChange: function () {
227
+ var checked = this.checkbox().prop("checked");
178
228
 
179
229
  var collection = this.collection;
180
230
  collection.each(function (model) {
181
231
  model.trigger("backgrid:select", model, checked);
182
232
  });
233
+
234
+ if (collection.fullCollection) {
235
+ collection.fullCollection.each(function (model) {
236
+ if (!collection.get(model.cid)) {
237
+ model.trigger("backgrid:selected", model, checked);
238
+ }
239
+ });
240
+ }
241
+
242
+ this.collection.trigger("backgrid:select-all", this.collection, checked);
183
243
  }
184
244
 
185
245
  });
186
246
 
187
247
  /**
188
248
  Convenient method to retrieve a list of selected models. This method only
189
- exists when the `SelectAll` extension has been included.
249
+ exists when the `SelectAll` extension has been included. Selected models
250
+ are retained across pagination.
190
251
 
191
252
  @member Backgrid.Grid
192
253
  @return {Array.<Backbone.Model>}
@@ -204,12 +265,28 @@
204
265
 
205
266
  var result = [];
206
267
  if (selectAllHeaderCell) {
207
- for (var modelId in selectAllHeaderCell.selectedModels) {
208
- result.push(this.collection.get(modelId));
268
+ var selectedModels = selectAllHeaderCell.selectedModels;
269
+ var collection = this.collection.fullCollection || this.collection;
270
+ for (var modelId in selectedModels) {
271
+ result.push(collection.get(modelId));
209
272
  }
210
273
  }
211
274
 
212
275
  return result;
213
276
  };
214
277
 
215
- }(window, jQuery, _, Backbone, Backgrid));
278
+ /**
279
+ Convenient method to deselect the selected models. This method is only
280
+ available when the `SelectAll` extension has been included.
281
+
282
+ @member Backgrid.Grid
283
+ */
284
+ Backgrid.Grid.prototype.clearSelectedModels = function () {
285
+ var selectedModels = this.getSelectedModels();
286
+ for (var i = 0, l = selectedModels.length; i < l; i++) {
287
+ var model = selectedModels[i];
288
+ model.trigger("backgrid:select", model, false);
289
+ }
290
+ };
291
+
292
+ }));
@@ -5,8 +5,21 @@
5
5
  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
6
6
  Licensed under the MIT @license.
7
7
  */
8
+ (function (root, factory) {
8
9
 
9
- (function (window, $, _, Backbone, Backgrid) {
10
+ // CommonJS
11
+ if (typeof exports == "object") {
12
+ require("select2");
13
+ module.exports = factory(root,
14
+ require("underscore"),
15
+ require("backgrid"));
16
+ }
17
+ // Browser
18
+ else factory(root, root._, root.Backgrid);
19
+
20
+ }(this, function (root, _, Backgrid) {
21
+
22
+ "use strict";
10
23
 
11
24
  /**
12
25
  Select2CellEditor is a cell editor that renders a `select2` select box
@@ -23,12 +36,13 @@
23
36
 
24
37
  /** @property */
25
38
  events: {
26
- "close": "save",
27
39
  "change": "save"
28
40
  },
29
41
 
30
42
  /** @property */
31
- select2Options: null,
43
+ select2Options: {
44
+ openOnEnter: false
45
+ },
32
46
 
33
47
  initialize: function () {
34
48
  Backgrid.SelectCellEditor.prototype.initialize.apply(this, arguments);
@@ -40,8 +54,7 @@
40
54
  edit mode.
41
55
  */
42
56
  setSelect2Options: function (options) {
43
- this.select2Options = _.extend({containerCssClass: "select2-container"},
44
- options || {});
57
+ this.select2Options = _.extend(options || {});
45
58
  },
46
59
 
47
60
  /**
@@ -53,7 +66,6 @@
53
66
  render: function () {
54
67
  Backgrid.SelectCellEditor.prototype.render.apply(this, arguments);
55
68
  this.$el.select2(this.select2Options);
56
- this.delegateEvents();
57
69
  return this;
58
70
  },
59
71
 
@@ -62,13 +74,29 @@
62
74
  */
63
75
  postRender: function () {
64
76
  var self = this;
65
- this.$el.parent()
66
- .find("." + this.select2Options.containerCssClass)
67
- .on("blur", function (e) {
68
- if (!e.relatedTarget) self.close(e);
69
- })
70
- .on("keydown", this.close)
71
- .attr("tabindex", -1).focus();
77
+ if (self.multiple) self.$el.select2("container").keydown(self.close);
78
+ else self.$el.data("select2").focusser.keydown(self.close);
79
+
80
+ self.$el.on("select2-blur", function (e) {
81
+ if (!self.multiple) {
82
+ e.type = "blur";
83
+ self.close(e);
84
+ }
85
+ else {
86
+ // HACK to get around https://github.com/ivaynberg/select2/issues/2011
87
+ // select2-blur is triggered from blur and is fired repeatibly under
88
+ // multiple select. Since blue is fired before everything, but focus
89
+ // is set in focus and click, we need to wait for a while so other
90
+ // event handlers may have a chance to run.
91
+ var id = root.setTimeout(function () {
92
+ root.clearTimeout(id);
93
+ if (!self.$el.select2("isFocused")) {
94
+ e.type = "blur";
95
+ self.close(e);
96
+ }
97
+ }, 200);
98
+ }
99
+ }).select2("focus");
72
100
  },
73
101
 
74
102
  remove: function () {
@@ -118,4 +146,4 @@
118
146
 
119
147
  });
120
148
 
121
- }(window, jQuery, _, Backbone, Backgrid));
149
+ }));
@@ -5,8 +5,17 @@
5
5
  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
6
6
  Licensed under the MIT @license.
7
7
  */
8
+ (function (root, factory) {
8
9
 
9
- (function (window, $, _, Backbone, Backgrid) {
10
+ // CommonJS
11
+ if (typeof exports == "object") {
12
+ module.exports = factory(require("underscore"),
13
+ require("backgrid"));
14
+ }
15
+ // Browser
16
+ else factory(root._, root.Backgrid);
17
+
18
+ }(this, function (_, Backgrid) {
10
19
 
11
20
  /**
12
21
  Renders a form with a text area and a save button in a modal dialog.
@@ -20,10 +29,10 @@
20
29
  tagName: "div",
21
30
 
22
31
  /** @property */
23
- className: "modal hide fade",
32
+ className: "modal fade",
24
33
 
25
34
  /** @property {function(Object, ?Object=): string} template */
26
- template: _.template('<form><div class="modal-header"><button type="button" class="close" data-dismiss="modal">&times;</button><h3><%- column.get("label") %></h3></div><div class="modal-body"><textarea cols="<%= cols %>" rows="<%= rows %>"><%- content %></textarea></div><div class="modal-footer"><input class="btn" type="submit" value="Save"/></div></form>'),
35
+ template: _.template('<div class="modal-dialog"><div class="modal-content"><form><div class="modal-header"><button type="button" class="close" data-dismiss="modal">&times;</button><h3><%- column.get("label") %></h3></div><div class="modal-body"><textarea cols="<%= cols %>" rows="<%= rows %>"><%- content %></textarea></div><div class="modal-footer"><input class="btn btn-primary" type="submit" value="Save"/></div></form></div></div>', null, {variable: null}),
27
36
 
28
37
  /** @property */
29
38
  cols: 80,
@@ -35,9 +44,9 @@
35
44
  events: {
36
45
  "keydown textarea": "clearError",
37
46
  "submit": "saveOrCancel",
38
- "hide": "saveOrCancel",
39
- "hidden": "close",
40
- "shown": "focus"
47
+ "hide.bs.modal": "saveOrCancel",
48
+ "hidden.bs.modal": "close",
49
+ "shown.bs.modal": "focus"
41
50
  },
42
51
 
43
52
  /**
@@ -100,7 +109,7 @@
100
109
  else if (!e || e.type == "submit" ||
101
110
  (e.type == "hide" &&
102
111
  newValue !== (this.model.get(this.column.get("name")) || '').replace(/\r/g, '') &&
103
- window.confirm("Would you like to save your changes?"))) {
112
+ confirm("Would you like to save your changes?"))) {
104
113
 
105
114
  model.set(column.get("name"), newValue);
106
115
  this.$el.modal("hide");
@@ -146,7 +155,7 @@
146
155
  @class Backgrid.Extension.TextCell
147
156
  @extends Backgrid.StringCell
148
157
  */
149
- var TextCell = Backgrid.Extension.TextCell = Backgrid.StringCell.extend({
158
+ Backgrid.Extension.TextCell = Backgrid.StringCell.extend({
150
159
 
151
160
  /** @property */
152
161
  className: "text-cell",
@@ -156,4 +165,4 @@
156
165
 
157
166
  });
158
167
 
159
- }(window, jQuery, _, Backbone, Backgrid));
168
+ }));
@@ -1,23 +1,36 @@
1
- /*
2
- backgrid
1
+ /*!
2
+ backgrid 0.3.5
3
3
  http://github.com/wyuenho/backgrid
4
4
 
5
- Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
6
- Licensed under the MIT @license.
5
+ Copyright (c) 2014 Jimmy Yuen Ho Wong and contributors <wyuenho@gmail.com>
6
+ Licensed under the MIT license.
7
7
  */
8
- (function (root, $, _, Backbone) {
8
+
9
+ (function (root, factory) {
10
+
11
+ if (typeof define === "function" && define.amd) {
12
+ // AMD (+ global for extensions)
13
+ define(["underscore", "backbone"], function (_, Backbone) {
14
+ return (root.Backgrid = factory(_, Backbone));
15
+ });
16
+ } else if (typeof exports === "object") {
17
+ // CommonJS
18
+ module.exports = factory(require("underscore"), require("backbone"));
19
+ } else {
20
+ // Browser
21
+ root.Backgrid = factory(root._, root.Backbone);
22
+ }}(this, function (_, Backbone) {
9
23
 
10
24
  "use strict";
25
+
11
26
  /*
12
27
  backgrid
13
28
  http://github.com/wyuenho/backgrid
14
29
 
15
30
  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
16
- Licensed under the MIT @license.
31
+ Licensed under the MIT license.
17
32
  */
18
33
 
19
- var window = root;
20
-
21
34
  // Copyright 2009, 2010 Kristopher Michael Kowal
22
35
  // https://github.com/kriskowal/es5-shim
23
36
  // ES5 15.5.4.20
@@ -41,10 +54,6 @@ if (!String.prototype.trim || ws.trim()) {
41
54
  };
42
55
  }
43
56
 
44
- function capitalize(s) {
45
- return String.fromCharCode(s.charCodeAt(0) - 32) + s.slice(1);
46
- }
47
-
48
57
  function lpad(str, length, padstr) {
49
58
  var paddingLen = length - (str + '').length;
50
59
  paddingLen = paddingLen < 0 ? 0 : paddingLen;
@@ -55,24 +64,17 @@ function lpad(str, length, padstr) {
55
64
  return padding + str;
56
65
  }
57
66
 
58
- var Backgrid = root.Backgrid = {
67
+ var $ = Backbone.$;
59
68
 
60
- VERSION: "0.2.6",
69
+ var Backgrid = {
61
70
 
62
71
  Extension: {},
63
72
 
64
- requireOptions: function (options, requireOptionKeys) {
65
- for (var i = 0; i < requireOptionKeys.length; i++) {
66
- var key = requireOptionKeys[i];
67
- if (_.isUndefined(options[key])) {
68
- throw new TypeError("'" + key + "' is required");
69
- }
70
- }
71
- },
72
-
73
73
  resolveNameToClass: function (name, suffix) {
74
74
  if (_.isString(name)) {
75
- var key = _.map(name.split('-'), function (e) { return capitalize(e); }).join('') + suffix;
75
+ var key = _.map(name.split('-'), function (e) {
76
+ return e.slice(0, 1).toUpperCase() + e.slice(1);
77
+ }).join('') + suffix;
76
78
  var klass = Backgrid[key] || Backgrid.Extension[key];
77
79
  if (_.isUndefined(klass)) {
78
80
  throw new ReferenceError("Class '" + key + "' not found");
@@ -81,7 +83,17 @@ var Backgrid = root.Backgrid = {
81
83
  }
82
84
 
83
85
  return name;
86
+ },
87
+
88
+ callByNeed: function () {
89
+ var value = arguments[0];
90
+ if (!_.isFunction(value)) return value;
91
+
92
+ var context = arguments[1];
93
+ var args = [].slice.call(arguments, 2);
94
+ return value.apply(context, !!(args + '') ? args : []);
84
95
  }
96
+
85
97
  };
86
98
  _.extend(Backgrid, Backbone.Events);
87
99
 
@@ -99,7 +111,7 @@ _.extend(Backgrid, Backbone.Events);
99
111
  var Command = Backgrid.Command = function (evt) {
100
112
  _.extend(this, {
101
113
  altKey: !!evt.altKey,
102
- char: evt.char,
114
+ "char": evt["char"],
103
115
  charCode: evt.charCode,
104
116
  ctrlKey: !!evt.ctrlKey,
105
117
  key: evt.key,
@@ -165,7 +177,7 @@ _.extend(Command.prototype, {
165
177
  http://github.com/wyuenho/backgrid
166
178
 
167
179
  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
168
- Licensed under the MIT @license.
180
+ Licensed under the MIT license.
169
181
  */
170
182
 
171
183
  /**
@@ -189,9 +201,10 @@ _.extend(CellFormatter.prototype, {
189
201
 
190
202
  @member Backgrid.CellFormatter
191
203
  @param {*} rawData
204
+ @param {Backbone.Model} model Used for more complicated formatting
192
205
  @return {*}
193
206
  */
194
- fromRaw: function (rawData) {
207
+ fromRaw: function (rawData, model) {
195
208
  return rawData;
196
209
  },
197
210
 
@@ -204,16 +217,18 @@ _.extend(CellFormatter.prototype, {
204
217
 
205
218
  @member Backgrid.CellFormatter
206
219
  @param {string} formattedData
220
+ @param {Backbone.Model} model Used for more complicated formatting
207
221
  @return {*|undefined}
208
222
  */
209
- toRaw: function (formattedData) {
223
+ toRaw: function (formattedData, model) {
210
224
  return formattedData;
211
225
  }
212
226
 
213
227
  });
214
228
 
215
229
  /**
216
- A floating point number formatter. Doesn't understand notation at the moment.
230
+ A floating point number formatter. Doesn't understand scientific notation at
231
+ the moment.
217
232
 
218
233
  @class Backgrid.NumberFormatter
219
234
  @extends Backgrid.CellFormatter
@@ -221,8 +236,7 @@ _.extend(CellFormatter.prototype, {
221
236
  @throws {RangeError} If decimals < 0 or > 20.
222
237
  */
223
238
  var NumberFormatter = Backgrid.NumberFormatter = function (options) {
224
- options = options ? _.clone(options) : {};
225
- _.extend(this, this.defaults, options);
239
+ _.extend(this, this.defaults, options || {});
226
240
 
227
241
  if (this.decimals < 0 || this.decimals > 20) {
228
242
  throw new RangeError("decimals must be between 0 and 20");
@@ -259,9 +273,10 @@ _.extend(NumberFormatter.prototype, {
259
273
 
260
274
  @member Backgrid.NumberFormatter
261
275
  @param {number} number
276
+ @param {Backbone.Model} model Used for more complicated formatting
262
277
  @return {string}
263
278
  */
264
- fromRaw: function (number) {
279
+ fromRaw: function (number, model) {
265
280
  if (_.isNull(number) || _.isUndefined(number)) return '';
266
281
 
267
282
  number = number.toFixed(~~this.decimals);
@@ -279,13 +294,18 @@ _.extend(NumberFormatter.prototype, {
279
294
 
280
295
  @member Backgrid.NumberFormatter
281
296
  @param {string} formattedData
297
+ @param {Backbone.Model} model Used for more complicated formatting
282
298
  @return {number|undefined} Undefined if the string cannot be converted to
283
299
  a number.
284
300
  */
285
- toRaw: function (formattedData) {
301
+ toRaw: function (formattedData, model) {
302
+ formattedData = formattedData.trim();
303
+
304
+ if (formattedData === '') return null;
305
+
286
306
  var rawData = '';
287
307
 
288
- var thousands = formattedData.trim().split(this.orderSeparator);
308
+ var thousands = formattedData.split(this.orderSeparator);
289
309
  for (var i = 0; i < thousands.length; i++) {
290
310
  rawData += thousands[i];
291
311
  }
@@ -306,6 +326,76 @@ _.extend(NumberFormatter.prototype, {
306
326
 
307
327
  });
308
328
 
329
+ /**
330
+ A number formatter that converts a floating point number, optionally
331
+ multiplied by a multiplier, to a percentage string and vice versa.
332
+
333
+ @class Backgrid.PercentFormatter
334
+ @extends Backgrid.NumberFormatter
335
+ @constructor
336
+ @throws {RangeError} If decimals < 0 or > 20.
337
+ */
338
+ var PercentFormatter = Backgrid.PercentFormatter = function () {
339
+ Backgrid.NumberFormatter.apply(this, arguments);
340
+ };
341
+
342
+ PercentFormatter.prototype = new Backgrid.NumberFormatter(),
343
+
344
+ _.extend(PercentFormatter.prototype, {
345
+
346
+ /**
347
+ @member Backgrid.PercentFormatter
348
+ @cfg {Object} options
349
+
350
+ @cfg {number} [options.multiplier=1] The number used to multiply the model
351
+ value for display.
352
+
353
+ @cfg {string} [options.symbol='%'] The symbol to append to the percentage
354
+ string.
355
+ */
356
+ defaults: _.extend({}, NumberFormatter.prototype.defaults, {
357
+ multiplier: 1,
358
+ symbol: "%"
359
+ }),
360
+
361
+ /**
362
+ Takes a floating point number, where the number is first multiplied by
363
+ `multiplier`, then converted to a formatted string like
364
+ NumberFormatter#fromRaw, then finally append `symbol` to the end.
365
+
366
+ @member Backgrid.PercentFormatter
367
+ @param {number} rawValue
368
+ @param {Backbone.Model} model Used for more complicated formatting
369
+ @return {string}
370
+ */
371
+ fromRaw: function (number, model) {
372
+ var args = [].slice.call(arguments, 1);
373
+ args.unshift(number * this.multiplier);
374
+ return (NumberFormatter.prototype.fromRaw.apply(this, args) || "0") + this.symbol;
375
+ },
376
+
377
+ /**
378
+ Takes a string, possibly appended with `symbol` and/or `decimalSeparator`,
379
+ and convert it back to a number for the model like NumberFormatter#toRaw,
380
+ and then dividing it by `multiplier`.
381
+
382
+ @member Backgrid.PercentFormatter
383
+ @param {string} formattedData
384
+ @param {Backbone.Model} model Used for more complicated formatting
385
+ @return {number|undefined} Undefined if the string cannot be converted to
386
+ a number.
387
+ */
388
+ toRaw: function (formattedValue, model) {
389
+ var tokens = formattedValue.split(this.symbol);
390
+ if (tokens && tokens[0] && tokens[1] === "" || tokens[1] == null) {
391
+ var rawValue = NumberFormatter.prototype.toRaw.call(this, tokens[0]);
392
+ if (_.isUndefined(rawValue)) return rawValue;
393
+ return rawValue / this.multiplier;
394
+ }
395
+ }
396
+
397
+ });
398
+
309
399
  /**
310
400
  Formatter to converts between various datetime formats.
311
401
 
@@ -320,8 +410,7 @@ _.extend(NumberFormatter.prototype, {
320
410
  @throws {Error} If both `includeDate` and `includeTime` are false.
321
411
  */
322
412
  var DatetimeFormatter = Backgrid.DatetimeFormatter = function (options) {
323
- options = options ? _.clone(options) : {};
324
- _.extend(this, this.defaults, options);
413
+ _.extend(this, this.defaults, options || {});
325
414
 
326
415
  if (!this.includeDate && !this.includeTime) {
327
416
  throw new Error("Either includeDate or includeTime must be true");
@@ -355,6 +444,8 @@ _.extend(DatetimeFormatter.prototype, {
355
444
  ISO_SPLITTER_RE: /T|Z| +/,
356
445
 
357
446
  _convert: function (data, validate) {
447
+ if ((data + '').trim() === '') return null;
448
+
358
449
  var date, time = null;
359
450
  if (_.isNumber(data)) {
360
451
  var jsDate = new Date(data);
@@ -413,10 +504,11 @@ _.extend(DatetimeFormatter.prototype, {
413
504
 
414
505
  @member Backgrid.DatetimeFormatter
415
506
  @param {string} rawData
507
+ @param {Backbone.Model} model Used for more complicated formatting
416
508
  @return {string|null|undefined} ISO-8601 string in UTC. Null and undefined
417
509
  values are returned as is.
418
510
  */
419
- fromRaw: function (rawData) {
511
+ fromRaw: function (rawData, model) {
420
512
  if (_.isNull(rawData) || _.isUndefined(rawData)) return '';
421
513
  return this._convert(rawData);
422
514
  },
@@ -430,12 +522,13 @@ _.extend(DatetimeFormatter.prototype, {
430
522
 
431
523
  @member Backgrid.DatetimeFormatter
432
524
  @param {string} formattedData
525
+ @param {Backbone.Model} model Used for more complicated formatting
433
526
  @return {string|undefined} ISO-8601 string in UTC. Undefined if a date is
434
527
  found when `includeDate` is false, or a time is found when `includeTime` is
435
528
  false, or if `includeDate` is true and a date is not found, or if
436
529
  `includeTime` is true and a time is not found.
437
530
  */
438
- toRaw: function (formattedData) {
531
+ toRaw: function (formattedData, model) {
439
532
  return this._convert(formattedData, true);
440
533
  }
441
534
 
@@ -458,9 +551,10 @@ _.extend(StringFormatter.prototype, {
458
551
 
459
552
  @member Backgrid.StringFormatter
460
553
  @param {*} rawValue
554
+ @param {Backbone.Model} model Used for more complicated formatting
461
555
  @return {string}
462
556
  */
463
- fromRaw: function (rawValue) {
557
+ fromRaw: function (rawValue, model) {
464
558
  if (_.isUndefined(rawValue) || _.isNull(rawValue)) return '';
465
559
  return rawValue + '';
466
560
  }
@@ -483,9 +577,10 @@ _.extend(EmailFormatter.prototype, {
483
577
 
484
578
  @member Backgrid.EmailFormatter
485
579
  @param {*} formattedData
580
+ @param {Backbone.Model} model Used for more complicated formatting
486
581
  @return {string|undefined}
487
582
  */
488
- toRaw: function (formattedData) {
583
+ toRaw: function (formattedData, model) {
489
584
  var parts = formattedData.trim().split("@");
490
585
  if (parts.length === 2 && _.all(parts)) {
491
586
  return formattedData;
@@ -496,6 +591,11 @@ _.extend(EmailFormatter.prototype, {
496
591
  /**
497
592
  Formatter for SelectCell.
498
593
 
594
+ If the type of a model value is not a string, it is expected that a subclass
595
+ of this formatter is provided to the SelectCell, with #toRaw overridden to
596
+ convert the string value returned from the DOM back to whatever value is
597
+ expected in the model.
598
+
499
599
  @class Backgrid.SelectFormatter
500
600
  @extends Backgrid.CellFormatter
501
601
  @constructor
@@ -509,9 +609,10 @@ _.extend(SelectFormatter.prototype, {
509
609
 
510
610
  @member Backgrid.SelectFormatter
511
611
  @param {*} rawValue
612
+ @param {Backbone.Model} model Used for more complicated formatting
512
613
  @return {Array.<*>}
513
614
  */
514
- fromRaw: function (rawValue) {
615
+ fromRaw: function (rawValue, model) {
515
616
  return _.isArray(rawValue) ? rawValue : rawValue != null ? [rawValue] : [];
516
617
  }
517
618
  });
@@ -521,7 +622,7 @@ _.extend(SelectFormatter.prototype, {
521
622
  http://github.com/wyuenho/backgrid
522
623
 
523
624
  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
524
- Licensed under the MIT @license.
625
+ Licensed under the MIT license.
525
626
  */
526
627
 
527
628
  /**
@@ -546,7 +647,6 @@ var CellEditor = Backgrid.CellEditor = Backbone.View.extend({
546
647
  `model` or `column` are undefined.
547
648
  */
548
649
  initialize: function (options) {
549
- Backgrid.requireOptions(options, ["formatter", "column", "model"]);
550
650
  this.formatter = options.formatter;
551
651
  this.column = options.column;
552
652
  if (!(this.column instanceof Column)) {
@@ -605,7 +705,7 @@ var InputCellEditor = Backgrid.InputCellEditor = CellEditor.extend({
605
705
  @param {string} [options.placeholder]
606
706
  */
607
707
  initialize: function (options) {
608
- CellEditor.prototype.initialize.apply(this, arguments);
708
+ InputCellEditor.__super__.initialize.apply(this, arguments);
609
709
 
610
710
  if (options.placeholder) {
611
711
  this.$el.attr("placeholder", options.placeholder);
@@ -617,7 +717,8 @@ var InputCellEditor = Backgrid.InputCellEditor = CellEditor.extend({
617
717
  exists.
618
718
  */
619
719
  render: function () {
620
- this.$el.val(this.formatter.fromRaw(this.model.get(this.column.get("name"))));
720
+ var model = this.model;
721
+ this.$el.val(this.formatter.fromRaw(model.get(this.column.get("name")), model));
621
722
  return this;
622
723
  },
623
724
 
@@ -654,7 +755,7 @@ var InputCellEditor = Backgrid.InputCellEditor = CellEditor.extend({
654
755
  e.stopPropagation();
655
756
 
656
757
  var val = this.$el.val();
657
- var newValue = formatter.toRaw(val);
758
+ var newValue = formatter.toRaw(val, model);
658
759
  if (_.isUndefined(newValue)) {
659
760
  model.trigger("backgrid:error", model, column, val);
660
761
  }
@@ -703,9 +804,9 @@ var Cell = Backgrid.Cell = Backbone.View.extend({
703
804
  tagName: "td",
704
805
 
705
806
  /**
706
- @property {Backgrid.CellFormatter|Object|string} [formatter=new CellFormatter()]
807
+ @property {Backgrid.CellFormatter|Object|string} [formatter=CellFormatter]
707
808
  */
708
- formatter: new CellFormatter(),
809
+ formatter: CellFormatter,
709
810
 
710
811
  /**
711
812
  @property {Backgrid.CellEditor} [editor=Backgrid.InputCellEditor] The
@@ -732,17 +833,43 @@ var Cell = Backgrid.Cell = Backbone.View.extend({
732
833
  said name cannot be found in the Backgrid module.
733
834
  */
734
835
  initialize: function (options) {
735
- Backgrid.requireOptions(options, ["model", "column"]);
736
836
  this.column = options.column;
737
837
  if (!(this.column instanceof Column)) {
738
838
  this.column = new Column(this.column);
739
839
  }
740
- this.formatter = Backgrid.resolveNameToClass(this.column.get("formatter") || this.formatter, "Formatter");
840
+
841
+ var column = this.column, model = this.model, $el = this.$el;
842
+
843
+ var formatter = Backgrid.resolveNameToClass(column.get("formatter") ||
844
+ this.formatter, "Formatter");
845
+
846
+ if (!_.isFunction(formatter.fromRaw) && !_.isFunction(formatter.toRaw)) {
847
+ formatter = new formatter();
848
+ }
849
+
850
+ this.formatter = formatter;
851
+
741
852
  this.editor = Backgrid.resolveNameToClass(this.editor, "CellEditor");
742
- this.listenTo(this.model, "change:" + this.column.get("name"), function () {
743
- if (!this.$el.hasClass("editor")) this.render();
853
+
854
+ this.listenTo(model, "change:" + column.get("name"), function () {
855
+ if (!$el.hasClass("editor")) this.render();
744
856
  });
745
- this.listenTo(this.model, "backgrid:error", this.renderError);
857
+
858
+ this.listenTo(model, "backgrid:error", this.renderError);
859
+
860
+ this.listenTo(column, "change:editable change:sortable change:renderable",
861
+ function (column) {
862
+ var changed = column.changedAttributes();
863
+ for (var key in changed) {
864
+ if (changed.hasOwnProperty(key)) {
865
+ $el.toggleClass(key, changed[key]);
866
+ }
867
+ }
868
+ });
869
+
870
+ if (Backgrid.callByNeed(column.editable(), column, model)) $el.addClass("editable");
871
+ if (Backgrid.callByNeed(column.sortable(), column, model)) $el.addClass("sortable");
872
+ if (Backgrid.callByNeed(column.renderable(), column, model)) $el.addClass("renderable");
746
873
  },
747
874
 
748
875
  /**
@@ -751,7 +878,8 @@ var Cell = Backgrid.Cell = Backbone.View.extend({
751
878
  */
752
879
  render: function () {
753
880
  this.$el.empty();
754
- this.$el.text(this.formatter.fromRaw(this.model.get(this.column.get("name"))));
881
+ var model = this.model;
882
+ this.$el.text(this.formatter.fromRaw(model.get(this.column.get("name")), model));
755
883
  this.delegateEvents();
756
884
  return this;
757
885
  },
@@ -779,7 +907,8 @@ var Cell = Backgrid.Cell = Backbone.View.extend({
779
907
  var model = this.model;
780
908
  var column = this.column;
781
909
 
782
- if (column.get("editable")) {
910
+ var editable = Backgrid.callByNeed(column.editable(), column, model);
911
+ if (editable) {
783
912
 
784
913
  this.currentEditor = new this.editor({
785
914
  column: this.column,
@@ -828,10 +957,10 @@ var Cell = Backgrid.Cell = Backbone.View.extend({
828
957
  */
829
958
  remove: function () {
830
959
  if (this.currentEditor) {
831
- this.currentEditor.remove.apply(this, arguments);
960
+ this.currentEditor.remove.apply(this.currentEditor, arguments);
832
961
  delete this.currentEditor;
833
962
  }
834
- return Backbone.View.prototype.remove.apply(this, arguments);
963
+ return Cell.__super__.remove.apply(this, arguments);
835
964
  }
836
965
 
837
966
  });
@@ -847,7 +976,7 @@ var StringCell = Backgrid.StringCell = Cell.extend({
847
976
  /** @property */
848
977
  className: "string-cell",
849
978
 
850
- formatter: new StringFormatter()
979
+ formatter: StringFormatter
851
980
 
852
981
  });
853
982
 
@@ -867,14 +996,33 @@ var UriCell = Backgrid.UriCell = Cell.extend({
867
996
  /** @property */
868
997
  className: "uri-cell",
869
998
 
999
+ /**
1000
+ @property {string} [title] The title attribute of the generated anchor. It
1001
+ uses the display value formatted by the `formatter.fromRaw` by default.
1002
+ */
1003
+ title: null,
1004
+
1005
+ /**
1006
+ @property {string} [target="_blank"] The target attribute of the generated
1007
+ anchor.
1008
+ */
1009
+ target: "_blank",
1010
+
1011
+ initialize: function (options) {
1012
+ UriCell.__super__.initialize.apply(this, arguments);
1013
+ this.title = options.title || this.title;
1014
+ this.target = options.target || this.target;
1015
+ },
1016
+
870
1017
  render: function () {
871
1018
  this.$el.empty();
872
- var formattedValue = this.formatter.fromRaw(this.model.get(this.column.get("name")));
1019
+ var rawValue = this.model.get(this.column.get("name"));
1020
+ var formattedValue = this.formatter.fromRaw(rawValue, this.model);
873
1021
  this.$el.append($("<a>", {
874
1022
  tabIndex: -1,
875
- href: formattedValue,
876
- title: formattedValue,
877
- target: "_blank"
1023
+ href: rawValue,
1024
+ title: this.title || formattedValue,
1025
+ target: this.target
878
1026
  }).text(formattedValue));
879
1027
  this.delegateEvents();
880
1028
  return this;
@@ -895,11 +1043,12 @@ var EmailCell = Backgrid.EmailCell = StringCell.extend({
895
1043
  /** @property */
896
1044
  className: "email-cell",
897
1045
 
898
- formatter: new EmailFormatter(),
1046
+ formatter: EmailFormatter,
899
1047
 
900
1048
  render: function () {
901
1049
  this.$el.empty();
902
- var formattedValue = this.formatter.fromRaw(this.model.get(this.column.get("name")));
1050
+ var model = this.model;
1051
+ var formattedValue = this.formatter.fromRaw(model.get(this.column.get("name")), model);
903
1052
  this.$el.append($("<a>", {
904
1053
  tabIndex: -1,
905
1054
  href: "mailto:" + formattedValue,
@@ -945,12 +1094,11 @@ var NumberCell = Backgrid.NumberCell = Cell.extend({
945
1094
  @param {Backgrid.Column} options.column
946
1095
  */
947
1096
  initialize: function (options) {
948
- Cell.prototype.initialize.apply(this, arguments);
949
- this.formatter = new this.formatter({
950
- decimals: this.decimals,
951
- decimalSeparator: this.decimalSeparator,
952
- orderSeparator: this.orderSeparator
953
- });
1097
+ NumberCell.__super__.initialize.apply(this, arguments);
1098
+ var formatter = this.formatter;
1099
+ formatter.decimals = this.decimals;
1100
+ formatter.decimalSeparator = this.decimalSeparator;
1101
+ formatter.orderSeparator = this.orderSeparator;
954
1102
  }
955
1103
 
956
1104
  });
@@ -974,6 +1122,43 @@ var IntegerCell = Backgrid.IntegerCell = NumberCell.extend({
974
1122
  decimals: 0
975
1123
  });
976
1124
 
1125
+ /**
1126
+ A PercentCell is another Backgrid.NumberCell that takes a floating number,
1127
+ optionally multiplied by a multiplier and display it as a percentage.
1128
+
1129
+ @class Backgrid.PercentCell
1130
+ @extends Backgrid.NumberCell
1131
+ */
1132
+ var PercentCell = Backgrid.PercentCell = NumberCell.extend({
1133
+
1134
+ /** @property */
1135
+ className: "percent-cell",
1136
+
1137
+ /** @property {number} [multiplier=1] */
1138
+ multiplier: PercentFormatter.prototype.defaults.multiplier,
1139
+
1140
+ /** @property {string} [symbol='%'] */
1141
+ symbol: PercentFormatter.prototype.defaults.symbol,
1142
+
1143
+ /** @property {Backgrid.CellFormatter} [formatter=Backgrid.PercentFormatter] */
1144
+ formatter: PercentFormatter,
1145
+
1146
+ /**
1147
+ Initializes this cell and the percent formatter.
1148
+
1149
+ @param {Object} options
1150
+ @param {Backbone.Model} options.model
1151
+ @param {Backgrid.Column} options.column
1152
+ */
1153
+ initialize: function () {
1154
+ PercentCell.__super__.initialize.apply(this, arguments);
1155
+ var formatter = this.formatter;
1156
+ formatter.multiplier = this.multiplier;
1157
+ formatter.symbol = this.symbol;
1158
+ }
1159
+
1160
+ });
1161
+
977
1162
  /**
978
1163
  DatetimeCell is a basic cell that accepts datetime string values in RFC-2822
979
1164
  or W3C's subset of ISO-8601 and displays them in ISO-8601 format. For a much
@@ -1019,12 +1204,11 @@ var DatetimeCell = Backgrid.DatetimeCell = Cell.extend({
1019
1204
  @param {Backgrid.Column} options.column
1020
1205
  */
1021
1206
  initialize: function (options) {
1022
- Cell.prototype.initialize.apply(this, arguments);
1023
- this.formatter = new this.formatter({
1024
- includeDate: this.includeDate,
1025
- includeTime: this.includeTime,
1026
- includeMilli: this.includeMilli
1027
- });
1207
+ DatetimeCell.__super__.initialize.apply(this, arguments);
1208
+ var formatter = this.formatter;
1209
+ formatter.includeDate = this.includeDate;
1210
+ formatter.includeTime = this.includeTime;
1211
+ formatter.includeMilli = this.includeMilli;
1028
1212
 
1029
1213
  var placeholder = this.includeDate ? "YYYY-MM-DD" : "";
1030
1214
  placeholder += (this.includeDate && this.includeTime) ? "T" : "";
@@ -1107,7 +1291,8 @@ var BooleanCellEditor = Backgrid.BooleanCellEditor = CellEditor.extend({
1107
1291
  uncheck otherwise.
1108
1292
  */
1109
1293
  render: function () {
1110
- var val = this.formatter.fromRaw(this.model.get(this.column.get("name")));
1294
+ var model = this.model;
1295
+ var val = this.formatter.fromRaw(model.get(this.column.get("name")), model);
1111
1296
  this.$el.prop("checked", val);
1112
1297
  return this;
1113
1298
  },
@@ -1145,12 +1330,12 @@ var BooleanCellEditor = Backgrid.BooleanCellEditor = CellEditor.extend({
1145
1330
  command.moveDown()) {
1146
1331
  e.preventDefault();
1147
1332
  e.stopPropagation();
1148
- var val = formatter.toRaw($el.prop("checked"));
1333
+ var val = formatter.toRaw($el.prop("checked"), model);
1149
1334
  model.set(column.get("name"), val);
1150
1335
  model.trigger("backgrid:edited", model, column, command);
1151
1336
  }
1152
1337
  else if (e.type == "change") {
1153
- var val = formatter.toRaw($el.prop("checked"));
1338
+ var val = formatter.toRaw($el.prop("checked"), model);
1154
1339
  model.set(column.get("name"), val);
1155
1340
  $el.focus();
1156
1341
  }
@@ -1184,10 +1369,13 @@ var BooleanCell = Backgrid.BooleanCell = Cell.extend({
1184
1369
  */
1185
1370
  render: function () {
1186
1371
  this.$el.empty();
1372
+ var model = this.model, column = this.column;
1373
+ var editable = Backgrid.callByNeed(column.editable(), column, model);
1187
1374
  this.$el.append($("<input>", {
1188
1375
  tabIndex: -1,
1189
1376
  type: "checkbox",
1190
- checked: this.formatter.fromRaw(this.model.get(this.column.get("name")))
1377
+ checked: this.formatter.fromRaw(model.get(column.get("name")), model),
1378
+ disabled: !editable
1191
1379
  }));
1192
1380
  this.delegateEvents();
1193
1381
  return this;
@@ -1214,10 +1402,11 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({
1214
1402
  },
1215
1403
 
1216
1404
  /** @property {function(Object, ?Object=): string} template */
1217
- template: _.template('<option value="<%- value %>" <%= selected ? \'selected="selected"\' : "" %>><%- text %></option>'),
1405
+ template: _.template('<option value="<%- value %>" <%= selected ? \'selected="selected"\' : "" %>><%- text %></option>', null, {variable: null}),
1218
1406
 
1219
1407
  setOptionValues: function (optionValues) {
1220
1408
  this.optionValues = optionValues;
1409
+ this.optionValues = _.result(this, "optionValues");
1221
1410
  },
1222
1411
 
1223
1412
  setMultiple: function (multiple) {
@@ -1231,7 +1420,7 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({
1231
1420
  options = options + this.template({
1232
1421
  text: nvps[i][0],
1233
1422
  value: nvps[i][1],
1234
- selected: selectedValues.indexOf(nvps[i][1]) > -1
1423
+ selected: _.indexOf(selectedValues, nvps[i][1]) > -1
1235
1424
  });
1236
1425
  }
1237
1426
  return options;
@@ -1248,9 +1437,10 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({
1248
1437
  this.$el.empty();
1249
1438
 
1250
1439
  var optionValues = _.result(this, "optionValues");
1251
- var selectedValues = this.formatter.fromRaw(this.model.get(this.column.get("name")));
1440
+ var model = this.model;
1441
+ var selectedValues = this.formatter.fromRaw(model.get(this.column.get("name")), model);
1252
1442
 
1253
- if (!_.isArray(optionValues)) throw TypeError("optionValues must be an array");
1443
+ if (!_.isArray(optionValues)) throw new TypeError("optionValues must be an array");
1254
1444
 
1255
1445
  var optionValue = null;
1256
1446
  var optionText = null;
@@ -1268,17 +1458,17 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({
1268
1458
  this.$el.append(this.template({
1269
1459
  text: optionText,
1270
1460
  value: optionValue,
1271
- selected: selectedValues.indexOf(optionValue) > -1
1461
+ selected: _.indexOf(selectedValues, optionValue) > -1
1272
1462
  }));
1273
1463
  }
1274
1464
  else if (_.isObject(optionValue)) {
1275
1465
  optgroupName = optionValue.name;
1276
1466
  optgroup = $("<optgroup></optgroup>", { label: optgroupName });
1277
- optgroup.append(this._renderOptions(optionValue.values, selectedValues));
1467
+ optgroup.append(this._renderOptions.call(this, optionValue.values, selectedValues));
1278
1468
  this.$el.append(optgroup);
1279
1469
  }
1280
1470
  else {
1281
- throw TypeError("optionValues elements must be a name-value pair or an object hash of { name: 'optgroup label', value: [option name-value pairs] }");
1471
+ throw new TypeError("optionValues elements must be a name-value pair or an object hash of { name: 'optgroup label', value: [option name-value pairs] }");
1282
1472
  }
1283
1473
  }
1284
1474
 
@@ -1288,14 +1478,12 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({
1288
1478
  },
1289
1479
 
1290
1480
  /**
1291
- Saves the value of the selected option to the model attribute. Triggers a
1292
- `backgrid:edited` Backbone event from the model.
1481
+ Saves the value of the selected option to the model attribute.
1293
1482
  */
1294
1483
  save: function (e) {
1295
1484
  var model = this.model;
1296
1485
  var column = this.column;
1297
- model.set(column.get("name"), this.formatter.toRaw(this.$el.val()));
1298
- model.trigger("backgrid:edited", model, column, new Command(e));
1486
+ model.set(column.get("name"), this.formatter.toRaw(this.$el.val(), model));
1299
1487
  },
1300
1488
 
1301
1489
  /**
@@ -1314,9 +1502,7 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({
1314
1502
  command.moveUp() || command.moveDown() || e.type == "blur") {
1315
1503
  e.preventDefault();
1316
1504
  e.stopPropagation();
1317
- if (e.type == "blur" && this.$el.find("option").length === 1) {
1318
- model.set(column.get("name"), this.formatter.toRaw(this.$el.val()));
1319
- }
1505
+ this.save(e);
1320
1506
  model.trigger("backgrid:edited", model, column, new Command(e));
1321
1507
  }
1322
1508
  }
@@ -1367,7 +1553,7 @@ var SelectCell = Backgrid.SelectCell = Cell.extend({
1367
1553
  multiple: false,
1368
1554
 
1369
1555
  /** @property */
1370
- formatter: new SelectFormatter(),
1556
+ formatter: SelectFormatter,
1371
1557
 
1372
1558
  /**
1373
1559
  @property {Array.<Array>|Array.<{name: string, values: Array.<Array>}>} optionValues
@@ -1387,8 +1573,7 @@ var SelectCell = Backgrid.SelectCell = Cell.extend({
1387
1573
  @throws {TypeError} If `optionsValues` is undefined.
1388
1574
  */
1389
1575
  initialize: function (options) {
1390
- Cell.prototype.initialize.apply(this, arguments);
1391
- Backgrid.requireOptions(this, ["optionValues"]);
1576
+ SelectCell.__super__.initialize.apply(this, arguments);
1392
1577
  this.listenTo(this.model, "backgrid:edit", function (model, column, cell, editor) {
1393
1578
  if (column.get("name") == this.column.get("name")) {
1394
1579
  editor.setOptionValues(this.optionValues);
@@ -1405,8 +1590,9 @@ var SelectCell = Backgrid.SelectCell = Cell.extend({
1405
1590
  render: function () {
1406
1591
  this.$el.empty();
1407
1592
 
1408
- var optionValues = this.optionValues;
1409
- var rawData = this.formatter.fromRaw(this.model.get(this.column.get("name")));
1593
+ var optionValues = _.result(this, "optionValues");
1594
+ var model = this.model;
1595
+ var rawData = this.formatter.fromRaw(model.get(this.column.get("name")), model);
1410
1596
 
1411
1597
  var selectedText = [];
1412
1598
 
@@ -1445,7 +1631,7 @@ var SelectCell = Backgrid.SelectCell = Cell.extend({
1445
1631
  }
1446
1632
  catch (ex) {
1447
1633
  if (ex instanceof TypeError) {
1448
- throw TypeError("'optionValues' must be of type {Array.<Array>|Array.<{name: string, values: Array.<Array>}>}");
1634
+ throw new TypeError("'optionValues' must be of type {Array.<Array>|Array.<{name: string, values: Array.<Array>}>}");
1449
1635
  }
1450
1636
  throw ex;
1451
1637
  }
@@ -1456,12 +1642,13 @@ var SelectCell = Backgrid.SelectCell = Cell.extend({
1456
1642
  }
1457
1643
 
1458
1644
  });
1645
+
1459
1646
  /*
1460
1647
  backgrid
1461
1648
  http://github.com/wyuenho/backgrid
1462
1649
 
1463
1650
  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
1464
- Licensed under the MIT @license.
1651
+ Licensed under the MIT license.
1465
1652
  */
1466
1653
 
1467
1654
  /**
@@ -1473,9 +1660,77 @@ var SelectCell = Backgrid.SelectCell = Cell.extend({
1473
1660
 
1474
1661
  @class Backgrid.Column
1475
1662
  @extends Backbone.Model
1476
- */
1663
+ */
1477
1664
  var Column = Backgrid.Column = Backbone.Model.extend({
1478
1665
 
1666
+ /**
1667
+ @cfg {Object} defaults Column defaults. To override any of these default
1668
+ values, you can either change the prototype directly to override
1669
+ Column.defaults globally or extend Column and supply the custom class to
1670
+ Backgrid.Grid:
1671
+
1672
+ // Override Column defaults globally
1673
+ Column.prototype.defaults.sortable = false;
1674
+
1675
+ // Override Column defaults locally
1676
+ var MyColumn = Column.extend({
1677
+ defaults: _.defaults({
1678
+ editable: false
1679
+ }, Column.prototype.defaults)
1680
+ });
1681
+
1682
+ var grid = new Backgrid.Grid(columns: new Columns([{...}, {...}], {
1683
+ model: MyColumn
1684
+ }));
1685
+
1686
+ @cfg {string} [defaults.name] The default name of the model attribute.
1687
+
1688
+ @cfg {string} [defaults.label] The default label to show in the header.
1689
+
1690
+ @cfg {string|Backgrid.Cell} [defaults.cell] The default cell type. If this
1691
+ is a string, the capitalized form will be used to look up a cell class in
1692
+ Backbone, i.e.: string => StringCell. If a Cell subclass is supplied, it is
1693
+ initialized with a hash of parameters. If a Cell instance is supplied, it
1694
+ is used directly.
1695
+
1696
+ @cfg {string|Backgrid.HeaderCell} [defaults.headerCell] The default header
1697
+ cell type.
1698
+
1699
+ @cfg {boolean|string|function(): boolean} [defaults.sortable=true] Whether
1700
+ this column is sortable. If the value is a string, a method will the same
1701
+ name will be looked up from the column instance to determine whether the
1702
+ column should be sortable. The method's signature must be `function
1703
+ (Backgrid.Column, Backbone.Model): boolean`.
1704
+
1705
+ @cfg {boolean|string|function(): boolean} [defaults.editable=true] Whether
1706
+ this column is editable. If the value is a string, a method will the same
1707
+ name will be looked up from the column instance to determine whether the
1708
+ column should be editable. The method's signature must be `function
1709
+ (Backgrid.Column, Backbone.Model): boolean`.
1710
+
1711
+ @cfg {boolean|string|function(): boolean} [defaults.renderable=true]
1712
+ Whether this column is renderable. If the value is a string, a method will
1713
+ the same name will be looked up from the column instance to determine
1714
+ whether the column should be renderable. The method's signature must be
1715
+ `function (Backrid.Column, Backbone.Model): boolean`.
1716
+
1717
+ @cfg {Backgrid.CellFormatter | Object | string} [defaults.formatter] The
1718
+ formatter to use to convert between raw model values and user input.
1719
+
1720
+ @cfg {"toggle"|"cycle"} [defaults.sortType="cycle"] Whether sorting will
1721
+ toggle between ascending and descending order, or cycle between insertion
1722
+ order, ascending and descending order.
1723
+
1724
+ @cfg {(function(Backbone.Model, string): *) | string} [defaults.sortValue]
1725
+ The function to use to extract a value from the model for comparison during
1726
+ sorting. If this value is a string, a method with the same name will be
1727
+ looked up from the column instance.
1728
+
1729
+ @cfg {"ascending"|"descending"|null} [defaults.direction=null] The initial
1730
+ sorting direction for this column. The default is ordered by
1731
+ Backbone.Model.cid, which usually means the collection is ordered by
1732
+ insertion order.
1733
+ */
1479
1734
  defaults: {
1480
1735
  name: undefined,
1481
1736
  label: undefined,
@@ -1483,6 +1738,9 @@ var Column = Backgrid.Column = Backbone.Model.extend({
1483
1738
  editable: true,
1484
1739
  renderable: true,
1485
1740
  formatter: undefined,
1741
+ sortType: "cycle",
1742
+ sortValue: undefined,
1743
+ direction: null,
1486
1744
  cell: undefined,
1487
1745
  headerCell: undefined
1488
1746
  },
@@ -1490,42 +1748,105 @@ var Column = Backgrid.Column = Backbone.Model.extend({
1490
1748
  /**
1491
1749
  Initializes this Column instance.
1492
1750
 
1493
- @param {Object} attrs Column attributes.
1494
- @param {string} attrs.name The name of the model attribute.
1495
- @param {string|Backgrid.Cell} attrs.cell The cell type.
1496
- If this is a string, the capitalized form will be used to look up a
1497
- cell class in Backbone, i.e.: string => StringCell. If a Cell subclass
1498
- is supplied, it is initialized with a hash of parameters. If a Cell
1499
- instance is supplied, it is used directly.
1500
- @param {string|Backgrid.HeaderCell} [attrs.headerCell] The header cell type.
1501
- @param {string} [attrs.label] The label to show in the header.
1502
- @param {boolean} [attrs.sortable=true]
1503
- @param {boolean} [attrs.editable=true]
1504
- @param {boolean} [attrs.renderable=true]
1505
- @param {Backgrid.CellFormatter|Object|string} [attrs.formatter] The
1506
- formatter to use to convert between raw model values and user input.
1751
+ @param {Object} attrs
1752
+
1753
+ @param {string} attrs.name The model attribute this column is responsible
1754
+ for.
1755
+
1756
+ @param {string|Backgrid.Cell} attrs.cell The cell type to use to render
1757
+ this column.
1758
+
1759
+ @param {string} [attrs.label]
1760
+
1761
+ @param {string|Backgrid.HeaderCell} [attrs.headerCell]
1762
+
1763
+ @param {boolean|string|function(): boolean} [attrs.sortable=true]
1764
+
1765
+ @param {boolean|string|function(): boolean} [attrs.editable=true]
1766
+
1767
+ @param {boolean|string|function(): boolean} [attrs.renderable=true]
1768
+
1769
+ @param {Backgrid.CellFormatter | Object | string} [attrs.formatter]
1770
+
1771
+ @param {"toggle"|"cycle"} [attrs.sortType="cycle"]
1772
+
1773
+ @param {(function(Backbone.Model, string): *) | string} [attrs.sortValue]
1507
1774
 
1508
1775
  @throws {TypeError} If attrs.cell or attrs.options are not supplied.
1509
- @throws {ReferenceError} If attrs.cell is a string but a cell class of
1776
+
1777
+ @throws {ReferenceError} If formatter is a string but a formatter class of
1510
1778
  said name cannot be found in the Backgrid module.
1511
1779
 
1512
1780
  See:
1513
1781
 
1782
+ - Backgrid.Column.defaults
1514
1783
  - Backgrid.Cell
1515
1784
  - Backgrid.CellFormatter
1516
1785
  */
1517
- initialize: function (attrs) {
1518
- Backgrid.requireOptions(attrs, ["cell", "name"]);
1519
-
1786
+ initialize: function () {
1520
1787
  if (!this.has("label")) {
1521
1788
  this.set({ label: this.get("name") }, { silent: true });
1522
1789
  }
1523
1790
 
1524
1791
  var headerCell = Backgrid.resolveNameToClass(this.get("headerCell"), "HeaderCell");
1792
+
1525
1793
  var cell = Backgrid.resolveNameToClass(this.get("cell"), "Cell");
1526
- this.set({ cell: cell, headerCell: headerCell }, { silent: true });
1794
+
1795
+ this.set({cell: cell, headerCell: headerCell}, { silent: true });
1796
+ },
1797
+
1798
+ /**
1799
+ Returns an appropriate value extraction function from a model for sorting.
1800
+
1801
+ If the column model contains an attribute `sortValue`, if it is a string, a
1802
+ method from the column instance identifified by the `sortValue` string is
1803
+ returned. If it is a function, it it returned as is. If `sortValue` isn't
1804
+ found from the column model's attributes, a default value extraction
1805
+ function is returned which will compare according to the natural order of
1806
+ the value's type.
1807
+
1808
+ @return {function(Backbone.Model, string): *}
1809
+ */
1810
+ sortValue: function () {
1811
+ var sortValue = this.get("sortValue");
1812
+ if (_.isString(sortValue)) return this[sortValue];
1813
+ else if (_.isFunction(sortValue)) return sortValue;
1814
+
1815
+ return function (model, colName) {
1816
+ return model.get(colName);
1817
+ };
1527
1818
  }
1528
1819
 
1820
+ /**
1821
+ @member Backgrid.Column
1822
+ @protected
1823
+ @method sortable
1824
+ @return {function(Backgrid.Column, Backbone.Model): boolean | boolean}
1825
+ */
1826
+
1827
+ /**
1828
+ @member Backgrid.Column
1829
+ @protected
1830
+ @method editable
1831
+ @return {function(Backgrid.Column, Backbone.Model): boolean | boolean}
1832
+ */
1833
+
1834
+ /**
1835
+ @member Backgrid.Column
1836
+ @protected
1837
+ @method renderable
1838
+ @return {function(Backgrid.Column, Backbone.Model): boolean | boolean}
1839
+ */
1840
+ });
1841
+
1842
+ _.each(["sortable", "renderable", "editable"], function (key) {
1843
+ Column.prototype[key] = function () {
1844
+ var value = this.get(key);
1845
+ if (_.isString(value)) return this[value];
1846
+ else if (_.isFunction(value)) return value;
1847
+
1848
+ return !!value;
1849
+ };
1529
1850
  });
1530
1851
 
1531
1852
  /**
@@ -1541,12 +1862,13 @@ var Columns = Backgrid.Columns = Backbone.Collection.extend({
1541
1862
  */
1542
1863
  model: Column
1543
1864
  });
1865
+
1544
1866
  /*
1545
1867
  backgrid
1546
1868
  http://github.com/wyuenho/backgrid
1547
1869
 
1548
1870
  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
1549
- Licensed under the MIT @license.
1871
+ Licensed under the MIT license.
1550
1872
  */
1551
1873
 
1552
1874
  /**
@@ -1562,8 +1884,6 @@ var Row = Backgrid.Row = Backbone.View.extend({
1562
1884
  /** @property */
1563
1885
  tagName: "tr",
1564
1886
 
1565
- requiredOptions: ["columns", "model"],
1566
-
1567
1887
  /**
1568
1888
  Initializes a row view instance.
1569
1889
 
@@ -1575,8 +1895,6 @@ var Row = Backgrid.Row = Backbone.View.extend({
1575
1895
  */
1576
1896
  initialize: function (options) {
1577
1897
 
1578
- Backgrid.requireOptions(options, this.requiredOptions);
1579
-
1580
1898
  var columns = this.columns = options.columns;
1581
1899
  if (!(columns instanceof Backbone.Collection)) {
1582
1900
  columns = this.columns = new Columns(columns);
@@ -1587,22 +1905,11 @@ var Row = Backgrid.Row = Backbone.View.extend({
1587
1905
  cells.push(this.makeCell(columns.at(i), options));
1588
1906
  }
1589
1907
 
1590
- this.listenTo(columns, "change:renderable", function (column, renderable) {
1591
- for (var i = 0; i < cells.length; i++) {
1592
- var cell = cells[i];
1593
- if (cell.column.get("name") == column.get("name")) {
1594
- if (renderable) cell.$el.show(); else cell.$el.hide();
1595
- }
1596
- }
1597
- });
1598
-
1599
1908
  this.listenTo(columns, "add", function (column, columns) {
1600
1909
  var i = columns.indexOf(column);
1601
1910
  var cell = this.makeCell(column, options);
1602
1911
  cells.splice(i, 0, cell);
1603
1912
 
1604
- if (!cell.column.get("renderable")) cell.$el.hide();
1605
-
1606
1913
  var $el = this.$el;
1607
1914
  if (i === 0) {
1608
1915
  $el.prepend(cell.render().$el);
@@ -1646,11 +1953,8 @@ var Row = Backgrid.Row = Backbone.View.extend({
1646
1953
  this.$el.empty();
1647
1954
 
1648
1955
  var fragment = document.createDocumentFragment();
1649
-
1650
1956
  for (var i = 0; i < this.cells.length; i++) {
1651
- var cell = this.cells[i];
1652
- fragment.appendChild(cell.render().el);
1653
- if (!cell.column.get("renderable")) cell.$el.hide();
1957
+ fragment.appendChild(this.cells[i].render().el);
1654
1958
  }
1655
1959
 
1656
1960
  this.el.appendChild(fragment);
@@ -1687,19 +1991,17 @@ var EmptyRow = Backgrid.EmptyRow = Backbone.View.extend({
1687
1991
  /** @property */
1688
1992
  tagName: "tr",
1689
1993
 
1690
- /** @property */
1994
+ /** @property {string|function(): string} */
1691
1995
  emptyText: null,
1692
1996
 
1693
1997
  /**
1694
1998
  Initializer.
1695
1999
 
1696
2000
  @param {Object} options
1697
- @param {string} options.emptyText
2001
+ @param {string|function(): string} options.emptyText
1698
2002
  @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata.
1699
2003
  */
1700
2004
  initialize: function (options) {
1701
- Backgrid.requireOptions(options, ["emptyText", "columns"]);
1702
-
1703
2005
  this.emptyText = options.emptyText;
1704
2006
  this.columns = options.columns;
1705
2007
  },
@@ -1712,20 +2014,21 @@ var EmptyRow = Backgrid.EmptyRow = Backbone.View.extend({
1712
2014
 
1713
2015
  var td = document.createElement("td");
1714
2016
  td.setAttribute("colspan", this.columns.length);
1715
- td.textContent = this.emptyText;
2017
+ td.appendChild(document.createTextNode(_.result(this, "emptyText")));
1716
2018
 
1717
- this.el.setAttribute("class", "empty");
2019
+ this.el.className = "empty";
1718
2020
  this.el.appendChild(td);
1719
2021
 
1720
2022
  return this;
1721
2023
  }
1722
2024
  });
2025
+
1723
2026
  /*
1724
2027
  backgrid
1725
2028
  http://github.com/wyuenho/backgrid
1726
2029
 
1727
2030
  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
1728
- Licensed under the MIT @license.
2031
+ Licensed under the MIT license.
1729
2032
  */
1730
2033
 
1731
2034
  /**
@@ -1746,12 +2049,6 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({
1746
2049
  "click a": "onClick"
1747
2050
  },
1748
2051
 
1749
- /**
1750
- @property {null|"ascending"|"descending"} _direction The current sorting
1751
- direction of this column.
1752
- */
1753
- _direction: null,
1754
-
1755
2052
  /**
1756
2053
  Initializer.
1757
2054
 
@@ -1761,43 +2058,50 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({
1761
2058
  @throws {TypeError} If options.column or options.collection is undefined.
1762
2059
  */
1763
2060
  initialize: function (options) {
1764
- Backgrid.requireOptions(options, ["column", "collection"]);
1765
2061
  this.column = options.column;
1766
2062
  if (!(this.column instanceof Column)) {
1767
2063
  this.column = new Column(this.column);
1768
2064
  }
1769
- this.listenTo(this.collection, "backgrid:sort", this._resetCellDirection);
2065
+
2066
+ var column = this.column, collection = this.collection, $el = this.$el;
2067
+
2068
+ this.listenTo(column, "change:editable change:sortable change:renderable",
2069
+ function (column) {
2070
+ var changed = column.changedAttributes();
2071
+ for (var key in changed) {
2072
+ if (changed.hasOwnProperty(key)) {
2073
+ $el.toggleClass(key, changed[key]);
2074
+ }
2075
+ }
2076
+ });
2077
+ this.listenTo(column, "change:direction", this.setCellDirection);
2078
+ this.listenTo(column, "change:name change:label", this.render);
2079
+
2080
+ if (Backgrid.callByNeed(column.editable(), column, collection)) $el.addClass("editable");
2081
+ if (Backgrid.callByNeed(column.sortable(), column, collection)) $el.addClass("sortable");
2082
+ if (Backgrid.callByNeed(column.renderable(), column, collection)) $el.addClass("renderable");
2083
+
2084
+ this.listenTo(collection.fullCollection || collection, "sort", this.removeCellDirection);
1770
2085
  },
1771
2086
 
1772
2087
  /**
1773
- Gets or sets the direction of this cell. If called directly without
1774
- parameters, returns the current direction of this cell, otherwise sets
1775
- it. If a `null` is given, sets this cell back to the default order.
1776
-
1777
- @param {null|"ascending"|"descending"} dir
1778
- @return {null|string} The current direction or the changed direction.
2088
+ Event handler for the collection's `sort` event. Removes all the CSS
2089
+ direction classes.
1779
2090
  */
1780
- direction: function (dir) {
1781
- if (arguments.length) {
1782
- if (this._direction) this.$el.removeClass(this._direction);
1783
- if (dir) this.$el.addClass(dir);
1784
- this._direction = dir;
1785
- }
1786
-
1787
- return this._direction;
2091
+ removeCellDirection: function () {
2092
+ this.$el.removeClass("ascending").removeClass("descending");
2093
+ this.column.set("direction", null);
1788
2094
  },
1789
2095
 
1790
2096
  /**
1791
- Event handler for the Backbone `backgrid:sort` event. Resets this cell's
1792
- direction to default if sorting is being done on another column.
1793
-
1794
- @private
2097
+ Event handler for the column's `change:direction` event. If this
2098
+ HeaderCell's column is being sorted on, it applies the direction given as a
2099
+ CSS class to the header cell. Removes all the CSS direction classes
2100
+ otherwise.
1795
2101
  */
1796
- _resetCellDirection: function (sortByColName, direction, comparator, collection) {
1797
- if (collection == this.collection) {
1798
- if (sortByColName !== this.column.get("name")) this.direction(null);
1799
- else this.direction(direction);
1800
- }
2102
+ setCellDirection: function (column, direction) {
2103
+ this.$el.removeClass("ascending").removeClass("descending");
2104
+ if (column.cid == this.column.cid) this.$el.addClass(direction);
1801
2105
  },
1802
2106
 
1803
2107
  /**
@@ -1808,115 +2112,47 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({
1808
2112
  onClick: function (e) {
1809
2113
  e.preventDefault();
1810
2114
 
1811
- var columnName = this.column.get("name");
1812
-
1813
- if (this.column.get("sortable")) {
1814
- if (this.direction() === "ascending") {
1815
- this.sort(columnName, "descending", function (left, right) {
1816
- var leftVal = left.get(columnName);
1817
- var rightVal = right.get(columnName);
1818
- if (leftVal === rightVal) {
1819
- return 0;
1820
- }
1821
- else if (leftVal > rightVal) { return -1; }
1822
- return 1;
1823
- });
1824
- }
1825
- else if (this.direction() === "descending") {
1826
- this.sort(columnName, null);
1827
- }
1828
- else {
1829
- this.sort(columnName, "ascending", function (left, right) {
1830
- var leftVal = left.get(columnName);
1831
- var rightVal = right.get(columnName);
1832
- if (leftVal === rightVal) {
1833
- return 0;
1834
- }
1835
- else if (leftVal < rightVal) { return -1; }
1836
- return 1;
1837
- });
1838
- }
1839
- }
1840
- },
1841
-
1842
- /**
1843
- If the underlying collection is a Backbone.PageableCollection in
1844
- server-mode or infinite-mode, a page of models is fetched after sorting is
1845
- done on the server.
1846
-
1847
- If the underlying collection is a Backbone.PageableCollection in
1848
- client-mode, or any
1849
- [Backbone.Collection](http://backbonejs.org/#Collection) instance, sorting
1850
- is done on the client side. If the collection is an instance of a
1851
- Backbone.PageableCollection, sorting will be done globally on all the pages
1852
- and the current page will then be returned.
1853
-
1854
- Triggers a Backbone `backgrid:sort` event from the collection when done
1855
- with the column name, direction, comparator and a reference to the
1856
- collection.
1857
-
1858
- @param {string} columnName
1859
- @param {null|"ascending"|"descending"} direction
1860
- @param {function(*, *): number} [comparator]
1861
-
1862
- See [Backbone.Collection#comparator](http://backbonejs.org/#Collection-comparator)
1863
- */
1864
- sort: function (columnName, direction, comparator) {
1865
-
1866
- comparator = comparator || this._cidComparator;
1867
-
2115
+ var column = this.column;
1868
2116
  var collection = this.collection;
2117
+ var event = "backgrid:sort";
1869
2118
 
1870
- if (Backbone.PageableCollection && collection instanceof Backbone.PageableCollection) {
1871
- var order;
1872
- if (direction === "ascending") order = -1;
1873
- else if (direction === "descending") order = 1;
1874
- else order = null;
1875
-
1876
- collection.setSorting(order ? columnName : null, order);
1877
-
1878
- if (collection.mode == "client") {
1879
- if (!collection.fullCollection.comparator) {
1880
- collection.fullCollection.comparator = comparator;
1881
- }
1882
- collection.fullCollection.sort();
1883
- }
1884
- else collection.fetch({reset: true});
1885
- }
1886
- else {
1887
- collection.comparator = comparator;
1888
- collection.sort();
2119
+ function cycleSort(header, col) {
2120
+ if (column.get("direction") === "ascending") collection.trigger(event, col, "descending");
2121
+ else if (column.get("direction") === "descending") collection.trigger(event, col, null);
2122
+ else collection.trigger(event, col, "ascending");
1889
2123
  }
1890
2124
 
1891
- this.collection.trigger("backgrid:sort", columnName, direction, comparator, this.collection);
1892
- },
1893
-
1894
- /**
1895
- Default comparator for Backbone.Collections. Sorts cids in ascending
1896
- order. The cids of the models are assumed to be in insertion order.
1897
-
1898
- @private
1899
- @param {*} left
1900
- @param {*} right
1901
- */
1902
- _cidComparator: function (left, right) {
1903
- var lcid = left.cid, rcid = right.cid;
1904
- if (!_.isUndefined(lcid) && !_.isUndefined(rcid)) {
1905
- lcid = lcid.slice(1) * 1, rcid = rcid.slice(1) * 1;
1906
- if (lcid < rcid) return -1;
1907
- else if (lcid > rcid) return 1;
2125
+ function toggleSort(header, col) {
2126
+ if (column.get("direction") === "ascending") collection.trigger(event, col, "descending");
2127
+ else collection.trigger(event, col, "ascending");
1908
2128
  }
1909
2129
 
1910
- return 0;
2130
+ var sortable = Backgrid.callByNeed(column.sortable(), column, this.collection);
2131
+ if (sortable) {
2132
+ var sortType = column.get("sortType");
2133
+ if (sortType === "toggle") toggleSort(this, column);
2134
+ else cycleSort(this, column);
2135
+ }
1911
2136
  },
1912
2137
 
1913
2138
  /**
1914
- Renders a header cell with a sorter and a label.
2139
+ Renders a header cell with a sorter, a label, and a class name for this
2140
+ column.
1915
2141
  */
1916
2142
  render: function () {
1917
2143
  this.$el.empty();
1918
- var $label = $("<a>").text(this.column.get("label")).append("<b class='sort-caret'></b>");
1919
- this.$el.append($label);
2144
+ var column = this.column;
2145
+ var sortable = Backgrid.callByNeed(column.sortable(), column, this.collection);
2146
+ var label;
2147
+ if(sortable){
2148
+ label = $("<a>").text(column.get("label")).append("<b class='sort-caret'></b>");
2149
+ } else {
2150
+ label = document.createTextNode(column.get("label"));
2151
+ }
2152
+
2153
+ this.$el.append(label);
2154
+ this.$el.addClass(column.get("name"));
2155
+ this.$el.addClass(column.get("direction"));
1920
2156
  this.delegateEvents();
1921
2157
  return this;
1922
2158
  }
@@ -1931,8 +2167,6 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({
1931
2167
  */
1932
2168
  var HeaderRow = Backgrid.HeaderRow = Backgrid.Row.extend({
1933
2169
 
1934
- requiredOptions: ["columns", "collection"],
1935
-
1936
2170
  /**
1937
2171
  Initializer.
1938
2172
 
@@ -1983,8 +2217,6 @@ var Header = Backgrid.Header = Backbone.View.extend({
1983
2217
  @throws {TypeError} If options.columns or options.model is undefined.
1984
2218
  */
1985
2219
  initialize: function (options) {
1986
- Backgrid.requireOptions(options, ["columns", "collection"]);
1987
-
1988
2220
  this.columns = options.columns;
1989
2221
  if (!(this.columns instanceof Backbone.Collection)) {
1990
2222
  this.columns = new Columns(this.columns);
@@ -2016,12 +2248,13 @@ var Header = Backgrid.Header = Backbone.View.extend({
2016
2248
  }
2017
2249
 
2018
2250
  });
2251
+
2019
2252
  /*
2020
2253
  backgrid
2021
2254
  http://github.com/wyuenho/backgrid
2022
2255
 
2023
2256
  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
2024
- Licensed under the MIT @license.
2257
+ Licensed under the MIT license.
2025
2258
  */
2026
2259
 
2027
2260
  /**
@@ -2044,14 +2277,13 @@ var Body = Backgrid.Body = Backbone.View.extend({
2044
2277
  @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns
2045
2278
  Column metadata.
2046
2279
  @param {Backgrid.Row} [options.row=Backgrid.Row] The Row class to use.
2047
- @param {string} [options.emptyText] The text to display in the empty row.
2280
+ @param {string|function(): string} [options.emptyText] The text to display in the empty row.
2048
2281
 
2049
2282
  @throws {TypeError} If options.columns or options.collection is undefined.
2050
2283
 
2051
2284
  See Backgrid.Row.
2052
2285
  */
2053
2286
  initialize: function (options) {
2054
- Backgrid.requireOptions(options, ["columns", "collection"]);
2055
2287
 
2056
2288
  this.columns = options.columns;
2057
2289
  if (!(this.columns instanceof Backbone.Collection)) {
@@ -2076,6 +2308,7 @@ var Body = Backgrid.Body = Backbone.View.extend({
2076
2308
  this.listenTo(collection, "remove", this.removeRow);
2077
2309
  this.listenTo(collection, "sort", this.refresh);
2078
2310
  this.listenTo(collection, "reset", this.refresh);
2311
+ this.listenTo(collection, "backgrid:sort", this.sort);
2079
2312
  this.listenTo(collection, "backgrid:edited", this.moveToNextCell);
2080
2313
  },
2081
2314
 
@@ -2121,8 +2354,6 @@ var Body = Backgrid.Body = Backbone.View.extend({
2121
2354
  return;
2122
2355
  }
2123
2356
 
2124
- options = _.extend({render: true}, options || {});
2125
-
2126
2357
  var row = new this.row({
2127
2358
  columns: this.columns,
2128
2359
  model: model
@@ -2135,14 +2366,14 @@ var Body = Backgrid.Body = Backbone.View.extend({
2135
2366
  var $children = $el.children();
2136
2367
  var $rowEl = row.render().$el;
2137
2368
 
2138
- if (options.render) {
2139
- if (index >= $children.length) {
2140
- $el.append($rowEl);
2141
- }
2142
- else {
2143
- $children.eq(index).before($rowEl);
2144
- }
2369
+ if (index >= $children.length) {
2370
+ $el.append($rowEl);
2145
2371
  }
2372
+ else {
2373
+ $children.eq(index).before($rowEl);
2374
+ }
2375
+
2376
+ return this;
2146
2377
  },
2147
2378
 
2148
2379
  /**
@@ -2184,6 +2415,8 @@ var Body = Backgrid.Body = Backbone.View.extend({
2184
2415
 
2185
2416
  this.rows.splice(options.index, 1);
2186
2417
  this._unshiftEmptyRowMayBe();
2418
+
2419
+ return this;
2187
2420
  },
2188
2421
 
2189
2422
  /**
@@ -2247,18 +2480,115 @@ var Body = Backgrid.Body = Backbone.View.extend({
2247
2480
  return Backbone.View.prototype.remove.apply(this, arguments);
2248
2481
  },
2249
2482
 
2483
+ /**
2484
+ If the underlying collection is a Backbone.PageableCollection in
2485
+ server-mode or infinite-mode, a page of models is fetched after sorting is
2486
+ done on the server.
2487
+
2488
+ If the underlying collection is a Backbone.PageableCollection in
2489
+ client-mode, or any
2490
+ [Backbone.Collection](http://backbonejs.org/#Collection) instance, sorting
2491
+ is done on the client side. If the collection is an instance of a
2492
+ Backbone.PageableCollection, sorting will be done globally on all the pages
2493
+ and the current page will then be returned.
2494
+
2495
+ Triggers a Backbone `backgrid:sorted` event from the collection when done
2496
+ with the column, direction and a reference to the collection.
2497
+
2498
+ @param {Backgrid.Column|string} column
2499
+ @param {null|"ascending"|"descending"} direction
2500
+
2501
+ See [Backbone.Collection#comparator](http://backbonejs.org/#Collection-comparator)
2502
+ */
2503
+ sort: function (column, direction) {
2504
+
2505
+ if (!_.contains(["ascending", "descending", null], direction)) {
2506
+ throw new RangeError('direction must be one of "ascending", "descending" or `null`');
2507
+ }
2508
+
2509
+ if (_.isString(column)) column = this.columns.findWhere({name: column});
2510
+
2511
+ var collection = this.collection;
2512
+
2513
+ var order;
2514
+ if (direction === "ascending") order = -1;
2515
+ else if (direction === "descending") order = 1;
2516
+ else order = null;
2517
+
2518
+ var comparator = this.makeComparator(column.get("name"), order,
2519
+ order ?
2520
+ column.sortValue() :
2521
+ function (model) {
2522
+ return model.cid.replace('c', '') * 1;
2523
+ });
2524
+
2525
+ if (Backbone.PageableCollection &&
2526
+ collection instanceof Backbone.PageableCollection) {
2527
+
2528
+ collection.setSorting(order && column.get("name"), order,
2529
+ {sortValue: column.sortValue()});
2530
+
2531
+ if (collection.fullCollection) {
2532
+ // If order is null, pageable will remove the comparator on both sides,
2533
+ // in this case the default insertion order comparator needs to be
2534
+ // attached to get back to the order before sorting.
2535
+ if (collection.fullCollection.comparator == null) {
2536
+ collection.fullCollection.comparator = comparator;
2537
+ }
2538
+ collection.fullCollection.sort();
2539
+ collection.trigger("backgrid:sorted", column, direction, collection);
2540
+ }
2541
+ else collection.fetch({reset: true, success: function () {
2542
+ collection.trigger("backgrid:sorted", column, direction, collection);
2543
+ }});
2544
+ }
2545
+ else {
2546
+ collection.comparator = comparator;
2547
+ collection.sort();
2548
+ collection.trigger("backgrid:sorted", column, direction, collection);
2549
+ }
2550
+
2551
+ column.set("direction", direction);
2552
+
2553
+ return this;
2554
+ },
2555
+
2556
+ makeComparator: function (attr, order, func) {
2557
+
2558
+ return function (left, right) {
2559
+ // extract the values from the models
2560
+ var l = func(left, attr), r = func(right, attr), t;
2561
+
2562
+ // if descending order, swap left and right
2563
+ if (order === 1) t = l, l = r, r = t;
2564
+
2565
+ // compare as usual
2566
+ if (l === r) return 0;
2567
+ else if (l < r) return -1;
2568
+ return 1;
2569
+ };
2570
+ },
2571
+
2250
2572
  /**
2251
2573
  Moves focus to the next renderable and editable cell and return the
2252
2574
  currently editing cell to display mode.
2253
2575
 
2576
+ Triggers a `backgrid:next` event on the model with the indices of the row
2577
+ and column the user *intended* to move to, and whether the intended move
2578
+ was going to go out of bounds. Note that *out of bound* always means an
2579
+ attempt to go past the end of the last row.
2580
+
2254
2581
  @param {Backbone.Model} model The originating model
2255
2582
  @param {Backgrid.Column} column The originating model column
2256
2583
  @param {Backgrid.Command} command The Command object constructed from a DOM
2257
- Event
2584
+ event
2258
2585
  */
2259
2586
  moveToNextCell: function (model, column, command) {
2260
2587
  var i = this.collection.indexOf(model);
2261
2588
  var j = this.columns.indexOf(column);
2589
+ var cell, renderable, editable, m, n;
2590
+
2591
+ this.rows[i].cells[j].exitEditMode();
2262
2592
 
2263
2593
  if (command.moveUp() || command.moveDown() || command.moveLeft() ||
2264
2594
  command.moveRight() || command.save()) {
@@ -2266,34 +2596,50 @@ var Body = Backgrid.Body = Backbone.View.extend({
2266
2596
  var maxOffset = l * this.collection.length;
2267
2597
 
2268
2598
  if (command.moveUp() || command.moveDown()) {
2269
- var row = this.rows[i + (command.moveUp() ? -1 : 1)];
2270
- if (row) row.cells[j].enterEditMode();
2599
+ m = i + (command.moveUp() ? -1 : 1);
2600
+ var row = this.rows[m];
2601
+ if (row) {
2602
+ cell = row.cells[j];
2603
+ if (Backgrid.callByNeed(cell.column.editable(), cell.column, model)) {
2604
+ cell.enterEditMode();
2605
+ model.trigger("backgrid:next", m, j, false);
2606
+ }
2607
+ }
2608
+ else model.trigger("backgrid:next", m, j, true);
2271
2609
  }
2272
2610
  else if (command.moveLeft() || command.moveRight()) {
2273
2611
  var right = command.moveRight();
2274
2612
  for (var offset = i * l + j + (right ? 1 : -1);
2275
2613
  offset >= 0 && offset < maxOffset;
2276
2614
  right ? offset++ : offset--) {
2277
- var m = ~~(offset / l);
2278
- var n = offset - m * l;
2279
- var cell = this.rows[m].cells[n];
2280
- if (cell.column.get("renderable") && cell.column.get("editable")) {
2615
+ m = ~~(offset / l);
2616
+ n = offset - m * l;
2617
+ cell = this.rows[m].cells[n];
2618
+ renderable = Backgrid.callByNeed(cell.column.renderable(), cell.column, cell.model);
2619
+ editable = Backgrid.callByNeed(cell.column.editable(), cell.column, model);
2620
+ if (renderable && editable) {
2281
2621
  cell.enterEditMode();
2622
+ model.trigger("backgrid:next", m, n, false);
2282
2623
  break;
2283
2624
  }
2284
2625
  }
2626
+
2627
+ if (offset == maxOffset) {
2628
+ model.trigger("backgrid:next", ~~(offset / l), offset - m * l, true);
2629
+ }
2285
2630
  }
2286
2631
  }
2287
2632
 
2288
- this.rows[i].cells[j].exitEditMode();
2633
+ return this;
2289
2634
  }
2290
2635
  });
2636
+
2291
2637
  /*
2292
2638
  backgrid
2293
2639
  http://github.com/wyuenho/backgrid
2294
2640
 
2295
2641
  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
2296
- Licensed under the MIT @license.
2642
+ Licensed under the MIT license.
2297
2643
  */
2298
2644
 
2299
2645
  /**
@@ -2313,7 +2659,6 @@ var Footer = Backgrid.Footer = Backbone.View.extend({
2313
2659
  Initializer.
2314
2660
 
2315
2661
  @param {Object} options
2316
- @param {*} options.parent The parent view class of this footer.
2317
2662
  @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns
2318
2663
  Column metadata.
2319
2664
  @param {Backbone.Collection} options.collection
@@ -2321,7 +2666,6 @@ var Footer = Backgrid.Footer = Backbone.View.extend({
2321
2666
  @throws {TypeError} If options.columns or options.collection is undefined.
2322
2667
  */
2323
2668
  initialize: function (options) {
2324
- Backgrid.requireOptions(options, ["columns", "collection"]);
2325
2669
  this.columns = options.columns;
2326
2670
  if (!(this.columns instanceof Backbone.Collection)) {
2327
2671
  this.columns = new Backgrid.Columns(this.columns);
@@ -2329,12 +2673,13 @@ var Footer = Backgrid.Footer = Backbone.View.extend({
2329
2673
  }
2330
2674
 
2331
2675
  });
2676
+
2332
2677
  /*
2333
2678
  backgrid
2334
2679
  http://github.com/wyuenho/backgrid
2335
2680
 
2336
2681
  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
2337
- Licensed under the MIT @license.
2682
+ Licensed under the MIT license.
2338
2683
  */
2339
2684
 
2340
2685
  /**
@@ -2405,7 +2750,7 @@ var Grid = Backgrid.Grid = Backbone.View.extend({
2405
2750
  Initializes a Grid instance.
2406
2751
 
2407
2752
  @param {Object} options
2408
- @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata.
2753
+ @param {Backbone.Collection.<Backgrid.Columns>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata.
2409
2754
  @param {Backbone.Collection} options.collection The collection of tabular model data to display.
2410
2755
  @param {Backgrid.Header} [options.header=Backgrid.Header] An optional Header class to override the default.
2411
2756
  @param {Backgrid.Body} [options.body=Backgrid.Body] An optional Body class to override the default.
@@ -2413,34 +2758,37 @@ var Grid = Backgrid.Grid = Backbone.View.extend({
2413
2758
  @param {Backgrid.Footer} [options.footer=Backgrid.Footer] An optional Footer class.
2414
2759
  */
2415
2760
  initialize: function (options) {
2416
- Backgrid.requireOptions(options, ["columns", "collection"]);
2417
-
2418
2761
  // Convert the list of column objects here first so the subviews don't have
2419
2762
  // to.
2420
2763
  if (!(options.columns instanceof Backbone.Collection)) {
2421
- options.columns = new Columns(options.columns);
2764
+ options.columns = new Columns(options.columns || this.columns);
2422
2765
  }
2423
2766
  this.columns = options.columns;
2424
2767
 
2425
- var passedThruOptions = _.omit(options, ["el", "id", "attributes",
2426
- "className", "tagName", "events"]);
2427
-
2428
- this.header = options.header || this.header;
2429
- this.header = new this.header(passedThruOptions);
2768
+ var filteredOptions = _.omit(options, ["el", "id", "attributes",
2769
+ "className", "tagName", "events"]);
2430
2770
 
2771
+ // must construct body first so it listens to backgrid:sort first
2431
2772
  this.body = options.body || this.body;
2432
- this.body = new this.body(passedThruOptions);
2773
+ this.body = new this.body(filteredOptions);
2774
+
2775
+ this.header = options.header || this.header;
2776
+ if (this.header) {
2777
+ this.header = new this.header(filteredOptions);
2778
+ }
2433
2779
 
2434
2780
  this.footer = options.footer || this.footer;
2435
2781
  if (this.footer) {
2436
- this.footer = new this.footer(passedThruOptions);
2782
+ this.footer = new this.footer(filteredOptions);
2437
2783
  }
2438
2784
 
2439
2785
  this.listenTo(this.columns, "reset", function () {
2440
- this.header = new (this.header.remove().constructor)(passedThruOptions);
2441
- this.body = new (this.body.remove().constructor)(passedThruOptions);
2786
+ if (this.header) {
2787
+ this.header = new (this.header.remove().constructor)(filteredOptions);
2788
+ }
2789
+ this.body = new (this.body.remove().constructor)(filteredOptions);
2442
2790
  if (this.footer) {
2443
- this.footer = new (this.footer.remove().constructor)(passedThruOptions);
2791
+ this.footer = new (this.footer.remove().constructor)(filteredOptions);
2444
2792
  }
2445
2793
  this.render();
2446
2794
  });
@@ -2449,15 +2797,17 @@ var Grid = Backgrid.Grid = Backbone.View.extend({
2449
2797
  /**
2450
2798
  Delegates to Backgrid.Body#insertRow.
2451
2799
  */
2452
- insertRow: function (model, collection, options) {
2453
- return this.body.insertRow(model, collection, options);
2800
+ insertRow: function () {
2801
+ this.body.insertRow.apply(this.body, arguments);
2802
+ return this;
2454
2803
  },
2455
2804
 
2456
2805
  /**
2457
2806
  Delegates to Backgrid.Body#removeRow.
2458
2807
  */
2459
- removeRow: function (model, collection, options) {
2460
- return this.body.removeRow(model, collection, options);
2808
+ removeRow: function () {
2809
+ this.body.removeRow.apply(this.body, arguments);
2810
+ return this;
2461
2811
  },
2462
2812
 
2463
2813
  /**
@@ -2466,14 +2816,9 @@ var Grid = Backgrid.Grid = Backbone.View.extend({
2466
2816
  happen.
2467
2817
 
2468
2818
  @param {Object} [options] Options for `Backgrid.Columns#add`.
2469
- @param {boolean} [options.render=true] Whether to render the column
2470
- immediately after insertion.
2471
-
2472
- @chainable
2473
2819
  */
2474
- insertColumn: function (column, options) {
2475
- options = options || {render: true};
2476
- this.columns.add(column, options);
2820
+ insertColumn: function () {
2821
+ this.columns.add.apply(this.columns, arguments);
2477
2822
  return this;
2478
2823
  },
2479
2824
 
@@ -2483,11 +2828,17 @@ var Grid = Backgrid.Grid = Backbone.View.extend({
2483
2828
  needs to happen.
2484
2829
 
2485
2830
  @param {Object} [options] Options for `Backgrid.Columns#remove`.
2831
+ */
2832
+ removeColumn: function () {
2833
+ this.columns.remove.apply(this.columns, arguments);
2834
+ return this;
2835
+ },
2486
2836
 
2487
- @chainable
2837
+ /**
2838
+ Delegates to Backgrid.Body#sort.
2488
2839
  */
2489
- removeColumn: function (column, options) {
2490
- this.columns.remove(column, options);
2840
+ sort: function () {
2841
+ this.body.sort.apply(this.body, arguments);
2491
2842
  return this;
2492
2843
  },
2493
2844
 
@@ -2499,7 +2850,9 @@ var Grid = Backgrid.Grid = Backbone.View.extend({
2499
2850
  render: function () {
2500
2851
  this.$el.empty();
2501
2852
 
2502
- this.$el.append(this.header.render().$el);
2853
+ if (this.header) {
2854
+ this.$el.append(this.header.render().$el);
2855
+ }
2503
2856
 
2504
2857
  if (this.footer) {
2505
2858
  this.$el.append(this.footer.render().$el);
@@ -2520,12 +2873,12 @@ var Grid = Backgrid.Grid = Backbone.View.extend({
2520
2873
  @chainable
2521
2874
  */
2522
2875
  remove: function () {
2523
- this.header.remove.apply(this.header, arguments);
2876
+ this.header && this.header.remove.apply(this.header, arguments);
2524
2877
  this.body.remove.apply(this.body, arguments);
2525
2878
  this.footer && this.footer.remove.apply(this.footer, arguments);
2526
2879
  return Backbone.View.prototype.remove.apply(this, arguments);
2527
2880
  }
2528
2881
 
2529
2882
  });
2530
-
2531
- }(this, jQuery, _, Backbone));
2883
+ return Backgrid;
2884
+ }));