PageRenderTime 46ms CodeModel.GetById 3ms app.highlight 36ms RepoModel.GetById 1ms app.codeStats 0ms

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

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