Drupal 10 add Allowed HTML tags by using a CKEditor5 plugin

By kenneth, Sun, 11/26/2023 - 12:29

Authored on

Image

Drupal 10 comes with CKEditor 5 by default included in the core, at one point in time I had to limit the allowed HTML tags, however, I realized we are NOT longer able to manually overwrite the `Allowed HTML tags` field in the text format settings:

CKEditor 5 Allowed HTML tags

 

It turns out the CKEditor has a particular way of retrieving or defining which are the allowed HTML tags to be available to use.

Let's say you need to include the `mark` HTML tag: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/mark

In order to achieve this I created a custom module to add a new CKEditor plugin,

 content_authoring

Here are some important things I learned while I created this new feature. We have to include a new plugin, which we need to extend from the `CKEditor5PluginDefault` class also we should define our CKEditor plugin on both frameworks (Drupal and CKEditor5 itself), we are going to do so by using the CKEditor5Plugin annotation object. 

A curious thing, I have noticed, we need to make the plugin name unique, I think that is the reason why the plugin requires to you to use the module name plus the plugin name, in our case, `content_authoring_extra_html_tags`. Therefore, we are going to stick with that and use it along the way while we are coding our new plugin.

ExtraHtmlTags plugin

Here's our definition:

<?php

namespace Drupal\content_authoring\Plugin\CKEditor5Plugin;

use Drupal\ckeditor5\Plugin\CKEditor5PluginDefault;

/**
 * Defines the "extraHtmlTags" plugin.
 *
 * @CKEditor5Plugin(
 *   id = "content_authoring_extra_html_tags",
 *   ckeditor5 = @CKEditor5AspectsOfCKEditor5Plugin(
 *    plugins = { "content_authoring.ExtraHtmlTags" },
 *   ),
 *   drupal = @DrupalAspectsOfCKEditor5Plugin(
 *    label = @Translation("Extra HTML tags"),
 *    library = "content_authoring/drupal.content_authoring.ckeditor5",
 *    admin_library = "content_authoring/drupal.content_authoring.admin",
 *    elements = {
 *      "<mark>",
 *    },
 *    toolbar_items = {
 *      "content_authoring_extra_html_tags" = {
 *        "label" = "Extra HTML tags",
 *      }
 *    },
 *   ),
 * )
 */
class ExtraHtmlTags extends CKEditor5PluginDefault {

}

Take a deep breath, and take a look at how the annotation has CKEditor and Drupal definitions, we are using the "CKEditor5AspectsOfCKEditor5Plugin" to let CKEditor5 know the required things it needs to load the plugin(s) we are exposing, also we have the "DrupalAspectsOfCKEditor5Plugin" to talk with Drupal and informs it about the new plugin available.

Of course, the library YML file needs to have those two libraries defined. The "library" attribute exposes the  CKEditor5 plugin itself, and the "admin_library" will work when we are working in the text format Drupal admin UI page. Finally the final pieces, the "elements" attribute is where we are adding the new HTML tags we are going to allow to work with, and we need to also use the "toolbar_items" attribute to expose the widget in the CKEditor5 for the Drupal admin page.

CKEditor5 Extra HTML tags widget 

Library YML definition

We are defining the two libraries that we are using for the CKEditor5 plugin:

 drupal.content_authoring.ckeditor5:
  version: VERSION
  js:
    js/plugins/ckeditor5/content_authoring.ckeditor5.js: {}
  css:
    component:
      css/content_authoring.css: { }
  dependencies:
    - ckeditor5/internal.drupal.ckeditor5

drupal.content_authoring.admin:
  version: VERSION
  css:
    component:
      css/content_authoring.admin.css: {}

 

Both CSS files are defined in order to configure the image background to use for our new plugin, otherwise, it won't be visible and it could drive us to a lot of misunderstanding and debugging hours to figure out what we are doing wrong.

content_authoring.css

.ck-extra_html_tags-button:before {
  content: '';
  display: inline-block;
  width: 2rem;
  height: 1.5rem;
  background: url(images/extra-html-tags.png) no-repeat 50% 50%;
  background-size: 20px;
}

content_authoring.admin.css 

.ckeditor5-toolbar-button.ckeditor5-toolbar-button-content_authoring_extra_html_tags {
  background-image: url(images/extra-html-tags.png);
  background-size: 20px;
}

 

CKEditor5 Plugin

As you might noticed, the library for the CKEditor5 plugin is using adding a dependency for the "ckeditor5/internal.drupal.ckeditor5" library, the reason for that is to have available the CKEditor5 object while we work with our javascript file. We are going to include a new attribute for that object which will include our plugin object. In our use case, we can see 

ckeditor5 = @CKEditor5AspectsOfCKEditor5Plugin(
  plugins = { "content_authoring.ExtraHtmlTags" },
),

