Skip to main content

CKEditor widgets: an alternative to Paragraphs?

Wednesday 15 de July de 2020

It is quite common for publishers to want to be able to insert predefined HTML fragments (small components and even layouts) into their content.

In some websites, such as online media outlets, these pieces of code can grow in number until they become a full-fledged library of simple and reusable components: quotes, information boxes, call-to-action, etc.

The first impulse of a Drupalista might be to use a combination of fields + field groups and/or paragraphs, overlooking a solution that in many occasions is simpler and easier to maintain, just as solid and that provides great advantages for the end-user: CKeditor widgets.

 

What is a widget?

In view of the need to offer a set of predefined formats, it is common to use CKeditor styles, which allow us to associate an HTML element and a class (or ID, or even inline CSS). When clicking on this style, CKeditor tries to convert the element in focus.
 

Configuración de estilos de Ckeditor

 

A widget is a more complete and advanced solution to this problem:

  1. Allows (and keeps) any HTML structure.
    While the style is applied to a single element, a widget allows any markup. In addition, CKEditor treats the widget as a whole and keeps its structure intact. This avoids a common problem where an applied style can be unintentionally altered when editing text, resulting in malformed or broken HTML that CKEditor filters out and removes
     
  2. It is configurable, draggable and usable.
    While a style can often be invisible, the widget is clearly distinguished by its yellow outline and drag icon.
     
  3. It can be prepopulated.
    Styles are applied to the text and can't be prepopulated to show the expected content to the user. Widgets can be initialized already completed, telling the user what the component looks like and what to enter.
     
  4. You can contextually limit the allowed HTML tags.
    This is extremely useful to give power to the user without resulting in malformed, semantically incorrect HTML or altering our design. You can also indicate a different configuration for each editable zone.

     

    A simple widget case: call-to-action.

    Let's say our client wants to be able to freely insert a call-to-action (CTA) like this one:

    Un call-to-action

     

    Initially, this CTA is a good candidate to be implemented as a paragraph divided into three fields: a plain text for the title, a formatted text for the body and a link field for the button. 

    The result could be as follows:
     

    Propueta de paragraph

    However, this example is a good candidate to be implemented as a widget instead, as long as we can confirm that it is a purely visual component that does not need to interact with Drupal (e.g. the link does not need to autocomplete, reference a Drupal entity, etc...).

    Next, we will show how this same component would be developed as a widget.

     

    1 - Create the Drupal plugin

    The first step is to create a module and, within it, a CKeditor plugin. For the purpose of this tutorial we are going to use the autogenerated code through Drupal Console or Drush (from version 9):

    drupal generate:plugin:ckeditorbutton
    drush generate plugin-ckeditor
    

    If you use Drupal Console, beware: in certain versions some values need to be corrected. You can also follow this article if you prefer to do it by hand.

    In its most minimalist implementation, the Drupal plugin simply has to report the icon address and the plugin.js file, and the rest is already CKeditor's responsibility. This will be enough for us.

    It's time to go to our text editor settings and drag the new button to our toolbar.

     

    El botón, añadido a CKeditor

     

    2 - Create the CKeditor widget

    At this point we have a button that does nothing. It's time to turn it into a CKeditor widget that allows us to insert our component.

    CKEditor has an excellent tutorial where it tells step by step how to create a widget so here we are going to focus only on some implementation details.

    The first thing we need to know is that a widget is a special type of CKeditor plugin that depends on the 'widget' plugin and doesn't implement the usual methods. Hence, we need to add the dependency and (if we've used the generator) remove the call to editor.addCommand().

    But, most importantly, to integrate correctly with Drupal we have to make an explicit call to editor.ui.addButton(), something that doesn't appear in the official tutorial and that can be the source of more than one headache.

     

    CKEDITOR.plugins.add( 'cta_widget', {
        // Necesitarás añadir esta dependencia:
        requires: 'widget',
    
        // Comprueba que tiene el mismo id que tu botón:
        icons: 'cta_widget',
    
        init: function( editor ) {
            
            // Añade esta llamada dentro de init
            editor.ui.addButton('cta_widget', {
                label: 'CTA Widget',
                id: 'cta_widget',
                command: 'cta_widget',
                icon: this.path + 'icons/cta.png'
            });
        }
    });

    Once this is done, we only have to configure the widget using the editor.widgets.add() method:

    editor.widgets.add( 'cta_widget', {
        template: ''
        editables: {},
        allowedContent: ''
        upcast: function( element ) {},
    });

     

    3 - Define the HTML

    The template property is a string that will define the desired HTML markup. In this string we must include the complete markup, including both editable and non-editable elements. In our case, it could be something like this:

    template:
        '<div class="cta">' + 
            '<h3 class="cta__title">Insert title here...</h3>' + 
            '<p class="cta__text">Insert your text here</p>' + 
            '<div class="cta__link"><a href="#">Double click to edit link</a></div>' + 
        '</div>',

    Note how a default text is also provided. This allows us to pre-populate the widget with values that tell the user what kind of information they expect to receive.

    If we go to our CKeditor at this point and press the button, this HTML should be inserted and, if we have loaded the CSS correctly, we should see it well styled. However, in this state it is still not editable.

    4 - Crear editable areas and restrict content

    The property 'editables' from editor.widgets.add() allows to define an object to name the different editable areas and allows us to configure them. In our case, a good configuration is the following:

    editables: {
        title: {
            selector: '.cta__title',
            allowedContent: 'strong em',
        },
        content: {
            selector: '.cta__text',
            allowedContent: 'strong em mark br; a[!href]',
        },
        link: {
            selector: '.cta__link',
            allowedContent: 'a[!href]',
        }
    },

    As you can see, we're mapping HTML elements like <h3 class="cta__title"> with entries like title, identified by a selector. In addition, we are making use of a very interesting functionality that is the ability to define the type of content that each editable zone allows.

    The allowedFormats key uses the special syntax of CKeditor's Allowed Content Rules and allows us to restrict the allowed HTML, a very useful functionality and much more granular than Drupal text formats.

    In this case, we are avoiding in the title the use of anything other than <strong> and <em>, which is usually a quite common requirement from clients in title fields. In the body of the text we allow the use of a few more elements and in the button we only allow links.

    At this point the widget should already be functional and should allow the user to create, edit and even drag and drop it:

    At this point, someone might suggest that the .cta__link editable element might be an <a> instead of a <div>. This is true, but it is not possible with the standard Drupal configuration of CKEditor since only the elements defined in CKEDITOR.$dtd.editable can be converted into editable zones.

    It is possible to change this configuration to add the elements of type <a>, but this exceeds the purpose of this tutorial, so we will leave it as it is for now.

    5 - The final touch: autedection and permissions

    In its current state, the widget still has two possible problems.

    On the one hand, if we're using the "Limit allowed HTML tags and correct faulty HTML" filter, it's quite possible that when we save the content or go into "HTML source" mode we'll lose the CSS classes. This is due to the sanitizing of CKEditor we talked about before.

    If we were in a standard configuration of CKEditor, it would be enough to add the following configuration to editor.widgets.add() and it would add it to CKeditor's whitelist:

    allowedContent: 'h3(cta__title); div(cta__text, cta__link)',

    However, this is not enough since Drupal implements its own mode of popularizing the CKEditor configuration. Since it seems that at the moment there is no good way to add new rules programmatically, we will have to add it manually in the configuration of each editor:
     

    Configuración de CKEDITOR

     

    In addition, simply inserting an HTML file identical to our widget does not mean that it is automatically instantiated. In other words, in the case of directly copying and pasting the HTML of our component, the user might want to edit it and find that it is not possible.

    This is quickly solved by implementing the upcast(), method, which is not mandatory, but highly recommended:

    upcast: function( element ) {
        return element.name == 'div' && element.hasClass( 'cta' );
    },

    Each time it processes the HTML, CKEditor iterates over the HTML element by element and gives us a chance to indicate that we want to initialize our widget. In this case, we're telling CKEditor to try to build the widget if it finds a <div> with the .cta class. The editor will check to see if this element matches our template, and if it does, it will initialize it.

    Our final code will look like this:

    CKEDITOR.plugins.add( 'cta_widget', {
        // Widget plugin dependency
        requires: 'widget',
        icons: 'cta_widget',
        init: function( editor ) {
            editor.widgets.add( 'cta_widget', {
                template:
                    '<div class="cta">' + 
    	                '<h3 class="cta__title">Insert title here...</h3>' + 
                        '<p class="cta__text">Insert your text here</p>' + 
                        '<div class="cta__link"><a href="#">Double click to edit link</a></div>' + 
                    '</div>',
    
                editables: {
                    title: {
                        selector: '.cta__title',
                        allowedContent: 'strong em',
                    },
                    content: {
                        selector: '.cta__text',
                        allowedContent: 'strong em mark br; a[!href]',
                    },
                    link: {
                        selector: '.cta__link',
                        allowedContent: 'a[!href]',
                    }
                },
                
                // Not necessary, this is configured in Drupal, but is good to have it here as well.
                allowedContent:
                    'h3(cta__title); div(cta__text, cta__link)',
                
                // Not mandatory but allows widget recongnition on copy & paste.
                upcast: function( element ) {
                    return element.name == 'section' && element.hasClass( 'cta__widget' );
                },
            });
    
            // This is needed in Drupal
            editor.ui.addButton('cta_widget', {
                label: 'CTA Widget',
                id: 'cta_widget',
                command: 'cta_widget',
                icon: this.path + 'icons/cta.png'
            });
        }
    });
    
    

    When is a widget a better option than a paragraph?

    The main thesis of this article is that CKeditor widgets can replace paragraphs and fields in some cases. This may sound bad at first glance: why abandon the capabilities of Drupal 8 to delegate them to a text editor? Aren't there precisely the fields or the paragraphs to avoid the temptation to solve everything in the editor? Won't it introduce more long-term maintenance problems than it eliminates in the short term?

    It should be stressed that you should only consider this option in some very controlled cases where the following requirements are met:

    1. The HTML piece to be inserted is totally aesthetic in nature, that is, it does not depend on or interact in any way with Drupal.
      • Do you need to access the logged-in user, make a reference to an entity, access translations? Forget about widgets.
      • Is it a beautiful or different way of presenting information that could perfectly be shown without any style? Is it pure front-end? Then it is a good candidate.
    2. No major changes in its HTML structure are expected, at least in the medium term: there may be styling changes, but the HTML will remain intact. 

    If a component meets both requirements, it is highly recommended to consider it as a widget, as it introduces several improvements over the paragraphs.

    • Avoid overloading our projects with unnecessary fields and paragraphs.
       
    • It provides the editor user with a real WYSIWYG experience in which they can check in real time what the result of their work is. This is usually one of the main complaints that editors have about Drupal.
       
    • It allows for easier and more immediate markup. To style the HTML and CSS of a paragraph sometimes it is necessary to intervene at several levels, using several templates, hooks or field fomatters. With a CKeditor widget, the markup is written directly in a template. In a certain sense (I repeat: in a certain sense), it is closer to the Component-Driven Design paradigm of Vue, React or Angular.
       
    • Thanks to CKEditor's Allowed Content Rules system, it allows to define with granularity the HTML allowed and forbidden in each editable zone in a more detailed and individualized way than Drupal's fields would allow. This allows us to give more power to the editor without introducing the risk of malformed or faulty HTML.
       
    • We haven't had time to go into it in depth, but it is possible to expose widget configuration options using the dialog API. This way, we could expose options like adding margin or padding, removing certain editable areas, and so on.

    In short, CKEditor widgets are a valid alternative to some component types that we usually solve with Drupal tools like Paragraphs. In the case of purely visual, small components with stable requirements, widgets bring many advantages to the end user and are much easier to develop and maintain.

    Paragraphs
    CKeditor
    Site Building
    HTML
    JavaScript