css/edit.css | 8 ++++ edit.module | 38 ++-------------- js/app.js | 111 ++++++++++++++++++++++++++++++++++----------- js/backbone.drupalform.js | 11 ++++- js/edit.js | 63 ++++++++----------------- js/routers/edit-router.js | 4 +- js/theme.js | 25 ++++++++++ js/viejs/EditService.js | 8 +++- js/views/menu-view.js | 62 +++++++++++++++++++------ js/views/modal-view.js | 5 +- js/views/overlay-view.js | 43 ++++++++++-------- js/views/toolbar-view.js | 62 +++++++++++++++---------- 12 files changed, 271 insertions(+), 169 deletions(-) diff --git a/css/edit.css b/css/edit.css index 1c88ebb..b95bde8 100644 --- a/css/edit.css +++ b/css/edit.css @@ -61,6 +61,13 @@ transition: opacity .2s ease; } +.edit-animate-only-background-and-padding { + -webkit-transition: background, padding .2s ease; + -moz-transition: background, padding .2s ease; + -ms-transition: background, padding .2s ease; + -o-transition: background, padding .2s ease; + transition: background, padding .2s ease; +} @@ -230,6 +237,7 @@ .edit-form form { padding: 1em; } .edit-form .form-item { margin: 0; } .edit-form .form-wrapper { margin: .5em; } +.edit-form .form-wrapper .form-wrapper { margin: inherit; } .edit-form .form-actions { display: none; } .edit-form input { max-width: 100%; } diff --git a/edit.module b/edit.module index 94e7b24..0d638bd 100644 --- a/edit.module +++ b/edit.module @@ -76,39 +76,9 @@ function edit_toolbar() { ), ), 'tray' => array( - '#heading' => t('In-place editing operations'), - 'view_edit_toggle' => array( - '#theme' => 'links__toolbar_edit', - '#attributes' => array( - 'id' => 'edit_view-edit-toggles', - 'class' => 'menu', - ), - '#links' => array( - 'view' => array( - 'title' => t('View'), - 'href' => request_path(), - 'fragment' => 'view', - 'attributes' => array( - 'title' => t('Exit quick edit mode.'), - 'role' => 'button', - 'class' => array('edit_view-edit-toggle', 'edit-view'), - ), - ), - 'edit' => array( - 'title' => t('Quick edit'), - 'href' => request_path(), - 'fragment' => 'quick-edit', - 'attributes' => array( - 'title' => t('Enter quick edit mode.'), - 'role' => 'button', - 'class' => array('edit_view-edit-toggle', 'edit-edit'), - ), - ), - ), - '#attached' => array( - 'library' => array( - array('edit', 'edit'), - ), + '#attached' => array( + 'library' => array( + array('edit', 'edit'), ), ), ), @@ -222,7 +192,7 @@ function edit_preprocess_field(&$variables) { // Mark this field as editable and provide metadata through data- attributes. $variables['attributes']['data-edit-field-label'] = $instance->definition['label']; $variables['attributes']['data-edit-id'] = $entity->entityType() . ':' . $entity->id() . ':' . $field_name . ':' . $langcode . ':' . $view_mode; - $variables['attributes']['aria-label'] = t('Edit entity @type @id, field @field', array('@type' => $entity->entityType(), '@id' => $entity->id(), '@field' => $instance->definition['label'])); + $variables['attributes']['aria-label'] = t('Entity @type @id, field @field', array('@type' => $entity->entityType(), '@id' => $entity->id(), '@field' => $instance->definition['label'])); $variables['attributes']['class'][] = 'edit-field'; $variables['attributes']['class'][] = 'edit-allowed'; $variables['attributes']['class'][] = 'edit-type-' . $editor; diff --git a/js/app.js b/js/app.js index 8745236..5160d84 100644 --- a/js/app.js +++ b/js/app.js @@ -17,7 +17,7 @@ singleEditorStates: [], // State. - $entityElements: [], + $entityElements: null, /** * Implements Backbone Views' initialize() function. @@ -39,35 +39,21 @@ this.activeEditorStates = ['activating', 'active']; this.singleEditorStates = _.union(['highlighted'], this.activeEditorStates); + this.$entityElements = $([]); + // Use Create's Storage widget. this.$el.createStorage({ vie: this.vie, editableNs: 'createeditable' }); - // Instantiate an EditableEntity widget for each property. - var that = this; - this.$entityElements = this.domService.findSubjectElements().each(function() { - $(this).createEditable({ - vie: that.vie, - disabled: true, - state: 'inactive', - acceptStateChange: that.acceptEditorStateChange, - statechange: function(event, data) { - that.editorStateChange(data.previous, data.current, data.propertyEditor); - }, - decoratePropertyEditor: function(data) { - that.decorateEditor(data.propertyEditor); - } - }); - }); - - // Instantiate OverlayView + // Instantiate OverlayView. var overlayView = new Drupal.edit.views.OverlayView({ + el: (Drupal.theme('editOverlay', {})), model: this.model }); - // Instantiate MenuView + // Instantiate MenuView. var editMenuView = new Drupal.edit.views.MenuView({ el: this.el, model: this.model @@ -78,6 +64,56 @@ }, /** + * Finds editable properties within a given context. + * + * Finds editable properties, registers them with the app, updates their + * state to match the current app state. + * + * @param $context + * A jQuery-wrapped context DOM element within which will be searched. + */ + findEditableProperties: function($context) { + var that = this; + var newState = (this.model.get('isViewing')) ? 'inactive' : 'candidate'; + + this.domService.findSubjectElements($context).each(function() { + var $element = $(this); + + // Ignore editable properties for which we've already set up Create.js. + if (that.$entityElements.index($element) !== -1) { + return; + } + + $element + // Instantiate an EditableEntity widget. + .createEditable({ + vie: that.vie, + disabled: true, + state: 'inactive', + acceptStateChange: that.acceptEditorStateChange, + statechange: function(event, data) { + that.editorStateChange(data.previous, data.current, data.propertyEditor); + }, + decoratePropertyEditor: function(data) { + that.decorateEditor(data.propertyEditor); + } + }) + // This event is triggered just before Edit removes an EditableEntity + // widget, so that we can do proper clean-up. + .on('destroyedPropertyEditor.edit', function(event, editor) { + that.undecorateEditor(editor); + that.$entityElements = that.$entityElements.not($(this)); + + }) + // Transition the new PropertyEditor into the current state. + .createEditable('setState', newState); + + // Add this new EditableEntity widget element to the list. + that.$entityElements = that.$entityElements.add($element); + }); + }, + + /** * Sets the state of PropertyEditor widgets when edit mode begins or ends. * * Should be called whenever EditAppModel's "isViewing" changes. @@ -96,7 +132,7 @@ this._manageDocumentFocus(); Drupal.edit.setMessage(Drupal.t('In place edit mode is active'), Drupal.t('Page navigation is limited to editable items.'), Drupal.t('Press escape to exit')); } - else { + else if (newState === 'inactive') { this._releaseDocumentFocusManagement(); Drupal.edit.setMessage(Drupal.t('Edit mode is inactive.'), Drupal.t('Resume normal page navigation')); } @@ -331,6 +367,28 @@ editor.toolbarView.stateChange(data.previous, data.current); }); }, + + /** + * Undecorates an editor (PropertyEditor). + * + * Whenever a property has been updated, the old HTML will be replaced by + * the new (re-rendered) HTML. The EditableEntity widget will be destroyed, + * as will be the PropertyEditor widget. This method ensures Edit's editor + * views also are removed properly. + * + * @param editor + * The PropertyEditor widget object. + */ + undecorateEditor: function(editor) { + editor.toolbarView.undelegateEvents(); + editor.toolbarView.remove(); + delete editor.toolbarView; + editor.decorationView.undelegateEvents(); + // Don't call .remove() on the decoration view, because that would remove + // a potentially rerendered field. + delete editor.decorationView; + }, + /** * Makes elements other than the editables unreachable via the tab key. * @@ -357,7 +415,7 @@ 'tabindex': 0, 'role': 'button' }); - // Store the first editable in the set. + // Instantiate a variable to hold the editable element in the set. var $currentEditable; // We're using simple function scope to manage 'this' for the internal // handler, so save this as that. @@ -395,7 +453,7 @@ if (event.keyCode === 9) { var context = ''; // Include the view mode toggle with the editables selector. - var selector = editablesSelector + ', .edit_view-edit-toggle.edit-view'; + var selector = editablesSelector + ', #toolbar-tab-edit'; activeEditor = that.model.get('activeEditor'); var $confirmDialog = $('#edit_modal'); // If the edit modal is active, that is the tabbing context. @@ -411,7 +469,7 @@ else if (activeEditor) { context = $(activeEditor.$formContainer).add(activeEditor.toolbarView.$el); // Include the view mode toggle with the editables selector. - selector = inputsSelector + ', .edit_view-edit-toggle.edit-view'; + selector = inputsSelector; if (!$currentEditable || $currentEditable.is(editablesSelector)) { $currentEditable = $(selector, context).eq(-1); } @@ -423,7 +481,6 @@ } var count = $editables.length - 1; var index = $editables.index($currentEditable); - console.log(index + " of " + count); // Navigate backwards. if (event.shiftKey) { // Beginning of the set, loop to the end. @@ -456,13 +513,15 @@ event.stopPropagation(); } }); + // Set focus on the edit button initially. + $('#toolbar-tab-edit').focus(); }, /** * Removes key management and edit accessibility features from the DOM. */ _releaseDocumentFocusManagement: function () { $(document).off('keydown.edit'); - $('.edit-candidate.edit-editable').removeAttr('tabindex role'); + $('.edit-allowed.edit-field').removeAttr('tabindex role'); } }); diff --git a/js/backbone.drupalform.js b/js/backbone.drupalform.js index cc0e5c3..ba79e76 100644 --- a/js/backbone.drupalform.js +++ b/js/backbone.drupalform.js @@ -45,7 +45,7 @@ Backbone.syncDrupalFormWidget = function(method, model, options) { // @todo: POSTPONED_ON(Drupal core, http://drupal.org/node/1784216) // Once full JSON-LD support in Drupal core lands, we can ensure that the // models that VIE maintains are properly updated. - changedAttributes[predicate] = 'JSON-LD representation N/A yet.'; + changedAttributes[predicate] = undefined; changedAttributes[predicate + '/rendered'] = response.data; options.success(changedAttributes); }; @@ -126,7 +126,14 @@ Backbone.syncDirect = function(method, model, options) { Drupal.edit.util.form.unajaxifySaving(jQuery(ajax.element)); jQuery('#edit_backstage form').remove(); - options.success(); + // Call Backbone.sync's success callback with the rerendered field. + var changedAttributes = {}; + // @todo: POSTPONED_ON(Drupal core, http://drupal.org/node/1784216) + // Once full JSON-LD support in Drupal core lands, we can ensure that the + // models that VIE maintains are properly updated. + changedAttributes[predicate] = jQuery(response.data).find('.field-item').html(); + changedAttributes[predicate + '/rendered'] = response.data; + options.success(changedAttributes); }; // Unsuccessfully saved; validation errors. diff --git a/js/edit.js b/js/edit.js index e208c34..d900977 100644 --- a/js/edit.js +++ b/js/edit.js @@ -18,39 +18,33 @@ var $messages; Drupal.edit = Drupal.edit || {}; -Drupal.behaviors.editDiscoverEditables = { - attach: function(context) { - // @todo BLOCKED_ON(VIE.js, how to let VIE know that some content was - // removed and how to scan new content for VIE entities, to make them - // editable?) - // - // Also see ToolbarView.save(). - // We need to separate the discovery of editables if we want updated - // or new content (added by code other than Edit) to be detected - // automatically. Once we implement this, we'll be able to get rid of all - // calls to Drupal.edit.domService.findSubjectElements() :) - } -}; - /** * Attach toggling behavior and in-place editing. */ Drupal.behaviors.edit = { attach: function(context) { - $('#edit_view-edit-toggles').once('edit-init', Drupal.edit.init); + var $context = $(context); + + // Initialize the Edit app. + $context.find('#toolbar-tab-edit').once('edit-init', Drupal.edit.init); - // As soon as there is at least one editable field, show the Edit tab in the - // toolbar. - if ($(context).find('.edit-field.edit-allowed').length) { + // As soon as there is at least one editable property, show the Edit tab in + // the toolbar. + if ($context.find('.edit-field.edit-allowed').length) { $('.toolbar .icon-edit.edit-nothing-editable-hidden').removeClass('edit-nothing-editable-hidden'); } + + // Find editable properties, make them editable. + if (Drupal.edit.app) { + Drupal.edit.app.findEditableProperties($context); + } } }; Drupal.edit.init = function() { // Append a messages element for appending interaction updates for screen // readers. - $messages = $(Drupal.theme('editMessageBox')).appendTo(this); + $messages = $(Drupal.theme('editMessageBox')).appendTo($(this).parent()); // Instantiate EditAppView, which is the controller of it all. EditAppModel // instance tracks global state (viewing/editing in-place). var appModel = new Drupal.edit.models.EditAppModel(); @@ -66,6 +60,11 @@ Drupal.edit.init = function() { // Start Backbone's history/route handling. Backbone.history.start(); + + // For now, we work with a singleton app, because for Drupal.behaviors to be + // able to discover new editable properties that get AJAXed in, it must know + // with which app instance they should be associated. + Drupal.edit.app = app; }; /** @@ -76,34 +75,10 @@ Drupal.edit.init = function() { * @param {String} message * A string to be inserted into the message area. */ -Drupal.edit.setMessage = function (message) { +Drupal.edit.setMessage = function(message) { var args = Array.prototype.slice.call(arguments); args.unshift('editMessage'); $messages.html(Drupal.theme.apply(this, args)); -} - -/** - * A region to post messages that a screen reading UA will announce. - * - * @return {String} - * A string representing a DOM fragment. - */ -Drupal.theme.editMessageBox = function () { - return '
'; }; -/** - * Wrap message strings in p tags. - * - * @return {String} - * A string representing a DOM fragment. - */ -Drupal.theme.editMessage = function () { - var messages = Array.prototype.slice.call(arguments); - var output = ''; - for (var i = 0; i < messages.length; i++) { - output += '

' + messages[i] + '

'; - } - return output; -}; })(jQuery, Backbone, Drupal); diff --git a/js/routers/edit-router.js b/js/routers/edit-router.js index 4d7b196..56b76d0 100644 --- a/js/routers/edit-router.js +++ b/js/routers/edit-router.js @@ -13,7 +13,7 @@ Drupal.edit.routers.EditRouter = Backbone.Router.extend({ appModel: null, routes: { - "quick-edit": "edit", + "edit": "edit", "view": "view", "": "view" }, @@ -40,7 +40,7 @@ Drupal.edit.routers.EditRouter = Backbone.Router.extend({ that.appModel.set('isViewing', true); } else { - that.navigate('#quick-edit'); + that.navigate('#edit'); } }); } diff --git a/js/theme.js b/js/theme.js index 9a72724..80dcbef 100644 --- a/js/theme.js +++ b/js/theme.js @@ -147,4 +147,29 @@ Drupal.theme.editFormContainer = function(settings) { return html; }; +/** + * A region to post messages that a screen reading UA will announce. + * + * @return {String} + * A string representing a DOM fragment. + */ +Drupal.theme.editMessageBox = function() { + return '
'; +}; + +/** + * Wrap message strings in p tags. + * + * @return {String} + * A string representing a DOM fragment. + */ +Drupal.theme.editMessage = function() { + var messages = Array.prototype.slice.call(arguments); + var output = ''; + for (var i = 0; i < messages.length; i++) { + output += '

' + messages[i] + '

'; + } + return output; +}; + })(jQuery, Drupal); diff --git a/js/viejs/EditService.js b/js/viejs/EditService.js index ff9f0e8..f52a6c0 100644 --- a/js/viejs/EditService.js +++ b/js/viejs/EditService.js @@ -245,13 +245,19 @@ } entityPredicates[predicate] = value; + entityPredicates[predicate + '/rendered'] = predicateElement[0].outerHTML; }); return entityPredicates; }, readElementValue : function(predicate, element) { // Unlike in RdfaService there is parsing needed here. - return jQuery.trim(element.html()); + if (element.hasClass('edit-type-form')) { + return undefined; + } + else { + return jQuery.trim(element.html()); + } }, // Subject elements are the DOM elements containing a single or multiple diff --git a/js/views/menu-view.js b/js/views/menu-view.js index 834f743..2bcdab9 100644 --- a/js/views/menu-view.js +++ b/js/views/menu-view.js @@ -10,15 +10,32 @@ Drupal.edit = Drupal.edit || {}; Drupal.edit.views = Drupal.edit.views || {}; Drupal.edit.views.MenuView = Backbone.View.extend({ + events: { + 'click #toolbar-tab-edit': 'editClickHandler' + }, + /** * Implements Backbone Views' initialize() function. */ initialize: function() { _.bindAll(this, 'stateChange'); this.model.on('change:isViewing', this.stateChange); - - // We have to call stateChange() here, because URL fragments are not passed - // the server, thus the wrong anchor may be marked as active. + // @todo + // Re-implement hook_toolbar and the corresponding JavaScript behaviors + // once https://drupal.org/node/1847198 is resolved. The toolbar tray is + // necessary when the page request is processed because its render element + // has an #attached property with the Edit module library code assigned to + // it. Currently a toolbar tab is not passed as a renderable array, so + // #attached properties are not processed. The toolbar tray DOM element is + // unnecessary right now, so it is removed. + this.$el.find('#toolbar-tray-edit').remove(); + // Respond to clicks on other toolbar tabs. This temporary pending + // improvements to the toolbar module. + $('#toolbar-administration').on('click.edit', '.bar a:not(#toolbar-tab-edit)', _.bind(function (event) { + this.model.set('isViewing', true); + }, this)); + // We have to call stateChange() here because URL fragments are not passed + // to the server, thus the wrong anchor may be marked as active. this.stateChange(); }, @@ -26,16 +43,35 @@ Drupal.edit.views.MenuView = Backbone.View.extend({ * Listens to app state changes. */ stateChange: function() { - // Unmark whichever one is currently marked as active. - this.$('a.edit_view-edit-toggle') - .removeClass('active') - .parent().removeClass('active'); - - // Mark the correct one as active. - var activeAnchor = this.model.get('isViewing') ? 'view' : 'edit'; - this.$('a.edit_view-edit-toggle.edit-' + activeAnchor) - .addClass('active') - .parent().addClass('active'); + var isViewing = this.model.get('isViewing'); + // Toggle the state of the Toolbar Edit tab based on the isViewing state. + this.$el.find('#toolbar-tab-edit') + .toggleClass('active', !isViewing) + .attr('aria-pressed', !isViewing); + // Manage the toolbar state until + // https://drupal.org/node/1847198 is resolved + if (!isViewing) { + // Remove the 'toolbar-tray-open' class from the body element. + this.$el.removeClass('toolbar-tray-open'); + // Deactivate any other active tabs and trays. + this.$el + .find('.bar a', '#toolbar-administration') + .not('#toolbar-tab-edit') + .add('.tray', '#toolbar-administration') + .removeClass('active'); + } + }, + /** + * Handles clicks on the edit tab of the toolbar. + * + * @param {Object} event + */ + editClickHandler: function (event) { + var isViewing = this.model.get('isViewing'); + // Toggle the href of the Toolbar Edit tab based on the isViewing state. The + // href value should represent to state to be entered. + this.$el.find('#toolbar-tab-edit').attr('href', (isViewing) ? '#edit' : '#view'); + this.model.set('isViewing', !isViewing); } }); diff --git a/js/views/modal-view.js b/js/views/modal-view.js index 3989c7e..2e3b49c 100644 --- a/js/views/modal-view.js +++ b/js/views/modal-view.js @@ -48,10 +48,7 @@ Drupal.edit.views.ModalView = Backbone.View.extend({ this.$elementsToHide = $([]) .add((editor.element.hasClass('edit-belowoverlay')) ? null : editor.element) .add(editor.toolbarView.$el) - .add((editor.options.editorName === 'form') - ? editor.$formContainer - : editor.element.next('.edit-validation-errors') - ); + .add((editor.options.editorName === 'form') ? editor.$formContainer : editor.element.next('.edit-validation-errors')); this.$elementsToHide.addClass('edit-belowoverlay'); // Step 2: the modal. When the user makes a choice, the UI elements that diff --git a/js/views/overlay-view.js b/js/views/overlay-view.js index e357014..87bf81c 100644 --- a/js/views/overlay-view.js +++ b/js/views/overlay-view.js @@ -20,15 +20,19 @@ Drupal.edit.views.OverlayView = Backbone.View.extend({ /** * Implements Backbone Views' initialize() function. */ - initialize: function(options) { + initialize: function (options) { _.bindAll(this, 'stateChange'); this.model.on('change:isViewing', this.stateChange); + // Add the overlay to the page. + this.$el + .addClass('edit-animate-slow edit-animate-invisible') + .appendTo('body'); }, /** * Listens to app state changes. */ - stateChange: function() { + stateChange: function () { if (this.model.get('isViewing')) { this.remove(); return; @@ -40,9 +44,9 @@ Drupal.edit.views.OverlayView = Backbone.View.extend({ * Equates clicks anywhere on the overlay to clicking the active editor's (if * any) "close" button. * - * @param event + * @param {Object} event */ - onClick: function(event) { + onClick: function (event) { event.preventDefault(); var activeEditor = this.model.get('activeEditor'); if (activeEditor) { @@ -50,32 +54,31 @@ Drupal.edit.views.OverlayView = Backbone.View.extend({ var predicate = activeEditor.options.property; editableEntity.setState('candidate', predicate, { reason: 'overlay' }); } + else { + this.model.set('isViewing', true); + } }, /** - * Inserts the overlay element and appends it to the body. + * Reveal the overlay element. */ - render: function() { - this.setElement( - $(Drupal.theme('editOverlay', {})) - .appendTo('body') - .addClass('edit-animate-slow edit-animate-invisible') - ); - // Animations - this.$el.css('top', $('#navbar').outerHeight()); - this.$el.removeClass('edit-animate-invisible'); + render: function () { + this.$el + .show() + .css('top', $('#navbar').outerHeight()) + .removeClass('edit-animate-invisible'); }, /** - * Remove the overlay element. + * Hide the overlay element. */ - remove: function() { + remove: function () { var that = this; this.$el - .addClass('edit-animate-invisible') - .on(Drupal.edit.util.constants.transitionEnd, function (event) { - that.$el.remove(); - }); + .addClass('edit-animate-invisible') + .on(Drupal.edit.util.constants.transitionEnd, function (event) { + that.$el.hide(); + }); } }); diff --git a/js/views/toolbar-view.js b/js/views/toolbar-view.js index 42acb2b..899b9e3 100644 --- a/js/views/toolbar-view.js +++ b/js/views/toolbar-view.js @@ -20,6 +20,9 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({ predicate : null, editorName: null, + _loader: null, + _loaderVisibleStart: 0, + _id: null, events: { @@ -51,6 +54,9 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({ this.predicate = this.editor.options.property; this.editorName = this.editor.options.editorName; + this._loader = null; + this._loaderVisibleStart = 0; + // Generate a DOM-compatible ID for the toolbar DOM element. var propertyID = Drupal.edit.util.calcPropertyID(this.entity, this.predicate); this._id = 'edit-toolbar-for-' + propertyID.replace(/\//g, '_'); @@ -139,22 +145,22 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({ // attributes are now the "original" attributes. entity._originalAttributes = entity._previousAttributes = _.clone(entity.attributes); - // Replace the old content with the new content. - var updatedField = entity.get(predicate + '/rendered'); - var $inner = $(updatedField).html(); - editor.element.html($inner); - - // @todo BLOCKED_ON(VIE.js, how to let VIE know that some content was removed and how to scan new content for VIE entities, to make them editable?) - // Also see Drupal.behaviors.editDiscoverEditables. - // VIE doesn't seem to like this? :) It seems that if I delete/ - // overwrite an existing field, that VIE refuses to find the same - // predicate again for the same entity? - // self.$el.replaceWith(updatedField); - // debugger; - // console.log(self.$el, self.el, Drupal.edit.domService.findSubjectElements(self.$el)); - // Drupal.edit.domService.findSubjectElements(self.$el).each(Drupal.edit.prepareFieldView); + // Get data necessary to rerender property before it is unavailable. + var updatedProperty = entity.get(predicate + '/rendered'); + var $propertyWrapper = editor.element.closest('.edit-field'); + var $context = $propertyWrapper.parent(); editableEntity.setState('candidate', predicate); + // Unset the property, because it will be parsed again from the DOM, iff + // its new value causes it to still be rendered. + entity.unset(predicate, { silent: true }); + entity.unset(predicate + '/rendered', { silent: true }); + // Trigger event to allow for proper clean-up of editor-specific views. + editor.element.trigger('destroyedPropertyEditor.edit', editor); + + // Replace the old content with the new content. + $propertyWrapper.replaceWith(updatedProperty); + Drupal.attachBehaviors($context); }, // Save attempted but failed due to validation errors. @@ -229,22 +235,32 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({ }, /** - * Indicate in the 'info' toolgroup that we're waiting for a server reponse. + * Indicates in the 'info' toolgroup that we're waiting for a server reponse. + * + * Prevents flickering loading indicator by only showing it after 0.6 seconds + * and if it is shown, only hiding it after another 0.6 seconds. * * @param bool enabled * Whether the loading indicator should be displayed or not. */ setLoadingIndicator: function(enabled) { + var that = this; if (enabled) { - this.addClass('info', 'loading'); + this._loader = setTimeout(function() { + that.addClass('info', 'loading'); + that._loaderVisibleStart = new Date().getTime(); + }, 600); } else { - // Only stop showing the loading indicator after half a second to prevent - // it from flashing, which is bad UX. - var that = this; - setTimeout(function() { - that.removeClass('info', 'loading'); - }, 500); + var currentTime = new Date().getTime(); + clearTimeout(this._loader); + if (this._loaderVisibleStart) { + setTimeout(function() { + that.removeClass('info', 'loading'); + }, this._loaderVisibleStart + 600 - currentTime); + } + this._loader = null; + this._loaderVisibleStart = 0; } }, @@ -260,7 +276,7 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({ .find('.edit-toolbar') // Append the "info" toolgroup into the toolbar. .append(Drupal.theme('editToolgroup', { - classes: 'info', + classes: 'info edit-animate-only-background-and-padding', buttons: [ { label: label, classes: 'blank-button label' } ]