core/modules/edit/css/edit.css | 13 +- core/modules/edit/edit.module | 24 ++- core/modules/edit/js/createjs/editable.js | 31 +--- .../editingWidgets/drupalcontenteditablewidget.js | 7 + .../edit/js/createjs/editingWidgets/formwidget.js | 11 ++ core/modules/edit/js/edit.js | 9 +- core/modules/edit/js/util.js | 31 +++- core/modules/edit/js/viejs/EditService.js | 18 +- .../edit/js/views/propertyeditordecoration-view.js | 53 +++--- core/modules/edit/js/views/toolbar-view.js | 71 ++++---- core/modules/edit/lib/Drupal/edit/EditBundle.php | 8 +- .../edit/lib/Drupal/edit/EditController.php | 2 +- core/modules/edit/lib/Drupal/edit/EditorBase.php | 26 +++ .../edit/lib/Drupal/edit/EditorInterface.php | 60 +++++++ .../edit/lib/Drupal/edit/EditorSelector.php | 172 ++++++++------------ .../edit/lib/Drupal/edit/MetadataGenerator.php | 42 +++-- .../lib/Drupal/edit/MetadataGeneratorInterface.php | 2 +- .../edit/lib/Drupal/edit/Plugin/EditorManager.php | 48 ++++++ .../Drupal/edit/Plugin/ProcessedTextEditorBase.php | 26 --- .../edit/Plugin/ProcessedTextEditorInterface.php | 35 ---- .../edit/Plugin/ProcessedTextEditorManager.php | 46 ------ .../edit/Plugin/edit/editor/DirectEditor.php | 57 +++++++ .../Drupal/edit/Plugin/edit/editor/FormEditor.php | 43 +++++ .../edit/lib/Drupal/edit/Tests/EditTestBase.php | 4 +- .../lib/Drupal/edit/Tests/EditorSelectionTest.php | 38 +++-- .../Drupal/edit/Tests/MetadataGeneratorTest.php | 76 ++++++++- .../edit_test/Plugin/edit/editor/WysiwygEditor.php | 67 ++++++++ .../processed_text_editor/TestProcessedEditor.php | 32 ---- 28 files changed, 641 insertions(+), 411 deletions(-) diff --git a/core/modules/edit/css/edit.css b/core/modules/edit/css/edit.css index 646454b..37e10eb 100644 --- a/core/modules/edit/css/edit.css +++ b/core/modules/edit/css/edit.css @@ -121,7 +121,7 @@ outline: none; } .edit-field.edit-editable, -.edit-field.edit-type-direct .edit-editable { +.edit-field .edit-editable { box-shadow: 0 0 1px 1px #4d9de9; } @@ -131,12 +131,12 @@ } .edit-field.edit-editable.edit-highlighted, .edit-form.edit-editable.edit-highlighted, -.edit-field.edit-type-direct .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-editable.edit-highlighted.edit-validation-error, .edit-form.edit-editable.edit-highlighted.edit-validation-error, -.edit-field.edit-type-direct .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-form.edit-editable .form-item .error { @@ -146,7 +146,7 @@ /* Editing (focused) editable. */ .edit-form.edit-editable.edit-editing, -.edit-field.edit-type-direct .edit-editable.edit-editing { +.edit-field .edit-editable.edit-editing { /* In the latest design, there's no special styling when editing as opposed to * just hovering. * This will be necessary again for http://drupal.org/node/1844220. @@ -290,9 +290,8 @@ .edit-toolbar-heightfaker { clip: rect(-1000px, 1000px, auto, -1000px); /* Remove bottom box-shadow. */ } -/* Exception: when used for a directly WYSIWYG editable field that is actively - being edited. */ -.edit-type-direct-with-wysiwyg .edit-editing .edit-toolbar-heightfaker { +/* Exception: when the toolbar is instructed to be "full width". */ +.edit-toolbar-fullwidth .edit-toolbar-heightfaker { width: 100%; clip: auto; } diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module index e3beec0..98a2181 100644 --- a/core/modules/edit/edit.module +++ b/core/modules/edit/edit.module @@ -104,8 +104,6 @@ function edit_library_info() { // Create.js subclasses. $path . '/js/createjs/editable.js' => $options, $path . '/js/createjs/storage.js' => $options, - $path . '/js/createjs/editingWidgets/formwidget.js' => $options, - $path . '/js/createjs/editingWidgets/drupalcontenteditablewidget.js' => $options, // Other. $path . '/js/util.js' => $options, $path . '/js/theme.js' => $options, @@ -135,6 +133,26 @@ function edit_library_info() { array('system', 'drupalSettings'), ), ); + $libraries['edit.editor.form'] = array( + 'title' => '"Form" Create.js PropertyEditor widget', + 'version' => VERSION, + 'js' => array( + $path . '/js/createjs/editingWidgets/formwidget.js' => $options, + ), + 'dependencies' => array( + array('edit', 'edit'), + ), + ); + $libraries['edit.editor.direct'] = array( + 'title' => '"Direct" Create.js PropertyEditor widget', + 'version' => VERSION, + 'js' => array( + $path . '/js/createjs/editingWidgets/drupalcontenteditablewidget.js' => $options, + ), + 'dependencies' => array( + array('edit', 'edit'), + ), + ); return $libraries; } @@ -145,7 +163,7 @@ function edit_library_info() { function edit_preprocess_field(&$variables) { $element = $variables['element']; $entity = $element['#object']; - $variables['attributes']['data-edit-id'] = $entity->entityType() . ':' . $entity->id() . ':' . $element['#field_name'] . ':' . $element['#language'] . ':' . $element['#view_mode']; + $variables['attributes']['data-edit-id'] = $entity->entityType() . '/' . $entity->id() . '/' . $element['#field_name'] . '/' . $element['#language'] . '/' . $element['#view_mode']; } /** diff --git a/core/modules/edit/js/createjs/editable.js b/core/modules/edit/js/createjs/editable.js index aac1ed2..1316023 100644 --- a/core/modules/edit/js/createjs/editable.js +++ b/core/modules/edit/js/createjs/editable.js @@ -1,8 +1,8 @@ /** * @file - * Determines which editor to use based on a class attribute. + * Determines which editor (Create.js PropertyEditor widget) to use. */ -(function (jQuery, drupalSettings) { +(function (jQuery, Drupal, drupalSettings) { "use strict"; @@ -13,31 +13,18 @@ this.options.domService = 'edit'; this.options.predicateSelector = '*'; //'.edit-field.edit-allowed'; - this.options.editors.direct = { - widget: 'drupalContentEditableWidget', - options: {} - }; - this.options.editors['direct-with-wysiwyg'] = { - widget: drupalSettings.edit.wysiwygEditorWidgetName, - options: {} - }; - this.options.editors.form = { - widget: 'drupalFormWidget', - options: {} - }; + // The Create.js PropertyEditor widget configuration is not hardcoded; it + // is generated by the server. + this.options.propertyEditorWidgetsConfiguration = drupalSettings.edit.editors; jQuery.Midgard.midgardEditable.prototype._create.call(this); }, _propertyEditorName: function(data) { - if (jQuery(this.element).hasClass('edit-type-direct')) { - if (jQuery(this.element).hasClass('edit-type-direct-with-wysiwyg')) { - return 'direct-with-wysiwyg'; - } - return 'direct'; - } - return 'form'; + // Pick a PropertyEditor widget for a property depending on its metadata. + var propertyID = Drupal.edit.util.calcPropertyID(data.entity, data.property); + return Drupal.edit.metadataCache[propertyID].editor; } }); -})(jQuery, drupalSettings); +})(jQuery, Drupal, drupalSettings); diff --git a/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js b/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js index c773e6e..5671f39 100644 --- a/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js +++ b/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js @@ -9,6 +9,13 @@ jQuery.widget('Drupal.drupalContentEditableWidget', jQuery.Create.editWidget, { /** + * Implements getEditUISettings() method. + */ + getEditUISettings: function() { + return { padding: true, unifiedToolbar: false, fullWidthToolbar: false }; + }, + + /** * Implements jQuery UI widget factory's _init() method. * * @todo: POSTPONED_ON(Create.js, https://github.com/bergie/create/issues/142) diff --git a/core/modules/edit/js/createjs/editingWidgets/formwidget.js b/core/modules/edit/js/createjs/editingWidgets/formwidget.js index f7c77cd..3238566 100644 --- a/core/modules/edit/js/createjs/editingWidgets/formwidget.js +++ b/core/modules/edit/js/createjs/editingWidgets/formwidget.js @@ -12,6 +12,13 @@ $formContainer: null, /** + * Implements getEditUISettings() method. + */ + getEditUISettings: function() { + return { padding: false, unifiedToolbar: false, fullWidthToolbar: false }; + }, + + /** * Implements jQuery UI widget factory's _init() method. * * @todo: POSTPONED_ON(Create.js, https://github.com/bergie/create/issues/142) @@ -42,11 +49,15 @@ case 'candidate': if (from !== 'inactive') { this.disable(); + if (from !== 'highlighted') { + this.element.removeClass('edit-belowoverlay'); + } } break; case 'highlighted': break; case 'activating': + this.element.addClass('edit-belowoverlay'); this.enable(); break; case 'active': diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js index 1f6c715..cfaf76b 100644 --- a/core/modules/edit/js/edit.js +++ b/core/modules/edit/js/edit.js @@ -39,14 +39,7 @@ Drupal.behaviors.edit = { field.$el .attr('data-edit-field-label', meta.label) .attr('aria-label', meta.aria) - .addClass('edit-field edit-type-' + meta.editor); - if (meta.editor === 'direct-with-wysiwyg') { - field.$el - // This editor also uses the Backbone.syncDirect saving mechanism. - .addClass('edit-type-direct') - .attr('data-edit-text-format', meta.format) - .addClass((meta.formatHasTransformations) ? 'edit-text-with-transformation-filters' : 'edit-text-without-transformation-filters'); - } + .addClass('edit-field edit-type-' + ((meta.editor === 'form') ? 'form' : 'direct')); } return true; diff --git a/core/modules/edit/js/util.js b/core/modules/edit/js/util.js index ef5f3dc..6633859 100644 --- a/core/modules/edit/js/util.js +++ b/core/modules/edit/js/util.js @@ -2,7 +2,7 @@ * @file * Provides utility functions for Edit. */ -(function($, Drupal, drupalSettings) { +(function($, _, Drupal, drupalSettings) { "use strict"; @@ -16,6 +16,33 @@ Drupal.edit.util.calcPropertyID = function(entity, predicate) { return entity.getSubjectUri() + '/' + predicate; }; +/** + * Retrieves a setting of the editor-specific Edit UI integration. + * + * If the editor does not implement the optional getEditUISettings() method, or + * if it doesn't set a value for a certain setting, then the default value will + * be used. + * + * @param editor + * A Create.js PropertyEditor widget instance. + * @param setting + * Name of the Edit UI integration setting. + * + * @return {*} + */ +Drupal.edit.util.getEditUISetting = function(editor, setting) { + var settings = {}; + var defaultSettings = { + padding: false, + unifiedToolbar: false, + fullWidthToolbar: false + }; + if (typeof editor.getEditUISettings === 'function') { + settings = editor.getEditUISettings(); + } + return _.extend(defaultSettings, settings)[setting]; +}; + Drupal.edit.util.buildUrl = function(id, urlFormat) { var parts = id.split('/'); return Drupal.formatString(decodeURIComponent(urlFormat), { @@ -148,4 +175,4 @@ Drupal.edit.util.form = { } }; -})(jQuery, Drupal, drupalSettings); +})(jQuery, _, Drupal, drupalSettings); diff --git a/core/modules/edit/js/viejs/EditService.js b/core/modules/edit/js/viejs/EditService.js index f52a6c0..00cb04b 100644 --- a/core/modules/edit/js/viejs/EditService.js +++ b/core/modules/edit/js/viejs/EditService.js @@ -62,7 +62,7 @@ // Let's only have this overhead for direct types. Form-based editors are // handled in backbone.drupalform.js and the PropertyEditor instance. - if (!jQuery(element).hasClass('edit-type-direct')) { + if (jQuery(element).hasClass('edit-type-form')) { return; } @@ -142,7 +142,7 @@ // Returns the "URI" of an entity of an element in format // `/`. getElementSubject: function (element) { - return this._getID(element).split(':').slice(0, 2).join('/'); + return this._getID(element).split('/').slice(0, 2).join('/'); }, // Returns the field name for an element in format @@ -152,11 +152,11 @@ if (!this._getID(element)) { throw new Error('Could not find predicate for element'); } - return this._getID(element).split(':').slice(2, 5).join('/'); + return this._getID(element).split('/').slice(2, 5).join('/'); }, getElementType: function (element) { - return this._getID(element).split(':').slice(0, 1)[0]; + return this._getID(element).split('/').slice(0, 1)[0]; }, // Reads all editable entities (currently each Drupal field is considered an @@ -214,15 +214,7 @@ if (type.attributes.get(predicate)) { return type; } - - var label = element.data('edit-field-label'); - var range = 'Form'; - if (element.hasClass('edit-type-direct')) { - range = 'Direct'; - } - if (element.hasClass('edit-type-direct-with-wysiwyg')) { - range = 'Wysiwyg'; - } + var range = predicate.split('/')[0]; type.attributes.add(predicate, [range], 0, 1, { label: element.data('edit-field-label') }); diff --git a/core/modules/edit/js/views/propertyeditordecoration-view.js b/core/modules/edit/js/views/propertyeditordecoration-view.js index 269259a..0eb4e45 100644 --- a/core/modules/edit/js/views/propertyeditordecoration-view.js +++ b/core/modules/edit/js/views/propertyeditordecoration-view.js @@ -12,10 +12,6 @@ Drupal.edit = Drupal.edit || {}; Drupal.edit.views = Drupal.edit.views || {}; Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ - editor: null, - entity: null, - predicate : null, - editorName: null, toolbarId: null, _widthAttributeIsEmpty: null, @@ -35,8 +31,6 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ * - editor: the editor object with an 'options' object that has these keys: * * entity: the VIE entity for the property. * * property: the predicate of the property. - * * editorName: the editor name: 'form', 'direct' or - * 'direct-with-wysiwyg'. * * widget: the parent EditableeEntity widget. * - toolbarId: the ID attribute of the toolbar as rendered in the DOM. */ @@ -44,10 +38,6 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ this.editor = options.editor; this.toolbarId = options.toolbarId; - this.entity = this.editor.options.entity; - this.predicate = this.editor.options.property; - this.editorName = this.editor.options.editorName; - this.$el.css('background-color', this._getBgColor(this.$el)); }, @@ -66,7 +56,7 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ if (from !== 'inactive') { this.stopHighlight(); if (from !== 'highlighted') { - this.stopEdit(this.editorName); + this.stopEdit(); } } break; @@ -74,16 +64,15 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ this.startHighlight(); break; case 'activating': - // NOTE: this step only exists for the 'form' editor! It is skipped by - // the 'direct' and 'direct-with-wysiwyg' editors, because no loading is - // necessary. - this.prepareEdit(this.editorName); + // NOTE: this state is not used by every editor! It's only used by those + // that need to interact with the server. + this.prepareEdit(); break; case 'active': - if (this.editorName !== 'form') { - this.prepareEdit(this.editorName); + if (from !== 'activating') { + this.prepareEdit(); } - this.startEdit(this.editorName); + this.startEdit(); break; case 'changed': break; @@ -130,7 +119,7 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ undecorate: function () { this.$el - .removeClass('edit-candidate edit-editable edit-highlighted edit-editing edit-belowoverlay'); + .removeClass('edit-candidate edit-editable edit-highlighted edit-editing'); }, startHighlight: function () { @@ -146,26 +135,22 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ .removeClass('edit-highlighted'); }, - prepareEdit: function(editorName) { + prepareEdit: function() { this.$el.addClass('edit-editing'); // While editing, don't show *any* other editors. // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133) // Revisit this. $('.edit-candidate').not('.edit-editing').removeClass('edit-editable'); - - if (editorName === 'form') { - this.$el.addClass('edit-belowoverlay'); - } }, - startEdit: function(editorName) { - if (editorName !== 'form') { + startEdit: function() { + if (this.getEditUISetting('padding')) { this._pad(); } }, - stopEdit: function(editorName) { + stopEdit: function() { this.$el.removeClass('edit-highlighted edit-editing'); // Make the other editors show up again. @@ -173,14 +158,20 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ // Revisit this. $('.edit-candidate').addClass('edit-editable'); - if (editorName === 'form') { - this.$el.removeClass('edit-belowoverlay'); - } - else { + if (this.getEditUISetting('padding')) { this._unpad(); } }, + /** + * Retrieves a setting of the editor-specific Edit UI integration. + * + * @see Drupal.edit.util.getEditUISetting(). + */ + getEditUISetting: function(setting) { + return Drupal.edit.util.getEditUISetting(this.editor, setting); + }, + _pad: function () { var self = this; diff --git a/core/modules/edit/js/views/toolbar-view.js b/core/modules/edit/js/views/toolbar-view.js index b60276b..90f5db7 100644 --- a/core/modules/edit/js/views/toolbar-view.js +++ b/core/modules/edit/js/views/toolbar-view.js @@ -40,8 +40,7 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({ * - editor: the editor object with an 'options' object that has these keys: * * entity: the VIE entity for the property. * * property: the predicate of the property. - * * editorName: the editor name: 'form', 'direct' or - * 'direct-with-wysiwyg'. + * * editorName: the editor name. * * element: the jQuery-wrapped editor DOM element * - $storageWidgetEl: the DOM element on which the Create Storage widget is * initialized. @@ -71,8 +70,8 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({ break; case 'candidate': if (from !== 'inactive') { - if (from !== 'highlighted' && this.editorName !== 'form') { - this._unpad(this.editorName); + if (from !== 'highlighted' && this.getEditUISetting('padding')) { + this._unpad(); } this.remove(); } @@ -86,12 +85,16 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({ this.setLoadingIndicator(true); break; case 'active': - this.startEdit(this.editorName); + this.startEdit(); this.setLoadingIndicator(false); - if (this.editorName !== 'form') { - this._pad(this.editorName); + if (this.getEditUISetting('fullWidthToolbar')) { + this.$el.addClass('edit-toolbar-fullwidth'); } - if (this.editorName === 'direct-with-wysiwyg') { + + if (this.getEditUISetting('padding')) { + this._pad(); + } + if (this.getEditUISetting('unifiedToolbar')) { this.insertWYSIWYGToolGroups(); } break; @@ -304,25 +307,33 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({ }, /** + * Retrieves a setting of the editor-specific Edit UI integration. + * + * @see Drupal.edit.util.getEditUISetting(). + */ + getEditUISetting: function(setting) { + return Drupal.edit.util.getEditUISetting(this.editor, setting); + }, + + /** * Adjusts the toolbar to accomodate padding on the PropertyEditor widget. * * @see PropertyEditorDecorationView._pad(). */ - _pad: function(editorName) { - // The whole toolbar must move to the top when the property's DOM element - // is displayed inline. - if (this.editor.element.css('display') === 'inline') { - this.$el.css('top', parseInt(this.$el.css('top'), 10) - 5 + 'px'); - } + _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') { + 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' }); - // When using a WYSIWYG editor, the width of the toolbar must match the - // width of the editable. - if (editorName === 'direct-with-wysiwyg') { - $hf.css({ width: this.editor.element.width() + 10 }); - } + // 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.editor.element.width() + 10 }); + } }, /** @@ -330,14 +341,14 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({ * * @see PropertyEditorDecorationView._unpad(). */ - _unpad: function(editorName) { - // Move the toolbar back to its original position. - var $hf = this.$el.find('.edit-toolbar-heightfaker'); - $hf.css({ bottom: '1px', left: '' }); - // When using a WYSIWYG editor, restore the width of the toolbar. - if (editorName === 'direct-with-wysiwyg') { - $hf.css({ width: '' }); - } + _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() { diff --git a/core/modules/edit/lib/Drupal/edit/EditBundle.php b/core/modules/edit/lib/Drupal/edit/EditBundle.php index d92fd73..f64c29b 100644 --- a/core/modules/edit/lib/Drupal/edit/EditBundle.php +++ b/core/modules/edit/lib/Drupal/edit/EditBundle.php @@ -20,18 +20,18 @@ class EditBundle extends Bundle { * Overrides Symfony\Component\HttpKernel\Bundle\Bundle::build(). */ public function build(ContainerBuilder $container) { - // Register the plugin managers for our plugin types with the dependency injection container. - $container->register('plugin.manager.edit.processed_text_editor', 'Drupal\edit\Plugin\ProcessedTextEditorManager'); + $container->register('plugin.manager.edit.editor', 'Drupal\edit\Plugin\EditorManager'); $container->register('access_check.edit.entity_field', 'Drupal\edit\Access\EditEntityFieldAccessCheck') ->addTag('access_check'); $container->register('edit.editor.selector', 'Drupal\edit\EditorSelector') - ->addArgument(new Reference('plugin.manager.edit.processed_text_editor')); + ->addArgument(new Reference('plugin.manager.edit.editor')); $container->register('edit.metadata.generator', 'Drupal\edit\MetadataGenerator') ->addArgument(new Reference('access_check.edit.entity_field')) - ->addArgument(new Reference('edit.editor.selector')); + ->addArgument(new Reference('edit.editor.selector')) + ->addArgument(new Reference('plugin.manager.edit.editor')); } } diff --git a/core/modules/edit/lib/Drupal/edit/EditController.php b/core/modules/edit/lib/Drupal/edit/EditController.php index 7b33ffe..16644ab 100644 --- a/core/modules/edit/lib/Drupal/edit/EditController.php +++ b/core/modules/edit/lib/Drupal/edit/EditController.php @@ -42,7 +42,7 @@ public function metadata(Request $request) { $metadata = array(); foreach ($fields as $field) { - list($entity_type, $entity_id, $field_name, $langcode, $view_mode) = explode(':', $field); + list($entity_type, $entity_id, $field_name, $langcode, $view_mode) = explode('/', $field); // Load the entity. if (!$entity_type || !entity_get_info($entity_type)) { diff --git a/core/modules/edit/lib/Drupal/edit/EditorBase.php b/core/modules/edit/lib/Drupal/edit/EditorBase.php new file mode 100644 index 0000000..d2e4d09 --- /dev/null +++ b/core/modules/edit/lib/Drupal/edit/EditorBase.php @@ -0,0 +1,26 @@ +processedTextEditorManager = $processed_text_editor_manager; + public function __construct(PluginManagerInterface $editor_manager) { + $this->editorManager = $editor_manager; } /** * Implements \Drupal\edit\EditorSelectorInterface::getEditor(). */ public function getEditor($formatter_type, FieldInstance $instance, array $items) { + // Build a static cache of the editors that have registered themselves as + // alternatives to a certain editor. + if (!isset($this->alternatives)) { + $editors = $this->editorManager->getDefinitions(); + foreach ($editors as $alternative_editor_id => $editor) { + if (isset($editor['alternativeTo'])) { + foreach ($editor['alternativeTo'] as $original_editor_id) { + $this->alternatives[$original_editor_id][] = $alternative_editor_id; + } + } + } + } + // Check if the formatter defines an appropriate in-place editor. For // example, text formatters displaying untrimmed text can choose to use the // 'direct' editor. If the formatter doesn't specify, fall back to the // 'form' editor, since that can work for any field. Formatter definitions // can use 'disabled' to explicitly opt out of in-place editing. $formatter_info = field_info_formatter_types($formatter_type); - $editor = isset($formatter_info['edit']['editor']) ? $formatter_info['edit']['editor'] : 'form'; - if ($editor == 'disabled') { + $editor_id = isset($formatter_info['edit']['editor']) ? $formatter_info['edit']['editor'] : 'form'; + if ($editor_id === 'disabled') { return; } + elseif ($editor_id === 'form') { + return 'form'; + } - // The same text formatters can be used for single-valued and multivalued - // fields and for processed and unprocessed text, so we can't rely on the - // formatter definition for the final determination, because: - // - The direct editor does not work for multivalued fields. - // - Processed text can benefit from a WYSIWYG editor. - // - Empty processed text without an already selected format requires a form - // to select one. - // @todo The processed text logic is too coupled to text fields. Figure out - // how to generalize to other textual field types. - // @todo All of this might hint at formatter *definitions* not being the - // ideal place for editor specification. Moving the determination to - // something that works with instantiated formatters, not just their - // definitions, could alleviate that, but might come with its own - // challenges. - if ($editor == 'direct') { - $field = field_info_field($instance['field_name']); - if ($field['cardinality'] != 1) { - // The direct editor does not work for multivalued fields. - $editor = 'form'; - } - elseif (!empty($instance['settings']['text_processing'])) { - $format_id = $items[0]['format']; - if (isset($format_id)) { - $wysiwyg_plugin = $this->getProcessedTextEditorPlugin(); - if (isset($wysiwyg_plugin) && $wysiwyg_plugin->checkFormatCompatibility($format_id)) { - // Yay! Even though the text is processed, there's a WYSIWYG editor - // that can work with it. - $editor = 'direct-with-wysiwyg'; - } - else { - // @todo We might not have to downgrade all the way to 'form'. The - // 'direct' editor might be appropriate for some kinds of - // processed text. - $editor = 'form'; - } - } - else { - // If a format is not yet selected, a form is needed to select one. - $editor = 'form'; - } + // No early return, so create a list of all choices. + $editor_choices = array($editor_id); + if (isset($this->alternatives[$editor_id])) { + $editor_choices = array_merge($editor_choices, $this->alternatives[$editor_id]); + } + + // Make a choice. + foreach ($editor_choices as $editor_id) { + $editor = $this->editorManager->createInstance($editor_id); + if ($editor->isCompatible($instance, $items)) { + return $editor_id; } } - return $editor; + // We still don't have a choice, so fall back to the default 'form' editor. + return 'form'; } /** * Implements \Drupal\edit\EditorSelectorInterface::getAllEditorAttachments(). + * + * @todo Instead of loading all JS/CSS for all editors, load them lazily when + * needed. + * @todo The NestedArray stuff is wonky. */ public function getAllEditorAttachments() { - $this->getProcessedTextEditorPlugin(); - if (!isset($this->processedTextEditorPlugin)) { - return array(); + $attachments = array(); + $definitions = $this->editorManager->getDefinitions(); + + // Editor plugins' attachments. + $editor_ids = array_keys($definitions); + foreach ($editor_ids as $editor_id) { + $editor = $this->editorManager->createInstance($editor_id); + $attachments[] = $editor->getAttachments();; } - $js = array(); - - // Add library and settings for the selected processed text editor plugin. - $definition = $this->processedTextEditorPlugin->getDefinition(); - if (!empty($definition['library'])) { - $js['library'][] = array($definition['library']['module'], $definition['library']['name']); - } - $this->processedTextEditorPlugin->addJsSettings(); - - // Also add the setting to register it with Create.js - if (!empty($definition['propertyEditorName'])) { - $js['js'][] = array( - 'data' => array( - 'edit' => array( - 'wysiwygEditorWidgetName' => $definition['propertyEditorName'], - ), - ), - 'type' => 'setting' + // JavaScript settings for Edit. + foreach ($definitions as $definition) { + $attachments[] = array( + // This will be used in Create.js' propertyEditorWidgetsConfiguration. + 'js' => array( + array( + 'type' => 'setting', + 'data' => array('edit' => array('editors' => array( + $definition['id'] => array('widget' => $definition['jsClassName']) + ))) + ) + ) ); } - return $js; - } - - /** - * Returns the plugin to use for the 'direct-with-wysiwyg' editor. - * - * @return \Drupal\edit\Plugin\ProcessedTextEditorInterface - * The editor plugin. - * - * @todo We currently only support one plugin (the first one returned by the - * manager) for the 'direct-with-wysiwyg' editor on any given page. Enhance - * this to allow different ones per element (e.g., Aloha for one text field - * and CKEditor for another one). - * - * @todo The terminology here is confusing. 'direct-with-wysiwyg' is one of - * several possible "editor"s for processed text. When using it, we need to - * integrate a particular WYSIWYG editor, which in Create.js is called a - * "PropertyEditor widget", but we're not yet including "widget" in the name - * of ProcessedTextEditorInterface to minimize confusion with Field API - * widgets. So, we're currently refering to these as "plugins", which is - * correct in that it's using Drupal's Plugin API, but less informative than - * naming it "widget" or similar. - */ - protected function getProcessedTextEditorPlugin() { - if (!isset($this->processedTextEditorPlugin)) { - $definitions = $this->processedTextEditorManager->getDefinitions(); - if (count($definitions)) { - $plugin_ids = array_keys($definitions); - $plugin_id = $plugin_ids[0]; - $this->processedTextEditorPlugin = $this->processedTextEditorManager->createInstance($plugin_id); - } - } - return $this->processedTextEditorPlugin; + return NestedArray::mergeDeepArray($attachments); } } diff --git a/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php b/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php index 5a7da04..29a01cb 100644 --- a/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php +++ b/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php @@ -8,6 +8,7 @@ namespace Drupal\edit; use Drupal\Core\Entity\EntityInterface; +use Drupal\Component\Plugin\PluginManagerInterface; use Drupal\field\FieldInstance; use Drupal\edit\Access\EditEntityFieldAccessCheckInterface; @@ -32,16 +33,26 @@ class MetadataGenerator implements MetadataGeneratorInterface { protected $editorSelector; /** + * The manager for editor (Create.js PropertyEditor widget) plugins. + * + * @var \Drupal\Component\Plugin\PluginManagerInterface + */ + protected $editorManager; + + /** * Constructs a new MetadataGenerator. * * @param \Drupal\edit\Access\EditEntityFieldAccessCheckInterface $access_checker * An object that checks if a user has access to edit a given field. * @param \Drupal\edit\EditorSelectorInterface $editor_selector * An object that determines which editor to attach to a given field. + * @param \Drupal\Component\Plugin\PluginManagerInterface + * The manager for editor plugins. */ - public function __construct(EditEntityFieldAccessCheckInterface $access_checker, EditorSelectorInterface $editor_selector) { + public function __construct(EditEntityFieldAccessCheckInterface $access_checker, EditorSelectorInterface $editor_selector, PluginManagerInterface $editor_manager) { $this->accessChecker = $access_checker; $this->editorSelector = $editor_selector; + $this->editorManager = $editor_manager; } /** @@ -56,31 +67,30 @@ public function generate(EntityInterface $entity, FieldInstance $instance, $lang return array('access' => FALSE); } - $label = $instance['label']; + // Early-return if no editor is available. $formatter_id = entity_get_render_display($entity, $view_mode)->getFormatter($instance['field_name'])->getPluginId(); $items = $entity->get($field_name); $items = $items[$langcode]; - $editor = $this->editorSelector->getEditor($formatter_id, $instance, $items); + $editor_id = $this->editorSelector->getEditor($formatter_id, $instance, $items); + if (!isset($editor_id)) { + return array('access' => FALSE); + } + + // Gather metadata, allow the editor to add additional metadata of its own. + $label = $instance['label']; + $editor = $this->editorManager->createInstance($editor_id); $metadata = array( 'label' => $label, 'access' => TRUE, - 'editor' => $editor, + 'editor' => $editor_id, 'aria' => t('Entity @type @id, field @field', array('@type' => $entity->entityType(), '@id' => $entity->id(), '@field' => $label)), ); - // Additional metadata for WYSIWYG editor integration. - if ($editor === 'direct-with-wysiwyg') { - $format_id = $items[0]['format']; - $metadata['format'] = $format_id; - $metadata['formatHasTransformations'] = $this->textFormatHasTransformationFilters($format_id); + $custom_metadata = $editor->getMetadata($instance, $items); + if (count($custom_metadata)) { + $metadata['custom'] = $custom_metadata; } - return $metadata; - } - /** - * Returns whether the text format has transformation filters. - */ - protected function textFormatHasTransformationFilters($format_id) { - return (bool) count(array_intersect(array(FILTER_TYPE_TRANSFORM_REVERSIBLE, FILTER_TYPE_TRANSFORM_IRREVERSIBLE), filter_get_filter_types_by_format($format_id))); + return $metadata; } } diff --git a/core/modules/edit/lib/Drupal/edit/MetadataGeneratorInterface.php b/core/modules/edit/lib/Drupal/edit/MetadataGeneratorInterface.php index 9e4fb5d..ff052d7 100644 --- a/core/modules/edit/lib/Drupal/edit/MetadataGeneratorInterface.php +++ b/core/modules/edit/lib/Drupal/edit/MetadataGeneratorInterface.php @@ -20,7 +20,7 @@ * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity being edited. - * @param Drupal\field\FieldInstance $instance + * @param \Drupal\field\FieldInstance $instance * The field instance of the field being edited. * @param string $langcode * The name of the language for which the field is being edited. diff --git a/core/modules/edit/lib/Drupal/edit/Plugin/EditorManager.php b/core/modules/edit/lib/Drupal/edit/Plugin/EditorManager.php new file mode 100644 index 0000000..66876a6 --- /dev/null +++ b/core/modules/edit/lib/Drupal/edit/Plugin/EditorManager.php @@ -0,0 +1,48 @@ +discovery = new AnnotatedClassDiscovery('edit', 'editor'); + $this->discovery = new ProcessDecorator($this->discovery, array($this, 'processDefinition')); + $this->discovery = new AlterDecorator($this->discovery, 'edit_editor'); + $this->discovery = new CacheDecorator($this->discovery, 'edit:editor'); + $this->factory = new DefaultFactory($this->discovery); + } + + /** + * Overrides \Drupal\Component\Plugin\PluginManagerBase::processDefinition(). + */ + public function processDefinition(&$definition, $plugin_id) { + parent::processDefinition($definition, $plugin_id); + + // @todo Remove this check once http://drupal.org/node/1780396 is resolved. + if (!module_exists($definition['module'])) { + $definition = NULL; + return; + } + } + +} diff --git a/core/modules/edit/lib/Drupal/edit/Plugin/ProcessedTextEditorBase.php b/core/modules/edit/lib/Drupal/edit/Plugin/ProcessedTextEditorBase.php deleted file mode 100644 index 6e1cace..0000000 --- a/core/modules/edit/lib/Drupal/edit/Plugin/ProcessedTextEditorBase.php +++ /dev/null @@ -1,26 +0,0 @@ -discovery = new AnnotatedClassDiscovery('edit', 'processed_text_editor'); - $this->discovery = new ProcessDecorator($this->discovery, array($this, 'processDefinition')); - $this->discovery = new AlterDecorator($this->discovery, 'edit_wysiwyg'); - $this->discovery = new CacheDecorator($this->discovery, 'edit:wysiwyg'); - $this->factory = new DefaultFactory($this->discovery); - } - - /** - * Overrides Drupal\Component\Plugin\PluginManagerBase::processDefinition(). - */ - public function processDefinition(&$definition, $plugin_id) { - parent::processDefinition($definition, $plugin_id); - - // @todo Remove this check once http://drupal.org/node/1780396 is resolved. - if (!module_exists($definition['module'])) { - $definition = NULL; - return; - } - } - -} diff --git a/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/DirectEditor.php b/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/DirectEditor.php new file mode 100644 index 0000000..0a386c5 --- /dev/null +++ b/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/DirectEditor.php @@ -0,0 +1,57 @@ + array( + array('edit', 'edit.editor.direct'), + ), + ); + } +} diff --git a/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/FormEditor.php b/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/FormEditor.php new file mode 100644 index 0000000..59e8d67 --- /dev/null +++ b/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/FormEditor.php @@ -0,0 +1,43 @@ + array( + array('edit', 'edit.editor.form'), + ), + ); + } + +} diff --git a/core/modules/edit/lib/Drupal/edit/Tests/EditTestBase.php b/core/modules/edit/lib/Drupal/edit/Tests/EditTestBase.php index 573ce93..18d92d8 100644 --- a/core/modules/edit/lib/Drupal/edit/Tests/EditTestBase.php +++ b/core/modules/edit/lib/Drupal/edit/Tests/EditTestBase.php @@ -2,7 +2,7 @@ /** * @file - * Definition of Drupal\edit\Tests\EditTestBase. + * Contains \Drupal\edit\Tests\EditTestBase. */ namespace Drupal\edit\Tests; @@ -20,7 +20,7 @@ class EditTestBase extends DrupalUnitTestBase { * * @var array */ - public static $modules = array('system', 'entity', 'field_test', 'field', 'number', 'text', 'edit', 'edit_test'); + public static $modules = array('system', 'entity', 'field_test', 'field', 'number', 'text', 'edit'); /** * Sets the default field storage backend for fields created during tests. diff --git a/core/modules/edit/lib/Drupal/edit/Tests/EditorSelectionTest.php b/core/modules/edit/lib/Drupal/edit/Tests/EditorSelectionTest.php index 5217b92..199a525 100644 --- a/core/modules/edit/lib/Drupal/edit/Tests/EditorSelectionTest.php +++ b/core/modules/edit/lib/Drupal/edit/Tests/EditorSelectionTest.php @@ -2,12 +2,12 @@ /** * @file - * Definition of Drupal\edit\Tests\EditorSelectionTest. + * Contains \Drupal\edit\Tests\EditorSelectionTest. */ namespace Drupal\edit\Tests; -use Drupal\edit\Plugin\ProcessedTextEditorManager; +use Drupal\edit\Plugin\EditorManager; use Drupal\edit\EditorSelector; /** @@ -16,6 +16,13 @@ class EditorSelectionTest extends EditTestBase { /** + * The manager for editor (Create.js PropertyEditor widget) plugins. + * + * @var \Drupal\Component\Plugin\PluginManagerInterface + */ + protected $editorManager; + + /** * The editor selector object to be tested. * * @var \Drupal\edit\EditorSelectorInterface @@ -33,12 +40,8 @@ public static function getInfo() { function setUp() { parent::setUp(); - // @todo Rather than using the real ProcessedTextEditorManager, which can - // find all text editor plugins in the codebase, create a mock one for - // testing that is populated with only the ones we want to test. - $text_editor_manager = new ProcessedTextEditorManager(); - - $this->editorSelector = new EditorSelector($text_editor_manager); + $this->editorManager = new EditorManager(); + $this->editorSelector = new EditorSelector($this->editorManager); } /** @@ -99,10 +102,14 @@ function testText() { /** * Tests a textual field, with text processing, with cardinality 1 and >1, - * always with a ProcessedTextEditor plug-in present, but with varying text - * format compatibility. + * always with an Editor plugin present that supports textual fields with text + * processing, but with varying text format compatibility. */ function testTextWysiwyg() { + // Enable edit_test module so that the 'wysiwyg' Create.js PropertyEditor + // widget becomes available. + $this->enableModules(array('edit_test'), FALSE); + $field_name = 'field_textarea'; $this->createFieldWithInstance( $field_name, 'text', 1, 'Long text field', @@ -116,18 +123,15 @@ function testTextWysiwyg() { array() ); - // ProcessedTextEditor plug-in compatible with the full_html text format. - state()->set('edit_test.compatible_format', 'full_html'); - // Pretend there is an entity with these items for the field. $items = array(array('value' => 'Hello, world!', 'format' => 'filtered_html')); - // Editor selection with cardinality 1, without compatible text format. - $this->assertEqual('form', $this->getSelectedEditor($items, $field_name), "Without cardinality 1, and the filtered_html text format, the 'form' editor is selected."); + // Editor selection w/ cardinality 1, text format w/o associated text editor. + $this->assertEqual('form', $this->getSelectedEditor($items, $field_name), "With cardinality 1, and the filtered_html text format, the 'form' editor is selected."); - // Editor selection with cardinality 1, with compatible text format. + // Editor selection w/ cardinality 1, text format w/ associated text editor. $items[0]['format'] = 'full_html'; - $this->assertEqual('direct-with-wysiwyg', $this->getSelectedEditor($items, $field_name), "With cardinality 1, and the full_html text format, the 'direct-with-wysiwyg' editor is selected."); + $this->assertEqual('wysiwyg', $this->getSelectedEditor($items, $field_name), "With cardinality 1, and the full_html text format, the 'wysiwyg' editor is selected."); // Editor selection with text processing, cardinality >1 $this->field_textarea_field['cardinality'] = 2; diff --git a/core/modules/edit/lib/Drupal/edit/Tests/MetadataGeneratorTest.php b/core/modules/edit/lib/Drupal/edit/Tests/MetadataGeneratorTest.php index e8060ea..17bc891 100644 --- a/core/modules/edit/lib/Drupal/edit/Tests/MetadataGeneratorTest.php +++ b/core/modules/edit/lib/Drupal/edit/Tests/MetadataGeneratorTest.php @@ -2,14 +2,14 @@ /** * @file - * Definition of Drupal\edit\Tests\MetadataGeneratorTest. + * Contains \Drupal\edit\Tests\MetadataGeneratorTest. */ namespace Drupal\edit\Tests; use Drupal\edit\EditorSelector; use Drupal\edit\MetadataGenerator; -use Drupal\edit\Plugin\ProcessedTextEditorManager; +use Drupal\edit\Plugin\EditorManager; use Drupal\edit_test\MockEditEntityFieldAccessCheck; /** @@ -18,6 +18,13 @@ class MetadataGeneratorTest extends EditTestBase { /** + * The manager for editor (Create.js PropertyEditor widget) plugins. + * + * @var \Drupal\Component\Plugin\PluginManagerInterface + */ + protected $editorManager; + + /** * The metadata generator object to be tested. * * @var \Drupal\edit\MetadataGeneratorInterface.php @@ -49,14 +56,10 @@ public static function getInfo() { function setUp() { parent::setUp(); - // @todo Rather than using the real ProcessedTextEditorManager, which can - // find all text editor plugins in the codebase, create a mock one for - // testing that is populated with only the ones we want to test. - $text_editor_manager = new ProcessedTextEditorManager(); - + $this->editorManager = new EditorManager(); $this->accessChecker = new MockEditEntityFieldAccessCheck(); - $this->editorSelector = new EditorSelector($text_editor_manager); - $this->metadataGenerator = new MetadataGenerator($this->accessChecker, $this->editorSelector); + $this->editorSelector = new EditorSelector($this->editorManager); + $this->metadataGenerator = new MetadataGenerator($this->accessChecker, $this->editorSelector, $this->editorManager); } /** @@ -119,6 +122,61 @@ function testSimpleEntityType() { 'aria' => 'Entity test_entity 1, field Simple number field', ); $this->assertEqual($expected_2, $metadata_2, 'The correct metadata is generated for the second field.'); + } + + function testEditorWithCustomMetadata() { + $this->enableModules(array('filter')); + // Enable edit_test module so that the WYSIWYG Create.js PropertyEditor + // widget becomes available. + $this->enableModules(array('edit_test'), FALSE); + + // Create a rich text field. + $field_name = 'field_rich'; + $field_label = 'Rich text field'; + $this->createFieldWithInstance( + $field_name, 'text', 1, $field_label, + // Instance settings. + array('text_processing' => 1), + // Widget type & settings. + 'text_textfield', + array('size' => 42), + // 'default' formatter type & settings. + 'text_default', + array() + ); + + // Create a text format. + $full_html_format = array( + 'format' => 'full_html', + 'name' => 'Full HTML', + 'weight' => 1, + 'filters' => array( + 'filter_htmlcorrector' => array('status' => 1), + ), + ); + $full_html_format = (object) $full_html_format; + filter_format_save($full_html_format); + + // Create an entity with values for this rich text field. + $this->entity = field_test_create_entity(); + $this->is_new = TRUE; + $this->entity->{$field_name}[LANGUAGE_NOT_SPECIFIED] = array(array('value' => 'Test', 'format' => 'full_html')); + field_test_entity_save($this->entity); + $entity = entity_load('test_entity', $this->entity->ftid); + + // Verify metadata. + $instance = field_info_instance($entity->entityType(), $field_name, $entity->bundle()); + $metadata = $this->metadataGenerator->generate($entity, $instance, LANGUAGE_NOT_SPECIFIED, 'default'); + $expected = array( + 'access' => TRUE, + 'label' => 'Rich text field', + 'editor' => 'wysiwyg', + 'aria' => 'Entity test_entity 1, field Rich text field', + 'custom' => array( + 'format' => 'full_html' + ), + ); + $this->assertEqual($expected, $metadata, 'The correct metadata (including custom metadata) is generated.'); } } diff --git a/core/modules/edit/tests/modules/lib/Drupal/edit_test/Plugin/edit/editor/WysiwygEditor.php b/core/modules/edit/tests/modules/lib/Drupal/edit_test/Plugin/edit/editor/WysiwygEditor.php new file mode 100644 index 0000000..943848f --- /dev/null +++ b/core/modules/edit/tests/modules/lib/Drupal/edit_test/Plugin/edit/editor/WysiwygEditor.php @@ -0,0 +1,67 @@ + array( + array('edit_test', 'not-existing-wysiwyg'), + ), + ); + } +} diff --git a/core/modules/edit/tests/modules/lib/Drupal/edit_test/Plugin/edit/processed_text_editor/TestProcessedEditor.php b/core/modules/edit/tests/modules/lib/Drupal/edit_test/Plugin/edit/processed_text_editor/TestProcessedEditor.php deleted file mode 100644 index b43e77f..0000000 --- a/core/modules/edit/tests/modules/lib/Drupal/edit_test/Plugin/edit/processed_text_editor/TestProcessedEditor.php +++ /dev/null @@ -1,32 +0,0 @@ -get('edit_test.compatible_format') == $format_id; - } - -}