Adding CKEditor custom editor styles in a theme (Content block/rich text editor)

This is a community-contributed tutorial. This tutorial is over a year old and may not apply to your version of Concrete CMS.
Feb 15, 2017

This tutorial is a continuation of the Adding Redactor custom styles in a theme (content block/rich text editor) tutorial. If you are learning about adding custom editor styles for the first time, please read that tutorial first.

With the release of Concrete CMS version 8, the Redactor rich text editor has been replaced with CKEditor. Existing styles written for Redactor are compatible with CKEditor by converting them into a form CKEditor can use. These Redactor custom editor styles don't take full advantage of what CKEditor can do and are limited to applying classes to basic block-level elements and wrapping text selections in a span and applying classes to it. CKEditor in comparison offers block-level, object, inline, and custom style types.

This tutorial requires Concrete version 8.2 or greater.

CKEditor Style Types

Block

Block styles are applied to block-level elements. They work by targeting a specific block-level element or replacing a selected block-level element. A block style will use one of the following block-level elements: address, div, h1, h2, h3, h4, h5, h6, p, and pre. Block styles can use one or more elements.

Single versus multiple element block styles

Block styles behave differently if the style contains one element or multiple elements. When more than one block-level element is used in a style, the style is treated as an object style.

When applied to a block-level element that is different from the block style element, the block style element will replace the block-level element. An example would be if your block style element was div and you selected text within a p text block, after applying the block style the p element would be replaced with div.

Example: single element

array(
    'title' => t('Red Border Div'), 
    'element' => 'div', 
    'attributes' => array('class' => 'red-border')
)

When this style is applied to a block-level element, that block-level element is replaced with div and is assigned the class "red-border".

If multiple block style elements are used, there will be no element replacement, instead only elements that match the block style elements are targeted. An example would be if your block style elements were h1, h2, and h3, the custom style would only be available for those three elements. If you selected text in a p element, that custom style would not be available.

Example: multiple elements

array(
    'title' => t('Green H1-H3'),
    'element' => array('h1', 'h2', 'h3'),
    'attributes' => array('class' => 'green-heading')
)

This style targets h1, h2, and h3 elements only and adds the class "green-heading".

Object

Objects styles are applied to selectable non-text objects. An object style will use one of the following elements: a, embed, hr, img, li, object, ol, table, td, tr, and ul. They don't replace elements or wrap selected text, instead they just target elements. Object styles can use one or more elements.

Example: single element

array(
    'title' => t('Light Grey Zebra Stripe Table'),
    'element' => 'table',
    'attributes' => array('class' => 'light-grey-zebra-table')
)

This style targets table elements only and adds the "light-grey-zebra-table" class.

Example: multiple elements

array(
    'title' => t('Red Lists'),
    'element' => array('ol', 'ul'),
    'attributes' => array('class' => 'red-list')
)

This style targets ol and ul elements only and adds the "red-list" class.

Inline

Inline styles are applied to inline elements and text selections. They work by targeting specific inline elements or wrapping a text selection in an inline element. An inline style can use inline elements not already defined in other style types. Inline styles can use one or more elements.

Single versus multiple element inline styles

Inline styles behave differently if the style contains one element or multiple elements. When more than one inline element is used in a style, the style is treated as an object style.

When using one style element, after you apply the style to a text selection, the text selection will be wrapped in the inline style element. An example would be if your style element was span and you selected some text, after applying the custom style the selected text would be wrapped in a span element.

Example: single element

array(
    'title' => t('Yellow Highlight'), 
    'element' => 'span', 
    'attributes' => array('class' => 'yellow-highlight')
)

This style will wrap a text selection in a span element and apply the class "yellow-highlight".

If multiple style elements are used, the selected text will not be wrapped in an inline element, instead only elements that match the inline style elements are targeted. An example would be if your style elements were sup and sub, the custom style would only be available for those two elements. If the selected text was in a span element, that custom style would not be available.

