Skip to content

Craft 6 support - #641

Merged
i-just merged 103 commits into
6.xfrom
feature/craft-6
Nov 10, 2026
Merged

i-just merged 103 commits into
6.xfrom
feature/craft-6

Conversation

@i-just

@i-just i-just commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Ports CKEditor to Craft 6: native plugin class, controllers, events, field and settings UI, with no Yii 2 adapter.

Requires

What's changed

Port

  • Native Craft 6 plugin class, controllers, events, GraphQL, console commands and assets
  • The CKEditor input is a native UI control (ckeditor:editor), and the field settings use Craft 6 UI controls
  • The Yii 2 adapter and legacy class aliases are removed
  • Craft 5 CSS classes, CSS variables and markup are replaced with Craft 6 utilities, tokens and web components
  • Image uploads use Craft 6's uploader
  • The editor no longer falls back to the legacy element editor. Nested entries, pasting and image entries go through the owner bridge, and nested entries always open in the native slideout
  • Craft.uuid(), Craft.trim(), Craft.escapeHtml() and jQuery are replaced with native APIs where that's straightforward
  • Pint replaces ECS; PHPStan and Rector use Craft 6's versions

Field settings

  • nystudio107/craft-code-editor is no longer required, since it doesn't support Craft 6 yet. The config options (JSON/JavaScript) and custom styles (CSS) settings are plain text areas, without syntax highlighting, validation or autocomplete. The Monaco code is commented out and tagged TODO(code-editor), so it can be restored once there's a Craft 6 release.

HTML sanitizing

  • HTML Purifier is replaced by Craft 6's HTML sanitizers (symfony/html-sanitizer). Existing Purify HTML / HTML Purifier Config settings are mapped onto Sanitize HTML / HTML Sanitizer by craftcms/html-field
  • CKEditor extends the selected sanitizer's config with the markup the field needs (nested entry tags, list attributes, to-do list inputs, link attributes) and then dispatches HtmlSanitizerConfigResolving, as v5 did for every HTML Purifier config. Sanitizers registered as instances are used as-is
  • Link target and rel attributes are only allowed when the field's link options or advanced link fields can add them, as in v5

Nested entries

  • Cards use the card component's slots, as in Matrix (ElementHtml::elementCardData())
  • Card HTML is sent alongside the field value instead of inside it (data-card-html)

Security

  • ckeditor/entry-card-html now requires view access to the entry, and integer IDs
  • The fallback card's error message is escaped, which closes a stored XSS that a crafted data-entry-id could trigger

Breaking changes

See UPGRADE.md.

