From 0346f8b5c961abfda26cc44631c52fc08c8723ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= Date: Thu, 9 May 2013 00:54:34 -0400 Subject: [PATCH] Issue #1678002-107 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit a61d2300f029625d6863ebd1b053660c7e510d72 Author: J. Renée Beach Date: Thu May 9 00:50:43 2013 -0400 Dealing with marking the fields changed. Signed-off-by: J. Renée Beach commit b155a45153e4ba6c919ed68fb18d9272d22fdbfe Author: J. Renée Beach Date: Thu May 9 00:22:25 2013 -0400 I think it's actually working. Signed-off-by: J. Renée Beach commit 340750c9d3125a0eb10d6e09d40bf089eda4a6a9 Author: J. Renée Beach Date: Thu May 9 00:02:06 2013 -0400 Almost there. Signed-off-by: J. Renée Beach commit 3ac4b0fd8d681c58d4109eacc397122975983142 Author: J. Renée Beach Date: Wed May 8 23:10:35 2013 -0400 Switching between fields works. Signed-off-by: J. Renée Beach commit 444467a8d6bb65cacf0b04aed64fdb5816550b53 Author: J. Renée Beach Date: Wed May 8 19:11:24 2013 -0400 Clicking between fields works. Signed-off-by: J. Renée Beach commit 39176d5a8e05beec39efcb00f3352403924929a3 Author: J. Renée Beach Date: Wed May 8 18:58:49 2013 -0400 Cleaner positioning. Signed-off-by: J. Renée Beach commit cc7c8ade7ab329ff239237eeb261b570734da852 Author: J. Renée Beach Date: Wed May 8 18:44:44 2013 -0400 Moving closer to a nice toolbar. Signed-off-by: J. Renée Beach commit 3d31f89837f599b5d28c8bfd1f0b41cf5c4da4b7 Author: J. Renée Beach Date: Wed May 8 16:03:31 2013 -0400 it's dirty pants but it does the job. The entity title is now displayed in the entity toolbar. Signed-off-by: J. Renée Beach commit 18865e9b71d30d615c8d3c5da429a5858c60206d Author: J. Renée Beach Date: Tue May 7 23:49:33 2013 -0400 Issue #1678002-98 commit 4b2902b7f45267b67ef8249edb07723312dcb737 Author: J. Renée Beach Date: Tue May 7 23:38:26 2013 -0400 Positioning of the toolbar is really smooth now. Signed-off-by: J. Renée Beach commit c8f368bca89dd47cd5ca1b6d48cf622d1776ef62 Author: J. Renée Beach Date: Tue May 7 15:56:49 2013 -0400 1678002-a Signed-off-by: J. Renée Beach Signed-off-by: J. Renée Beach commit 0460f2174e3c5b5db8ba330e946947cbcb24ff0f Author: J. Renée Beach Date: Tue May 7 23:47:08 2013 -0400 1901100-24 Signed-off-by: J. Renée Beach Signed-off-by: J. Renée Beach --- core/modules/edit/css/edit.css | 120 +++---- core/modules/edit/edit.module | 3 + core/modules/edit/js/edit.js | 3 +- core/modules/edit/js/editors/directEditor.js | 4 +- core/modules/edit/js/editors/formEditor.js | 10 +- core/modules/edit/js/models/EntityModel.js | 98 ++++++ core/modules/edit/js/theme.js | 29 +- core/modules/edit/js/views/AppView.js | 153 ++++++++- core/modules/edit/js/views/EditorDecorationView.js | 83 ++--- core/modules/edit/js/views/EditorView.js | 15 +- core/modules/edit/js/views/EntityToolbarView.js | 336 ++++++++++++++++++++ core/modules/edit/js/views/FieldToolbarView.js | 282 +++------------- .../edit/lib/Drupal/edit/EditController.php | 7 +- .../edit/lib/Drupal/edit/MetadataGenerator.php | 12 +- .../lib/Drupal/edit/MetadataGeneratorInterface.php | 4 +- .../editor/js/editor.formattedTextEditor.js | 4 +- 16 files changed, 753 insertions(+), 410 deletions(-) create mode 100644 core/modules/edit/js/views/EntityToolbarView.js diff --git a/core/modules/edit/css/edit.css b/core/modules/edit/css/edit.css index 6a5ac83..6302b5a 100644 --- a/core/modules/edit/css/edit.css +++ b/core/modules/edit/css/edit.css @@ -61,17 +61,6 @@ 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; -} - - - - /** * Candidate editables + editables being edited. * @@ -89,25 +78,29 @@ } .edit-field.edit-editable, .edit-field .edit-editable { - box-shadow: 0 0 1px 1px #4d9de9; + box-shadow: 0 0 1px 2px #4d9de9; } /* Highlighted (hovered) editable. */ .edit-editable.edit-highlighted { z-index: 305; - min-width: 200px; } -.edit-field.edit-editable.edit-highlighted, -.edit-form.edit-editable.edit-highlighted, -.edit-field .edit-editable.edit-highlighted { - box-shadow: 0 0 1px 1px #0199ff, 0 0 3px 3px rgba(153, 153, 153, .5); +.edit-field.edit-highlighted, +.edit-form.edit-highlighted, +.edit-field .edit-highlighted { + box-shadow: 0 0 1px 2px #0199ff, 0 0 3px 5px rgba(153, 153, 153, .5); } -.edit-field.edit-editable.edit-highlighted.edit-validation-error, -.edit-form.edit-editable.edit-highlighted.edit-validation-error, -.edit-field .edit-editable.edit-highlighted.edit-validation-error { - box-shadow: 0 0 1px 1px red, 0 0 3px 3px rgba(153, 153, 153, .5); +.edit-field.edit-changed, +.edit-form.edit-changed, +.edit-field .edit-changed { + box-shadow: 0 0 1px 2px orange, 0 0 3px 5px rgba(153, 153, 153, .5); } -.edit-form.edit-editable .form-item .error { +.edit-field.edit-validation-error, +.edit-form.edit-validation-error, +.edit-field .edit-validation-error { + box-shadow: 0 0 1px 2px red, 0 0 3px 5px rgba(153, 153, 153, .5); +} +.edit-form .form-item .error { border: 1px solid #eea0a0; } @@ -210,11 +203,25 @@ * Edit mode: toolbars */ -/* Trick: wrap statically positioned elements in relatively positioned element - without changing its location. This allows us to absolutely position the - toolbar. -*/ -.edit-toolbar-container, +/** + * Entity toolbar. + */ +.edit-toolbar-container { + border: 1px solid #a8a8a8; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; + background-color: white; + border: 1px solid #ababab; + position: absolute; + -webkit-transition: all 0.2s; + transition: all 0.2s; + width: 20em; + z-index: 350; +} .edit-form-container { position: relative; padding: 0; @@ -223,51 +230,24 @@ vertical-align: baseline; z-index: 310; } -.edit-toolbar-container { - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - -o-user-select: none; - user-select: none; +.edit-toolgroup.ops { + float: right; /* LTR */ } - -.edit-toolbar-heightfaker { - height: auto; - position: absolute; - bottom: 1px; - box-shadow: 0 0 1px 1px #0199ff, 0 0 3px 3px rgba(153, 153, 153, .5); - background: #fff; - display: none; +.edit-toolgroup.ops > * { + display: inline-block; } -.edit-highlighted .edit-toolbar-heightfaker { - display: block; +.edit-toolbar-label { + overflow: hidden; + padding: 0.333em 0.5em; } /* The toolbar; these are not necessarily visible. */ .edit-toolbar { - position: relative; - height: 100%; font-family: 'Droid sans', 'Lucida Grande', sans-serif; } -.edit-toolbar-heightfaker { - clip: rect(-1000px, 1000px, auto, -1000px); /* Remove bottom box-shadow. */ -} -/* Exception: when the toolbar is instructed to be "full width". */ -.edit-toolbar-fullwidth .edit-toolbar-heightfaker { - width: 100%; - clip: auto; -} - - -/* The toolbar contains toolgroups; these are visible. */ -.edit-toolgroup { - float: left; /* LTR */ -} /* Info toolgroup. */ .edit-toolgroup.info { - float: left; /* LTR */ font-weight: bolder; padding: 0 5px; background: #fff url('../images/throbber.gif') no-repeat -60px 60px; @@ -276,18 +256,14 @@ padding-right: 35px; background-position: 90% 50%; } - -/* Operations toolgroup. */ -.edit-toolgroup.ops { - float: right; /* LTR */ - margin-left: 5px; +.edit-toolbar-fullwidth { + width: 100%; } - .edit-toolgroup.wysiwyg-floated { float: right; } .edit-toolgroup.wysiwyg-main { - clear: left; + clear: both; width: 100%; padding-left: 0; } @@ -299,12 +275,10 @@ */ #edit_modal button, .edit-toolbar button { - float: left; /* LTR */ display: block; height: 29px; min-width: 29px; padding: 3px 6px 6px 6px; - margin: 4px 5px 1px 0; border: 1px solid #fff; border-radius: 3px; color: white; @@ -313,17 +287,15 @@ cursor: pointer; } #edit_modal button { - float: none; display: inline-block; } /* Button with icons. */ #edit_modal button span, .edit-toolbar button span { - width: 22px; - height: 19px; + width: 18px; + height: 16px; display: block; - float: left; } .edit-toolbar span.close { background: url('../images/close.png') no-repeat 3px 2px; diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module index 6c80f31..981dc7d 100644 --- a/core/modules/edit/edit.module +++ b/core/modules/edit/edit.module @@ -78,6 +78,7 @@ function edit_library_info() { // Views. $path . '/js/views/AppView.js' => $options, $path . '/js/views/EditorDecorationView.js' => $options, + $path . '/js/views/EntityToolbarView.js' => $options, $path . '/js/views/ContextualLinkView.js' => $options, $path . '/js/views/ModalView.js' => $options, $path . '/js/views/FieldToolbarView.js' => $options, @@ -104,8 +105,10 @@ function edit_library_info() { array('system', 'underscore'), array('system', 'backbone'), array('system', 'jquery.form'), + array('system', 'jquery.ui.position'), array('system', 'drupal.form'), array('system', 'drupal.ajax'), + array('system', 'drupal.debounce'), array('system', 'drupalSettings'), ), ); diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js index c68ebb3..17a99a7 100644 --- a/core/modules/edit/js/edit.js +++ b/core/modules/edit/js/edit.js @@ -303,7 +303,8 @@ function initializeEntityContextualLink (contextualLink) { else if (hasFieldWithPermission(editIDs)) { var entityModel = new Drupal.edit.EntityModel({ id: contextualLink.entityID, - el: contextualLink.region + el: contextualLink.region, + title: Drupal.edit.Metadata.get(contextualLink.entityID, 'title') }); Drupal.edit.collections.entities.add(entityModel); diff --git a/core/modules/edit/js/editors/directEditor.js b/core/modules/edit/js/editors/directEditor.js index 61fdf95..815f8c1 100644 --- a/core/modules/edit/js/editors/directEditor.js +++ b/core/modules/edit/js/editors/directEditor.js @@ -48,7 +48,7 @@ Drupal.edit.editors.direct = Drupal.edit.EditorView.extend({ /** * {@inheritdoc} */ - stateChange: function (model, state) { + stateChange: function (model, state, options) { var from = model.previous('state'); var to = state; switch (to) { @@ -81,7 +81,7 @@ Drupal.edit.editors.direct = Drupal.edit.EditorView.extend({ if (from === 'invalid') { this.removeValidationErrors(); } - this.save(); + this.save(options); break; case 'saved': break; diff --git a/core/modules/edit/js/editors/formEditor.js b/core/modules/edit/js/editors/formEditor.js index b356135..a4aba5c 100644 --- a/core/modules/edit/js/editors/formEditor.js +++ b/core/modules/edit/js/editors/formEditor.js @@ -16,7 +16,7 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({ /** * {@inheritdoc} */ - stateChange: function (model, state) { + stateChange: function (model, state, options) { var from = model.previous('state'); var to = state; switch (to) { @@ -40,7 +40,7 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({ case 'changed': break; case 'saving': - this.save(); + this.save(options); break; case 'saved': break; @@ -129,10 +129,11 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({ /** * {@inheritdoc} */ - save: function () { + save: function (options) { var $formContainer = this.model.attributes.editorView.$formContainer; var $submit = $formContainer.find('.edit-form-submit'); var base = $submit.attr('id'); + var callback = (options || {}).callback || function () {}; var that = this; // Successfully saved. @@ -144,6 +145,9 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({ // Then, set the 'html' attribute on the field model. This will cause the // field to be rerendered. that.model.set('html', response.data); + + // Invoke the optional callback. + callback.call(); }; // Unsuccessfully saved; validation errors. diff --git a/core/modules/edit/js/models/EntityModel.js b/core/modules/edit/js/models/EntityModel.js index 9edfea3..d1fb64c 100644 --- a/core/modules/edit/js/models/EntityModel.js +++ b/core/modules/edit/js/models/EntityModel.js @@ -21,12 +21,69 @@ $.extend(Drupal.edit, { // Indicates whether this instance of this entity is currently being // edited. isActive: false, + // The current processing state of an entity. + state: 'inactive', // A Drupal.edit.FieldCollection for all fields of this entity. fields: null }, + + /** + * + */ initialize: function () { + this.set('fields', new Drupal.edit.FieldCollection()); + + // Instantiate configuration for state handling. + // @see Drupal.edit.FieldModel.states + // @todo, these shouldn't be defined here. currently the position method + // is using them to find an active field. The FieldCollection should have a + // method that returns this. + this.activeEditorStates = ['activating', 'active', 'changed']; + this.singleEditorStates = _.union(['highlighted'], this.activeEditorStates); + + // Respond to field view changes. + this.on('viewChanged', this.viewChange, this); + + // The state of the entity is largely dependent on the state of its + // fields. + this.get('fields').on('change:state', this.fieldStateChange, this); + + // The entity keeps its own state progression. + this.on('change:state', this.stateChange, this); }, + + /** + * + */ + fieldStateChange: function (model, state, options) { + var from = model.previous('state'); + var to = state; + switch (to) { + case 'inactive': + break; + case 'candidate': + break; + case 'highlighted': + break; + case 'activating': + break; + case 'active': + break; + case 'changed': + break; + case 'saving': + break; + case 'saved': + break; + case 'invalid': + break; + default: + break; + } + this.set('state', to); + }, + destroy: function(options) { if (this.get('isActive')) { throw new Error("EntityModel cannot be destroyed while it is being edited."); @@ -37,6 +94,47 @@ $.extend(Drupal.edit, { this.get('fields').each(function (fieldModel) { fieldModel.destroy(); }); + }, + + /** + * Listens to FieldModel editor state changes. + * + * @param Drupal.edit.FieldModel model + * @param String state + * The state of an editable element. Used to determine display and behavior. + */ + stateChange: function (model, state, options) { + var from = model.previous('state'); + var to = state; + switch (to) { + case 'inactive': + break; + case 'candidate': + break; + case 'highlighted': + break; + case 'activating': + break; + case 'active': + break; + case 'changed': + break; + case 'saving': + break; + case 'saved': + break; + case 'invalid': + break; + default: + break; + } + }, + + /** + * + */ + viewChange: function (view) { + this.trigger('fieldViewChange', view); } }), diff --git a/core/modules/edit/js/theme.js b/core/modules/edit/js/theme.js index 7bef553..0ecb1b5 100644 --- a/core/modules/edit/js/theme.js +++ b/core/modules/edit/js/theme.js @@ -64,17 +64,31 @@ Drupal.theme.editModal = function(settings) { * @return * The corresponding HTML. */ -Drupal.theme.editToolbarContainer = function(settings) { +Drupal.theme.editEntityToolbar = function(settings) { var html = ''; - html += '
'; - html += '
'; - html += '
'; - html += '
'; + html += '
'; + html += '
'; + html += '
'; + html += '
'; + html += '
'; html += '
'; return html; }; /** + * Theme function for a toolbar container of the Edit module. + * + * @param settings + * An object with the following keys: + * - id: the id to apply to the toolbar container. + * @return + * The corresponding HTML. + */ +Drupal.theme.editFieldToolbar = function(settings) { + return '
'; +}; + +/** * Theme function for a toolbar toolgroup of the Edit module. * * @param settings @@ -86,9 +100,10 @@ Drupal.theme.editToolbarContainer = function(settings) { * The corresponding HTML. */ Drupal.theme.editToolgroup = function(settings) { - var classes = 'edit-toolgroup edit-animate-slow edit-animate-invisible edit-animate-delay-veryfast'; + var classes = (settings.classes || []); + classes.unshift('edit-toolgroup'); var html = ''; - html += '
0) { - event.stopPropagation(); - } - else { - callback(); - } } }); diff --git a/core/modules/edit/js/views/EditorView.js b/core/modules/edit/js/views/EditorView.js index 791549e..7b04beb 100644 --- a/core/modules/edit/js/views/EditorView.js +++ b/core/modules/edit/js/views/EditorView.js @@ -76,7 +76,7 @@ $.extend(Drupal.edit, { * @param String state * The state of the associated field. One of Drupal.edit.FieldModel.states. */ - stateChange: function (model, state) { + stateChange: function (model, state, options) { var from = model.previous('state'); var to = state; switch (to) { @@ -92,6 +92,11 @@ $.extend(Drupal.edit, { if (from === 'invalid') { this.removeValidationErrors(); } + + // Attempt to save if the field was previously in the changed state. + if (from === 'changed') { + this.model.set('state', 'saving'); + } break; case 'highlighted': // Nothing to do for the typical in-place editor: it should not be @@ -129,7 +134,7 @@ $.extend(Drupal.edit, { if (from === 'invalid') { this.removeValidationErrors(); } - this.save(); + this.save(options); break; case 'saved': // Nothing to do for the typical in-place editor. Immediately after @@ -159,8 +164,9 @@ $.extend(Drupal.edit, { /** * Saves the modified value in the in-place editor for this field. */ - save: function () { + save: function (options) { var model = this.model; + var callback = (options || {}).callback || function () {}; function fillAndSubmitForm (value) { var $form = jQuery('#edit_backstage form'); @@ -208,6 +214,9 @@ $.extend(Drupal.edit, { // Then, set the 'html' attribute on the field model. This will cause the // field to be rerendered. model.set('html', response.data); + + // Invoke the optional callback. + callback.call(); }; // Unsuccessfully saved; validation errors. diff --git a/core/modules/edit/js/views/EntityToolbarView.js b/core/modules/edit/js/views/EntityToolbarView.js new file mode 100644 index 0000000..d8e2c65 --- /dev/null +++ b/core/modules/edit/js/views/EntityToolbarView.js @@ -0,0 +1,336 @@ +/** + * @file + * A Backbone View that provides an entity level toolbar. + */ +(function ($, Backbone, Drupal, debounce) { + +"use strict"; + +Drupal.edit.EntityToolbarView = Backbone.View.extend({ + + _loader: null, + _loaderVisibleStart: 0, + _fieldToolbarRoot: null, + _fieldLabelRoot: null, + + events: function () { + var map = { + 'click.edit button.field-save': 'onClickSave', + 'click.edit button.field-close': 'onClickClose' + } + return map; + }, + + /** + * Implements Backbone.View.prototype.initialize(). + */ + initialize: function (options) { + var that = this; + + this.appModel = options.appModel; + + this.model.on('change:isActive', this.render, this); + this.model.on('change:state', this.stateChange, this); + this.model.on('fieldViewChange', this.fieldViewChangeHandler, this); + + this.appModel.on('change:highlightedEditor', this.render, this); + this.appModel.on('change:activeEditor', this.render, this); + + $(window).on('resize.edit scroll.edit', debounce($.proxy(this.windowChangeHandler, this), 150)); + + // Set the el into its own property. Eventually the el property will be + // replaced with the rendered toolbar. + this.$entity = this.$el; + + // Set the toolbar container to this view's el property. + this.buildToolbarEl(); + this._fieldToolbarRoot = this.$el.find('.edit-toolbar-field').get(0); + + this._loader = null; + this._loaderVisibleStart = 0; + + this.render(); + }, + + /** + * Implements Backbone.View.prototype.render(). + */ + render: function (model, changeValue) { + + if (this.model.get('isActive')) { + // If the toolbar container doesn't exist, create it. + if ($('body').children('#edit-entity-toolbar').length === 0) { + $('body').append(this.$el); + } + + this.label(); + + this.show('ops'); + // If render is being called and the toolbar is already visible, just + // reposition it. + this.position(); + } + else { + this.remove(); + } + + return this; + }, + + /** + * + */ + windowChangeHandler: function (event) { + this.position(); + }, + + /** + * + */ + fieldViewChangeHandler: function (view) { + this.render(this, view); + }, + + /** + * Uses the jQuery.ui.position() method to position the entity toolbar. + */ + position: function (element) { + clearTimeout(this.timer); + var that = this; + // Vary the edge of the positioning according to the direction of language + // in the document. + var edge = (document.documentElement.dir === 'rtl') ? 'right' : 'left'; + // If a field in this entity is active, position against it. + var activeEditor = Drupal.edit.app.model.get('activeEditor'); + var activeEkditorView = activeEditor && activeEditor.get('editorView'); + var activeEditedElement = activeEkditorView && activeEkditorView.getEditedElement(); + + // Label of a highlighted field, if it exists. + var highlightedEditor = Drupal.edit.app.model.get('highlightedEditor'); + var highlightedEditorView = highlightedEditor && highlightedEditor.get('editorView'); + var highlightedEditedElement = highlightedEditorView && highlightedEditorView.getEditedElement(); + // Prefer the specified element from the parameters, then the acive field + // and finally the entity itself to determine the position of the toolbar. + var of = element || activeEditedElement || highlightedEditedElement || this.$entity; + // Uses the jQuery.ui.position() method. Use a timeout to move the toolbar + // only after the user has focused on an editable for 250ms. This prevents + // the toolbar from jumping around the screen. + this.timer = setTimeout(function () { + that.$el + .position({ + my: edge + ' bottom', + at: edge + ' top', + of: of, + // Eliminate some of the placement jitteriness by flooring the suggested + // values. + using: function (suggested, info) { + info.element.element.css({ + left: Math.floor(suggested.left), + top: Math.floor(suggested.top) + }); + } + }) + .css({ + 'max-width': $(of).outerWidth(), + 'width': '100%' + }); + }, 250); + }, + + /** + * Determines the actions to take given a change of state. + * + * @param Drupal.edit.EntityModel model + * @param String state + * The state of the associated field. One of Drupal.edit.EntityModel.states. + */ + stateChange: function (model, state, options) { + var from = model.previous('state'); + var to = state; + switch (to) { + case 'inactive': + break; + case 'candidate': + break; + case 'highlighted': + break; + case 'activating': + this.setLoadingIndicator(true); + break; + case 'active': + this.setLoadingIndicator(false); + break; + case 'changed': + this.$el + .find('button.save') + .addClass('blue-button') + .removeClass('gray-button'); + break; + case 'saving': + this.setLoadingIndicator(true); + break; + case 'saved': + this.setLoadingIndicator(false); + break; + case 'invalid': + this.setLoadingIndicator(false); + break; + default: + break; + } + }, + + /** + * Set the model state to 'saving' when the save button is clicked. + * + * @param jQuery event + */ + onClickSave: function (event) { + event.stopPropagation(); + event.preventDefault(); + Drupal.edit.app.save(this.model); + }, + + /** + * Sets the model state to candidate when the cancel button is clicked. + * + * @param jQuery event + */ + onClickClose: function (event) { + event.stopPropagation(); + event.preventDefault(); + Drupal.edit.app.close(this.model); + }, + + /** + * + */ + buildToolbarEl: function () { + var $toolbar; + $toolbar = $(Drupal.theme('editEntityToolbar', { + id: 'edit-entity-toolbar' + })); + + $toolbar + .find('.edit-toolbar-entity') + // Append the "ops" toolgroup into the toolbar. + .prepend(Drupal.theme('editToolgroup', { + classes: ['ops'], + buttons: [ + { label: Drupal.t('Save'), type: 'submit', classes: 'field-save save gray-button' }, + { label: '' + Drupal.t('Close') + '', classes: 'field-close close gray-button' } + ] + })); + + // Give the toolbar a sensible starting position so that it doesn't + // animiate on to the screen from a far off corner. + $toolbar + .css({ + left: this.$entity.offset().left, + top: this.$entity.offset().top + }); + + this.setElement($toolbar); + }, + + /** + * + */ + getToolbarRoot: function () { + return this._fieldToolbarRoot; + }, + + /** + * 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 Boolean enabled + * Whether the loading indicator should be displayed or not. + */ + setLoadingIndicator: function (enabled) { + var that = this; + if (enabled) { + this._loader = setTimeout(function() { + that.addClass('info', 'loading'); + that._loaderVisibleStart = new Date().getTime(); + }, 600); + } + else { + 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; + } + }, + + /** + * + */ + label: function () { + // The entity title. + var title = this.model.get('title'); + + // Label of an active field, if it exists. + var activeEditor = Drupal.edit.app.model.get('activeEditor'); + var activeFieldLabel = activeEditor && activeEditor.get('label'); + activeFieldLabel = activeFieldLabel && activeFieldLabel + '::' + title; + + // Label of a highlighted field, if it exists. + var highlightedEditor = Drupal.edit.app.model.get('highlightedEditor'); + var highlightedFieldLabel = highlightedEditor && highlightedEditor.get('label'); + highlightedFieldLabel = highlightedFieldLabel && highlightedFieldLabel + '::' + title; + + this.$el + .find('.edit-toolbar-label') + .text(activeFieldLabel || highlightedFieldLabel || title); + }, + + /** + * Adds classes to a toolgroup. + * + * @param String toolgroup + * A toolgroup name. + */ + addClass: function (toolgroup, classes) { + this._find(toolgroup).addClass(classes); + }, + + /** + * Removes classes from a toolgroup. + * + * @param String toolgroup + * A toolgroup name. + */ + removeClass: function (toolgroup, classes) { + this._find(toolgroup).removeClass(classes); + }, + + /** + * Finds a toolgroup. + * + * @param String toolgroup + * A toolgroup name. + */ + _find: function (toolgroup) { + return this.$el.find('.edit-toolbar .edit-toolgroup.' + toolgroup); + }, + + /** + * Shows a toolgroup. + * + * @param String toolgroup + * A toolgroup name. + */ + show: function (toolgroup) { + this.$el.removeClass('edit-animate-invisible'); + } +}); + +})(jQuery, Backbone, Drupal, Drupal.debounce); diff --git a/core/modules/edit/js/views/FieldToolbarView.js b/core/modules/edit/js/views/FieldToolbarView.js index 117debb..741b97f 100644 --- a/core/modules/edit/js/views/FieldToolbarView.js +++ b/core/modules/edit/js/views/FieldToolbarView.js @@ -12,32 +12,22 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({ $field: null, - _loader: null, - _loaderVisibleStart: 0, - _id: null, - events: { - 'click.edit button.label': 'onClickInfoLabel', - 'mouseleave.edit': 'onMouseLeave', - 'click.edit button.field-save': 'onClickSave', - 'click.edit button.field-close': 'onClickClose' - }, - /** * Implements Backbone.View.prototype.initialize(). */ initialize: function (options) { this.$field = options.$field; + this.$root = this.$el; + this.setElement(); this.editorView = options.editorView; + this.entityModel = options.entityModel; - this._loader = null; - this._loaderVisibleStart = 0; + this.model.on('change:state', this.stateChange, this); // Generate a DOM-compatible ID for the form container DOM element. this._id = 'edit-toolbar-for-' + this.model.get('editID').replace(/\//g, '_'); - - this.model.on('change:state', this.stateChange, this); }, /** @@ -47,19 +37,17 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({ * toolbar is being inserted into the DOM, it will be inserted differently. */ render: function () { - // Render toolbar. - this.setElement($(Drupal.theme('editToolbarContainer', { + // Render toolbar and set it as the view's element. + this.setElement($(Drupal.theme('editFieldToolbar', { id: this._id }))); // Insert in DOM. if (this.$field.css('display') === 'inline') { - this.$el.prependTo(this.$field.offsetParent()); - var pos = this.$field.position(); - this.$el.css('left', pos.left).css('top', pos.top); + this.$el.prependTo(this.$field); } else { - this.$el.insertBefore(this.$field); + this.$el.prependTo(this.$root); } return this; @@ -72,7 +60,7 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({ * @param String state * The state of the associated field. One of Drupal.edit.FieldModel.states. */ - stateChange: function (model, state) { + stateChange: function (model, state, options) { var from = model.previous('state'); var to = state; switch (to) { @@ -82,181 +70,35 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({ } break; case 'candidate': - if (from === 'inactive') { - this.render(); - } - else { - // Remove all toolgroups; they're no longer necessary. - this.$el - .removeClass('edit-highlighted edit-editing') - .find('.edit-toolbar .edit-toolgroup').remove(); - if (from !== 'highlighted' && this.getEditUISetting('padding')) { - this._unpad(); - } - } + // Remove the toolbar; it is no longer necessary. + this.$el.remove(); break; case 'highlighted': - // As soon as we highlight, make sure we have a toolbar in the DOM (with at least a title). - this.startHighlight(); break; case 'activating': - this.setLoadingIndicator(true); - break; - case 'active': - this.startEdit(); - this.setLoadingIndicator(false); + this.render(); + if (this.getEditUISetting('fullWidthToolbar')) { this.$el.addClass('edit-toolbar-fullwidth'); } - - if (this.getEditUISetting('padding')) { - this._pad(); - } if (this.getEditUISetting('unifiedToolbar')) { this.insertWYSIWYGToolGroups(); } break; + case 'active': + break; case 'changed': - this.$el - .find('button.save') - .addClass('blue-button') - .removeClass('gray-button'); break; case 'saving': - this.setLoadingIndicator(true); break; case 'saved': - this.setLoadingIndicator(false); break; case 'invalid': - this.setLoadingIndicator(false); break; } }, /** - * Redirects the click.edit-event to the editor DOM element. - * - * @param jQuery event - */ - onClickInfoLabel: function (event) { - event.stopPropagation(); - event.preventDefault(); - // Redirects the event to the editor DOM element. - this.$field.trigger('click.edit'); - }, - - /** - * Controls mouseleave events. - * - * A mouseleave to the editor doesn't matter; a mouseleave to something else - * counts as a mouseleave on the editor itself. - * - * @param jQuery event - */ - onMouseLeave: function (event) { - if (event.relatedTarget !== this.$field[0] && !$.contains(this.$field, event.relatedTarget)) { - this.$field.trigger('mouseleave.edit'); - } - event.stopPropagation(); - }, - - /** - * Set the model state to 'saving' when the save button is clicked. - * - * @param jQuery event - */ - onClickSave: function (event) { - event.stopPropagation(); - event.preventDefault(); - this.model.set('state', 'saving'); - }, - - /** - * Sets the model state to candidate when the cancel button is clicked. - * - * @param jQuery event - */ - onClickClose: function (event) { - event.stopPropagation(); - event.preventDefault(); - this.model.set('state', 'candidate', { reason: 'cancel' }); - }, - - /** - * 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 Boolean enabled - * Whether the loading indicator should be displayed or not. - */ - setLoadingIndicator: function (enabled) { - var that = this; - if (enabled) { - this._loader = setTimeout(function() { - that.addClass('info', 'loading'); - that._loaderVisibleStart = new Date().getTime(); - }, 600); - } - else { - 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; - } - }, - - /** - * - */ - startHighlight: function () { - // Retrieve the lavel to show for this field. - var label = this.model.get('label'); - - this.$el - .addClass('edit-highlighted') - .find('.edit-toolbar') - // Append the "info" toolgroup into the toolbar. - .append(Drupal.theme('editToolgroup', { - classes: 'info edit-animate-only-background-and-padding', - buttons: [ - { label: label, classes: 'blank-button label' } - ] - })); - - // Animations. - var that = this; - setTimeout(function () { - that.show('info'); - }, 0); - }, - - /** - * - */ - startEdit: function () { - this.$el - .addClass('edit-editing') - .find('.edit-toolbar') - // Append the "ops" toolgroup into the toolbar. - .append(Drupal.theme('editToolgroup', { - classes: 'ops', - buttons: [ - { label: Drupal.t('Save'), type: 'submit', classes: 'field-save save gray-button' }, - { label: '' + Drupal.t('Close') + '', classes: 'field-close close gray-button' } - ] - })); - this.show('ops'); - }, - - /** * Retrieves a setting of the editor-specific Edit UI integration. * * @param String setting @@ -268,64 +110,24 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({ }, /** - * Adjusts the toolbar to accomodate padding on the editor. - * - * @see EditorDecorationView._pad(). - */ - _pad: function () { - // The whole toolbar must move to the top when the property's DOM element - // is displayed inline. - if (this.$field.css('display') === 'inline') { - this.$el.css('top', parseInt(this.$el.css('top'), 10) - 5 + 'px'); - } - - // The toolbar must move to the top and the left. - var $hf = this.$el.find('.edit-toolbar-heightfaker'); - $hf.css({ bottom: '6px', left: '-5px' }); - - if (this.getEditUISetting('fullWidthToolbar')) { - $hf.css({ width: this.$field.width() + 10 }); - } - }, - - /** - * Undoes the changes made by _pad(). - * - * @see EditorDecorationView._unpad(). - */ - _unpad: function () { - // Move the toolbar back to its original position. - var $hf = this.$el.find('.edit-toolbar-heightfaker'); - $hf.css({ bottom: '1px', left: '' }); - - if (this.getEditUISetting('fullWidthToolbar')) { - $hf.css({ width: '' }); - } - }, - - /** * */ insertWYSIWYGToolGroups: function () { this.$el - .find('.edit-toolbar') .append(Drupal.theme('editToolgroup', { id: this.getFloatedWysiwygToolgroupId(), - classes: 'wysiwyg-floated', + classes: ['wysiwyg-floated', 'edit-animate-slow', 'edit-animate-invisible', 'edit-animate-delay-veryfast'], buttons: [] })) .append(Drupal.theme('editToolgroup', { id: this.getMainWysiwygToolgroupId(), - classes: 'wysiwyg-main', + classes: ['wysiwyg-main', 'edit-animate-slow', 'edit-animate-invisible', 'edit-animate-delay-veryfast'], buttons: [] })); // Animate the toolgroups into visibility. - var that = this; - setTimeout(function () { - that.show('wysiwyg-floated'); - that.show('wysiwyg-main'); - }, 0); + this.show('wysiwyg-floated'); + this.show('wysiwyg-main'); }, /** @@ -365,43 +167,37 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({ }, /** - * Shows a toolgroup. - * - * @param String toolgroup - * A toolgroup name. - */ - show: function (toolgroup) { - this._find(toolgroup).removeClass('edit-animate-invisible'); - }, - - /** - * Adds classes to a toolgroup. - * - * @param String toolgroup - * A toolgroup name. - */ - addClass: function (toolgroup, classes) { - this._find(toolgroup).addClass(classes); - }, - - /** - * Removes classes from a toolgroup. + * Finds a toolgroup. * * @param String toolgroup * A toolgroup name. */ - removeClass: function (toolgroup, classes) { - this._find(toolgroup).removeClass(classes); + _find: function (toolgroup) { + return this.$el.find('.edit-toolgroup.' + toolgroup); }, /** - * Finds a toolgroup. + * Shows a toolgroup. * * @param String toolgroup * A toolgroup name. */ - _find: function (toolgroup) { - return this.$el.find('.edit-toolbar .edit-toolgroup.' + toolgroup); + show: function (toolgroup) { + var that = this; + var $group = this._find(toolgroup); + // Attach a transitionEnd event handler to the toolbar group so that update + // events can be triggered after the animations have ended. + $group + .on(Drupal.edit.util.constants.transitionEnd, function (event) { + that.entityModel.trigger('viewChanged', that); + $group.off(Drupal.edit.util.constants.transitionEnd); + }); + // The call to remove the class and start the animation must be started in + // the next animation frame or the event handler attached above won't be + // triggered. + window.setTimeout(function () { + $group.removeClass('edit-animate-invisible'); + }, 0); } }); diff --git a/core/modules/edit/lib/Drupal/edit/EditController.php b/core/modules/edit/lib/Drupal/edit/EditController.php index 44e8456..90590c5 100644 --- a/core/modules/edit/lib/Drupal/edit/EditController.php +++ b/core/modules/edit/lib/Drupal/edit/EditController.php @@ -64,8 +64,11 @@ public function metadata(Request $request) { if (!$langcode || (field_valid_language($langcode) !== $langcode)) { throw new NotFoundHttpException(); } - - $metadata[$field] = $metadataGenerator->generate($entity, $instance, $langcode, $view_mode); + $entity_id = $entity->entityType() . '/' . $entity_id; + if (!isset($metadata[$entity_id])) { + $metadata[$entity_id] = $metadataGenerator->generateEntity($entity, $langcode); + } + $metadata[$field] = $metadataGenerator->generateField($entity, $instance, $langcode, $view_mode); } $response->addCommand(new MetaDataCommand($metadata)); diff --git a/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php b/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php index cecc676..5f7e41b 100644 --- a/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php +++ b/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php @@ -55,10 +55,19 @@ public function __construct(EditEntityFieldAccessCheckInterface $access_checker, $this->editorManager = $editor_manager; } + public function generateEntity(EntityInterface $entity, $langcode) { + $title = $entity->getTranslation($langcode, FALSE)->get('title')->getValue(); + $title = $title[0]['value']; + return array( + 'type' => 'entity', + 'title' => $title + ); + } + /** * Implements \Drupal\edit\MetadataGeneratorInterface::generate(). */ - public function generate(EntityInterface $entity, FieldInstance $instance, $langcode, $view_mode) { + public function generateField(EntityInterface $entity, FieldInstance $instance, $langcode, $view_mode) { $field_name = $instance['field_name']; // Early-return if user does not have access. @@ -79,6 +88,7 @@ public function generate(EntityInterface $entity, FieldInstance $instance, $lang $label = $instance['label']; $editor = $this->editorManager->createInstance($editor_id); $metadata = array( + 'type' => 'field', 'label' => check_plain($label), 'access' => TRUE, 'editor' => $editor_id, diff --git a/core/modules/edit/lib/Drupal/edit/MetadataGeneratorInterface.php b/core/modules/edit/lib/Drupal/edit/MetadataGeneratorInterface.php index 16db770..d785171 100644 --- a/core/modules/edit/lib/Drupal/edit/MetadataGeneratorInterface.php +++ b/core/modules/edit/lib/Drupal/edit/MetadataGeneratorInterface.php @@ -15,6 +15,8 @@ */ interface MetadataGeneratorInterface { + public function generateEntity(EntityInterface $entity, $langcode); + /** * Generates in-place editing metadata for an entity field. * @@ -34,6 +36,6 @@ * - aria: the ARIA label. * - custom: (optional) any additional metadata that the editor provides. */ - public function generate(EntityInterface $entity, FieldInstance $instance, $langcode, $view_mode); + public function generateField(EntityInterface $entity, FieldInstance $instance, $langcode, $view_mode); } diff --git a/core/modules/editor/js/editor.formattedTextEditor.js b/core/modules/editor/js/editor.formattedTextEditor.js index 5d3bd01..8534257 100644 --- a/core/modules/editor/js/editor.formattedTextEditor.js +++ b/core/modules/editor/js/editor.formattedTextEditor.js @@ -51,7 +51,7 @@ Drupal.edit.editors.editor = Drupal.edit.EditorView.extend({ /** * {@inheritdoc} */ - stateChange: function (model, state) { + stateChange: function (model, state, options, callback) { var that = this; var from = model.previous('state'); var to = state; @@ -116,7 +116,7 @@ Drupal.edit.editors.editor = Drupal.edit.EditorView.extend({ if (from === 'invalid') { this.removeValidationErrors(); } - this.save(); + this.save(options); break; case 'saved': -- 1.7.10.4