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:
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,
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.
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:
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
1721
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);