Skip to content

Refactor package registry - #628

Merged
brandonkelly merged 3 commits into
5.xfrom
feature/ckeditor-package-registry
Sep 28, 2026
Merged

brandonkelly merged 3 commits into
5.xfrom
feature/ckeditor-package-registry

Conversation

@brianjhanson

@brianjhanson brianjhanson commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

I'm opening this because it's interesting, but it does touch more than #625 does.

Plugin::registerCkeditorPackage() now works regardless of plugin load order, from init() or a Craft::$app->onInit() callback. CKEditor doesn't read the package list until it actually needs it, and any package registered after that is loaded immediately. With this change, the import map is built at View::EVENT_END_PAGE (#625 uses EVENT_BEGIN_PAGE; end page is the last point before the <head> gets written, so it also catches anything registered while the page renders, but either works).

Plugin::registerCkeditorPackage(TokensAsset::class, 'tokens.js');

This takes things even further by also fixing some potential bugs where two packages could export a plugin with the same name. That used to generate duplicate imports, which would break all CKEditor instances. Third-party plugins are now referenced through namespace imports (import * as __ckePackage0 from '…'). Field config JS can still refer to a third-party plugin by name (e.g. extraPlugins: [Tokens]), and that package gets imported for the field as long as the name is unique.

This also only loads third-party packages that are active within the toolbar (plus packages with no toolbar items, no plugins, or that the field's config JS refers to by name).

A few smaller fixes came along for the ride:

  • Package toolbar items and plugin mappings were re-registered every time a CKEditor field rendered. They're registered once per request now.
  • Grouped toolbar items like [['cardInsert', 'cardEdit']] never matched the toolbar, so their plugins were always removed, and the toolbar builder threw Missing component: cardInsert,cardEdit. They work now.
  • Leaving a package's button out of the toolbar only removes that package's plugins, not same-named plugins from other packages.

Asset recipes

Load for every field

public array $pluginNames = ['PasteCleaner'];
public array $toolbarItems = [];

Only side effects (the bundle is registered for every field)

public $js = [['telemetry.js', 'type' => 'module']];
public array $pluginNames = [];
public array $toolbarItems = [];

A little of everything

public array $pluginNames = ['Tokens'];
public array $toolbarItems = ['tokens'];

public function registerPackage(): void
{
    parent::registerPackage();

    // Loaded for every field
    CkeditorConfig::registerPackage($this->namespace, [
        'plugins' => ['TokensAutocomplete'],
    ]);
}

Grouped buttons (any one of them in the toolbar loads the package)

public array $pluginNames = ['Card', 'CardUI'];
public array $toolbarItems = [
    ['cardInsert', 'cardEdit'],
];

CKEditor features

use craft\ckeditor\helpers\CkeditorConfig;

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

Potentially breaking changes

  • Code that relied on every package bundle coming along with CkeditorAsset will now have to call Plugin::registerCkeditorPackageBundles($view). This is already handled by CKEditor fields and field settings, so this is probably an edge case.
  • A package's JS, CSS, and translations only load on fields whose toolbar includes one of its buttons. Anything the module did just by being loaded won't happen on other fields.
  • Toolbar items with several items won't be grouped anymore. A package declaring ['a', 'b'] used to show up as one locked group in the toolbar builder, and now shows two separate items (which is what BaseCkeditorPackageAsset documents). Use [['a', 'b']] to keep them grouped. Saved toolbars aren't affected.
  • Bundles passed to registerCkeditorPackage() that don't extend BaseCkeditorPackageAsset are no longer registered. That was never documented, but it did happen before.
  • CkeditorConfig::getImportStatements() is deprecated.

brianjhanson and others added 3 commits September 25, 2026 10:53
Field config JS can refer to a third-party plugin class by name, e.g.
`extraPlugins: [Tokens]`, to enable it without its toolbar button. Now
that third-party packages are only imported when their buttons are in
the toolbar, and via namespace imports, that reference would throw a
ReferenceError.

When a field's JS config mentions a third-party plugin by name, import
its package with a bare named import and register its asset bundle,
unless more than one package provides a plugin with that name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@i-just i-just left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested it, and all looked good.

With the event removed from this, the user can simply use the methods that were mentioned in the README since v5 was released, which is great. And the plugin’s name doesn’t affect the load order anymore.

@brianjhanson
brianjhanson marked this pull request as ready for review September 28, 2026 16:03
@brandonkelly
brandonkelly merged commit 83fa36c into 5.x Sep 28, 2026
9 checks passed
@brandonkelly
brandonkelly deleted the feature/ckeditor-package-registry branch September 28, 2026 16:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants