CKEditor.php 14.3 KB
Newer Older
1
2
3
4
<?php

/**
 * @file
5
 * Contains \Drupal\ckeditor\Plugin\Editor\CKEditor.
6
7
 */

8
namespace Drupal\ckeditor\Plugin\Editor;
9

10
11
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
12
use Drupal\ckeditor\CKEditorPluginManager;
13
use Drupal\Core\Language\Language;
14
use Drupal\Core\Language\LanguageManager;
15
use Drupal\editor\Plugin\EditorBase;
16
use Drupal\editor\Annotation\Editor;
17
use Drupal\Core\Annotation\Translation;
18
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
19
use Drupal\editor\Entity\Editor as EditorEntity;
20
use Symfony\Component\DependencyInjection\ContainerInterface;
21
22
23
24

/**
 * Defines a CKEditor-based text editor for Drupal.
 *
25
 * @Editor(
26
27
 *   id = "ckeditor",
 *   label = @Translation("CKEditor"),
28
 *   supports_inline_editing = TRUE
29
30
 * )
 */
31
class CKEditor extends EditorBase implements ContainerFactoryPluginInterface {
32

33
34
35
36
37
38
39
40
41
42
43
44
45
46
  /**
   * The module handler to invoke hooks on.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * The language manager.
   *
   * @var \Drupal\Core\Language\LanguageManager
   */
  protected $languageManager;

47
  /**
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
   * The CKEditor plugin manager.
   *
   * @var \Drupal\ckeditor\CKEditorPluginManager
   */
  protected $ckeditorPluginManager;

  /**
   * 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 array $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\ckeditor\CKEditorPluginManager $ckeditor_plugin_manager
   *   The CKEditor plugin manager.
65
66
67
68
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler to invoke hooks on.
   * @param \Drupal\Core\Language\LanguageManager $language_manager
   *   The language manager.
69
   */
70
  public function __construct(array $configuration, $plugin_id, array $plugin_definition, CKEditorPluginManager $ckeditor_plugin_manager, ModuleHandlerInterface $module_handler, LanguageManager $language_manager) {
71
72
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->ckeditorPluginManager = $ckeditor_plugin_manager;
73
74
    $this->moduleHandler = $module_handler;
    $this->languageManager = $language_manager;
75
76
77
78
79
80
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) {
81
82
83
84
85
86
87
88
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('plugin.manager.ckeditor.plugin'),
      $container->get('module_handler'),
      $container->get('language_manager')
    );
89
90
91
92
  }

  /**
   * {@inheritdoc}
93
94
95
96
   */
  public function getDefaultSettings() {
    return array(
      'toolbar' => array(
97
98
        'rows' => array(
          // Button groups.
99
          array(
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
            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',),
            ),
120
121
122
123
124
125
126
127
          ),
        ),
      ),
      'plugins' => array(),
    );
  }

  /**
128
   * {@inheritdoc}
129
   */
