PageRenderTime 8ms CodeModel.GetById 2ms app.highlight 37ms RepoModel.GetById 1ms app.codeStats 0ms

/auiplugin/src/main/resources/experimental/js/atlassian/restfultable/restfultable.js

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