Move into nested docroot

This commit is contained in:
Rob Davies 2017-02-13 15:31:17 +00:00
parent 83a0d3a149
commit c8b70abde9
13405 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,40 @@
<?php
namespace Drupal\ckeditor\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a CKEditorPlugin annotation object.
*
* Plugin Namespace: Plugin\CKEditorPlugin
*
* For a working example, see \Drupal\ckeditor\Plugin\CKEditorPlugin\DrupalImage
*
* @see \Drupal\ckeditor\CKEditorPluginInterface
* @see \Drupal\ckeditor\CKEditorPluginBase
* @see \Drupal\ckeditor\CKEditorPluginManager
* @see hook_ckeditor_plugin_info_alter()
* @see plugin_api
*
* @Annotation
*/
class CKEditorPlugin extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The human-readable name of the CKEditor plugin.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $label;
}

View file

@ -0,0 +1,54 @@
<?php
namespace Drupal\ckeditor;
use Drupal\Core\Plugin\PluginBase;
use Drupal\editor\Entity\Editor;
/**
* Defines a base CKEditor plugin implementation.
*
* No other CKEditor plugins can be internal, unless a different CKEditor build
* than the one provided by Drupal core is used. Most CKEditor plugins don't
* need to provide additional settings forms.
*
* This base class assumes that your plugin has buttons that you want to be
* enabled through the toolbar builder UI. It is still possible to also
* implement the CKEditorPluginContextualInterface (for contextual enabling) and
* CKEditorPluginConfigurableInterface interfaces (for configuring plugin
* settings).
*
* NOTE: the Drupal plugin ID should correspond to the CKEditor plugin name.
*
* @see \Drupal\ckeditor\CKEditorPluginInterface
* @see \Drupal\ckeditor\CKEditorPluginButtonsInterface
* @see \Drupal\ckeditor\CKEditorPluginContextualInterface
* @see \Drupal\ckeditor\CKEditorPluginConfigurableInterface
* @see \Drupal\ckeditor\CKEditorPluginManager
* @see \Drupal\ckeditor\Annotation\CKEditorPlugin
* @see plugin_api
*/
abstract class CKEditorPluginBase extends PluginBase implements CKEditorPluginInterface, CKEditorPluginButtonsInterface {
/**
* {@inheritdoc}
*/
function isInternal() {
return FALSE;
}
/**
* {@inheritdoc}
*/
function getDependencies(Editor $editor) {
return array();
}
/**
* {@inheritdoc}
*/
function getLibraries(Editor $editor) {
return array();
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace Drupal\ckeditor;
/**
* Defines an interface for CKEditor plugins with buttons.
*
* This allows a CKEditor plugin to define which buttons it provides, so that
* users can configure a CKEditor toolbar instance via the toolbar builder UI.
* If at least one button that this plugin provides is added to the toolbar via
* the toolbar builder UI, then this plugin will be enabled automatically.
*
* If a CKEditor plugin implements this interface, it can still also implement
* CKEditorPluginContextualInterface if it wants a button to conditionally be
* added as well. The downside of conditionally adding buttons is that the user
* cannot see these buttons in the toolbar builder UI.
*
* @see \Drupal\ckeditor\CKEditorPluginInterface
* @see \Drupal\ckeditor\CKEditorPluginContextualInterface
* @see \Drupal\ckeditor\CKEditorPluginConfigurableInterface
* @see \Drupal\ckeditor\CKEditorPluginCssInterface
* @see \Drupal\ckeditor\CKEditorPluginBase
* @see \Drupal\ckeditor\CKEditorPluginManager
* @see \Drupal\ckeditor\Annotation\CKEditorPlugin
* @see plugin_api
*/
interface CKEditorPluginButtonsInterface extends CKEditorPluginInterface {
/**
* Returns the buttons that this plugin provides, along with metadata.
*
* The metadata is used by the CKEditor module to generate a visual CKEditor
* toolbar builder UI.
*
* @return array
* An array of buttons that are provided by this plugin. This will
* only be used in the administrative section for assembling the toolbar.
* Each button should by keyed by its CKEditor button name, and should
* contain an array of button properties, including:
* - label: A human-readable, translated button name.
* - image: An image for the button to be used in the toolbar.
* - image_rtl: If the image needs to have a right-to-left version, specify
* an alternative file that will be used in RTL editors.
* - image_alternative: If this button does not render as an image, specify
* an HTML string representing the contents of this button.
* - image_alternative_rtl: Similar to image_alternative, but a
* right-to-left version.
* - attributes: An array of HTML attributes which should be added to this
* button when rendering the button in the administrative section for
* assembling the toolbar.
* - multiple: Boolean value indicating if this button may be added multiple
* times to the toolbar. This typically is only applicable for dividers
* and group indicators.
*/
public function getButtons();
}

View file

@ -0,0 +1,46 @@
<?php
namespace Drupal\ckeditor;
use Drupal\Core\Form\FormStateInterface;
use Drupal\editor\Entity\Editor;
/**
* Defines an interface for configurable CKEditor plugins.
*
* This allows a CKEditor plugin to define a settings form. These settings can
* then be automatically passed on to the corresponding CKEditor instance via
* CKEditorPluginInterface::getConfig().
*
* @see \Drupal\ckeditor\CKEditorPluginInterface
* @see \Drupal\ckeditor\CKEditorPluginButtonsInterface
* @see \Drupal\ckeditor\CKEditorPluginContextualInterface
* @see \Drupal\ckeditor\CKEditorPluginCssInterface
* @see \Drupal\ckeditor\CKEditorPluginBase
* @see \Drupal\ckeditor\CKEditorPluginManager
* @see \Drupal\ckeditor\Annotation\CKEditorPlugin
* @see plugin_api
*/
interface CKEditorPluginConfigurableInterface extends CKEditorPluginInterface {
/**
* Returns a settings form to configure this CKEditor plugin.
*
* If the plugin'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-editor 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 \Drupal\Core\Form\FormStateInterface $form_state
* The state of the entire filter administration form.
* @param \Drupal\editor\Entity\Editor $editor
* A configured text editor object.
*
* @return array
* A render array for the settings form.
*/
public function settingsForm(array $form, FormStateInterface $form_state, Editor $editor);
}

View file

@ -0,0 +1,42 @@
<?php
namespace Drupal\ckeditor;
use Drupal\editor\Entity\Editor;
/**
* Defines an interface for contextually enabled CKEditor plugins.
*
* Contextually enabled CKEditor plugins can be enabled via an explicit setting,
* or enable themselves based on the configuration of another setting, such as
* enabling based on a particular button being present in the toolbar.
*
* If a contextually enabled CKEditor plugin must also be configurable (for
* instance, in the case where it must be enabled based on an explicit setting),
* then one must also implement the CKEditorPluginConfigurableInterface
* interface.
*
* @see \Drupal\ckeditor\CKEditorPluginInterface
* @see \Drupal\ckeditor\CKEditorPluginButtonsInterface
* @see \Drupal\ckeditor\CKEditorPluginConfigurableInterface
* @see \Drupal\ckeditor\CKEditorPluginCssInterface
* @see \Drupal\ckeditor\CKEditorPluginBase
* @see \Drupal\ckeditor\CKEditorPluginManager
* @see \Drupal\ckeditor\Annotation\CKEditorPlugin
* @see plugin_api
*/
interface CKEditorPluginContextualInterface extends CKEditorPluginInterface {
/**
* Checks if this plugin should be enabled based on the editor configuration.
*
* The editor's settings can be retrieved via $editor->getSettings().
*
* @param \Drupal\editor\Entity\Editor $editor
* A configured text editor object.
*
* @return bool
*/
public function isEnabled(Editor $editor);
}

View file

@ -0,0 +1,39 @@
<?php
namespace Drupal\ckeditor;
use Drupal\editor\Entity\Editor;
/**
* Defines an interface for CKEditor plugins with associated CSS.
*
* This allows a CKEditor plugin to add additional CSS in iframe CKEditor
* instances without needing to implement hook_ckeditor_css_alter().
*
* @see \Drupal\ckeditor\CKEditorPluginInterface
* @see \Drupal\ckeditor\CKEditorPluginButtonsInterface
* @see \Drupal\ckeditor\CKEditorPluginContextualInterface
* @see \Drupal\ckeditor\CKEditorPluginConfigurableInterface
* @see \Drupal\ckeditor\CKEditorPluginBase
* @see \Drupal\ckeditor\CKEditorPluginManager
* @see \Drupal\ckeditor\Annotation\CKEditorPlugin
* @see plugin_api
*/
interface CKEditorPluginCssInterface extends CKEditorPluginInterface {
/**
* Retrieves enabled plugins' iframe instance CSS files.
*
* Note: this does not use a Drupal asset library because this CSS will be
* loaded by CKEditor, not by Drupal.
*
* @param \Drupal\editor\Entity\Editor $editor
* A configured text editor object.
*
* @return string[]
* An array of CSS files. This is a flat list of file paths relative to
* the Drupal root.
*/
public function getCssFiles(Editor $editor);
}

View file

@ -0,0 +1,99 @@
<?php
namespace Drupal\ckeditor;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\editor\Entity\Editor;
/**
* Defines an interface for (loading of) CKEditor plugins.
*
* This is the most basic CKEditor plugin interface; it provides the bare
* minimum information. Solely implementing this interface is not sufficient to
* be able to enable the plugin though a CKEditor plugin can either be enabled
* automatically when a button it provides is present in the toolbar, or when
* some programmatically defined condition is true. In the former case,
* implement the CKEditorPluginButtonsInterface interface, in the latter case,
* implement the CKEditorPluginContextualInterface interface. It is also
* possible to implement both, for advanced use cases.
*
* Finally, if your plugin must be configurable, you can also implement the
* CKEditorPluginConfigurableInterface interface.
*
* @see \Drupal\ckeditor\CKEditorPluginButtonsInterface
* @see \Drupal\ckeditor\CKEditorPluginContextualInterface
* @see \Drupal\ckeditor\CKEditorPluginConfigurableInterface
* @see \Drupal\ckeditor\CKEditorPluginCssInterface
* @see \Drupal\ckeditor\CKEditorPluginBase
* @see \Drupal\ckeditor\CKEditorPluginManager
* @see \Drupal\ckeditor\Annotation\CKEditorPlugin
* @see plugin_api
*/
interface CKEditorPluginInterface extends PluginInspectionInterface {
/**
* Indicates if this plugin is part of the optimized CKEditor build.
*
* Plugins marked as internal are implicitly loaded as part of CKEditor.
*
* @return bool
*/
public function isInternal();
/**
* Returns a list of plugins this plugin requires.
*
* @param \Drupal\editor\Entity\Editor $editor
* A configured text editor object.
* @return array
* An unindexed array of plugin names this plugin requires. Each plugin is
* is identified by its annotated ID.
*/
public function getDependencies(Editor $editor);
/**
* Returns a list of libraries this plugin requires.
*
* These libraries will be attached to the text_format element on which the
* editor is being loaded.
*
* @param \Drupal\editor\Entity\Editor $editor
* A configured text editor object.
* @return array
* An array of libraries suitable for usage in a render API #attached
* property.
*/
public function getLibraries(Editor $editor);
/**
* Returns the Drupal root-relative file path to the plugin JavaScript file.
*
* Note: this does not use a Drupal library because this uses CKEditor's API,
* see http://docs.cksource.com/ckeditor_api/symbols/CKEDITOR.resourceManager.html#addExternal.
*
* @return string|false
* The Drupal root-relative path to the file, FALSE if an internal plugin.
*/
public function getFile();
/**
* Returns the additions to CKEDITOR.config for a specific CKEditor instance.
*
* The editor's settings can be retrieved via $editor->getSettings(), but be
* aware that it may not yet contain plugin-specific settings, because the
* user may not yet have configured the form.
* If there are plugin-specific settings (verify with isset()), they can be
* found at
* @code
* $settings = $editor->getSettings();
* $plugin_specific_settings = $settings['plugins'][$plugin_id];
* @endcode
*
* @param \Drupal\editor\Entity\Editor $editor
* A configured text editor object.
* @return array
* A keyed array, whose keys will end up as keys under CKEDITOR.config.
*/
public function getConfig(Editor $editor);
}

View file

@ -0,0 +1,221 @@
<?php
namespace Drupal\ckeditor;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\editor\Entity\Editor;
/**
* Provides a CKEditor Plugin plugin manager.
*
* @see \Drupal\ckeditor\CKEditorPluginInterface
* @see \Drupal\ckeditor\CKEditorPluginButtonsInterface
* @see \Drupal\ckeditor\CKEditorPluginContextualInterface
* @see \Drupal\ckeditor\CKEditorPluginConfigurableInterface
* @see \Drupal\ckeditor\CKEditorPluginCssInterface
* @see \Drupal\ckeditor\CKEditorPluginBase
* @see \Drupal\ckeditor\Annotation\CKEditorPlugin
* @see plugin_api
*/
class CKEditorPluginManager extends DefaultPluginManager {
/**
* Constructs a CKEditorPluginManager object.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/CKEditorPlugin', $namespaces, $module_handler, 'Drupal\ckeditor\CKEditorPluginInterface', 'Drupal\ckeditor\Annotation\CKEditorPlugin');
$this->alterInfo('ckeditor_plugin_info');
$this->setCacheBackend($cache_backend, 'ckeditor_plugins');
}
/**
* Retrieves enabled plugins' files, keyed by plugin ID.
*
* For CKEditor plugins that implement:
* - CKEditorPluginButtonsInterface, not CKEditorPluginContextualInterface,
* a plugin is enabled if at least one of its buttons is in the toolbar;
* - CKEditorPluginContextualInterface, not CKEditorPluginButtonsInterface,
* a plugin is enabled if its isEnabled() method returns TRUE
* - both of these interfaces, a plugin is enabled if either is the case.
*
* Internal plugins (those that are part of the bundled build of CKEditor) are
* excluded by default, since they are loaded implicitly. If you need to know
* even implicitly loaded (i.e. internal) plugins, then set the optional
* second parameter.
*
* @param \Drupal\editor\Entity\Editor $editor
* A configured text editor object.
* @param bool $include_internal_plugins
* Defaults to FALSE. When set to TRUE, plugins whose isInternal() method
* returns TRUE will also be included.
* @return array
* A list of the enabled CKEditor plugins, with the plugin IDs as keys and
* the Drupal root-relative plugin files as values.
* For internal plugins, the value is NULL.
*/
public function getEnabledPluginFiles(Editor $editor, $include_internal_plugins = FALSE) {
$plugins = array_keys($this->getDefinitions());
$toolbar_buttons = $this->getEnabledButtons($editor);
$enabled_plugins = array();
$additional_plugins = array();
foreach ($plugins as $plugin_id) {
$plugin = $this->createInstance($plugin_id);
if (!$include_internal_plugins && $plugin->isInternal()) {
continue;
}
$enabled = FALSE;
// Enable this plugin if it provides a button that has been enabled.
if ($plugin instanceof CKEditorPluginButtonsInterface) {
$plugin_buttons = array_keys($plugin->getButtons());
$enabled = (count(array_intersect($toolbar_buttons, $plugin_buttons)) > 0);
}
// Otherwise enable this plugin if it declares itself as enabled.
if (!$enabled && $plugin instanceof CKEditorPluginContextualInterface) {
$enabled = $plugin->isEnabled($editor);
}
if ($enabled) {
$enabled_plugins[$plugin_id] = ($plugin->isInternal()) ? NULL : $plugin->getFile();
// Check if this plugin has dependencies that also need to be enabled.
$additional_plugins = array_merge($additional_plugins, array_diff($plugin->getDependencies($editor), $additional_plugins));
}
}
// Add the list of dependent plugins.
foreach ($additional_plugins as $plugin_id) {
$plugin = $this->createInstance($plugin_id);
$enabled_plugins[$plugin_id] = ($plugin->isInternal()) ? NULL : $plugin->getFile();
}
// Always return plugins in the same order.
asort($enabled_plugins);
return $enabled_plugins;
}
/**
* Gets the enabled toolbar buttons in the given text editor instance.
*
* @param \Drupal\editor\Entity\Editor $editor
* A configured text editor object.
*
* @return string[]
* A list of the toolbar buttons enabled in the given text editor instance.
*/
public static function getEnabledButtons(Editor $editor) {
$toolbar_rows = [];
$settings = $editor->getSettings();
foreach ($settings['toolbar']['rows'] as $row_number => $row) {
$toolbar_rows[] = array_reduce($settings['toolbar']['rows'][$row_number], function (&$result, $button_group) {
return array_merge($result, $button_group['items']);
}, []);
}
return array_unique(NestedArray::mergeDeepArray($toolbar_rows));
}
/**
* Retrieves all available CKEditor buttons, keyed by plugin ID.
*
* @return array
* All available CKEditor buttons, with plugin IDs as keys and button
* metadata (as implemented by getButtons()) as values.
*
* @see \Drupal\ckeditor\CKEditorPluginButtonsInterface::getButtons()
*/
public function getButtons() {
$plugins = array_keys($this->getDefinitions());
$buttons_plugins = array();
foreach ($plugins as $plugin_id) {
$plugin = $this->createInstance($plugin_id);
if ($plugin instanceof CKEditorPluginButtonsInterface) {
$buttons_plugins[$plugin_id] = $plugin->getButtons();
}
}
return $buttons_plugins;
}
/**
* Injects the CKEditor plugins settings forms as a vertical tabs subform.
*
* @param array &$form
* A reference to an associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param \Drupal\editor\Entity\Editor $editor
* A configured text editor object.
*/
public function injectPluginSettingsForm(array &$form, FormStateInterface $form_state, Editor $editor) {
$definitions = $this->getDefinitions();
foreach (array_keys($definitions) as $plugin_id) {
$plugin = $this->createInstance($plugin_id);
if ($plugin instanceof CKEditorPluginConfigurableInterface) {
$plugin_settings_form = array();
$form['plugins'][$plugin_id] = array(
'#type' => 'details',
'#title' => $definitions[$plugin_id]['label'],
'#open' => TRUE,
'#group' => 'editor][settings][plugin_settings',
'#attributes' => array(
'data-ckeditor-plugin-id' => $plugin_id,
),
);
// Provide enough metadata for the drupal.ckeditor.admin library to
// allow it to automatically show/hide the vertical tab containing the
// settings for this plugin. Only do this if it's a CKEditor plugin that
// just provides buttons, don't do this if it's a contextually enabled
// CKEditor plugin. After all, in the latter case, we can't know when
// its settings should be shown!
if ($plugin instanceof CKEditorPluginButtonsInterface && !$plugin instanceof CKEditorPluginContextualInterface) {
$form['plugins'][$plugin_id]['#attributes']['data-ckeditor-buttons'] = implode(' ', array_keys($plugin->getButtons()));
}
$form['plugins'][$plugin_id] += $plugin->settingsForm($plugin_settings_form, $form_state, $editor);
}
}
}
/**
* Retrieves enabled plugins' iframe instance CSS files, keyed by plugin ID.
*
* @param \Drupal\editor\Entity\Editor $editor
* A configured text editor object.
*
* @return string[]
* Enabled plugins CKEditor CSS files, with plugin IDs as keys and CSS file
* paths relative to the Drupal root (as implemented by getCssFiles()) as
* values.
*
* @see \Drupal\ckeditor\CKEditorPluginCssInterface::getCssFiles()
*/
public function getCssFiles(Editor $editor) {
$enabled_plugins = array_keys($this->getEnabledPluginFiles($editor, TRUE));
$css_files = array();
foreach ($enabled_plugins as $plugin_id) {
$plugin = $this->createInstance($plugin_id);
if ($plugin instanceof CKEditorPluginCssInterface) {
$css_files[$plugin_id] = $plugin->getCssFiles($editor);
}
}
return $css_files;
}
}

View file

@ -0,0 +1,88 @@
<?php
namespace Drupal\ckeditor\Plugin\CKEditorPlugin;
use Drupal\ckeditor\CKEditorPluginBase;
use Drupal\ckeditor\CKEditorPluginConfigurableInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\editor\Entity\Editor;
/**
* Defines the "drupalimage" plugin.
*
* @CKEditorPlugin(
* id = "drupalimage",
* label = @Translation("Image"),
* module = "ckeditor"
* )
*/
class DrupalImage extends CKEditorPluginBase implements CKEditorPluginConfigurableInterface {
/**
* {@inheritdoc}
*/
public function getFile() {
return drupal_get_path('module', 'ckeditor') . '/js/plugins/drupalimage/plugin.js';
}
/**
* {@inheritdoc}
*/
public function getLibraries(Editor $editor) {
return array(
'core/drupal.ajax',
);
}
/**
* {@inheritdoc}
*/
public function getConfig(Editor $editor) {
return array(
'drupalImage_dialogTitleAdd' => t('Insert Image'),
'drupalImage_dialogTitleEdit' => t('Edit Image'),
);
}
/**
* {@inheritdoc}
*/
public function getButtons() {
return array(
'DrupalImage' => array(
'label' => t('Image'),
'image' => drupal_get_path('module', 'ckeditor') . '/js/plugins/drupalimage/icons/drupalimage.png',
),
);
}
/**
* {@inheritdoc}
*
* @see \Drupal\editor\Form\EditorImageDialog
* @see editor_image_upload_settings_form()
*/
public function settingsForm(array $form, FormStateInterface $form_state, Editor $editor) {
$form_state->loadInclude('editor', 'admin.inc');
$form['image_upload'] = editor_image_upload_settings_form($editor);
$form['image_upload']['#attached']['library'][] = 'ckeditor/drupal.ckeditor.drupalimage.admin';
$form['image_upload']['#element_validate'][] = array($this, 'validateImageUploadSettings');
return $form;
}
/**
* #element_validate handler for the "image_upload" element in settingsForm().
*
* Moves the text editor's image upload settings from the DrupalImage plugin's
* own settings into $editor->image_upload.
*
* @see \Drupal\editor\Form\EditorImageDialog
* @see editor_image_upload_settings_form()
*/
function validateImageUploadSettings(array $element, FormStateInterface $form_state) {
$settings = &$form_state->getValue(array('editor', 'settings', 'plugins', 'drupalimage', 'image_upload'));
$form_state->get('editor')->setImageUploadSettings($settings);
$form_state->unsetValue(array('editor', 'settings', 'plugins', 'drupalimage'));
}
}

View file

@ -0,0 +1,107 @@
<?php
namespace Drupal\ckeditor\Plugin\CKEditorPlugin;
use Drupal\Component\Plugin\PluginBase;
use Drupal\editor\Entity\Editor;
use Drupal\ckeditor\CKEditorPluginInterface;
use Drupal\ckeditor\CKEditorPluginContextualInterface;
use Drupal\ckeditor\CKEditorPluginCssInterface;
/**
* Defines the "drupalimagecaption" plugin.
*
* @CKEditorPlugin(
* id = "drupalimagecaption",
* label = @Translation("Drupal image caption widget"),
* module = "ckeditor"
* )
*/
class DrupalImageCaption extends PluginBase implements CKEditorPluginInterface, CKEditorPluginContextualInterface, CKEditorPluginCssInterface {
/**
* {@inheritdoc}
*/
public function isInternal() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getDependencies(Editor $editor) {
return array();
}
/**
* {@inheritdoc}
*/
public function getLibraries(Editor $editor) {
return array(
'ckeditor/drupal.ckeditor.plugins.drupalimagecaption',
);
}
/**
* {@inheritdoc}
*/
public function getFile() {
return drupal_get_path('module', 'ckeditor') . '/js/plugins/drupalimagecaption/plugin.js';
}
/**
* {@inheritdoc}
*/
public function getConfig(Editor $editor) {
$format = $editor->getFilterFormat();
return array(
'image2_captionedClass' => 'caption caption-img',
'image2_alignClasses' => array('align-left', 'align-center', 'align-right'),
'drupalImageCaption_captionPlaceholderText' => t('Enter caption here'),
// Only enable those parts of DrupalImageCaption for which the
// corresponding Drupal text filters are enabled.
'drupalImageCaption_captionFilterEnabled' => $format->filters('filter_caption')->status,
'drupalImageCaption_alignFilterEnabled' => $format->filters('filter_align')->status,
);
}
/**
* {@inheritdoc}
*/
public function getCssFiles(Editor $editor) {
return array(
drupal_get_path('module', 'ckeditor') . '/css/plugins/drupalimagecaption/ckeditor.drupalimagecaption.css'
);
}
/**
* {@inheritdoc}
*/
function isEnabled(Editor $editor) {
if (!$editor->hasAssociatedFilterFormat()) {
return FALSE;
}
// Automatically enable this plugin if the text format associated with this
// text editor uses the filter_align or filter_caption filter and the
// DrupalImage button is enabled.
$format = $editor->getFilterFormat();
if ($format->filters('filter_align')->status || $format->filters('filter_caption')->status) {
$enabled = FALSE;
$settings = $editor->getSettings();
foreach ($settings['toolbar']['rows'] as $row) {
foreach ($row as $group) {
foreach ($group['items'] as $button) {
if ($button === 'DrupalImage') {
$enabled = TRUE;
}
}
}
}
return $enabled;
}
return FALSE;
}
}

View file

@ -0,0 +1,62 @@
<?php
namespace Drupal\ckeditor\Plugin\CKEditorPlugin;
use Drupal\ckeditor\CKEditorPluginBase;
use Drupal\editor\Entity\Editor;
/**
* Defines the "drupallink" plugin.
*
* @CKEditorPlugin(
* id = "drupallink",
* label = @Translation("Drupal link"),
* module = "ckeditor"
* )
*/
class DrupalLink extends CKEditorPluginBase {
/**
* {@inheritdoc}
*/
public function getFile() {
return drupal_get_path('module', 'ckeditor') . '/js/plugins/drupallink/plugin.js';
}
/**
* {@inheritdoc}
*/
public function getLibraries(Editor $editor) {
return array(
'core/drupal.ajax',
);
}
/**
* {@inheritdoc}
*/
public function getConfig(Editor $editor) {
return array(
'drupalLink_dialogTitleAdd' => t('Add Link'),
'drupalLink_dialogTitleEdit' => t('Edit Link'),
);
}
/**
* {@inheritdoc}
*/
public function getButtons() {
$path = drupal_get_path('module', 'ckeditor') . '/js/plugins/drupallink';
return array(
'DrupalLink' => array(
'label' => t('Link'),
'image' => $path . '/icons/drupallink.png',
),
'DrupalUnlink' => array(
'label' => t('Unlink'),
'image' => $path . '/icons/drupalunlink.png',
),
);
}
}

View file

@ -0,0 +1,607 @@
<?php
namespace Drupal\ckeditor\Plugin\CKEditorPlugin;
use Drupal\ckeditor\CKEditorPluginBase;
use Drupal\ckeditor\CKEditorPluginContextualInterface;
use Drupal\ckeditor\CKEditorPluginManager;
use Drupal\Component\Utility\Html;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\editor\Entity\Editor;
use Drupal\filter\Plugin\FilterInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines the "internal" plugin (i.e. core plugins part of our CKEditor build).
*
* @CKEditorPlugin(
* id = "internal",
* label = @Translation("CKEditor core")
* )
*/
class Internal extends CKEditorPluginBase implements ContainerFactoryPluginInterface, CKEditorPluginContextualInterface {
/**
* The cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* Constructs a \Drupal\ckeditor\Plugin\CKEditorPlugin\Internal object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* The cache backend.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, CacheBackendInterface $cache_backend) {
$this->cache = $cache_backend;
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* Creates an instance of the plugin.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The container to pull out services used in the plugin.
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
*
* @return static
* Returns an instance of this plugin.
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('cache.default')
);
}
/**
* {@inheritdoc}
*/
public function isInternal() {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function isEnabled(Editor $editor) {
// This plugin represents the core CKEditor plugins. They're always enabled:
// its configuration is always necessary.
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getFile() {
// This plugin is already part of Drupal core's CKEditor build.
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getConfig(Editor $editor) {
// Reasonable defaults that provide expected basic behavior.
$config = array(
'customConfig' => '', // Don't load CKEditor's config.js file.
'pasteFromWordPromptCleanup' => TRUE,
'resize_dir' => 'vertical',
'justifyClasses' => array('text-align-left', 'text-align-center', 'text-align-right', 'text-align-justify'),
'entities' => FALSE,
'disableNativeSpellChecker' => FALSE,
);
// Add the allowedContent setting, which ensures CKEditor only allows tags
// and attributes that are allowed by the text format for this text editor.
list($config['allowedContent'], $config['disallowedContent']) = $this->generateACFSettings($editor);
// Add the format_tags setting, if its button is enabled.
$toolbar_buttons = CKEditorPluginManager::getEnabledButtons($editor);
if (in_array('Format', $toolbar_buttons)) {
$config['format_tags'] = $this->generateFormatTagsSetting($editor);
}
return $config;
}
/**
* {@inheritdoc}
*/
public function getButtons() {
$button = function($name, $direction = 'ltr') {
// In the markup below, we mostly use the name (which may include spaces),
// but in one spot we use it as a CSS class, so strip spaces.
// Note: this uses str_replace() instead of Html::cleanCssIdentifier()
// because we must provide these class names exactly how CKEditor expects
// them in its library, which cleanCssIdentifier() does not do.
$class_name = str_replace(' ', '', $name);
return [
'#type' => 'inline_template',
'#template' => '<a href="#" class="cke-icon-only cke_{{ direction }}" role="button" title="{{ name }}" aria-label="{{ name }}"><span class="cke_button_icon cke_button__{{ classname }}_icon">{{ name }}</span></a>',
'#context' => [
'direction' => $direction,
'name' => $name,
'classname' => $class_name,
],
];
};
return array(
// "basicstyles" plugin.
'Bold' => array(
'label' => t('Bold'),
'image_alternative' => $button('bold'),
'image_alternative_rtl' => $button('bold', 'rtl'),
),
'Italic' => array(
'label' => t('Italic'),
'image_alternative' => $button('italic'),
'image_alternative_rtl' => $button('italic', 'rtl'),
),
'Underline' => array(
'label' => t('Underline'),
'image_alternative' => $button('underline'),
'image_alternative_rtl' => $button('underline', 'rtl'),
),
'Strike' => array(
'label' => t('Strike-through'),
'image_alternative' => $button('strike'),
'image_alternative_rtl' => $button('strike', 'rtl'),
),
'Superscript' => array(
'label' => t('Superscript'),
'image_alternative' => $button('super script'),
'image_alternative_rtl' => $button('super script', 'rtl'),
),
'Subscript' => array(
'label' => t('Subscript'),
'image_alternative' => $button('sub script'),
'image_alternative_rtl' => $button('sub script', 'rtl'),
),
// "removeformat" plugin.
'RemoveFormat' => array(
'label' => t('Remove format'),
'image_alternative' => $button('remove format'),
'image_alternative_rtl' => $button('remove format', 'rtl'),
),
// "justify" plugin.
'JustifyLeft' => array(
'label' => t('Align left'),
'image_alternative' => $button('justify left'),
'image_alternative_rtl' => $button('justify left', 'rtl'),
),
'JustifyCenter' => array(
'label' => t('Align center'),
'image_alternative' => $button('justify center'),
'image_alternative_rtl' => $button('justify center', 'rtl'),
),
'JustifyRight' => array(
'label' => t('Align right'),
'image_alternative' => $button('justify right'),
'image_alternative_rtl' => $button('justify right', 'rtl'),
),
'JustifyBlock' => array(
'label' => t('Justify'),
'image_alternative' => $button('justify block'),
'image_alternative_rtl' => $button('justify block', 'rtl'),
),
// "list" plugin.
'BulletedList' => array(
'label' => t('Bullet list'),
'image_alternative' => $button('bulleted list'),
'image_alternative_rtl' => $button('bulleted list', 'rtl'),
),
'NumberedList' => array(
'label' => t('Numbered list'),
'image_alternative' => $button('numbered list'),
'image_alternative_rtl' => $button('numbered list', 'rtl'),
),
// "indent" plugin.
'Outdent' => array(
'label' => t('Outdent'),
'image_alternative' => $button('outdent'),
'image_alternative_rtl' => $button('outdent', 'rtl'),
),
'Indent' => array(
'label' => t('Indent'),
'image_alternative' => $button('indent'),
'image_alternative_rtl' => $button('indent', 'rtl'),
),
// "undo" plugin.
'Undo' => array(
'label' => t('Undo'),
'image_alternative' => $button('undo'),
'image_alternative_rtl' => $button('undo', 'rtl'),
),
'Redo' => array(
'label' => t('Redo'),
'image_alternative' => $button('redo'),
'image_alternative_rtl' => $button('redo', 'rtl'),
),
// "blockquote" plugin.
'Blockquote' => array(
'label' => t('Blockquote'),
'image_alternative' => $button('blockquote'),
'image_alternative_rtl' => $button('blockquote', 'rtl'),
),
// "horizontalrule" plugin
'HorizontalRule' => array(
'label' => t('Horizontal rule'),
'image_alternative' => $button('horizontal rule'),
'image_alternative_rtl' => $button('horizontal rule', 'rtl'),
),
// "clipboard" plugin.
'Cut' => array(
'label' => t('Cut'),
'image_alternative' => $button('cut'),
'image_alternative_rtl' => $button('cut', 'rtl'),
),
'Copy' => array(
'label' => t('Copy'),
'image_alternative' => $button('copy'),
'image_alternative_rtl' => $button('copy', 'rtl'),
),
'Paste' => array(
'label' => t('Paste'),
'image_alternative' => $button('paste'),
'image_alternative_rtl' => $button('paste', 'rtl'),
),
// "pastetext" plugin.
'PasteText' => array(
'label' => t('Paste Text'),
'image_alternative' => $button('paste text'),
'image_alternative_rtl' => $button('paste text', 'rtl'),
),
// "pastefromword" plugin.
'PasteFromWord' => array(
'label' => t('Paste from Word'),
'image_alternative' => $button('paste from word'),
'image_alternative_rtl' => $button('paste from word', 'rtl'),
),
// "specialchar" plugin.
'SpecialChar' => array(
'label' => t('Character map'),
'image_alternative' => $button('special char'),
'image_alternative_rtl' => $button('special char', 'rtl'),
),
'Format' => array(
'label' => t('HTML block format'),
'image_alternative' => [
'#type' => 'inline_template',
'#template' => '<a href="#" role="button" aria-label="{{ format_text }}"><span class="ckeditor-button-dropdown">{{ format_text }}<span class="ckeditor-button-arrow"></span></span></a>',
'#context' => [
'format_text' => t('Format'),
],
],
),
// "table" plugin.
'Table' => array(
'label' => t('Table'),
'image_alternative' => $button('table'),
'image_alternative_rtl' => $button('table', 'rtl'),
),
// "showblocks" plugin.
'ShowBlocks' => array(
'label' => t('Show blocks'),
'image_alternative' => $button('show blocks'),
'image_alternative_rtl' => $button('show blocks', 'rtl'),
),
// "sourcearea" plugin.
'Source' => array(
'label' => t('Source code'),
'image_alternative' => $button('source'),
'image_alternative_rtl' => $button('source', 'rtl'),
),
// "maximize" plugin.
'Maximize' => array(
'label' => t('Maximize'),
'image_alternative' => $button('maximize'),
'image_alternative_rtl' => $button('maximize', 'rtl'),
),
// No plugin, separator "button" for toolbar builder UI use only.
'-' => array(
'label' => t('Separator'),
'image_alternative' => [
'#type' => 'inline_template',
'#template' => '<a href="#" role="button" aria-label="{{ button_separator_text }}" class="ckeditor-separator"></a>',
'#context' => [
'button_separator_text' => t('Button separator'),
],
],
'attributes' => array(
'class' => array('ckeditor-button-separator'),
'data-drupal-ckeditor-type' => 'separator',
),
'multiple' => TRUE,
),
);
}
/**
* Builds the "format_tags" configuration part of the CKEditor JS settings.
*
* @see getConfig()
*
* @param \Drupal\editor\Entity\Editor $editor
* A configured text editor object.
*
* @return array
* An array containing the "format_tags" configuration.
*/
protected function generateFormatTagsSetting(Editor $editor) {
// When no text format is associated yet, assume no tag is allowed.
// @see \Drupal\Editor\EditorInterface::hasAssociatedFilterFormat()
if (!$editor->hasAssociatedFilterFormat()) {
return array();
}
$format = $editor->getFilterFormat();
$cid = 'ckeditor_internal_format_tags:' . $format->id();
if ($cached = $this->cache->get($cid)) {
$format_tags = $cached->data;
}
else {
// The <p> tag is always allowed — HTML without <p> tags is nonsensical.
$format_tags = ['p'];
// Given the list of possible format tags, automatically determine whether
// the current text format allows this tag, and thus whether it should show
// up in the "Format" dropdown.
$possible_format_tags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre'];
foreach ($possible_format_tags as $tag) {
$input = '<' . $tag . '>TEST</' . $tag . '>';
$output = trim(check_markup($input, $editor->id()));
if (Html::load($output)->getElementsByTagName($tag)->length !== 0) {
$format_tags[] = $tag;
}
}
$format_tags = implode(';', $format_tags);
// Cache the "format_tags" configuration. This cache item is infinitely
// valid; it only changes whenever the text format is changed, hence it's
// tagged with the text format's cache tag.
$this->cache->set($cid, $format_tags, Cache::PERMANENT, $format->getCacheTags());
}
return $format_tags;
}
/**
* Builds the ACF part of the CKEditor JS settings.
*
* This ensures that CKEditor obeys the HTML restrictions defined by Drupal's
* filter system, by enabling CKEditor's Advanced Content Filter (ACF)
* functionality: http://ckeditor.com/blog/CKEditor-4.1-RC-Released.
*
* @see getConfig()
*
* @param \Drupal\editor\Entity\Editor $editor
* A configured text editor object.
*
* @return array
* An array with two values:
* - the first value is the "allowedContent" setting: a well-formatted array
* or TRUE. The latter indicates that anything is allowed.
* - the second value is the "disallowedContent" setting: a well-formatted
* array or FALSE. The latter indicates that nothing is disallowed.
*/
protected function generateACFSettings(Editor $editor) {
// When no text format is associated yet, assume nothing is disallowed, so
// set allowedContent to true.
if (!$editor->hasAssociatedFilterFormat()) {
return TRUE;
}
$format = $editor->getFilterFormat();
$filter_types = $format->getFilterTypes();
// When nothing is disallowed, set allowedContent to true.
if (!in_array(FilterInterface::TYPE_HTML_RESTRICTOR, $filter_types)) {
return array(TRUE, FALSE);
}
// Generate setting that accurately reflects allowed tags and attributes.
else {
$get_attribute_values = function($attribute_values, $allowed_values) {
$values = array_keys(array_filter($attribute_values, function($value) use ($allowed_values) {
if ($allowed_values) {
return $value !== FALSE;
}
else {
return $value === FALSE;
}
}));
if (count($values)) {
return implode(',', $values);
}
else {
return NULL;
}
};
$html_restrictions = $format->getHtmlRestrictions();
// When all HTML is allowed, also set allowedContent to true and
// disallowedContent to false.
if ($html_restrictions === FALSE) {
return array(TRUE, FALSE);
}
$allowed = array();
$disallowed = array();
if (isset($html_restrictions['forbidden_tags'])) {
foreach ($html_restrictions['forbidden_tags'] as $tag) {
$disallowed[$tag] = TRUE;
}
}
foreach ($html_restrictions['allowed'] as $tag => $attributes) {
// Tell CKEditor the tag is allowed, but no attributes.
if ($attributes === FALSE) {
$allowed[$tag] = array(
'attributes' => FALSE,
'styles' => FALSE,
'classes' => FALSE,
);
}
// Tell CKEditor the tag is allowed, as well as any attribute on it. The
// "style" and "class" attributes are handled separately by CKEditor:
// they are disallowed even if you specify it in the list of allowed
// attributes, unless you state specific values for them that are
// allowed. Or, in this case: any value for them is allowed.
elseif ($attributes === TRUE) {
$allowed[$tag] = array(
'attributes' => TRUE,
'styles' => TRUE,
'classes' => TRUE,
);
// We've just marked that any value for the "style" and "class"
// attributes is allowed. However, that may not be the case: the "*"
// tag may still apply restrictions.
// Since CKEditor's ACF follows the following principle:
// Once validated, an element or its property cannot be
// invalidated by another rule.
// That means that the most permissive setting wins. Which means that
// it will still be allowed by CKEditor, for instance, to define any
// style, no matter what the "*" tag's restrictions may be. If there
// is a setting for either the "style" or "class" attribute, it cannot
// possibly be more permissive than what was set above. Hence, inherit
// from the "*" tag where possible.
if (isset($html_restrictions['allowed']['*'])) {
$wildcard = $html_restrictions['allowed']['*'];
if (isset($wildcard['style'])) {
if (!is_array($wildcard['style'])) {
$allowed[$tag]['styles'] = $wildcard['style'];
}
else {
$allowed_styles = $get_attribute_values($wildcard['style'], TRUE);
if (isset($allowed_styles)) {
$allowed[$tag]['styles'] = $allowed_styles;
}
else {
unset($allowed[$tag]['styles']);
}
}
}
if (isset($wildcard['class'])) {
if (!is_array($wildcard['class'])) {
$allowed[$tag]['classes'] = $wildcard['class'];
}
else {
$allowed_classes = $get_attribute_values($wildcard['class'], TRUE);
if (isset($allowed_classes)) {
$allowed[$tag]['classes'] = $allowed_classes;
}
else {
unset($allowed[$tag]['classes']);
}
}
}
}
}
// Tell CKEditor the tag is allowed, along with some tags.
elseif (is_array($attributes)) {
// Set defaults (these will be overridden below if more specific
// values are present).
$allowed[$tag] = array(
'attributes' => FALSE,
'styles' => FALSE,
'classes' => FALSE,
);
// Configure allowed attributes, allowed "style" attribute values and
// allowed "class" attribute values.
// CKEditor only allows specific values for the "class" and "style"
// attributes; so ignore restrictions on other attributes, which
// Drupal filters may provide.
// NOTE: A Drupal contrib module can subclass this class, override the
// getConfig() method, and override the JavaScript at
// Drupal.editors.ckeditor to somehow make validation of values for
// attributes other than "class" and "style" work.
$allowed_attributes = array_filter($attributes, function($value) {
return $value !== FALSE;
});
if (count($allowed_attributes)) {
$allowed[$tag]['attributes'] = implode(',', array_keys($allowed_attributes));
}
if (isset($allowed_attributes['style'])) {
if (is_bool($allowed_attributes['style'])) {
$allowed[$tag]['styles'] = $allowed_attributes['style'];
}
elseif (is_array($allowed_attributes['style'])) {
$allowed_classes = $get_attribute_values($allowed_attributes['style'], TRUE);
if (isset($allowed_classes)) {
$allowed[$tag]['styles'] = $allowed_classes;
}
}
}
if (isset($allowed_attributes['class'])) {
if (is_bool($allowed_attributes['class'])) {
$allowed[$tag]['classes'] = $allowed_attributes['class'];
}
elseif (is_array($allowed_attributes['class'])) {
$allowed_classes = $get_attribute_values($allowed_attributes['class'], TRUE);
if (isset($allowed_classes)) {
$allowed[$tag]['classes'] = $allowed_classes;
}
}
}
// Handle disallowed attributes analogously. However, to handle *dis-
// allowed* attribute values, we must look at *allowed* attributes'
// disallowed attribute values! After all, a disallowed attribute
// implies that all of its possible attribute values are disallowed,
// thus we must look at the disallowed attribute values on allowed
// attributes.
$disallowed_attributes = array_filter($attributes, function($value) {
return $value === FALSE;
});
if (count($disallowed_attributes)) {
// No need to blacklist the 'class' or 'style' attributes; CKEditor
// handles them separately (if no specific class or style attribute
// values are allowed, then those attributes are disallowed).
if (isset($disallowed_attributes['class'])) {
unset($disallowed_attributes['class']);
}
if (isset($disallowed_attributes['style'])) {
unset($disallowed_attributes['style']);
}
$disallowed[$tag]['attributes'] = implode(',', array_keys($disallowed_attributes));
}
if (isset($allowed_attributes['style']) && is_array($allowed_attributes['style'])) {
$disallowed_styles = $get_attribute_values($allowed_attributes['style'], FALSE);
if (isset($disallowed_styles)) {
$disallowed[$tag]['styles'] = $disallowed_styles;
}
}
if (isset($allowed_attributes['class']) && is_array($allowed_attributes['class'])) {
$disallowed_classes = $get_attribute_values($allowed_attributes['class'], FALSE);
if (isset($disallowed_classes)) {
$disallowed[$tag]['classes'] = $disallowed_classes;
}
}
}
}
ksort($allowed);
ksort($disallowed);
return array($allowed, $disallowed);
}
}
}

View file

@ -0,0 +1,135 @@
<?php
namespace Drupal\ckeditor\Plugin\CKEditorPlugin;
use Drupal\ckeditor\CKEditorPluginBase;
use Drupal\ckeditor\CKEditorPluginConfigurableInterface;
use Drupal\ckeditor\CKEditorPluginCssInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManager;
use Drupal\Core\Language\LanguageInterface;
use Drupal\editor\Entity\Editor;
/**
* Defines the "language" plugin.
*
* @CKEditorPlugin(
* id = "language",
* label = @Translation("Language")
* )
*/
class Language extends CKEditorPluginBase implements CKEditorPluginConfigurableInterface, CKEditorPluginCssInterface {
/**
* {@inheritdoc}
*/
public function isInternal() {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getFile() {
// This plugin is already part of Drupal core's CKEditor build.
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getLibraries(Editor $editor) {
return ['ckeditor/drupal.ckeditor.plugins.language'];
}
/**
* {@inheritdoc}
*/
public function getConfig(Editor $editor) {
$language_list = [];
$config = ['language_list' => 'un'];
$settings = $editor->getSettings();
if (isset($settings['plugins']['language'])) {
$config = $settings['plugins']['language'];
}
$predefined_languages = ($config['language_list'] === 'all') ?
LanguageManager::getStandardLanguageList() :
LanguageManager::getUnitedNationsLanguageList();
// Generate the language_list setting as expected by the CKEditor Language
// plugin, but key the values by the full language name so that we can sort
// them later on.
foreach ($predefined_languages as $langcode => $language) {
$english_name = $language[0];
$direction = empty($language[2]) ? NULL : $language[2];
if ($direction === LanguageInterface::DIRECTION_RTL) {
$language_list[$english_name] = $langcode . ':' . $english_name . ':rtl';
}
else {
$language_list[$english_name] = $langcode . ':' . $english_name;
}
}
// Sort on full language name.
ksort($language_list);
$config = ['language_list' => array_values($language_list)];
return $config;
}
/**
* {@inheritdoc}
*/
public function getButtons() {
return [
'Language' => [
'label' => $this->t('Language'),
'image_alternative' => [
'#type' => 'inline_template',
'#template' => '<a href="#" class="cke-icon-only" role="button" title="' . $this->t('Language') . '" aria-label="' . $this->t('Language') . '"><span class="cke_button_icon cke_button__language_icon">' . $this->t('Language') . '</span></a>',
],
],
];
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state, Editor $editor) {
// Defaults.
$config = ['language_list' => 'un'];
$settings = $editor->getSettings();
if (isset($settings['plugins']['language'])) {
$config = $settings['plugins']['language'];
}
$predefined_languages = LanguageManager::getStandardLanguageList();
$form['language_list'] = array(
'#title' => $this->t('Language list'),
'#title_display' => 'invisible',
'#type' => 'select',
'#options' => [
'un' => $this->t("United Nations' official languages"),
'all' => $this->t('All @count languages', ['@count' => count($predefined_languages)]),
],
'#default_value' => $config['language_list'],
'#description' => $this->t('The list of languages to show in the language dropdown. The basic list will only show the <a href=":url">six official languages of the UN</a>. The extended list will show all @count languages that are available in Drupal.', [
':url' => 'https://www.un.org/en/sections/about-un/official-languages',
'@count' => count($predefined_languages),
]),
'#attached' => ['library' => ['ckeditor/drupal.ckeditor.language.admin']],
);
return $form;
}
/**
* {@inheritdoc}
*/
function getCssFiles(Editor $editor) {
return array(
drupal_get_path('module', 'ckeditor') . '/css/plugins/language/ckeditor.language.css'
);
}
}

View file

@ -0,0 +1,166 @@
<?php
namespace Drupal\ckeditor\Plugin\CKEditorPlugin;
use Drupal\ckeditor\CKEditorPluginBase;
use Drupal\ckeditor\CKEditorPluginConfigurableInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\editor\Entity\Editor;
/**
* Defines the "stylescombo" plugin.
*
* @CKEditorPlugin(
* id = "stylescombo",
* label = @Translation("Styles dropdown")
* )
*/
class StylesCombo extends CKEditorPluginBase implements CKEditorPluginConfigurableInterface {
/**
* {@inheritdoc}
*/
public function isInternal() {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getFile() {
// This plugin is already part of Drupal core's CKEditor build.
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getConfig(Editor $editor) {
$config = array();
$settings = $editor->getSettings();
if (!isset($settings['plugins']['stylescombo']['styles'])) {
return $config;
}
$styles = $settings['plugins']['stylescombo']['styles'];
$config['stylesSet'] = $this->generateStylesSetSetting($styles);
return $config;
}
/**
* {@inheritdoc}
*/
public function getButtons() {
return array(
'Styles' => array(
'label' => t('Font style'),
'image_alternative' => [
'#type' => 'inline_template',
'#template' => '<a href="#" role="button" aria-label="{{ styles_text }}"><span class="ckeditor-button-dropdown">{{ styles_text }}<span class="ckeditor-button-arrow"></span></span></a>',
'#context' => [
'styles_text' => t('Styles'),
],
],
),
);
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state, Editor $editor) {
// Defaults.
$config = array('styles' => '');
$settings = $editor->getSettings();
if (isset($settings['plugins']['stylescombo'])) {
$config = $settings['plugins']['stylescombo'];
}
$form['styles'] = array(
'#title' => t('Styles'),
'#title_display' => 'invisible',
'#type' => 'textarea',
'#default_value' => $config['styles'],
'#description' => t('A list of classes that will be provided in the "Styles" dropdown. Enter one or more classes on each line in the format: element.classA.classB|Label. Example: h1.title|Title. Advanced example: h1.fancy.title|Fancy title.<br />These styles should be available in your theme\'s CSS file.'),
'#attached' => array(
'library' => array('ckeditor/drupal.ckeditor.stylescombo.admin'),
),
'#element_validate' => array(
array($this, 'validateStylesValue'),
),
);
return $form;
}
/**
* #element_validate handler for the "styles" element in settingsForm().
*/
public function validateStylesValue(array $element, FormStateInterface $form_state) {
$styles_setting = $this->generateStylesSetSetting($element['#value']);
if ($styles_setting === FALSE) {
$form_state->setError($element, t('The provided list of styles is syntactically incorrect.'));
}
else {
$style_names = array_map(function ($style) { return $style['name']; }, $styles_setting);
if (count($style_names) !== count(array_unique($style_names))) {
$form_state->setError($element, t('Each style must have a unique label.'));
}
}
}
/**
* Builds the "stylesSet" configuration part of the CKEditor JS settings.
*
* @see getConfig()
*
* @param string $styles
* The "styles" setting.
* @return array|false
* An array containing the "stylesSet" configuration, or FALSE when the
* syntax is invalid.
*/
protected function generateStylesSetSetting($styles) {
$styles_set = array();
// Early-return when empty.
$styles = trim($styles);
if (empty($styles)) {
return $styles_set;
}
$styles = str_replace(array("\r\n", "\r"), "\n", $styles);
foreach (explode("\n", $styles) as $style) {
$style = trim($style);
// Ignore empty lines in between non-empty lines.
if (empty($style)) {
continue;
}
// Validate syntax: element[.class...]|label pattern expected.
if (!preg_match('@^ *[a-zA-Z0-9]+ *(\\.[a-zA-Z0-9_-]+ *)*\\| *.+ *$@', $style)) {
return FALSE;
}
// Parse.
list($selector, $label) = explode('|', $style);
$classes = explode('.', $selector);
$element = array_shift($classes);
// Build the data structure CKEditor's stylescombo plugin expects.
// @see http://docs.cksource.com/CKEditor_3.x/Developers_Guide/Styles
$configured_style = array(
'name' => trim($label),
'element' => trim($element),
);
if (!empty($classes)) {
$configured_style['attributes'] = array(
'class' => implode(' ', array_map('trim', $classes))
);
}
$styles_set[] = $configured_style;
}
return $styles_set;
}
}

View file

@ -0,0 +1,432 @@
<?php
namespace Drupal\ckeditor\Plugin\Editor;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\ckeditor\CKEditorPluginManager;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\RendererInterface;
use Drupal\editor\Plugin\EditorBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\editor\Entity\Editor;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a CKEditor-based text editor for Drupal.
*
* @Editor(
* id = "ckeditor",
* label = @Translation("CKEditor"),
* supports_content_filtering = TRUE,
* supports_inline_editing = TRUE,
* is_xss_safe = FALSE,
* supported_element_types = {
* "textarea"
* }
* )
*/
class CKEditor extends EditorBase implements ContainerFactoryPluginInterface {
/**
* The module handler to invoke hooks on.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The CKEditor plugin manager.
*
* @var \Drupal\ckeditor\CKEditorPluginManager
*/
protected $ckeditorPluginManager;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs a Drupal\Component\Plugin\PluginBase object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\ckeditor\CKEditorPluginManager $ckeditor_plugin_manager
* The CKEditor plugin manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke hooks on.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, CKEditorPluginManager $ckeditor_plugin_manager, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager, RendererInterface $renderer) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->ckeditorPluginManager = $ckeditor_plugin_manager;
$this->moduleHandler = $module_handler;
$this->languageManager = $language_manager;
$this->renderer = $renderer;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('plugin.manager.ckeditor.plugin'),
$container->get('module_handler'),
$container->get('language_manager'),
$container->get('renderer')
);
}
/**
* {@inheritdoc}
*/
public function getDefaultSettings() {
return array(
'toolbar' => array(
'rows' => array(
// Button groups.
array(
array(
'name' => t('Formatting'),
'items' => array('Bold', 'Italic',),
),
array(
'name' => t('Links'),
'items' => array('DrupalLink', 'DrupalUnlink',),
),
array(
'name' => t('Lists'),
'items' => array('BulletedList', 'NumberedList',),
),
array(
'name' => t('Media'),
'items' => array('Blockquote', 'DrupalImage',),
),
array(
'name' => t('Tools'),
'items' => array('Source',),
),
),
),
),
'plugins' => ['language' => ['language_list' => 'un']],
);
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state, Editor $editor) {
$settings = $editor->getSettings();
$ckeditor_settings_toolbar = array(
'#theme' => 'ckeditor_settings_toolbar',
'#editor' => $editor,
'#plugins' => $this->ckeditorPluginManager->getButtons(),
);
$form['toolbar'] = array(
'#type' => 'container',
'#attached' => array(
'library' => array('ckeditor/drupal.ckeditor.admin'),
'drupalSettings' => [
'ckeditor' => [
'toolbarAdmin' => (string) $this->renderer->renderPlain($ckeditor_settings_toolbar),
],
],
),
'#attributes' => array('class' => array('ckeditor-toolbar-configuration')),
);
$form['toolbar']['button_groups'] = array(
'#type' => 'textarea',
'#title' => t('Toolbar buttons'),
'#default_value' => json_encode($settings['toolbar']['rows']),
'#attributes' => array('class' => array('ckeditor-toolbar-textarea')),
);
// CKEditor plugin settings, if any.
$form['plugin_settings'] = array(
'#type' => 'vertical_tabs',
'#title' => t('CKEditor plugin settings'),
'#attributes' => array(
'id' => 'ckeditor-plugin-settings',
),
);
$this->ckeditorPluginManager->injectPluginSettingsForm($form, $form_state, $editor);
if (count(Element::children($form['plugins'])) === 0) {
unset($form['plugins']);
unset($form['plugin_settings']);
}
// Hidden CKEditor instance. We need a hidden CKEditor instance with all
// plugins enabled, so we can retrieve CKEditor's per-feature metadata (on
// which tags, attributes, styles and classes are enabled). This metadata is
// necessary for certain filters' (for instance, the html_filter filter)
// settings to be updated accordingly.
// Get a list of all external plugins and their corresponding files.
$plugins = array_keys($this->ckeditorPluginManager->getDefinitions());
$all_external_plugins = array();
foreach ($plugins as $plugin_id) {
$plugin = $this->ckeditorPluginManager->createInstance($plugin_id);
if (!$plugin->isInternal()) {
$all_external_plugins[$plugin_id] = $plugin->getFile();
}
}
// Get a list of all buttons that are provided by all plugins.
$all_buttons = array_reduce($this->ckeditorPluginManager->getButtons(), function($result, $item) {
return array_merge($result, array_keys($item));
}, array());
// Build a fake Editor object, which we'll use to generate JavaScript
// settings for this fake Editor instance.
$fake_editor = Editor::create(array(
'format' => $editor->id(),
'editor' => 'ckeditor',
'settings' => array(
// Single toolbar row, single button group, all existing buttons.
'toolbar' => array(
'rows' => array(
0 => array(
0 => array(
'name' => 'All existing buttons',
'items' => $all_buttons,
)
)
),
),
'plugins' => $settings['plugins'],
),
));
$config = $this->getJSSettings($fake_editor);
// Remove the ACF configuration that is generated based on filter settings,
// because otherwise we cannot retrieve per-feature metadata.
unset($config['allowedContent']);
$form['hidden_ckeditor'] = array(
'#markup' => '<div id="ckeditor-hidden" class="hidden"></div>',
'#attached' => array(
'drupalSettings' => ['ckeditor' => ['hiddenCKEditorConfig' => $config]],
),
);
return $form;
}
/**
* {@inheritdoc}
*/
public function settingsFormSubmit(array $form, FormStateInterface $form_state) {
// Modify the toolbar settings by reference. The values in
// $form_state->getValue(array('editor', 'settings')) will be saved directly
// by editor_form_filter_admin_format_submit().
$toolbar_settings = &$form_state->getValue(array('editor', 'settings', 'toolbar'));
// The rows key is not built into the form structure, so decode the button
// groups data into this new key and remove the button_groups key.
$toolbar_settings['rows'] = json_decode($toolbar_settings['button_groups'], TRUE);
unset($toolbar_settings['button_groups']);
// Remove the plugin settings' vertical tabs state; no need to save that.
if ($form_state->hasValue(array('editor', 'settings', 'plugins'))) {
$form_state->unsetValue(array('editor', 'settings', 'plugin_settings'));
}
}
/**
* {@inheritdoc}
*/
public function getJSSettings(Editor $editor) {
$settings = array();
// Get the settings for all enabled plugins, even the internal ones.
$enabled_plugins = array_keys($this->ckeditorPluginManager->getEnabledPluginFiles($editor, TRUE));
foreach ($enabled_plugins as $plugin_id) {
$plugin = $this->ckeditorPluginManager->createInstance($plugin_id);
$settings += $plugin->getConfig($editor);
}
// Fall back on English if no matching language code was found.
$display_langcode = 'en';
// Map the interface language code to a CKEditor translation if interface
// translation is enabled.
if ($this->moduleHandler->moduleExists('locale')) {
$ckeditor_langcodes = $this->getLangcodes();
$language_interface = $this->languageManager->getCurrentLanguage();
if (isset($ckeditor_langcodes[$language_interface->getId()])) {
$display_langcode = $ckeditor_langcodes[$language_interface->getId()];
}
}
// Next, set the most fundamental CKEditor settings.
$external_plugin_files = $this->ckeditorPluginManager->getEnabledPluginFiles($editor);
$settings += array(
'toolbar' => $this->buildToolbarJSSetting($editor),
'contentsCss' => $this->buildContentsCssJSSetting($editor),
'extraPlugins' => implode(',', array_keys($external_plugin_files)),
'language' => $display_langcode,
// Configure CKEditor to not load styles.js. The StylesCombo plugin will
// set stylesSet according to the user's settings, if the "Styles" button
// is enabled. We cannot get rid of this until CKEditor will stop loading
// styles.js by default.
// See http://dev.ckeditor.com/ticket/9992#comment:9.
'stylesSet' => FALSE,
);
// Finally, set Drupal-specific CKEditor settings.
$root_relative_file_url = function ($uri) {
return file_url_transform_relative(file_create_url($uri));
};
$settings += array(
'drupalExternalPlugins' => array_map($root_relative_file_url, $external_plugin_files),
);
// Parse all CKEditor plugin JavaScript files for translations.
if ($this->moduleHandler->moduleExists('locale')) {
locale_js_translate(array_values($external_plugin_files));
}
ksort($settings);
return $settings;
}
/**
* Returns a list of language codes supported by CKEditor.
*
* @return array
* An associative array keyed by language codes.
*/
public function getLangcodes() {
// Cache the file system based language list calculation because this would
// be expensive to calculate all the time. The cache is cleared on core
// upgrades which is the only situation the CKEditor file listing should
// change.
$langcode_cache = \Drupal::cache()->get('ckeditor.langcodes');
if (!empty($langcode_cache)) {
$langcodes = $langcode_cache->data;
}
if (empty($langcodes)) {
$langcodes = array();
// Collect languages included with CKEditor based on file listing.
$files = scandir('core/assets/vendor/ckeditor/lang');
foreach ($files as $file) {
if ($file[0] !== '.' && preg_match('/\.js$/', $file)) {
$langcode = basename($file, '.js');
$langcodes[$langcode] = $langcode;
}
}
\Drupal::cache()->set('ckeditor.langcodes', $langcodes);
}
// Get language mapping if available to map to Drupal language codes.
// This is configurable in the user interface and not expensive to get, so
// we don't include it in the cached language list.
$language_mappings = $this->moduleHandler->moduleExists('language') ? language_get_browser_drupal_langcode_mappings() : array();
foreach ($langcodes as $langcode) {
// If this language code is available in a Drupal mapping, use that to
// compute a possibility for matching from the Drupal langcode to the
// CKEditor langcode.
// For instance, CKEditor uses the langcode 'no' for Norwegian, Drupal
// uses 'nb'. This would then remove the 'no' => 'no' mapping and replace
// it with 'nb' => 'no'. Now Drupal knows which CKEditor translation to
// load.
if (isset($language_mappings[$langcode]) && !isset($langcodes[$language_mappings[$langcode]])) {
$langcodes[$language_mappings[$langcode]] = $langcode;
unset($langcodes[$langcode]);
}
}
return $langcodes;
}
/**
* {@inheritdoc}
*/
public function getLibraries(Editor $editor) {
$libraries = array(
'ckeditor/drupal.ckeditor',
);
// Get the required libraries for any enabled plugins.
$enabled_plugins = array_keys($this->ckeditorPluginManager->getEnabledPluginFiles($editor));
foreach ($enabled_plugins as $plugin_id) {
$plugin = $this->ckeditorPluginManager->createInstance($plugin_id);
$additional_libraries = array_diff($plugin->getLibraries($editor), $libraries);
$libraries = array_merge($libraries, $additional_libraries);
}
return $libraries;
}
/**
* Builds the "toolbar" configuration part of the CKEditor JS settings.
*
* @see getJSSettings()
*
* @param \Drupal\editor\Entity\Editor $editor
* A configured text editor object.
* @return array
* An array containing the "toolbar" configuration.
*/
public function buildToolbarJSSetting(Editor $editor) {
$toolbar = array();
$settings = $editor->getSettings();
foreach ($settings['toolbar']['rows'] as $row) {
foreach ($row as $group) {
$toolbar[] = $group;
}
$toolbar[] = '/';
}
return $toolbar;
}
/**
* Builds the "contentsCss" configuration part of the CKEditor JS settings.
*
* @see getJSSettings()
*
* @param \Drupal\editor\Entity\Editor $editor
* A configured text editor object.
* @return array
* An array containing the "contentsCss" configuration.
*/
public function buildContentsCssJSSetting(Editor $editor) {
$css = array(
drupal_get_path('module', 'ckeditor') . '/css/ckeditor-iframe.css',
drupal_get_path('module', 'system') . '/css/components/align.module.css',
);
$this->moduleHandler->alter('ckeditor_css', $css, $editor);
// Get a list of all enabled plugins' iframe instance CSS files.
$plugins_css = array_reduce($this->ckeditorPluginManager->getCssFiles($editor), function($result, $item) {
return array_merge($result, array_values($item));
}, array());
$css = array_merge($css, $plugins_css);
$css = array_merge($css, _ckeditor_theme_css());
$css = array_map('file_create_url', $css);
$css = array_map('file_url_transform_relative', $css);
return array_values($css);
}
}

View file

@ -0,0 +1,284 @@
<?php
namespace Drupal\ckeditor\Tests;
use Drupal\Component\Serialization\Json;
use Drupal\editor\Entity\Editor;
use Drupal\filter\FilterFormatInterface;
use Drupal\simpletest\WebTestBase;
use Drupal\filter\Entity\FilterFormat;
/**
* Tests administration of CKEditor.
*
* @group ckeditor
*/
class CKEditorAdminTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('filter', 'editor', 'ckeditor');
/**
* A user with the 'administer filters' permission.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
protected function setUp() {
parent::setUp();
// Create text format.
$filtered_html_format = FilterFormat::create(array(
'format' => 'filtered_html',
'name' => 'Filtered HTML',
'weight' => 0,
'filters' => array(),
));
$filtered_html_format->save();
// Create admin user.
$this->adminUser = $this->drupalCreateUser(array('administer filters'));
}
/**
* Tests configuring a text editor for an existing text format.
*/
function testExistingFormat() {
$ckeditor = $this->container->get('plugin.manager.editor')->createInstance('ckeditor');
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/config/content/formats/manage/filtered_html');
// Ensure no Editor config entity exists yet.
$editor = Editor::load('filtered_html');
$this->assertFalse($editor, 'No Editor config entity exists yet.');
// Verify the "Text Editor" <select> when a text editor is available.
$select = $this->xpath('//select[@name="editor[editor]"]');
$select_is_disabled = $this->xpath('//select[@name="editor[editor]" and @disabled="disabled"]');
$options = $this->xpath('//select[@name="editor[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.');
$this->assertTrue(((string) $options[0]) === 'None', 'Option 1 in the Text Editor select is "None".');
$this->assertTrue(((string) $options[1]) === 'CKEditor', 'Option 2 in the Text Editor select is "CKEditor".');
$this->assertTrue(((string) $options[0]['selected']) === 'selected', 'Option 1 ("None") is selected.');
// Select the "CKEditor" editor and click the "Save configuration" button.
$edit = array(
'editor[editor]' => 'ckeditor',
);
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
$this->assertRaw(t('You must configure the selected text editor.'));
// Ensure the CKEditor editor returns the expected default settings.
$expected_default_settings = array(
'toolbar' => array(
'rows' => array(
// Button groups
array(
array(
'name' => 'Formatting',
'items' => array('Bold', 'Italic',),
),
array(
'name' => 'Links',
'items' => array('DrupalLink', 'DrupalUnlink',),
),
array(
'name' => 'Lists',
'items' => array('BulletedList', 'NumberedList',),
),
array(
'name' => 'Media',
'items' => array('Blockquote', 'DrupalImage',),
),
array(
'name' => 'Tools',
'items' => array('Source',),
),
),
),
),
'plugins' => ['language' => ['language_list' => 'un']],
);
$this->assertIdentical($this->castSafeStrings($ckeditor->getDefaultSettings()), $expected_default_settings);
// Keep the "CKEditor" editor selected and click the "Configure" button.
$this->drupalPostAjaxForm(NULL, $edit, 'editor_configure');
$editor = Editor::load('filtered_html');
$this->assertFalse($editor, 'No Editor config entity exists yet.');
// Ensure that drupalSettings is correct.
$ckeditor_settings_toolbar = array(
'#theme' => 'ckeditor_settings_toolbar',
'#editor' => Editor::create(['editor' => 'ckeditor']),
'#plugins' => $this->container->get('plugin.manager.ckeditor.plugin')->getButtons(),
);
$this->assertEqual(
$this->drupalSettings['ckeditor']['toolbarAdmin'],
$this->container->get('renderer')->renderPlain($ckeditor_settings_toolbar),
'CKEditor toolbar settings are rendered as part of drupalSettings.'
);
// Ensure the toolbar buttons configuration value is initialized to the
// expected default value.
$expected_buttons_value = json_encode($expected_default_settings['toolbar']['rows']);
$this->assertFieldByName('editor[settings][toolbar][button_groups]', $expected_buttons_value);
// Ensure the styles textarea exists and is initialized empty.
$styles_textarea = $this->xpath('//textarea[@name="editor[settings][plugins][stylescombo][styles]"]');
$this->assertFieldByXPath('//textarea[@name="editor[settings][plugins][stylescombo][styles]"]', '', 'The styles textarea exists and is empty.');
$this->assertTrue(count($styles_textarea) === 1, 'The "styles" textarea exists.');
// Submit the form to save the selection of CKEditor as the chosen editor.
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
// Ensure an Editor object exists now, with the proper settings.
$expected_settings = $expected_default_settings;
$expected_settings['plugins']['stylescombo']['styles'] = '';
$editor = Editor::load('filtered_html');
$this->assertTrue($editor instanceof Editor, 'An Editor config entity exists now.');
$this->assertEqual($expected_settings, $editor->getSettings(), 'The Editor config entity has the correct settings.');
// Configure the Styles plugin, and ensure the updated settings are saved.
$this->drupalGet('admin/config/content/formats/manage/filtered_html');
$edit = array(
'editor[settings][plugins][stylescombo][styles]' => "h1.title|Title\np.callout|Callout\n\n",
);
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
$expected_settings['plugins']['stylescombo']['styles'] = "h1.title|Title\np.callout|Callout\n\n";
$editor = Editor::load('filtered_html');
$this->assertTrue($editor instanceof Editor, 'An Editor config entity exists.');
$this->assertEqual($expected_settings, $editor->getSettings(), 'The Editor config entity has the correct settings.');
// Change the buttons that appear on the toolbar (in JavaScript, this is
// done via drag and drop, but here we can only emulate the end result of
// that interaction). Test multiple toolbar rows and a divider within a row.
$this->drupalGet('admin/config/content/formats/manage/filtered_html');
$expected_settings['toolbar']['rows'][0][] = array(
'name' => 'Action history',
'items' => array('Undo', '|', 'Redo', 'JustifyCenter'),
);
$edit = array(
'editor[settings][toolbar][button_groups]' => json_encode($expected_settings['toolbar']['rows']),
);
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
$editor = Editor::load('filtered_html');
$this->assertTrue($editor instanceof Editor, 'An Editor config entity exists.');
$this->assertEqual($expected_settings, $editor->getSettings(), 'The Editor config entity has the correct settings.');
// Check that the markup we're setting for the toolbar buttons (actually in
// JavaScript's drupalSettings, and Unicode-escaped) is correctly rendered.
$this->drupalGet('admin/config/content/formats/manage/filtered_html');
// Create function to encode HTML as we expect it in drupalSettings.
$json_encode = function($html) {
return trim(Json::encode($html), '"');
};
// Check the Button separator.
$this->assertRaw($json_encode('<li data-drupal-ckeditor-button-name="-" class="ckeditor-button-separator ckeditor-multiple-button" data-drupal-ckeditor-type="separator"><a href="#" role="button" aria-label="Button separator" class="ckeditor-separator"></a></li>'));
// Check the Format dropdown.
$this->assertRaw($json_encode('<li data-drupal-ckeditor-button-name="Format" class="ckeditor-button"><a href="#" role="button" aria-label="Format"><span class="ckeditor-button-dropdown">Format<span class="ckeditor-button-arrow"></span></span></a></li>'));
// Check the Styles dropdown.
$this->assertRaw($json_encode('<li data-drupal-ckeditor-button-name="Styles" class="ckeditor-button"><a href="#" role="button" aria-label="Styles"><span class="ckeditor-button-dropdown">Styles<span class="ckeditor-button-arrow"></span></span></a></li>'));
// Check strikethrough.
$this->assertRaw($json_encode('<li data-drupal-ckeditor-button-name="Strike" class="ckeditor-button"><a href="#" class="cke-icon-only cke_ltr" role="button" title="strike" aria-label="strike"><span class="cke_button_icon cke_button__strike_icon">strike</span></a></li>'));
// Now enable the ckeditor_test module, which provides one configurable
// CKEditor plugin — this should not affect the Editor config entity.
\Drupal::service('module_installer')->install(array('ckeditor_test'));
$this->resetAll();
$this->container->get('plugin.manager.ckeditor.plugin')->clearCachedDefinitions();
$this->drupalGet('admin/config/content/formats/manage/filtered_html');
$ultra_llama_mode_checkbox = $this->xpath('//input[@type="checkbox" and @name="editor[settings][plugins][llama_contextual_and_button][ultra_llama_mode]" and not(@checked)]');
$this->assertTrue(count($ultra_llama_mode_checkbox) === 1, 'The "Ultra llama mode" checkbox exists and is not checked.');
$editor = Editor::load('filtered_html');
$this->assertTrue($editor instanceof Editor, 'An Editor config entity exists.');
$this->assertEqual($expected_settings, $editor->getSettings(), 'The Editor config entity has the correct settings.');
// Finally, check the "Ultra llama mode" checkbox.
$this->drupalGet('admin/config/content/formats/manage/filtered_html');
$edit = array(
'editor[settings][plugins][llama_contextual_and_button][ultra_llama_mode]' => '1',
);
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
$this->drupalGet('admin/config/content/formats/manage/filtered_html');
$ultra_llama_mode_checkbox = $this->xpath('//input[@type="checkbox" and @name="editor[settings][plugins][llama_contextual_and_button][ultra_llama_mode]" and @checked="checked"]');
$this->assertTrue(count($ultra_llama_mode_checkbox) === 1, 'The "Ultra llama mode" checkbox exists and is checked.');
$expected_settings['plugins']['llama_contextual_and_button']['ultra_llama_mode'] = TRUE;
$editor = Editor::load('filtered_html');
$this->assertTrue($editor instanceof Editor, 'An Editor config entity exists.');
$this->assertEqual($expected_settings, $editor->getSettings());
}
/**
* Tests configuring a text editor for a new text format.
*
* This test only needs to ensure that the basics of the CKEditor
* configuration form work; details are tested in testExistingFormat().
*/
function testNewFormat() {
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/config/content/formats/add');
// Verify the "Text Editor" <select> when a text editor is available.
$select = $this->xpath('//select[@name="editor[editor]"]');
$select_is_disabled = $this->xpath('//select[@name="editor[editor]" and @disabled="disabled"]');
$options = $this->xpath('//select[@name="editor[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.');
$this->assertTrue(((string) $options[0]) === 'None', 'Option 1 in the Text Editor select is "None".');
$this->assertTrue(((string) $options[1]) === 'CKEditor', 'Option 2 in the Text Editor select is "CKEditor".');
$this->assertTrue(((string) $options[0]['selected']) === 'selected', 'Option 1 ("None") is selected.');
// Name our fancy new text format, select the "CKEditor" editor and click
// the "Configure" button.
$edit = array(
'name' => 'My amazing text format',
'format' => 'amazing_format',
'editor[editor]' => 'ckeditor',
);
$this->drupalPostAjaxForm(NULL, $edit, 'editor_configure');
$filter_format = FilterFormat::load('amazing_format');
$this->assertFalse($filter_format, 'No FilterFormat config entity exists yet.');
$editor = Editor::load('amazing_format');
$this->assertFalse($editor, 'No Editor config entity exists yet.');
// Ensure the toolbar buttons configuration value is initialized to the
// default value.
$ckeditor = $this->container->get('plugin.manager.editor')->createInstance('ckeditor');
$default_settings = $ckeditor->getDefaultSettings();
$expected_buttons_value = json_encode($default_settings['toolbar']['rows']);
$this->assertFieldByName('editor[settings][toolbar][button_groups]', $expected_buttons_value);
// Regression test for https://www.drupal.org/node/2606460.
$this->assertTrue(strpos($this->drupalSettings['ckeditor']['toolbarAdmin'], '<li data-drupal-ckeditor-button-name="Bold" class="ckeditor-button"><a href="#" class="cke-icon-only cke_ltr" role="button" title="bold" aria-label="bold"><span class="cke_button_icon cke_button__bold_icon">bold</span></a></li>') !== FALSE);
// Ensure the styles textarea exists and is initialized empty.
$styles_textarea = $this->xpath('//textarea[@name="editor[settings][plugins][stylescombo][styles]"]');
$this->assertFieldByXPath('//textarea[@name="editor[settings][plugins][stylescombo][styles]"]', '', 'The styles textarea exists and is empty.');
$this->assertTrue(count($styles_textarea) === 1, 'The "styles" textarea exists.');
// Submit the form to create both a new text format and an associated text
// editor.
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
// Ensure a FilterFormat object exists now.
$filter_format = FilterFormat::load('amazing_format');
$this->assertTrue($filter_format instanceof FilterFormatInterface, 'A FilterFormat config entity exists now.');
// Ensure an Editor object exists now, with the proper settings.
$expected_settings = $default_settings;
$expected_settings['plugins']['stylescombo']['styles'] = '';
$editor = Editor::load('amazing_format');
$this->assertTrue($editor instanceof Editor, 'An Editor config entity exists now.');
$this->assertEqual($this->castSafeStrings($expected_settings), $this->castSafeStrings($editor->getSettings()), 'The Editor config entity has the correct settings.');
}
}

View file

@ -0,0 +1,245 @@
<?php
namespace Drupal\ckeditor\Tests;
use Drupal\editor\Entity\Editor;
use Drupal\simpletest\WebTestBase;
use Drupal\filter\Entity\FilterFormat;
/**
* Tests loading of CKEditor.
*
* @group ckeditor
*/
class CKEditorLoadingTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('filter', 'editor', 'ckeditor', 'node');
/**
* An untrusted user with access to only the 'plain_text' format.
*
* @var \Drupal\user\UserInterface
*/
protected $untrustedUser;
/**
* A normal user with access to the 'plain_text' and 'filtered_html' formats.
*
* @var \Drupal\user\UserInterface
*/
protected $normalUser;
protected function setUp() {
parent::setUp();
// Create text format, associate CKEditor.
$filtered_html_format = FilterFormat::create(array(
'format' => 'filtered_html',
'name' => 'Filtered HTML',
'weight' => 0,
'filters' => array(),
));
$filtered_html_format->save();
$editor = Editor::create([
'format' => 'filtered_html',
'editor' => 'ckeditor',
]);
$editor->save();
// Create a second format without an associated editor so a drop down select
// list is created when selecting formats.
$full_html_format = FilterFormat::create(array(
'format' => 'full_html',
'name' => 'Full HTML',
'weight' => 1,
'filters' => array(),
));
$full_html_format->save();
// Create node type.
$this->drupalCreateContentType(array(
'type' => 'article',
'name' => 'Article',
));
$this->untrustedUser = $this->drupalCreateUser(array('create article content', 'edit any article content'));
$this->normalUser = $this->drupalCreateUser(array('create article content', 'edit any article content', 'use text format filtered_html', 'use text format full_html'));
}
/**
* Tests loading of CKEditor CSS, JS and JS settings.
*/
function testLoading() {
// The untrusted user:
// - has access to 1 text format (plain_text);
// - doesn't have access to the filtered_html text format, so: no text editor.
$this->drupalLogin($this->untrustedUser);
$this->drupalGet('node/add/article');
list($settings, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck();
$this->assertFalse($editor_settings_present, 'No Text Editor module settings.');
$this->assertFalse($editor_js_present, 'No Text Editor JavaScript.');
$this->assertTrue(count($body) === 1, 'A body field exists.');
$this->assertTrue(count($format_selector) === 0, 'No text format selector exists on the page.');
$hidden_input = $this->xpath('//input[@type="hidden" and contains(@class, "editor")]');
$this->assertTrue(count($hidden_input) === 0, 'A single text format hidden input does not exist on the page.');
$this->assertNoRaw(drupal_get_path('module', 'ckeditor') . '/js/ckeditor.js', 'CKEditor glue JS is absent.');
// On pages where there would never be a text editor, CKEditor JS is absent.
$this->drupalGet('user');
$this->assertNoRaw(drupal_get_path('module', 'ckeditor') . '/js/ckeditor.js', 'CKEditor glue JS is absent.');
// The normal user:
// - has access to 2 text formats;
// - does have access to the filtered_html text format, so: CKEditor.
$this->drupalLogin($this->normalUser);
$this->drupalGet('node/add/article');
list($settings, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck();
$ckeditor_plugin = $this->container->get('plugin.manager.editor')->createInstance('ckeditor');
$editor = Editor::load('filtered_html');
$expected = array('formats' => array('filtered_html' => array(
'format' => 'filtered_html',
'editor' => 'ckeditor',
'editorSettings' => $this->castSafeStrings($ckeditor_plugin->getJSSettings($editor)),
'editorSupportsContentFiltering' => TRUE,
'isXssSafe' => FALSE,
)));
$this->assertTrue($editor_settings_present, "Text Editor module's JavaScript settings are on the page.");
$this->assertIdentical($expected, $this->castSafeStrings($settings['editor']), "Text Editor module's JavaScript settings on the page are correct.");
$this->assertTrue($editor_js_present, 'Text Editor JavaScript is present.');
$this->assertTrue(count($body) === 1, 'A body field exists.');
$this->assertTrue(count($format_selector) === 1, 'A single text format selector exists on the page.');
$specific_format_selector = $this->xpath('//select[contains(@class, "filter-list") and @data-editor-for="edit-body-0-value"]');
$this->assertTrue(count($specific_format_selector) === 1, 'A single text format selector exists on the page and has a "data-editor-for" attribute with the correct value.');
$this->assertTrue(in_array('ckeditor/drupal.ckeditor', explode(',', $settings['ajaxPageState']['libraries'])), 'CKEditor glue library is present.');
// Enable the ckeditor_test module, customize configuration. In this case,
// there is additional CSS and JS to be loaded.
// NOTE: the tests in CKEditorTest already ensure that changing the
// configuration also results in modified CKEditor configuration, so we
// don't test that here.
\Drupal::service('module_installer')->install(array('ckeditor_test'));
$this->container->get('plugin.manager.ckeditor.plugin')->clearCachedDefinitions();
$editor_settings = $editor->getSettings();
$editor_settings['toolbar']['rows'][0][0]['items'][] = 'Llama';
$editor->setSettings($editor_settings);
$editor->save();
$this->drupalGet('node/add/article');
list($settings, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck();
$expected = array(
'formats' => array(
'filtered_html' => array(
'format' => 'filtered_html',
'editor' => 'ckeditor',
'editorSettings' => $this->castSafeStrings($ckeditor_plugin->getJSSettings($editor)),
'editorSupportsContentFiltering' => TRUE,
'isXssSafe' => FALSE,
)));
$this->assertTrue($editor_settings_present, "Text Editor module's JavaScript settings are on the page.");
$this->assertIdentical($expected, $this->castSafeStrings($settings['editor']), "Text Editor module's JavaScript settings on the page are correct.");
$this->assertTrue($editor_js_present, 'Text Editor JavaScript is present.');
$this->assertTrue(in_array('ckeditor/drupal.ckeditor', explode(',', $settings['ajaxPageState']['libraries'])), 'CKEditor glue library is present.');
// Assert that CKEditor uses Drupal's cache-busting query string by
// comparing the setting sent with the page with the current query string.
$settings = $this->getDrupalSettings();
$expected = $settings['ckeditor']['timestamp'];
$this->assertIdentical($expected, \Drupal::state()->get('system.css_js_query_string'), "CKEditor scripts cache-busting string is correct before flushing all caches.");
// Flush all caches then make sure that $settings['ckeditor']['timestamp']
// still matches.
drupal_flush_all_caches();
$this->assertIdentical($expected, \Drupal::state()->get('system.css_js_query_string'), "CKEditor scripts cache-busting string is correct after flushing all caches.");
}
/**
* Tests presence of essential configuration even without Internal's buttons.
*/
protected function testLoadingWithoutInternalButtons() {
// Change the CKEditor text editor configuration to only have link buttons.
// This means:
// - 0 buttons are from \Drupal\ckeditor\Plugin\CKEditorPlugin\Internal
// - 2 buttons are from \Drupal\ckeditor\Plugin\CKEditorPlugin\DrupalLink
$filtered_html_editor = Editor::load('filtered_html');
$settings = $filtered_html_editor->getSettings();
$settings['toolbar']['rows'] = [
0 => [
0 => [
'name' => 'Links',
'items' => [
'DrupalLink',
'DrupalUnlink',
],
],
],
];
$filtered_html_editor->setSettings($settings)->save();
// Even when no buttons of \Drupal\ckeditor\Plugin\CKEditorPlugin\Internal
// are in use, its configuration (Internal::getConfig()) is still essential:
// this is configuration that is associated with the (custom, optimized)
// build of CKEditor that Drupal core ships with. For example, it configures
// CKEditor to not perform its default action of loading a config.js file,
// to not convert special characters into HTML entities, and the allowedContent
// setting to configure CKEditor's Advanced Content Filter.
$this->drupalLogin($this->normalUser);
$this->drupalGet('node/add/article');
$editor_settings = $this->getDrupalSettings()['editor']['formats']['filtered_html']['editorSettings'];
$this->assertTrue(isset($editor_settings['customConfig']));
$this->assertTrue(isset($editor_settings['entities']));
$this->assertTrue(isset($editor_settings['allowedContent']));
$this->assertTrue(isset($editor_settings['disallowedContent']));
}
/**
* Tests loading of theme's CKEditor stylesheets defined in the .info file.
*/
function testExternalStylesheets() {
$theme_handler = \Drupal::service('theme_handler');
// Case 1: Install theme which has an absolute external CSS URL.
$theme_handler->install(['test_ckeditor_stylesheets_external']);
$theme_handler->setDefault('test_ckeditor_stylesheets_external');
$expected = [
'https://fonts.googleapis.com/css?family=Open+Sans',
];
$this->assertIdentical($expected, _ckeditor_theme_css('test_ckeditor_stylesheets_external'));
// Case 2: Install theme which has an external protocol-relative CSS URL.
$theme_handler->install(['test_ckeditor_stylesheets_protocol_relative']);
$theme_handler->setDefault('test_ckeditor_stylesheets_protocol_relative');
$expected = [
'//fonts.googleapis.com/css?family=Open+Sans',
];
$this->assertIdentical($expected, _ckeditor_theme_css('test_ckeditor_stylesheets_protocol_relative'));
// Case 3: Install theme which has a relative CSS URL.
$theme_handler->install(['test_ckeditor_stylesheets_relative']);
$theme_handler->setDefault('test_ckeditor_stylesheets_relative');
$expected = [
'core/modules/system/tests/themes/test_ckeditor_stylesheets_relative/css/yokotsoko.css',
];
$this->assertIdentical($expected, _ckeditor_theme_css('test_ckeditor_stylesheets_relative'));
}
protected function getThingsToCheck() {
$settings = $this->getDrupalSettings();
return array(
// JavaScript settings.
$settings,
// Editor.module's JS settings present.
isset($settings['editor']),
// Editor.module's JS present. Note: ckeditor/drupal.ckeditor depends on
// editor/drupal.editor, hence presence of the former implies presence of
// the latter.
isset($settings['ajaxPageState']['libraries']) && in_array('ckeditor/drupal.ckeditor', explode(',', $settings['ajaxPageState']['libraries'])),
// Body field.
$this->xpath('//textarea[@id="edit-body-0-value"]'),
// Format selector.
$this->xpath('//select[contains(@class, "filter-list")]'),
);
}
}

View file

@ -0,0 +1,99 @@
<?php
namespace Drupal\ckeditor\Tests;
use Drupal\editor\Entity\Editor;
use Drupal\simpletest\WebTestBase;
use Drupal\filter\Entity\FilterFormat;
/**
* Tests administration of the CKEditor StylesCombo plugin.
*
* @group ckeditor
*/
class CKEditorStylesComboAdminTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['filter', 'editor', 'ckeditor'];
/**
* A user with the 'administer filters' permission.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* A random generated format machine name.
*
* @var string
*/
protected $format;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->format = strtolower($this->randomMachineName());
$filter_format = FilterFormat::create([
'format' => $this->format,
'name' => $this->randomString(),
'filters' => [],
]);
$filter_format->save();
$editor = Editor::create([
'format' => $this->format,
'editor' => 'ckeditor',
]);
$editor->save();
$this->adminUser = $this->drupalCreateUser(['administer filters']);
}
/**
* Tests StylesCombo settings for an existing text format.
*/
function testExistingFormat() {
$ckeditor = $this->container->get('plugin.manager.editor')->createInstance('ckeditor');
$default_settings = $ckeditor->getDefaultSettings();
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/config/content/formats/manage/' . $this->format);
// Ensure an Editor config entity exists, with the proper settings.
$expected_settings = $default_settings;
$editor = Editor::load($this->format);
$this->assertEqual($expected_settings, $editor->getSettings(), 'The Editor config entity has the correct settings.');
// Case 1: Configure the Styles plugin with different labels for each style,
// and ensure the updated settings are saved.
$this->drupalGet('admin/config/content/formats/manage/' . $this->format);
$edit = [
'editor[settings][plugins][stylescombo][styles]' => "h1.title|Title\np.callout|Callout\n\n",
];
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
$expected_settings['plugins']['stylescombo']['styles'] = "h1.title|Title\np.callout|Callout\n\n";
$editor = Editor::load($this->format);
$this->assertEqual($expected_settings, $editor->getSettings(), 'The Editor config entity has the correct settings.');
// Case 2: Configure the Styles plugin with same labels for each style, and
// ensure that an error is displayed and that the updated settings are not
// saved.
$this->drupalGet('admin/config/content/formats/manage/' . $this->format);
$edit = [
'editor[settings][plugins][stylescombo][styles]' => "h1.title|Title\np.callout|Title\n\n",
];
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
$this->assertRaw(t('Each style must have a unique label.'));
$expected_settings['plugins']['stylescombo']['styles'] = "h1.title|Title\np.callout|Callout\n\n";
$editor = Editor::load($this->format);
$this->assertEqual($expected_settings, $editor->getSettings(), 'The Editor config entity has the correct settings.');
}
}

View file

@ -0,0 +1,77 @@
<?php
namespace Drupal\ckeditor\Tests;
use Drupal\filter\Entity\FilterFormat;
use Drupal\editor\Entity\Editor;
use Drupal\simpletest\WebTestBase;
use Drupal\Component\Serialization\Json;
/**
* Tests CKEditor toolbar buttons when the language direction is RTL.
*
* @group ckeditor
*/
class CKEditorToolbarButtonTest extends WebTestBase {
/**
* Modules to enable for this test.
*
* @var array
*/
public static $modules = ['filter', 'editor', 'ckeditor', 'locale'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create a text format and associate this with CKEditor.
FilterFormat::create([
'format' => 'full_html',
'name' => 'Full HTML',
'weight' => 1,
'filters' => [],
])->save();
Editor::create([
'format' => 'full_html',
'editor' => 'ckeditor',
])->save();
// Create a new user with admin rights.
$this->admin_user = $this->drupalCreateUser([
'administer languages',
'access administration pages',
'administer site configuration',
'administer filters',
]);
}
/**
* Method tests CKEditor image buttons.
*/
public function testImageButtonDisplay() {
$this->drupalLogin($this->admin_user);
// Install the Arabic language (which is RTL) and configure as the default.
$edit = [];
$edit['predefined_langcode'] = 'ar';
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
$edit = ['site_default_language' => 'ar'];
$this->drupalPostForm('admin/config/regional/language', $edit, t('Save configuration'));
// Once the default language is changed, go to the tested text format
// configuration page.
$this->drupalGet('admin/config/content/formats/manage/full_html');
// Check if any image button is loaded in CKEditor json.
$json_encode = function($html) {
return trim(Json::encode($html), '"');
};
$markup = $json_encode(file_url_transform_relative(file_create_url('core/modules/ckeditor/js/plugins/drupalimage/icons/drupalimage.png')));
$this->assertRaw($markup);
}
}