Skip to content
FriendsOfREDAXOPublic

About

Integrates the CKEditor5 into REDAXO CMS

Topics

Resources

Code of conduct

Stars

60 stars

Watchers

4 watching

Forks

Repository files navigation

CKEditor 5 for REDAXO

CKEditor 5 integration for REDAXO with profile-based configuration, REDAXO media/link dialogs, snippets, style management, and import/export workflows.

Quickstart

1. Install the addon and create a profile

  1. Install the addon via the REDAXO installer.
  2. Go to CKEditor 5 → Profiles and create a profile (e.g. default).

2. Add the editor to a module input

<textarea
  class="form-control cke5-editor"
  name="REX_INPUT_VALUE[1]"
  data-profile="default"
  data-lang="<?php echo \Cke5\Utils\Cke5Lang::getUserLang(); ?>"
  data-content-lang="<?php echo \Cke5\Utils\Cke5Lang::getOutputLang(); ?>"
>REX_VALUE[1]</textarea>

3. Output in the module output

<div class="ck-content">
  REX_VALUE[id="1" output="html"]
</div>

4. Include the CSS in the frontend

The bundled CSS file provides correct rendering of lists, tables, media, and other CKEditor elements. Include it in the <head> of every page:

<link rel="stylesheet" href="/assets/addons/cke5/cke5_content_styles.css">

Note: All styles are prefixed with .ck-content. The output element must carry this class for the styles to apply. Base typography (font family, size, color, line height) inherits from the parent element — your project CSS (UIkit, Bootstrap, etc.) is not overridden.


Feature Overview

Editor and UX

  • Modern CKEditor 5 integration in REDAXO backend
  • Theme support (dark, auto, notheme)
  • Language-aware placeholders and UI/content language handling
  • Height control via data attributes (data-min-height, data-max-height)
  • Stable initialization for repeated/dynamic fields (for example MBlock reindex)

Profile System

  • Profile manager for editor configurations
  • Drag and drop/tag-based profile editing
  • Expert mode with raw expert_definition + expert_suboption
  • Live preview page for profile output and integration snippets

Styles and Snippets

  • Single style entities with element/classes and optional CSS
  • Style groups with JSON configuration and optional CSS
  • Snippet entities selectable per profile (replacement for templates)
  • Auto-generated backend CSS from configured style/style-group CSS definitions

Media and Links

  • REDAXO media integration (openREXMedia) for image insertion/replacement
  • REDAXO link integration (openLinkMap, media links, mailto:, tel:, YTable)
  • Image upload endpoint for media pool upload workflows
  • Image toolbar safeguards for image linking (linkImage)

Plugin Runtime

  • Native addon plugins loaded at runtime:
    • RedaxoLinkIntegration
    • RedaxoMediaImage
    • RedaxoClearWidget
    • RedaxoQuickEdit
    • RedaxoSnippets
    • RedaxoPastePlainTextToggle
    • RedaxoMarkdownPasteToggle
    • RedaxoMinimapToggle
    • RedaxoForLists
    • RedaxoForTable
    • RedaxoVideoWidgetTest
  • External plugin registry support via addon API and JS config
  • Toolbar alias transformations for external plugins

Tables and Lists

  • for_table properties for table, column, row and cell via native dropdown panels
  • Optional class presets for table/column/row/cell in profile settings
  • Global table class defaults in Profiles > Defaults > Global settings
  • Backward-compatible toolbar normalization:
    • tableProperties -> forTableProperties
    • tableColumnProperties -> forTableColumnProperties
    • tableRowProperties -> forTableRowProperties
    • tableCellProperties -> forTableCellProperties
  • If forTableProperties is present in old profiles, column/row properties are auto-added
  • Unified ordered-list numbering logic across browsers (including nested decimal levels and start index)

QuickEdit Command Menu

QuickEdit opens an inline command menu at the cursor position when typing / in the editor. It only lists commands that fit the active profile configuration, for example enabled heading levels or media/widget actions whose toolbar item is present in the profile.

The feature can be disabled globally in CKEditor 5 > Profiles > Defaults > Global settings. It uses the normal rex_config_form storage format: |1| enables QuickEdit, null or an empty value disables it. If the config key does not exist yet, QuickEdit defaults to enabled for compatibility.

