Customizing CKEditor in Drupal 7 and 8

Submitted by Kevin on

Having worked on new ways of doing layouts with the Paragraphs module and worked with Institute Communications on the development of a new version of the Georgia Tech web theme, I realized that there was still one component of website visual design that I had yet to address:  a visual building block toolkit for content creators and editors.  Custom layouts are a part of this puzzle, but they do not address the need for custom styled headings, buttons, introductory text, etc. that need to be part of the actual content of a page.

The challenge here is that you want to make these custom styles easy to use and easy to maintain.  The most obvious approach is to add them to your chosen WYSIWYG editor, but figuring out how to do that can be tricky.  Here at Georgia Tech we use Drupal and in turn the preferred Drupal editor, CKEditor.  This is helpful, as there are lots of guides out there for extending both Drupal and CKEditor, but it can take time to dig through them and figure out how to make it all work.

One option is to try to create your own plugin widgets for CKEditor, but I chose to keep things simpler and just create a workable drop-down Style selector.  Such a selector is built into CKEditor, but has to be populated with your custom styles for it to be useful.

The Functional Side

For reference, here is a list of the styled content components that I've defined.  Not all of these are done via CKEditor, but the full list gives a sense of all the pieces and parts you might need in your own toolkit:

  • CKEditor Components
    • Buttons (Block styling with hover effect, in either Navy Blue, Gold or Grey using accessible color contrasts)
    • Heading Lines with Color Backgrounds (Navy Blue, Gold, and Grey, each using accessible color contrasts)
    • Introductory Text (Larger font size, white text on navy blue background)
  • Other Style Definitions
    • Navigational Menu Blocks in Sidebars
    • Sidebar Boxes via Drupal Blocks
    • Sidebar Boxes placed within Paragraphs Layout structures
    • Blockquote Custom Styling
    • Table Header Rows/Columns in Navy Blue
    • Two, Three, and Four column Unordered and Ordered Lists (collapse to single column on small width displays)*

* Ideally, the multicolumn styles would be applicable via CKEditor, but CKEditor won't apply custom styles to the UL or OL elements, at least by default, and I haven't found a way to enable custom styles for those two HTML elements.  Unfortunately, some level of intelligence is needed to decided how many columns is best for a list, so I can't just apply one set of rules to all UL and OL tags in the content areas.  Thus, I'm left with simply having class names for each option that have to be applied manually via the source view in CKEditor, which isn't ideal, but works.

The Technical Side

Everything I'm doing is built around CKEditor 4.6.  Standard caveats apply to using the information below against any older or newer version:  your mileage may vary.  You'll need several parts and pieces to implement the changes that I've made. I'll break them down into two parts: Drupal Hooks, CSS, and JavaScript

Drupal Hooks

While you can configure the style menu in Drupal 8 from the administrative GUI, it's easier for multi-site implementation to just do your configuration via a module. That way, the settings will always be uniform across all of your sites. (Note: by doing the configuration this way, anything entered into the style configuration option in the administrative GUI will be completely ignored, which depending on your point of view might actually be a good thing.)

Drupal 7 Code


function MODULENAME_ckeditor_settings_alter(array &$settings, $conf) {

  $settings['contentsCss'][] = '/' . drupal_get_path('module','MODULENAME') . '/css/iac-gt-core.css';
  $settings['format_tags'] = "p;h2;h3;h4;h5;h6;pre;code;address;div";
  $settings['format_code'] = array('name' => 'Program Code', 'element' => 'code');
  $settings['fontSize_sizes'] = '80%/80%;90%/90%;Normal/100%;110%/110%;120%/120%;130%/130%;140%/140%;150%/150%';
  $settings['stylesCombo_stylesSet'] = 'drupal:/' . drupal_get_path('module','MODULENAME') . '/js/ckeditor.styles.js';
  $settings['extraAllowedContent'] = '*(*)'; /* Allow all classes on all elements */

}

Drupal 8 Code


function MODULENAME_preprocess_page(&$variables) {

  $variables['#attached']['library'][] = 'MODULENAME/custom';

}

function MODULENAME_editor_js_settings_alter(array &$settings) {

  foreach (array_keys($settings['editor']['formats']) as $text_format_id) {
    $settings['editor']['formats'][$text_format_id]['editorSettings']['contentsCss'][] = '/' . drupal_get_path('module','MODULENAME') . '/css/iac-gt-core.css';
    $settings['editor']['formats'][$text_format_id]['editorSettings']['format_tags'] = "p;h2;h3;h4;h5;h6;pre;code;address;div";
    $settings['editor']['formats'][$text_format_id]['editorSettings']['format_code'] = array('name' => 'Program Code', 'element' => 'code');
    $settings['editor']['formats'][$text_format_id]['editorSettings']['stylesCombo_stylesSet'] = 'drupal:/' . drupal_get_path('module','MODULENAME') . '/js/ckeditor.styles.js';
    $settings['editor']['formats'][$text_format_id]['editorSettings']['fontSize_sizes'] = '80%/80%;90%/90%;Normal/100%;110%/110%;120%/120%;130%/130%;140%/140%;150%/150%';
  }

}

So, what's going on here? First of all, be sure that you change 'MODULENAME' to the machine name of the module you're writing to implement these settings. Otherwise, it's not going to work.

  • contentsCss points CKEditor to the extra CSS defenitions for all of the custom styles that we're using. If you don't do this, you won't see your styles applied within the WYSIWYG editor or the drop-down Style selector.
  • format_tags is redfining the list of regular formats (in the Format drop-down selector) to remove the H1 tag, since we don't want content creators to use it.
  • format_code is used to add an additional standard format, in our case the 'code' tag for marking up program code snippets. Because this isn't a custom class/style, I didn't want it buried in the drop-down Style selector where it might never be found.
  • stylesCombo_stylesSet points CKEditor to the definitions JavaScript file for the custom styles I've defined. More on this in a bit.
  • fontSize_sizes redfined the available list of sizes via the drop-down Font Size selector to be percentages, which are more accessibility compliant than using point sizes. You may or may not want to provide the Font Size selector, but I find that it's a good compromise in the battle to convince content people not to use headings just to make text larger on the screen/page.
  • extraAllowedContent is only needed for Drupal 7. The default configuration of the CKEditor module for Drupal 7 has more restrictions on allowed HTML properties by default, and does not allow class names to be attached to most HTML elements. This setting change enables classnames (but not direct style definitions) for any tag, making CKEditor in Drupal 7 behave like it does in Drupal 8. This is needed to keep our custom class based styles from being stripped out by CKEditor's clean-up code.

The second function for Drupal 8, MODULENAME_preprocess_page() is the way you get your custom CSS included on every page in your site. Of course, you also need to define your 'MODULENAME/custom' library in a MODULENAME.libraries.yaml file, but that's a standard Drupal 8 module concept that you can learn about elsewhere.

Custom CSS Code

The file you point to in your CKEditor custom settings should define all of your custom styles for each of your custom class names. I found that the easiest way to manage all of this CSS was to design each component in it's own separate file, then aggregate all of those files into my master iac-gt-core.css file. This makes it much easier to work on individual components, as you're not having to wade through tons of additional CSS code to find the rules you're wanting to change.

There are a few special rules that I use that are worth mentioning:


DIV.cke_combopanel {
  width: 300px;
}

This rule widens out the drop-down selectors so that you can actually read enough of the custom format and style names to know which is which.


UL.cke_panel_list ELEMENT+CLASS-SPECIFIER-GOES-HERE {
  RULES GO HERE
}

This generic construct shows how to tweak your styling rules so that when they are applied to the name of the style in the drop-down Style selector, they will still look good. In essence, create additional rules by prepending "UL.cke_panel_list" to the specifier to limit those customizations to just the drop-down Style selector.

An example of when you might want to do this is if your applied style includes margins that are needed for the style on a normal page, but would mess up the flow of the drop-down Style selector when it is expanded. You may also have to add some creative UL.cke_panel_list rules to make sure your color choices don't get overridden in the drop-down Style Selector.

Custom JavaScript Code

The last piece of the puzzle is a custom JavaScript file that defines all of your custom styles. Here's my sample set:


if(typeof(CKEDITOR) !== 'undefined') {
    CKEDITOR.addStylesSet( 'drupal',
    [
            { name : 'Black/Gold H2 Heading'    , element : 'h2', attributes: { 'class' : 'gt-heading-gold' } },
            { name : 'White/Blue H2 Heading'    , element : 'h2', attributes: { 'class' : 'gt-heading-blue' } },
            { name : 'White/Grey H2 Heading'    , element : 'h2', attributes: { 'class' : 'gt-heading-grey' } },
            { name : 'Black/Gold H3 Heading'    , element : 'h3', attributes: { 'class' : 'gt-heading-gold' } },
            { name : 'White/Blue H3 Heading'    , element : 'h3', attributes: { 'class' : 'gt-heading-blue' } },
            { name : 'White/Grey H3 Heading'    , element : 'h3', attributes: { 'class' : 'gt-heading-grey' } },
            { name : 'White/Blue Introduction Paragraph'  , element : 'p', attributes: { 'class' : 'iac-blue-introduction' } },
            { name : 'Blue/Gold GT Button'      , element : 'p', attributes: { 'class' : 'gt-button-gold' } },
            { name : 'White/Blue GT Button'     , element : 'p', attributes: { 'class' : 'gt-button-blue' } },
            { name : 'White/Grey GT Button'     , element : 'p', attributes: { 'class' : 'gt-button-grey' } },

    ]);

}

This is a useful reference to bookmark, as most example show how to directly define a style in terms of applying CSS properties directly to the selected text via the 'style' property. However, it's much more sensible for long-term management to apply class names instead of direct style definitions. That way, you can change the styles attached to a class, and everything using that class will get the updates immediately. The code above shows the correct way to have a CKEditor style simply wrap the selected text with the indicated HTML element with that element's "class" property set to the correct custom value.