PageRenderTime 43ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/src/body.js

https://github.com/TechnotronicOz/backgrid
JavaScript | 384 lines | 196 code | 55 blank | 133 comment | 51 complexity | 49e182be9cac032c34b12c85aeac8ffb MD5 | raw file
Possible License(s): MIT
  1. /*
  2. backgrid
  3. http://github.com/wyuenho/backgrid
  4. Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
  5. Licensed under the MIT license.
  6. */
  7. /**
  8. Body is the table body which contains the rows inside a table. Body is
  9. responsible for refreshing the rows after sorting, insertion and removal.
  10. @class Backgrid.Body
  11. @extends Backbone.View
  12. */
  13. var Body = Backgrid.Body = Backbone.View.extend({
  14. /** @property */
  15. tagName: "tbody",
  16. /**
  17. Initializer.
  18. @param {Object} options
  19. @param {Backbone.Collection} options.collection
  20. @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns
  21. Column metadata.
  22. @param {Backgrid.Row} [options.row=Backgrid.Row] The Row class to use.
  23. @param {string|function(): string} [options.emptyText] The text to display in the empty row.
  24. @throws {TypeError} If options.columns or options.collection is undefined.
  25. See Backgrid.Row.
  26. */
  27. initialize: function (options) {
  28. this.columns = options.columns;
  29. if (!(this.columns instanceof Backbone.Collection)) {
  30. this.columns = new Columns(this.columns);
  31. }
  32. this.row = options.row || Row;
  33. this.rows = this.collection.map(function (model) {
  34. var row = new this.row({
  35. columns: this.columns,
  36. model: model
  37. });
  38. return row;
  39. }, this);
  40. this.emptyText = options.emptyText;
  41. this._unshiftEmptyRowMayBe();
  42. var collection = this.collection;
  43. this.listenTo(collection, "add", this.insertRow);
  44. this.listenTo(collection, "remove", this.removeRow);
  45. this.listenTo(collection, "sort", this.refresh);
  46. this.listenTo(collection, "reset", this.refresh);
  47. this.listenTo(collection, "backgrid:sort", this.sort);
  48. this.listenTo(collection, "backgrid:edited", this.moveToNextCell);
  49. },
  50. _unshiftEmptyRowMayBe: function () {
  51. if (this.rows.length === 0 && this.emptyText != null) {
  52. this.rows.unshift(new EmptyRow({
  53. emptyText: this.emptyText,
  54. columns: this.columns
  55. }));
  56. }
  57. },
  58. /**
  59. This method can be called either directly or as a callback to a
  60. [Backbone.Collecton#add](http://backbonejs.org/#Collection-add) event.
  61. When called directly, it accepts a model or an array of models and an
  62. option hash just like
  63. [Backbone.Collection#add](http://backbonejs.org/#Collection-add) and
  64. delegates to it. Once the model is added, a new row is inserted into the
  65. body and automatically rendered.
  66. When called as a callback of an `add` event, splices a new row into the
  67. body and renders it.
  68. @param {Backbone.Model} model The model to render as a row.
  69. @param {Backbone.Collection} collection When called directly, this
  70. parameter is actually the options to
  71. [Backbone.Collection#add](http://backbonejs.org/#Collection-add).
  72. @param {Object} options When called directly, this must be null.
  73. See:
  74. - [Backbone.Collection#add](http://backbonejs.org/#Collection-add)
  75. */
  76. insertRow: function (model, collection, options) {
  77. if (this.rows[0] instanceof EmptyRow) this.rows.pop().remove();
  78. // insertRow() is called directly
  79. if (!(collection instanceof Backbone.Collection) && !options) {
  80. this.collection.add(model, (options = collection));
  81. return;
  82. }
  83. var row = new this.row({
  84. columns: this.columns,
  85. model: model
  86. });
  87. var index = collection.indexOf(model);
  88. this.rows.splice(index, 0, row);
  89. var $el = this.$el;
  90. var $children = $el.children();
  91. var $rowEl = row.render().$el;
  92. if (index >= $children.length) {
  93. $el.append($rowEl);
  94. }
  95. else {
  96. $children.eq(index).before($rowEl);
  97. }
  98. return this;
  99. },
  100. /**
  101. The method can be called either directly or as a callback to a
  102. [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove)
  103. event.
  104. When called directly, it accepts a model or an array of models and an
  105. option hash just like
  106. [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove) and
  107. delegates to it. Once the model is removed, a corresponding row is removed
  108. from the body.
  109. When called as a callback of a `remove` event, splices into the rows and
  110. removes the row responsible for rendering the model.
  111. @param {Backbone.Model} model The model to remove from the body.
  112. @param {Backbone.Collection} collection When called directly, this
  113. parameter is actually the options to
  114. [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove).
  115. @param {Object} options When called directly, this must be null.
  116. See:
  117. - [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove)
  118. */
  119. removeRow: function (model, collection, options) {
  120. // removeRow() is called directly
  121. if (!options) {
  122. this.collection.remove(model, (options = collection));
  123. this._unshiftEmptyRowMayBe();
  124. return;
  125. }
  126. if (_.isUndefined(options.render) || options.render) {
  127. this.rows[options.index].remove();
  128. }
  129. this.rows.splice(options.index, 1);
  130. this._unshiftEmptyRowMayBe();
  131. return this;
  132. },
  133. /**
  134. Reinitialize all the rows inside the body and re-render them. Triggers a
  135. Backbone `backgrid:refresh` event from the collection along with the body
  136. instance as its sole parameter when done.
  137. */
  138. refresh: function () {
  139. for (var i = 0; i < this.rows.length; i++) {
  140. this.rows[i].remove();
  141. }
  142. this.rows = this.collection.map(function (model) {
  143. var row = new this.row({
  144. columns: this.columns,
  145. model: model
  146. });
  147. return row;
  148. }, this);
  149. this._unshiftEmptyRowMayBe();
  150. this.render();
  151. this.collection.trigger("backgrid:refresh", this);
  152. return this;
  153. },
  154. /**
  155. Renders all the rows inside this body. If the collection is empty and
  156. `options.emptyText` is defined and not null in the constructor, an empty
  157. row is rendered, otherwise no row is rendered.
  158. */
  159. render: function () {
  160. this.$el.empty();
  161. var fragment = document.createDocumentFragment();
  162. for (var i = 0; i < this.rows.length; i++) {
  163. var row = this.rows[i];
  164. fragment.appendChild(row.render().el);
  165. }
  166. this.el.appendChild(fragment);
  167. this.delegateEvents();
  168. return this;
  169. },
  170. /**
  171. Clean up this body and it's rows.
  172. @chainable
  173. */
  174. remove: function () {
  175. for (var i = 0; i < this.rows.length; i++) {
  176. var row = this.rows[i];
  177. row.remove.apply(row, arguments);
  178. }
  179. return Backbone.View.prototype.remove.apply(this, arguments);
  180. },
  181. /**
  182. If the underlying collection is a Backbone.PageableCollection in
  183. server-mode or infinite-mode, a page of models is fetched after sorting is
  184. done on the server.
  185. If the underlying collection is a Backbone.PageableCollection in
  186. client-mode, or any
  187. [Backbone.Collection](http://backbonejs.org/#Collection) instance, sorting
  188. is done on the client side. If the collection is an instance of a
  189. Backbone.PageableCollection, sorting will be done globally on all the pages
  190. and the current page will then be returned.
  191. Triggers a Backbone `backgrid:sorted` event from the collection when done
  192. with the column, direction and a reference to the collection.
  193. @param {Backgrid.Column|string} column
  194. @param {null|"ascending"|"descending"} direction
  195. See [Backbone.Collection#comparator](http://backbonejs.org/#Collection-comparator)
  196. */
  197. sort: function (column, direction) {
  198. if (!_.contains(["ascending", "descending", null], direction)) {
  199. throw new RangeError('direction must be one of "ascending", "descending" or `null`');
  200. }
  201. if (_.isString(column)) column = this.columns.findWhere({name: column});
  202. var collection = this.collection;
  203. var order;
  204. if (direction === "ascending") order = -1;
  205. else if (direction === "descending") order = 1;
  206. else order = null;
  207. var comparator = this.makeComparator(column.get("name"), order,
  208. order ?
  209. column.sortValue() :
  210. function (model) {
  211. return model.cid.replace('c', '') * 1;
  212. });
  213. if (Backbone.PageableCollection &&
  214. collection instanceof Backbone.PageableCollection) {
  215. collection.setSorting(order && column.get("name"), order,
  216. {sortValue: column.sortValue()});
  217. if (collection.fullCollection) {
  218. // If order is null, pageable will remove the comparator on both sides,
  219. // in this case the default insertion order comparator needs to be
  220. // attached to get back to the order before sorting.
  221. if (collection.fullCollection.comparator == null) {
  222. collection.fullCollection.comparator = comparator;
  223. }
  224. collection.fullCollection.sort();
  225. collection.trigger("backgrid:sorted", column, direction, collection);
  226. }
  227. else collection.fetch({reset: true, success: function () {
  228. collection.trigger("backgrid:sorted", column, direction, collection);
  229. }});
  230. }
  231. else {
  232. collection.comparator = comparator;
  233. collection.sort();
  234. collection.trigger("backgrid:sorted", column, direction, collection);
  235. }
  236. column.set("direction", direction);
  237. return this;
  238. },
  239. makeComparator: function (attr, order, func) {
  240. return function (left, right) {
  241. // extract the values from the models
  242. var l = func(left, attr), r = func(right, attr), t;
  243. // if descending order, swap left and right
  244. if (order === 1) t = l, l = r, r = t;
  245. // compare as usual
  246. if (l === r) return 0;
  247. else if (l < r) return -1;
  248. return 1;
  249. };
  250. },
  251. /**
  252. Moves focus to the next renderable and editable cell and return the
  253. currently editing cell to display mode.
  254. Triggers a `backgrid:next` event on the model with the indices of the row
  255. and column the user *intended* to move to, and whether the intended move
  256. was going to go out of bounds. Note that *out of bound* always means an
  257. attempt to go past the end of the last row.
  258. @param {Backbone.Model} model The originating model
  259. @param {Backgrid.Column} column The originating model column
  260. @param {Backgrid.Command} command The Command object constructed from a DOM
  261. event
  262. */
  263. moveToNextCell: function (model, column, command) {
  264. var i = this.collection.indexOf(model);
  265. var j = this.columns.indexOf(column);
  266. var cell, renderable, editable, m, n;
  267. this.rows[i].cells[j].exitEditMode();
  268. if (command.moveUp() || command.moveDown() || command.moveLeft() ||
  269. command.moveRight() || command.save()) {
  270. var l = this.columns.length;
  271. var maxOffset = l * this.collection.length;
  272. if (command.moveUp() || command.moveDown()) {
  273. m = i + (command.moveUp() ? -1 : 1);
  274. var row = this.rows[m];
  275. if (row) {
  276. cell = row.cells[j];
  277. if (Backgrid.callByNeed(cell.column.editable(), cell.column, model)) {
  278. cell.enterEditMode();
  279. model.trigger("backgrid:next", m, j, false);
  280. }
  281. }
  282. else model.trigger("backgrid:next", m, j, true);
  283. }
  284. else if (command.moveLeft() || command.moveRight()) {
  285. var right = command.moveRight();
  286. for (var offset = i * l + j + (right ? 1 : -1);
  287. offset >= 0 && offset < maxOffset;
  288. right ? offset++ : offset--) {
  289. m = ~~(offset / l);
  290. n = offset - m * l;
  291. cell = this.rows[m].cells[n];
  292. renderable = Backgrid.callByNeed(cell.column.renderable(), cell.column, cell.model);
  293. editable = Backgrid.callByNeed(cell.column.editable(), cell.column, model);
  294. if (renderable && editable) {
  295. cell.enterEditMode();
  296. model.trigger("backgrid:next", m, n, false);
  297. break;
  298. }
  299. }
  300. if (offset == maxOffset) {
  301. model.trigger("backgrid:next", ~~(offset / l), offset - m * l, true);
  302. }
  303. }
  304. }
  305. return this;
  306. }
  307. });