Other addons can add menu entries by loading a JavaScript file before editor instances are created. The recommended way is to register that file via the CKE5 plugin registry in the addon boot.php:

<?php

use Cke5\PluginRegistry;

if (rex::isBackend() && rex_addon::get('cke5')->isAvailable()) {
    PluginRegistry::addPlugin(
        'my_quickedit_commands',
        rex_url::addonAssets('my_addon', 'js/cke5-quickedit-commands.js')
    );
}

The loaded JavaScript file extends window.CKE5_QUICKEDIT_COMMANDS:

window.CKE5_QUICKEDIT_COMMANDS = window.CKE5_QUICKEDIT_COMMANDS || [];

window.CKE5_QUICKEDIT_COMMANDS.push({
  id: 'myQuickAction',
  label: 'My action',
  keys: ['my', 'action'],
  icon: 'M',
  toolbarItem: 'link',
  execute: function (editor) {
    editor.execute('link', 'https://redaxo.org');
  }
});

Supported properties:

  • id: unique technical command name.
  • label: text shown in the QuickEdit menu.
  • keys: optional search aliases for filtering after /....
  • icon: short text/icon for the leading column.
  • toolbarItem: toolbar item that must be present in the active profile.
  • toolbarAny: alternative toolbar items; at least one must be present.
  • command: CKEditor command to execute.
  • commandArgs: optional arguments for editor.execute(command, commandArgs).
  • execute(editor): custom execution logic if a regular CKEditor command is not enough.

An entry is shown only if its toolbarItem or one of its toolbarAny items exists in the active profile. When command is used, that CKEditor command must also be registered.

Installation

  1. Install addon (Installer or package deployment).
  2. Run REDAXO update/install routine.
  3. Open CKEditor 5 > Profiles and configure at least one profile.
  4. Use the profile in your textarea via data-profile.

Maintenance

For regular CKEditor vendor updates, use the addon-local workflow:

cd public/redaxo/src/addons/cke5
pnpm install
pnpm run vendor:update

This regenerates the vendor under assets/vendor/ckeditor5-modern and synchronizes the shipped copy under public/assets/addons/cke5/vendor/ckeditor5-modern.

Frontend Styling

Important: Do not edit the bundled CSS

The file /assets/addons/cke5/cke5_content_styles.css is automatically regenerated and overwritten on every pnpm run content-styles:update. Any direct changes will be lost.

The correct approach: Include the bundled CSS, then load your own project file afterwards that only overrides the desired variables.

Creating your own project CSS

<!-- In <head>: bundled CKEditor CSS first, then your own -->
<link rel="stylesheet" href="/assets/addons/cke5/cke5_content_styles.css">
<link rel="stylesheet" href="/assets/project/cke5-content.css">

Your own cke5-content.css only contains overrides:

/* assets/project/cke5-content.css */

/* Adapt colors to the project */
:root {
  --ck-content-blockquote-border: #your-color;
  --ck-content-pre-bg: #f5f5f5;
  --ck-content-pre-color: #222;
  --ck-content-pre-border: #ddd;
}

Available CSS variables

These variables can be overridden in your project:

Variable Affects
--ck-content-blockquote-border Blockquote border color
--ck-content-pre-color Code block text color
--ck-content-pre-bg Code block background
--ck-content-pre-border Code block border color
--ck-content-hr-bg Horizontal rule color
--ck-content-pagebreak-label-bg Page break label background
--ck-content-pagebreak-label-color Page break label text color
--ck-content-pagebreak-line Page break line color
--ck-content-color-image-caption-background Image caption background
--ck-content-color-image-caption-text Image caption text color
--ck-content-color-table-caption-background Table caption background
--ck-content-color-table-caption-text Table caption text color

Dark mode with a custom toggle

For projects with a custom dark mode toggle (e.g. [data-theme="dark"] on <html>):

/* assets/project/cke5-content.css */

[data-theme="dark"] {
  --ck-content-blockquote-border: #555;
  --ck-content-pre-color: #d4d4d4;
  --ck-content-pre-bg: rgba(255, 255, 255, 0.06);
  --ck-content-pre-border: #444;
  --ck-content-hr-bg: #444;
  --ck-content-pagebreak-label-bg: #2a2a2a;
  --ck-content-pagebreak-label-color: #ccc;
  --ck-content-pagebreak-line: #555;
  --ck-content-color-image-caption-background: #1e1e1e;
  --ck-content-color-image-caption-text: #ccc;
  --ck-content-color-table-caption-background: #1e1e1e;
  --ck-content-color-table-caption-text: #ccc;
}

