First-Party Plugins

A guide to enabling a plugin’s plugins.


Base Module #

1. Create the module in modules/ckeditor/Module.php #

PHP
<?php

namespace modules\ckeditor;

use craft\ckeditor\Field as CkeditorField;
use craft\ckeditor\helpers\CkeditorConfig;
use craft\htmlfield\events\ModifyPurifierConfigEvent;
use yii\base\Event;
use yii\base\Module as BaseModule;

class Module extends BaseModule
{
    public function init(): void
    {
        parent::init();

        // Add registration lines here

        Event::on(
            CkeditorField::class,
            CkeditorField::EVENT_MODIFY_PURIFIER_CONFIG,
            static function(ModifyPurifierConfigEvent $event): void {
                $definition = $event->config->getDefinition('HTML', true);
                if ($definition === null) {
                    return;
                }

                // Add purifier rules here
            }
        );
    }
}

2. Register it in config/app.php #

PHP
'modules' => [
   'ckeditor' => [
       'class' => \modules\ckeditor\Module::class,
   ],
],
'bootstrap' => ['ckeditor'],

3. Add PSR-4 autoloading to composer.json #

This is only needed if you don’t already have "modules\\": “modules/".

JSON
"autoload": {
    "psr-4": {
        "modules\\ckeditor\\": "modules/ckeditor/"
    }
},

3. Run dump-autoload #

Shell
ddev composer dump-autoload

The Plugins #

 Highlight #

PHP
// Registration line
CkeditorConfig::registerFirstPartyPackage(['Highlight'], ['highlight']);
PHP
// Purifier rule
$definition->addElement('mark', 'Inline', 'Inline', 'Common');

Marker and pen colours. Emits <mark class="marker-yellow">. Without the rule the highlight vanishes on save. Six defaults: marker-yellow, marker-green, marker-pink, marker-blue, pen-red, pen-green. Needs front-end CSS, since CKEditor's styles are CP-only. Restrict the palette with a highlight.options array in the field's config JSON.

 Special Characters #

PHP
CkeditorConfig::registerFirstPartyPackage(['SpecialCharacters', 'SpecialCharactersEssentials'], ['specialCharacters']);

Character picker. Inserts plain characters, so no purifier rule. Always pair with SpecialCharactersEssentials, the base plugin alone gives you an empty panel. For a narrower picker, swap Essentials for individual sets: SpecialCharactersArrows, SpecialCharactersCurrency, SpecialCharactersLatin, SpecialCharactersMathematical, SpecialCharactersText.

 Emoji #

PHP
CkeditorConfig::registerFirstPartyPackage(['Emoji'], ['emoji']);

Picker plus :shortcode autocomplete. Plain unicode output, no rule needed. Verify your DB collation is utf8mb4 or emoji will mangle on save. That’s a Craft-install thing, not a CKEditor one.

 Show Blocks #

PHP
CkeditorConfig::registerFirstPartyPackage(['ShowBlocks'], ['showBlocks']);

Toggles block outlines in the editor. Pure editing aid, no output, no rule.

 Accessibility Help #

PHP
CkeditorConfig::registerFirstPartyPackage(['AccessibilityHelp'], ['accessibilityHelp']);

Alt+0 dialog listing keyboard shortcuts. No output. The cheapest win here.

 Text Transformation #

PHP
CkeditorConfig::registerFirstPartyPackage(['TextTransformation']);

Autocorrect as editors type - smart quotes, em dashes, ellipses. No button, so it applies to every field with no per-field opt-out. Output is plain characters, no rule. Configure or disable specific transformations with a typing.transformations object in the field's config JSON.  On a site where editors paste code samples, you'll want that!

 Mention #

PHP
CkeditorConfig::registerFirstPartyPackage(['Mention']);
PHP
$definition->addAttribute('span', 'data-mention', 'Text');

No button. Emits <span class="mention" data-mention="@ben">. Does nothing until you give it a feed, and a feed is a function, which the CP's JSON box can't hold. Craft wraps .js config files in an IIFE returning a config object, so put it in config/ckeditor/Mention.js:

JavaScript
const items = [
    { id: '@ben', name: 'Ben', link: 'https://bensomething.com' },
];

return {
    mention: {
        feeds: [{
            marker: '@',
            minimumCharacters: 1,
            feed: (query) => items.filter((item) =>
                item.id.toLowerCase().includes(query.toLowerCase())
            ),
        }],
    },
};

Select it under the field's Config Options → use a config file. That replaces the field's JSON config entirely, so merge any existing options in. For real data, swap items for a fetch() against a controller returning [{id, name, link}].

 Table Column Resize #

PHP
CkeditorConfig::registerFirstPartyPackage(['TableColumnResize']);
PHP
$definition->addElement('colgroup', 'Block', 'Optional: col', 'Common');
$definition->addElement('col', 'Block', 'Empty', 'Common', ['span' => 'Number']);

Drag handles on table columns. No button of its own, it extends the existing table feature, so the toolbar needs insertTable. Widths ride on <colgroup><col style="width:40%">. <col> escapes html-field's inline-style stripper, so the widths survive.

 Image Resize #

PHP
CkeditorConfig::registerFirstPartyPackage(['ImageResize']);
PHP
$definition->addAttribute('img', 'style', 'Text');

Resize handles. Buttons live in the image toolbar, not the main one. The rule isn't sufficient on its own: html-field runs a second pass that strips inline styles from img unless the field's "Remove inline styles" setting is off, so widths can still vanish on save. Turn that setting off on any field using this.