Issue #1886566 by Wim Leers, sun, effulgentsia, quicksketch: Make WYSIWYG editors available for in-place editing.

8.0.x
webchick 2013-03-20 14:38:12 -04:00
parent d97b8dac4b
commit 03b6a41085
24 changed files with 711 additions and 159 deletions

View File

@ -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);

View File

@ -18,7 +18,8 @@ use Drupal\editor\Plugin\Core\Entity\Editor;
* @Plugin(
* id = "ckeditor",
* label = @Translation("CKEditor"),
* module = "ckeditor"
* module = "ckeditor",
* supports_inline_editing = TRUE
* )
*/
class CKEditor extends EditorBase {

View File

@ -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(

View File

@ -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'

View File

@ -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);

View File

@ -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);

View File

@ -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,

View File

@ -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.

View File

@ -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);

View File

@ -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')

View File

@ -1,28 +0,0 @@
<?php
/**
* @file
* Definition of Drupal\edit\Ajax\FieldRenderedWithoutTransformationFiltersCommand.
*/
namespace Drupal\edit\Ajax;
use Drupal\Core\Ajax\CommandInterface;
/**
* AJAX command to rerender a processed text field without any transformation
* filters.
*/
class FieldRenderedWithoutTransformationFiltersCommand extends BaseCommand {
/**
* Constructs a FieldRenderedWithoutTransformationFiltersCommand object.
*
* @param string $data
* The data to pass on to the client side.
*/
public function __construct($data) {
parent::__construct('editFieldRenderedWithoutTransformationFilters', $data);
}
}

View File

@ -16,7 +16,6 @@ use Drupal\Core\Entity\EntityInterface;
use Drupal\edit\Ajax\FieldFormCommand;
use Drupal\edit\Ajax\FieldFormSavedCommand;
use Drupal\edit\Ajax\FieldFormValidationErrorsCommand;
use Drupal\edit\Ajax\FieldRenderedWithoutTransformationFiltersCommand;
/**
* Returns responses for Edit module routes.
@ -117,31 +116,4 @@ class EditController extends ContainerAware {
return $response;
}
/**
* Returns an Ajax response to render a text field without transformation filters.
*
* @param int $entity
* The entity of which a processed text field is being rerendered.
* @param string $field_name
* The name of the (processed text) field that that is being rerendered
* @param string $langcode
* The name of the language for which the processed text field is being
* rererendered.
* @param string $view_mode
* The view mode the processed text field should be rerendered in.
* @return \Drupal\Core\Ajax\AjaxResponse
* The Ajax response.
*/
public function getUntransformedText(EntityInterface $entity, $field_name, $langcode, $view_mode) {
$response = new AjaxResponse();
$output = field_view_field($entity, $field_name, $view_mode, $langcode);
$langcode = $output['#language'];
// Direct text editing is only supported for single-valued fields.
$editable_text = check_markup($output['#items'][0]['value'], $output['#items'][0]['format'], $langcode, FALSE, array(FILTER_TYPE_TRANSFORM_REVERSIBLE, FILTER_TYPE_TRANSFORM_IRREVERSIBLE));
$response->addCommand(new FieldRenderedWithoutTransformationFiltersCommand($editable_text));
return $response;
}
}

View File