130
  public function settingsForm(array $form, array &$form_state, EditorEntity $editor) {
131
132
133
    $ckeditor_settings_toolbar = array(
      '#theme' => 'ckeditor_settings_toolbar',
      '#editor' => $editor,
134
      '#plugins' => $this->ckeditorPluginManager->getButtons(),
135
    );
136
137
138
139
140
141
142
143
    $form['toolbar'] = array(
      '#type' => 'container',
      '#attached' => array(
        'library' => array(array('ckeditor', 'drupal.ckeditor.admin')),
        'js' => array(
          array(
            'type' => 'setting',
            'data' => array('ckeditor' => array(
144
              'toolbarAdmin' => drupal_render($ckeditor_settings_toolbar),
145
146
147
148
149
150
            )),
          )
        ),
      ),
      '#attributes' => array('class' => array('ckeditor-toolbar-configuration')),
    );
151
152

    $form['toolbar']['button_groups'] = array(
153
154
      '#type' => 'textarea',
      '#title' => t('Toolbar buttons'),
155
      '#default_value' => json_encode($editor->settings['toolbar']['rows']),
156
157
158
159
160
161
      '#attributes' => array('class' => array('ckeditor-toolbar-textarea')),
    );

    // CKEditor plugin settings, if any.
    $form['plugin_settings'] = array(
      '#type' => 'vertical_tabs',
162
      '#title' => t('CKEditor plugin settings'),
163
164
165
      '#attributes' => array(
        'id' => 'ckeditor-plugin-settings',
      ),
166
    );
167
    $this->ckeditorPluginManager->injectPluginSettingsForm($form, $form_state, $editor);
168
169
170
171
172
    if (count(element_children($form['plugins'])) === 0) {
      unset($form['plugins']);
      unset($form['plugin_settings']);
    }

173
174
175
176
177
178
    // 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' (e.g. the html_filter filter) settings to
    // be updated accordingly.
    // Get a list of all external plugins and their corresponding files.
179
    $plugins = array_keys($this->ckeditorPluginManager->getDefinitions());
180
181
    $all_external_plugins = array();
    foreach ($plugins as $plugin_id) {
182
      $plugin = $this->ckeditorPluginManager->createInstance($plugin_id);
183
184
185
186
187
      if (!$plugin->isInternal()) {
        $all_external_plugins[$plugin_id] = $plugin->getFile();
      }
    }
    // Get a list of all buttons that are provided by all plugins.
188
    $all_buttons = array_reduce($this->ckeditorPluginManager->getButtons(), function($result, $item) {
189
190
191
192
193
      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 = entity_create('editor', array(
194
      'format' => $editor->id(),
195
196
      'editor' => 'ckeditor',
      'settings' => array(
197
198
199
200
201
202
203
204
205
206
207
        // Single toolbar row, single button group, all existing buttons.
        'toolbar' => array(
         'rows' => array(
           0 => array(
             0 => array(
               'name' => 'All existing buttons',
               'items' => $all_buttons,
             )
           )
         ),
        ),
208
209
210
        'plugins' => $editor->settings['plugins'],
      ),
    ));
211
212
213
214
    $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']);
215
216
217
218
219
220
221
    $form['hidden_ckeditor'] = array(
      '#markup' => '<div id="ckeditor-hidden" class="element-hidden"></div>',
      '#attached' => array(
        'js' => array(
          array(
            'type' => 'setting',
            'data' => array('ckeditor' => array(
222
              'hiddenCKEditorConfig' => $config,
223
224
225
226
227
228
            )),
          ),
        ),
      ),
    );

229
230
231
232
    return $form;
  }

  /**
233
   * {@inheritdoc}
234
235
236
237
238
239
240
   */
  public function settingsFormSubmit(array $form, array &$form_state) {
    // Modify the toolbar settings by reference. The values in
    // $form_state['values']['editor']['settings'] will be saved directly by
    // editor_form_filter_admin_format_submit().
    $toolbar_settings = &$form_state['values']['editor']['settings']['toolbar'];

241
242
243
244
    // 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']);
245
246
247
248
249
250
251
252

    // Remove the plugin settings' vertical tabs state; no need to save that.
    if (isset($form_state['values']['editor']['settings']['plugins'])) {
      unset($form_state['values']['editor']['settings']['plugin_settings']);
    }
  }

  /**
253
   * {@inheritdoc}
254
   */
255
  public function getJSSettings(EditorEntity $editor) {
256
257
258
    $settings = array();

    // Get the settings for all enabled plugins, even the internal ones.
259
    $enabled_plugins = array_keys($this->ckeditorPluginManager->getEnabledPluginFiles($editor, TRUE));
260
    foreach ($enabled_plugins as $plugin_id) {
261
      $plugin = $this->ckeditorPluginManager->createInstance($plugin_id);
262
263
264
      $settings += $plugin->getConfig($editor);
    }

265
266
267
268
269
270
271
272
273
274
    // Fall back on English if no matching language code was found.
    $display_langcode = 'en';

    // Map the interface language code to a CKEditor translation.
    $ckeditor_langcodes = $this->getLangcodes();
    $language_interface = $this->languageManager->getLanguage(Language::TYPE_INTERFACE);
    if (isset($ckeditor_langcodes[$language_interface->id])) {
      $display_langcode = $ckeditor_langcodes[$language_interface->id];
    }

275
    // Next, set the most fundamental CKEditor settings.
276
    $external_plugin_files = $this->ckeditorPluginManager->getEnabledPluginFiles($editor);
277
278
279
    $settings += array(
      'toolbar' => $this->buildToolbarJSSetting($editor),
      'contentsCss' => $this->buildContentsCssJSSetting($editor),
280
      'extraPlugins' => implode(',', array_keys($external_plugin_files)),
281
      'language' => $display_langcode,
282
283
284
285
286
287
      // 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,
288
289
290
291
    );

    // Finally, set Drupal-specific CKEditor settings.
    $settings += array(
292
      'drupalExternalPlugins' => array_map('file_create_url', $external_plugin_files),
293
294
    );

295
296
297
298
299
    // Parse all CKEditor plugin JavaScript files for translations.
    if ($this->moduleHandler->moduleExists('locale')) {
      locale_js_translate(array_values($settings['drupalExternalPlugins']));
    }

300
301
    ksort($settings);

302
303
304
    return $settings;
  }

305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
  /**
   * 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 = cache('ckeditor.languages')->get('langcodes');
    if (!empty($langcode_cache)) {
      $langcodes = $langcode_cache->data;
    }
    if (empty($langcodes)) {
      $langcodes = array();
      // Collect languages included with CKEditor based on file listing.
323
324
325
      $ckeditor_languages = new \GlobIterator(DRUPAL_ROOT . '/core/assets/vendor/ckeditor/lang/*.js');
      foreach ($ckeditor_languages as $language_file) {
        $langcode = $language_file->getBasename('.js');
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
        $langcodes[$langcode] = $langcode;
      }
      cache('ckeditor.languages')->set('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.
      // e.g. 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;
  }

351
  /**
352
   * {@inheritdoc}
353
   */
354
  public function getLibraries(EditorEntity $editor) {
355
    $libraries = array(
356
357
      array('ckeditor', 'drupal.ckeditor'),
    );
358
359

    // Get the required libraries for any enabled plugins.
360
    $enabled_plugins = array_keys($this->ckeditorPluginManager->getEnabledPluginFiles($editor));
361
362
363
364
365
366
367
368
369
    foreach ($enabled_plugins as $plugin_id) {
      $plugin = $this->ckeditorPluginManager->createInstance($plugin_id);
      $additional_libraries = array_udiff($plugin->getLibraries($editor), $libraries, function($a, $b) {
        return $a[0] === $b[0] && $a[1] === $b[1] ? 0 : 1;
      });
      $libraries = array_merge($libraries, $additional_libraries);
    }

    return $libraries;
370
371
372
373
374
375
376
  }

  /**
   * Builds the "toolbar" configuration part of the CKEditor JS settings.
   *
   * @see getJSSettings()
   *
377
   * @param \Drupal\editor\Entity\Editor $editor
378
379
380
381
   *   A configured text editor object.
   * @return array
   *   An array containing the "toolbar" configuration.
   */
382
  public function buildToolbarJSSetting(EditorEntity $editor) {
383
    $toolbar = array();
384
385
386
    foreach ($editor->settings['toolbar']['rows'] as $row) {
      foreach ($row as $group) {
        $toolbar[] = $group;
387
388
389
390
391
392
393
394
395
396
397
      }
      $toolbar[] = '/';
    }
    return $toolbar;
  }

  /**
   * Builds the "contentsCss" configuration part of the CKEditor JS settings.
   *
   * @see getJSSettings()
   *
398
   * @param \Drupal\editor\Entity\Editor $editor
399
400
401
402
   *   A configured text editor object.
   * @return array
   *   An array containing the "contentsCss" configuration.
   */
403
  public function buildContentsCssJSSetting(EditorEntity $editor) {
404
405
    $css = array(
      drupal_get_path('module', 'ckeditor') . '/css/ckeditor-iframe.css',
406
      drupal_get_path('module', 'system') . '/css/system.module.css',
407
408
    );
    drupal_alter('ckeditor_css', $css, $editor);
409
    $css = array_merge($css, _ckeditor_theme_css());
410
411
412
413
414
415
    $css = array_map('file_create_url', $css);

    return array_values($css);
  }

}