PageRenderTime 40ms CodeModel.GetById 2ms app.highlight 31ms RepoModel.GetById 1ms app.codeStats 0ms

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

https://bitbucket.org/zephor/aui-archive
JavaScript | 937 lines | 590 code | 142 blank | 205 comment | 68 complexity | a8fc5ff7b315fc6fbbfc626aaef5690c 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.ROW_EDIT, function (field) {
590                AJS.triggerEvtForInst(this._events.EDIT_ROW, {}, [this, instance]);
591                instance.edit(this, field);
592            });
593
594            $row = view.render().$el;
595
596            if (index !== -1) {
597
598                if (typeof index === "number" && $rows.length !== 0) {
599                    $row.insertBefore($rows[index]);
600                } else {
601                    this.$tbody.append($row);
602                }
603            }
604
605            $row.data(this.dataKeys.ROW_VIEW, view);
606
607            // deactivate all rows - used in the cases, such as opening a dropdown where you do not want the table editable
608            // or any interactions
609            view.bind(this._events.MODAL, function () {
610                instance.$table.removeClass(instance.classNames.ALLOW_HOVER);
611                instance.$tbody.sortable("disable");
612                $.each(instance.getRows(), function () {
613                    if (!instance.isRowBeingEdited(this)) {
614                        this.delegateEvents({}); // clear all events
615                    }
616                });
617            });
618
619            view.bind(this._events.ANIMATION_STARTED, function () {
620                instance.$table.removeClass(instance.classNames.ALLOW_HOVER);
621            });
622
623            view.bind(this._events.ANIMATION_FINISHED, function () {
624                instance.$table.addClass(instance.classNames.ALLOW_HOVER);
625            });
626
627            // activate all rows - used in the cases, such as opening a dropdown where you do not want the table editable
628            // or any interactions
629            view.bind(this._events.MODELESS, function () {
630                instance.$table.addClass(instance.classNames.ALLOW_HOVER);
631                instance.$tbody.sortable("enable");
632                $.each(instance.getRows(), function () {
633                    if (!instance.isRowBeingEdited(this)) {
634                        this.delegateEvents(); // rebind all events
635                    }
636                });
637            });
638
639            // ensure that when this row is focused no other are
640            this._applyFocusCoordinator(view);
641
642            this.trigger(this._events.ROW_INITIALIZED, view);
643
644            return view;
645        },
646
647        /**
648         * Returns if the row is edit mode or note
649         *
650         * @param {AJS.RestfulTable.Row} - read onyl row to check if being edited
651         * @return {Boolean}
652         */
653        isRowBeingEdited: function (row) {
654
655            var isBeingEdited = false;
656
657            $.each(this.editRows, function () {
658                if (this.el === row.el) {
659                    isBeingEdited = true;
660                    return false;
661                }
662            });
663
664            return isBeingEdited;
665        },
666
667        /**
668         * Ensures that when supplied view is focused no others are
669         *
670         * @param {Backbone.View} view
671         * @return {AJS.RestfulTable}
672         */
673        _applyFocusCoordinator: function (view) {
674
675            var instance = this;
676
677            if (!view.hasFocusBound) {
678
679                view.hasFocusBound = true;
680
681                view.bind(this._events.FOCUS, function () {
682                    if (instance.focusedRow && instance.focusedRow !== view) {
683                        instance.focusedRow.trigger(instance._events.BLUR);
684                    }
685                    instance.focusedRow = view;
686                    if (view instanceof AJS.RestfulTable.Row && instance._createRow) {
687                        instance._createRow.enable();
688                    }
689                });
690            }
691
692            return this;
693        },
694
695        /**
696         * Remove specificed row from collection holding rows being concurrently edited
697         *
698         * @param {AJS.RestfulTable.EditRow} editView
699         * @return {AJS.RestfulTable}
700         */
701        _removeEditRow: function (editView) {
702            var index = $.inArray(editView, this.editRows);
703            this.editRows.splice(index, 1);
704            return this;
705        },
706
707        /**
708         * Focuses last row still being edited or create row (if it exists)
709         *
710         * @return {AJS.RestfulTable}
711         */
712        _shiftFocusAfterEdit: function () {
713
714            if (this.editRows.length > 0) {
715                this.editRows[this.editRows.length-1].trigger(this._events.FOCUS);
716            } else if (this._createRow) {
717                this._createRow.trigger(this._events.FOCUS);
718            }
719
720            return this;
721        },
722
723        /**
724         * Evaluate if we save row when we blur. We can only do this when there is one row being edited at a time, otherwise
725         * it causes an infinate loop JRADEV-5325
726         *
727         * @return {boolean}
728         */
729        _saveEditRowOnBlur: function () {
730            return this.editRows.length <= 1;
731        },
732
733        /**
734         * Dismisses rows being edited concurrently that have no changes
735         */
736        dismissEditRows: function () {
737            var instance = this;
738            $.each(this.editRows, function () {
739                if (!this.hasUpdates()) {
740                    this.trigger(instance._events.FINISHED_EDITING);
741                }
742            });
743        },
744
745        /**
746         * Converts readonly row to editable view
747         *
748         * @param {Backbone.View} row
749         * @param {String} field - field name to focus
750         * @return {Backbone.View} editRow
751         */
752        edit: function (row, field) {
753
754            var instance = this,
755                    editRow = new this.options.views.editRow({
756                        el: row.el,
757                        columns: this.options.columns,
758                        isUpdateMode: true,
759                        allowReorder: this.options.allowReorder,
760                        fieldFocusSelector: this.options.fieldFocusSelector,
761                        model: row.model,
762                        cancelAccessKey: this.options.cancelAccessKey,
763                        submitAccessKey: this.options.submitAccessKey
764                    }),
765                    values = row.model.toJSON();
766            values.update = true;
767            editRow.render({
768                errors: {},
769                update: true,
770                values: values
771            })
772                    .bind(instance._events.UPDATED, function (model, focusUpdated) {
773                        instance._removeEditRow (this);
774                        this.unbind();
775                        row.render().delegateEvents(); // render and rebind events
776                        row.trigger(instance._events.UPDATED); // trigger blur fade out
777                        if (focusUpdated !== false) {
778                            instance._shiftFocusAfterEdit();
779                        }
780                    })
781                    .bind(instance._events.VALIDATION_ERROR, function () {
782                        this.trigger(instance._events.FOCUS);
783                    })
784                    .bind(instance._events.FINISHED_EDITING, function () {
785                        instance._removeEditRow(this);
786                        row.render().delegateEvents();
787                        this.unbind();  // avoid any other updating, blurring, finished editing, cancel events being fired
788                    })
789                    .bind(instance._events.CANCEL, function () {
790                        instance._removeEditRow(this);
791                        this.unbind();  // avoid any other updating, blurring, finished editing, cancel events being fired
792                        row.render().delegateEvents(); // render and rebind events
793                        instance._shiftFocusAfterEdit();
794                    })
795                    .bind(instance._events.BLUR, function () {
796                        instance.dismissEditRows(); // dismiss edit rows that have no changes
797                        if (instance._saveEditRowOnBlur()) {
798                            this.trigger(instance._events.SAVE, false);  // save row, which if successful will call the updated event above
799                        }
800                    });
801
802            // Ensure that if focus is pulled to another row, we blur the edit row
803            this._applyFocusCoordinator(editRow);
804
805            // focus edit row, which has the flow on effect of blurring current focused row
806            editRow.trigger(instance._events.FOCUS, field);
807
808            // disables form fields
809            if (instance._createRow) {
810                instance._createRow.disable();
811            }
812
813            this.editRows.push(editRow);
814
815            return editRow;
816        },
817
818
819        /**
820         * Renders all specified rows
821         *
822         * @param {Array} array of objects describing Backbone.Model's to render
823         * @return {AJS.RestfulTable}
824         */
825        renderRows: function (rows) {
826            var comparator = this._models.comparator, els = [];
827
828            this._models.comparator = undefined; // disable temporarily, assume rows are sorted
829
830            var models = _.map(rows, function(row) {
831                var model = new this.options.model(row);
832                els.push(this._renderRow(model, -1).el);
833                return model;
834            }, this);
835            this._models.add(models, {silent:true});
836
837            this._models.comparator = comparator;
838
839            this.removeNoEntriesMsg();
840
841            this.$tbody.append(els);
842
843            return this;
844        },
845
846        /**
847         * Gets default options
848         *
849         * @param {Object} options
850         */
851        _getDefaultOptions: function (options) {
852            return {
853                model: options.model || AJS.RestfulTable.EntryModel,
854                allowEdit: true,
855                views: {
856                    editRow: AJS.RestfulTable.EditRow,
857                    row: AJS.RestfulTable.Row
858                },
859                Collection: Backbone.Collection.extend({
860                    url: options.resources.self,
861                    model: options.model || AJS.RestfulTable.EntryModel
862                }),
863                allowReorder: false,
864                fieldFocusSelector: function(name) {
865                    return ":input[name=" + name + "], #" + name;
866                },
867                loadingMsg: options.loadingMsg || AJS.I18n.getText("aui.words.loading")
868            }
869        }
870
871    });
872
873    // jQuery data keys (http://api.jquery.com/jQuery.data/)
874    AJS.RestfulTable.DataKeys = {
875        ENABLED_SUBMIT: "enabledSubmit",
876        ROW_VIEW: "RestfulTable_Row_View"
877    };
878
879    // CSS style classes. DON'T hard code
880    AJS.RestfulTable.ClassNames = {
881        NO_VALUE: "aui-restfultable-editable-no-value",
882        NO_ENTRIES: "aui-restfultable-no-entires",
883        RESTFUL_TABLE: "aui-restfultable",
884        ROW: "aui-restfultable-row",
885        READ_ONLY: "aui-restfultable-readonly",
886        ACTIVE: "aui-restfultable-active",
887        ALLOW_HOVER: "aui-restfultable-allowhover",
888        FOCUSED: "aui-restfultable-focused",
889        MOVEABLE: "aui-restfultable-movable",
890        ANIMATING: "aui-restfultable-animate",
891        DISABLED: "aui-restfultable-disabled",
892        SUBMIT: "aui-restfultable-submit",
893        CANCEL: "aui-restfultable-cancel",
894        EDIT_ROW: "aui-restfultable-editrow",
895        CREATE: "aui-restfultable-create",
896        DRAG_HANDLE: "aui-restfultable-draghandle",
897        ORDER: "aui-restfultable-order",
898        EDITABLE: "aui-restfultable-editable",
899        ERROR: "error",
900        DELETE: "aui-restfultable-delete",
901        LOADING: "loading"
902    };
903
904    // Custom events
905    AJS.RestfulTable.Events = {
906
907        // AJS events
908        REORDER_SUCCESS: "RestfulTable.reorderSuccess",
909        ROW_ADDED: "RestfulTable.rowAdded",
910        ROW_REMOVED: "RestfulTable.rowRemoved",
911        EDIT_ROW: "RestfulTable.switchedToEditMode",
912        SERVER_ERROR: "RestfulTable.serverError",
913
914        // backbone events
915        CREATED: "created",
916        UPDATED: "updated",
917        FOCUS: "focus",
918        BLUR: "blur",
919        SUBMIT: "submit",
920        SAVE: "save",
921        MODAL: "modal",
922        MODELESS: "modeless",
923        CANCEL: "cancel",
924        CONTENT_REFRESHED: "contentRefreshed",
925        RENDER: "render",
926        FINISHED_EDITING: "finishedEditing",
927        VALIDATION_ERROR: "validationError",
928        SUBMIT_STARTED: "submitStarted",
929        SUBMIT_FINISHED: "submitFinished",
930        ANIMATION_STARTED: "animationStarted",
931        ANIMATION_FINISHED: "animationFinisehd",
932        INITIALIZED: "initialized",
933        ROW_INITIALIZED: "rowInitialized",
934        ROW_EDIT: "editRow"
935    };
936
937})(AJS.$);