From bb269f7423ed48735861b256409656be0d5195ef Mon Sep 17 00:00:00 2001
From: Kuba Niegowski
Date: Mon, 9 Mar 2026 18:32:39 +0100
Subject: [PATCH 01/22] Adding aggregated roots config.
---
.../ckeditor5-core/src/editor/editorconfig.ts | 239 ++++++++++++++++++
.../src/editor/utils/normalizerootsconfig.ts | 151 +++++++++++
packages/ckeditor5-core/src/index.ts | 2 +
.../src/ballooneditor.ts | 22 +-
.../src/ballooneditorui.ts | 8 +-
.../tests/ballooneditor.js | 61 ++++-
.../src/classiceditor.ts | 23 +-
.../src/classiceditorui.ts | 4 +-
.../tests/classiceditor.js | 61 ++++-
.../tests/classiceditorui.js | 48 +++-
.../src/decouplededitor.ts | 26 +-
.../src/decouplededitorui.ts | 8 +-
.../tests/decouplededitor.js | 61 ++++-
.../tests/decouplededitorui.js | 26 +-
.../src/inlineeditor.ts | 25 +-
.../src/inlineeditorui.ts | 8 +-
.../tests/inlineeditor.js | 61 ++++-
.../tests/inlineeditorui.js | 26 +-
.../src/multirooteditor.ts | 57 ++---
.../src/multirooteditorui.ts | 6 +-
.../tests/multirooteditor.js | 27 +-
21 files changed, 765 insertions(+), 185 deletions(-)
create mode 100644 packages/ckeditor5-core/src/editor/utils/normalizerootsconfig.ts
diff --git a/packages/ckeditor5-core/src/editor/editorconfig.ts b/packages/ckeditor5-core/src/editor/editorconfig.ts
index eaf15b14b2d..c708b6ae9c6 100644
--- a/packages/ckeditor5-core/src/editor/editorconfig.ts
+++ b/packages/ckeditor5-core/src/editor/editorconfig.ts
@@ -109,6 +109,12 @@ export interface EditorConfig extends EngineConfig {
* If `config.initialData` is not set when the editor is initialized, the data received in `Editor.create()` call
* will be used to set `config.initialData`. As a result, `initialData` is always set in the editor's config and
* plugins can read and/or modify it during initialization.
+ *
+ * **This property has been deprecated and will be removed in the future versions of CKEditor. Please use
+ * {@link module:core/editor/editorconfig~EditorConfig#root `root.initialData`} or
+ * {@link module:core/editor/editorconfig~EditorConfig#roots `roots..initialData`} instead.**
+ *
+ * @deprecated
*/
initialData?: string | Record;
@@ -537,6 +543,12 @@ export interface EditorConfig extends EngineConfig {
* element passed to the `create()` method.
*
* See the {@glink features/editor-placeholder "Editor placeholder"} guide for more information and live examples.
+ *
+ * **This property has been deprecated and will be removed in the future versions of CKEditor. Please use
+ * {@link module:core/editor/editorconfig~EditorConfig#root `root.placeholder`} or
+ * {@link module:core/editor/editorconfig~EditorConfig#roots `roots..placeholder`} instead.**
+ *
+ * @deprecated
*/
placeholder?: string | Record;
@@ -864,8 +876,229 @@ export interface EditorConfig extends EngineConfig {
* .then( ... )
* .catch( ... );
* ```
+ *
+ * **This property has been deprecated and will be removed in the future versions of CKEditor. Please use
+ * {@link module:core/editor/editorconfig~EditorConfig#root `root.label`} or
+ * {@link module:core/editor/editorconfig~EditorConfig#roots `roots..label`} instead.**
+ *
+ * @deprecated
*/
label?: string | Record;
+
+ /**
+ * The root configuration options for the default `main` root.
+ *
+ * This option is an alias for `config.roots.main`.
+ */
+ root?: RootConfig;
+
+ /**
+ * The root configuration options grouped by the root name.
+ *
+ * ```ts
+ * ClassicEditor
+ * .create( document.querySelector( '#editor' ), {
+ * roots: {
+ * main: {
+ * initialData: '
Hello world!
',
+ * placeholder: 'Type some text...',
+ * label: 'Main content'
+ * }
+ * }
+ * } );
+ * ```
+ */
+ roots?: Record;
+}
+
+/**
+ * TODO
+ */
+export interface RootConfig {
+
+ /**
+ * The initial editor data to be used instead of the provided element's HTML content.
+ *
+ * ```ts
+ * ClassicEditor
+ * .create( document.querySelector( '#editor' ), {
+ * root: {
+ * initialData: '
Initial data
Foo bar.
'
+ * }
+ * } )
+ * .then( ... )
+ * .catch( ... );
+ * ```
+ *
+ * By default, the editor is initialized with the content of the element on which this editor is initialized.
+ * This configuration option lets you override this behavior and pass different initial data.
+ * It is especially useful if it is difficult for your integration to put the data inside the HTML element.
+ *
+ * If your editor implementation uses multiple roots, you should provide config for roots individually:
+ *
+ * ```ts
+ * MultiRootEditor.create(
+ * // Roots for the editor:
+ * {
+ * header: document.querySelector( '#header' ),
+ * content: document.querySelector( '#content' ),
+ * leftSide: document.querySelector( '#left-side' ),
+ * rightSide: document.querySelector( '#right-side' )
+ * },
+ * // Config:
+ * {
+ * roots: {
+ * header: {
+ * initialData: '
Content for header part.
'
+ * },
+ * content: {
+ * initialData: '
Content for main part.
'
+ * },
+ * leftSide: {
+ * initialData: '
Content for left-side box.
'
+ * },
+ * rightSide: {
+ * initialData: '
Content for right-side box.
'
+ * }
+ * }
+ * }
+ * )
+ * .then( ... )
+ * .catch( ... );
+ * ```
+ * TODO link to root and roots config docs
+ *
+ * See also {@link module:core/editor/editor~Editor.create Editor.create()} documentation for the editor implementation which you use.
+ *
+ * **Note:** If initial data is passed to `Editor.create()` in the first parameter (instead of a DOM element), and,
+ * at the same time, `config.initialData` is set, an error will be thrown as those two options exclude themselves.
+ *
+ * If `config.root.initialData` is not set when the editor is initialized, the data received in `Editor.create()` call
+ * will be used to set `config.roots.main.initialData`. As a result, `initialData` is always set in the editor's config and
+ * plugins can read and/or modify it during initialization. TODO breaking change - the updated initialData changes location
+ */
+ initialData?: string;
+
+ /**
+ * Specifies the text displayed in the editor when there is no content (editor is empty). It is intended to
+ * help users locate the editor in the application (form) and prompt them to input the content. Work similarly
+ * as to the native DOM
+ * [`placeholder` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#The_placeholder_attribute)
+ * used by inputs.
+ *
+ * ```ts
+ * ClassicEditor
+ * .create( document.querySelector( '#editor' ), {
+ * root: {
+ * placeholder: 'Type some text...'
+ * }
+ * } )
+ * .then( ... )
+ * .catch( ... );
+ * ```
+ *
+ * If your editor implementation uses multiple roots, you should provide config for roots individually:
+ *
+ * ```ts
+ * MultiRootEditor.create(
+ * // Roots for the editor:
+ * {
+ * header: document.querySelector( '#header' ),
+ * content: document.querySelector( '#content' ),
+ * leftSide: document.querySelector( '#left-side' ),
+ * rightSide: document.querySelector( '#right-side' )
+ * },
+ * // Config:
+ * {
+ * roots: {
+ * header: {
+ * placeholder: 'Type header...'
+ * },
+ * content: {
+ * placeholder: 'Type content...'
+ * },
+ * leftSide: {
+ * placeholder: 'Type left-side...'
+ * },
+ * rightSide: {
+ * placeholder: 'Type right-side...'
+ * }
+ * }
+ * }
+ * )
+ * .then( ... )
+ * .catch( ... );
+ * ```
+ *
+ * The placeholder text is displayed as a pseudo–element of an empty paragraph in the editor content.
+ * The paragraph has the `.ck-placeholder` CSS class and the `data-placeholder` attribute.
+ *
+ * ```html
+ *
+ * ::before
+ *
+ * ```
+ *
+ * **Note**: Placeholder text can also be set using the `placeholder` attribute if a `
',
+ root: {
+ initialData: '
Hello from CKEditor 5 in SPFX React app!
'
+ },
table: {
contentToolbar: [
'tableColumn', 'tableRow', 'mergeTableCells',
@@ -244,9 +246,9 @@ const ForceTableBorderImportant = ( editor: Editor ) => {
*/
const enforceBorderStyles = ( domElement: HTMLElement, defaults: { width: string, style: string, color: string } ) => {
// Skip layout tables (used for positioning, not data)
- if (
- domElement.closest('figure')?.classList.contains( 'layout-table' ) ||
- domElement.classList.contains( 'layout-table' )
+ if (
+ domElement.closest('figure')?.classList.contains( 'layout-table' ) ||
+ domElement.classList.contains( 'layout-table' )
) {
return;
}
@@ -302,7 +304,7 @@ const ForceTableBorderImportant = ( editor: Editor ) => {
The internal `StylesMap` utility used by the editor's conversion system does not natively support the `!important` flag during style normalization.
-
+
The provided workaround, therefore, performs low-level DOM manipulation during the render cycle instead of relying solely on `StylesMap`. By applying these styles directly to the DOM elements after they are mapped, we ensure the priority flag remains intact and is correctly interpreted by the browser.
diff --git a/docs/updating/migration-to-cdn/react.md b/docs/updating/migration-to-cdn/react.md
index d7b7c407cf2..c5f97c50449 100644
--- a/docs/updating/migration-to-cdn/react.md
+++ b/docs/updating/migration-to-cdn/react.md
@@ -64,7 +64,9 @@ function App() {
mention: {
// Mention configuration
},
- initialData: '
'
+ * }
* }
* } )
* .then( editor => {
From d9a53ed62195dbf2ac9c3b0ce061a9f73b97a323 Mon Sep 17 00:00:00 2001
From: Mateusz Baginski
Date: Wed, 11 Mar 2026 16:01:01 +0100
Subject: [PATCH 08/22] Adjust paths.
---
packages/ckeditor5-editor-balloon/src/ballooneditor.ts | 2 +-
packages/ckeditor5-editor-decoupled/src/decouplededitor.ts | 2 +-
packages/ckeditor5-editor-inline/src/inlineeditor.ts | 2 +-
packages/ckeditor5-editor-multi-root/src/multirooteditor.ts | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/packages/ckeditor5-editor-balloon/src/ballooneditor.ts b/packages/ckeditor5-editor-balloon/src/ballooneditor.ts
index 091af3165c7..76649b895a4 100644
--- a/packages/ckeditor5-editor-balloon/src/ballooneditor.ts
+++ b/packages/ckeditor5-editor-balloon/src/ballooneditor.ts
@@ -148,7 +148,7 @@ export class BalloonEditor extends /* #__PURE__ */ ElementApiMixin( Editor ) {
* This lets you dynamically append the editor to your web page whenever it is convenient for you. You may use this method if your
* web page content is generated on the client side and the DOM structure is not ready at the moment when you initialize the editor.
*
- * # Using an existing DOM element (and data provided in `config.initialData`)
+ * # Using an existing DOM element (and data provided in `config.root.initialData`)
*
* You can also mix these two ways by providing a DOM element to be used and passing the initial data through the configuration:
*
diff --git a/packages/ckeditor5-editor-decoupled/src/decouplededitor.ts b/packages/ckeditor5-editor-decoupled/src/decouplededitor.ts
index 3694c0bb165..5714fb15303 100644
--- a/packages/ckeditor5-editor-decoupled/src/decouplededitor.ts
+++ b/packages/ckeditor5-editor-decoupled/src/decouplededitor.ts
@@ -176,7 +176,7 @@ export class DecoupledEditor extends /* #__PURE__ */ ElementApiMixin( Editor ) {
* This lets you dynamically append the editor to your web page whenever it is convenient for you. You may use this method if your
* web page content is generated on the client side and the DOM structure is not ready at the moment when you initialize the editor.
*
- * # Using an existing DOM element (and data provided in `config.initialData`)
+ * # Using an existing DOM element (and data provided in `config.root.initialData`)
*
* You can also mix these two ways by providing a DOM element to be used and passing the initial data through the configuration:
*
diff --git a/packages/ckeditor5-editor-inline/src/inlineeditor.ts b/packages/ckeditor5-editor-inline/src/inlineeditor.ts
index 89a432bdff8..13e3ae1abae 100644
--- a/packages/ckeditor5-editor-inline/src/inlineeditor.ts
+++ b/packages/ckeditor5-editor-inline/src/inlineeditor.ts
@@ -149,7 +149,7 @@ export class InlineEditor extends /* #__PURE__ */ ElementApiMixin( Editor ) {
* This lets you dynamically append the editor to your web page whenever it is convenient for you. You may use this method if your
* web page content is generated on the client side and the DOM structure is not ready at the moment when you initialize the editor.
*
- * # Using an existing DOM element (and data provided in `config.initialData`)
+ * # Using an existing DOM element (and data provided in `config.root.initialData`)
*
* You can also mix these two ways by providing a DOM element to be used and passing the initial data through the configuration:
*
diff --git a/packages/ckeditor5-editor-multi-root/src/multirooteditor.ts b/packages/ckeditor5-editor-multi-root/src/multirooteditor.ts
index 0fb1f42bb3d..6414cce4a56 100644
--- a/packages/ckeditor5-editor-multi-root/src/multirooteditor.ts
+++ b/packages/ckeditor5-editor-multi-root/src/multirooteditor.ts
@@ -837,7 +837,7 @@ export class MultiRootEditor extends Editor {
* This lets you dynamically append the editor to your web page whenever it is convenient for you. You may use this method if your
* web page content is generated on the client side and the DOM structure is not ready at the moment when you initialize the editor.
*
- * # Using an existing DOM element (and data provided in `config.initialData`)
+ * # Using an existing DOM element (and data provided in `config.root.initialData`)
*
* You can also mix these two ways by providing a DOM element to be used and passing the initial data through the configuration:
*
From eb4cb4d50afa0cd8ec2f4e5d348eb78784a0ccd0 Mon Sep 17 00:00:00 2001
From: Mateusz Baginski
Date: Wed, 11 Mar 2026 16:04:52 +0100
Subject: [PATCH 09/22] Fix path typo.
---
packages/ckeditor5-editor-multi-root/src/multirooteditor.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/ckeditor5-editor-multi-root/src/multirooteditor.ts b/packages/ckeditor5-editor-multi-root/src/multirooteditor.ts
index 6414cce4a56..90b839d788a 100644
--- a/packages/ckeditor5-editor-multi-root/src/multirooteditor.ts
+++ b/packages/ckeditor5-editor-multi-root/src/multirooteditor.ts
@@ -837,7 +837,7 @@ export class MultiRootEditor extends Editor {
* This lets you dynamically append the editor to your web page whenever it is convenient for you. You may use this method if your
* web page content is generated on the client side and the DOM structure is not ready at the moment when you initialize the editor.
*
- * # Using an existing DOM element (and data provided in `config.root.initialData`)
+ * # Using an existing DOM element (and data provided in `config.roots..initialData`)
*
* You can also mix these two ways by providing a DOM element to be used and passing the initial data through the configuration:
*
From 0f57ed972bf85978cad512813808c39cb1a1764b Mon Sep 17 00:00:00 2001
From: Mateusz Baginski
Date: Wed, 11 Mar 2026 16:10:29 +0100
Subject: [PATCH 10/22] Fix few api docs.
---
packages/ckeditor5-editor-classic/src/classiceditor.ts | 2 +-
packages/ckeditor5-heading/src/title.ts | 4 +++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/packages/ckeditor5-editor-classic/src/classiceditor.ts b/packages/ckeditor5-editor-classic/src/classiceditor.ts
index 8b66650d968..43e6735f6a8 100644
--- a/packages/ckeditor5-editor-classic/src/classiceditor.ts
+++ b/packages/ckeditor5-editor-classic/src/classiceditor.ts
@@ -141,7 +141,7 @@ export class ClassicEditor extends /* #__PURE__ */ ElementApiMixin( Editor ) {
* This lets you dynamically append the editor to your web page whenever it is convenient for you. You may use this method if your
* web page content is generated on the client side and the DOM structure is not ready at the moment when you initialize the editor.
*
- * # Replacing a DOM element (and data provided in `config.initialData`)
+ * # Replacing a DOM element (and data provided in `config.root.initialData`)
*
* You can also mix these two ways by providing a DOM element to be used and passing the initial data through the configuration:
*
diff --git a/packages/ckeditor5-heading/src/title.ts b/packages/ckeditor5-heading/src/title.ts
index 6c47bf0021d..2161a8b3282 100644
--- a/packages/ckeditor5-heading/src/title.ts
+++ b/packages/ckeditor5-heading/src/title.ts
@@ -607,7 +607,9 @@ function shouldRemoveLastParagraph( placeholder: ModelElement, root: ModelRootEl
* title: {
* placeholder: 'My custom placeholder for the title'
* },
- * placeholder: 'My custom placeholder for the body'
+ * root: {
+ * placeholder: 'My custom placeholder for the body'
+ * }
* } )
* .then( ... )
* .catch( ... );
From 8296538412315466f5e2f949d913dfa57321678a Mon Sep 17 00:00:00 2001
From: Mateusz Baginski
Date: Wed, 11 Mar 2026 16:36:12 +0100
Subject: [PATCH 11/22] Move snippets changes to different branch.
---
docs/getting-started/integrations/next-js.md | 4 +---
.../integrations/react-default-npm.md | 8 ++------
docs/getting-started/integrations/sharepoint.md | 12 +++++-------
.../setup/getting-and-setting-data.md | 6 ++----
docs/updating/migration-to-cdn/react.md | 8 ++------
packages/ckeditor5-autosave/tests/autosave.js | 8 ++------
6 files changed, 14 insertions(+), 32 deletions(-)
diff --git a/docs/getting-started/integrations/next-js.md b/docs/getting-started/integrations/next-js.md
index 8a7070e276e..4ab70400462 100644
--- a/docs/getting-started/integrations/next-js.md
+++ b/docs/getting-started/integrations/next-js.md
@@ -72,9 +72,7 @@ function CustomEditor() {
licenseKey: '',
plugins: [ Essentials, Paragraph, Bold, Italic, FormatPainter ],
toolbar: [ 'undo', 'redo', '|', 'bold', 'italic', '|', 'formatPainter' ],
- root: {
- initialData: '
'
} }
/>
);
diff --git a/docs/getting-started/integrations/sharepoint.md b/docs/getting-started/integrations/sharepoint.md
index 13227ddacc9..4a3b027ee72 100644
--- a/docs/getting-started/integrations/sharepoint.md
+++ b/docs/getting-started/integrations/sharepoint.md
@@ -142,9 +142,7 @@ export default class RichTextEditor extends React.ComponentHello from CKEditor 5 in SPFX React app!'
- },
+ initialData: '
Hello from CKEditor 5 in SPFX React app!
',
table: {
contentToolbar: [
'tableColumn', 'tableRow', 'mergeTableCells',
@@ -246,9 +244,9 @@ const ForceTableBorderImportant = ( editor: Editor ) => {
*/
const enforceBorderStyles = ( domElement: HTMLElement, defaults: { width: string, style: string, color: string } ) => {
// Skip layout tables (used for positioning, not data)
- if (
- domElement.closest('figure')?.classList.contains( 'layout-table' ) ||
- domElement.classList.contains( 'layout-table' )
+ if (
+ domElement.closest('figure')?.classList.contains( 'layout-table' ) ||
+ domElement.classList.contains( 'layout-table' )
) {
return;
}
@@ -304,7 +302,7 @@ const ForceTableBorderImportant = ( editor: Editor ) => {
The internal `StylesMap` utility used by the editor's conversion system does not natively support the `!important` flag during style normalization.
-
+
The provided workaround, therefore, performs low-level DOM manipulation during the render cycle instead of relying solely on `StylesMap`. By applying these styles directly to the DOM elements after they are mapped, we ensure the priority flag remains intact and is correctly interpreted by the browser.
diff --git a/docs/getting-started/setup/getting-and-setting-data.md b/docs/getting-started/setup/getting-and-setting-data.md
index 6ee43403801..51e5cb49da2 100644
--- a/docs/getting-started/setup/getting-and-setting-data.md
+++ b/docs/getting-started/setup/getting-and-setting-data.md
@@ -34,15 +34,13 @@ ClassicEditor
licenseKey: '', // Or 'GPL'.
plugins: [ /* ... */ ],
toolbar: [ /* ... */ ],
- root: {
- initialData: '
Hello, world!
'
- }
+ initialData: '
Hello, world!
'
} )
.then( /* ... */ )
.catch( /* ... */ );
```
-The {@link module:core/editor/editorconfig~EditorConfig.root.initialData `initialData`} property will initialize the editor with the provided data, overriding the content provided at the HTML level.
+The {@link module:core/editor/editorconfig~EditorConfig.initialData `initialData`} property will initialize the editor with the provided data, overriding the content provided at the HTML level.
If you are setting up the editor with integrations like {@link getting-started/integrations/react-default-npm React}, consult the documentation for additional properties provided to initialize the data.
diff --git a/docs/updating/migration-to-cdn/react.md b/docs/updating/migration-to-cdn/react.md
index c5f97c50449..d7b7c407cf2 100644
--- a/docs/updating/migration-to-cdn/react.md
+++ b/docs/updating/migration-to-cdn/react.md
@@ -64,9 +64,7 @@ function App() {
mention: {
// Mention configuration
},
- root: {
- initialData: '
'
} )
.then( _editor => {
editor = _editor;
From 631c6f45cc1bb1e068f0e48ed6caf452c4881a48 Mon Sep 17 00:00:00 2001
From: Kuba Niegowski
Date: Wed, 11 Mar 2026 22:15:29 +0100
Subject: [PATCH 12/22] Fixed issues with watchdog and multi root editor.
---
.../src/multirooteditor.ts | 22 +--
.../ckeditor5-watchdog/src/editorwatchdog.ts | 125 +++++++++++-------
.../tests/editorwatchdog.js | 43 ++++--
.../tests/manual/watchdog-data.html | 25 ++++
.../tests/manual/watchdog-data.js | 108 +++++++++++++++
.../tests/manual/watchdog-data.md | 11 ++
.../tests/manual/watchdog-multi-root-data.js | 5 +-
.../manual/watchdog-multi-root-elements.js | 5 +-
8 files changed, 274 insertions(+), 70 deletions(-)
create mode 100644 packages/ckeditor5-watchdog/tests/manual/watchdog-data.html
create mode 100644 packages/ckeditor5-watchdog/tests/manual/watchdog-data.js
create mode 100644 packages/ckeditor5-watchdog/tests/manual/watchdog-data.md
diff --git a/packages/ckeditor5-editor-multi-root/src/multirooteditor.ts b/packages/ckeditor5-editor-multi-root/src/multirooteditor.ts
index 90b839d788a..2d51917c502 100644
--- a/packages/ckeditor5-editor-multi-root/src/multirooteditor.ts
+++ b/packages/ckeditor5-editor-multi-root/src/multirooteditor.ts
@@ -149,16 +149,13 @@ export class MultiRootEditor extends Editor {
throw new CKEditorError( 'multi-root-editor-root-deprecated-config-roots-attributes', null );
}
- const rootNames = Object.keys( this.config.get( 'roots' )! );
-
- for ( const rootName of rootNames ) {
- const rootConfig = this.config.get( 'roots' )![ rootName ];
+ const rootsConfig = Object.entries( this.config.get( 'roots' )! );
+ for ( const [ rootName, rootConfig ] of rootsConfig ) {
// Create root and `UIView` element for each editable container.
const root = this.model.document.createRoot( '$root', rootName );
if ( rootConfig.lazyLoad ) {
- // TODO do not load initialData for lazy roots.
root._isLoaded = false;
}
@@ -172,6 +169,7 @@ export class MultiRootEditor extends Editor {
* @error multi-root-editor-root-attributes-no-root
*/
+ // TODO is this correct for lazy root?
const attributes = rootConfig.modelElement?.attributes;
if ( attributes ) {
@@ -183,8 +181,8 @@ export class MultiRootEditor extends Editor {
this.data.on( 'init', () => {
this.model.enqueueChange( { isUndoable: false }, writer => {
- for ( const rootName of rootNames ) {
- const rootConfig = this.config.get( 'roots' )![ rootName ];
+ for ( const [ rootName, rootConfig ] of rootsConfig ) {
+ // TODO check if should set on non loaded root.
const attributes = rootConfig.modelElement?.attributes || {};
const root = this.model.document.getRoot( rootName )!;
@@ -203,7 +201,12 @@ export class MultiRootEditor extends Editor {
label: extractRootsConfigField( this.config.get( 'roots' )!, 'label' )
};
- const view = new MultiRootEditorUIView( this.locale, this.editing.view, rootNames, options );
+ // TODO make it nicer
+ const rootsNames = rootsConfig
+ .filter( ( [ , { lazyLoad } ] ) => !lazyLoad )
+ .map( ( [ rootName ] ) => rootName );
+
+ const view = new MultiRootEditorUIView( this.locale, this.editing.view, rootsNames, options );
this.ui = new MultiRootEditorUI( this, view );
@@ -515,7 +518,7 @@ export class MultiRootEditor extends Editor {
* @param label The accessible label text describing the editable to the assistive technologies.
* @returns The created DOM element. Append it in a desired place in your application.
*/
- public createEditable( root: ModelRootElement, placeholder?: string, label?: string ): HTMLElement {
+ public createEditable( root: ModelRootElement, placeholder?: string, label?: string ): HTMLElement { // TODO
const editable = this.ui.view.createEditable( root.rootName, undefined, label );
this.ui.addEditable( editable, placeholder );
@@ -983,6 +986,7 @@ function extractRootsConfigField(
): Record> {
return Object.fromEntries(
Object.entries( rootsConfig )
+ .filter( ( [ , config ] ) => !config.lazyLoad )
.map( ( [ rootName, config ] ) => [ rootName, config[ key ] ] )
.filter( ( [ , value ] ) => value !== undefined )
);
diff --git a/packages/ckeditor5-watchdog/src/editorwatchdog.ts b/packages/ckeditor5-watchdog/src/editorwatchdog.ts
index 480778aadf8..9a39c2cd8a3 100644
--- a/packages/ckeditor5-watchdog/src/editorwatchdog.ts
+++ b/packages/ckeditor5-watchdog/src/editorwatchdog.ts
@@ -7,13 +7,19 @@
* @module watchdog/editorwatchdog
*/
-import { throttle, cloneDeepWith, isElement, type DebouncedFunc } from 'es-toolkit/compat';
+import { throttle, cloneDeepWith, isElement as _isElement, type DebouncedFunc } from 'es-toolkit/compat';
import { areConnectedThroughProperties } from './utils/areconnectedthroughproperties.js';
import { Watchdog, type WatchdogConfig } from './watchdog.js';
-import type { CKEditorError } from '@ckeditor/ckeditor5-utils';
+import { type CKEditorError, Config } from '@ckeditor/ckeditor5-utils';
import type { ModelNode, ModelText, ModelElement, ModelWriter } from '@ckeditor/ckeditor5-engine';
-import type { Editor, EditorConfig, Context, EditorReadyEvent } from '@ckeditor/ckeditor5-core';
-import type { RootAttributes } from '@ckeditor/ckeditor5-editor-multi-root';
+import {
+ type Editor,
+ type EditorConfig,
+ type Context,
+ type EditorReadyEvent,
+ type RootConfig,
+ normalizeRootsConfig
+} from '@ckeditor/ckeditor5-core';
/**
* A watchdog for CKEditor 5 editors.
@@ -56,10 +62,21 @@ export class EditorWatchdog extends Watchdog {
*/
private _elementOrData?: HTMLElement | string | Record | Record;
+ /**
+ * TODO
+ */
+ private _editorAttachTo: HTMLElement | null = null;
+
+ /**
+ * TODO
+ */
+ private _isSingleRootEditor: boolean = true;
+
/**
* Specifies whether the editor was initialized using document data (`true`) or HTML elements (`false`).
*/
- private _initUsingData = true;
+ // TODO clean it
+ // private _initUsingData = true;
/**
* The latest record of the editor editable elements. Used to restart the editor.
@@ -179,55 +196,58 @@ export class EditorWatchdog extends Watchdog {
// We are not interested in any data set in config or in `.create()` first parameter. It will be replaced anyway.
// But we need to set them correctly to make sure that proper roots are created.
//
- // Since a different set of roots will be created, `lazyRoots` and `rootsAttributes` properties must be managed too.
-
- // Keys are root names, values are ''. Used when the editor was initialized by setting the first parameter to document data.
- const existingRoots: Record = {};
- // Keeps lazy roots. They may be different when compared to initial config if some of the roots were loaded.
- const lazyRoots: Array = [];
- // Roots attributes from the old config. Will be referred when setting new attributes.
- const oldRootsAttributes: Record = this._config!.rootsAttributes || {};
- // New attributes to be set. Is filled only for roots that still exist in the document.
- const rootsAttributes: Record = {};
-
- // Traverse through the roots saved when the editor crashed and set up the discussed values.
- for ( const [ rootName, rootData ] of Object.entries( this._data!.roots ) ) {
- if ( rootData.isLoaded ) {
- existingRoots[ rootName ] = '';
- rootsAttributes[ rootName ] = oldRootsAttributes[ rootName ] || {};
- } else {
- lazyRoots.push( rootName );
- }
- }
+ // Since a different set of roots will be created, lazy-roots and roots-attributes must be managed too.
+
+ const elementOrData = this._isSingleRootEditor ?
+ this._editorAttachTo || '' :
+ this._editables;
+
+ // Normalize the roots configuration based on the editor source element or data and the editor configuration.
+ const config = new Config( this._config! );
+
+ normalizeRootsConfig( elementOrData, config, this._isSingleRootEditor ? 'main' : false );
const updatedConfig: EditorConfig = {
...this._config,
+ roots: config.get( 'roots' ),
extraPlugins: this._config!.extraPlugins || [],
- lazyRoots,
- rootsAttributes,
_watchdogInitialData: this._data
};
- // Delete `initialData` as it is not needed. Data will be set by the watchdog based on `_watchdogInitialData`.
- // First parameter of the editor `.create()` will be used to set up initial roots.
- delete updatedConfig.initialData;
+ // Add content loading plugin to the editor configuration.
+ // This plugin will be responsible for loading the editor data into the editor after it is restarted.
+ updatedConfig.extraPlugins!.push( EditorWatchdogInitPlugin );
- updatedConfig.extraPlugins!.push( EditorWatchdogInitPlugin as any );
-
- if ( this._initUsingData ) {
- return this.create( existingRoots, updatedConfig, updatedConfig.context );
- } else {
- // Set correct editables to make sure that proper roots are created and linked with DOM elements.
- // No need to set initial data, as it would be discarded anyway.
- //
- // If one element was initially set in `elementOrData`, then use that original element to restart the editor.
- // This is for compatibility purposes with single-root editor types.
- if ( isElement( this._elementOrData ) ) {
- return this.create( this._elementOrData, updatedConfig, updatedConfig.context );
+ // Collect existing roots configuration and update it. This will ensure that the same set of roots
+ // will be created after the restart, and they will have correct lazy loading and attributes configuration.
+ const updatedRootsConfig: Record = {};
+
+ for ( const [ rootName, rootData ] of Object.entries( this._data!.roots ) ) {
+ const rootConfig = updatedConfig.roots![ rootName ] || Object.create( null );
+
+ // TODO update comment: Delete `initialData` as it is not needed. Data will be set by the watchdog
+ // based on `_watchdogInitialData`.
+ rootConfig.initialData = '';
+
+ if ( rootData.isLoaded ) {
+ rootConfig.lazyLoad = false;
} else {
- return this.create( this._editables, updatedConfig, updatedConfig.context );
+ delete rootConfig.modelElement?.attributes;
}
+
+ updatedRootsConfig[ rootName ] = rootConfig;
}
+
+ updatedConfig.roots = updatedRootsConfig;
+
+ // Delete `initialData` as it is not needed. Data will be set by the watchdog based on `_watchdogInitialData`.
+ // First parameter of the editor `.create()` will be used to set up initial roots.
+ delete updatedConfig.initialData;
+
+ // Also alias for main root should not provide initial data.
+ delete updatedConfig.root?.initialData;
+
+ return this.create( elementOrData, updatedConfig, updatedConfig.context );
} )
.then( () => {
this._fire( 'restart' );
@@ -252,10 +272,10 @@ export class EditorWatchdog extends Watchdog {
this._elementOrData = elementOrData;
- // Use document data in the first parameter of the editor `.create()` call only if it was used like this originally.
- // Use document data if a string or object with strings was passed.
- this._initUsingData = typeof elementOrData == 'string' ||
- ( Object.keys( elementOrData ).length > 0 && typeof Object.values( elementOrData )[ 0 ] == 'string' );
+ // Store the original DOM element for single-root editors. We can't use editable elements as ClassicEditor
+ // expects the attachment element.
+ this._editorAttachTo = isElement( elementOrData ) ? elementOrData : null;
+ this._isSingleRootEditor = isElement( elementOrData ) || typeof elementOrData == 'string';
// Clone configuration because it might be shared within multiple watchdog instances. Otherwise,
// when an error occurs in one of these editors, the watchdog will restart all of them.
@@ -273,7 +293,7 @@ export class EditorWatchdog extends Watchdog {
this._lastDocumentVersion = editor.model.document.version;
this._data = this._getData();
- if ( !this._initUsingData ) {
+ if ( !this._editorAttachTo ) {
this._editables = this._getEditables();
}
@@ -337,7 +357,7 @@ export class EditorWatchdog extends Watchdog {
try {
this._data = this._getData();
- if ( !this._initUsingData ) {
+ if ( !this._editorAttachTo ) {
this._editables = this._getEditables();
}
@@ -628,3 +648,10 @@ export type EditorWatchdogCreatorFunction = (
elementOrData: HTMLElement | string | Record | Record,
config: EditorConfig
) => Promise;
+
+/**
+ * An alias for `isElement` from `es-toolkit/compat` with additional type guard.
+ */
+function isElement( value: any ): value is Element {
+ return _isElement( value );
+}
diff --git a/packages/ckeditor5-watchdog/tests/editorwatchdog.js b/packages/ckeditor5-watchdog/tests/editorwatchdog.js
index 31a335963b1..6c7c13c0fac 100644
--- a/packages/ckeditor5-watchdog/tests/editorwatchdog.js
+++ b/packages/ckeditor5-watchdog/tests/editorwatchdog.js
@@ -1602,15 +1602,24 @@ describe( 'EditorWatchdog', () => {
content: '
',
- modelElement: {
- attributes: { order: 2 }
- }
+ modelAttributes: { order: 2 }
}
},
placeholder: 'Type in some content'
From f1984397a422903c207995fc42e946d9155d8141 Mon Sep 17 00:00:00 2001
From: Kuba Niegowski
Date: Sat, 14 Mar 2026 13:36:18 +0100
Subject: [PATCH 21/22] Editable options stored as root attributes to propagate
in RTC and RH.
---
.../src/multirooteditor.ts | 142 +++++++++++++++---
.../tests/multirooteditor.js | 2 +-
2 files changed, 123 insertions(+), 21 deletions(-)
diff --git a/packages/ckeditor5-editor-multi-root/src/multirooteditor.ts b/packages/ckeditor5-editor-multi-root/src/multirooteditor.ts
index ca93c7ad345..dfe10ae537b 100644
--- a/packages/ckeditor5-editor-multi-root/src/multirooteditor.ts
+++ b/packages/ckeditor5-editor-multi-root/src/multirooteditor.ts
@@ -169,7 +169,6 @@ export class MultiRootEditor extends Editor {
* @error multi-root-editor-root-attributes-no-root
*/
- // TODO is this correct for lazy root?
const attributes = rootConfig.modelAttributes;
if ( attributes ) {
@@ -179,18 +178,31 @@ export class MultiRootEditor extends Editor {
}
}
+ // Registering `$rootEditableOptions` attribute to make it available in the editor model.
+ // This allows to store editable options for each root in the model, and make them available on other RTC clients.
+ // We do not use `registerRootAttribute()` method here, as this attribute is used internally
+ // and should not be returned by `getRootsAttributes()` method.
+ this.editing.model.schema.extend( '$root', { allowAttributes: '$rootEditableOptions' } );
+
this.data.on( 'init', () => {
this.model.enqueueChange( { isUndoable: false }, writer => {
for ( const [ rootName, rootConfig ] of rootsConfig ) {
- // TODO check if should set on non loaded root.
- const attributes = rootConfig.modelAttributes || {};
const root = this.model.document.getRoot( rootName )!;
- for ( const [ key, value ] of Object.entries( attributes ) ) {
+ for ( const [ key, value ] of Object.entries( rootConfig.modelAttributes || {} ) ) {
if ( value !== null ) {
- writer.setAttribute( key, value, root! );
+ writer.setAttribute( key, value, root );
}
}
+
+ // Set editable config for consistency with `addRoot()` method. This will allow features
+ // to use the same configuration for both initially loaded and dynamically added roots.
+ const rootEditableOptions: RootEditableOptions = {
+ ...rootConfig.placeholder && { placeholder: rootConfig.placeholder },
+ ...rootConfig.label && { label: rootConfig.label }
+ };
+
+ writer.setAttribute( '$rootEditableOptions', rootEditableOptions, root );
}
} );
} );
@@ -351,8 +363,10 @@ export class MultiRootEditor extends Editor {
/**
* Adds a new root to the editor.
*
+ * TODO check if this description requires an update.
+ *
* ```ts
- * editor.addRoot( 'myRoot', { data: '
' } );
* ```
*
* After a root is added, you will be able to modify and retrieve its data.
@@ -393,7 +407,7 @@ export class MultiRootEditor extends Editor {
* ```ts
* // Add a collapsed root at fourth position from top.
* // Keep in mind that these are just examples of attributes. You need to provide your own features that will handle the attributes.
- * editor.addRoot( 'myRoot', { attributes: { isCollapsed: true, index: 4 } } );
+ * editor.addRoot( 'myRoot', { modelAttributes: { isCollapsed: true, index: 4 } } );
* ```
*
* Note that attributes added together with a root are automatically registered.
@@ -421,24 +435,52 @@ export class MultiRootEditor extends Editor {
* @param rootName Name of the root to add.
* @param options Additional options for the added root.
*/
- public addRoot(
- rootName: string,
- { data = '', attributes = {}, elementName = '$root', isUndoable = false }: AddRootOptions = {}
- ): void {
+ public addRoot( rootName: string, options?: RootConfig & AddRootUndoable ): void;
+
+ /**
+ * Adds a new root to the editor.
+ *
+ * ```ts
+ * editor.addRoot( 'myRoot', { data: '
Initial root data.
' } );
+ * ```
+ *
+ * TODO link to other signature for more details
+ *
+ * @deprecated
+ *
+ * @param rootName Name of the root to add.
+ * @param options Additional options for the added root.
+ */
+ // eslint-disable-next-line @typescript-eslint/unified-signatures
+ public addRoot( rootName: string, options?: AddRootOptions ): void;
+
+ public addRoot( rootName: string, options: AddRootOptions & RootConfig & AddRootUndoable = {} ): void {
+ const initialData: string = options.initialData || options.data || '';
+ const modelAttributes: RootAttributes = options.modelAttributes || options.attributes || {};
+ const modelElement: string = options.elementName || '$root';
+
const _addRoot = ( writer: ModelWriter ) => {
- const root = writer.addRoot( rootName, elementName );
+ const root = writer.addRoot( rootName, modelElement );
- if ( data ) {
- writer.insert( this.data.parse( data, root ), root, 0 );
+ if ( initialData ) {
+ writer.insert( this.data.parse( initialData, root ), root, 0 );
}
- for ( const key of Object.keys( attributes ) ) {
+ for ( const key of Object.keys( modelAttributes ) ) {
this.registerRootAttribute( key );
- writer.setAttribute( key, attributes[ key ], root );
+ writer.setAttribute( key, modelAttributes[ key ], root );
}
+
+ // Storing editable options as a root attribute to make them available on other RTC clients.
+ const rootEditableOptions: RootEditableOptions = {
+ ...options.placeholder && { placeholder: options.placeholder },
+ ...options.label && { label: options.label }
+ };
+
+ writer.setAttribute( '$rootEditableOptions', rootEditableOptions, root );
};
- if ( isUndoable ) {
+ if ( options.isUndoable ) {
this.model.change( _addRoot );
} else {
this.model.enqueueChange( { isUndoable: false }, _addRoot );
@@ -513,15 +555,45 @@ export class MultiRootEditor extends Editor {
* The new DOM editable is attached to the model root and can be used to modify the root content.
*
* @param root Root for which the editable element should be created.
+ * @param options.placeholder Placeholder for the editable element. If not set, placeholder value from the
+ * {@link module:core/editor/editorconfig~EditorConfig#placeholder editor configuration} will be used (if it was provided).
+ * @param options.label The accessible label text describing the editable to the assistive technologies.
+ * @returns The created DOM element. Append it in a desired place in your application.
+ */
+ public createEditable( root: ModelRootElement, options?: RootEditableOptions ): HTMLElement;
+
+ /**
+ * Creates and returns a new DOM editable element for the given root element.
+ *
+ * The new DOM editable is attached to the model root and can be used to modify the root content.
+ *
+ * **Note**: this method signature is deprecated and will be removed in one of the next releases.
+ * Use the signature with options object instead.
+ *
+ * @deprecated
+ *
+ * @param root Root for which the editable element should be created.
* @param placeholder Placeholder for the editable element. If not set, placeholder value from the
* {@link module:core/editor/editorconfig~EditorConfig#placeholder editor configuration} will be used (if it was provided).
* @param label The accessible label text describing the editable to the assistive technologies.
* @returns The created DOM element. Append it in a desired place in your application.
*/
- public createEditable( root: ModelRootElement, placeholder?: string, label?: string ): HTMLElement { // TODO
- const editable = this.ui.view.createEditable( root.rootName, undefined, label );
+ public createEditable( root: ModelRootElement, placeholder?: string, label?: string ): HTMLElement;
- this.ui.addEditable( editable, placeholder );
+ public createEditable( root: ModelRootElement, optionsOrPlaceholder?: RootEditableOptions | string, label?: string ): HTMLElement {
+ let placeholder: string | undefined;
+
+ if ( !optionsOrPlaceholder || typeof optionsOrPlaceholder === 'string' ) {
+ placeholder = optionsOrPlaceholder;
+ } else {
+ placeholder = optionsOrPlaceholder?.placeholder;
+ label = optionsOrPlaceholder?.label;
+ }
+
+ const rootEditableConfig: RootEditableOptions = root.getAttribute( '$rootEditableOptions' ) || {};
+ const editable = this.ui.view.createEditable( root.rootName, undefined, label || rootEditableConfig.label );
+
+ this.ui.addEditable( editable, placeholder || rootEditableConfig.placeholder );
this.editing.view.forceRender();
@@ -1043,6 +1115,8 @@ export type LoadRootEvent = DecoratedMethodEvent;
/**
* Additional options available when adding a root.
+ *
+ * @deprecated
*/
export type AddRootOptions = {
@@ -1067,6 +1141,17 @@ export type AddRootOptions = {
isUndoable?: boolean;
};
+/**
+ * Declares an additional options available when adding a root.
+ */
+export type AddRootUndoable = {
+
+ /**
+ * Whether creating the root can be undone (using the undo feature) or not.
+ */
+ isUndoable?: boolean;
+};
+
/**
* Additional options available when loading a root.
*/
@@ -1076,3 +1161,20 @@ export type LoadRootOptions = Omit
* Attributes set on a model root element.
*/
export type RootAttributes = Record;
+
+/**
+ * Additional options for the created editable element.
+ */
+export interface RootEditableOptions {
+
+ /**
+ * Placeholder for the editable element. If not set, placeholder value from the
+ * {@link module:core/editor/editorconfig~EditorConfig#placeholder editor configuration} will be used (if it was provided). TODO link
+ */
+ placeholder?: string;
+
+ /**
+ * The accessible label text describing the editable to the assistive technologies.
+ */
+ label?: string;
+}
diff --git a/packages/ckeditor5-editor-multi-root/tests/multirooteditor.js b/packages/ckeditor5-editor-multi-root/tests/multirooteditor.js
index e6e547a6bc7..1cd012f0899 100644
--- a/packages/ckeditor5-editor-multi-root/tests/multirooteditor.js
+++ b/packages/ckeditor5-editor-multi-root/tests/multirooteditor.js
@@ -1268,7 +1268,7 @@ describe( 'MultiRootEditor', () => {
expect( editableElement.children[ 0 ].dataset.placeholder ).to.equal( 'new' );
} );
- it( 'should alow for setting a custom label to the editable', () => {
+ it( 'should allow for setting a custom label to the editable', () => {
editor.addRoot( 'new' );
editor.createEditable( editor.model.document.getRoot( 'new' ), undefined, 'Custom label' );
From cf2fb2982d909515160516fd8d5ba0aaeb8074c5 Mon Sep 17 00:00:00 2001
From: Kuba Niegowski
Date: Sat, 14 Mar 2026 14:07:43 +0100
Subject: [PATCH 22/22] Added tests.
---
.../tests/multirooteditor.js | 149 ++++++++++++++++++
1 file changed, 149 insertions(+)
diff --git a/packages/ckeditor5-editor-multi-root/tests/multirooteditor.js b/packages/ckeditor5-editor-multi-root/tests/multirooteditor.js
index 1cd012f0899..4efa103cbb0 100644
--- a/packages/ckeditor5-editor-multi-root/tests/multirooteditor.js
+++ b/packages/ckeditor5-editor-multi-root/tests/multirooteditor.js
@@ -830,6 +830,67 @@ describe( 'MultiRootEditor', () => {
expect( root ).not.to.be.null;
expect( root.isAttached() ).to.be.false;
} );
+
+ it( 'should init the root with given initialData', () => {
+ editor.addRoot( 'bar', { initialData: '