PageRenderTime 62ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/web-app/lib/backgrid/backgrid.js

https://github.com/delfianto/rams-web-app
JavaScript | 2172 lines | 935 code | 285 blank | 952 comment | 175 complexity | 0831a992ca6f5a414fc56e8d13bb9a48 MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause
  1. /*
  2. backgrid
  3. http://github.com/wyuenho/backgrid
  4. Copyright (c) 2013 Jimmy Yuen Ho Wong
  5. Licensed under the MIT @license.
  6. */
  7. (function (root, $, _, Backbone) {
  8. "use strict";
  9. /*
  10. backgrid
  11. http://github.com/wyuenho/backgrid
  12. Copyright (c) 2013 Jimmy Yuen Ho Wong
  13. Licensed under the MIT @license.
  14. */
  15. var window = root;
  16. var Backgrid = root.Backgrid = {
  17. VERSION: "0.1.4",
  18. Extension: {}
  19. };
  20. // Copyright 2009, 2010 Kristopher Michael Kowal
  21. // https://github.com/kriskowal/es5-shim
  22. // ES5 15.5.4.20
  23. // http://es5.github.com/#x15.5.4.20
  24. var ws = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003" +
  25. "\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028" +
  26. "\u2029\uFEFF";
  27. if (!String.prototype.trim || ws.trim()) {
  28. // http://blog.stevenlevithan.com/archives/faster-trim-javascript
  29. // http://perfectionkills.com/whitespace-deviations/
  30. ws = "[" + ws + "]";
  31. var trimBeginRegexp = new RegExp("^" + ws + ws + "*"),
  32. trimEndRegexp = new RegExp(ws + ws + "*$");
  33. String.prototype.trim = function trim() {
  34. if (this === undefined || this === null) {
  35. throw new TypeError("can't convert " + this + " to object");
  36. }
  37. return String(this)
  38. .replace(trimBeginRegexp, "")
  39. .replace(trimEndRegexp, "");
  40. };
  41. }
  42. function capitalize(s) {
  43. return String.fromCharCode(s.charCodeAt(0) - 32) + s.slice(1);
  44. }
  45. function lpad(str, length, padstr) {
  46. var paddingLen = length - (str + '').length;
  47. paddingLen = paddingLen < 0 ? 0 : paddingLen;
  48. var padding = '';
  49. for (var i = 0; i < paddingLen; i++) {
  50. padding = padding + padstr;
  51. }
  52. return padding + str;
  53. }
  54. function requireOptions(options, requireOptionKeys) {
  55. for (var i = 0; i < requireOptionKeys.length; i++) {
  56. var key = requireOptionKeys[i];
  57. if (_.isUndefined(options[key])) {
  58. throw new TypeError("'" + key + "' is required");
  59. }
  60. }
  61. }
  62. function resolveNameToClass(name, suffix) {
  63. if (_.isString(name)) {
  64. var key = capitalize(name) + suffix;
  65. var klass = Backgrid[key] || Backgrid.Extension[key];
  66. if (_.isUndefined(klass)) {
  67. throw new ReferenceError("Class '" + key + "' not found");
  68. }
  69. return klass;
  70. }
  71. return name;
  72. }
  73. /*
  74. backgrid
  75. http://github.com/wyuenho/backgrid
  76. Copyright (c) 2013 Jimmy Yuen Ho Wong
  77. Licensed under the MIT @license.
  78. */
  79. /**
  80. Just a convenient class for interested parties to subclass.
  81. The default Cell classes don't require the formatter to be a subclass of
  82. Formatter as long as the fromRaw(rawData) and toRaw(formattedData) methods
  83. are defined.
  84. @abstract
  85. @class Backgrid.CellFormatter
  86. @constructor
  87. */
  88. var CellFormatter = Backgrid.CellFormatter = function () {
  89. };
  90. _.extend(CellFormatter.prototype, {
  91. /**
  92. Takes a raw value from a model and returns a formatted string for display.
  93. @member Backgrid.CellFormatter
  94. @param {*} rawData
  95. @return {string}
  96. */
  97. fromRaw: function (rawData) {
  98. return rawData;
  99. },
  100. /**
  101. Takes a formatted string, usually from user input, and returns a
  102. appropriately typed value for persistence in the model.
  103. If the user input is invalid or unable to be converted to a raw value
  104. suitable for persistence in the model, toRaw must return `undefined`.
  105. @member Backgrid.CellFormatter
  106. @param {string} formattedData
  107. @return {*|undefined}
  108. */
  109. toRaw: function (formattedData) {
  110. return formattedData;
  111. }
  112. });
  113. /**
  114. A floating point number formatter. Doesn't understand notation at the moment.
  115. @class Backgrid.NumberFormatter
  116. @extends Backgrid.CellFormatter
  117. @constructor
  118. @throws {RangeError} If decimals < 0 or > 20.
  119. */
  120. var NumberFormatter = Backgrid.NumberFormatter = function (options) {
  121. options = options ? _.clone(options) : {};
  122. _.extend(this, this.defaults, options);
  123. if (this.decimals < 0 || this.decimals > 20) {
  124. throw new RangeError("decimals must be between 0 and 20");
  125. }
  126. };
  127. NumberFormatter.prototype = new CellFormatter;
  128. _.extend(NumberFormatter.prototype, {
  129. /**
  130. @member Backgrid.NumberFormatter
  131. @cfg {Object} options
  132. @cfg {number} [options.decimals=2] Number of decimals to display. Must be an integer.
  133. @cfg {string} [options.decimalSeparator='.'] The separator to use when
  134. displaying decimals.
  135. @cfg {string} [options.orderSeparator=','] The separator to use to
  136. separator thousands. May be an empty string.
  137. */
  138. defaults: {
  139. decimals: 2,
  140. decimalSeparator: '.',
  141. orderSeparator: ','
  142. },
  143. HUMANIZED_NUM_RE: /(\d)(?=(?:\d{3})+$)/g,
  144. /**
  145. Takes a floating point number and convert it to a formatted string where
  146. every thousand is separated by `orderSeparator`, with a `decimal` number of
  147. decimals separated by `decimalSeparator`. The number returned is rounded
  148. the usual way.
  149. @member Backgrid.NumberFormatter
  150. @param {number} number
  151. @return {string}
  152. */
  153. fromRaw: function (number) {
  154. if (isNaN(number) || number === null) return '';
  155. number = number.toFixed(~~this.decimals);
  156. var parts = number.split('.');
  157. var integerPart = parts[0];
  158. var decimalPart = parts[1] ? (this.decimalSeparator || '.') + parts[1] : '';
  159. return integerPart.replace(this.HUMANIZED_NUM_RE, '$1' + this.orderSeparator) + decimalPart;
  160. },
  161. /**
  162. Takes a string, possibly formatted with `orderSeparator` and/or
  163. `decimalSeparator`, and convert it back to a number.
  164. @member Backgrid.NumberFormatter
  165. @param {string} formattedData
  166. @return {number|undefined} Undefined if the string cannot be converted to
  167. a number.
  168. */
  169. toRaw: function (formattedData) {
  170. var rawData = '';
  171. var thousands = formattedData.trim().split(this.orderSeparator);
  172. for (var i = 0; i < thousands.length; i++) {
  173. rawData += thousands[i];
  174. }
  175. var decimalParts = rawData.split(this.decimalSeparator);
  176. rawData = '';
  177. for (var i = 0; i < decimalParts.length; i++) {
  178. rawData = rawData + decimalParts[i] + '.';
  179. }
  180. if (rawData[rawData.length - 1] === '.') {
  181. rawData = rawData.slice(0, rawData.length - 1);
  182. }
  183. var result = (rawData * 1).toFixed(~~this.decimals) * 1;
  184. if (_.isNumber(result) && !_.isNaN(result)) return result;
  185. }
  186. });
  187. /**
  188. Formatter to converts between various datetime string formats.
  189. This class only understands ISO-8601 formatted datetime strings. See
  190. Backgrid.Extension.MomentFormatter if you need a much more flexible datetime
  191. formatter.
  192. @class Backgrid.DatetimeFormatter
  193. @extends Backgrid.CellFormatter
  194. @constructor
  195. @throws {Error} If both `includeDate` and `includeTime` are false.
  196. */
  197. var DatetimeFormatter = Backgrid.DatetimeFormatter = function (options) {
  198. options = options ? _.clone(options) : {};
  199. _.extend(this, this.defaults, options);
  200. if (!this.includeDate && !this.includeTime) {
  201. throw new Error("Either includeDate or includeTime must be true");
  202. }
  203. };
  204. DatetimeFormatter.prototype = new CellFormatter;
  205. _.extend(DatetimeFormatter.prototype, {
  206. /**
  207. @member Backgrid.DatetimeFormatter
  208. @cfg {Object} options
  209. @cfg {boolean} [options.includeDate=true] Whether the values include the
  210. date part.
  211. @cfg {boolean} [options.includeTime=true] Whether the values include the
  212. time part.
  213. @cfg {boolean} [options.includeMilli=false] If `includeTime` is true,
  214. whether to include the millisecond part, if it exists.
  215. */
  216. defaults: {
  217. includeDate: true,
  218. includeTime: true,
  219. includeMilli: false
  220. },
  221. DATE_RE: /^([+\-]?\d{4})-(\d{2})-(\d{2})$/,
  222. TIME_RE: /^(\d{2}):(\d{2}):(\d{2})(\.(\d{3}))?$/,
  223. ISO_SPLITTER_RE: /T|Z| +/,
  224. _convert: function (data, validate) {
  225. if (_.isNull(data) || _.isUndefined(data)) return data;
  226. data = data.trim();
  227. var parts = data.split(this.ISO_SPLITTER_RE) || [];
  228. var date = this.DATE_RE.test(parts[0]) ? parts[0] : '';
  229. var time = date && parts[1] ? parts[1] : this.TIME_RE.test(parts[0]) ? parts[0] : '';
  230. var YYYYMMDD = this.DATE_RE.exec(date) || [];
  231. var HHmmssSSS = this.TIME_RE.exec(time) || [];
  232. if (validate) {
  233. if (this.includeDate && _.isUndefined(YYYYMMDD[0])) return;
  234. if (this.includeTime && _.isUndefined(HHmmssSSS[0])) return;
  235. if (!this.includeDate && date) return;
  236. if (!this.includeTime && time) return;
  237. }
  238. var jsDate = new Date(Date.UTC(YYYYMMDD[1] * 1 || 0,
  239. YYYYMMDD[2] * 1 - 1 || 0,
  240. YYYYMMDD[3] * 1 || 0,
  241. HHmmssSSS[1] * 1 || null,
  242. HHmmssSSS[2] * 1 || null,
  243. HHmmssSSS[3] * 1 || null,
  244. HHmmssSSS[5] * 1 || null));
  245. var result = '';
  246. if (this.includeDate) {
  247. result = lpad(jsDate.getUTCFullYear(), 4, 0) + '-' + lpad(jsDate.getUTCMonth() + 1, 2, 0) + '-' + lpad(jsDate.getUTCDate(), 2, 0);
  248. }
  249. if (this.includeTime) {
  250. result = result + (this.includeDate ? 'T' : '') + lpad(jsDate.getUTCHours(), 2, 0) + ':' + lpad(jsDate.getUTCMinutes(), 2, 0) + ':' + lpad(jsDate.getUTCSeconds(), 2, 0);
  251. if (this.includeMilli) {
  252. result = result + '.' + lpad(jsDate.getUTCMilliseconds(), 3, 0);
  253. }
  254. }
  255. if (this.includeDate && this.includeTime) {
  256. result += "Z";
  257. }
  258. return result;
  259. },
  260. /**
  261. Converts an ISO-8601 formatted datetime string to a datetime string, date
  262. string or a time string. The timezone is ignored if supplied.
  263. @member Backgrid.DatetimeFormatter
  264. @param {string} rawData
  265. @return {string|null|undefined} ISO-8601 string in UTC. Null and undefined values are returned as is.
  266. */
  267. fromRaw: function (rawData) {
  268. return this._convert(rawData);
  269. },
  270. /**
  271. Converts an ISO-8601 formatted datetime string to a datetime string, date
  272. string or a time string. The timezone is ignored if supplied. This method
  273. parses the input values exactly the same way as
  274. Backgrid.Extension.MomentFormatter#fromRaw(), in addition to doing some
  275. sanity checks.
  276. @member Backgrid.DatetimeFormatter
  277. @param {string} formattedData
  278. @return {string|undefined} ISO-8601 string in UTC. Undefined if a date is
  279. found `includeDate` is false, or a time is found if `includeTime` is false,
  280. or if `includeDate` is true and a date is not found, or if `includeTime` is
  281. true and a time is not found.
  282. */
  283. toRaw: function (formattedData) {
  284. return this._convert(formattedData, true);
  285. }
  286. });
  287. /*
  288. backgrid
  289. http://github.com/wyuenho/backgrid
  290. Copyright (c) 2013 Jimmy Yuen Ho Wong
  291. Licensed under the MIT @license.
  292. */
  293. /**
  294. Generic cell editor base class. Only defines an initializer for a number of
  295. required parameters.
  296. @abstract
  297. @class Backgrid.CellEditor
  298. @extends Backbone.View
  299. */
  300. var CellEditor = Backgrid.CellEditor = Backbone.View.extend({
  301. /**
  302. Initializer.
  303. @param {Object} options
  304. @param {*} options.parent
  305. @param {Backgrid.CellFormatter} options.formatter
  306. @param {Backgrid.Column} options.column
  307. @param {Backbone.Model} options.model
  308. @throws {TypeError} If `formatter` is not a formatter instance, or when
  309. `model` or `column` are undefined.
  310. */
  311. initialize: function (options) {
  312. requireOptions(options, ["formatter", "column", "model"]);
  313. this.parent = options.parent;
  314. this.formatter = options.formatter;
  315. this.column = options.column;
  316. if (!(this.column instanceof Column)) {
  317. this.column = new Column(this.column);
  318. }
  319. if (this.parent && _.isFunction(this.parent.on)) {
  320. this.listenTo(this.parent, "editing", this.postRender);
  321. }
  322. },
  323. /**
  324. Post-rendering setup and initialization. Focuses the cell editor's `el` in
  325. this default implementation. **Should** be called by Cell classes after
  326. calling Backgrid.CellEditor#render.
  327. */
  328. postRender: function () {
  329. this.$el.focus();
  330. return this;
  331. }
  332. });
  333. /**
  334. InputCellEditor the cell editor type used by most core cell types. This cell
  335. editor renders a text input box as its editor. The input will render a
  336. placeholder if the value is empty on supported browsers.
  337. @class Backgrid.InputCellEditor
  338. @extends Backgrid.CellEditor
  339. */
  340. var InputCellEditor = Backgrid.InputCellEditor = CellEditor.extend({
  341. /** @property */
  342. tagName: "input",
  343. /** @property */
  344. attributes: {
  345. type: "text"
  346. },
  347. /** @property */
  348. events: {
  349. "blur": "saveOrCancel",
  350. "keydown": "saveOrCancel"
  351. },
  352. /**
  353. Initializer. Removes this `el` from the DOM when a `done` event is
  354. triggered.
  355. @param {Object} options
  356. @param {Backgrid.CellFormatter} options.formatter
  357. @param {Backgrid.Column} options.column
  358. @param {Backbone.Model} options.model
  359. @param {string} [options.placeholder]
  360. */
  361. initialize: function (options) {
  362. CellEditor.prototype.initialize.apply(this, arguments);
  363. if (options.placeholder) {
  364. this.$el.attr("placeholder", options.placeholder);
  365. }
  366. this.listenTo(this, "done", this.remove);
  367. },
  368. /**
  369. Renders a text input with the cell value formatted for display, if it
  370. exists.
  371. */
  372. render: function () {
  373. this.$el.val(this.formatter.fromRaw(this.model.get(this.column.get("name"))));
  374. return this;
  375. },
  376. /**
  377. If the key pressed is `enter` or `tab`, converts the value in the editor to
  378. a raw value for the model using the formatter.
  379. If the key pressed is `esc` the changes are undone.
  380. If the editor's value was changed and goes out of focus (`blur`), the event
  381. is intercepted, cancelled so the cell remains in focus pending for further
  382. action.
  383. Triggers a Backbone `done` event when successful. `error` if the value
  384. cannot be converted. Classes listening to the `error` event, usually the
  385. Cell classes, should respond appropriately, usually by rendering some kind
  386. of error feedback.
  387. @param {Event} e
  388. */
  389. saveOrCancel: function (e) {
  390. if (e.type === "keydown") {
  391. // enter or tab
  392. if (e.keyCode === 13 || e.keyCode === 9) {
  393. e.preventDefault();
  394. var valueToSet = this.formatter.toRaw(this.$el.val());
  395. if (_.isUndefined(valueToSet) || !this.model.set(this.column.get("name"), valueToSet,
  396. {validate: true})) {
  397. this.trigger("error");
  398. }
  399. else {
  400. this.trigger("done");
  401. }
  402. }
  403. // esc
  404. else if (e.keyCode === 27) {
  405. // undo
  406. e.stopPropagation();
  407. this.trigger("done");
  408. }
  409. }
  410. else if (e.type === "blur") {
  411. if (this.formatter.fromRaw(this.model.get(this.column.get("name"))) === this.$el.val()) {
  412. this.trigger("done");
  413. }
  414. else {
  415. var self = this;
  416. var timeout = window.setTimeout(function () {
  417. self.$el.focus();
  418. window.clearTimeout(timeout);
  419. }, 1);
  420. }
  421. }
  422. },
  423. postRender: function () {
  424. // move the cursor to the end on firefox if text is right aligned
  425. if (this.$el.css("text-align") === "right") {
  426. var val = this.$el.val();
  427. this.$el.focus().val(null).val(val);
  428. }
  429. else {
  430. this.$el.focus();
  431. }
  432. return this;
  433. }
  434. });
  435. /**
  436. The super-class for all Cell types. By default, this class renders a plain
  437. table cell with the model value converted to a string using the
  438. formatter. The table cell is clickable, upon which the cell will go into
  439. editor mode, which is rendered by a Backgrid.InputCellEditor instance by
  440. default. Upon any formatting errors, this class will add a `error` CSS class
  441. to the table cell.
  442. @abstract
  443. @class Backgrid.Cell
  444. @extends Backbone.View
  445. */
  446. var Cell = Backgrid.Cell = Backbone.View.extend({
  447. /** @property */
  448. tagName: "td",
  449. /**
  450. @property {Backgrid.CellFormatter|Object|string} [formatter=new CellFormatter()]
  451. */
  452. formatter: new CellFormatter(),
  453. /**
  454. @property {Backgrid.CellEditor} [editor=Backgrid.InputCellEditor] The
  455. default editor for all cell instances of this class. This value must be a
  456. class, it will be automatically instantiated upon entering edit mode.
  457. See Backgrid.CellEditor
  458. */
  459. editor: InputCellEditor,
  460. /** @property */
  461. events: {
  462. "click": "enterEditMode"
  463. },
  464. /**
  465. Initializer.
  466. @param {Object} options
  467. @param {Backbone.Model} options.model
  468. @param {Backgrid.Column} options.column
  469. @throws {ReferenceError} If formatter is a string but a formatter class of
  470. said name cannot be found in the Backgrid module.
  471. */
  472. initialize: function (options) {
  473. requireOptions(options, ["model", "column"]);
  474. this.column = options.column;
  475. if (!(this.column instanceof Column)) {
  476. this.column = new Column(this.column);
  477. }
  478. this.formatter = resolveNameToClass(this.formatter, "Formatter");
  479. this.editor = resolveNameToClass(this.editor, "CellEditor");
  480. this.listenTo(this.model, "change:" + this.column.get("name"), function () {
  481. if (!this.$el.hasClass("editor")) this.render();
  482. });
  483. },
  484. /**
  485. Render a text string in a table cell. The text is converted from the
  486. model's raw value for this cell's column.
  487. */
  488. render: function () {
  489. this.$el.empty().text(this.formatter.fromRaw(this.model.get(this.column.get("name"))));
  490. return this;
  491. },
  492. /**
  493. If this column is editable, a new CellEditor instance is instantiated with
  494. its required parameters and listens on the editor's `done` and `error`
  495. events. When the editor is `done`, edit mode is exited. When the editor
  496. triggers an `error` event, it means the editor is unable to convert the
  497. current user input to an apprpriate value for the model's column. An
  498. `editor` CSS class is added to the cell upon entering edit mode.
  499. */
  500. enterEditMode: function (e) {
  501. if (this.column.get("editable")) {
  502. this.currentEditor = new this.editor({
  503. parent: this,
  504. column: this.column,
  505. model: this.model,
  506. formatter: this.formatter
  507. });
  508. /**
  509. Backbone Event. Fired when a cell is entering edit mode and an editor
  510. instance has been constructed, but before it is rendered and inserted
  511. into the DOM.
  512. @event edit
  513. @param {Backgrid.Cell} cell This cell instance.
  514. @param {Backgrid.CellEditor} editor The cell editor constructed.
  515. */
  516. this.trigger("edit", this, this.currentEditor);
  517. this.listenTo(this.currentEditor, "done", this.exitEditMode);
  518. this.listenTo(this.currentEditor, "error", this.renderError);
  519. this.$el.empty();
  520. this.undelegateEvents();
  521. this.$el.append(this.currentEditor.$el);
  522. this.currentEditor.render();
  523. this.$el.addClass("editor");
  524. /**
  525. Backbone Event. Fired when a cell has finished switching to edit mode.
  526. @event editing
  527. @param {Backgrid.Cell} cell This cell instance.
  528. @param {Backgrid.CellEditor} editor The cell editor constructed.
  529. */
  530. this.trigger("editing", this, this.currentEditor);
  531. }
  532. },
  533. /**
  534. Put an `error` CSS class on the table cell.
  535. */
  536. renderError: function () {
  537. this.$el.addClass("error");
  538. },
  539. /**
  540. Removes the editor and re-render in display mode.
  541. */
  542. exitEditMode: function () {
  543. this.$el.removeClass("error");
  544. this.currentEditor.off(null, null, this);
  545. this.currentEditor.remove();
  546. delete this.currentEditor;
  547. this.$el.removeClass("editor");
  548. this.render();
  549. this.delegateEvents();
  550. },
  551. /**
  552. Clean up this cell.
  553. @chainable
  554. */
  555. remove: function () {
  556. if (this.currentEditor) {
  557. this.currentEditor.remove.apply(this, arguments);
  558. delete this.currentEditor;
  559. }
  560. return Backbone.View.prototype.remove.apply(this, arguments);
  561. }
  562. });
  563. /**
  564. StringCell displays HTML escaped strings and accepts anything typed in.
  565. @class Backgrid.StringCell
  566. @extends Backgrid.Cell
  567. */
  568. var StringCell = Backgrid.StringCell = Cell.extend({
  569. /** @property */
  570. className: "string-cell"
  571. // No formatter needed. Strings call auto-escaped by jQuery on insertion.
  572. });
  573. /**
  574. UriCell renders an HTML `<a>` anchor for the value and accepts URIs as user
  575. input values. A URI input is URI encoded using `encodeURI()` before writing
  576. to the underlying model.
  577. @class Backgrid.UriCell
  578. @extends Backgrid.Cell
  579. */
  580. var UriCell = Backgrid.UriCell = Cell.extend({
  581. /** @property */
  582. className: "uri-cell",
  583. formatter: {
  584. fromRaw: function (rawData) {
  585. return rawData;
  586. },
  587. toRaw: function (formattedData) {
  588. var result = encodeURI(formattedData);
  589. return result === "undefined" ? undefined : result;
  590. }
  591. },
  592. render: function () {
  593. this.$el.empty();
  594. var formattedValue = this.formatter.fromRaw(this.model.get(this.column.get("name")));
  595. this.$el.append($("<a>", {
  596. href: formattedValue,
  597. title: formattedValue,
  598. target: "_blank"
  599. }).text(formattedValue));
  600. return this;
  601. }
  602. });
  603. /**
  604. Like Backgrid.UriCell, EmailCell renders an HTML `<a>` anchor for the
  605. value. The `href` in the anchor is prefixed with `mailto:`. EmailCell will
  606. complain if the user enters a string that doesn't contain the `@` sign.
  607. @class Backgrid.EmailCell
  608. @extends Backgrid.Cell
  609. */
  610. var EmailCell = Backgrid.EmailCell = Cell.extend({
  611. /** @property */
  612. className: "email-cell",
  613. formatter: {
  614. fromRaw: function (rawData) {
  615. return rawData;
  616. },
  617. toRaw: function (formattedData) {
  618. var parts = formattedData.split("@");
  619. if (parts.length === 2 && _.all(parts)) {
  620. return formattedData;
  621. }
  622. }
  623. },
  624. render: function () {
  625. this.$el.empty();
  626. var formattedValue = this.formatter.fromRaw(this.model.get(this.column.get("name")));
  627. this.$el.append($("<a>", {
  628. href: "mailto:" + formattedValue,
  629. title: formattedValue
  630. }).text(formattedValue));
  631. return this;
  632. }
  633. });
  634. /**
  635. NumberCell is a generic cell that renders all numbers. Numbers are formatted
  636. using a Backgrid.NumberFormatter.
  637. @class Backgrid.NumberCell
  638. @extends Backgrid.Cell
  639. */
  640. var NumberCell = Backgrid.NumberCell = Cell.extend({
  641. /** @property */
  642. className: "number-cell",
  643. /**
  644. @property {number} [decimals=2] Must be an integer.
  645. */
  646. decimals: NumberFormatter.prototype.defaults.decimals,
  647. /** @property {string} [decimalSeparator='.'] */
  648. decimalSeparator: NumberFormatter.prototype.defaults.decimalSeparator,
  649. /** @property {string} [orderSeparator=','] */
  650. orderSeparator: NumberFormatter.prototype.defaults.orderSeparator,
  651. /** @property {Backgrid.CellFormatter} [formatter=Backgrid.NumberFormatter] */
  652. formatter: NumberFormatter,
  653. /**
  654. Initializes this cell and the number formatter.
  655. @param {Object} options
  656. @param {Backbone.Model} options.model
  657. @param {Backgrid.Column} options.column
  658. */
  659. initialize: function (options) {
  660. Cell.prototype.initialize.apply(this, arguments);
  661. this.formatter = new this.formatter({
  662. decimals: this.decimals,
  663. decimalSeparator: this.decimalSeparator,
  664. orderSeparator: this.orderSeparator
  665. });
  666. }
  667. });
  668. /**
  669. An IntegerCell is just a Backgrid.NumberCell with 0 decimals. If a floating point
  670. number is supplied, the number is simply rounded the usual way when
  671. displayed.
  672. @class Backgrid.IntegerCell
  673. @extends Backgrid.NumberCell
  674. */
  675. var IntegerCell = Backgrid.IntegerCell = NumberCell.extend({
  676. /** @property */
  677. className: "integer-cell",
  678. /**
  679. @property {number} decimals Must be an integer.
  680. */
  681. decimals: 0
  682. });
  683. /**
  684. DatetimeCell is a basic cell that accepts datetime string values in RFC-2822
  685. or W3C's subset of ISO-8601 and displays them in ISO-8601 format. For a much
  686. more sophisticated date time cell with better datetime formatting, take a
  687. look at the Backgrid.Extension.MomentCell extension.
  688. @class Backgrid.DatetimeCell
  689. @extends Backgrid.Cell
  690. See:
  691. - Backgrid.Extension.MomentCell
  692. - Backgrid.DatetimeFormatter
  693. */
  694. var DatetimeCell = Backgrid.DatetimeCell = Cell.extend({
  695. /** @property */
  696. className: "datetime-cell",
  697. /**
  698. @property {boolean} [includeDate=true]
  699. */
  700. includeDate: DatetimeFormatter.prototype.defaults.includeDate,
  701. /**
  702. @property {boolean} [includeTime=true]
  703. */
  704. includeTime: DatetimeFormatter.prototype.defaults.includeTime,
  705. /**
  706. @property {boolean} [includeMilli=false]
  707. */
  708. includeMilli: DatetimeFormatter.prototype.defaults.includeMilli,
  709. /** @property {Backgrid.CellFormatter} [formatter=Backgrid.DatetimeFormatter] */
  710. formatter: DatetimeFormatter,
  711. /**
  712. Initializes this cell and the datetime formatter.
  713. @param {Object} options
  714. @param {Backbone.Model} options.model
  715. @param {Backgrid.Column} options.column
  716. */
  717. initialize: function (options) {
  718. Cell.prototype.initialize.apply(this, arguments);
  719. this.formatter = new this.formatter({
  720. includeDate: this.includeDate,
  721. includeTime: this.includeTime,
  722. includeMilli: this.includeMilli
  723. });
  724. var placeholder = this.includeDate ? "YYYY-MM-DD" : "";
  725. placeholder += (this.includeDate && this.includeTime) ? "T" : "";
  726. placeholder += this.includeTime ? "HH:mm:ss" : "";
  727. placeholder += (this.includeTime && this.includeMilli) ? ".SSS" : "";
  728. this.editor = this.editor.extend({
  729. attributes: _.extend({}, this.editor.prototype.attributes, this.editor.attributes, {
  730. placeholder: placeholder
  731. })
  732. });
  733. }
  734. });
  735. /**
  736. DateCell is a Backgrid.DatetimeCell without the time part.
  737. @class Backgrid.DateCell
  738. @extends Backgrid.DatetimeCell
  739. */
  740. var DateCell = Backgrid.DateCell = DatetimeCell.extend({
  741. /** @property */
  742. className: "date-cell",
  743. /** @property */
  744. includeTime: false
  745. });
  746. /**
  747. TimeCell is a Backgrid.DatetimeCell without the date part.
  748. @class Backgrid.TimeCell
  749. @extends Backgrid.DatetimeCell
  750. */
  751. var TimeCell = Backgrid.TimeCell = DatetimeCell.extend({
  752. /** @property */
  753. className: "time-cell",
  754. /** @property */
  755. includeDate: false
  756. });
  757. /**
  758. BooleanCell is a different kind of cell in that there's no difference between
  759. display mode and edit mode and this cell type always renders a checkbox for
  760. selection.
  761. @class Backgrid.BooleanCell
  762. @extends Backgrid.Cell
  763. */
  764. var BooleanCell = Backgrid.BooleanCell = Cell.extend({
  765. /** @property */
  766. className: "boolean-cell",
  767. /**
  768. BooleanCell simple uses a default HTML checkbox template instead of a
  769. CellEditor instance.
  770. @property {function(Object, ?Object=): string} editor The Underscore.js template to
  771. render the editor.
  772. */
  773. editor: _.template("<input type='checkbox'<%= checked ? checked='checked' : '' %> />'"),
  774. /**
  775. Since the editor is not an instance of a CellEditor subclass, more things
  776. need to be done in BooleanCell class to listen to editor mode events.
  777. */
  778. events: {
  779. "click": "enterEditMode",
  780. "blur input[type=checkbox]": "exitEditMode",
  781. "change input[type=checkbox]": "save"
  782. },
  783. /**
  784. Renders a checkbox and check it if the model value of this column is true,
  785. uncheck otherwise.
  786. */
  787. render: function () {
  788. this.$el.empty();
  789. this.currentEditor = $(this.editor({
  790. checked: this.formatter.fromRaw(this.model.get(this.column.get("name")))
  791. }));
  792. this.$el.append(this.currentEditor);
  793. return this;
  794. },
  795. /**
  796. Simple focuses the checkbox and add an `editor` CSS class to the cell.
  797. */
  798. enterEditMode: function (e) {
  799. this.$el.addClass("editor");
  800. this.currentEditor.focus();
  801. },
  802. /**
  803. Removed the `editor` CSS class from the cell.
  804. */
  805. exitEditMode: function (e) {
  806. this.$el.removeClass("editor");
  807. },
  808. /**
  809. Set true to the model attribute if the checkbox is checked, false
  810. otherwise.
  811. */
  812. save: function (e) {
  813. var val = this.formatter.toRaw(this.currentEditor.prop("checked"));
  814. this.model.set(this.column.get("name"), val);
  815. }
  816. });
  817. /**
  818. SelectCellEditor renders an HTML `<select>` fragment as the editor.
  819. @class Backgrid.SelectCellEditor
  820. @extends Backgrid.CellEditor
  821. */
  822. var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({
  823. /** @property */
  824. tagName: "select",
  825. /** @property */
  826. events: {
  827. "change": "save",
  828. "blur": "save"
  829. },
  830. /** @property {function(Object, ?Object=): string} template */
  831. template: _.template('<option value="<%- value %>" <%= selected ? \'selected="selected"\' : "" %>><%- text %></option>'),
  832. setOptionValues: function (optionValues) {
  833. this.optionValues = optionValues;
  834. },
  835. _renderOptions: function (nvps, currentValue) {
  836. var options = '';
  837. for (var i = 0; i < nvps.length; i++) {
  838. options = options + this.template({
  839. text: nvps[i][0],
  840. value: nvps[i][1],
  841. selected: currentValue == nvps[i][1]
  842. });
  843. }
  844. return options;
  845. },
  846. /**
  847. Renders the options if `optionValues` is a list of name-value pairs. The
  848. options are contained inside option groups if `optionValues` is a list of
  849. object hashes. The name is rendered at the option text and the value is the
  850. option value. If `optionValues` is a function, it is called without a
  851. parameter.
  852. */
  853. render: function () {
  854. this.$el.empty();
  855. var optionValues = _.result(this, "optionValues");
  856. var currentValue = this.model.get(this.column.get("name"));
  857. if (!_.isArray(optionValues)) throw TypeError("optionValues must be an array");
  858. var optionValue = null;
  859. var optionText = null;
  860. var optionValue = null;
  861. var optgroupName = null;
  862. var optgroup = null;
  863. for (var i = 0; i < optionValues.length; i++) {
  864. var optionValue = optionValues[i];
  865. if (_.isArray(optionValue)) {
  866. optionText = optionValue[0];
  867. optionValue = optionValue[1];
  868. this.$el.append(this.template({
  869. text: optionText,
  870. value: optionValue,
  871. selected: optionValue == currentValue
  872. }));
  873. }
  874. else if (_.isObject(optionValue)) {
  875. optgroupName = optionValue.name;
  876. optgroup = $("<optgroup></optgroup>", { label: optgroupName });
  877. optgroup.append(this._renderOptions(optionValue.values, currentValue));
  878. this.$el.append(optgroup);
  879. }
  880. else {
  881. throw TypeError("optionValues elements must be a name-value pair or an object hash of { name: 'optgroup label', value: [option name-value pairs] }");
  882. }
  883. }
  884. return this;
  885. },
  886. /**
  887. Saves the value of the selected option to the model attribute. Triggers a
  888. `done` Backbone event.
  889. */
  890. save: function (e) {
  891. this.model.set(this.column.get("name"), this.formatter.toRaw(this.$el.val()));
  892. this.trigger("done");
  893. }
  894. });
  895. /**
  896. SelectCell is also a different kind of cell in that upon going into edit mode
  897. the cell renders a list of options for to pick from, as opposed to an input
  898. box.
  899. SelectCell cannot be referenced by its string name when used in a column
  900. definition because requires an `optionValues` class attribute to be
  901. defined. `optionValues` can either be a list of name-value pairs, to be
  902. rendered as options, or a list of object hashes which consist of a key *name*
  903. which is the option group name, and a key *values* which is a list of
  904. name-value pairs to be rendered as options under that option group.
  905. In addition, `optionValues` can also be a parameter-less function that
  906. returns one of the above. If the options are static, it is recommended the
  907. returned values to be memoized. _.memoize() is a good function to help with
  908. that.
  909. @class Backgrid.SelectCell
  910. @extends Backgrid.Cell
  911. */
  912. var SelectCell = Backgrid.SelectCell = Cell.extend({
  913. /** @property */
  914. className: "select-cell",
  915. /** @property */
  916. editor: SelectCellEditor,
  917. /**
  918. @property {Array.<Array>|Array.<{name: string, values: Array.<Array>}>} optionValues
  919. */
  920. optionValues: undefined,
  921. /**
  922. Initializer.
  923. @param {Object} options
  924. @param {Backbone.Model} options.model
  925. @param {Backgrid.Column} options.column
  926. @throws {TypeError} If `optionsValues` is undefined.
  927. */
  928. initialize: function (options) {
  929. Cell.prototype.initialize.apply(this, arguments);
  930. requireOptions(this, ["optionValues"]);
  931. this.optionValues = _.result(this, "optionValues");
  932. this.listenTo(this, "edit", this.setOptionValues);
  933. },
  934. setOptionValues: function (cell, editor) {
  935. editor.setOptionValues(this.optionValues);
  936. },
  937. /**
  938. Renders the label using the raw value as key to look up from `optionValues`.
  939. @throws {TypeError} If `optionValues` is malformed.
  940. */
  941. render: function () {
  942. this.$el.empty();
  943. var optionValues = this.optionValues;
  944. var rawData = this.formatter.fromRaw(this.model.get(this.column.get("name")));
  945. try {
  946. if (!_.isArray(optionValues) || _.isEmpty(optionValues)) throw new TypeError;
  947. for (var i = 0; i < optionValues.length; i++) {
  948. var optionValue = optionValues[i];
  949. if (_.isArray(optionValue)) {
  950. var optionText = optionValue[0];
  951. var optionValue = optionValue[1];
  952. if (optionValue == rawData) {
  953. this.$el.append(optionText);
  954. break;
  955. }
  956. }
  957. else if (_.isObject(optionValue)) {
  958. var optionGroupValues = optionValue.values;
  959. for (var j = 0; j < optionGroupValues.length; j++) {
  960. var optionGroupValue = optionGroupValues[j];
  961. if (optionGroupValue[1] == rawData) {
  962. this.$el.append(optionGroupValue[0]);
  963. break;
  964. }
  965. }
  966. }
  967. else {
  968. throw new TypeError;
  969. }
  970. }
  971. }
  972. catch (ex) {
  973. if (ex instanceof TypeError) {
  974. throw TypeError("'optionValues' must be of type {Array.<Array>|Array.<{name: string, values: Array.<Array>}>}");
  975. }
  976. throw ex;
  977. }
  978. return this;
  979. }
  980. });
  981. /*
  982. backgrid
  983. http://github.com/wyuenho/backgrid
  984. Copyright (c) 2013 Jimmy Yuen Ho Wong
  985. Licensed under the MIT @license.
  986. */
  987. /**
  988. A Column is a placeholder for column metadata.
  989. You usually don't need to create an instance of this class yourself as a
  990. collection of column instances will be created for you from a list of column
  991. attributes in the Backgrid.js view class constructors.
  992. @class Backgrid.Column
  993. @extends Backbone.Model
  994. */
  995. var Column = Backgrid.Column = Backbone.Model.extend({
  996. defaults: {
  997. name: undefined,
  998. label: undefined,
  999. sortable: true,
  1000. editable: true,
  1001. renderable: true,
  1002. formatter: undefined,
  1003. cell: undefined,
  1004. headerCell: undefined
  1005. },
  1006. /**
  1007. Initializes this Column instance.
  1008. @param {Object} attrs Column attributes.
  1009. @param {string} attrs.name The name of the model attribute.
  1010. @param {string|Backgrid.Cell} attrs.cell The cell type.
  1011. If this is a string, the capitalized form will be used to look up a
  1012. cell class in Backbone, i.e.: string => StringCell. If a Cell subclass
  1013. is supplied, it is initialized with a hash of parameters. If a Cell
  1014. instance is supplied, it is used directly.
  1015. @param {string|Backgrid.HeaderCell} [attrs.headerCell] The header cell type.
  1016. @param {string} [attrs.label] The label to show in the header.
  1017. @param {boolean} [attrs.sortable=true]
  1018. @param {boolean} [attrs.editable=true]
  1019. @param {boolean} [attrs.renderable=true]
  1020. @param {Backgrid.CellFormatter|Object|string} [attrs.formatter] The
  1021. formatter to use to convert between raw model values and user input.
  1022. @throws {TypeError} If attrs.cell or attrs.options are not supplied.
  1023. @throws {ReferenceError} If attrs.cell is a string but a cell class of
  1024. said name cannot be found in the Backgrid module.
  1025. See:
  1026. - Backgrid.Cell
  1027. - Backgrid.CellFormatter
  1028. */
  1029. initialize: function (attrs) {
  1030. requireOptions(attrs, ["cell", "name"]);
  1031. if (!this.has("label")) {
  1032. this.set({ label: this.get("name") }, { silent: true });
  1033. }
  1034. var cell = resolveNameToClass(this.get("cell"), "Cell");
  1035. this.set({ cell: cell }, { silent: true });
  1036. }
  1037. });
  1038. /**
  1039. A Backbone collection of Column instances.
  1040. @class Backgrid.Columns
  1041. @extends Backbone.Collection
  1042. */
  1043. var Columns = Backgrid.Columns = Backbone.Collection.extend({
  1044. /**
  1045. @property {Backgrid.Column} model
  1046. */
  1047. model: Column
  1048. });
  1049. /*
  1050. backgrid
  1051. http://github.com/wyuenho/backgrid
  1052. Copyright (c) 2013 Jimmy Yuen Ho Wong
  1053. Licensed under the MIT @license.
  1054. */
  1055. /**
  1056. Row is a simple container view that takes a model instance and a list of
  1057. column metadata describing how each of the model's attribute is to be
  1058. rendered, and apply the appropriate cell to each attribute.
  1059. @class Backgrid.Row
  1060. @extends Backbone.View
  1061. */
  1062. var Row = Backgrid.Row = Backbone.View.extend({
  1063. /** @property */
  1064. tagName: "tr",
  1065. initOptionRequires: ["columns", "model"],
  1066. /**
  1067. Initializes a row view instance.
  1068. @param {Object} options
  1069. @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata.
  1070. @param {Backbone.Model} options.model The model instance to render.
  1071. @throws {TypeError} If options.columns or options.model is undefined.
  1072. */
  1073. initialize: function (options) {
  1074. requireOptions(options, this.initOptionRequires);
  1075. var columns = this.columns = options.columns;
  1076. if (!(columns instanceof Backbone.Collection)) {
  1077. columns = this.columns = new Columns(columns);
  1078. }
  1079. var cells = this.cells = [];
  1080. for (var i = 0; i < columns.length; i++) {
  1081. cells.push(this.makeCell(columns.at(i), options));
  1082. }
  1083. this.listenTo(columns, "change:renderable", function (column, renderable) {
  1084. for (var i = 0; i < cells.length; i++) {
  1085. var cell = cells[i];
  1086. if (cell.column.get("name") == column.get("name")) {
  1087. if (renderable) cell.$el.show(); else cell.$el.hide();
  1088. }
  1089. }
  1090. });
  1091. this.listenTo(columns, "add", function (column, columns) {
  1092. var i = columns.indexOf(column);
  1093. var cell = this.makeCell(column, options);
  1094. cells.splice(i, 0, cell);
  1095. if (!cell.column.get("renderable")) cell.$el.hide();
  1096. var $el = this.$el;
  1097. if (i === 0) {
  1098. $el.prepend(cell.render().$el);
  1099. }
  1100. else if (i === columns.length - 1) {
  1101. $el.append(cell.render().$el);
  1102. }
  1103. else {
  1104. $el.children().eq(i).before(cell.render().$el);
  1105. }
  1106. });
  1107. this.listenTo(columns, "remove", function (column, columns, opts) {
  1108. cells[opts.index].remove();
  1109. cells.splice(opts.index, 1);
  1110. });
  1111. },
  1112. /**
  1113. Factory method for making a cell. Used by #initialize internally. Override
  1114. this to provide an appropriate cell instance for a custom Row subclass.
  1115. @protected
  1116. @param {Backgrid.Column} column
  1117. @param {Object} options The options passed to #initialize.
  1118. @return {Backgrid.Cell}
  1119. */
  1120. makeCell: function (column) {
  1121. return new (column.get("cell"))({
  1122. column: column,
  1123. model: this.model
  1124. });
  1125. },
  1126. /**
  1127. Renders a row of cells for this row's model.
  1128. */
  1129. render: function () {
  1130. this.$el.empty();
  1131. var fragment = document.createDocumentFragment();
  1132. for (var i = 0; i < this.cells.length; i++) {
  1133. var cell = this.cells[i];
  1134. fragment.appendChild(cell.render().el);
  1135. if (!cell.column.get("renderable")) cell.$el.hide();
  1136. }
  1137. this.el.appendChild(fragment);
  1138. return this;
  1139. },
  1140. /**
  1141. Clean up this row and its cells.
  1142. @chainable
  1143. */
  1144. remove: function () {
  1145. for (var i = 0; i < this.cells.length; i++) {
  1146. var cell = this.cells[i];
  1147. cell.remove.apply(cell, arguments);
  1148. }
  1149. return Backbone.View.prototype.remove.apply(this, arguments);
  1150. }
  1151. });
  1152. /*
  1153. backgrid
  1154. http://github.com/wyuenho/backgrid
  1155. Copyright (c) 2013 Jimmy Yuen Ho Wong
  1156. Licensed under the MIT @license.
  1157. */
  1158. /**
  1159. HeaderCell is a special cell class that renders a column header cell. If the
  1160. column is sortable, a sorter is also rendered and will trigger a table
  1161. refresh after sorting.
  1162. @class Backgrid.HeaderCell
  1163. @extends Backbone.View
  1164. */
  1165. var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({
  1166. /** @property */
  1167. tagName: "th",
  1168. /** @property */
  1169. events: {
  1170. "click a": "onClick"
  1171. },
  1172. /**
  1173. @property {null|"ascending"|"descending"} _direction The current sorting
  1174. direction of this column.
  1175. */
  1176. _direction: null,
  1177. /**
  1178. Initializer.
  1179. @param {Object} options
  1180. @param {Backgrid.Column|Object} options.column
  1181. @throws {TypeError} If options.column or options.collection is undefined.
  1182. */
  1183. initialize: function (options) {
  1184. requireOptions(options, ["column", "collection"]);
  1185. this.column = options.column;
  1186. if (!(this.column instanceof Column)) {
  1187. this.column = new Column(this.column);
  1188. }
  1189. this.listenTo(Backbone, "backgrid:sort", this._resetCellDirection);
  1190. },
  1191. /**
  1192. Gets or sets the direction of this cell. If called directly without
  1193. parameters, returns the current direction of this cell, otherwise sets
  1194. it. If a `null` is given, sets this cell back to the default order.
  1195. @param {null|"ascending"|"descending"} dir
  1196. @return {null|string} The current direction or the changed direction.
  1197. */
  1198. direction: function (dir) {
  1199. if (arguments.length) {
  1200. if (this._direction) this.$el.removeClass(this._direction);
  1201. if (dir) this.$el.addClass(dir);
  1202. this._direction = dir;
  1203. }
  1204. return this._direction;
  1205. },
  1206. /**
  1207. Event handler for the Backbone `backgrid:sort` event. Resets this cell's
  1208. direction to default if sorting is being done on another column.
  1209. @private
  1210. */
  1211. _resetCellDirection: function (sortByColName, direction, comparator, collection) {
  1212. if (collection == this.collection) {
  1213. if (sortByColName !== this.column.get("name")) this.direction(null);
  1214. else this.direction(direction);
  1215. }
  1216. },
  1217. /**
  1218. Event handler for the `click` event on the cell's anchor. If the column is
  1219. sortable, clicking on the anchor will cycle through 3 sorting orderings -
  1220. `ascending`, `descending`, and default.
  1221. */
  1222. onClick: function (e) {
  1223. e.preventDefault();
  1224. var columnName = this.column.get("name");
  1225. if (this.column.get("sortable")) {
  1226. if (this.direction() === "ascending") {
  1227. this.sort(columnName, "descending", function (left, right) {
  1228. var leftVal = left.get(columnName);
  1229. var rightVal = right.get(columnName);
  1230. if (leftVal === rightVal) {
  1231. return 0;
  1232. }
  1233. else if (leftVal > rightVal) {
  1234. return -1;
  1235. }
  1236. return 1;
  1237. });
  1238. }
  1239. else if (this.direction() === "descending") {
  1240. this.sort(columnName, null);
  1241. }
  1242. else {
  1243. this.sort(columnName, "ascending", function (left, right) {
  1244. var leftVal = left.get(columnName);
  1245. var rightVal = right.get(columnName);
  1246. if (leftVal === rightVal) {
  1247. return 0;
  1248. }
  1249. else if (leftVal < rightVal) {
  1250. return -1;
  1251. }
  1252. return 1;
  1253. });
  1254. }
  1255. }
  1256. },
  1257. /**
  1258. If the underlying collection is a Backbone.PageableCollection in
  1259. server-mode or infinite-mode, a page of models is fetched after sorting is
  1260. done on the server.
  1261. If the underlying collection is a Backbone.PageableCollection in
  1262. client-mode, or any
  1263. [Backbone.Collection](http://backbonejs.org/#Collection) instance, sorting
  1264. is done on the client side. If the collection is an instance of a
  1265. Backbone.PageableCollection, sorting will be done globally on all the pages
  1266. and the current page will then be returned.
  1267. Triggers a Backbone `backgrid:sort` event when done.
  1268. @param {string} columnName
  1269. @param {null|"ascending"|"descending"} direction
  1270. @param {function(*, *): number} [comparator]
  1271. See [Backbone.Collection#comparator](http://backbonejs.org/#Collection-comparator)
  1272. */
  1273. sort: function (columnName, direction, comparator) {
  1274. comparator = comparator || this._cidComparator;
  1275. var collection = this.collection;
  1276. if (Backbone.PageableCollection && collection instanceof Backbone.PageableCollection) {
  1277. var order;
  1278. if (direction === "ascending") order = -1;
  1279. else if (direction === "descending") order = 1;
  1280. else order = null;
  1281. collection.setSorting(order ? columnName : null, order);
  1282. if (collection.mode == "client") {
  1283. if (!collection.fullCollection.comparator) {
  1284. collection.fullCollection.comparator = comparator;
  1285. }
  1286. collection.fullCollection.sort();
  1287. }
  1288. else collection.fetch();
  1289. }
  1290. else {
  1291. collection.comparator = comparator;
  1292. collection.sort();
  1293. }
  1294. /**
  1295. Global Backbone event. Fired when the sorter is clicked on a sortable
  1296. column.
  1297. @event backgrid:sort
  1298. @param {string} columnName
  1299. @param {null|"ascending"|"descending"} direction
  1300. @param {function(*, *): number} comparator A Backbone.Collection#comparator.
  1301. @param {Backbone.Collection} collection
  1302. */
  1303. Backbone.trigger("backgrid:sort", columnName, direction, comparator, this.collection);
  1304. },
  1305. /**
  1306. Default comparator for Backbone.Collections. Sorts cids in ascending
  1307. order. The cids of the models are assumed to be in insertion order.
  1308. @private
  1309. @param {*} left
  1310. @param {*} right
  1311. */
  1312. _cidComparator: function (left, right) {
  1313. var lcid = left.cid, rcid = right.cid;
  1314. if (!_.isUndefined(lcid) && !_.isUndefined(rcid)) {
  1315. lcid = lcid.slice(1) * 1, rcid = rcid.slice(1) * 1;
  1316. if (lcid < rcid) return -1;
  1317. else if (lcid > rcid) return 1;
  1318. }
  1319. return 0;
  1320. },
  1321. /**
  1322. Renders a header cell with a sorter and a label.
  1323. */
  1324. render: function () {
  1325. this.$el.empty();
  1326. var $label = $("<a>").text(this.column.get("label")).append("<b class='sort-caret'></b>");
  1327. this.$el.append($label);
  1328. return this;
  1329. }
  1330. });
  1331. /**
  1332. HeaderRow is a controller for a row of header cells.
  1333. @class Backgrid.HeaderRow
  1334. @extends Backgrid.Row
  1335. */
  1336. var HeaderRow = Backgrid.HeaderRow = Backgrid.Row.extend({
  1337. initOptionRequires: ["columns", "collection"],
  1338. /**
  1339. Initializer.
  1340. @param {Object} options
  1341. @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns
  1342. @param {Backgrid.HeaderCell} [options.headerCell] Customized default
  1343. HeaderCell for all the columns. Supply a HeaderCell class or instance to a
  1344. the `headerCell` key in a column definition for column-specific header
  1345. rendering.
  1346. @throws {TypeError} If options.columns or options.collection is undefined.
  1347. */
  1348. initialize: function () {
  1349. Backgrid.Row.prototype.initialize.apply(this, arguments);
  1350. },
  1351. makeCell: function (column, options) {
  1352. var headerCell = column.get("headerCell") || options.headerCell || HeaderCell;
  1353. headerCell = new headerCell({
  1354. column: column,
  1355. collection: this.collection
  1356. });
  1357. return headerCell;
  1358. }
  1359. });
  1360. /**
  1361. Header is a special structural view class that renders a table head with a
  1362. single row of header cells.
  1363. @class Backgrid.Header
  1364. @extends Backbone.View
  1365. */
  1366. var Header = Backgrid.Header = Backbone.View.extend({
  1367. /** @property */
  1368. tagName: "thead",
  1369. /**
  1370. Initializer. Initializes this table head view to contain a single header
  1371. row view.
  1372. @param {Object} options
  1373. @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata.
  1374. @param {Backbone.Model} options.model The model instance to render.
  1375. @throws {TypeError} If options.columns or options.model is undefined.
  1376. */
  1377. initialize: function (options) {
  1378. requireOptions(options, ["columns", "collection"]);
  1379. this.columns = options.columns;
  1380. if (!(this.columns instanceof Backbone.Collection)) {
  1381. this.columns = new Columns(this.columns);
  1382. }
  1383. this.row = new Backgrid.HeaderRow({
  1384. columns: this.columns,
  1385. collection: this.collection
  1386. });
  1387. },
  1388. /**
  1389. Renders this table head with a single row of header cells.
  1390. */
  1391. render: function () {
  1392. this.$el.append(this.row.render().$el);
  1393. return this;
  1394. },
  1395. /**
  1396. Clean up this header and its row.
  1397. @chainable
  1398. */
  1399. remove: function () {
  1400. this.row.remove.apply(this.row, arguments);
  1401. return Backbone.View.prototype.remove.apply(this, arguments);
  1402. }
  1403. });
  1404. /*
  1405. backgrid
  1406. http://github.com/wyuenho/backgrid
  1407. Copyright (c) 2013 Jimmy Yuen Ho Wong
  1408. Licensed under the MIT @license.
  1409. */
  1410. /**
  1411. Body is the table body which contains the rows inside a table. Body is
  1412. responsible for refreshing the rows after sorting, insertion and removal.
  1413. @class Backgrid.Body
  1414. @extends Backbone.View
  1415. */
  1416. var Body = Backgrid.Body = Backbone.View.extend({
  1417. /** @property */
  1418. tagName: "tbody",
  1419. /**
  1420. Initializer.
  1421. @param {Object} options
  1422. @param {Backbone.Collection} options.collection
  1423. @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns
  1424. Column metadata
  1425. @param {Backgrid.Row} [options.row=Backgrid.Row] The Row class to use.
  1426. @throws {TypeError} If options.columns or options.collection is undefined.
  1427. See Backgrid.Row.
  1428. */
  1429. initialize: function (options) {
  1430. requireOptions(options, ["columns", "collection"]);
  1431. this.columns = options.columns;
  1432. if (!(this.columns instanceof Backbone.Collection)) {
  1433. this.columns = new Columns(this.columns);
  1434. }
  1435. this.row = options.row || Row;
  1436. this.rows = this.collection.map(function (model) {
  1437. var row = new this.row({
  1438. columns: this.columns,
  1439. model: model
  1440. });
  1441. return row;
  1442. }, this);
  1443. var collection = this.collection;
  1444. this.listenTo(collection, "add", this.insertRow);
  1445. this.listenTo(collection, "remove", this.removeRow);
  1446. this.listenTo(collection, "sort", this.refresh);
  1447. this.listenTo(collection, "reset", this.refresh);
  1448. },
  1449. /**
  1450. This method can be called either directly or as a callback to a
  1451. [Backbone.Collecton#add](http://backbonejs.org/#Collection-add) event.
  1452. When called directly, it accepts a model or an array of models and an
  1453. option hash just like
  1454. [Backbone.Collection#add](http://backbonejs.org/#Collection-add) and
  1455. delegates to it. Once the model is added, a new row is inserted into the
  1456. body and automatically rendered.
  1457. When called as a callback of an `add` event, splices a new row into the
  1458. body and renders it.
  1459. @param {Backbone.Model} model The model to render as a row.
  1460. @param {Backbone.Collection} collection When called directly, this
  1461. parameter is actually the options to
  1462. [Backbone.Collection#add](http://backbonejs.org/#Collection-add).
  1463. @param {Object} options When called directly, this must be null.
  1464. See:
  1465. - [Backbone.Collection#add](http://backbonejs.org/#Collection-add)
  1466. */
  1467. insertRow: function (model, collection, options) {
  1468. // insertRow() is called directly
  1469. if (!(collection instanceof Backbone.Collection) && !options) {
  1470. this.collection.add(model, (options = collection));
  1471. return;
  1472. }
  1473. options = _.extend({render: true}, options || {});
  1474. var row = new this.row({
  1475. columns: this.columns,
  1476. model: model
  1477. });
  1478. var index = collection.indexOf(model);
  1479. this.rows.splice(index, 0, row);
  1480. var $el = this.$el;
  1481. var $children = $el.children();
  1482. var $rowEl = row.render().$el;
  1483. if (options.render) {
  1484. if (index >= $children.length) {
  1485. $el.append($rowEl);
  1486. }
  1487. else {
  1488. $children.eq(index).before($rowEl);
  1489. }
  1490. }
  1491. },
  1492. /**
  1493. The method can be called either directly or as a callback to a
  1494. [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove)
  1495. event.
  1496. When called directly, it accepts a model or an array of models and an
  1497. option hash just like
  1498. [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove) and
  1499. delegates to it. Once the model is removed, a corresponding row is removed
  1500. from the body.
  1501. When called as a callback of a `remove` event, splices into the rows and
  1502. removes the row responsible for rendering the model.
  1503. @param {Backbone.Model} model The model to remove from the body.
  1504. @param {Backbone.Collection} collection When called directly, this
  1505. parameter is actually the options to
  1506. [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove).
  1507. @param {Object} options When called directly, this must be null.
  1508. See:
  1509. - [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove)
  1510. */
  1511. removeRow: function (model, collection, options) {
  1512. // removeRow() is called directly
  1513. if (!options) {
  1514. this.collection.remove(model, (options = collection));
  1515. return;
  1516. }
  1517. if (_.isUndefined(options.render) || options.render) {
  1518. this.rows[options.index].remove();
  1519. }
  1520. this.rows.splice(options.index, 1);
  1521. },
  1522. /**
  1523. Reinitialize all the rows inside the body and re-render them.
  1524. @chainable
  1525. */
  1526. refresh: function () {
  1527. var self = this;
  1528. _.each(self.rows, function (row) {
  1529. row.remove();
  1530. });
  1531. self.rows = self.collection.map(function (model) {
  1532. var row = new self.row({
  1533. columns: self.columns,
  1534. model: model
  1535. });
  1536. return row;
  1537. });
  1538. self.render();
  1539. Backbone.trigger("backgrid:refresh");
  1540. return self;
  1541. },
  1542. /**
  1543. Renders all the rows inside this body.
  1544. */
  1545. render: function () {
  1546. this.$el.empty();
  1547. var fragment = document.createDocumentFragment();
  1548. for (var i = 0; i < this.rows.length; i++) {
  1549. var row = this.rows[i];
  1550. fragment.appendChild(row.render().el);
  1551. }
  1552. this.el.appendChild(fragment);
  1553. return this;
  1554. },
  1555. /**
  1556. Clean up this body and it's rows.
  1557. @chainable
  1558. */
  1559. remove: function () {
  1560. for (var i = 0; i < this.rows.length; i++) {
  1561. var row = this.rows[i];
  1562. row.remove.apply(row, arguments);
  1563. }
  1564. return Backbone.View.prototype.remove.apply(this, arguments);
  1565. }
  1566. });
  1567. /*
  1568. backgrid
  1569. http://github.com/wyuenho/backgrid
  1570. Copyright (c) 2013 Jimmy Yuen Ho Wong
  1571. Licensed under the MIT @license.
  1572. */
  1573. /**
  1574. A Footer is a generic class that only defines a default tag `tfoot` and
  1575. number of required parameters in the initializer.
  1576. @abstract
  1577. @class Backgrid.Footer
  1578. @extends Backbone.View
  1579. */
  1580. var Footer = Backgrid.Footer = Backbone.View.extend({
  1581. /** @property */
  1582. tagName: "tfoot",
  1583. /**
  1584. Initializer.
  1585. @param {Object} options
  1586. @param {*} options.parent The parent view class of this footer.
  1587. @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns
  1588. Column metadata.
  1589. @param {Backbone.Collection} options.collection
  1590. @throws {TypeError} If options.columns or options.collection is undefined.
  1591. */
  1592. initialize: function (options) {
  1593. requireOptions(options, ["columns", "collection"]);
  1594. this.parent = options.parent;
  1595. this.columns = options.columns;
  1596. if (!(this.columns instanceof Backbone.Collection)) {
  1597. this.columns = new Backgrid.Columns(this.columns);
  1598. }
  1599. }
  1600. });
  1601. /*
  1602. backgrid
  1603. http://github.com/wyuenho/backgrid
  1604. Copyright (c) 2013 Jimmy Yuen Ho Wong
  1605. Licensed under the MIT @license.
  1606. */
  1607. /**
  1608. Grid represents a data grid that has a header, body and an optional footer.
  1609. By default, a Grid treats each model in a collection as a row, and each
  1610. attribute in a model as a column. To render a grid you must provide a list of
  1611. column metadata and a collection to the Grid constructor. Just like any
  1612. Backbone.View class, the grid is rendered as a DOM node fragment when you
  1613. call render().
  1614. var grid = Backgrid.Grid({
  1615. columns: [{ name: "id", label: "ID", type: "string" },
  1616. // ...
  1617. ],
  1618. collections: books
  1619. });
  1620. $("#table-container").append(grid.render().el);
  1621. Optionally, if you want to customize the rendering of the grid's header and
  1622. footer, you may choose to extend Backgrid.Header and Backgrid.Footer, and
  1623. then supply that class or an instance of that class to the Grid constructor.
  1624. See the documentation for Header and Footer for further details.
  1625. var grid = Backgrid.Grid({
  1626. columns: [{ name: "id", label: "ID", type: "string" }],
  1627. collections: books,
  1628. header: Backgrid.Header.extend({
  1629. //...
  1630. }),
  1631. footer: Backgrid.Paginator
  1632. });
  1633. Finally, if you want to override how the rows are rendered in the table body,
  1634. you can supply a Body subclass as the `body` attribute that uses a different
  1635. Row class.
  1636. @class Backgrid.Grid
  1637. @extends Backbone.View
  1638. See:
  1639. - Backgrid.Column
  1640. - Backgrid.Header
  1641. - Backgrid.Body
  1642. - Backgrid.Row
  1643. - Backgrid.Footer
  1644. */
  1645. var Grid = Backgrid.Grid = Backbone.View.extend({
  1646. /** @property */
  1647. tagName: "table",
  1648. /** @property */
  1649. className: "backgrid",
  1650. /** @property */
  1651. header: Header,
  1652. /** @property */
  1653. body: Body,
  1654. /** @property */
  1655. footer: null,
  1656. /**
  1657. Initializes a Grid instance.
  1658. @param {Object} options
  1659. @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata.
  1660. @param {Backbone.Collection} options.collection The collection of tabular model data to display.
  1661. @param {Backgrid.Header} [options.header=Backgrid.Header] An optional Header class to override the default.
  1662. @param {Backgrid.Body} [options.body=Backgrid.Body] An optional Body class to override the default.
  1663. @param {Backgrid.Row} [options.row=Backgrid.Row] An optional Row class to override the default.
  1664. @param {Backgrid.Footer} [options.footer=Backgrid.Footer] An optional Footer class.
  1665. */
  1666. initialize: function (options) {
  1667. requireOptions(options, ["columns", "collection"]);
  1668. // Convert the list of column objects here first so the subviews don't have
  1669. // to.
  1670. if (!(options.columns instanceof Backbone.Collection)) {
  1671. options.columns = new Columns(options.columns);
  1672. }
  1673. this.columns = options.columns;
  1674. this.header = options.header || this.header;
  1675. this.header = new this.header(options);
  1676. this.body = options.body || this.body;
  1677. this.body = new this.body(options);
  1678. this.footer = options.footer || this.footer;
  1679. if (this.footer) {
  1680. this.footer = new this.footer(options);
  1681. }
  1682. this.listenTo(this.columns, "reset", function () {
  1683. this.header = new (this.header.remove().constructor)(options);
  1684. this.body = new (this.body.remove().constructor)(options);
  1685. if (this.footer) this.footer = new (this.footer.remove().constructor)(options);
  1686. this.render();
  1687. });
  1688. },
  1689. /**
  1690. Delegates to Backgrid.Body#insertRow.
  1691. */
  1692. insertRow: function (model, collection, options) {
  1693. return this.body.insertRow(model, collection, options);
  1694. },
  1695. /**
  1696. Delegates to Backgrid.Body#removeRow.
  1697. */
  1698. removeRow: function (model, collection, options) {
  1699. return this.body.removeRow(model, collection, options);
  1700. },
  1701. /**
  1702. Delegates to Backgrid.Columns#add for adding a column. Subviews can listen
  1703. to the `add` event from their internal `columns` if rerendering needs to
  1704. happen.
  1705. @param {Object} [options] Options for `Backgrid.Columns#add`.
  1706. @param {boolean} [options.render=true] Whether to render the column
  1707. immediately after insertion.
  1708. @chainable
  1709. */
  1710. insertColumn: function (column, options) {
  1711. options = options || {render: true};
  1712. this.columns.add(column, options);
  1713. return this;
  1714. },
  1715. /**
  1716. Delegates to Backgrid.Columns#remove for removing a column. Subviews can
  1717. listen to the `remove` event from the internal `columns` if rerendering
  1718. needs to happen.
  1719. @param {Object} [options] Options for `Backgrid.Columns#remove`.
  1720. @chainable
  1721. */
  1722. removeColumn: function (column, options) {
  1723. this.columns.remove(column, options);
  1724. return this;
  1725. },
  1726. /**
  1727. Renders the grid's header, then footer, then finally the body.
  1728. */
  1729. render: function () {
  1730. this.$el.empty();
  1731. this.$el.append(this.header.render().$el);
  1732. if (this.footer) {
  1733. this.$el.append(this.footer.render().$el);
  1734. }
  1735. this.$el.append(this.body.render().$el);
  1736. /**
  1737. Backbone event. Fired when the grid has been successfully rendered.
  1738. @event rendered
  1739. */
  1740. this.trigger("rendered");
  1741. return this;
  1742. },
  1743. /**
  1744. Clean up this grid and its subviews.
  1745. @chainable
  1746. */
  1747. remove: function () {
  1748. this.header.remove.apply(this.header, arguments);
  1749. this.body.remove.apply(this.body, arguments);
  1750. this.footer && this.footer.remove.apply(this.footer, arguments);
  1751. return Backbone.View.prototype.remove.apply(this, arguments);
  1752. }
  1753. });
  1754. }(this, $, _, Backbone));