3 namespace Drupal\ckeditor\Plugin\Editor;
5 use Drupal\Core\Extension\ModuleHandlerInterface;
6 use Drupal\ckeditor\CKEditorPluginManager;
7 use Drupal\Core\Form\FormStateInterface;
8 use Drupal\Core\Language\LanguageManagerInterface;
9 use Drupal\Core\Render\Element;
10 use Drupal\Core\Render\RendererInterface;
11 use Drupal\editor\Plugin\EditorBase;
12 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
13 use Drupal\editor\Entity\Editor;
14 use Symfony\Component\DependencyInjection\ContainerInterface;
17 * Defines a CKEditor-based text editor for Drupal.
21 * label = @Translation("CKEditor"),
22 * supports_content_filtering = TRUE,
23 * supports_inline_editing = TRUE,
24 * is_xss_safe = FALSE,
25 * supported_element_types = {
30 class CKEditor extends EditorBase implements ContainerFactoryPluginInterface {
33 * The module handler to invoke hooks on.
35 * @var \Drupal\Core\Extension\ModuleHandlerInterface
37 protected $moduleHandler;
40 * The language manager.
42 * @var \Drupal\Core\Language\LanguageManagerInterface
44 protected $languageManager;
47 * The CKEditor plugin manager.
49 * @var \Drupal\ckeditor\CKEditorPluginManager
51 protected $ckeditorPluginManager;
56 * @var \Drupal\Core\Render\RendererInterface
61 * Constructs a Drupal\Component\Plugin\PluginBase object.
63 * @param array $configuration
64 * A configuration array containing information about the plugin instance.
65 * @param string $plugin_id
66 * The plugin_id for the plugin instance.
67 * @param mixed $plugin_definition
68 * The plugin implementation definition.
69 * @param \Drupal\ckeditor\CKEditorPluginManager $ckeditor_plugin_manager
70 * The CKEditor plugin manager.
71 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
72 * The module handler to invoke hooks on.
73 * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
74 * The language manager.
75 * @param \Drupal\Core\Render\RendererInterface $renderer
78 public function __construct(array $configuration, $plugin_id, $plugin_definition, CKEditorPluginManager $ckeditor_plugin_manager, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager, RendererInterface $renderer) {
79 parent::__construct($configuration, $plugin_id, $plugin_definition);
80 $this->ckeditorPluginManager = $ckeditor_plugin_manager;
81 $this->moduleHandler = $module_handler;
82 $this->languageManager = $language_manager;
83 $this->renderer = $renderer;
89 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
94 $container->get('plugin.manager.ckeditor.plugin'),
95 $container->get('module_handler'),
96 $container->get('language_manager'),
97 $container->get('renderer')
104 public function getDefaultSettings() {
111 'name' => $this->t('Formatting'),
112 'items' => ['Bold', 'Italic'],
115 'name' => $this->t('Links'),
116 'items' => ['DrupalLink', 'DrupalUnlink'],
119 'name' => $this->t('Lists'),
120 'items' => ['BulletedList', 'NumberedList'],
123 'name' => $this->t('Media'),
124 'items' => ['Blockquote', 'DrupalImage'],
127 'name' => $this->t('Tools'),
128 'items' => ['Source'],
133 'plugins' => ['language' => ['language_list' => 'un']],
140 public function settingsForm(array $form, FormStateInterface $form_state, Editor $editor) {
141 $settings = $editor->getSettings();
143 $ckeditor_settings_toolbar = [
144 '#theme' => 'ckeditor_settings_toolbar',
145 '#editor' => $editor,
146 '#plugins' => $this->ckeditorPluginManager->getButtons(),
149 '#type' => 'container',
151 'library' => ['ckeditor/drupal.ckeditor.admin'],
152 'drupalSettings' => [
154 'toolbarAdmin' => (string) $this->renderer->renderPlain($ckeditor_settings_toolbar),
158 '#attributes' => ['class' => ['ckeditor-toolbar-configuration']],
161 $form['toolbar']['button_groups'] = [
162 '#type' => 'textarea',
163 '#title' => $this->t('Toolbar buttons'),
164 '#default_value' => json_encode($settings['toolbar']['rows']),
165 '#attributes' => ['class' => ['ckeditor-toolbar-textarea']],
168 // CKEditor plugin settings, if any.
169 $form['plugin_settings'] = [
170 '#type' => 'vertical_tabs',
171 '#title' => $this->t('CKEditor plugin settings'),
173 'id' => 'ckeditor-plugin-settings',
176 $this->ckeditorPluginManager->injectPluginSettingsForm($form, $form_state, $editor);
177 if (count(Element::children($form['plugins'])) === 0) {
178 unset($form['plugins']);
179 unset($form['plugin_settings']);
182 // Hidden CKEditor instance. We need a hidden CKEditor instance with all
183 // plugins enabled, so we can retrieve CKEditor's per-feature metadata (on
184 // which tags, attributes, styles and classes are enabled). This metadata is
185 // necessary for certain filters' (for instance, the html_filter filter)
186 // settings to be updated accordingly.
187 // Get a list of all external plugins and their corresponding files.
188 $plugins = array_keys($this->ckeditorPluginManager->getDefinitions());
189 $all_external_plugins = [];
190 foreach ($plugins as $plugin_id) {
191 $plugin = $this->ckeditorPluginManager->createInstance($plugin_id);
192 if (!$plugin->isInternal()) {
193 $all_external_plugins[$plugin_id] = $plugin->getFile();
196 // Get a list of all buttons that are provided by all plugins.
197 $all_buttons = array_reduce($this->ckeditorPluginManager->getButtons(), function($result, $item) {
198 return array_merge($result, array_keys($item));
200 // Build a fake Editor object, which we'll use to generate JavaScript
201 // settings for this fake Editor instance.
202 $fake_editor = Editor::create([
203 'format' => $editor->id(),
204 'editor' => 'ckeditor',
206 // Single toolbar row, single button group, all existing buttons.
211 'name' => 'All existing buttons',
212 'items' => $all_buttons,
217 'plugins' => $settings['plugins'],
220 $config = $this->getJSSettings($fake_editor);
221 // Remove the ACF configuration that is generated based on filter settings,
222 // because otherwise we cannot retrieve per-feature metadata.
223 unset($config['allowedContent']);
224 $form['hidden_ckeditor'] = [
225 '#markup' => '<div id="ckeditor-hidden" class="hidden"></div>',
227 'drupalSettings' => ['ckeditor' => ['hiddenCKEditorConfig' => $config]],
237 public function settingsFormSubmit(array $form, FormStateInterface $form_state) {
238 // Modify the toolbar settings by reference. The values in
239 // $form_state->getValue(array('editor', 'settings')) will be saved directly
240 // by editor_form_filter_admin_format_submit().
241 $toolbar_settings = &$form_state->getValue(['editor', 'settings', 'toolbar']);
243 // The rows key is not built into the form structure, so decode the button
244 // groups data into this new key and remove the button_groups key.
245 $toolbar_settings['rows'] = json_decode($toolbar_settings['button_groups'], TRUE);
246 unset($toolbar_settings['button_groups']);
248 // Remove the plugin settings' vertical tabs state; no need to save that.
249 if ($form_state->hasValue(['editor', 'settings', 'plugins'])) {
250 $form_state->unsetValue(['editor', 'settings', 'plugin_settings']);
257 public function getJSSettings(Editor $editor) {
260 // Get the settings for all enabled plugins, even the internal ones.
261 $enabled_plugins = array_keys($this->ckeditorPluginManager->getEnabledPluginFiles($editor, TRUE));
262 foreach ($enabled_plugins as $plugin_id) {
263 $plugin = $this->ckeditorPluginManager->createInstance($plugin_id);
264 $settings += $plugin->getConfig($editor);
267 // Fall back on English if no matching language code was found.
268 $display_langcode = 'en';
270 // Map the interface language code to a CKEditor translation if interface
271 // translation is enabled.
272 if ($this->moduleHandler->moduleExists('locale')) {
273 $ckeditor_langcodes = $this->getLangcodes();
274 $language_interface = $this->languageManager->getCurrentLanguage();
275 if (isset($ckeditor_langcodes[$language_interface->getId()])) {
276 $display_langcode = $ckeditor_langcodes[$language_interface->getId()];
280 // Next, set the most fundamental CKEditor settings.
281 $external_plugin_files = $this->ckeditorPluginManager->getEnabledPluginFiles($editor);
283 'toolbar' => $this->buildToolbarJSSetting($editor),
284 'contentsCss' => $this->buildContentsCssJSSetting($editor),
285 'extraPlugins' => implode(',', array_keys($external_plugin_files)),
286 'language' => $display_langcode,
287 // Configure CKEditor to not load styles.js. The StylesCombo plugin will
288 // set stylesSet according to the user's settings, if the "Styles" button
289 // is enabled. We cannot get rid of this until CKEditor will stop loading
290 // styles.js by default.
291 // See http://dev.ckeditor.com/ticket/9992#comment:9.
292 'stylesSet' => FALSE,
295 // Finally, set Drupal-specific CKEditor settings.
296 $root_relative_file_url = function ($uri) {
297 return file_url_transform_relative(file_create_url($uri));
300 'drupalExternalPlugins' => array_map($root_relative_file_url, $external_plugin_files),
303 // Parse all CKEditor plugin JavaScript files for translations.
304 if ($this->moduleHandler->moduleExists('locale')) {
305 locale_js_translate(array_values($external_plugin_files));
314 * Returns a list of language codes supported by CKEditor.
317 * An associative array keyed by language codes.
319 public function getLangcodes() {
320 // Cache the file system based language list calculation because this would
321 // be expensive to calculate all the time. The cache is cleared on core
322 // upgrades which is the only situation the CKEditor file listing should
324 $langcode_cache = \Drupal::cache()->get('ckeditor.langcodes');
325 if (!empty($langcode_cache)) {
326 $langcodes = $langcode_cache->data;
328 if (empty($langcodes)) {
330 // Collect languages included with CKEditor based on file listing.
331 $files = scandir('core/assets/vendor/ckeditor/lang');
332 foreach ($files as $file) {
333 if ($file[0] !== '.' && preg_match('/\.js$/', $file)) {
334 $langcode = basename($file, '.js');
335 $langcodes[$langcode] = $langcode;
338 \Drupal::cache()->set('ckeditor.langcodes', $langcodes);
341 // Get language mapping if available to map to Drupal language codes.
342 // This is configurable in the user interface and not expensive to get, so
343 // we don't include it in the cached language list.
344 $language_mappings = $this->moduleHandler->moduleExists('language') ? language_get_browser_drupal_langcode_mappings() : [];
345 foreach ($langcodes as $langcode) {
346 // If this language code is available in a Drupal mapping, use that to
347 // compute a possibility for matching from the Drupal langcode to the
348 // CKEditor langcode.
349 // For instance, CKEditor uses the langcode 'no' for Norwegian, Drupal
350 // uses 'nb'. This would then remove the 'no' => 'no' mapping and replace
351 // it with 'nb' => 'no'. Now Drupal knows which CKEditor translation to
353 if (isset($language_mappings[$langcode]) && !isset($langcodes[$language_mappings[$langcode]])) {
354 $langcodes[$language_mappings[$langcode]] = $langcode;
355 unset($langcodes[$langcode]);
365 public function getLibraries(Editor $editor) {
367 'ckeditor/drupal.ckeditor',
370 // Get the required libraries for any enabled plugins.
371 $enabled_plugins = array_keys($this->ckeditorPluginManager->getEnabledPluginFiles($editor));
372 foreach ($enabled_plugins as $plugin_id) {
373 $plugin = $this->ckeditorPluginManager->createInstance($plugin_id);
374 $additional_libraries = array_diff($plugin->getLibraries($editor), $libraries);
375 $libraries = array_merge($libraries, $additional_libraries);
382 * Builds the "toolbar" configuration part of the CKEditor JS settings.
384 * @see getJSSettings()
386 * @param \Drupal\editor\Entity\Editor $editor
387 * A configured text editor object.
389 * An array containing the "toolbar" configuration.
391 public function buildToolbarJSSetting(Editor $editor) {
394 $settings = $editor->getSettings();
395 foreach ($settings['toolbar']['rows'] as $row) {
396 foreach ($row as $group) {
405 * Builds the "contentsCss" configuration part of the CKEditor JS settings.
407 * @see getJSSettings()
409 * @param \Drupal\editor\Entity\Editor $editor
410 * A configured text editor object.
412 * An array containing the "contentsCss" configuration.
414 public function buildContentsCssJSSetting(Editor $editor) {
416 drupal_get_path('module', 'ckeditor') . '/css/ckeditor-iframe.css',
417 drupal_get_path('module', 'system') . '/css/components/align.module.css',
419 $this->moduleHandler->alter('ckeditor_css', $css, $editor);
420 // Get a list of all enabled plugins' iframe instance CSS files.
421 $plugins_css = array_reduce($this->ckeditorPluginManager->getCssFiles($editor), function($result, $item) {
422 return array_merge($result, array_values($item));
424 $css = array_merge($css, $plugins_css);
425 $css = array_merge($css, _ckeditor_theme_css());
426 $css = array_map('file_create_url', $css);
427 $css = array_map('file_url_transform_relative', $css);
429 return array_values($css);