@ -16,7 +16,7 @@ use Drupal\field\FieldInstance;
*
* @Plugin(
* id = "direct",
* jsClassName = "drupalContentEditableWidget",
* jsClassName = "direct",
* module = "edit"
* )
*/
@ -50,7 +50,7 @@ class DirectEditor extends EditorBase {
public function getAttachments() {
return array(
'library' => array(
array('edit', 'edit.editor.direct'),
array('edit', 'edit.editorWidget.direct'),
),
);
}

View File

@ -16,7 +16,7 @@ use Drupal\field\FieldInstance;
*
* @Plugin(
* id = "form",
* jsClassName = "drupalFormWidget",
* jsClassName = "formEditEditor",
* module = "edit"
* )
*/
@ -35,7 +35,7 @@ class FormEditor extends EditorBase {
public function getAttachments() {
return array(
'library' => array(
array('edit', 'edit.editor.form'),
array('edit', 'edit.editorWidget.form'),
),
);
}

View File

@ -29,7 +29,7 @@ class EditTestBase extends DrupalUnitTestBase {
$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);

View File

@ -56,8 +56,6 @@ class MetadataGeneratorTest extends EditTestBase {
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);

View File

@ -78,10 +78,47 @@ 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().
*/

View File

@ -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'

View File

@ -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);

View File

@ -0,0 +1,29 @@
<?php
/**
* @file
* Contains \Drupal\editor\Ajax\GetUntransformedTextCommand.
*/
namespace Drupal\editor\Ajax;
use Drupal\Core\Ajax\CommandInterface;
use Drupal\edit\Ajax\BaseCommand;
/**
* AJAX command to rerender a processed text field without any transformation
* filters.
*/
class GetUntransformedTextCommand extends BaseCommand {
/**
* Constructs a GetUntransformedTextCommand object.
*
* @param string $data
* The data to pass on to the client side.
*/
public function __construct($data) {
parent::__construct('editorGetUntransformedText', $data);
}
}

View File

@ -0,0 +1,47 @@
<?php
/**
* @file
* Contains \Drupal\editor\EditorController.
*/
namespace Drupal\editor;
use Symfony\Component\DependencyInjection\ContainerAware;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Entity\EntityInterface;
use Drupal\editor\Ajax\GetUntransformedTextCommand;
/**
* Returns responses for Editor module routes.
*/
class EditorController extends ContainerAware {
/**
* Returns an Ajax response to render a text field without transformation filters.
*
* @param int $entity
* The entity of which a processed text field is being rerendered.
* @param string $field_name
* The name of the (processed text) field that that is being rerendered
* @param string $langcode
* The name of the language for which the processed text field is being
* rererendered.
* @param string $view_mode
* The view mode the processed text field should be rerendered in.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The Ajax response.
*/
public function getUntransformedText(EntityInterface $entity, $field_name, $langcode, $view_mode) {
$response = new AjaxResponse();
// Direct text editing is only supported for single-valued fields.
$field = $entity->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;
}
}

View File

@ -0,0 +1,100 @@
<?php
/**
* @file
* Contains \Drupal\editor\Plugin\edit\editor\Editor.
*/
namespace Drupal\editor\Plugin\edit\editor;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Component\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
use Drupal\edit\EditorInterface;
use Drupal\field\FieldInstance;
/**
* Defines the "editor" Create.js PropertyEditor widget.
*
* @Plugin(
* id = "editor",
* jsClassName = "editor",
* alternativeTo = {"direct"},
* module = "editor"
* )
*/
class Editor extends PluginBase implements EditorInterface {
/**
* Implements \Drupal\edit\Plugin\EditorInterface::isCompatible().
*/
function isCompatible(FieldInstance $instance, array $items) {
$field = field_info_field($instance['field_name']);
// This editor is incompatible with multivalued fields.
if ($field['cardinality'] != 1) {
return FALSE;
}
// This editor is compatible with processed ("rich") text fields; but only
// if there is a currently active text format, that text format has an
// associated editor and that editor supports inline editing.
elseif (!empty($instance['settings']['text_processing'])) {
$format_id = $items[0]['format'];
if (isset($format_id) && $editor = editor_load($format_id)) {
$definition = drupal_container()->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;
}
}

View File

@ -0,0 +1,200 @@
<?php
/**
* @file
* Contains \Drupal\editor\Tests\EditorIntegrationTest.
*/
namespace Drupal\editor\Tests;
use Drupal\edit\EditorSelector;
use Drupal\edit\MetadataGenerator;
use Drupal\edit\Plugin\EditorManager;
use Drupal\edit\Tests\EditTestBase;
use Drupal\edit_test\MockEditEntityFieldAccessCheck;
use Drupal\editor\EditorController;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests Edit module integration (Editor module's inline editing support).
*/
class EditIntegrationTest extends EditTestBase {
/**
* The manager for editor (Create.js PropertyEditor widget) plug-ins.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $editorManager;
/**
* The metadata generator object to be tested.
*
* @var \Drupal\edit\MetadataGeneratorInterface.php
*/
protected $metadataGenerator;
/**
* The editor selector object to be used by the metadata generator object.
*
* @var \Drupal\edit\EditorSelectorInterface
*/
protected $editorSelector;
/**
* The access checker object to be used by the metadata generator object.
*
* @var \Drupal\edit\Access\EditEntityFieldAccessCheckInterface
*/
protected $accessChecker;
/**
* The name of the field ued for tests.
*
* @var string
*/
protected $field_name;
public static function getInfo() {
return array(
'name' => '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.');
}
}

View File

@ -18,7 +18,8 @@ use Drupal\editor\Plugin\Core\Entity\Editor;
* @Plugin(
* id = "unicorn",
* label = @Translation("Unicorn Editor"),
* module = "editor_test"
* module = "editor_test",
* supports_inline_editing = TRUE
* )
*/
class UnicornEditor extends EditorBase {