prefers-color-scheme: dark is already covered by the bundled CSS. An additional block is only needed when the project toggle is controlled via a custom CSS class or attribute.


Basic Usage

Minimal textarea integration

<textarea
  class="form-control cke5-editor"
  data-profile="default"
  data-lang="<?php echo \Cke5\Utils\Cke5Lang::getUserLang(); ?>"
  data-content-lang="<?php echo \Cke5\Utils\Cke5Lang::getOutputLang(); ?>"
  name="REX_INPUT_VALUE[1]"
>REX_VALUE[1]</textarea>

With height limits

<textarea
  class="form-control cke5-editor"
  data-profile="default"
  data-min-height="220"
  data-max-height="700"
  data-lang="<?php echo \Cke5\Utils\Cke5Lang::getUserLang(); ?>"
  name="REX_INPUT_VALUE[2]"
>REX_VALUE[2]</textarea>

Frontend output

REX_VALUE[id="1" output="html"]

Integration Examples

MForm

$mform = new MForm();
$mform->addTextAreaField(1, [
    'label' => 'Text',
    'class' => 'cke5-editor',
    'data-profile' => 'default',
    'data-lang' => \Cke5\Utils\Cke5Lang::getUserLang(),
    'data-content-lang' => \Cke5\Utils\Cke5Lang::getOutputLang(),
]);

echo $mform->show();

MBlock

$id = 1;
$mform = new MForm();
$mform->addFieldset('Accordion');
$mform->addTextField("$id.0.title", ['label' => 'Title']);
$mform->addTextAreaField("$id.0.text", [
    'label' => 'Text',
    'class' => 'cke5-editor',
    'data-profile' => 'default',
    'data-lang' => \Cke5\Utils\Cke5Lang::getUserLang(),
    'data-content-lang' => \Cke5\Utils\Cke5Lang::getOutputLang(),
]);

echo MBlock::show($id, $mform->show());

YForm (custom attributes)

{"class":"cke5-editor","data-profile":"default","data-lang":"en","data-content-lang":"en"}

Profiles: Practical Notes

  • Toolbar uses CKEditor-style identifiers (link, insertImage, snippets, ...).
  • Legacy aliases are migrated/normalized internally where applicable.
  • Snippets are selected per profile.
  • Style groups and styles are selected per profile and merged for output config.
  • In profile edit mode, language placeholders can be configured per REDAXO locale.

JSON Configuration Cookbook (Profile Fields)

Several profile fields expect JSON input. This section gives working starter examples.

1) link_decorators_definition

This profile field defines manual link decorators. They appear as switches in the link dialog and add classes or attributes to the generated <a> element.

Format

Use the official CKEditor format: a JSON object whose keys are unique technical decorator names. Every entry contains at least mode: "manual", a label, and output configuration through classes, attributes, or styles.

{
  "btnPrimary": {
    "mode": "manual",
    "label": "Button Primary",
    "classes": ["btn", "btn-primary"],
    "attributes": {
      "role": "button"
    }
  },
  "newWindow": {
    "mode": "manual",
    "label": "Open in a new window",
    "attributes": {
      "target": "_blank",
      "rel": "noopener noreferrer"
    }
  }
}

The legacy format containing a list of individual objects is still accepted, but should not be used for new configurations.

Properties

Property Type Meaning
mode String Must be "manual" in this JSON field.
label String Label of the switch in the link dialog.
classes String or string array CSS classes applied to the link. An array such as ["btn", "btn-primary"] is recommended.
attributes Object HTML attributes such as role, rel, target, download, or data-*.
styles Object Inline styles as property/value pairs. Use only when classes are insufficient.
defaultValue Boolean When true, the decorator is enabled by default for new links.
redaxoExclusiveGroup String Optional CKE5 extension for mutually exclusive manual decorators.

Classes and attributes

For CSS classes, classes is the official and recommended CKEditor property:

"classes": ["btn", "btn-default"]

CKEditor can also process "attributes": {"class": "btn btn-default"} as a regular HTML attribute. New configurations should still use classes because CKEditor then explicitly handles the values as class tokens. "attributes": "classes" is invalid because attributes must always be a JSON object.