Known gaps

  • Nested entry card markup copies core's SelectableCardList.vue slot layout
  • The owner bridge uses the undocumented craft:nested-owner-editor-request event
  • ElementCardHtmlResolving no longer fires for CKEditor cards, unless [6.x] Fire ElementCardPartsResolving for every element card cms#20335 (or something similar) gets approved and merged for the core
  • The settings code fields lack Monaco until nystudio107/craft-code-editor supports Craft 6. Restoring it means re-adding "nystudio107/craft-code-editor" to composer.json and uncommenting the TODO(code-editor) blocks
  • PHPStan reports HtmlSanitizers::config() as undefined, and the field tests fail, until [6.x] Add HtmlSanitizerManager::config() cms#20328 is merged into 6.x
  • Legacy config/craft/htmlpurifier/*.json configs (only loaded with the Yii 2 adapter) aren't extended with CKEditor's markup, so they strip nested entries and other markup they don't allow on save. The README and UPGRADE.md tell people to recreate them in config/craft/sanitizers/
  • The field settings toolbar builder still uses the legacy Garnish (Base, DragDrop) and jQuery, because Craft doesn't share @craftcms/garnish with plugins yet
  • Entry type dropdown IDs use crypto.randomUUID(), which browsers only provide over HTTPS or on localhost, the same as Craft 6's own Vue code

🤖 Generated with Claude Code

i-just and others added 30 commits November 2, 2026 15:43
- Require Craft 6, PHP 8.5, craftcms/yii2-adapter and the Craft 6
  branch of craftcms/html-field
- Port the field and its data classes to native Craft 6 APIs on top of
  CraftCms\HtmlField\HtmlField; the CP input and settings UI still
  render through the adapter's legacy HTML bridge
- Replace HTML Purifier with Craft's HTML sanitizers
- Register the field type through the native field type registries
- Start UPGRADE.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Extend CraftCms\Cms\Plugin\Plugin and move initialization to boot()
- Move to the CraftCms\Ckeditor namespace and Laravel-style folders,
  keeping the old class names loadable as aliases
- Port the settings to PluginSettings with a settingsUi() page
- Port the controllers to plain Laravel controllers routed from
  routes/actions.php, keeping the existing action URLs
- Move templates, translations and the icon to resources/ and lang/
- Remove CKEditor's own reference tracking in favor of Craft's field
  references, with a migration that copies existing references and
  renames the stored field type
- Document the changes in UPGRADE.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Replace the Yii-style EVENT_MODIFY_CONFIG and EVENT_DEFINE_LINK_OPTIONS
  events with EditorConfigResolving and LinkOptionsResolving Laravel
  events
- Add HtmlSanitizerConfigResolving to replace the removed HTML Purifier
  config event, and build each field's sanitizer only once
- Port the GraphQL types to CraftCms\Ckeditor\Gql on native APIs
- Bind the plugin settings in the container
- Stop implementing EagerLoadingFieldInterface, matching Craft 5, so
  GraphQL resolves the field value instead of eager-loaded entries
- Document the changes in UPGRADE.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Replace the Yii console controller with ckeditor:convert:redactor and
  ckeditor:convert:matrix Artisan commands, keeping the old names as
  aliases; the Matrix conversion now generates a Laravel content
  migration
- Replace the Yii asset bundles with published files and a
  CkeditorAssets service, and add CkeditorPackage for registering
  CKEditor packages from other plugins
- Move the front-end sources and builds to resources/assets/
- Render the field input and settings with HtmlStack, InputNamespace
  and template() instead of the legacy view
- Remove craft.app and view usage from the templates
- Update the README and UPGRADE.md for Craft 6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Add CkeditorControl (`ckeditor:editor`) and a Vue component that loads
  the editor's assets, imports the field's editor setup module, and emits
  the editor data as the control value on every change
- Replace the adapter's legacy field HTML bridge with uiControl(); the
  field settings still use the adapter for now
- Add an owner bridge so nested entries and image entries are created
  for the owner's draft, and existing nested entries are edited in a
  native slideout that saves into the owner's draft
- Pass the layout element UID through the editor config, and use Craft 6
  element class names in the editor JS
- Use a system icon for the field type

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Replace the legacy settings template with settingsUi(), using core
  controls for entry types, heading levels, link fields, limits, assets,
  sanitizing and GraphQL settings
- Add EditorConfigControl (`ckeditor:editor-config`), which mounts the
  toolbar builder, config options and custom styles, starts
  craft-code-editor's Monaco editors, and emits their values together
- Add a "Show as separate buttons" setting in place of the per-entry-type
  expanded toggle
- Remove the legacy settings template, entry type select input and
  field settings controller actions
- Drop the remaining Yii 2 adapter classes from the field

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Drop craftcms/yii2-adapter from composer.json; the plugin now runs on
  native Craft 6 APIs only, and offers category links only when the
  adapter's Category element type is installed
- Update composer.lock for Craft 6
- Accept a delimiter in ConvertRedactorCommand::split()
- Finish UPGRADE.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Replace craftcms/phpstan with phpstan/phpstan ^2.1 and
  larastan/larastan ^3.13, and orchestra/testbench ^11.0 so Larastan
  can boot Laravel
- Replace craftcms/rector (Craft 4 to 5 upgrade rules, which pinned
  PHPStan 1) with rector/rector ^2.3 and driftingly/rector-laravel ^2.6.2
- Point phpstan.neon at Larastan and PHP 8.5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The plugin uses Craft 6 APIs (like CraftCms\Cms\Ui) that aren't in a
tagged release yet, so load craftcms/cms from ../cms for development.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Return null for entry type configs without an ID or UID, instead of
  catching the exception getEntryType() throws for them
- Annotate nested entries when rendering cards
- Remove incorrect @var tags and a redundant array_values() call
- Ignore the adapter-only Craft class in the category link sources
- Pass booleans to abort_unless()
- Fix the return types of EntryType::getUsageConfig() and
  CkeditorConfigSchema::interface2ref()

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Render the JSON/JavaScript/File and CSS/File toggles with Craft 6's
craft-button-group and craft-button elements, and fix the "keep
JavaScript" path when switching to JSON with the native Listbox.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace craftcms/ecs (Craft 5 coding standard) with laravel/pint ^1.22,
as Craft 6 uses, and point the check-cs and fix-cs scripts at it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drop src/legacy-aliases.php and the craft\ckeditor\ autoload root, so
the Craft 5 class names are no longer loadable. The upgrade migration
renames the stored field type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Hide the toolbar builder's used items, since Craft 6's layered
  `.hidden` utility loses to the item's display rule
- Give every Craft 5 CSS variable a Craft 6 design token fallback, so
  the editor and field settings render correctly without the legacy
  control panel styles (this also restores the separator's width)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the Expand to a separate button / Collapse to a dropdown chip action
and the Show as a separate button indicator on the field settings' entry
type chips, keeping the expanded value via EntryTypeSelect::extraValueKeys().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The toolbar builder, config options and custom styles are now separate
settings controls, with their labels and instructions coming from the
settings UI. Config Options and Custom Styles are listed under the
Advanced group, as in Craft 5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moves the CKEditor UI controls to Vue single-file components under
resources/js, bundled through the plugin's $vite input and registered
from resources/js/cp.ts, following Commerce. Upgrades Vite to 8 for
laravel-vite-plugin, and rebuilds the editor and field settings bundles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds declare(strict_types=1) to the PHP files in src/ and routes/,
matching Craft 6 core. Casts the values strict types would otherwise
reject, so behavior stays the same: empty config option and custom
style values, nested entry HTML, and the encoded config options JSON.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drops the yii2 keyword and the yiisoft/yii2-composer plugin allowance,
and points the docs, RSS and documentation URLs at the 6.x branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Excludes the JS sources and build configs from the Composer archive,
marks the built assets in resources/assets/*/dist and public/build as
generated, and removes entries for files that don't exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the forms.select macro shim with core's select component, so
the editor config template no longer imports _includes/forms.twig. The
file option maps are converted to the label/value list it expects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Whitespace only. Matches core's Twig templates, and removes the extra
indentation left over from the removed field wrappers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Uses core's <craft-icon> instead of Craft.ui.icon(), which fetched the
SVG through an action request and left the indicator empty for a moment
after the page loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
craft-field lays its input row out with flexbox, so the container needs to grow to fill it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vite copies publicDir (the package's public/ folder, now home to the cp build) into outDir.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use ElementHtml::elementCardData() and fill craft-card's header, content,
footer and thumbnail slots, and update the fallback error card markup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The control panel's CSS reset removes block margins and list styles, and
the editable area's padding was too big.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Identify the entry by its owner and field in the slideout URL, and only
move it into the owner's draft when it isn't already there, like Matrix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refresh the card after every slideout save, including autosaves, by
replacing its HTML in place, and render it with the user's provisional
draft.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the progress bar under the field, which duplicated what the
upload notification already reports. The placeholder goes in at the drop
position and its source is swapped for the asset's once the upload
lands; creating the owner's draft replaces the editor's value, so
pending placeholders are put back after a reload.
brianjhanson and others added 2 commits November 4, 2026 14:48
Keeps the Craft.* and Garnish.* shape at the call sites, so the diff
stays close to the original and CKEditor's own locale `t` is no longer
shadowed. The generated editor module keeps a named import, since a
namespace called Craft there would shadow the global for a field's
custom config JS.
Remove upload placeholders and their restoration hooks. Fix canceled toolbar animations, respect reduced motion, cache configuration lookups, and clone toolbar components directly. Repair corrupted strings and trim unused module declarations and dev exports.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
i-just and others added 21 commits November 5, 2026 13:46
resources/assets/fieldsettings/vite/ held the output of the first Vite
builds, from before outDir was switched to dist/. Nothing loads it, and
it still contained the old jQuery/Velocity ToolbarBuilder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lock file still listed jQuery as a root peer dependency after it was
removed from package.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing reads window.Craft any more, and the global const would hide a
missing `import * as Craft from '@craftcms/cp'` from type-checking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Web Animations API doesn't write in-between values to the inline
style the way Velocity did, so releasing a source item mid-collapse
snapped it back to full width. Read the margin from the computed style
instead, which includes running animations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each modal mounts its own Vue app into a host element on <body>, which
only the handle's destroy() removes, so every image or element link
selection left one behind. Also stop passing closeOtherModals to the
link selector, since the modal ignores it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The element editor panel never triggers beforeClose, so the cursor and
focus weren't restored after creating an entry from the toolbar. Use
the onClose setting instead, and place the cursor after the new card,
adding a paragraph when no text block follows it. Canceling still
returns the cursor to where it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every name used from @craftcms/cp in the TypeScript bundle has to be
declared in craftcms-cp.d.ts and listed in vite.config.mts, and one
missing from the Vite list is only noticed under the dev server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The row relied on Craft 5's .flex for its gap and wrapping, so in Craft 6
its items sat flush against each other and the sites dropdown stayed on
the same line instead of breaking onto its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without one, addActionsToChip() had nowhere to put the Replace and
Remove actions, and its jQuery fallback no longer runs. Request the chip
with its action menu, limited to the View and Edit actions, and add
Replace and Remove to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Craft 6 replaced the “Copy value from site” field action with the Compare
screen and removed CrossSiteCopyableFieldInterface, so update craftcms/cms
and drop the interface and copyCrossSiteValue().

The CKEditor control is now readable and writable, so its value (with the
nested entries’ card HTML) can be copied between sites, drafts and
revisions. As the Compare screen copies the HTML as is, the field now
duplicates any nested entries the element doesn’t own in its site when
it’s saved, and points the value at the duplicates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[6.x] Move CKEditor off jQuery and the Craft globals
Add a persistent Craft 6 development workbench
…ed without the field posted

The editor only posts changed values, so after the first autosave the CKEditor field isn't dirty, but propagateAll still copies its content (with this site's nested entry IDs) to the other sites. Mark the field dirty in that case, so NestedElementManager duplicates the nested entries and adjusts the other sites' values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e” field actions

Craft 6 added “Copy value” and “Paste value” field actions for controls that
can be read and written, which the CKEditor control is. Update craftcms/cms
and handle them the way core’s controls do:

- Only copy values between controls that allow the same entry types, via
  `CkeditorControl::getValueScope()`, as Table does for its money columns.
- Preview copied values in the “copied” card as their first lines of text,
  with nested entries as badges, registered with `Cp.$ui.registerValuePreview()`
  in `Cp.booting()`. Register the controls in `Cp.booting()` too, as the
  docs say.
- When saving, duplicate nested entries owned through another field into this
  one, and leave out ones of entry types the field doesn’t allow.
- Keep read-only values’ nested entries as `<craft-entry>` elements, with their
  cards sent alongside, so values copied from read-only fields keep them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@i-just
i-just merged commit 0d463fa into 6.x Nov 10, 2026
12 checks passed
@i-just
i-just deleted the feature/craft-6 branch November 10, 2026 18:42
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