Issue #1886566 by Wim Leers, sun, effulgentsia, quicksketch: Make WYSIWYG editors available for in-place editing.
parent
d97b8dac4b
commit
03b6a41085
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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().
|
||||
*/
|
||||
|
|
|
@ -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'
|
|
@ -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);
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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.');
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue