core/modules/editor/editor.module | 15 ++ core/modules/editor/js/editor.createjs.js | 151 +++++++++++++++++ .../Drupal/editor/Plugin/edit/editor/Editor.php | 100 +++++++++++ .../Drupal/editor/Tests/EditIntegrationTest.php | 175 ++++++++++++++++++++ .../Plugin/editor/editor/UnicornEditor.php | 3 +- 5 files changed, 443 insertions(+), 1 deletion(-) diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module index a40c249..4151945 100644 --- a/core/modules/editor/editor.module +++ b/core/modules/editor/editor.module @@ -78,6 +78,21 @@ function editor_library_info() { array('system', 'jquery.once'), ), ); + // Create.js PropertyEditor widget library names begin with "edit.editor". + $libraries['edit.editor.wysiwyg'] = array( + 'title' => '"Editor" Create.js PropertyEditor widget', + 'version' => VERSION, + 'js' => array( + $path . '/js/editor.createjs.js' => array( + 'scope' => 'footer', + 'attributes' => array('defer' => TRUE), + ) + ), + 'dependencies' => array( + array('edit', 'edit'), + array('editor', 'drupal.editor'), + ), + ); return $libraries; } diff --git a/core/modules/editor/js/editor.createjs.js b/core/modules/editor/js/editor.createjs.js new file mode 100644 index 0000000..56780dd --- /dev/null +++ b/core/modules/editor/js/editor.createjs.js @@ -0,0 +1,151 @@ +/** + * @file + * Text editor-based Create.js widget for processed text content in Drupal. + * + * Depends on Editor.module. Works with any (WYSIWYG) editor that implements the + * attachInlineEditor(), detach() and onChange() methods. + */ +(function (jQuery, Drupal, drupalSettings) { + +"use strict"; + + jQuery.widget('Drupal.drupalWysiwygWidget', jQuery.Create.editWidget, { + + textFormat: null, + textFormatHasTransformations: null, + textEditor: null, + + /** + * Implements getEditUISettings() method. + */ + getEditUISettings: function() { + return { padding: true, unifiedToolbar: true, fullWidthToolbar: true }; + }, + + /** + * 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 propertyID = Drupal.edit.util.calcPropertyID(this.options.entity, this.options.property); + var metadata = Drupal.edit.metadataCache[propertyID].custom; + + this.textFormat = drupalSettings.editor.formats[metadata.format]; + this.textFormatHasTransformations = metadata.formatHasTransformations; + this.textEditor = Drupal.editors[this.textFormat.editor]; + + this._bindEvents(); + }, + + /** + * Binds to events. + */ + _bindEvents: function() { + var that = this; + + // Sets the state to 'activated' upon clicking the element. + this.element.on('click.edit', function(event) { + event.stopPropagation(); + event.preventDefault(); + that.options.activating(); + }); + }, + + /** + * Makes this PropertyEditor widget react to state changes. + */ + stateChange: function(from, to) { + var that = this; + switch (to) { + case 'inactive': + break; + case 'candidate': + if (from !== 'inactive') { + if (from !== 'highlighted') { + this.element.attr('contentEditable', 'false'); + this.textEditor.detach(this.element.get(0), this.textFormat); + } + + this._removeValidationErrors(); + this._cleanUp(); + this._bindEvents(); + } + break; + case 'highlighted': + break; + case 'activating': + // When transformation filters have been been applied to the processed + // text of this field, then we'll need to load a re-rendered version of + // it without the transformation filters. + if (this.textFormatHasTransformations) { + Drupal.edit.util.loadRerenderedProcessedText({ + $editorElement: this.element, + propertyID: Drupal.edit.util.calcPropertyID(this.options.entity, this.options.property), + callback: function (rerendered) { + that.element.html(rerendered); + that.options.activated(); + } + }); + } + // When no transformation filters have been applied: start WYSIWYG + // editing immediately! + else { + this.options.activated(); + } + break; + case 'active': + this.element.attr('contentEditable', 'true'); + this.textEditor.attachInlineEditor( + this.element.get(0), + this.textFormat, + this.toolbarView.getMainWysiwygToolgroupId(), + this.toolbarView.getFloatedWysiwygToolgroupId() + ); + + // Sets the state to 'changed' whenever the content has changed. + this.textEditor.onChange(this.element.get(0), function (html) { + that.options.changed(html); + }); + break; + case 'changed': + break; + case 'saving': + this._removeValidationErrors(); + break; + case 'saved': + break; + case 'invalid': + break; + } + }, + + /** + * Removes validation errors' markup changes, if any. + * + * @todo: this should not be necessary, will be obviated by edit.module. + */ + _removeValidationErrors: function() { + this.element + .removeClass('edit-validation-error') + .next('.edit-validation-errors').remove(); + }, + + /** + * Cleans up after the widget has been saved. + * + * @todo: this should not be necessary, will be obviated by edit.module. + */ + _cleanUp: function() { + Drupal.edit.util.form.unajaxifySaving(jQuery('#edit_backstage form .edit-form-submit')); + jQuery('#edit_backstage form').remove(); + } + }); + +})(jQuery, Drupal, drupalSettings); diff --git a/core/modules/editor/lib/Drupal/editor/Plugin/edit/editor/Editor.php b/core/modules/editor/lib/Drupal/editor/Plugin/edit/editor/Editor.php new file mode 100644 index 0000000..5aee9ef --- /dev/null +++ b/core/modules/editor/lib/Drupal/editor/Plugin/edit/editor/Editor.php @@ -0,0 +1,100 @@ +get('plugin.manager.editor')->getDefinition($editor->editor); + if ($definition['supports_inline_editing'] === TRUE) { + return TRUE; + } + } + + return FALSE; + } + } + + /** + * Implements \Drupal\edit\Plugin\EditorInterface::getMetadata(). + */ + function getMetadata(FieldInstance $instance, array $items) { + $format_id = $items[0]['format']; + $metadata['format'] = $format_id; + $metadata['formatHasTransformations'] = $this->textFormatHasTransformationFilters($format_id); + 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))); + } + + /** + * Implements \Drupal\edit\EditorInterface::getAttachments(). + */ + public function getAttachments() { + global $user; + + $user_format_ids = array_keys(filter_formats($user)); + $manager = drupal_container()->get('plugin.manager.editor'); + $definitions = $manager->getDefinitions(); + + // Filter the current user's text to those that support inline editing. + $formats = array(); + foreach ($user_format_ids as $format_id) { + $editor = editor_load($format_id); + if ($editor && isset($definitions[$editor->editor]) && isset($definitions[$editor->editor]['supports_inline_editing']) && $definitions[$editor->editor]['supports_inline_editing'] === TRUE) { + $formats[] = $format_id; + } + } + + // Get the attachments for all text editors that the user might use. + $attachments = $manager->getAttachments($formats); + + // Also include editor.module's Create.js PropertyEditor widget. + $attachments['library'][] = array('editor', 'edit.editor.wysiwyg'); + + return $attachments; + } + +} diff --git a/core/modules/editor/lib/Drupal/editor/Tests/EditIntegrationTest.php b/core/modules/editor/lib/Drupal/editor/Tests/EditIntegrationTest.php new file mode 100644 index 0000000..875f1ef --- /dev/null +++ b/core/modules/editor/lib/Drupal/editor/Tests/EditIntegrationTest.php @@ -0,0 +1,175 @@ + 'In-place text editors (Edit module integration)', + 'description' => 'Tests Edit module integration (Editor module\'s inline editing support).', + 'group' => 'Text Editor', + ); + } + + function setUp() { + parent::setUp(); + + // Install the Filter module. + $this->installSchema('system', 'url_alias'); + $this->enableModules(array('user', 'filter')); + + // Enable the Text Editor and Text Editor Test module. + $this->enableModules(array('editor', 'editor_test'), FALSE); + + // Create a field. + $this->field_name = 'field_textarea'; + $this->createFieldWithInstance( + $this->field_name, 'text', 1, 'Long text field', + // Instance settings. + array('text_processing' => 1), + // Widget type & settings. + 'text_textarea', + array('size' => 42), + // 'default' formatter type & settings. + 'text_default', + array() + ); + + // Create text format. + $full_html_format = entity_create('filter_format', array( + 'format' => 'full_html', + 'name' => 'Full HTML', + 'weight' => 1, + 'filters' => array(), + )); + $full_html_format->save(); + + // Associate text editor with text format. + $editor = entity_create('editor', array( + 'format' => $full_html_format->format, + 'editor' => 'unicorn', + )); + $editor->save(); + } + + /** + * Retrieves the FieldInstance object for the given field and returns the + * editor that Edit selects. + */ + protected function getSelectedEditor($items, $field_name, $view_mode = 'default') { + $options = entity_get_display('test_entity', 'test_bundle', $view_mode)->getComponent($field_name); + $field_instance = field_info_instance('test_entity', $field_name, 'test_bundle'); + return $this->editorSelector->getEditor($options['type'], $field_instance, $items); + } + + /** + * Tests editor selection when the Editor module is present. + * + * 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. + */ + function testEditorSelection() { + $this->editorManager = new EditorManager(); + $this->editorSelector = new EditorSelector($this->editorManager); + + // Pretend there is an entity with these items for the field. + $items = array(array('value' => 'Hello, world!', 'format' => 'filtered_html')); + + // Editor selection w/ cardinality 1, text format w/o associated text editor. + $this->assertEqual('form', $this->getSelectedEditor($items, $this->field_name), "With cardinality 1, and the filtered_html text format, the 'form' editor is selected."); + + // Editor selection w/ cardinality 1, text format w/ associated text editor. + $items[0]['format'] = 'full_html'; + $this->assertEqual('editor', $this->getSelectedEditor($items, $this->field_name), "With cardinality 1, and the full_html text format, the 'editor' editor is selected."); + + // Editor selection with text processing, cardinality >1 + $this->field_textarea_field['cardinality'] = 2; + field_update_field($this->field_textarea_field); + $items[] = array('value' => 'Hallo, wereld!', 'format' => 'full_html'); + $this->assertEqual('form', $this->getSelectedEditor($items, $this->field_name), "With cardinality >1, and both items using the full_html text format, the 'form' editor is selected."); + } + + /** + * Tests (custom) metadata when the "Editor" Create.js editor is used. + */ + function testMetadata() { + $this->editorManager = new EditorManager(); + $this->accessChecker = new MockEditEntityFieldAccessCheck(); + $this->editorSelector = new EditorSelector($this->editorManager); + $this->metadataGenerator = new MetadataGenerator($this->accessChecker, $this->editorSelector, $this->editorManager); + + // Create an entity with values for the field. + $this->entity = field_test_create_entity(); + $this->is_new = TRUE; + $this->entity->{$this->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(), $this->field_name, $entity->bundle()); + $metadata = $this->metadataGenerator->generate($entity, $instance, LANGUAGE_NOT_SPECIFIED, 'default'); + $expected = array( + 'access' => TRUE, + 'label' => 'Long text field', + 'editor' => 'editor', + 'aria' => 'Entity test_entity 1, field Long text field', + 'custom' => array( + 'format' => 'full_html', + 'formatHasTransformations' => FALSE, + ), + ); + $this->assertEqual($expected, $metadata, 'The correct metadata (including custom metadata) is generated.'); + } + +} diff --git a/core/modules/editor/tests/modules/lib/Drupal/editor_test/Plugin/editor/editor/UnicornEditor.php b/core/modules/editor/tests/modules/lib/Drupal/editor_test/Plugin/editor/editor/UnicornEditor.php index 6a0fbaa..4ba8fac 100644 --- a/core/modules/editor/tests/modules/lib/Drupal/editor_test/Plugin/editor/editor/UnicornEditor.php +++ b/core/modules/editor/tests/modules/lib/Drupal/editor_test/Plugin/editor/editor/UnicornEditor.php @@ -18,7 +18,8 @@ * @Plugin( * id = "unicorn", * label = @Translation("Unicorn Editor"), - * module = "editor_test" + * module = "editor_test", + * supports_inline_editing = TRUE * ) */ class UnicornEditor extends EditorBase {