Example: multiple elements

array(
    'title' => t('Red Subscript Superscript'),
    'element' => array('sub', 'sup'),
    'attributes' => array('class' => 'red-sub-sup')
)

This style will target only sub and sup elements and will apply the "red-sub-sup" class.

Custom

CKEditor plugins can define special style handlers which can be applied in certain situations. One type of custom handler is used for widgets. Widget styles only accept class attributes, other style definitions will be ignored. An example widget in Concrete is the Enhanced Image plugin.

Example: Enhanced Image custom widget style

array(
    'title' => t('Blue Border Circle'),
    'type' => 'widget',
    'widget' => 'image',
    'attributes' => array('class' => 'blue-border img-circle')
)

This custom style targets images added by the Enhanced Image plugin widget.

CKEditor Style Rules

title

The title rule is the custom style name that will be displayed in the styles drop-down. This rule is required and must be unique.

Example:

array(
    'title' => t('Style Name'),
    'element' => 'element_name',
    'attributes' => array('attribute_name' => 'attribute_value')
)

element

The element rule is the element(s) that will be targeted, the element to be replaced with, or the element to be wrapped with. Single and multiple elements can be used. When multiple elements are used, they are added as values in an array.

Example: single element

array(
    'title' => t('Style Name'),
    'element' => 'element_name',
    'attributes' => array('attribute_name' => 'attribute_value')
)

Example: multiple elements

array(
    'title' => t('Style Name'),
    'element' => array('element_name1', 'element_name2'),
    'attributes' => array('attribute_name' => 'attribute_value')
)

styles

The styles rule is used to define inline CSS that will be applied to an element. Multiple CSS declarations can be used, with each declaration added as a key value pair. The CSS property is the key and the CSS value is the value.

Example: single CSS declaration

array(
    'title' => t('Style Name'),
    'element' => 'element_name',
    'styles' => array('css_property' => 'css_value')
)

Example: multiple CSS declarations

array(
    'title' => t('Style Name'),
    'element' => 'element_name',
    'styles' => array(
        'css_property1' => 'css_value1',
        'css_property2' => 'css_value2',
    )
)

attributes

The attributes rule is used to define element attributes that will be applied to an element. Example element attributes are class, ID, and data attribute, but can be any attribute supported by a specific element. Multiple attributes can be used, with each attribute name and attribute value added as a key value pair. The attribute name is the key and the attribute value is the value.

Example: single attribute

array(
    'title' => t('Style Name'),
    'element' => 'element_name',
    'attributes' => array('attribute_name' => 'attribute_value')
)

Example: multiple attributes

array(
    'title' => t('Style Name'),
    'element' => 'element_name',
    'attributes' => array(
        'attribute_name1' => 'attribute_value1',
        'attribute_name2' => 'attribute_value2',
    )
)

type

The type rule is used in combination with the widget rule and tells CKEditor that this is a custom widget style. The value of the type rule must be set to widget.

Example:

array(
    'title' => t('Style Name'),
    'type' => 'widget',
    'widget' => 'widget_name',
    'attributes' => array('class' => 'class-name')
)

widget

The widget rule is the name of the widget that the custom widget style will apply to. When using this rule, the type rule must also be set.

Example:

array(
    'title' => t('Style Name'),
    'type' => 'widget',
    'widget' => 'widget_name',
    'attributes' => array('class' => 'class-name')
)

Redactor Backward Compatible Rules

menuClass

The menuClass rule is used in Redactor custom styles for adding a preview of the style in the Custom Styles drop-down. This rule is optional in Redactor and unused in CKEditor. If not used it can be omitted.

spanClass

The spanClass rule is used in Redactor custom styles for setting a custom class, or classes, to selected text or text blocks. This rule is required for making custom styles backwards compatible with Redactor. When used with CKEditor, the spanClass rule is converted to an attributes class rule.

forceBlock

