/* drucafe */

Friday, February 12, 2016

Adding custom styles to CKEditor in Drupal 8

In my version of Drupal 8 (8.0.3) the CKEditor Styles combo was empty out of the box.


Adding custom styles can be done in two ways. Both ways are more or less explained in this answer, which is not for styles but for rtl language settings, but the procedure is analogous. I'll give a detailed explanation here how to do it both ways.


[Edit]
After writing this tutorial and implementing the solution to my site, I noticed on path /admin/config/content/formats/manage/full_html that there appeared an option to set those styles in a window. It wasn't there before. Anyways, this tutorial may still be useful for someone who prefers to set the styles programmatically.


Method 1: Registering styles definition in a hook

This is the simplest solution. You just need to create a custom module in Drupal and implement hook_editor_js_settings_alter with the styles definition in nested arrays (you can read this tutorial how to create a module).

function mymodule_editor_js_settings_alter(array &$settings) {
  global $base_url;
  foreach ($settings['editor']['formats'] as $name => $value) {
    $settings['editor']['formats'][$name]['editorSettings']['stylesSet'] = array(
      array('name' => 'Blue Title',  'element' => 'h2', 'styles' => array('color' => "Blue")),
      array('name' => 'CSS Style',  'element' => 'span', 'attributes' => array('class' => "my-style")),
    );
  }
}

That's it, nothing else is needed. Such defined styles get applied automatically to the Styles combo. This gives the following effect:

Method 2: Registering styles definitions in a javascript file

This is a more complicated way, but for me it is more convenient, especially if there are a lot of styles, because I don't have to write nested php arrays, instead I write nested javascript arrays which are more readible for me. Anyway, the effect is the same. And you also need a custom module for it.

According to this CKEditor doc about styles, you need to do two things:
  • define the styles as a stylesSet (register their definition)
  • apply this stylesSet to the editor

Define the stylesSet in javascript

In order to define the styles, create a javascript file named e.g. modules/custom/mymodule/js/custom.config.js (sounds familiar if you used it in Drupal 7) with the following content:

(function ($, Drupal) {
    Drupal.behaviors.customCKEditorConfig = {
        attach: function (context, settings) {
            if ((typeof CKEDITOR !== "undefined") && (CKEDITOR.stylesSet.get('my_styles') == null)) {
                CKEDITOR.stylesSet.add( 'my_styles', [
                    // Block-level styles
                    { name: 'Blue Title', element: 'h2', styles: { 'color': 'Blue' } },
                    // Inline styles
                    { name: 'CSS Style', element: 'span', attributes: { 'class': 'my_style' } },
                ] );
            }
        }
    }
})(jQuery, Drupal);

By the way, although in Drupal 8 jQuery is not added obligatory to a page as in Drupal 7 (so you should ensure yourself that it's added), I didn't have to worry about it - it was added by some other module or maybe theme. But it's necessary to pay attention to it.

Load the javascript when CKEditor is loaded 

Then somehow load this file on the page whenever CKEditor is loaded. In order to load a javascript file in Drupal 8, you need to
  • define a library containing this javascript file in a custom module
  • attach this library in a hook 

Create a library containing the javascript file

In order to create a library, create file
modules/custom/mymodule/mymodule.libraries.yml
with the following content (attention, .yml files must be indented exactly or they will not work):

ckeditor-custom-config:
  version: 1.x
  js:
    js/custom.config.js: {}

Use some hook to attach the library

As for the hook to attach the library, I used hook_form_alter for it together with checking the form id. There are probably better ways to do it, but it's working.

Create file modules/custom/mymodule/mymodule.module implementing the hook:

function mymodule_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
  if (($form['#form_id'] == 'node_article_edit_form') || ($form['#form_id'] == 'node_page_edit_form')) {
    if (isset($form['#attached']['library'])) {
      $form['#attached']['library'][] = 'mymodule/ckeditor-custom-config';
    } else {
      $form['#attached']['library'] = array('mymodule/ckeditor-custom-config');
    }
  }
}

(If you add other content types to Drupal later, you need to check for them, too, in this hook)

At this point, the javascript file should be loaded on the article edit page, but the Styles combo will still be empty. That's because, according to the mentioned CKEditor docs, we have only defined the styles as a stylesSet in our javascript file so far, but we haven't yet applied them to the editor.

Apply the styleSet to the editor

The CKEditor documentation states that in order to apply the stylesSet you need to do this somewhere in javascript:
CKEDITOR.config.stylesSet = 'my_styles';

However, I couldn't find a good way to do it in javascript. That's because, in the end, all javascript files are loaded in a loop as Drupal behaviors and it happens that my module's javascript containing the above config was always overwritten by Drupal core ckeditor behavior which resets this config setting to empty string. And as per this answer, it is difficult to ensure any specific order of loading behaviors. So in the end, to apply the styleSet to the editor, I once again used the hook_editor_js_settings_alter, but this time not for defining all the styles, but only for defining this one config setting.

Add this function to your modules/custom/mymodule/mymodule.module file:

function mymodule_editor_js_settings_alter(array &$settings) {
  foreach ($settings['editor']['formats'] as $name => $value) {
    $settings['editor']['formats'][$name]['editorSettings']['stylesSet'] = 'my_styles';
  }
}

In effect, you should achieve the same styles in the Styles combo as before.

10 comments:

  1. Hi,
    You should test if "editorSettings" exits in your hook_editor_js_settings_alter before adding anything into it. If not your javascript object drupalSettings.editor.formats will be malformed, and the format switcher will throw an error "TypeError: format.editor is undefined"


    ReplyDelete
  2. This comment has been removed by a blog administrator.

    ReplyDelete
  3. For first method also need to add this to name space to make it work
    use Drupal\Core\Form\FormStateInterface;
    use Drupal\editor\Entity\Editor;

    ReplyDelete
  4. Thanks for this tutorial, it was helpful.

    ReplyDelete