core/modules/ckeditor/js/ckeditor.js | 73 +++++-- .../ckeditor/Plugin/editor/editor/CKEditor.php | 3 +- core/modules/edit/edit.module | 5 +- core/modules/edit/edit.routing.yml | 8 - core/modules/edit/js/backbone.drupalform.js | 22 ++- .../editingWidgets/drupalcontenteditablewidget.js | 36 +--- .../edit/js/createjs/editingWidgets/formwidget.js | 4 +- core/modules/edit/js/util.js | 35 ---- .../edit/js/views/propertyeditordecoration-view.js | 30 ++- core/modules/edit/js/views/toolbar-view.js | 6 + ...RenderedWithoutTransformationFiltersCommand.php | 28 --- .../edit/lib/Drupal/edit/EditController.php | 28 --- .../edit/Plugin/edit/editor/DirectEditor.php | 4 +- .../Drupal/edit/Plugin/edit/editor/FormEditor.php | 4 +- .../edit/lib/Drupal/edit/Tests/EditTestBase.php | 2 +- .../Drupal/edit/Tests/MetadataGeneratorTest.php | 2 - core/modules/editor/editor.module | 37 ++++ core/modules/editor/editor.routing.yml | 7 + core/modules/editor/js/editor.createjs.js | 157 +++++++++++++++ .../editor/Ajax/GetUntransformedTextCommand.php | 29 +++ .../editor/lib/Drupal/editor/EditorController.php | 47 +++++ .../Drupal/editor/Plugin/edit/editor/Editor.php | 100 ++++++++++ .../Drupal/editor/Tests/EditIntegrationTest.php | 200 ++++++++++++++++++++ .../Plugin/editor/editor/UnicornEditor.php | 3 +- 24 files changed, 711 insertions(+), 159 deletions(-) diff --git a/core/modules/ckeditor/js/ckeditor.js b/core/modules/ckeditor/js/ckeditor.js index b8318be..f7f0e0e 100644 --- a/core/modules/ckeditor/js/ckeditor.js +++ b/core/modules/ckeditor/js/ckeditor.js @@ -1,20 +1,11 @@ -(function (Drupal, CKEDITOR) { +(function (Drupal, CKEDITOR, $) { "use strict"; Drupal.editors.ckeditor = { attach: function (element, format) { - var externalPlugins = format.editorSettings.externalPlugins; - // Register and load additional CKEditor plugins as necessary. - if (externalPlugins) { - for (var pluginName in externalPlugins) { - if (externalPlugins.hasOwnProperty(pluginName)) { - CKEDITOR.plugins.addExternal(pluginName, externalPlugins[pluginName], ''); - } - } - delete format.editorSettings.drupalExternalPlugins; - } + this._loadExternalPlugins(format); return !!CKEDITOR.replace(element, format.editorSettings); }, @@ -26,11 +17,69 @@ Drupal.editors.ckeditor = { } else { editor.destroy(); + element.removeAttribute('contentEditable'); } } return !!editor; + }, + + onChange: function (element, callback) { + var editor = CKEDITOR.dom.element.get(element).getEditor(); + if (editor) { + var changed = function () { + callback(editor.getData()); + }; + // @todo Make this more elegant once http://dev.ckeditor.com/ticket/9794 + // is fixed. + editor.on('key', changed); + editor.on('paste', changed); + editor.on('afterCommandExec', changed); + } + return !!editor; + }, + + attachInlineEditor: function (element, format, mainToolbarId, floatedToolbarId) { + this._loadExternalPlugins(format); + + var settings = $.extend(true, {}, format.editorSettings); + + // If a toolbar is already provided for "true WYSIWYG" (in-place editing), + // then use that toolbar instead: override the default settings to render + // CKEditor UI's top toolbar into mainToolbar, and don't render the bottom + // toolbar at all. (CKEditor doesn't need a floated toolbar.) + if (mainToolbarId) { + var settingsOverride = { + extraPlugins: 'sharedspace', + removePlugins: 'floatingspace,elementspath', + sharedSpaces: { + top: mainToolbarId + } + }; + settings.extraPlugins += ',' + settingsOverride.extraPlugins; + settings.removePlugins += ',' + settingsOverride.removePlugins; + settings.sharedSpaces = settingsOverride.sharedSpaces; + } + + // CKEditor requires an element to already have the contentEditable + // attribute set to "true", otherwise it won't attach an inline editor. + element.setAttribute('contentEditable', 'true'); + + return !!CKEDITOR.inline(element, settings); + }, + + _loadExternalPlugins: function(format) { + var externalPlugins = format.editorSettings.drupalExternalPlugins; + // Register and load additional CKEditor plugins as necessary. + if (externalPlugins) { + for (var pluginName in externalPlugins) { + if (externalPlugins.hasOwnProperty(pluginName)) { + CKEDITOR.plugins.addExternal(pluginName, externalPlugins[pluginName], ''); + } + } + delete format.editorSettings.drupalExternalPlugins; + } } }; -})(Drupal, CKEDITOR); +})(Drupal, CKEDITOR, jQuery); diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/editor/editor/CKEditor.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/editor/editor/CKEditor.php index 57b857d..37a843e 100644 --- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/editor/editor/CKEditor.php +++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/editor/editor/CKEditor.php @@ -18,7 +18,8 @@ * @Plugin( * id = "ckeditor", * label = @Translation("CKEditor"), - * module = "ckeditor" + * module = "ckeditor", + * supports_inline_editing = TRUE * ) */ class CKEditor extends EditorBase { diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module index e4a3f82..a6ee046 100644 --- a/core/modules/edit/edit.module +++ b/core/modules/edit/edit.module @@ -97,7 +97,6 @@ function edit_library_info() { 'data' => array('edit' => array( 'metadataURL' => url('edit/metadata'), 'fieldFormURL' => url('edit/form/!entity_type/!id/!field_name/!langcode/!view_mode'), - 'rerenderProcessedTextURL' => url('edit/text/!entity_type/!id/!field_name/!langcode/!view_mode'), 'context' => 'body', )), 'type' => 'setting', @@ -118,7 +117,7 @@ function edit_library_info() { array('system', 'drupalSettings'), ), ); - $libraries['edit.editor.form'] = array( + $libraries['edit.editorWidget.form'] = array( 'title' => '"Form" Create.js PropertyEditor widget', 'version' => VERSION, 'js' => array( @@ -128,7 +127,7 @@ function edit_library_info() { array('edit', 'edit'), ), ); - $libraries['edit.editor.direct'] = array( + $libraries['edit.editorWidget.direct'] = array( 'title' => '"Direct" Create.js PropertyEditor widget', 'version' => VERSION, 'js' => array( diff --git a/core/modules/edit/edit.routing.yml b/core/modules/edit/edit.routing.yml index f63dc82..d66881d 100644 --- a/core/modules/edit/edit.routing.yml +++ b/core/modules/edit/edit.routing.yml @@ -12,11 +12,3 @@ edit_field_form: requirements: _permission: 'access in-place editing' _access_edit_entity_field: 'TRUE' - -edit_text: - pattern: '/edit/text/{entity_type}/{entity}/{field_name}/{langcode}/{view_mode}' - defaults: - _controller: '\Drupal\edit\EditController::getUntransformedText' - requirements: - _permission: 'access in-place editing' - _access_edit_entity_field: 'TRUE' diff --git a/core/modules/edit/js/backbone.drupalform.js b/core/modules/edit/js/backbone.drupalform.js index 859b8a6..9bd2c89 100644 --- a/core/modules/edit/js/backbone.drupalform.js +++ b/core/modules/edit/js/backbone.drupalform.js @@ -125,8 +125,7 @@ Backbone.syncDirect = function(method, model, options) { // Successfully saved. Drupal.ajax[base].commands.editFieldFormSaved = function (ajax, response, status) { - Drupal.edit.util.form.unajaxifySaving(jQuery(ajax.element)); - jQuery('#edit_backstage form').remove(); + Backbone.syncDirectCleanUp(); // Call Backbone.sync's success callback with the rerendered field. var changedAttributes = {}; @@ -163,4 +162,23 @@ Backbone.syncDirect = function(method, model, options) { } }; +/** + * Cleans up the hidden form that Backbone.syncDirect uses for syncing. + * + * This is called automatically by Backbone.syncDirect when saving is successful + * (i.e. when there are no validation errors). Only when editing is canceled + * while a PropertyEditor widget is in the invalid state, this must be called + * "manually" (in practice, ToolbarView does this). This is necessary because + * Backbone.syncDirect is not aware of the application state, it only does the + * syncing. + * An alternative could be to also remove the hidden form when validation errors + * occur, but then the form must be retrieved again, thus resulting in another + * roundtrip, which is bad for front-end performance. + */ +Backbone.syncDirectCleanUp = function() { + var $submit = jQuery('#edit_backstage form .edit-form-submit'); + Drupal.edit.util.form.unajaxifySaving($submit); + jQuery('#edit_backstage form').remove(); +}; + })(jQuery, Backbone, Drupal); diff --git a/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js b/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js index caac604..bc86a04 100644 --- a/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js +++ b/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js @@ -6,7 +6,9 @@ "use strict"; - jQuery.widget('Drupal.drupalContentEditableWidget', jQuery.Create.editWidget, { + // @todo D8: use jQuery UI Widget bridging. + // @see http://drupal.org/node/1874934#comment-7124904 + jQuery.widget('DrupalEditEditor.direct', jQuery.Create.editWidget, { /** * Implements getEditUISettings() method. @@ -54,8 +56,6 @@ if (from !== 'inactive') { // Removes the "contenteditable" attribute. this.disable(); - this._removeValidationErrors(); - this._cleanUp(); } break; case 'highlighted': @@ -70,42 +70,14 @@ case 'changed': break; case 'saving': - this._removeValidationErrors(); break; case 'saved': break; case 'invalid': break; } - }, - - /** - * Removes validation errors' markup changes, if any. - * - * Note: this only needs to happen for type=direct, because for type=direct, - * the property DOM element itself is modified; this is not the case for - * type=form. - */ - _removeValidationErrors: function() { - this.element - .removeClass('edit-validation-error') - .next('.edit-validation-errors').remove(); - }, - - /** - * Cleans up after the widget has been saved. - * - * Note: this is where the Create.Storage and accompanying Backbone.sync - * abstractions "leak" implementation details. That is only the case because - * we have to use Drupal's Form API as a transport mechanism. It is - * unfortunately a stateful transport mechanism, and that's why we have to - * clean it up here. This clean-up is only necessary when canceling the - * editing of a property after having attempted to save at least once. - */ - _cleanUp: function() { - Drupal.edit.util.form.unajaxifySaving(jQuery('#edit_backstage form .edit-form-submit')); - jQuery('#edit_backstage form').remove(); } + }); })(jQuery, Drupal); diff --git a/core/modules/edit/js/createjs/editingWidgets/formwidget.js b/core/modules/edit/js/createjs/editingWidgets/formwidget.js index 2e9bef4..8824a5d 100644 --- a/core/modules/edit/js/createjs/editingWidgets/formwidget.js +++ b/core/modules/edit/js/createjs/editingWidgets/formwidget.js @@ -6,7 +6,9 @@ "use strict"; - $.widget('Drupal.drupalFormWidget', $.Create.editWidget, { + // @todo D8: change the name to "form" + use jQuery UI Widget bridging. + // @see http://drupal.org/node/1874934#comment-7124904 + $.widget('DrupalEditEditor.formEditEditor', $.Create.editWidget, { id: null, $formContainer: null, diff --git a/core/modules/edit/js/util.js b/core/modules/edit/js/util.js index 6633859..e0aa490 100644 --- a/core/modules/edit/js/util.js +++ b/core/modules/edit/js/util.js @@ -54,41 +54,6 @@ Drupal.edit.util.buildUrl = function(id, urlFormat) { }); }; -/** - * Loads rerendered processed text for a given property. - * - * Leverages Drupal.ajax' ability to have scoped (per-instance) command - * implementations to be able to call a callback. - * - * @param options - * An object with the following keys: - * - $editorElement (required): the PredicateEditor DOM element. - * - propertyID (required): the property ID that uniquely identifies the - * property for which this form will be loaded. - * - callback (required: A callback function that will receive the rerendered - * processed text. - */ -Drupal.edit.util.loadRerenderedProcessedText = function(options) { - // Create a Drupal.ajax instance to load the form. - Drupal.ajax[options.propertyID] = new Drupal.ajax(options.propertyID, options.$editorElement, { - url: Drupal.edit.util.buildUrl(options.propertyID, drupalSettings.edit.rerenderProcessedTextURL), - event: 'edit-internal.edit', - submit: { nocssjs : true }, - progress: { type : null } // No progress indicator. - }); - // Implement a scoped editFieldRenderedWithoutTransformationFilters AJAX - // command: calls the callback. - Drupal.ajax[options.propertyID].commands.editFieldRenderedWithoutTransformationFilters = function(ajax, response, status) { - options.callback(response.data); - // Delete the Drupal.ajax instance that called this very function. - delete Drupal.ajax[options.propertyID]; - options.$editorElement.off('edit-internal.edit'); - }; - // This will ensure our scoped editFieldRenderedWithoutTransformationFilters - // AJAX command gets called. - options.$editorElement.trigger('edit-internal.edit'); -}; - Drupal.edit.util.form = { /** * Loads a form, calls a callback to inserts. diff --git a/core/modules/edit/js/views/propertyeditordecoration-view.js b/core/modules/edit/js/views/propertyeditordecoration-view.js index b512322..a3c0aaf 100644 --- a/core/modules/edit/js/views/propertyeditordecoration-view.js +++ b/core/modules/edit/js/views/propertyeditordecoration-view.js @@ -32,7 +32,8 @@ 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. - * * widget: the parent EditableeEntity widget. + * * widget: the parent EditableEntity widget. + * * editorName: the name of the PropertyEditor widget * - toolbarId: the ID attribute of the toolbar as rendered in the DOM. */ initialize: function(options) { @@ -40,6 +41,7 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ this.toolbarId = options.toolbarId; this.predicate = this.editor.options.property; + this.editorName = this.editor.options.editorName; // Only start listening to events as soon as we're no longer in the 'inactive' state. this.undelegateEvents(); @@ -53,6 +55,9 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ case 'inactive': if (from !== null) { this.undecorate(); + if (from === 'invalid') { + this._removeValidationErrors(); + } } break; case 'candidate': @@ -61,6 +66,9 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ this.stopHighlight(); if (from !== 'highlighted') { this.stopEdit(); + if (from === 'invalid') { + this._removeValidationErrors(); + } } } break; @@ -81,6 +89,9 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ case 'changed': break; case 'saving': + if (from === 'invalid') { + this._removeValidationErrors(); + } break; case 'saved': break; @@ -305,7 +316,24 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ else { callback(); } + }, + + /** + * Removes validation errors' markup changes, if any. + * + * Note: this only needs to happen for type=direct, because for type=direct, + * the property DOM element itself is modified; this is not the case for + * type=form. + */ + _removeValidationErrors: function() { + if (this.editorName !== 'form') { + this.$el + .removeClass('edit-validation-error') + .next('.edit-validation-errors') + .remove(); + } } + }); })(jQuery, Backbone, Drupal); diff --git a/core/modules/edit/js/views/toolbar-view.js b/core/modules/edit/js/views/toolbar-view.js index 3cf6eca..2075352 100644 --- a/core/modules/edit/js/views/toolbar-view.js +++ b/core/modules/edit/js/views/toolbar-view.js @@ -68,6 +68,9 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({ case 'inactive': if (from) { this.remove(); + if (this.editorName !== 'form') { + Backbone.syncDirectCleanUp(); + } } break; case 'candidate': @@ -75,6 +78,9 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({ this.render(); } else { + if (this.editorName !== 'form') { + Backbone.syncDirectCleanUp(); + } // Remove all toolgroups; they're no longer necessary. this.$el .removeClass('edit-highlighted edit-editing') diff --git a/core/modules/edit/lib/Drupal/edit/Ajax/FieldRenderedWithoutTransformationFiltersCommand.php b/core/modules/edit/lib/Drupal/edit/Ajax/FieldRenderedWithoutTransformationFiltersCommand.php deleted file mode 100644 index 53a8826..0000000 --- a/core/modules/edit/lib/Drupal/edit/Ajax/FieldRenderedWithoutTransformationFiltersCommand.php +++ /dev/null @@ -1,28 +0,0 @@ -addCommand(new FieldRenderedWithoutTransformationFiltersCommand($editable_text)); - - return $response; - } - } 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 index e224a01..28b86f9 100644 --- a/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/DirectEditor.php +++ b/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/DirectEditor.php @@ -16,7 +16,7 @@ * * @Plugin( * id = "direct", - * jsClassName = "drupalContentEditableWidget", + * jsClassName = "direct", * module = "edit" * ) */ @@ -50,7 +50,7 @@ function isCompatible(FieldInstance $instance, array $items) { public function getAttachments() { return array( 'library' => array( - array('edit', 'edit.editor.direct'), + array('edit', 'edit.editorWidget.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 index e9c453f..65ddc58 100644 --- a/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/FormEditor.php +++ b/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/FormEditor.php @@ -16,7 +16,7 @@ * * @Plugin( * id = "form", - * jsClassName = "drupalFormWidget", + * jsClassName = "formEditEditor", * module = "edit" * ) */ @@ -35,7 +35,7 @@ function isCompatible(FieldInstance $instance, array $items) { public function getAttachments() { return array( 'library' => array( - array('edit', 'edit.editor.form'), + array('edit', 'edit.editorWidget.form'), ), ); } diff --git a/core/modules/edit/lib/Drupal/edit/Tests/EditTestBase.php b/core/modules/edit/lib/Drupal/edit/Tests/EditTestBase.php index d614829..6ac8b3b 100644 --- a/core/modules/edit/lib/Drupal/edit/Tests/EditTestBase.php +++ b/core/modules/edit/lib/Drupal/edit/Tests/EditTestBase.php @@ -29,7 +29,7 @@ function setUp() { $this->installSchema('system', 'variable'); $this->installSchema('field', array('field_config', 'field_config_instance')); - $this->installSchema('entity_test', 'entity_test'); + $this->installSchema('entity_test', array('entity_test', 'entity_test_rev')); // Set default storage backend. variable_set('field_storage_default', $this->default_storage); diff --git a/core/modules/edit/lib/Drupal/edit/Tests/MetadataGeneratorTest.php b/core/modules/edit/lib/Drupal/edit/Tests/MetadataGeneratorTest.php index e41e6ed..5a4faef 100644 --- a/core/modules/edit/lib/Drupal/edit/Tests/MetadataGeneratorTest.php +++ b/core/modules/edit/lib/Drupal/edit/Tests/MetadataGeneratorTest.php @@ -56,8 +56,6 @@ public static function getInfo() { function setUp() { parent::setUp(); - $this->installSchema('field_test', 'test_entity_revision'); - $this->editorManager = new EditorManager($this->container->getParameter('container.namespaces')); $this->accessChecker = new MockEditEntityFieldAccessCheck(); $this->editorSelector = new EditorSelector($this->editorManager); diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module index 6db6210..ff3ac55 100644 --- a/core/modules/editor/editor.module +++ b/core/modules/editor/editor.module @@ -78,11 +78,48 @@ function editor_library_info() { array('system', 'jquery.once'), ), ); + // Create.js PropertyEditor widget library names begin with "edit.editor". + $libraries['edit.editorWidget.editor'] = array( + 'title' => '"Editor" Create.js PropertyEditor widget', + 'version' => VERSION, + 'js' => array( + $path . '/js/editor.createjs.js' => array( + 'scope' => 'footer', + 'attributes' => array('defer' => TRUE), + ), + array( + 'type' => 'setting', + 'data' => array( + 'editor' => array( + 'getUntransformedTextURL' => url('editor/!entity_type/!id/!field_name/!langcode/!view_mode'), + ) + ) + ), + ), + 'dependencies' => array( + array('edit', 'edit'), + array('editor', 'drupal.editor'), + array('system', 'drupal.ajax'), + array('system', 'drupalSettings'), + ), + ); return $libraries; } /** + * Implements hook_custom_theme(). + * + * @todo Add an event subscriber to the Ajax system to automatically set the + * base page theme for all Ajax requests, and then remove this one off. + */ +function editor_custom_theme() { + if (substr(current_path(), 0, 7) === 'editor/') { + return ajax_base_page_theme(); + } +} + +/** * Implements hook_form_FORM_ID_alter(). */ function editor_form_filter_admin_overview_alter(&$form, $form_state) { diff --git a/core/modules/editor/editor.routing.yml b/core/modules/editor/editor.routing.yml new file mode 100644 index 0000000..0bb56cf --- /dev/null +++ b/core/modules/editor/editor.routing.yml @@ -0,0 +1,7 @@ +editor_field_untransformed_text: + pattern: '/editor/{entity_type}/{entity}/{field_name}/{langcode}/{view_mode}' + defaults: + _controller: '\Drupal\editor\EditorController::getUntransformedText' + requirements: + _permission: 'access in-place editing' + _access_edit_entity_field: 'TRUE' diff --git a/core/modules/editor/js/editor.createjs.js b/core/modules/editor/js/editor.createjs.js new file mode 100644 index 0000000..c4bfae6 --- /dev/null +++ b/core/modules/editor/js/editor.createjs.js @@ -0,0 +1,157 @@ +/** + * @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 + * editor.js API, including the optional attachInlineEditor() and onChange() + * methods. + * For example, assuming that a hypothetical editor's name was "Magical Editor" + * and its editor.js API implementation lived at Drupal.editors.magical, this + * JavaScript would use: + * - Drupal.editors.magical.attachInlineEditor() + * - Drupal.editors.magical.onChange() + * - Drupal.editors.magical.detach() + */ +(function (jQuery, Drupal, drupalSettings) { + +"use strict"; + +// @todo D8: use jQuery UI Widget bridging. +// @see http://drupal.org/node/1874934#comment-7124904 +jQuery.widget('DrupalEditEditor.editor', jQuery.DrupalEditEditor.direct, { + + textFormat: null, + textFormatHasTransformations: null, + textEditor: null, + + /** + * Implements Create.editWidget.getEditUISettings. + */ + getEditUISettings: function () { + return { padding: true, unifiedToolbar: true, fullWidthToolbar: true }; + }, + + /** + * 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; + + this.textFormat = drupalSettings.editor.formats[metadata.format]; + this.textFormatHasTransformations = metadata.formatHasTransformations; + this.textEditor = Drupal.editors[this.textFormat.editor]; + }, + + /** + * Implements Create.editWidget.stateChange. + */ + stateChange: function (from, to) { + var that = this; + switch (to) { + case 'inactive': + break; + + case 'candidate': + // 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); + } + 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-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) { + that.element.html(untransformedText); + that.options.activated(); + }); + } + // When no transformation filters have been applied: start WYSIWYG + // editing immediately! + else { + this.options.activated(); + } + break; + + case 'active': + this.textEditor.attachInlineEditor( + this.element.get(0), + this.textFormat, + this.toolbarView.getMainWysiwygToolgroupId(), + this.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); + }); + break; + + case 'changed': + break; + + case 'saving': + break; + + case 'saved': + break; + + case 'invalid': + break; + } + }, + + /** + * Loads untransformed text for a given property. + * + * More accurately: it re-processes processed text to exclude transformation + * filters used by the text format. + * + * @param String propertyID + * A property ID that uniquely identifies the given property. + * @param jQuery $editorElement + * The property's PropertyEditor DOM element. + * @param Function callback + * A callback function that will receive the untransformed text. + * + * @see \Drupal\editor\Ajax\GetUntransformedTextCommand + */ + _getUntransformedText: function (propertyID, $editorElement, callback) { + // Create a Drupal.ajax instance to load the form. + Drupal.ajax[propertyID] = new Drupal.ajax(propertyID, $editorElement, { + url: Drupal.edit.util.buildUrl(propertyID, drupalSettings.editor.getUntransformedTextURL), + event: 'editor-internal.editor', + submit: { nocssjs : true }, + progress: { type : null } // No progress indicator. + }); + // Implement a scoped editorGetUntransformedText AJAX command: calls the + // callback. + Drupal.ajax[propertyID].commands.editorGetUntransformedText = function(ajax, response, status) { + callback(response.data); + // Delete the Drupal.ajax instance that called this very function. + delete Drupal.ajax[propertyID]; + $editorElement.off('editor-internal.editor'); + }; + // This will ensure our scoped editorGetUntransformedText AJAX command + // gets called. + $editorElement.trigger('editor-internal.editor'); + } + +}); + +})(jQuery, Drupal, drupalSettings); diff --git a/core/modules/editor/lib/Drupal/editor/Ajax/GetUntransformedTextCommand.php b/core/modules/editor/lib/Drupal/editor/Ajax/GetUntransformedTextCommand.php new file mode 100644 index 0000000..c1971d1 --- /dev/null +++ b/core/modules/editor/lib/Drupal/editor/Ajax/GetUntransformedTextCommand.php @@ -0,0 +1,29 @@ +getTranslation($langcode, FALSE)->$field_name; + $editable_text = check_markup($field->value, $field->format, $langcode, FALSE, array(FILTER_TYPE_TRANSFORM_REVERSIBLE, FILTER_TYPE_TRANSFORM_IRREVERSIBLE)); + $response->addCommand(new GetUntransformedTextCommand($editable_text)); + + return $response; + } + +} 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..da8f189 --- /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 formats 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.editorWidget.editor'); + + 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..9e443f8 --- /dev/null +++ b/core/modules/editor/lib/Drupal/editor/Tests/EditIntegrationTest.php @@ -0,0 +1,200 @@ + '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')); + + // 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('entity_test', 'entity_test', $view_mode)->getComponent($field_name); + $field_instance = field_info_instance('entity_test', $field_name, 'entity_test'); + 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->container->getParameter('container.namespaces')); + $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->container->getParameter('container.namespaces')); + $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 = entity_create('entity_test', array()); + $this->entity->{$this->field_name}->value = 'Test'; + $this->entity->{$this->field_name}->format = 'full_html'; + $this->entity->save(); + $entity = entity_load('entity_test', $this->entity->id()); + + // 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 entity_test 1, field Long text field', + 'custom' => array( + 'format' => 'full_html', + 'formatHasTransformations' => FALSE, + ), + ); + $this->assertEqual($expected, $metadata, 'The correct metadata (including custom metadata) is generated.'); + } + + /** + * Tests GetUntransformedTextCommand AJAX command. + */ + function testGetUntransformedTextCommand() { + // Create an entity with values for the field. + $this->entity = entity_create('entity_test', array()); + $this->entity->{$this->field_name}->value = 'Test'; + $this->entity->{$this->field_name}->format = 'full_html'; + $this->entity->save(); + $entity = entity_load('entity_test', $this->entity->id()); + + // Verify AJAX response. + $controller = new EditorController(); + $request = new Request(); + $response = $controller->getUntransformedText($entity, $this->field_name, LANGUAGE_NOT_SPECIFIED, 'default'); + $expected = array( + array( + 'command' => 'editorGetUntransformedText', + 'data' => 'Test', + ) + ); + $this->assertEqual(drupal_json_encode($expected), $response->prepare($request)->getContent(), 'The GetUntransformedTextCommand AJAX command works correctly.'); + } +} 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 6b0dacf..75deb09 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 {