UNPKG

32.9 kBJavaScriptView Raw
1import $ from './jquery';
2import 'jquery-ui/ui/core';
3import 'jquery-ui/ui/widget';
4import 'jquery-ui/ui/widgets/mouse';
5import 'jquery-ui/ui/widgets/draggable';
6import 'jquery-ui/ui/widgets/sortable';
7import * as logger from './internal/log';
8import Backbone from 'backbone';
9import classNames from './restful-table/class-names';
10import CustomCreateView from './restful-table/custom-create-view';
11import CustomEditView from './restful-table/custom-edit-view';
12import CustomReadView from './restful-table/custom-read-view';
13import dataKeys from './restful-table/data-keys';
14import EditRow from './restful-table/edit-row';
15import EntryModel from './restful-table/entry-model';
16import {triggerEvtForInst} from './restful-table/event-handlers';
17import events from './restful-table/event-names';
18import globalize from './internal/globalize';
19import Row from './restful-table/row';
20import { I18n } from './i18n';
21import {spinner} from './restful-table/spinner';
22
23/**
24 * A table whose entries/rows can be retrieved, added and updated via REST (CRUD).
25 * It uses backbone.js to sync the table's state back to the server, avoiding page refreshes.
26 *
27 * @class RestfulTable
28 */
29var RestfulTable = Backbone.View.extend({
30 /**
31 * @param {!Object} options
32 * ... {!Object} resources
33 * ... ... {(string|function(function(Array.<Object>)))} all - URL of REST resource OR function that retrieves all entities.
34 * ... ... {string} self - URL of REST resource to sync a single entities state (CRUD).
35 * ... {!(selector|Element|jQuery)} el - Table element or selector of the table element to populate.
36 * ... {!Array.<Object>} columns - Which properties of the entities to render. The id of a column maps to the property of an entity.
37 * ... {Object} views
38 * ... ... {RestfulTable.EditRow} editRow - Backbone view that renders the edit & create row. Your view MUST extend RestfulTable.EditRow.
39 * ... ... {RestfulTable.Row} row - Backbone view that renders the readonly row. Your view MUST extend RestfulTable.Row.
40 * ... {boolean} allowEdit - Is the table editable. If true, clicking row will switch it to edit state. Default true.
41 * ... {boolean} allowDelete - Can entries be removed from the table, default true.
42 * ... {boolean} allowCreate - Can new entries be added to the table, default true.
43 * ... {boolean} allowReorder - Can we drag rows to reorder them, default false.
44 * ... {boolean} autoFocus - Automatically set focus to first field on init, default false.
45 * ... {boolean} reverseOrder - Reverse the order of rows, default false.
46 * ... {boolean} silent - Do not trigger a "refresh" event on sort, default false.
47 * ... {String} id - The id for the table. This id will be used to fire events specific to this instance.
48 * ... {string} createPosition - If set to "bottom", place the create form at the bottom of the table instead of the top.
49 * ... {string} addPosition - If set to "bottom", add new rows at the bottom of the table instead of the top. If undefined, createPosition will be used to define where to add the new row.
50 * ... {string} noEntriesMsg - Text to display under the table header if it is empty, default empty.
51 * ... {string} loadingMsg - Text/HTML to display while loading, default "Loading".
52 * ... {string} submitAccessKey - Access key for submitting.
53 * ... {string} cancelAccessKey - Access key for canceling.
54 * ... @property {RestfulTable~deleteConfirmationCallback} deleteConfirmationCallback - function returning Promise determining if row should be deleted or not
55 * ... {function(string): (selector|jQuery|Element)} fieldFocusSelector - Element to focus on given a name.
56 * ... {EntryModel} model - Backbone model representing a row, default EntryModel.
57 * ... {Backbone.Collection} Collection - Backbone collection representing the entire table, default Backbone.Collection.
58 * @callback deleteConfirmationCallback
59 */
60 initialize: function (options) {
61 var instance = this;
62
63 // combine default and user options
64 instance.options = $.extend(true, instance._getDefaultOptions(options), options);
65
66 // Prefix events for this instance with this id.
67 instance.id = this.options.id;
68
69 // faster lookup
70 instance._event = events;
71 instance.classNames = classNames;
72 instance.dataKeys = dataKeys;
73
74 // shortcuts to popular elements
75 this.$table = $(options.el)
76 .addClass(this.classNames.RESTFUL_TABLE)
77 .addClass(this.classNames.ALLOW_HOVER)
78 .addClass('aui');
79
80 this.$table.wrapAll("<form class='aui' action='#' />");
81
82 this.$thead = $('<thead/>');
83 this.$theadRow = $('<tr />').appendTo(this.$thead);
84 this.$tbody = $('<tbody/>');
85
86 if (!this.$table.length) {
87 throw new Error('RestfulTable: Init failed! The table you have specified [' + this.$table.selector + '] cannot be found.');
88 }
89
90 if (!this.options.columns) {
91 throw new Error("RestfulTable: Init failed! You haven't provided any columns to render.");
92 }
93
94 if (this.options.deleteConfirmationCallback && !(this.options.deleteConfirmationCallback instanceof Function)) {
95 throw new Error('RestfulTable: Init failed! deleteConfirmationCallback is not a function');
96 }
97
98 // Let user know the table is loading
99 this.showGlobalLoading();
100 this.options.columns.forEach(function (column) {
101 var header = $.isFunction(column.header) ? column.header() : column.header;
102 if (typeof header === 'undefined') {
103 logger.warn('You have not specified [header] for column [' + column.id + ']. Using id for now...');
104 header = column.id;
105 }
106
107 instance.$theadRow.append('<th>' + header + '</th>');
108 });
109
110 // columns for submit buttons and loading indicator used when editing
111 instance.$theadRow.append('<th></th><th></th>');
112
113 // create a new Backbone collection to represent rows (http://documentcloud.github.com/backbone/#Collection)
114 this._models = this._createCollection();
115
116 // shortcut to the class we use to create rows
117 this._rowClass = this.options.views.row;
118
119 this.editRows = []; // keep track of rows that are being edited concurrently
120
121 this.$table.closest('form').submit(function (e) {
122 if (instance.focusedRow) {
123 // Delegates saving of row. See EditRow.submit
124 instance.focusedRow.trigger(instance._event.SAVE);
125 }
126 e.preventDefault();
127 });
128
129 if (this.options.allowReorder) {
130 // Add allowance for another cell to the <thead>
131 this.$theadRow.prepend('<th />');
132
133 // Allow drag and drop reordering of rows
134 this.$tbody.sortable({
135 handle: '.' + this.classNames.DRAG_HANDLE,
136 helper: function (e, elt) {
137 var helper = $('<div/>').attr('class', elt.attr('class')).addClass(instance.classNames.MOVEABLE);
138 elt.children().each(function () {
139 var $td = $(this);
140
141 // .offsetWidth/.outerWidth() is broken in webkit for tables, so we do .clientWidth + borders
142 // Need to coerce the border-left-width to an in because IE - http://bugs.jquery.com/ticket/10855
143 var borderLeft = parseInt(0 + $td.css('border-left-width'), 10);
144 var borderRight = parseInt(0 + $td.css('border-right-width'), 10);
145 var width = $td[0].clientWidth + borderLeft + borderRight;
146
147 helper.append($('<div/>').html($td.html()).attr('class', $td.attr('class')).width(width));
148 });
149
150 helper = $("<div class='aui-restfultable-readonly'/>").append(helper); // Basically just to get the styles.
151 helper.css({left: elt.offset().left}); // To align with the other table rows, since we've locked scrolling on x.
152 helper.appendTo(document.body);
153
154 return helper;
155 },
156 start: function (event, ui) {
157 var cachedHeight = ui.helper[0].clientHeight;
158 var $this = ui.placeholder.find('td');
159
160 // Make sure that when we start dragging widths do not change
161 ui.item
162 .addClass(instance.classNames.MOVEABLE)
163 .children().each(function (i) {
164 $(this).width($this.eq(i).width());
165 });
166
167 // Create a <td> to add to the placeholder <tr> to inherit CSS styles.
168 var td = '<td colspan="' + instance.getColumnCount() + '">&nbsp;</td>';
169
170 ui.placeholder.html(td).css({
171 height: cachedHeight,
172 visibility: 'visible'
173 });
174
175 // Stop hover effects etc from occuring as we move the mouse (while dragging) over other rows
176 instance.getRowFromElement(ui.item[0]).trigger(instance._event.MODAL);
177 },
178 stop: function (event, ui) {
179 if ($(ui.item[0]).is(':visible')) {
180 ui.item
181 .removeClass(instance.classNames.MOVEABLE)
182 .children().attr('style', '');
183
184 ui.placeholder.removeClass(instance.classNames.ROW);
185
186 // Return table to a normal state
187 instance.getRowFromElement(ui.item[0]).trigger(instance._event.MODELESS);
188 }
189 },
190 update: function (event, ui) {
191 var context = {
192 row: instance.getRowFromElement(ui.item[0]),
193 item: ui.item,
194 nextItem: ui.item.next(),
195 prevItem: ui.item.prev()
196 };
197
198 instance.move(context);
199 },
200 axis: 'y',
201 delay: 0,
202 containment: 'document',
203 cursor: 'move',
204 scroll: true,
205 zIndex: 8000
206 });
207
208 // Prevent text selection while reordering.
209 this.$tbody.on('selectstart mousedown', function (event) {
210 return !$(event.target).is('.' + instance.classNames.DRAG_HANDLE);
211 });
212 }
213
214 if (this.options.allowCreate !== false) {
215
216 // Create row responsible for adding new entries ...
217 this._createRow = new this.options.views.editRow({
218 columns: this.options.columns,
219 isCreateRow: true,
220 model: this.options.model.extend({
221 url: function () {
222 return instance.options.resources.self;
223 }
224 }),
225 cancelAccessKey: this.options.cancelAccessKey,
226 submitAccessKey: this.options.submitAccessKey,
227 allowReorder: this.options.allowReorder,
228 fieldFocusSelector: this.options.fieldFocusSelector
229 });
230 this._createRow.on(this._event.CREATED, function (values) {
231 if ((typeof instance.options.addPosition === 'undefined' && instance.options.createPosition === 'bottom') ||
232 instance.options.addPosition === 'bottom') {
233 instance.addRow(values);
234 } else {
235 instance.addRow(values, 0);
236 }
237 });
238 this._createRow.on(this._event.VALIDATION_ERROR, function () {
239 this.trigger(instance._event.FOCUS);
240 });
241 this._createRow.render({
242 errors: {},
243 values: {}
244 });
245
246 // ... and appends it as the first row
247 this.$create = $('<tbody class="' + this.classNames.CREATE + '" />')
248 .append(this._createRow.el);
249
250 // Manage which row has focus
251 this._applyFocusCoordinator(this._createRow);
252
253 // focus create row
254 if (this.options.autoFocus) {
255 this._createRow.trigger(this._event.FOCUS);
256 }
257 }
258
259 // when a model is removed from the collection, remove it from the viewport also
260 this._models.on('remove', function (model) {
261 instance.getRows().forEach(function (row) {
262 if (row.model === model) {
263 if (row.hasFocus() && instance._createRow) {
264 instance._createRow.trigger(instance._event.FOCUS);
265 }
266 instance.removeRow(row);
267 }
268 });
269 });
270
271 this.fetchInitialResources();
272 },
273
274 fetchInitialResources: function () {
275 var instance = this;
276 if ($.isFunction(this.options.resources.all)) {
277 this.options.resources.all(function (entries) {
278 instance.populate(entries);
279 });
280 } else {
281 $.get(this.options.resources.all, function (entries) {
282 instance.populate(entries);
283 });
284 }
285 },
286
287 move: function (context) {
288
289 var instance = this;
290
291 var createRequest = function (afterElement) {
292 if (!afterElement.length) {
293 return {
294 position: 'First'
295 };
296 } else {
297 var afterModel = instance.getRowFromElement(afterElement).model;
298 return {
299 after: afterModel.url()
300 };
301 }
302 };
303
304 if (context.row) {
305
306 var data = instance.options.reverseOrder ? createRequest(context.nextItem) : createRequest(context.prevItem);
307
308 $.ajax({
309 url: context.row.model.url() + '/move',
310 type: 'POST',
311 dataType: 'json',
312 contentType: 'application/json',
313 data: JSON.stringify(data),
314 complete: function () {
315 // hides loading indicator (spinner)
316 context.row.hideLoading();
317 },
318 success: function (xhr) {
319 triggerEvtForInst(instance._event.REORDER_SUCCESS, instance, [xhr]);
320 },
321 error: function (xhr) {
322 var responseData = $.parseJSON(xhr.responseText || xhr.data);
323 triggerEvtForInst(instance._event.SERVER_ERROR, instance, [responseData, xhr, this]);
324 }
325 });
326
327 // shows loading indicator (spinner)
328 context.row.showLoading();
329 }
330 },
331
332 _createCollection: function () {
333 var instance = this;
334
335 // create a new Backbone collection to represent rows (http://documentcloud.github.com/backbone/#Collection)
336 var RowsAwareCollection = this.options.Collection.extend({
337 // Force the collection to re-sort itself. You don't need to call this under normal
338 // circumstances, as the set will maintain sort order as each item is added.
339 sort: function (options) {
340 options || (options = {});
341 if (!this.comparator) {
342 throw new Error('Cannot sort a set without a comparator');
343 }
344 this.tableRows = instance.getRows();
345 this.models = this.sortBy(this.comparator, this);
346 this.tableRows = undefined;
347 if (!options.silent) {
348 this.trigger('refresh', this, options);
349 }
350 return this;
351 },
352 remove: function (...args) {
353 this.tableRows = instance.getRows();
354 Backbone.Collection.prototype.remove.apply(this, args);
355 this.tableRows = undefined;
356 return this;
357 }
358 });
359
360 return new RowsAwareCollection([], {
361 comparator: function (row) {
362 // sort models in collection based on dom ordering
363 var index;
364
365 var currentTableRows = (this && this.tableRows !== undefined) ? this.tableRows : instance.getRows();
366 currentTableRows.some(function (item, i) {
367 if (item.model.id === row.id) {
368 index = i;
369 return true;
370 }
371 });
372 return index;
373 }
374 });
375 },
376
377 /**
378 * Refreshes table with entries
379 *
380 * @param entries
381 */
382 populate: function (entries) {
383 if (this.options.reverseOrder) {
384 entries.reverse();
385 }
386
387 this.hideGlobalLoading();
388 if (entries && entries.length) {
389 // Empty the models collection
390 this._models.reset([], {silent: true});
391 // Add all the entries to collection and render them
392 this.renderRows(entries);
393 // show message to user if we have no entries
394 if (this.isEmpty()) {
395 this.showNoEntriesMsg();
396 }
397 } else {
398 this.showNoEntriesMsg();
399 }
400
401 // Ok, lets let everyone know that we are done...
402 this.$table
403 .append(this.$thead);
404
405 if (this.options.createPosition === 'bottom') {
406 this.$table.append(this.$tbody)
407 .append(this.$create);
408 } else {
409 this.$table
410 .append(this.$create)
411 .append(this.$tbody);
412 }
413
414 this.$table.trigger(this._event.INITIALIZED, [this]);
415
416 triggerEvtForInst(this._event.INITIALIZED, this, [this]);
417
418 if (this.options.autoFocus) {
419 this.$table.find(':input:text:first').focus(); // set focus to first field
420 }
421 },
422
423 /**
424 * Shows loading indicator and text
425 *
426 * @return {RestfulTable}
427 */
428 showGlobalLoading: function () {
429 if (!this.$loading) {
430 this.$loading = $('<div class="aui-restfultable-init">' +
431 '<span class="aui-restfultable-loading">' + spinner + this.options.loadingMsg + '</span></div>');
432 }
433
434 if (!this.$loading.is(':visible')) {
435 this.$loading.insertAfter(this.$table);
436 }
437
438 return this;
439 },
440
441 /**
442 * Hides loading indicator and text
443 * @return {RestfulTable}
444 */
445 hideGlobalLoading: function () {
446 if (this.$loading) {
447 this.$loading.remove();
448 }
449 return this;
450 },
451
452
453 /**
454 * Adds row to collection and renders it
455 *
456 * @param {Object} values
457 * @param {number} index
458 * @return {RestfulTable}
459 */
460 addRow: function (values, index) {
461 var view;
462 var model;
463
464 if (!values.id) {
465 throw new Error('RestfulTable.addRow: to add a row values object must contain an id. ' +
466 'Maybe you are not returning it from your restend point?' +
467 'Recieved:' + JSON.stringify(values));
468 }
469
470 model = new this.options.model(values);
471
472
473 view = this._renderRow(model, index);
474
475 this._models.add(model);
476 this.removeNoEntriesMsg();
477
478 // Let everyone know we added a row
479 triggerEvtForInst(this._event.ROW_ADDED, this, [view, this]);
480 return this;
481 },
482
483 /**
484 * Provided a view, removes it from display and backbone collection
485 *
486 * @param row {Row} The row to remove.
487 */
488 removeRow: function (row) {
489 this._models.remove(row.model);
490 row.remove();
491
492 if (this.isEmpty()) {
493 this.showNoEntriesMsg();
494 }
495
496 // Let everyone know we removed a row
497 triggerEvtForInst(this._event.ROW_REMOVED, this, [row, this]);
498 },
499
500 /**
501 * Is there any entries in the table
502 *
503 * @return {Boolean}
504 */
505 isEmpty: function () {
506 return this._models.length === 0;
507 },
508
509 /**
510 * Gets all models
511 *
512 * @return {Backbone.Collection}
513 */
514 getModels: function () {
515 return this._models;
516 },
517
518 /**
519 * Gets table body
520 *
521 * @return {jQuery}
522 */
523 getTable: function () {
524 return this.$table;
525 },
526
527 /**
528 * Gets table body
529 *
530 * @return {jQuery}
531 */
532 getTableBody: function () {
533 return this.$tbody;
534 },
535
536 /**
537 * Gets create Row
538 *
539 * @return {EditRow}
540 */
541 getCreateRow: function () {
542 return this._createRow;
543 },
544
545 /**
546 * Gets the number of table columns, accounting for the number of
547 * additional columns added by RestfulTable itself
548 * (such as the drag handle column, buttons and actions columns)
549 *
550 * @return {Number}
551 */
552 getColumnCount: function () {
553 var staticFieldCount = 2; // accounts for the columns allocated to submit buttons and loading indicator
554 if (this.allowReorder) {
555 ++staticFieldCount;
556 }
557 return this.options.columns.length + staticFieldCount;
558 },
559
560 /**
561 * Get the Row that corresponds to the given <tr> element.
562 *
563 * @param {HTMLElement} tr
564 *
565 * @return {Row}
566 */
567 getRowFromElement: function (tr) {
568 return $(tr).data(this.dataKeys.ROW_VIEW);
569 },
570
571 /**
572 * Shows message {options.noEntriesMsg} to the user if there are no entries
573 *
574 * @return {RestfulTable}
575 */
576 showNoEntriesMsg: function () {
577
578 if (this.$noEntries) {
579 this.$noEntries.remove();
580 }
581
582 this.$noEntries = $('<tr>')
583 .addClass(this.classNames.NO_ENTRIES)
584 .append($('<td>')
585 .attr('colspan', this.getColumnCount())
586 .text(this.options.noEntriesMsg)
587 )
588 .appendTo(this.$tbody);
589
590 return this;
591 },
592
593 /**
594 * Removes message {options.noEntriesMsg} to the user if there ARE entries
595 *
596 * @return {RestfulTable}
597 */
598 removeNoEntriesMsg: function () {
599 if (this.$noEntries && this._models.length > 0) {
600 this.$noEntries.remove();
601 }
602 return this;
603 },
604
605 /**
606 * Gets the Row from their associated <tr> elements
607 *
608 * @return {Array}
609 */
610 getRows: function () {
611 var instance = this;
612 var views = [];
613
614 this.$tbody.find('.' + this.classNames.READ_ONLY).each(function () {
615 var $row = $(this);
616 var view = $row.data(instance.dataKeys.ROW_VIEW);
617
618 if (view) {
619 views.push(view);
620 }
621 });
622
623 return views;
624 },
625
626 /**
627 * Appends entry to end or specified index of table
628 *
629 * @param {EntryModel} model
630 * @param index
631 *
632 * @return {jQuery}
633 */
634 _renderRow: function (model, index) {
635 var instance = this;
636 var $rows = this.$tbody.find('.' + this.classNames.READ_ONLY);
637 var $row;
638 var view;
639
640 view = new this._rowClass({
641 model: model,
642 columns: this.options.columns,
643 allowEdit: this.options.allowEdit,
644 allowDelete: this.options.allowDelete,
645 allowReorder: this.options.allowReorder,
646 deleteConfirmationCallback: this.options.deleteConfirmationCallback
647 });
648
649 this.removeNoEntriesMsg();
650
651 view.on(this._event.ROW_EDIT, function (field) {
652 triggerEvtForInst(this._event.EDIT_ROW, {}, [this, instance]);
653 instance.edit(this, field);
654 });
655
656 $row = view.render().$el;
657
658 if (index !== -1) {
659
660 if (typeof index === 'number' && $rows.length !== 0) {
661 $row.insertBefore($rows[index]);
662 } else {
663 this.$tbody.append($row);
664 }
665 }
666
667 $row.data(this.dataKeys.ROW_VIEW, view);
668
669 // deactivate all rows - used in the cases, such as opening a dropdown where you do not want the table editable
670 // or any interactions
671 view.on(this._event.MODAL, function () {
672 instance.$table.removeClass(instance.classNames.ALLOW_HOVER);
673 instance.$tbody.sortable('disable');
674 instance.getRows().forEach(function (row) {
675 if (!instance.isRowBeingEdited(row)) {
676 row.delegateEvents({}); // clear all events
677 }
678 });
679 });
680
681 // activate all rows - used in the cases, such as opening a dropdown where you do not want the table editable
682 // or any interactions
683 view.on(this._event.MODELESS, function () {
684 instance.$table.addClass(instance.classNames.ALLOW_HOVER);
685 instance.$tbody.sortable('enable');
686 instance.getRows().forEach(function (row) {
687 if (!instance.isRowBeingEdited(row)) {
688 row.delegateEvents(); // rebind all events
689 }
690 });
691 });
692
693 // ensure that when this row is focused no other are
694 this._applyFocusCoordinator(view);
695
696 this.trigger(this._event.ROW_INITIALIZED, view);
697
698 return view;
699 },
700
701 /**
702 * Returns if the row is edit mode or note.
703 *
704 * @param {Row} row Read-only row to check if being edited.
705 *
706 * @return {Boolean}
707 */
708 isRowBeingEdited: function (row) {
709
710 var isBeingEdited = false;
711
712 this.editRows.some(function (editRow) {
713 if (editRow.el === row.el) {
714 isBeingEdited = true;
715 return true;
716 }
717 });
718
719 return isBeingEdited;
720 },
721
722 /**
723 * Ensures that when supplied view is focused no others are
724 *
725 * @param {Backbone.View} view
726 * @return {RestfulTable}
727 */
728 _applyFocusCoordinator: function (view) {
729 var instance = this;
730
731 if (!view.hasFocusBound) {
732 view.hasFocusBound = true;
733
734 view.on(this._event.FOCUS, function () {
735 if (instance.focusedRow && instance.focusedRow !== view) {
736 instance.focusedRow.trigger(instance._event.BLUR);
737 }
738 instance.focusedRow = view;
739 if (view instanceof Row && instance._createRow) {
740 instance._createRow.enable();
741 }
742 });
743 }
744
745 return this;
746 },
747
748 /**
749 * Remove specified row from collection holding rows being concurrently edited
750 *
751 * @param {EditRow} editView
752 *
753 * @return {RestfulTable}
754 */
755 _removeEditRow: function (editView) {
756 var index = $.inArray(editView, this.editRows);
757 this.editRows.splice(index, 1);
758 return this;
759 },
760
761 /**
762 * Focuses last row still being edited or create row (if it exists)
763 *
764 * @return {RestfulTable}
765 */
766 _shiftFocusAfterEdit: function () {
767
768 if (this.editRows.length > 0) {
769 this.editRows[this.editRows.length - 1].trigger(this._event.FOCUS);
770 } else if (this._createRow) {
771 this._createRow.trigger(this._event.FOCUS);
772 }
773
774 return this;
775 },
776
777 /**
778 * Evaluate if we save row when we blur. We can only do this when there is one row being edited at a time, otherwise
779 * it causes an infinite loop JRADEV-5325
780 *
781 * @return {boolean}
782 */
783 _saveEditRowOnBlur: function () {
784 return this.editRows.length <= 1;
785 },
786
787 /**
788 * Dismisses rows being edited concurrently that have no changes
789 */
790 dismissEditRows: function () {
791 this.editRows.forEach(function (editRow) {
792 if (!editRow.hasUpdates()) {
793 editRow.trigger(this._event.FINISHED_EDITING);
794 }
795 }, this);
796 },
797
798 /**
799 * Converts readonly row to editable view
800 *
801 * @param {Backbone.View} row
802 * @param {String} field - field name to focus
803 * @return {Backbone.View} editRow
804 */
805 edit: function (row, field) {
806 var instance = this;
807 var editRow = new this.options.views.editRow({
808 el: row.el,
809 columns: this.options.columns,
810 isUpdateMode: true,
811 allowReorder: this.options.allowReorder,
812 fieldFocusSelector: this.options.fieldFocusSelector,
813 model: row.model,
814 cancelAccessKey: this.options.cancelAccessKey,
815 submitAccessKey: this.options.submitAccessKey
816 });
817 var values = row.model.toJSON();
818
819 values.update = true;
820 editRow.render({
821 errors: {},
822 update: true,
823 values: values
824 })
825 .on(instance._event.UPDATED, function (model, focusUpdated) {
826 instance._removeEditRow (this);
827 this.off();
828 row.render().delegateEvents(); // render and rebind events
829 row.trigger(instance._event.UPDATED); // trigger blur fade out
830 if (focusUpdated !== false) {
831 instance._shiftFocusAfterEdit();
832 }
833 })
834 .on(instance._event.VALIDATION_ERROR, function () {
835 this.trigger(instance._event.FOCUS);
836 })
837 .on(instance._event.FINISHED_EDITING, function () {
838 instance._removeEditRow(this);
839 row.render().delegateEvents();
840 this.off(); // avoid any other updating, blurring, finished editing, cancel events being fired
841 })
842 .on(instance._event.CANCEL, function () {
843 instance._removeEditRow(this);
844 this.off(); // avoid any other updating, blurring, finished editing, cancel events being fired
845 row.render().delegateEvents(); // render and re` events
846 instance._shiftFocusAfterEdit();
847 })
848 .on(instance._event.BLUR, function () {
849 instance.dismissEditRows(); // dismiss edit rows that have no changes
850 if (instance._saveEditRowOnBlur()) {
851 this.trigger(instance._event.SAVE, false); // save row, which if successful will call the updated event above
852 }
853 });
854
855 // Ensure that if focus is pulled to another row, we blur the edit row
856 this._applyFocusCoordinator(editRow);
857
858 // focus edit row, which has the flow on effect of blurring current focused row
859 editRow.trigger(instance._event.FOCUS, field);
860
861 // disables form fields
862 if (instance._createRow) {
863 instance._createRow.disable();
864 }
865
866 this.editRows.push(editRow);
867
868 return editRow;
869 },
870
871
872 /**
873 * Renders all specified rows
874 *
875 * @param rows {Array<Backbone.Model>} array of objects describing Backbone.Model's to render
876 * @return {RestfulTable}
877 */
878 renderRows: function (rows = []) {
879 var comparator = this._models.comparator;
880 var els = [];
881
882 this._models.comparator = undefined; // disable temporarily, assume rows are sorted
883
884 var models = rows.map((row) => {
885 var model = new this.options.model(row);
886 els.push(this._renderRow(model, -1).el);
887 return model;
888 });
889
890 this._models.add(models, {silent: true});
891 this._models.comparator = comparator;
892 this.removeNoEntriesMsg();
893 this.$tbody.append(els);
894
895 return this;
896 },
897
898 /**
899 * Gets default options
900 *
901 * @param {Object} options
902 */
903 _getDefaultOptions: function (options) {
904 return {
905 model: options.model || EntryModel,
906 allowEdit: true,
907 views: {
908 editRow: EditRow,
909 row: Row
910 },
911 Collection: Backbone.Collection.extend({
912 url: options.resources.self,
913 model: options.model || EntryModel
914 }),
915 allowReorder: false,
916 fieldFocusSelector: function (name) {
917 return ':input[name=' + name + '], #' + name;
918 },
919 loadingMsg: options.loadingMsg || I18n.getText('aui.words.loading')
920 };
921 }
922});
923
924RestfulTable.ClassNames = classNames;
925RestfulTable.CustomCreateView = CustomCreateView;
926RestfulTable.CustomEditView = CustomEditView;
927RestfulTable.CustomReadView = CustomReadView;
928RestfulTable.DataKeys = dataKeys;
929RestfulTable.EditRow = EditRow;
930RestfulTable.EntryModel = EntryModel;
931RestfulTable.Events = events;
932RestfulTable.Row = Row;
933
934globalize('RestfulTable', RestfulTable);
935
936export default RestfulTable;