The forceBlock rule is used in Redactor custom styles for setting the style type to block-level or inline. This rule is converted into a form CKEditor can use.

  • A forceBlock rule set to 1 will target block-level elements: h1, h2, h3, h4, h5, h6, and p.
  • A forceBlock set to -1 will wrap the selected text in a span tag.
  • If no element is set and forceBlock is not set, the CKEditor style will be an inline style using a span element.

When making themes that are for v8 only or using styles that are just for CKEditor, menuClass, spanClass, and forceBlock are not necessary and can be omitted.

Use Redactor or CKEditor Styles Based on Concrete Version

By checking the Concrete version before defining a theme's editor styles in getThemeEditorClasses(), you can conditionally load Redactor or CKEditor styles. If the version is 5.7, then Redactor styles will be used, and if version 8, CKEditor styles will be used. This allows you to offer basic Redactor styles and more flexible, powerful CKEditor styles in the same theme without worrying about backwards compatibility and editor specific style rules.

This code allows you to use custom editor styles based on Concrete version:

public function getThemeEditorClasses()
{
    // check to see if the compat_is_version_8() function exists
    if (!function_exists('compat_is_version_8')) {
        // if not, create the compat_is_version_8() function
        function compat_is_version_8() {
            // return a boolean, true or false, if the interface Concrete\Core\Export\ExportableInterface exists
            // - Concrete 5.7 does not have this interface, so it will be false
            // - Concrete v8 does have this interface, so it will be true
            return interface_exists('\Concrete\Core\Export\ExportableInterface');
        }
    }

    // check the Concrete version
    if (compat_is_version_8()) {
        // concrete5 v8
        // - custom styles for CKEditor
    } else {
        // concrete5 5.7
        // - custom styles for Redactor
    }
}

This is an example of loading your custom styles based on the Concrete version and what the default Elemental theme custom styles look like written as CKEditor styles:

public function getThemeEditorClasses()
{
    // check to see if the compat_is_version_8() function exists
    if (!function_exists('compat_is_version_8')) {
        // if not, create the compat_is_version_8() function
        function compat_is_version_8() {
            // return a boolean, true or false, if the interface Concrete\Core\Export\ExportableInterface exists
            // - Concrete 5.7 does not have this interface, so it will be false
            // - Concrete v8 does have this interface, so it will be true
            return interface_exists('\Concrete\Core\Export\ExportableInterface');
        }
    }

    // check the Concrete version
    if (compat_is_version_8()) {
        // concrete5 v8
        // - custom styles for CKEditor
        return array(
            array(
                'title' => t('Title Thin'),
                'element' => array('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'),
                'attributes' => array('class' => 'title-thin')
            ),
            array(
                'title' => t('Title Caps Bold'),
                'element' => array('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'),
                'attributes' => array('class' => 'title-caps-bold')
            ),
            array(
                'title' => t('Title Caps'),
                'element' => array('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'),
                'attributes' => array('class' => 'title-caps')
            ),
            array(
                'title' => t('Image Caption'),
                'element' => 'span',
                'attributes' => array('class' => 'image-caption')
            ),
            array(
                'title' => t('Standard Button'),
                'element' => 'span',
                'attributes' => array('class' => 'btn btn-default')
            ),
            array(
                'title' => t('Success Button'),
                'element' => 'span',
                'attributes' => array('class' => 'btn btn-success')
            ),
            array(
                'title' => t('Primary Button'),
                'element' => 'span',
                'attributes' => array('class' => 'btn btn-primary')
            ),
        );
    } else {
        // concrete5 5.7
        // - custom styles for Redactor
        return array(
            array('title' => t('Title Thin'), 'menuClass' => 'title-thin', 'spanClass' => 'title-thin', 'forceBlock' => 1),
            array('title' => t('Title Caps Bold'), 'menuClass' => 'title-caps-bold', 'spanClass' => 'title-caps-bold', 'forceBlock' => 1),
            array('title' => t('Title Caps'), 'menuClass' => 'title-caps', 'spanClass' => 'title-caps', 'forceBlock' => 1),
            array('title' => t('Image Caption'), 'menuClass' => 'image-caption', 'spanClass' => 'image-caption', 'forceBlock' => '-1'),
            array('title' => t('Standard Button'), 'menuClass' => '', 'spanClass' => 'btn btn-default', 'forceBlock' => '-1'),
            array('title' => t('Success Button'), 'menuClass' => '', 'spanClass' => 'btn btn-success', 'forceBlock' => '-1'),
            array('title' => t('Primary Button'), 'menuClass' => '', 'spanClass' => 'btn btn-primary', 'forceBlock' => '-1')
        );
    }
}

