Commit ee1b6de6 authored by Dries's avatar Dries

Issue #1833716 by quicksketch, Wim Leers, effulgentsia: added WYSIWYG:...

Issue #1833716 by quicksketch, Wim Leers, effulgentsia: added WYSIWYG: Introduce 'Text editors' as part of filter format configuration.
parent a2ae42b3
<?php
/**
* @file
* Documentation for Text Editor API.
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Performs alterations on text editor definitions.
*
* @param array $editors
* An array of metadata of text editors, as collected by the plugin annotation
* discovery mechanism.
*
* @see \Drupal\editor\Plugin\EditorBase
*/
function hook_editor_info_alter(array &$editors) {
$editors['some_other_editor']['label'] = t('A different name');
$editors['some_other_editor']['library']['module'] = 'myeditoroverride';
}
/**
* Provides defaults for editor instances.
*
* Modules that extend the list of settings for a particular text editor library
* should specify defaults for those settings using this hook. These settings
* will be used for any new editors, as well as merged into any existing editor
* configuration that has not yet been provided with a specific value for a
* setting (as may happen when a module providing a new setting is enabled after
* the text editor has been configured).
*
* Note that only the top-level of this array is merged into the defaults. If
* multiple modules provide nested settings with the same top-level key, only
* the first will be used. Modules should avoid deep nesting of settings to
* avoid defaults being undefined.
*
* The return value of this hook is not cached. If retrieving defaults in a
* complex manner, the implementing module should provide its own caching inside
* the hook.
*
* @param $editor
* A string indicating the name of the editor library whose default settings
* are being provided.
*
* @return array
* An array of default settings that will be merged into the editor defaults.
*/
function hook_editor_default_settings($editor) {
return array(
'mymodule_new_setting1' => TRUE,
'mymodule_new_setting2' => array(
'foo' => 'baz',
'bar' => 'qux',
),
);
}
/**
* Modifies default settings for editor instances.
*
* Modules that extend the behavior of other modules may use this hook to change
* the default settings provided to new and existing editors. This hook should
* be used when changing an existing setting to a new value. To add a new
* default setting, hook_editor_default_settings() should be used.
*
* The return value of this hook is not cached. If retrieving defaults in a
* complex manner, the implementing module should provide its own caching inside
* the hook.
*
* @param $default_settings
* The array of default settings which may be modified, passed by reference.
* @param $editor
* A string indicating the name of the editor library whose default settings
* are being provided.
*
* @return array
* An array of default settings that will be merged into the editor defaults.
*
* @see hook_editor_default_settings()
*/
function hook_editor_default_settings_alter(&$default_settings, $editor) {
$default_settings['toolbar'] = array('Bold', 'Italics', 'Underline');
}
/**
* Modifies JavaScript settings that are added for text editors.
*
* @param array $settings
* All the settings that will be added to the page via drupal_add_js() for
* the text formats to which a user has access.
* @param array $formats
* The list of format objects for which settings are being added.
*/
function hook_editor_js_settings_alter(array &$settings, array $formats) {
if (isset($formats['filtered_html'])) {
$settings['filtered_html']['editor'][] = 'MyDifferentEditor';
$settings['filtered_html']['editorSettings']['buttons'] = array('strong', 'italic', 'underline');
}
}
/**
* @} End of "addtogroup hooks".
*/
name = Text Editor
description = "Allows to associate text formats with text editor libraries such as WYSIWYGs or toolbars."
package = Core
version = VERSION
core = 8.x
dependencies[] = filter
configure = admin/config/content/formats
This diff is collapsed.
/**
* @file
* Attaches behavior for the Editor module.
*/
(function ($, Drupal) {
"use strict";
/**
* Initialize an empty object for editors to place their attachment code.
*/
Drupal.editors = {};
/**
* Enables editors on text_format elements.
*/
Drupal.behaviors.editor = {
attach: function (context, settings) {
// If there are no editor settings, there are no editors to enable.
if (!settings.editor) {
return;
}
var $context = $(context);
var behavior = this;
$context.find('.editor').once('editor', function () {
var $this = $(this);
var activeFormatID = $this.val();
var field = behavior.findFieldForFormatSelector($this);
// Directly attach this editor, if the text format is enabled.
if (settings.editor.formats[activeFormatID]) {
Drupal.editorAttach(field, settings.editor.formats[activeFormatID]);
}
// Attach onChange handler to text format selector element.
if ($this.is('select')) {
$this.on('change.editorAttach', function () {
var newFormatID = $this.val();
// Prevent double-attaching if the change event is triggered manually.
if (newFormatID === activeFormatID) {
return;
}
// Detach the current editor (if any) and attach a new editor.
if (settings.editor.formats[activeFormatID]) {
Drupal.editorDetach(field, settings.editor.formats[activeFormatID]);
}
activeFormatID = newFormatID;
if (settings.editor.formats[activeFormatID]) {
Drupal.editorAttach(field, settings.editor.formats[activeFormatID]);
}
});
}
// Detach any editor when the containing form is submitted.
$this.parents('form').submit(function (event) {
// Do not detach if the event was canceled.
if (event.isDefaultPrevented()) {
return;
}
Drupal.editorDetach(field, settings.editor.formats[activeFormatID]);
});
});
},
detach: function (context, settings, trigger) {
var editors;
// The 'serialize' trigger indicates that we should simply update the
// underlying element with the new text, without destroying the editor.
if (trigger == 'serialize') {
// Removing the editor-processed class guarantees that the editor will
// be reattached. Only do this if we're planning to destroy the editor.
editors = $(context).find('.editor-processed');
}
else {
editors = $(context).find('.editor').removeOnce('editor');
}
var behavior = this;
editors.each(function () {
var $this = $(this);
var activeFormatID = $this.val();
var field = behavior.findFieldForFormatSelector($this);
Drupal.editorDetach(field, settings.editor.formats[activeFormatID], trigger);
});
},
findFieldForFormatSelector: function ($formatSelector) {
var field_id = $formatSelector.attr('data-editor-for');
return $('#' + field_id).get(0);
}
};
Drupal.editorAttach = function (field, format) {
if (format.editor) {
Drupal.editors[format.editor].attach(field, format);
}
};
Drupal.editorDetach = function (field, format, trigger) {
if (format.editor) {
Drupal.editors[format.editor].detach(field, format, trigger);
}
};
})(jQuery, Drupal);
<?php
/**
* @file
* Contains \Drupal\editor\EditorBundle.
*/
namespace Drupal\editor;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
/**
* Editor dependency injection container.
*/
class EditorBundle extends Bundle {
/**
* Overrides Symfony\Component\HttpKernel\Bundle\Bundle::build().
*/
public function build(ContainerBuilder $container) {
// Register the plugin manager for our plugin type with the dependency
// injection container.
$container->register('plugin.manager.editor', 'Drupal\editor\Plugin\EditorManager');
}
}
<?php
/**
* @file
* Contains \Drupal\editor\Plugin\Core\Entity\Editor.
*/
namespace Drupal\editor\Plugin\Core\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
/**
* Defines the configured text editor entity.
*
* @Plugin(
* id = "editor",
* label = @Translation("Editor"),
* module = "editor",
* controller_class = "Drupal\Core\Config\Entity\ConfigStorageController",
* config_prefix = "editor.editor",
* entity_keys = {
* "id" = "format",
* "uuid" = "uuid"
* }
* )
*/
class Editor extends ConfigEntityBase {
/**
* The machine name of the text format with which this configured text editor
* is associated.
*
* @var string
*/
public $format;
/**
* The name (plugin ID) of the text editor.
*
* @var string
*/
public $editor;
/**
* The array of settings for the text editor.
*
* @var array
*/
public $settings = array();
/**
* Overrides Drupal\Core\Entity\Entity::id().
*/
public function id() {
return $this->format;
}
/**
* Overrides Drupal\Core\Entity\Entity::label().
*/
public function label($langcode = NULL) {
$format = filter_format_load($this->format);
return $format->name;
}
/**
* Overrides Drupal\Core\Entity\Entity::__construct()
*/
public function __construct(array $values, $entity_type) {
parent::__construct($values, $entity_type);
$manager = drupal_container()->get('plugin.manager.editor');
$plugin = $manager->createInstance($this->editor);
// Initialize settings, merging module-provided defaults.
$default_settings = $plugin->getDefaultSettings();
$default_settings += module_invoke_all('editor_default_settings', $this->editor);
drupal_alter('editor_default_settings', $default_settings, $this->editor);
$this->settings += $default_settings;
}
}
<?php
/**
* @file
* Contains \Drupal\editor\Plugin\EditorBase.
*/
namespace Drupal\editor\Plugin;
use Drupal\Component\Plugin\PluginBase;
use Drupal\editor\Plugin\Core\Entity\Editor;
use Drupal\editor\Plugin\EditorInterface;
/**
* Defines a base class from which other modules providing editors may extend.
*
* This class provides default implementations of the EditorInterface so that
* classes extending this one do not need to implement every method.
*
* Plugins extending this class need to define a plugin definition array through
* annotation. These definition arrays may be altered through
* hook_editor_info_alter(). The definition includes the following keys:
*
* - id: The unique, system-wide identifier of the text editor. Typically named
* the same as the editor library.
* - label: The human-readable name of the text editor, translated.
* - module: The name of the module providing the plugin.
*
* A complete sample plugin definition should be defined as in this example:
*
* @code
* @Plugin(
* id = "myeditor",
* label = @Translation("My Editor"),
* module = "mymodule"
* )
* @endcode
*/
abstract class EditorBase extends PluginBase implements EditorInterface {
/**
* Implements \Drupal\editor\Plugin\EditorInterface::getDefaultSettings().
*/
public function getDefaultSettings() {
return array();
}
/**
* Implements \Drupal\editor\Plugin\EditorInterface::settingsForm().
*/
public function settingsForm(array $form, array &$form_state, Editor $editor) {
return $form;
}
/**
* Implements \Drupal\editor\Plugin\EditorInterface::settingsFormValidate().
*/
public function settingsFormValidate(array $form, array &$form_state) {
}
/**
* Implements \Drupal\editor\Plugin\EditorInterface::settingsFormSubmit().
*/
public function settingsFormSubmit(array $form, array &$form_state) {
}
}
<?php
/**
* @file
* Contains \Drupal\editor\Plugin\EditorInterface.
*/
namespace Drupal\editor\Plugin;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\editor\Plugin\Core\Entity\Editor;
/**
* Defines an interface for configurable text editors.
*
* Modules implementing this interface may want to extend the EditorBase
* class, which provides default implementations of each method where
* appropriate.
*/
interface EditorInterface extends PluginInspectionInterface {
/**
* Returns the default settings for this configurable text editor.
*
* @return array
* An array of settings as they would be stored by a configured text editor
* entity (\Drupal\editor\Plugin\Core\Entity\Editor).
*/
function getDefaultSettings();
/**
* Returns a settings form to configure this text editor.
*
* If the editor's behavior depends on extensive options and/or external data,
* then the implementing module can choose to provide a separate, global
* configuration page rather than per-text-format settings. In that case, this
* form should provide a link to the separate settings page.
*
* @param array $form
* An empty form array to be populated with a configuration form, if any.
* @param array $form_state
* The state of the entire filter administration form.
* @param \Drupal\editor\Plugin\Core\Entity\Editor $editor
* A configured text editor object.
*
* @return array
* A render array for the settings form.
*/
function settingsForm(array $form, array &$form_state, Editor $editor);
/**
* Validates the settings form for an editor.
*
* The contents of the editor settings are located in
* $form_state['values']['editor_settings']. Calls to form_error() should
* reflect this location in the settings form.
*
* @param array $form
* An associative array containing the structure of the form.
* @param array $form_state
* A reference to a keyed array containing the current state of the form.
*/
function settingsFormValidate(array $form, array &$form_state);
/**
* Modifies any values in the form state to prepare them for saving.
*
* Values in $form_state['values']['editor_settings'] are saved by Editor
* module in editor_form_filter_admin_format_submit().
*
* @param array $form
* An associative array containing the structure of the form.
* @param array $form_state
* A reference to a keyed array containing the current state of the form.
*/
function settingsFormSubmit(array $form, array &$form_state);
/**
* Returns JavaScript settings to be attached.
*
* Most text editors use JavaScript to provide a WYSIWYG or toolbar on the
* client-side interface. This method can be used to convert internal settings
* of the text editor into JavaScript variables that will be accessible when
* the text editor is loaded.
*
* @param \Drupal\editor\Plugin\Core\Entity\Editor $editor
* A configured text editor object.
*
* @return array
* An array of settings that will be added to the page for use by this text
* editor's JavaScript integration.
*
* @see drupal_process_attached()
* @see EditorManager::getAttachments()
*/
function getJSSettings(Editor $editor);
/**
* Returns libraries to be attached.
*
* Because this is a method, plugins can dynamically choose to attach a
* different library for different configurations, instead of being forced to
* always use the same method.
*
* @param \Drupal\editor\Plugin\Core\Entity\Editor $editor
* A configured text editor object.
*
* @return array
* An array of libraries that will be added to the page for use by this
* text editor.
*
* @see drupal_process_attached()
* @see EditorManager::getAttachments()
*/
function getLibraries(Editor $editor);
}
<?php
/**
* @file
* Contains \Drupal\editor\Plugin\EditorManager.
*/
namespace Drupal\editor\Plugin;
use Drupal\Component\Plugin\PluginManagerBase;
use Drupal\Component\Plugin\Factory\DefaultFactory;
use Drupal\Component\Plugin\Discovery\ProcessDecorator;
use Drupal\Core\Plugin\Discovery\AlterDecorator;
use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
use Drupal\Core\Plugin\Discovery\CacheDecorator;
/**
* Configurable text editor manager.
*/
class EditorManager extends PluginManagerBase {
/**
* Overrides \Drupal\Component\Plugin\PluginManagerBase::__construct().
*/
public function __construct() {
$this->discovery = new AnnotatedClassDiscovery('editor', 'editor');
$this->discovery = new ProcessDecorator($this->discovery, array($this, 'processDefinition'));
$this->discovery = new AlterDecorator($this->discovery, 'editor_info');
$this->discovery = new CacheDecorator($this->discovery, 'editor');
$this->factory = new DefaultFactory($this->discovery);
}
/**
* Populates a key-value pair of available text editors.
*
* @return array
* An array of translated text editor labels, keyed by ID.
*/
public function listOptions() {
$options = array();
foreach ($this->getDefinitions() as $key => $definition) {
$options[$key] = $definition['label'];
}
return $options;
}
/**
* Retrieves text editor libraries and JavaScript settings.
*
* @param array $format_ids
* An array of format IDs as returned by array_keys(filter_formats()).
*
* @return array
* An array of attachments, for use with #attached.
*
* @see drupal_process_attached()
*/
public function getAttachments(array $format_ids) {
$attachments = array('library' => array());
$settings = array();
foreach ($format_ids as $format_id) {
$editor = editor_load($format_id);
if (!$editor) {
continue;
}
$plugin = $this->createInstance($editor->editor);
// Libraries.
$attachments['library'] = array_merge($attachments['library'], $plugin->getLibraries($editor));
// JavaScript settings.
$settings[$format_id] = array(
'editor' => $editor->editor,
'editorSettings' => $plugin->getJSSettings($editor),
);
}
// We have all JavaScript settings, allow other modules to alter them.
drupal_alter('editor_js_settings', $settings, $formats);
if (empty($attachments['library']) && empty($settings)) {
return array();
}
$attachments['js'][] = array(
'type' => 'setting',
'data' => array('editor' => array('formats' => $settings)),
);
return $attachments;
}
/**
* Overrides Drupal\Component\Plugin\PluginManagerBase::processDefinition().
*/
public function processDefinition(&$definition, $plugin_id) {
parent::processDefinition($definition, $plugin_id);
// @todo Remove this check once http://drupal.org/node/1780396 is resolved.
if (!module_exists($definition['module'])) {
$definition = NULL;
return;
}
}
}
<?php
/**
* @file
* Definition of \Drupal\editor\Tests\EditorAdminTest.
*/
namespace Drupal\editor\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests administration of text editors.
*/
class EditorAdminTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('filter', 'editor');
public static function getInfo() {
return array(
'name' => 'Text editor administration',
'description' => 'Tests administration of text editors.',
'group' => 'Text Editor',
);
}
function setUp() {
parent::setUp();
// Add text format.
$filtered_html_format = array(
'format' => 'filtered_html',
'name' => 'Filtered HTML',
'weight' => 0,
'filters' => array(),
);
$filtered_html_format = (object) $filtered_html_format;
filter_format_save($filtered_html_format);
// Create admin user.
$this->admin_user = $this->drupalCreateUser(array('administer filters'));
}
function testWithoutEditorAvailable() {
$this->drupalLogin($this->admin_user);
$this->drupalGet('admin/config/content/formats/filtered_html');
// Ensure the form field order is correct.
$roles_pos = strpos($this->drupalGetContent(), 'Roles');
$editor_pos = strpos($this->drupalGetContent(), 'Text editor');
$filters_pos = strpos($this->drupalGetContent(), 'Enabled filters');
$this->assertTrue($roles_pos < $editor_pos && $editor_pos < $filters_pos, '"Text Editor" select appears in the correct location of the text format configuration UI.');
// Verify the <select>.
$select = $this->xpath('//select[@name="editor"]');
$select_is_disabled = $this->xpath('//select[@name="editor" and @disabled="disabled"]');
$options = $this->xpath('//select[@name="editor"]/option');
$this->assertTrue(count($select) === 1, 'The Text Editor select exists.');
$this->assertTrue(count($select_is_disabled) === 1, 'The Text Editor select is disabled.');
$this->assertTrue(count($options) === 1, 'The Text Editor select has only one option.');
$this->assertTrue(((string) $options[0]) === 'None', 'Option 1 in the he Text Editor select is "None".');
$this->assertRaw(t('This option is disabled because no modules that provide a text editor are currently enabled.'), 'Description for select present that tells users to install a text editor module.');
// Make a text editor available.
module_enable(array('editor_test'));
$this->rebuildContainer();
$this->resetAll();
$this->drupalGet('admin/config/content/formats/filtered_html');
// Verify the <select> when a text editor is available.
$select = $this->xpath('//select[@name="editor"]');
$select_is_disabled = $this->xpath('//select[@name="editor" and @disabled="disabled"]');
$options = $this->xpath('//select[@name="editor"]/option');
$this->assertTrue(count($select) === 1, 'The Text Editor select exists.');
$this->assertTrue(count($select_is_disabled) === 0, 'The Text Editor select is not disabled.');
$this->assertTrue(count($options) === 2, 'The Text Editor select has two options.');