Compatible alternative using class inside attributes:

{
  "btnDefault": {
    "mode": "manual",
    "label": "Bootstrap 3 Button Default",
    "attributes": {
      "class": "btn btn-default",
      "role": "button"
    }
  }
}

Classes and additional attributes can be combined:

{
  "btnDefault": {
    "mode": "manual",
    "label": "Bootstrap 3 Button Default",
    "classes": ["btn", "btn-default"],
    "attributes": {
      "role": "button",
      "data-variant": "default"
    }
  }
}

Exclusive groups

CKEditor treats manual decorators independently by default. If multiple button variants must not be active at the same time, assign the same redaxoExclusiveGroup value to at least two decorators:

{
  "btnPrimary": {
    "mode": "manual",
    "label": "Button Primary",
    "classes": ["btn", "btn-primary"],
    "redaxoExclusiveGroup": "linkButtonStyle"
  },
  "btnSuccess": {
    "mode": "manual",
    "label": "Button Success",
    "classes": ["btn", "btn-success"],
    "redaxoExclusiveGroup": "linkButtonStyle"
  },
  "nofollow": {
    "mode": "manual",
    "label": "Add nofollow",
    "attributes": {
      "rel": "nofollow"
    }
  }
}

redaxoExclusiveGroup is not an official CKEditor option; it is an extension provided by this addon. When saving a link, CKE5 disables the other active decorators in the same group. A group therefore has an effect only when it contains at least two entries. Independent decorators such as nofollow can remain active at the same time.

Global decorators

Link decorators can be enabled centrally under CKEditor 5 > Profiles > Defaults > Global settings and stored as a JSON object for all profiles with a link toolbar. Profile-specific decorators are then merged by their technical key: a profile entry wins when the same key exists, while additional global and profile entries remain available together.

Automatic decorators

The official CKEditor API also supports mode: "automatic". However, it requires an actual JavaScript function in callback, which cannot be stored as JSON. This profile field is therefore intended for manual decorators. The profile provides the blank_to_external option for automatically opening external links in a new tab.

Official API reference: CKEditor LinkConfig#decorators and LinkDecoratorManualDefinition.

2) mentions_definition

Defines custom mention feeds.

[
  {
    "marker": "@",
    "minimumCharacters": 1,
    "feed": ["@support", "@sales", "@redaktion", "@admin"]
  },
  {
    "marker": "#",
    "minimumCharacters": 1,
    "feed": ["#news", "#release", "#event", "#faq"]
  }
]

3) sprog_mention_definition

Sprog replacements are JSON-based and are exposed via { mention marker.

[
  { "id": "{{company}}", "text": "Friends Of REDAXO" },
  { "id": "{{support_mail}}", "text": "support@example.org" },
  { "id": "{{hotline}}", "text": "+49 000 123456" }
]

4) image_resize_options_definition

Defines explicit image resize options used in image toolbar.

[
  { "name": "resizeImage:original", "label": "Original", "value": null },
  { "name": "resizeImage:25", "label": "25%", "value": "25" },
  { "name": "resizeImage:50", "label": "50%", "value": "50" },
  { "name": "resizeImage:75", "label": "75%", "value": "75" }
]

Note: the addon normalizes names internally for profile output.

5) transformation_extra

Adds additional typing transformations.

[
  { "from": "->", "to": "→" },
  { "from": "<-", "to": "←" },
  { "from": "(c)", "to": "©" },
  { "from": "(r)", "to": "®" }
]

6) html_support_allow

Allow additional elements/attributes/classes/styles.

[
  {
    "name": "regex(/^(section|article|div)$/)",
    "attributes": true,
    "classes": true,
    "styles": true
  },
  {
    "name": "a",
    "attributes": ["target", "rel", "data-bs-toggle", "data-bs-target"],
    "classes": ["btn", "btn-primary", "btn-outline-secondary"],
    "styles": false
  }
]

7) html_support_disallow

Disallow specific patterns even if allowed elsewhere.

[
  {
    "name": "script",
    "attributes": true,
    "classes": true,
    "styles": true
  },
  {
    "name": "*",
    "attributes": ["on.*"]
  }
]

8) extra_definition

Advanced raw merge into generated profile JSON. Use with care.