Style Formatting

When dealing with long custom styles that have nested arrays (arrays within arrays) it is helpful to write the style across multiple lines. This makes the style much easier to read and understand.

Example: default formatting

array('title' => t('Complex Style'), 'element' => array('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'), 'styles' => array('font-weight' => 'bold', 'background' => 'lightgreen'), 'attributes' => array('class' => 'my-class', 'data-special' => 'my value'))

Example: multiple line formatting

array(
    'title' => t('Complex Style'),
    'element' => array('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'),
    'styles' => array(
        'font-weight' => 'bold',
        'background' => 'lightgreen'
    ),
    'attributes' => array(
        'class' => 'my-class',
        'data-special' => 'my value'
    )
),

If your theme requires PHP 5.4 or above, then you can write your styles using short array syntax.

Example: array() syntax

array('title' => t('Yellow Highlight'), 'element' => 'span', 'attributes' => array('class' => 'yellow-highlight'))

Example: short array syntax

['title' => t('Yellow Highlight'), 'element' => 'span', 'attributes' => ['class' => 'yellow-highlight']]

Mistakes to Avoid

Whenever possible, and with few exceptions, avoid using inline CSS to style content. Inline CSS has a very high selector specificity and is very hard to override and manage. When you switch themes, attempt to update your theme CSS, or migrate your content - that inline CSS will come along with all your content. Use CSS selectors instead.

An example might be that you use inline CSS to make the background of external link tags yellow because the color stands out and goes well with the rest of your theme colors. In the future you might refresh your site design and use completely different colors, but all your links will still have a yellow background, and now they don't match the rest of your theme. If you had used CSS selectors like class, element, and attribute selectors for styling, you could easily update your link styling across your entire site, instead you are stuck with links with yellow backgrounds forever unless you manually update all your content, write highly specific CSS styles using !important, or write a script to search through your content to remove the inline CSS.

Recent Tutorials
Customize locale icons
Oct 29, 2024
By myq.

How to customize locale (language region) flags

Concrete CMS Caching Guide
Oct 16, 2024

An overview of types of caching in Concrete and considerations when using them.

Redirect all requests to HTTPS
Oct 9, 2024
By myq.

How to follow best practices for a secure web

Upgrade Concrete versions 9.3.1 and 9.3.2
Sep 10, 2024
By myq.

How to get past a bug in versions 9.3.1 and 9.3.2 that prevents upgrading the Concrete core through the Dashboard

How to use Composer with Marketplace extensions
Aug 22, 2024

Composer can be used to manage third-party extensions from the marketplace

Controlling Google Tag Manager Tags Based on Concrete CMS Edit Toolbar Visibility
Aug 13, 2024

This document provides a step-by-step guide on how to control the firing of Google Tag Manager (GTM) tags based on the visibility of the Concrete CMS edit toolbar. It explains how to create a custom JavaScript variable in GTM to detect whether the edit toolbar is present on a page and how to set up a trigger that ensures GTM tags only fire when the toolbar is not visible. This setup is particularly useful for developers and marketers who want to ensure that tracking and analytics tags are not activated during content editing sessions, thereby preserving the accuracy of data collected.

Improvements?

Let us know by posting here.