Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2d467d8
Added api docs comments.
niegowski Apr 23, 2026
e04d95e
Updated html comments schema check.
niegowski Apr 23, 2026
e67f215
Updated GHS schema for hgroup element so it does not hardcode $root.
niegowski Apr 23, 2026
8100551
The Title feature should not hardcode $root.
niegowski Apr 23, 2026
a62fcb1
Reworked title feature for inline roots.
niegowski Apr 23, 2026
65d82d2
Title feature updated for 100% cc.
niegowski Apr 23, 2026
db8d099
Warn when Title is loaded but no root supports the title structure.
niegowski Apr 29, 2026
893f898
Added changelog entries.
niegowski Apr 29, 2026
52890d1
Added tests for title feature warning.
niegowski Apr 29, 2026
ac486fe
Removed not-needed checks.
niegowski Apr 29, 2026
9961f24
Added console.warn stub in tests for inline root integration.
niegowski Apr 29, 2026
ada4277
Merge branch 'refs/heads/master' into ck/20026
niegowski May 5, 2026
cff3e68
Added new linter rules for $root calls.
niegowski May 5, 2026
7da15d9
Merge branch 'master' into ck/20026
niegowski May 5, 2026
f443899
Multi root editor should use view-related root options in the root atโ€ฆ
niegowski May 6, 2026
4232296
Unified normalization of RootEditableOptions to model root attribute.
niegowski May 6, 2026
65e633a
Merge branch 'master' into ck/20026
niegowski May 6, 2026
d18cc59
Fixed watchdog-data manual test.
niegowski May 7, 2026
e37786e
Updated watchdog tests.
niegowski May 7, 2026
e142b09
Merge branch 'master' into ck/20026
niegowski May 8, 2026
5d52c02
Merge branch 'master' into ck/20026
niegowski May 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changelog/20260429140000_ck_20026_title_custom_roots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
type: Fix
scope:
- ckeditor5-heading
closes:
- https://github.com/ckeditor/ckeditor5/issues/20026
---

The Title feature now correctly handles editor configurations where some or all roots use a custom `modelElement`. Roots that do not accept the `title` element are silently skipped at runtime, and the feature logs a single warning when no root supports the title structure.
9 changes: 9 additions & 0 deletions .changelog/20260429140100_ck_20026_ghs_hgroup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
type: Fix
scope:
- ckeditor5-html-support
closes:
- https://github.com/ckeditor/ckeditor5/issues/20026
---

The General HTML Support schema for the `hgroup` element no longer hardcodes `$root` as its allowed parent, so the element is correctly registered in editor configurations using a custom root `modelElement`.
9 changes: 9 additions & 0 deletions .changelog/20260429140200_ck_20026_html_comments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
type: Fix
scope:
- ckeditor5-html-support
closes:
- https://github.com/ckeditor/ckeditor5/issues/20026
---