We're using our module name plus the actual plugin name, in that case, the CKEditor5 will expect to have the "content_authoring" attribute and the "ExtraHtmlTags" object inside. For the javascript file, we define a new class extending from "CKEditor5.core.Plugin" and overwriting the "init()" method to include our plugin and define any additional behavior we would like to have, in our case, we only need to expose the plugin in order to allow the new HTML tags to work with.

(function (Drupal, CKEditor5) {

  class ExtraHtmlTags extends CKEditor5.core.Plugin {
    init() {
      const editor = this.editor;

      editor.ui.componentFactory.add('content_authoring_extra_html_tags', () => {
        // The button will be an instance of ButtonView.
        const button = new CKEditor5.ui.ButtonView();

        button.set({
          label: Drupal.t('Extra HTML tags'),
          class: 'ck-extra_html_tags-button',
          tooltip: true,
        });

        // We do NOT need to render any element model but allow those HTML tags in the editor.
        // button.on( 'execute', () => {
        //   // Change the model using the model writer.
        //   editor.model.change( writer => {
        //
        //     // Insert the text at the user's current position.
        //     editor.model.insertContent(
        //
        //      writer.createElement( 'div', { class: 'extra-html-tags' } )
        //     );
        //   } );
        // } );

        return button;
      });
    }
  }

  CKEditor5.content_authoring = CKEditor5.content_authoring || {
    ExtraHtmlTags,
  };

})(Drupal, CKEditor5); 

 

It will be enough to inform CKEditor5 about the new plugin and it makes Drupal include the new HTML tag as an allowed one:

The mark HTML tag element allowed

Conclusion

Probably there are other ways to achieve the same thing, I am writing about what worked for me, and I hope it helps you in some way.  An alternative, it could be by altering any existing plugin and including another additional HTML tag, if so, we could implement the "hook_ckeditor5_plugin_info_alter" in our module.

/**
 * Implements hook_ckeditor5_plugin_info_alter().
 */
function content_authoring_ckeditor5_plugin_info_alter(array &$plugin_definitions) {
  // Check if the existing plugin definition exist in order to overwrite it.
  if (!isset($plugin_definitions['ckeditor5_image'])) {
    return;
  }
  
  /** @var \Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition $ckeditor5_image */
  $ckeditor5_image = &$plugin_definitions['ckeditor5_image']; // We extract the plugin as a reference (See & in front).
  $ckeditor5_image_definition = $ckeditor5_image->toArray();
  
  // Include any new HTML tag as a valid element and recreate the plugin definition (since we alter the reference)
  $ckeditor5_image_definition['drupal']['elements'][] = '<figure>';
  $ckeditor5_image = new CKEditor5PluginDefinition($ckeditor5_image_definition);
}

Happy coding!

Comments1

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.

Joshua Stuart Graham (not verified)

22/08/2024 - 19:51:58

See example code to make sure the ckeditor does't strip them.


(function (Drupal, CKEditor5) {

/**
* @see https://www.keboca.com/drupal-10-add-allowed-html-tags-using-ckeditor5-plugin
* @see https://ckeditor.com/docs/ckeditor5/latest/framework/tutorials/crash-course/model-and-schema.html
*/
class ExtraHtmlTags extends CKEditor5.core.Plugin {
init() {
var elementTags = [
'ruby',
'rp',
'rt',
'insert',
];
var ckeditorPrefix = 'content_authoring-ckeditor-';
const editor = this.editor;
this._defineSchema(ckeditorPrefix, elementTags);
this._defineConverters(ckeditorPrefix, elementTags);

editor.ui.componentFactory.add('content_authoring_ckeditor5_custom_elements_extra_html_tags', () => {
// The button will be an instance of ButtonView.
const button = new CKEditor5.ui.ButtonView();

button.set({
label: Drupal.t('Extra HTML tags'),
class: 'ck-extra_html_tags-button',
tooltip: true,
});

// We do NOT need to render any element model but allow those HTML tags in the editor.
button.on( 'execute', () => {
alert('This button is here to allow: ' + elementTags.map(function (elementTag) { return '<' + elementTag + '>';}).join(' '));
});

return button;
});
}
/**
* Registers the elements in the DOM.
*
* @private
*
* @see https://ckeditor.com/docs/ckeditor5/latest/framework/deep-dive/schema.html
*/
_defineSchema(ckeditorPrefix, elementTags) {
const schema = this.editor.model.schema;
for (let i = 0; i < elementTags.length; i++) {
schema.extend('$text', {
allowAttributes: ckeditorPrefix + elementTags[i]
});
}
}
_defineConverters(ckeditorPrefix, elementTags) { // ADDED
const conversion = this.editor.conversion;
for (let i = 0; i < elementTags.length; i++) {
conversion.attributeToElement({
model: ckeditorPrefix + elementTags[i],
view: elementTags[i]
});
}
}
}

CKEditor5.content_authoring_ckeditor5_custom_elements = CKEditor5.content_authoring_ckeditor5_custom_elements || {
ExtraHtmlTags,
};

})(Drupal, CKEditor5);