core/modules/edit/edit.module | 1 - core/modules/edit/js/edit.js | 66 +++++++++++----- core/modules/edit/js/editors/directEditor.js | 29 +++---- core/modules/edit/js/editors/editor.js | 81 -------------------- core/modules/edit/js/views/contextuallink-view.js | 45 +---------- .../edit/js/views/propertyeditordecoration-view.js | 10 +-- core/modules/edit/js/views/toolbar-view.js | 31 ++++---- core/modules/editor/js/editor.createjs.js | 65 ++++++++-------- 8 files changed, 114 insertions(+), 214 deletions(-) diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module index 3cec929..c83590a 100644 --- a/core/modules/edit/edit.module +++ b/core/modules/edit/edit.module @@ -72,7 +72,6 @@ function edit_library_info() { 'version' => VERSION, 'js' => array( // Core. - $path . '/js/editors/editor.js' => $options, $path . '/js/edit.js' => $options, // Views. $path . '/js/views/propertyeditordecoration-view.js' => $options, diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js index cad2b07..f89fc03 100644 --- a/core/modules/edit/js/edit.js +++ b/core/modules/edit/js/edit.js @@ -52,6 +52,10 @@ Drupal.behaviors.edit = { this.collections.editables = new Drupal.edit.EditablesCollection(); } + // Respond to entity model change events. + this.collections.entities + .on('change:isActive', this.enforceSingleActiveEntity, this); + this.collections.editables // Respond to editable model HTML representation change events. .on('change:html', this.updateAllKnownInstances, this); @@ -93,8 +97,6 @@ Drupal.behaviors.edit = { // Update the metadata cache. _.each(results, function(metadata, editID) { Drupal.edit.metadataCache[editID] = metadata; - // @temporary HACK: use the 'form' editor for all fields - Drupal.edit.metadataCache[editID].editor = 'form'; }); // Annotate the remaining fields based on the updated access cache. @@ -239,6 +241,26 @@ Drupal.behaviors.edit = { return false; }, + /** + * EntityModel Collection change handler, called on change:isActive, enforces + * a single active entity. + */ + enforceSingleActiveEntity: function (changedEntityModel) { + // When an entity is deactivated, we don't need to enforce anything. + if (changedEntityModel.get('isActive') === false) { + return; + } + + // This entity was activated; deactivate all other entities. + changedEntityModel.collection.chain() + .filter(function (entityModel) { + return entityModel.get('isActive') === true && entityModel !== changedEntityModel; + }) + .each(function (entityModel) { + entityModel.set('isActive', false); + }); + }, + // Updates all known instances of a specific entity field whenever the HTML // representation of one of them has changed. // @todo: this is currently prototype-level code, test this. The principle is @@ -456,43 +478,44 @@ $.extend(Drupal.edit, { // They are a sibling element before the editor's DOM element. var toolbarView = new Drupal.edit.ToolbarView({ model: fieldModel, - editor: editorView + $field: $el, + editorView: editorView }); // Decorate the editor's DOM element depending on its state. var decorationView = new Drupal.edit.PropertyEditorDecorationView({ el: $el, model: fieldModel, - editor: editorView, + editorView: editorView, toolbarId: toolbarView.getId() }); - // Create references; necessary for undecorate(). - fieldModel.set('decorationViews', { - editorView: editorView, - toolbarView: toolbarView, - decorationView: decorationView - }); + // Create references in the field model; necessary for undecorate() and + // necessary for some EditorView implementations. + fieldModel.set('editorView', editorView); + fieldModel.set('toolbarView', toolbarView); + fieldModel.set('decorationView', decorationView); }, // @todo rename to undecorateField undecorate: function (fieldModel) { - var decorationViews = fieldModel.get('decorationViews'); - // Unbind event handlers; remove toolbar element; delete toolbar view. - decorationViews.toolbarView.undelegateEvents(); - decorationViews.toolbarView.remove(); - delete decorationViews.toolbarView; + var toolbarView = fieldModel.get('toolbarView'); + toolbarView.undelegateEvents(); + toolbarView.remove(); + fieldModel.unset('toolbarView'); // Unbind event handlers; delete decoration view. Don't remove the element // because that would remove the field itself. - decorationViews.decorationView.undelegateEvents(); - delete decorationViews.decorationView; + var decorationView = fieldModel.get('decorationView'); + decorationView.undelegateEvents(); + fieldModel.unset('decorationView'); // Unbind event handlers; delete editor view. Don't remove the element // because that would remove the field itself. - decorationViews.editorView.undelegateEvents(); - delete decorationViews.editorView; + var editorView = fieldModel.get('editorView'); + editorView.undelegateEvents(); + fieldModel.unset('editorView'); }, /** @@ -540,7 +563,7 @@ $.extend(Drupal.edit, { var from = fieldModel.previous('state'); var to = state; - console.log("%c %s → %s %s", "background-color: black; color: white", from, to, fieldModel.get('editID')); + console.log("%c [APP] %s → %s %s", "background-color: black; color: white", from, to, fieldModel.get('editID')); // Keep track of the highlighted editor in the global state. if (_.indexOf(this.singleEditorStates, to) !== -1 && this.model.get('highlightedEditor') !== fieldModel) { @@ -672,6 +695,9 @@ $.extend(Drupal.edit, { }, initialize: function () { this.get('entity').get('fields').add(this); + this.on('change:state', function(model, state) { + console.log('%c [MOD] ' + this.previous('state') + ' → ' + state + ' ' + model.get('editID'), 'background-color: blue; color: white'); + }); this.on('error', function(model, error) { console.log('%c' + model.get("editID") + " " + error, 'color: orange'); }); diff --git a/core/modules/edit/js/editors/directEditor.js b/core/modules/edit/js/editors/directEditor.js index ebf1650..1a269d8 100644 --- a/core/modules/edit/js/editors/directEditor.js +++ b/core/modules/edit/js/editors/directEditor.js @@ -19,15 +19,7 @@ Drupal.edit.editors.direct = Backbone.View.extend({ }, /** - * Implements jQuery UI widget factory's _init() method. * - * @todo: POSTPONED_ON(Create.js, https://github.com/bergie/create/issues/142) - * Get rid of this once that issue is solved. - */ - _init: function() {}, - - /** - * Implements Create's _initialize() method. */ _initialize: function() { var that = this; @@ -35,13 +27,15 @@ Drupal.edit.editors.direct = Backbone.View.extend({ // Sets the state to 'changed' whenever the content has changed. var before = jQuery.trim(this.element.text()); this.element.on('keyup paste', function (event) { - if (that.options.disabled) { - return; - } var current = jQuery.trim(that.element.text()); if (before !== current) { before = current; - that.options.changed(current); + that.model.set('state', 'changed'); + + // @todo we have yet to set this value originally (before the editing + // starts) AND we have to handle the reverting aspect when editing is + // canceled, see editorStateChange(). + that.model.set('value', current); } }); }, @@ -49,7 +43,9 @@ Drupal.edit.editors.direct = Backbone.View.extend({ /** * Makes this PropertyEditor widget react to state changes. */ - stateChange: function(model, value) { + stateChange: function(model, state) { + var from = model.previous('state'); + var to = state; switch (value) { case 'inactive': break; @@ -62,7 +58,12 @@ Drupal.edit.editors.direct = Backbone.View.extend({ case 'highlighted': break; case 'activating': - this.options.activated(); + // As soon as the current state change has propagated, apply this one. + // @see http://jsfiddle.net/5MVzp/2/ vs. http://jsfiddle.net/5MVzp/3/ + var that = this; + _.defer(function() { + that.model.set('state', 'active'); + }); break; case 'active': // Sets the "contenteditable" attribute to "true". diff --git a/core/modules/edit/js/editors/editor.js b/core/modules/edit/js/editors/editor.js deleted file mode 100644 index e9a3696..0000000 --- a/core/modules/edit/js/editors/editor.js +++ /dev/null @@ -1,81 +0,0 @@ -(function ($, Drupal, drupalSettings) { - - 'use strict'; - - Drupal.edit = Drupal.edit || {}; - Drupal.edit.editors = Drupal.edit.editors || {}; - - Drupal.edit.editors.editor = function () {}; - - $.extend(Drupal.edit.editors.editor.prototype, { - // override to enable the widget - enable: function () { - this.element.attr('contenteditable', 'true'); - }, - // override to disable the widget - disable: function (disable) { - this.element.attr('contenteditable', 'false'); - }, - // called by the jQuery UI plugin factory when creating the property editor - // widget instance - _create: function () { - this._registerWidget(); - this._initialize(); - - if (_.isFunction(this.options.decorate) && _.isFunction(this.options.decorateParams)) { - // TRICKY: we can't use this.options.decorateParams()'s 'propertyName' - // parameter just yet, because it will only be available after this - // object has been created, but we're currently in the constructor! - // Hence we have to duplicate part of its logic here. - this.options.decorate(this.options.decorateParams(null, { - propertyName: this.options.property, - propertyEditor: this, - propertyElement: this.element, - // Deprecated. - editor: this, - predicate: this.options.property, - element: this.element - })); - } - }, - // called every time the property editor widget is called - _init: function () { - if (this.options.disabled) { - this.disable(); - return; - } - this.enable(); - }, - // override this function to initialize the property editor widget functions - _initialize: function () { - var self = this; - this.element.on('focus', function () { - if (self.options.disabled) { - return; - } - self.options.activated(); - }); - this.element.on('blur', function () { - if (self.options.disabled) { - return; - } - self.options.deactivated(); - }); - var before = this.element.html(); - this.element.on('keyup paste', function (event) { - if (self.options.disabled) { - return; - } - var current = jQuery(this).html(); - if (before !== current) { - before = current; - self.options.changed(current); - } - }); - }, - // used to register the property editor widget name with the DOM element - _registerWidget: function () { - this.element.data("createWidgetName", this.widgetName); - } - }); -})(jQuery, Drupal, drupalSettings); diff --git a/core/modules/edit/js/views/contextuallink-view.js b/core/modules/edit/js/views/contextuallink-view.js index 7de0472..76ef876 100644 --- a/core/modules/edit/js/views/contextuallink-view.js +++ b/core/modules/edit/js/views/contextuallink-view.js @@ -50,50 +50,7 @@ Drupal.edit.ContextualLinkView = Backbone.View.extend({ onClick: function (event) { event.preventDefault(); - var that = this; - var updateActiveEntity = function() { - // The active entity is the current entity, i.e. stop editing the current - // entity. - if (that.model.get('isActive') === true) { - that.model.set('isActive', false); - } - // The active entity is different from the current entity, i.e. start - // editing this entity instead of the previous one. - else { - // Stop editing the currently active entity, if any. - var currentlyActiveEntity = that.model.collection.where({ isActive: true }); - if (currentlyActiveEntity.length > 0) { - currentlyActiveEntity[0].set('isActive', false); - } - - // Start the editing of this entity. - that.model.set('isActive', true); - } - }; - - // If there's an active editor, attempt to set its state to 'candidate', and - // only then do what the user asked. - // (Only when all PropertyEditor widgets of an entity are in the 'candidate' - // state, it is possible to stop editing it.) - var activeEditor = this.model.get('activeEditor'); - if (activeEditor) { - debugger; - // @todo this branch has not yet been updated. - var editableEntity = activeEditor.options.widget; - var predicate = activeEditor.options.property; - editableEntity.setState('candidate', predicate, { reason: 'stop or switch' }, function(accepted) { - if (accepted) { - updateActiveEntity(); - } - else { - // No change. - } - }); - } - // Otherwise, we can immediately do what the user asked. - else { - updateActiveEntity(); - } + this.model.set('isActive', !this.model.get('isActive')); }, /** diff --git a/core/modules/edit/js/views/propertyeditordecoration-view.js b/core/modules/edit/js/views/propertyeditordecoration-view.js index 91d544e..b3f04ba 100644 --- a/core/modules/edit/js/views/propertyeditordecoration-view.js +++ b/core/modules/edit/js/views/propertyeditordecoration-view.js @@ -34,10 +34,11 @@ Drupal.edit.PropertyEditorDecorationView = Backbone.View.extend({ * - toolbarId: the ID attribute of the toolbar as rendered in the DOM. */ initialize: function(options) { - this.model.on('change:state', this.stateChange, this); + this.editorView = options.editorView; - this.editor = options.editor; this.toolbarId = options.toolbarId; + + this.model.on('change:state', this.stateChange, this); }, /** @@ -134,7 +135,6 @@ Drupal.edit.PropertyEditorDecorationView = Backbone.View.extend({ decorate: function () { this.$el.addClass('edit-animate-fast edit-candidate edit-editable'); - this.delegateEvents(); }, undecorate: function () { @@ -189,7 +189,7 @@ Drupal.edit.PropertyEditorDecorationView = Backbone.View.extend({ * @see Drupal.edit.util.getEditUISetting(). */ getEditUISetting: function(setting) { - return Drupal.edit.util.getEditUISetting(this.editor, setting); + return Drupal.edit.util.getEditUISetting(this.editorView, setting); }, _pad: function () { @@ -341,7 +341,7 @@ Drupal.edit.PropertyEditorDecorationView = Backbone.View.extend({ * type=form. */ _removeValidationErrors: function() { - if (this.editorName !== 'form') { + if (this.model.get('editor') !== 'form') { this.$el .removeClass('edit-validation-error') .next('.edit-validation-errors') diff --git a/core/modules/edit/js/views/toolbar-view.js b/core/modules/edit/js/views/toolbar-view.js index 359ad81..7497776 100644 --- a/core/modules/edit/js/views/toolbar-view.js +++ b/core/modules/edit/js/views/toolbar-view.js @@ -10,7 +10,7 @@ "use strict"; Drupal.edit.ToolbarView = Backbone.View.extend({ - editor: null, + $field: null, _loader: null, _loaderVisibleStart: 0, @@ -28,13 +28,14 @@ Drupal.edit.ToolbarView = Backbone.View.extend({ * Implements Backbone Views' initialize() function. */ initialize: function(options) { - this.editor = options.editor; + this.$field = options.$field; + this.editorView = options.editorView; this._loader = null; this._loaderVisibleStart = 0; // Generate a DOM-compatible ID for the form container DOM element. - this.elementId = 'edit-toolbar-for-' + this.model.get('editID').replace(/\//g, '_'); + this._id = 'edit-toolbar-for-' + this.model.get('editID').replace(/\//g, '_'); this.model.on('change:state', this.stateChange, this); }, @@ -48,18 +49,17 @@ Drupal.edit.ToolbarView = Backbone.View.extend({ render: function () { // Render toolbar. this.setElement($(Drupal.theme('editToolbarContainer', { - id: this.elementId + id: this._id }))); // Insert in DOM. - var $fieldElement = this.editor.$el; - if ($fieldElement.css('display') === 'inline') { - this.$el.prependTo($fieldElement.offsetParent()); - var pos = $fieldElement.position(); + 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); } else { - this.$el.insertBefore($fieldElement); + this.$el.insertBefore(this.$field); } return this; @@ -146,7 +146,7 @@ Drupal.edit.ToolbarView = Backbone.View.extend({ event.stopPropagation(); event.preventDefault(); // Redirects the event to the editor DOM element. - this.editor.element.trigger('click.edit'); + this.$field.trigger('click.edit'); }, /** @@ -156,9 +156,8 @@ Drupal.edit.ToolbarView = Backbone.View.extend({ * @param event */ onMouseLeave: function(event) { - var el = this.editor.el; - if (event.relatedTarget !== el && !$.contains(el, event.relatedTarget)) { - this.editor.$el.trigger('mouseleave.edit'); + if (event.relatedTarget !== this.$field[0] && !$.contains(this.$field, event.relatedTarget)) { + this.$field.trigger('mouseleave.edit'); } event.stopPropagation(); }, @@ -258,7 +257,7 @@ Drupal.edit.ToolbarView = Backbone.View.extend({ * @see Drupal.edit.util.getEditUISetting(). */ getEditUISetting: function(setting) { - return Drupal.edit.util.getEditUISetting(this.editor, setting); + return Drupal.edit.util.getEditUISetting(this.editorView, setting); }, /** @@ -269,7 +268,7 @@ Drupal.edit.ToolbarView = Backbone.View.extend({ _pad: function() { // The whole toolbar must move to the top when the property's DOM element // is displayed inline. - if (this.editor.element.css('display') === 'inline') { + if (this.$field.css('display') === 'inline') { this.$el.css('top', parseInt(this.$el.css('top'), 10) - 5 + 'px'); } @@ -278,7 +277,7 @@ Drupal.edit.ToolbarView = Backbone.View.extend({ $hf.css({ bottom: '6px', left: '-5px' }); if (this.getEditUISetting('fullWidthToolbar')) { - $hf.css({ width: this.editor.element.width() + 10 }); + $hf.css({ width: this.$field.width() + 10 }); } }, diff --git a/core/modules/editor/js/editor.createjs.js b/core/modules/editor/js/editor.createjs.js index d9e1c2e..c28509e 100644 --- a/core/modules/editor/js/editor.createjs.js +++ b/core/modules/editor/js/editor.createjs.js @@ -16,17 +16,11 @@ "use strict"; -Drupal.editor = Drupal.editor || {}; +Drupal.edit.editors.editor = Backbone.View.extend({ -Drupal.editor.TextEditor = function () { - this.textFormat = null; - this.textFormatHasTransformations = null; - this.textEditor = null; -}; - -// @todo D8: use jQuery UI Widget bridging. -// @see http://drupal.org/node/1874934#comment-7124904 -$.extend(Drupal.editor.TextEditor.prototype, Drupal.editor.Editor, { + textFormat: null, + textFormatHasTransformations: null, + textEditor: null, /** * Implements Create.editWidget.getEditUISettings. @@ -36,29 +30,24 @@ $.extend(Drupal.editor.TextEditor.prototype, Drupal.editor.Editor, { }, /** - * Implements jQuery.widget._init. - * - * @todo D8: Remove this. - * @see http://drupal.org/node/1874934 - */ - _init: function () {}, - - /** * Implements Create.editWidget._initialize. */ - _initialize: function () { - var propertyID = Drupal.edit.util.calcPropertyID(this.options.entity, this.options.property); - var metadata = Drupal.edit.metadataCache[propertyID].custom; - + initialize: function () { + var editID = this.model.get('editID'); + var metadata = Drupal.edit.metadataCache[editID].custom; this.textFormat = drupalSettings.editor.formats[metadata.format]; this.textFormatHasTransformations = metadata.formatHasTransformations; this.textEditor = Drupal.editors[this.textFormat.editor]; + + this.model.on('change:state', this.stateChange, this); }, /** * Implements Create.editWidget.stateChange. */ - stateChange: function (from, to) { + stateChange: function(model, state) { + var from = model.previous('state'); + var to = state; var that = this; switch (to) { case 'inactive': @@ -68,7 +57,7 @@ $.extend(Drupal.editor.TextEditor.prototype, Drupal.editor.Editor, { // Detach the text editor when entering the 'candidate' state from one // of the states where it could have been attached. if (from !== 'inactive' && from !== 'highlighted') { - this.textEditor.detach(this.element.get(0), this.textFormat); + this.textEditor.detach(this.$el.get(0), this.textFormat); } break; @@ -80,29 +69,39 @@ $.extend(Drupal.editor.TextEditor.prototype, Drupal.editor.Editor, { // text of this field, then we'll need to load a re-processed version of // it without the transformation filters. if (this.textFormatHasTransformations) { - var propertyID = Drupal.edit.util.calcPropertyID(this.options.entity, this.options.property); - this._getUntransformedText(propertyID, this.element, function (untransformedText) { + var editID = this.model.get('editID'); + this._getUntransformedText(editID, this.$el, function (untransformedText) { + debugger; that.element.html(untransformedText); - that.options.activated(); + that.model.set('state', 'active'); }); } // When no transformation filters have been applied: start WYSIWYG // editing immediately! else { - this.options.activated(); + // As soon as the current state change has propagated, apply this one. + // @see http://jsfiddle.net/5MVzp/2/ vs. http://jsfiddle.net/5MVzp/3/ + _.defer(function() { + that.model.set('state', 'active'); + }); } break; case 'active': this.textEditor.attachInlineEditor( - this.element.get(0), + this.$el.get(0), this.textFormat, - this.toolbarView.getMainWysiwygToolgroupId(), - this.toolbarView.getFloatedWysiwygToolgroupId() + this.model.get('toolbarView').getMainWysiwygToolgroupId(), + this.model.get('toolbarView').getFloatedWysiwygToolgroupId() ); // Set the state to 'changed' whenever the content has changed. - this.textEditor.onChange(this.element.get(0), function (html) { - that.options.changed(html); + this.textEditor.onChange(this.$el.get(0), function (value) { + that.model.set('state', 'changed'); + + // @todo we have yet to set this value originally (before the editing + // starts) AND we have to handle the reverting aspect when editing is + // canceled, see editorStateChange(). + that.model.set('value', value); }); break;