The HTML comments feature no longer assumes the root model element is `$root`. Comments are now preserved in editor configurations using a custom root `modelElement`.
5 changes: 5 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ export default defineConfig( [
'ckeditor5-rules/validate-module-tag': 'error',
'ckeditor5-rules/no-default-export': 'error',
'ckeditor5-rules/allow-svg-imports-only-in-icons-package': 'error',
'ckeditor5-rules/no-literal-dollar-root': [ 'error', {
allowedPackages: [ 'ckeditor5-engine', 'ckeditor5-core' ],
allowedCalls: [ 'is' ]
} ],
'ckeditor5-rules/require-explicit-data-context': 'error',
'ckeditor5-rules/ckeditor-plugin-flags': [ 'error', {
requiredFlags: [ {
name: 'isOfficialPlugin',
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@
"date-fns": "^4.0.0",
"esbuild": "^0.25.0",
"eslint": "^9.34.0",
"eslint-config-ckeditor5": "^15.0.0",
"eslint-config-ckeditor5": "^15.1.0",
"eslint-formatter-stylish": "^8.40.0",
"eslint-plugin-ckeditor5-rules": "^15.0.0",
"eslint-plugin-ckeditor5-rules": "^15.1.0",
"estree-walker": "^3.0.3",
"fs-extra": "^11.0.0",
"glob": "^13.0.0",
Expand Down
8 changes: 7 additions & 1 deletion packages/ckeditor5-core/src/editor/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -953,12 +953,18 @@ export abstract class Editor extends /* #__PURE__ */ ObservableMixin() {
* Registers a given string as a root attribute key. Registered root attributes are added to
* the {@link module:engine/model/schema~ModelSchema schema}.
*
* Note: Attributes passed in the configuration for multi-root editors
* **Note:** Attributes passed in the configuration for multi-root editors
* ({@link module:core/editor/editorconfig~EditorConfig#roots `config.roots.<rootName>.modelAttributes`}) or
* single-root editors ({@link module:core/editor/editorconfig~EditorConfig#root `config.root.modelAttributes`})
* are automatically registered when the editor is initialized. However, registering the same attribute twice
* does not have any negative impact, so it is recommended to use this method in any feature that uses
* root attributes.
*
* **Note:** Registered attributes are attached only to the generic `$root` schema element. A custom root
* {@link module:core/editor/editorconfig~RootConfig#modelElement `modelElement`} must opt into the `$root`
* attribute chain via `allowAttributesOf: '$root'` to inherit these attributes.
* See the {@glink framework/deep-dive/schema#custom-root-elements Custom root elements} section of the
* {@glink framework/deep-dive/schema Schema deep-dive} guide for more details.
*/
public registerRootAttribute( key: string ): void {
if ( this._registeredRootsAttributesKeys.has( key ) ) {
Expand Down
90 changes: 58 additions & 32 deletions packages/ckeditor5-editor-multi-root/src/multirooteditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export class MultiRootEditor extends Editor {

normalizeRootsConfig( sourceElementsOrData, this.config, false );
normalizeRootsAttributesConfig( this.config );
normalizeRootEditableOptionsConfig( this.config );

if ( this.config.get( 'lazyRoots' ) ) {
/**
Expand Down Expand Up @@ -175,30 +176,12 @@ export class MultiRootEditor extends Editor {
}
}

registerAndInitializeRootConfigAttributes( this );
// Register `$rootEditableOptions` unconditionally, so it is always returned by `getRootAttributes()` (e.g. for RH).
// The value is set via `config.roots.<rootName>.modelAttributes.$rootEditableOptions` (see `normalizeRootEditableOptionsConfig`),
// which also makes it round-trip through RTC's initial-data path.
this.registerRootAttribute( '$rootEditableOptions' );

// 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 ) {
const root = this.model.document.getRoot( rootName )!;

// 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 );
}
} );
} );
registerAndInitializeRootConfigAttributes( this );

const options = {
shouldToolbarGroupWhenFull: !this.config.get( 'toolbar.shouldNotGroupWhenFull' ),
Expand Down Expand Up @@ -447,7 +430,8 @@ export class MultiRootEditor extends Editor {

public addRoot( rootName: string, options: AddRootOptions & AddRootRootConfig = {} ): void {
const initialData: string = options.initialData || options.data || '';
const modelAttributes: EditorRootAttributes = options.modelAttributes || options.attributes || {};
const modelAttributes: EditorRootAttributes = { ...options.modelAttributes || options.attributes };
// eslint-disable-next-line ckeditor5-rules/no-literal-dollar-root -- public API default for `addRoot()`
const modelElement: string = options.modelElement || options.elementName || '$root';

if ( !this.model.schema.isLimit( modelElement ) ) {
Expand All @@ -474,6 +458,9 @@ export class MultiRootEditor extends Editor {
logWarning( 'multi-root-editor-add-root-element-option-ignored' );
}

// Storing editable options as a root attribute to make them available on other RTC clients.
ensureRootEditableOptions( modelAttributes, options );

const _addRoot = ( writer: ModelWriter ) => {
const root = writer.addRoot( rootName, modelElement );

Expand All @@ -485,14 +472,6 @@ export class MultiRootEditor extends Editor {
this.registerRootAttribute( key );
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 ( options.isUndoable ) {
Expand Down Expand Up @@ -1238,6 +1217,53 @@ function normalizeRootsAttributesConfig( config: Config<EditorConfig> ): void {
}
}

/**
* Normalize `placeholder` and `label` from `config.roots.<rootName>` into the `$rootEditableOptions` root model attribute,
* stored under `config.roots.<rootName>.modelAttributes`. This way the attribute is registered, set on initial data load
* and shipped through RTC initial-data path together with the rest of `modelAttributes`.
*
* This is also required by the revision history feature: on editor load, RH compares the latest revision data against
* `initialData` and `modelAttributes` passed to the editor and logs a warning if they do not match. Because `$rootEditableOptions`
* ends up in the revision data, it must also be present in `modelAttributes` (even as an empty object when no options
* are configured), otherwise the comparison reports a spurious mismatch.
*/
function normalizeRootEditableOptionsConfig( config: Config<EditorConfig> ): void {
const rootsConfig = config.get( 'roots' )!;

for ( const [ rootName, rootConfig ] of Object.entries( rootsConfig ) ) {
const modelAttributes: EditorRootAttributes = { ...rootConfig.modelAttributes };

if ( !ensureRootEditableOptions( modelAttributes, rootConfig ) ) {
continue;
}

config.set( `roots.${ rootName }.modelAttributes`, modelAttributes );
}
}

/**
* Mutates the given `modelAttributes` map by adding the `$rootEditableOptions` entry derived from `placeholder` and `label`.
* If `$rootEditableOptions` is already present, the map is left untouched.
*
* Returns `true` when the map was modified, `false` otherwise โ€” useful to skip downstream work (e.g. `config.set`)
* when nothing changed.
*/
function ensureRootEditableOptions(
modelAttributes: EditorRootAttributes,
{ placeholder, label }: RootEditableOptions
): boolean {
if ( '$rootEditableOptions' in modelAttributes ) {
return false;
}

modelAttributes.$rootEditableOptions = {
...placeholder && { placeholder },
...label && { label }
} satisfies RootEditableOptions;

return true;
}

function isElement( value: any ): value is Element {
return _isElement( value );
}
Expand Down
Loading