{
  "removePlugins": ["Autoformat"],
  "heading": {
    "options": [
      { "model": "paragraph", "title": "Paragraph", "class": "ck-heading_paragraph" },
      { "model": "heading2", "view": "h2", "title": "H2", "class": "ck-heading_heading2" }
    ]
  }
}

Tip: if you define removePlugins here, it is merged with existing remove list.

Validation Tips

  • Always use valid JSON (double quotes, no trailing commas).
  • Start with small JSON snippets and test in one profile first.
  • If a profile fails to behave as expected, open profile preview and inspect generated JSON.

Snippets Instead of Templates

Templates are no longer part of the active workflow. Use snippets for reusable editor content blocks.

Recommended workflow:

  1. Create snippets in Profiles > Customise > Snippets.
  2. Assign snippets to one or more profiles.
  3. Add snippets button to profile toolbar.

Export and Import

Export

Profiles > Export exports selected profiles including linked dependencies.

Export payload includes:

  • profiles
  • style_groups
  • styles
  • snippets

Import

Profiles > Import supports:

  • New bundle format (profiles + dependencies)
  • Legacy profile-only format

Import performs ID-based upsert for bundled tables and then profile import.

Console

The same bundles can be written and read without the backend, which is what a versioned profile set in a project repository or a deployment step needs:

# all profiles, indented
redaxo/bin/console cke5:export redaxo/data/addons/project/cke5_profiles.json --pretty

# a selection
redaxo/bin/console cke5:export bundle.json content hero

# import; an existing profile of the same name is kept unless it is named
redaxo/bin/console cke5:import bundle.json --overwrite=content --overwrite=hero
redaxo/bin/console cke5:import bundle.json --overwrite-all

cke5:export produces the same payload as the export page, and cke5:import regenerates cke5profiles.js afterwards — without that the editor keeps using the profile definitions it already has.

Config Page

CKEditor 5 > Config provides:

  • License key configuration
  • Upload/replace of editor runtime files (.js, .js.map)
  • Upload/replace translation files (.js)

Default runtime path is modern build under:

  • assets/addons/cke5/vendor/ckeditor5-modern/

API Example

Programmatically create an expert profile:

use Cke5\Creator\Cke5ProfilesApi;

$definition = json_encode([
    'toolbar' => [
        'items' => ['heading', '|', 'bold', 'italic', 'link', 'snippets', 'undo', 'redo'],
    ],
], JSON_UNESCAPED_UNICODE);

Cke5ProfilesApi::addProfile(
    'project_expert',
    'Project expert profile',
    $definition,
    null
);

Custom CSS Strategy

You can combine:

  • static custom CSS in addon/project assets
  • generated CSS from style/style-group definitions
  • optional external CSS paths configured per style/style group

The addon regenerates backend CSS artifacts when style/style-group data changes.

Plugin Development

For build sources, runtime plugin architecture, and external plugin integration, see:

  • PLUGIN_DEVELOPMENT.md

Troubleshooting

Editor does not initialize

  • Check that textarea has class cke5-editor.
  • Verify data-profile exists.
  • Verify cke5profiles.js is generated and loaded.

Image link button disabled/missing

  • Ensure profile image toolbar contains linkImage.
  • Ensure build contains LinkImage plugin.
  • Hard-reload backend after JS updates.

Translations not loaded

  • Verify configured translation path and files in Config page.
  • Ensure profile/user language maps to available CKEditor translation files.

License

MIT License

Credits

Project Lead

Joachim Dörr

Thomas Skerbis


Thanks to all Contributors who have shaped CKEditor 5 for REDAXO over the years!

Top Contributors

Contributor
1 crydotsnake
2 interweave-media
3 staabm
4 nandes2062
5 TobiasKrais
6 schuer
7 marcohanke
8 eaCe

A big thank you also to aeberhard, Bio-GitHub, dergel, V-Simos, VIEWSION, ynamite and ischfr.

Image credits

Demo content image: Frankfurt am Main Skyline by Leonhard_Niederwimmer on Pixabay — free to use under the Pixabay Content License.

Friends of REDAXO logo: friendsofredaxo.github.io — © Friends of REDAXO.

A project by Friends Of REDAXO

Support

About

Integrates the CKEditor5 into REDAXO CMS

Topics

Resources

Code of conduct

Stars

60 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages