@ckeditor/ckeditor5-editor-multi-root 48.2.0 → 48.3.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,986 +2,866 @@
2
2
  * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
4
  */
5
- import { rootAcceptsBlocks, Editor, normalizeMultiRootEditorConstructorParams, normalizeRootsConfig, secureSourceElement, registerAndInitializeRootConfigAttributes, normalizeViewRootElementDefinition, verifyRootElements } from '@ckeditor/ckeditor5-core/dist/index.js';
6
- import { CKEditorError, logWarning, decodeLicenseKey, isFeatureBlockedByLicenseKey, setDataInElement } from '@ckeditor/ckeditor5-utils/dist/index.js';
7
- import { EditorUI, EditorUIView, ToolbarView, MenuBarView, InlineEditableUIView } from '@ckeditor/ckeditor5-ui/dist/index.js';
8
- import { enableViewPlaceholder } from '@ckeditor/ckeditor5-engine/dist/index.js';
9
- import { isElement as isElement$1 } from 'es-toolkit/compat';
5
+ import { Editor, normalizeMultiRootEditorConstructorParams, normalizeRootsConfig, normalizeViewRootElementDefinition, registerAndInitializeRootConfigAttributes, rootAcceptsBlocks, secureSourceElement, verifyRootElements } from "@ckeditor/ckeditor5-core";
6
+ import { CKEditorError, decodeLicenseKey, isFeatureBlockedByLicenseKey, logWarning, setDataInElement } from "@ckeditor/ckeditor5-utils";
7
+ import { EditorUI, EditorUIView, InlineEditableUIView, MenuBarView, ToolbarView } from "@ckeditor/ckeditor5-ui";
8
+ import { enableViewPlaceholder } from "@ckeditor/ckeditor5-engine";
9
+ import { isElement } from "es-toolkit/compat";
10
10
 
11
11
  /**
12
- * The multi-root editor UI class.
13
- */ class MultiRootEditorUI extends EditorUI {
14
- /**
15
- * The main (top–most) view of the editor UI.
16
- */ view;
17
- /**
18
- * The editable element that was focused the last time when any of the editables had focus.
19
- */ _lastFocusedEditableElement;
20
- /**
21
- * Creates an instance of the multi-root editor UI class.
22
- *
23
- * @param editor The editor instance.
24
- * @param view The view of the UI.
25
- */ constructor(editor, view){
26
- super(editor);
27
- this.view = view;
28
- this._lastFocusedEditableElement = null;
29
- }
30
- /**
31
- * Initializes the UI.
32
- */ init() {
33
- const view = this.view;
34
- const editor = this.editor;
35
- // Resolved during UI init rather than in the editor constructor: by this point the plugin
36
- // initialization phase has finished, so the schema is fully populated and the check below
37
- // reflects any plugin-registered root types or additional content rules.
38
- // Done before `view.render()` so the CSS class lands on the DOM from the start.
39
- for (const editable of Object.values(this.view.editables)){
40
- editable.isInlineRoot = !rootAcceptsBlocks(editor, editable.name);
41
- }
42
- view.render();
43
- // Keep track of the last focused editable element. Knowing which one was focused
44
- // is useful when the focus moves from editable to other UI components like balloons
45
- // (especially inputs) but the editable remains the "focus context" (e.g. link balloon
46
- // attached to a link in an editable). In this case, the editable should preserve visual
47
- // focus styles.
48
- this.focusTracker.on('change:focusedElement', (evt, name, focusedElement)=>{
49
- for (const editable of Object.values(this.view.editables)){
50
- if (focusedElement === editable.element) {
51
- this._lastFocusedEditableElement = editable.element;
52
- }
53
- }
54
- });
55
- // If the focus tracker loses focus, stop tracking the last focused editable element.
56
- // Wherever the focus is restored, it will no longer be in the context of that editable
57
- // because the focus "came from the outside", as opposed to the focus moving from one element
58
- // to another within the editor UI.
59
- this.focusTracker.on('change:isFocused', (evt, name, isFocused)=>{
60
- if (!isFocused) {
61
- this._lastFocusedEditableElement = null;
62
- }
63
- });
64
- for (const editable of Object.values(this.view.editables)){
65
- this.addEditable(editable);
66
- }
67
- this._initToolbar();
68
- this.initMenuBar(this.view.menuBarView);
69
- this.fire('ready');
70
- }
71
- /**
72
- * Adds the editable to the editor UI.
73
- *
74
- * After the editable is added to the editor UI it can be considered "active".
75
- *
76
- * The editable is attached to the editor editing pipeline, which means that it will be updated as the editor model updates and
77
- * changing its content will be reflected in the editor model. Keystrokes, focus handling and placeholder are initialized.
78
- *
79
- * @param editable The editable instance to add.
80
- * @param placeholder Placeholder for the editable element. If not set, placeholder value from the
81
- * {@link module:core/editor/editorconfig~EditorConfig#placeholder editor configuration} will be used (if it was provided).
82
- */ addEditable(editable, placeholder) {
83
- // The editable UI element in DOM is available for sure only after the editor UI view has been rendered.
84
- // But it can be available earlier if a DOM element has been passed to `MultiRootEditor.create()`.
85
- const editableElement = editable.element;
86
- // Bind the editable UI element to the editing view, making it an end– and entry–point
87
- // of the editor's engine. This is where the engine meets the UI.
88
- this.editor.editing.view.attachDomRoot(editableElement, editable.name);
89
- // Register each editable UI view in the editor.
90
- this.setEditableElement(editable.name, editableElement);
91
- // Let the editable UI element respond to the changes in the global editor focus
92
- // tracker. It has been added to the same tracker a few lines above but, in reality, there are
93
- // many focusable areas in the editor, like balloons, toolbars or dropdowns and as long
94
- // as they have focus, the editable should act like it is focused too (although technically
95
- // it isn't), e.g. by setting the proper CSS class, visually announcing focus to the user.
96
- // Doing otherwise will result in editable focus styles disappearing, once e.g. the
97
- // toolbar gets focused.
98
- editable.bind('isFocused').to(this.focusTracker, 'isFocused', this.focusTracker, 'focusedElement', (isFocused, focusedElement)=>{
99
- // When the focus tracker is blurred, it means the focus moved out of the editor UI.
100
- // No editable will maintain focus then.
101
- if (!isFocused) {
102
- return false;
103
- }
104
- // If the focus tracker says the editor UI is focused and currently focused element
105
- // is the editable, then the editable should be visually marked as focused too.
106
- if (focusedElement === editableElement) {
107
- return true;
108
- } else {
109
- return this._lastFocusedEditableElement === editableElement;
110
- }
111
- });
112
- this._initPlaceholder(editable, placeholder);
113
- }
114
- /**
115
- * Removes the editable instance from the editor UI.
116
- *
117
- * Removed editable can be considered "deactivated".
118
- *
119
- * The editable is detached from the editing pipeline, so model changes are no longer reflected in it. All handling added in
120
- * {@link #addEditable} is removed.
121
- *
122
- * @param editable Editable to remove from the editor UI.
123
- */ removeEditable(editable) {
124
- const editingView = this.editor.editing.view;
125
- if (editingView.getDomRoot(editable.name)) {
126
- editingView.detachDomRoot(editable.name);
127
- }
128
- editable.unbind('isFocused');
129
- this.removeEditableElement(editable.name);
130
- }
131
- /**
132
- * @inheritDoc
133
- */ destroy() {
134
- super.destroy();
135
- for (const editable of Object.values(this.view.editables)){
136
- this.removeEditable(editable);
137
- }
138
- this.view.destroy();
139
- }
140
- /**
141
- * Initializes the editor main toolbar and its panel.
142
- */ _initToolbar() {
143
- const editor = this.editor;
144
- const view = this.view;
145
- const toolbar = view.toolbar;
146
- toolbar.fillFromConfig(editor.config.get('toolbar'), this.componentFactory);
147
- // Register the toolbar, so it becomes available for Alt+F10 and Esc navigation.
148
- this.addToolbar(view.toolbar);
149
- }
150
- /**
151
- * Enables the placeholder text on a given editable.
152
- *
153
- * @param editable Editable on which the placeholder should be set.
154
- * @param placeholder Placeholder for the editable element. If not set, placeholder value from the
155
- * {@link module:core/editor/editorconfig~EditorConfig#placeholder editor configuration} will be used (if it was provided).
156
- */ _initPlaceholder(editable, placeholder) {
157
- if (!placeholder) {
158
- placeholder = this.editor.config.get('roots')[editable.name]?.placeholder;
159
- }
160
- const editingView = this.editor.editing.view;
161
- const editingRoot = editingView.document.getRoot(editable.name);
162
- if (placeholder) {
163
- editingRoot.placeholder = placeholder;
164
- }
165
- enableViewPlaceholder({
166
- view: editingView,
167
- element: editingRoot,
168
- isDirectHost: editable.isInlineRoot,
169
- keepOnFocus: true
170
- });
171
- }
172
- }
12
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
13
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
14
+ */
15
+ /**
16
+ * @module editor-multi-root/multirooteditorui
17
+ */
18
+ /**
19
+ * The multi-root editor UI class.
20
+ */
21
+ var MultiRootEditorUI = class extends EditorUI {
22
+ /**
23
+ * The main (top–most) view of the editor UI.
24
+ */
25
+ view;
26
+ /**
27
+ * The editable element that was focused the last time when any of the editables had focus.
28
+ */
29
+ _lastFocusedEditableElement;
30
+ /**
31
+ * Creates an instance of the multi-root editor UI class.
32
+ *
33
+ * @param editor The editor instance.
34
+ * @param view The view of the UI.
35
+ */
36
+ constructor(editor, view) {
37
+ super(editor);
38
+ this.view = view;
39
+ this._lastFocusedEditableElement = null;
40
+ }
41
+ /**
42
+ * Initializes the UI.
43
+ */
44
+ init() {
45
+ const view = this.view;
46
+ const editor = this.editor;
47
+ for (const editable of Object.values(this.view.editables)) editable.isInlineRoot = !rootAcceptsBlocks(editor, editable.name);
48
+ view.render();
49
+ this.focusTracker.on("change:focusedElement", (evt, name, focusedElement) => {
50
+ for (const editable of Object.values(this.view.editables)) if (focusedElement === editable.element) this._lastFocusedEditableElement = editable.element;
51
+ });
52
+ this.focusTracker.on("change:isFocused", (evt, name, isFocused) => {
53
+ if (!isFocused) this._lastFocusedEditableElement = null;
54
+ });
55
+ for (const editable of Object.values(this.view.editables)) this.addEditable(editable);
56
+ this._initToolbar();
57
+ this.initMenuBar(this.view.menuBarView);
58
+ this.fire("ready");
59
+ }
60
+ /**
61
+ * Adds the editable to the editor UI.
62
+ *
63
+ * After the editable is added to the editor UI it can be considered "active".
64
+ *
65
+ * The editable is attached to the editor editing pipeline, which means that it will be updated as the editor model updates and
66
+ * changing its content will be reflected in the editor model. Keystrokes, focus handling and placeholder are initialized.
67
+ *
68
+ * @param editable The editable instance to add.
69
+ * @param placeholder Placeholder for the editable element. If not set, placeholder value from the
70
+ * {@link module:core/editor/editorconfig~EditorConfig#placeholder editor configuration} will be used (if it was provided).
71
+ */
72
+ addEditable(editable, placeholder) {
73
+ const editableElement = editable.element;
74
+ this.editor.editing.view.attachDomRoot(editableElement, editable.name);
75
+ this.setEditableElement(editable.name, editableElement);
76
+ editable.bind("isFocused").to(this.focusTracker, "isFocused", this.focusTracker, "focusedElement", (isFocused, focusedElement) => {
77
+ if (!isFocused) return false;
78
+ if (focusedElement === editableElement) return true;
79
+ else return this._lastFocusedEditableElement === editableElement;
80
+ });
81
+ this._initPlaceholder(editable, placeholder);
82
+ }
83
+ /**
84
+ * Removes the editable instance from the editor UI.
85
+ *
86
+ * Removed editable can be considered "deactivated".
87
+ *
88
+ * The editable is detached from the editing pipeline, so model changes are no longer reflected in it. All handling added in
89
+ * {@link #addEditable} is removed.
90
+ *
91
+ * @param editable Editable to remove from the editor UI.
92
+ */
93
+ removeEditable(editable) {
94
+ const editingView = this.editor.editing.view;
95
+ if (editingView.getDomRoot(editable.name)) editingView.detachDomRoot(editable.name);
96
+ editable.unbind("isFocused");
97
+ this.removeEditableElement(editable.name);
98
+ }
99
+ /**
100
+ * @inheritDoc
101
+ */
102
+ destroy() {
103
+ super.destroy();
104
+ for (const editable of Object.values(this.view.editables)) this.removeEditable(editable);
105
+ this.view.destroy();
106
+ }
107
+ /**
108
+ * Initializes the editor main toolbar and its panel.
109
+ */
110
+ _initToolbar() {
111
+ const editor = this.editor;
112
+ const view = this.view;
113
+ view.toolbar.fillFromConfig(editor.config.get("toolbar"), this.componentFactory);
114
+ this.addToolbar(view.toolbar);
115
+ }
116
+ /**
117
+ * Enables the placeholder text on a given editable.
118
+ *
119
+ * @param editable Editable on which the placeholder should be set.
120
+ * @param placeholder Placeholder for the editable element. If not set, placeholder value from the
121
+ * {@link module:core/editor/editorconfig~EditorConfig#placeholder editor configuration} will be used (if it was provided).
122
+ */
123
+ _initPlaceholder(editable, placeholder) {
124
+ if (!placeholder) placeholder = this.editor.config.get("roots")[editable.name]?.placeholder;
125
+ const editingView = this.editor.editing.view;
126
+ const editingRoot = editingView.document.getRoot(editable.name);
127
+ if (placeholder) editingRoot.placeholder = placeholder;
128
+ enableViewPlaceholder({
129
+ view: editingView,
130
+ element: editingRoot,
131
+ isDirectHost: editable.isInlineRoot,
132
+ keepOnFocus: true
133
+ });
134
+ }
135
+ };
173
136
 
174
137
  /**
175
- * The multi-root editor UI view. It is a virtual view providing an inline
176
- * {@link module:editor-multi-root/multirooteditoruiview~MultiRootEditorUIView#editable} and a
177
- * {@link module:editor-multi-root/multirooteditoruiview~MultiRootEditorUIView#toolbar}, but without any
178
- * specific arrangement of the components in the DOM.
179
- *
180
- * See {@link module:editor-multi-root/multirooteditor~MultiRootEditor.create `MultiRootEditor.create()`}
181
- * to learn more about this view.
182
- */ class MultiRootEditorUIView extends EditorUIView {
183
- /**
184
- * The main toolbar of the multi-root editor UI.
185
- */ toolbar;
186
- /**
187
- * Editable elements used by the multi-root editor UI.
188
- */ editables;
189
- editable;
190
- /**
191
- * Menu bar view instance.
192
- */ menuBarView;
193
- /**
194
- * The editing view instance this view is related to.
195
- */ _editingView;
196
- /**
197
- * Creates an instance of the multi-root editor UI view.
198
- *
199
- * @param locale The {@link module:core/editor/editor~Editor#locale} instance.
200
- * @param editingView The editing view instance this view is related to.
201
- * @param editableNames Names for all editable views. For each name, one
202
- * {@link module:ui/editableui/inline/inlineeditableuiview~InlineEditableUIView `InlineEditableUIView`} instance will be initialized.
203
- * @param options Configuration options for the view instance.
204
- * @param options.editableElements The editable elements to be used, assigned to their names. If not specified, they will be
205
- * automatically created by {@link module:ui/editableui/inline/inlineeditableuiview~InlineEditableUIView `InlineEditableUIView`}
206
- * instances.
207
- * @param options.shouldToolbarGroupWhenFull When set to `true` enables automatic items grouping
208
- * in the main {@link module:editor-multi-root/multirooteditoruiview~MultiRootEditorUIView#toolbar toolbar}.
209
- * See {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull} to learn more.
210
- * @param options.label When set, this value will be used as an accessible `aria-label` of the
211
- * {@link module:ui/editableui/editableuiview~EditableUIView editable view} elements.
212
- */ constructor(locale, editingView, editableNames, options = {}){
213
- super(locale);
214
- this._editingView = editingView;
215
- this.toolbar = new ToolbarView(locale, {
216
- shouldGroupWhenFull: options.shouldToolbarGroupWhenFull
217
- });
218
- this.menuBarView = new MenuBarView(locale);
219
- this.editables = {};
220
- // Create `InlineEditableUIView` instance for each editable.
221
- for (const editableName of editableNames){
222
- const editableElement = options.editableElements?.[editableName];
223
- let { label } = options;
224
- if (typeof label === 'object') {
225
- label = label[editableName];
226
- }
227
- this.createEditable(editableName, editableElement, label);
228
- }
229
- this.editable = Object.values(this.editables)[0];
230
- // This toolbar may be placed anywhere in the page so things like font size need to be reset in it.
231
- // Because of the above, make sure the toolbar supports rounded corners.
232
- // Also, make sure the toolbar has the proper dir attribute because its ancestor may not have one
233
- // and some toolbar item styles depend on this attribute.
234
- this.toolbar.extendTemplate({
235
- attributes: {
236
- class: [
237
- 'ck-reset_all',
238
- 'ck-rounded-corners'
239
- ],
240
- dir: locale.uiLanguageDirection
241
- }
242
- });
243
- this.menuBarView.extendTemplate({
244
- attributes: {
245
- class: [
246
- 'ck-reset_all',
247
- 'ck-rounded-corners'
248
- ],
249
- dir: locale.uiLanguageDirection
250
- }
251
- });
252
- }
253
- /**
254
- * Creates an editable instance with given name and registers it in the editor UI view.
255
- *
256
- * If `editableElement` is provided, the editable instance will be created on top of it. Otherwise, the editor will create a new
257
- * DOM element and use it instead.
258
- *
259
- * @param editableName The name for the editable.
260
- * @param editableElement DOM element for which the editable should be created.
261
- * @param label The accessible editable label used by assistive technologies.
262
- * @returns The created editable instance.
263
- */ createEditable(editableName, editableElement, label) {
264
- const editable = new InlineEditableUIView(this.locale, this._editingView, editableElement, {
265
- label
266
- });
267
- this.editables[editableName] = editable;
268
- editable.name = editableName;
269
- if (this.isRendered) {
270
- this.registerChild(editable);
271
- }
272
- return editable;
273
- }
274
- /**
275
- * Destroys and removes the editable from the editor UI view.
276
- *
277
- * @param editableName The name of the editable that should be removed.
278
- */ removeEditable(editableName) {
279
- const editable = this.editables[editableName];
280
- if (this.isRendered) {
281
- this.deregisterChild(editable);
282
- }
283
- delete this.editables[editableName];
284
- editable.destroy();
285
- }
286
- /**
287
- * @inheritDoc
288
- */ render() {
289
- super.render();
290
- this.registerChild(Object.values(this.editables));
291
- this.registerChild(this.toolbar);
292
- this.registerChild(this.menuBarView);
293
- }
294
- }
138
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
139
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
140
+ */
141
+ /**
142
+ * @module editor-multi-root/multirooteditoruiview
143
+ */
144
+ /**
145
+ * The multi-root editor UI view. It is a virtual view providing an inline
146
+ * {@link module:editor-multi-root/multirooteditoruiview~MultiRootEditorUIView#editable} and a
147
+ * {@link module:editor-multi-root/multirooteditoruiview~MultiRootEditorUIView#toolbar}, but without any
148
+ * specific arrangement of the components in the DOM.
149
+ *
150
+ * See {@link module:editor-multi-root/multirooteditor~MultiRootEditor.create `MultiRootEditor.create()`}
151
+ * to learn more about this view.
152
+ */
153
+ var MultiRootEditorUIView = class extends EditorUIView {
154
+ /**
155
+ * The main toolbar of the multi-root editor UI.
156
+ */
157
+ toolbar;
158
+ /**
159
+ * Editable elements used by the multi-root editor UI.
160
+ */
161
+ editables;
162
+ editable;
163
+ /**
164
+ * Menu bar view instance.
165
+ */
166
+ menuBarView;
167
+ /**
168
+ * The editing view instance this view is related to.
169
+ */
170
+ _editingView;
171
+ /**
172
+ * Creates an instance of the multi-root editor UI view.
173
+ *
174
+ * @param locale The {@link module:core/editor/editor~Editor#locale} instance.
175
+ * @param editingView The editing view instance this view is related to.
176
+ * @param editableNames Names for all editable views. For each name, one
177
+ * {@link module:ui/editableui/inline/inlineeditableuiview~InlineEditableUIView `InlineEditableUIView`} instance will be initialized.
178
+ * @param options Configuration options for the view instance.
179
+ * @param options.editableElements The editable elements to be used, assigned to their names. If not specified, they will be
180
+ * automatically created by {@link module:ui/editableui/inline/inlineeditableuiview~InlineEditableUIView `InlineEditableUIView`}
181
+ * instances.
182
+ * @param options.shouldToolbarGroupWhenFull When set to `true` enables automatic items grouping
183
+ * in the main {@link module:editor-multi-root/multirooteditoruiview~MultiRootEditorUIView#toolbar toolbar}.
184
+ * See {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull} to learn more.
185
+ * @param options.label When set, this value will be used as an accessible `aria-label` of the
186
+ * {@link module:ui/editableui/editableuiview~EditableUIView editable view} elements.
187
+ */
188
+ constructor(locale, editingView, editableNames, options = {}) {
189
+ super(locale);
190
+ this._editingView = editingView;
191
+ this.toolbar = new ToolbarView(locale, { shouldGroupWhenFull: options.shouldToolbarGroupWhenFull });
192
+ this.menuBarView = new MenuBarView(locale);
193
+ this.editables = {};
194
+ for (const editableName of editableNames) {
195
+ const editableElement = options.editableElements?.[editableName];
196
+ let { label } = options;
197
+ if (typeof label === "object") label = label[editableName];
198
+ this.createEditable(editableName, editableElement, label);
199
+ }
200
+ this.editable = Object.values(this.editables)[0];
201
+ this.toolbar.extendTemplate({ attributes: {
202
+ class: ["ck-reset_all", "ck-rounded-corners"],
203
+ dir: locale.uiLanguageDirection
204
+ } });
205
+ this.menuBarView.extendTemplate({ attributes: {
206
+ class: ["ck-reset_all", "ck-rounded-corners"],
207
+ dir: locale.uiLanguageDirection
208
+ } });
209
+ }
210
+ /**
211
+ * Creates an editable instance with given name and registers it in the editor UI view.
212
+ *
213
+ * If `editableElement` is provided, the editable instance will be created on top of it. Otherwise, the editor will create a new
214
+ * DOM element and use it instead.
215
+ *
216
+ * @param editableName The name for the editable.
217
+ * @param editableElement DOM element for which the editable should be created.
218
+ * @param label The accessible editable label used by assistive technologies.
219
+ * @returns The created editable instance.
220
+ */
221
+ createEditable(editableName, editableElement, label) {
222
+ const editable = new InlineEditableUIView(this.locale, this._editingView, editableElement, { label });
223
+ this.editables[editableName] = editable;
224
+ editable.name = editableName;
225
+ if (this.isRendered) this.registerChild(editable);
226
+ return editable;
227
+ }
228
+ /**
229
+ * Destroys and removes the editable from the editor UI view.
230
+ *
231
+ * @param editableName The name of the editable that should be removed.
232
+ */
233
+ removeEditable(editableName) {
234
+ const editable = this.editables[editableName];
235
+ if (this.isRendered) this.deregisterChild(editable);
236
+ delete this.editables[editableName];
237
+ editable.destroy();
238
+ }
239
+ /**
240
+ * @inheritDoc
241
+ */
242
+ render() {
243
+ super.render();
244
+ this.registerChild(Object.values(this.editables));
245
+ this.registerChild(this.toolbar);
246
+ this.registerChild(this.menuBarView);
247
+ }
248
+ };
295
249
 
296
250
  /**
297
- * The multi-root editor implementation.
298
- *
299
- * The multi-root editor provides multiple inline editable elements and a toolbar. All editable areas are controlled by one editor
300
- * instance, which means that they share common configuration, document ID, or undo stack.
301
- *
302
- * This type of editor is dedicated to integrations which require a customized UI with an open structure, featuring multiple editable areas,
303
- * allowing developers to have a control over the exact location of these editable areas.
304
- *
305
- * In order to create a multi-root editor instance, use the static
306
- * {@link module:editor-multi-root/multirooteditor~MultiRootEditor.create `MultiRootEditor.create()`} method.
307
- *
308
- * Note that you will need to attach the editor toolbar to your web page manually, in a desired place, after the editor is initialized.
309
- */ class MultiRootEditor extends Editor {
310
- /**
311
- * @inheritDoc
312
- */ static get editorName() {
313
- return 'MultiRootEditor';
314
- }
315
- /**
316
- * @inheritDoc
317
- */ ui;
318
- /**
319
- * The elements on which the editor has been initialized.
320
- */ sourceElements;
321
- /**
322
- * A set of lock IDs for enabling or disabling particular root.
323
- */ _readOnlyRootLocks = new Map();
324
- constructor(sourceElementsOrDataOrConfig, config = {}){
325
- const { sourceElementsOrData, editorConfig } = normalizeMultiRootEditorConstructorParams(sourceElementsOrDataOrConfig, config);
326
- super(editorConfig);
327
- normalizeRootsConfig(sourceElementsOrData, this.config, false);
328
- normalizeRootsAttributesConfig(this.config);
329
- normalizeRootEditableOptionsConfig(this.config);
330
- if (this.config.get('lazyRoots')) {
331
- /**
332
- * Using deprecated `config.lazyRoots` configuration option.
333
- * Use `config.roots.<rootName>.lazyLoad` instead.
334
- *
335
- * @error multi-root-editor-root-deprecated-config-lazy-roots
336
- */ throw new CKEditorError('multi-root-editor-root-deprecated-config-lazy-roots', null);
337
- }
338
- // From this point use only normalized `roots.<rootName>.element`, etc.
339
- const rootsConfig = Object.entries(this.config.get('roots'));
340
- this.sourceElements = {};
341
- const editableElements = {};
342
- for (const [rootName, rootConfig] of rootsConfig){
343
- const editableElement = getRootEditableElement(rootConfig);
344
- if (!editableElement) {
345
- continue;
346
- }
347
- if (isElement(editableElement)) {
348
- this.sourceElements[rootName] = editableElement;
349
- secureSourceElement(this, editableElement);
350
- }
351
- editableElements[rootName] = editableElement;
352
- }
353
- this.editing.view.document.roots.on('add', (evt, viewRoot)=>{
354
- // Here we change the standard binding of readOnly flag by adding
355
- // additional constraint that multi-root has (enabling / disabling particular root).
356
- viewRoot.unbind('isReadOnly');
357
- viewRoot.bind('isReadOnly').to(this.editing.view.document, 'isReadOnly', (isReadOnly)=>{
358
- return isReadOnly || this._readOnlyRootLocks.has(viewRoot.rootName);
359
- });
360
- // Hacky solution to nested editables.
361
- // Nested editables should be managed each separately and do not base on view document or view root.
362
- viewRoot.on('change:isReadOnly', (evt, prop, value)=>{
363
- const viewRange = this.editing.view.createRangeIn(viewRoot);
364
- for (const viewItem of viewRange.getItems()){
365
- if (viewItem.is('editableElement')) {
366
- viewItem.unbind('isReadOnly');
367
- viewItem.isReadOnly = value;
368
- }
369
- }
370
- });
371
- });
372
- for (const [rootName, rootConfig] of rootsConfig){
373
- // Create root and `UIView` element for each editable container.
374
- const root = this.model.document.createRoot(rootConfig.modelElement, rootName);
375
- if (rootConfig.lazyLoad) {
376
- root._isLoaded = false;
377
- }
378
- }
379
- // Register `$rootEditableOptions` unconditionally, so it is always returned by `getRootAttributes()` (e.g. for RH).
380
- // The value is set via `config.roots.<rootName>.modelAttributes.$rootEditableOptions` (see `normalizeRootEditableOptionsConfig`),
381
- // which also makes it round-trip through RTC's initial-data path.
382
- this.registerRootAttribute('$rootEditableOptions');
383
- registerAndInitializeRootConfigAttributes(this);
384
- const options = {
385
- shouldToolbarGroupWhenFull: !this.config.get('toolbar.shouldNotGroupWhenFull'),
386
- editableElements,
387
- label: extractRootsConfigField(this.config.get('roots'), 'label')
388
- };
389
- const view = new MultiRootEditorUIView(this.locale, this.editing.view, getNonLazyLoadRootsNames(rootsConfig), options);
390
- this.ui = new MultiRootEditorUI(this, view);
391
- this.model.document.on('change:data', ()=>{
392
- const changedRoots = this.model.document.differ.getChangedRoots();
393
- // Fire detaches first. If there are multiple roots removed and added in one batch, it should be easier to handle if
394
- // changes aren't mixed. Detaching will usually lead to just removing DOM elements. Detaching first will lead to a clean DOM
395
- // when new editables are added in `addRoot` event.
396
- for (const changes of changedRoots){
397
- const root = this.model.document.getRoot(changes.name);
398
- if (changes.state == 'detached') {
399
- this.fire('detachRoot', root);
400
- }
401
- }
402
- for (const changes of changedRoots){
403
- const root = this.model.document.getRoot(changes.name);
404
- if (changes.state == 'attached') {
405
- this.fire('addRoot', root);
406
- }
407
- }
408
- });
409
- // Overwrite `Model#canEditAt()` decorated method.
410
- // Check if the provided selection is inside a read-only root. If so, return `false`.
411
- this.listenTo(this.model, 'canEditAt', (evt, [selection])=>{
412
- // Skip empty selections.
413
- if (!selection) {
414
- return;
415
- }
416
- let selectionInReadOnlyRoot = false;
417
- for (const range of selection.getRanges()){
418
- const root = range.root;
419
- if (this._readOnlyRootLocks.has(root.rootName)) {
420
- selectionInReadOnlyRoot = true;
421
- break;
422
- }
423
- }
424
- // If selection is in read-only root, return `false` and prevent further processing.
425
- // Otherwise, allow for other callbacks (or default callback) to evaluate.
426
- if (selectionInReadOnlyRoot) {
427
- evt.return = false;
428
- evt.stop();
429
- }
430
- }, {
431
- priority: 'high'
432
- });
433
- this.decorate('loadRoot');
434
- this.on('loadRoot', (evt, [rootName])=>{
435
- const root = this.model.document.getRoot(rootName);
436
- if (!root) {
437
- /**
438
- * The root to load does not exist.
439
- *
440
- * @error multi-root-editor-load-root-no-root
441
- */ throw new CKEditorError('multi-root-editor-load-root-no-root', this, {
442
- rootName
443
- });
444
- }
445
- if (root._isLoaded) {
446
- /**
447
- * The root to load was already loaded before. The `loadRoot()` call has no effect.
448
- *
449
- * @error multi-root-editor-load-root-already-loaded
450
- */ logWarning('multi-root-editor-load-root-already-loaded');
451
- evt.stop();
452
- }
453
- }, {
454
- priority: 'highest'
455
- });
456
- verifyLicenseKey(this);
457
- function verifyLicenseKey(editor) {
458
- const licenseKey = editor.config.get('licenseKey');
459
- const decodedPayload = decodeLicenseKey(licenseKey);
460
- if (!decodedPayload) {
461
- return;
462
- }
463
- if (isFeatureBlockedByLicenseKey(decodedPayload, 'MRE')) {
464
- editor.enableReadOnlyMode(Symbol('invalidLicense'));
465
- editor._showLicenseError('featureNotAllowed', 'Multi-root editor');
466
- }
467
- }
468
- }
469
- /**
470
- * Destroys the editor instance, releasing all resources used by it.
471
- *
472
- * Updates the original editor element with the data if the
473
- * {@link module:core/editor/editorconfig~EditorConfig#updateSourceElementOnDestroy `updateSourceElementOnDestroy`}
474
- * configuration option is set to `true`.
475
- *
476
- * **Note**: The multi-root editor does not remove the toolbar and editable when destroyed. You can
477
- * do that yourself in the destruction chain, if you need to:
478
- *
479
- * ```ts
480
- * editor.destroy().then( () => {
481
- * // Remove the toolbar from DOM.
482
- * editor.ui.view.toolbar.element.remove();
483
- *
484
- * // Remove editable elements from DOM.
485
- * for ( const editable of Object.values( editor.ui.view.editables ) ) {
486
- * editable.element.remove();
487
- * }
488
- *
489
- * console.log( 'Editor was destroyed' );
490
- * } );
491
- * ```
492
- */ async destroy() {
493
- const shouldUpdateSourceElement = this.config.get('updateSourceElementOnDestroy');
494
- // Cache the data and editable DOM elements, then destroy.
495
- // It's safe to assume that the model->view conversion will not work after `super.destroy()`,
496
- // same as `ui.getEditableElement()` method will not return editables.
497
- const data = {};
498
- for (const rootName of Object.keys(this.sourceElements)){
499
- data[rootName] = shouldUpdateSourceElement ? this.getData({
500
- rootName
501
- }) : '';
502
- }
503
- this.ui.destroy();
504
- await super.destroy();
505
- for (const rootName of Object.keys(this.sourceElements)){
506
- setDataInElement(this.sourceElements[rootName], data[rootName]);
507
- }
508
- // To satisfy the return type and to keep it backward compatible.
509
- // eslint-disable-next-line no-useless-return
510
- return;
511
- }
512
- addRoot(rootName, options = {}) {
513
- const initialData = options.initialData || options.data || '';
514
- const modelAttributes = {
515
- ...options.modelAttributes || options.attributes
516
- };
517
- // eslint-disable-next-line ckeditor5-rules/no-literal-dollar-root -- public API default for `addRoot()`
518
- const modelElement = options.modelElement || options.elementName || '$root';
519
- if (!this.model.schema.isLimit(modelElement)) {
520
- /**
521
- * The model root element must be a {@link module:engine/model/schema~ModelSchemaItemDefinition#isLimit limit element}.
522
- * The element name specified in {@link module:editor-multi-root/multirooteditor~MultiRootEditor#addRoot:ROOT_CONFIG addRoot()}
523
- * options must be registered in the schema
524
- * with `isLimit` set to `true`.
525
- *
526
- * @error multi-root-editor-add-root-element-is-not-limit
527
- * @param rootName The name of the root that uses a non-limit element.
528
- * @param elementName The name of the model element used for the root.
529
- */ throw new CKEditorError('multi-root-editor-add-root-element-is-not-limit', this, {
530
- rootName,
531
- elementName: modelElement
532
- });
533
- }
534
- if (isElement(options.element)) {
535
- /**
536
- * Passing an existing DOM element as the `element` option of
537
- * {@link ~MultiRootEditor#addRoot:ROOT_CONFIG `addRoot()`} is not supported and will be ignored. The
538
- * `addRoot()` method only registers the model root; the DOM editable is created later by
539
- * {@link ~MultiRootEditor#createEditable `createEditable()`}.
540
- *
541
- * Pass a tag name string (e.g. `'h1'`) or a
542
- * {@link module:engine/view/elementdefinition~ViewElementDefinition view element definition}
543
- * instead, or omit the option to create a default `<div>`.
544
- *
545
- * @error multi-root-editor-add-root-element-option-ignored
546
- */ logWarning('multi-root-editor-add-root-element-option-ignored');
547
- }
548
- // Persist editable options as a root attribute so they are available on other RTC clients.
549
- setRootEditableOptions(modelAttributes, options);
550
- const _addRoot = (writer)=>{
551
- const root = writer.addRoot(rootName, modelElement);
552
- if (initialData) {
553
- writer.insert(this.data.parse(initialData, root), root, 0);
554
- }
555
- for (const key of Object.keys(modelAttributes)){
556
- this.registerRootAttribute(key);
557
- writer.setAttribute(key, modelAttributes[key], root);
558
- }
559
- };
560
- if (options.isUndoable) {
561
- this.model.change(_addRoot);
562
- } else {
563
- this.model.enqueueChange({
564
- isUndoable: false
565
- }, _addRoot);
566
- }
567
- }
568
- /**
569
- * Detaches a root from the editor.
570
- *
571
- * ```ts
572
- * editor.detachRoot( 'myRoot' );
573
- * ```
574
- *
575
- * A detached root is not entirely removed from the editor model, however it can be considered removed.
576
- *
577
- * After a root is detached all its children are removed, all markers inside it are removed, and whenever something is inserted to it,
578
- * it is automatically removed as well. Finally, a detached root is not returned by
579
- * {@link module:engine/model/document~ModelDocument#getRootNames} by default.
580
- *
581
- * It is possible to re-add a previously detached root calling {@link #addRoot}.
582
- *
583
- * Whenever a root is detached, the editor instance will fire {@link #event:detachRoot `detachRoot` event}. The event is also
584
- * called when the root is detached indirectly, e.g. by the undo feature or on a remote client during real-time collaboration.
585
- *
586
- * Note, that this method only detached a root in the editor model. It **does not** destroy the DOM editable element linked with
587
- * the root and it **does not** remove the DOM element from the DOM structure of your application.
588
- *
589
- * To properly remove a DOM editable element after a root was detached, listen to {@link #event:detachRoot `detachRoot` event}
590
- * and call {@link #detachEditable}. Then, remove the DOM element from your application.
591
- *
592
- * ```ts
593
- * editor.on( 'detachRoot', ( evt, root ) => {
594
- * const editableElement = editor.detachEditable( root );
595
- *
596
- * // You may want to do an additional DOM clean-up here.
597
- *
598
- * editableElement.remove();
599
- * } );
600
- *
601
- * // ...
602
- *
603
- * editor.detachRoot( 'myRoot' ); // Will detach the root, and remove the DOM editable element.
604
- * ```
605
- *
606
- * By setting `isUndoable` flag to `true`, you can allow for re-adding the root using the undo feature.
607
- *
608
- * Additionally, you can group detaching multiple roots in one undo step. This can be useful if the roots are combined into one,
609
- * bigger UI element, and you want them all to be re-added together.
610
- *
611
- * ```ts
612
- * editor.model.change( () => {
613
- * editor.detachRoot( 'left-row-3', true );
614
- * editor.detachRoot( 'center-row-3', true );
615
- * editor.detachRoot( 'right-row-3', true );
616
- * } );
617
- * ```
618
- *
619
- * @param rootName Name of the root to detach.
620
- * @param isUndoable Whether detaching the root can be undone (using the undo feature) or not.
621
- */ detachRoot(rootName, isUndoable = false) {
622
- if (isUndoable) {
623
- this.model.change((writer)=>writer.detachRoot(rootName));
624
- } else {
625
- this.model.enqueueChange({
626
- isUndoable: false
627
- }, (writer)=>writer.detachRoot(rootName));
628
- }
629
- }
630
- createEditable(root, optionsOrPlaceholder, label) {
631
- let placeholder;
632
- let element;
633
- if (!optionsOrPlaceholder || typeof optionsOrPlaceholder === 'string') {
634
- placeholder = optionsOrPlaceholder;
635
- } else {
636
- placeholder = optionsOrPlaceholder?.placeholder;
637
- label = optionsOrPlaceholder?.label;
638
- element = optionsOrPlaceholder?.element;
639
- }
640
- const rootEditableConfig = root.getAttribute('$rootEditableOptions') || {};
641
- // Both the call-site `element` and the stored `rootEditableConfig.element` may be a tag name string
642
- // or an `HTMLElement` and need normalization - `setRootEditableOptions()` does this at write time, but
643
- // callers can also pre-supply `$rootEditableOptions` directly without going through it.
644
- const editableElement = normalizeViewRootElementDefinition(element || rootEditableConfig.element);
645
- const editable = this.ui.view.createEditable(root.rootName, editableElement, label || rootEditableConfig.label);
646
- editable.isInlineRoot = !rootAcceptsBlocks(this, root.rootName);
647
- this.ui.addEditable(editable, placeholder || rootEditableConfig.placeholder);
648
- this.editing.view.forceRender();
649
- return editable.element;
650
- }
651
- /**
652
- * Detaches the DOM editable element that was attached to the given root.
653
- *
654
- * @param root Root for which the editable element should be detached.
655
- * @returns The DOM element that was detached. You may want to remove it from your application DOM structure.
656
- */ detachEditable(root) {
657
- const rootName = root.rootName;
658
- const editable = this.ui.view.editables[rootName];
659
- this.ui.removeEditable(editable);
660
- this.ui.view.removeEditable(rootName);
661
- return editable.element;
662
- }
663
- /**
664
- * Loads a root that has previously been declared in
665
- * {@link module:core/editor/editorconfig~EditorConfig#roots `config.roots.<rootName>.lazyLoad`} configuration option.
666
- *
667
- * **Important! Lazy roots loading is an experimental feature, and may become deprecated. Be advised of the following
668
- * known limitations:**
669
- *
670
- * * **Real-time collaboration integrations that use
671
- * [uploaded editor bundles](https://ckeditor.com/docs/cs/latest/guides/collaboration/editor-bundle.html) are not supported. Using
672
- * lazy roots will lead to unexpected behavior and data loss.**
673
- * * **Revision history feature will read and process the whole document on editor initialization, possibly defeating the purpose
674
- * of using the lazy roots loading. Additionally, when the document is loaded for the first time, all roots need to be loaded,
675
- * to make sure that the initial revision data includes all roots. Otherwise, you may experience data loss.**
676
- * * **Multiple features, that require full document data to be loaded, will produce incorrect or confusing results if not all
677
- * roots are loaded. These include: bookmarks, find and replace, word count, pagination, document exports, document outline,
678
- * and table of contents.**
679
- *
680
- * Only roots specified in the editor config can be loaded. A root cannot be loaded multiple times. A root cannot be unloaded and
681
- * loading a root cannot be reverted using the undo feature.
682
- *
683
- * When a root becomes loaded, it will be treated by the editor as though it was just added. This, among others, means that all
684
- * related events and mechanisms will be fired, including {@link ~MultiRootEditor#event:addRoot `addRoot` event},
685
- * {@link module:engine/model/document~ModelDocument#event:change `model.Document` `change` event}, model post-fixers and conversion.
686
- *
687
- * Until the root becomes loaded, all above mechanisms are suppressed.
688
- *
689
- * This method is {@link module:utils/observablemixin~Observable#decorate decorated}.
690
- *
691
- * Note that attributes loaded together with a root are automatically registered.
692
- *
693
- * See also {@link ~MultiRootEditor#registerRootAttribute `MultiRootEditor#registerRootAttribute()`} and
694
- * {@link module:core/editor/editorconfig~EditorConfig#roots `config.roots.<rootName>.modelAttributes` configuration option}.
695
- *
696
- * When this method is used in real-time collaboration environment, its effects become asynchronous as the editor will first synchronize
697
- * with the remote editing session, before the root is added to the editor.
698
- *
699
- * If the root has been already loaded by any other client, the additional data passed in `loadRoot()` parameters will be ignored.
700
- *
701
- * @param rootName Name of the root to load.
702
- * @param options Additional options for the loaded root.
703
- * @fires loadRoot
704
- */ loadRoot(rootName, { data = '', attributes = {} } = {}) {
705
- // `root` will be defined as it is guaranteed by a check in a higher priority callback.
706
- const root = this.model.document.getRoot(rootName);
707
- this.model.enqueueChange({
708
- isUndoable: false
709
- }, (writer)=>{
710
- if (data) {
711
- writer.insert(this.data.parse(data, root), root, 0);
712
- }
713
- for (const key of Object.keys(attributes)){
714
- this.registerRootAttribute(key);
715
- writer.setAttribute(key, attributes[key], root);
716
- }
717
- root._isLoaded = true;
718
- this.model.document.differ._bufferRootLoad(root);
719
- });
720
- }
721
- /**
722
- * Returns the document data for all attached roots.
723
- *
724
- * @param options Additional configuration for the retrieved data.
725
- * Editor features may introduce more configuration options that can be set through this parameter.
726
- * @param options.trim Whether returned data should be trimmed. This option is set to `'empty'` by default,
727
- * which means that whenever editor content is considered empty, an empty string is returned. To turn off trimming
728
- * use `'none'`. In such cases exact content will be returned (for example `'<p>&nbsp;</p>'` for an empty editor).
729
- * @returns The full document data.
730
- */ getFullData(options) {
731
- const data = {};
732
- for (const rootName of this.model.document.getRootNames()){
733
- data[rootName] = this.data.get({
734
- ...options,
735
- rootName
736
- });
737
- }
738
- return data;
739
- }
740
- /**
741
- * Returns attributes for all attached roots.
742
- *
743
- * Note: all and only {@link ~MultiRootEditor#registerRootAttribute registered} roots attributes will be returned.
744
- * If a registered root attribute is not set for a given root, `null` will be returned.
745
- *
746
- * @returns Object with roots attributes. Keys are roots names, while values are attributes set on given root.
747
- */ getRootsAttributes() {
748
- const rootsAttributes = {};
749
- for (const rootName of this.model.document.getRootNames()){
750
- rootsAttributes[rootName] = this.getRootAttributes(rootName);
751
- }
752
- return rootsAttributes;
753
- }
754
- /**
755
- * Switches given editor root to the read-only mode.
756
- *
757
- * In contrary to {@link module:core/editor/editor~Editor#enableReadOnlyMode `enableReadOnlyMode()`}, which switches the whole editor
758
- * to the read-only mode, this method turns only a particular root to the read-only mode. This can be useful when you want to prevent
759
- * editing only a part of the editor content.
760
- *
761
- * When you switch a root to the read-only mode, you need provide a unique identifier (`lockId`) that will identify this request. You
762
- * will need to provide the same `lockId` when you will want to
763
- * {@link module:editor-multi-root/multirooteditor~MultiRootEditor#enableRoot re-enable} the root.
764
- *
765
- * ```ts
766
- * const model = editor.model;
767
- * const myRoot = model.document.getRoot( 'myRoot' );
768
- *
769
- * editor.disableRoot( 'myRoot', 'my-lock' );
770
- * model.canEditAt( myRoot ); // `false`
771
- *
772
- * editor.disableRoot( 'myRoot', 'other-lock' );
773
- * editor.disableRoot( 'myRoot', 'other-lock' ); // Multiple locks with the same ID have no effect.
774
- * model.canEditAt( myRoot ); // `false`
775
- *
776
- * editor.enableRoot( 'myRoot', 'my-lock' );
777
- * model.canEditAt( myRoot ); // `false`
778
- *
779
- * editor.enableRoot( 'myRoot', 'other-lock' );
780
- * model.canEditAt( myRoot ); // `true`
781
- * ```
782
- *
783
- * See also {@link module:core/editor/editor~Editor#enableReadOnlyMode `Editor#enableReadOnlyMode()`} and
784
- * {@link module:editor-multi-root/multirooteditor~MultiRootEditor#enableRoot `MultiRootEditor#enableRoot()`}.
785
- *
786
- * @param rootName Name of the root to switch to read-only mode.
787
- * @param lockId A unique ID for setting the editor to the read-only state.
788
- */ disableRoot(rootName, lockId) {
789
- if (rootName == '$graveyard') {
790
- /**
791
- * You cannot disable the `$graveyard` root.
792
- *
793
- * @error multi-root-editor-cannot-disable-graveyard-root
794
- */ throw new CKEditorError('multi-root-editor-cannot-disable-graveyard-root', this);
795
- }
796
- const locksForGivenRoot = this._readOnlyRootLocks.get(rootName);
797
- if (locksForGivenRoot) {
798
- locksForGivenRoot.add(lockId);
799
- } else {
800
- this._readOnlyRootLocks.set(rootName, new Set([
801
- lockId
802
- ]));
803
- const editableRootElement = this.editing.view.document.getRoot(rootName);
804
- editableRootElement.isReadOnly = true;
805
- // Since one of the roots has changed read-only state, we need to refresh all commands that affect data.
806
- Array.from(this.commands.commands()).forEach((command)=>command.affectsData && command.refresh());
807
- }
808
- }
809
- /**
810
- * Removes given read-only lock from the given root.
811
- *
812
- * See {@link module:editor-multi-root/multirooteditor~MultiRootEditor#disableRoot `disableRoot()`}.
813
- *
814
- * @param rootName Name of the root to switch back from the read-only mode.
815
- * @param lockId A unique ID for setting the editor to the read-only state.
816
- */ enableRoot(rootName, lockId) {
817
- const locksForGivenRoot = this._readOnlyRootLocks.get(rootName);
818
- if (!locksForGivenRoot || !locksForGivenRoot.has(lockId)) {
819
- return;
820
- }
821
- if (locksForGivenRoot.size === 1) {
822
- this._readOnlyRootLocks.delete(rootName);
823
- const editableRootElement = this.editing.view.document.getRoot(rootName);
824
- editableRootElement.isReadOnly = this.isReadOnly;
825
- // Since one of the roots has changed read-only state, we need to refresh all commands that affect data.
826
- Array.from(this.commands.commands()).forEach((command)=>command.affectsData && command.refresh());
827
- } else {
828
- locksForGivenRoot.delete(lockId);
829
- }
830
- }
831
- static async create(sourceElementsOrDataOrConfig, config = {}) {
832
- const editor = new this(sourceElementsOrDataOrConfig, config);
833
- await editor.initPlugins();
834
- // Roots are created in the editor constructor (before plugins are loaded), but the schema is only fully
835
- // built after plugins register their items during init(). Custom root element names (e.g. registered by a
836
- // plugin) may not exist in the schema at construction time, so we defer this check until here.
837
- verifyRootElements(editor);
838
- await editor.ui.init();
839
- const initialData = extractRootsConfigField(editor.config.get('roots'), 'initialData');
840
- // This is checked directly before setting the initial data,
841
- // as plugins may change `EditorConfig#initialData` value.
842
- editor._verifyRootsWithInitialData(initialData);
843
- await editor.data.init(initialData);
844
- editor.fire('ready');
845
- return editor;
846
- }
847
- /**
848
- * @internal
849
- */ _verifyRootsWithInitialData(initialData) {
850
- // Roots that are not in the initial data.
851
- for (const rootName of this.model.document.getRootNames()){
852
- if (!(rootName in initialData)) {
853
- /**
854
- * Editor roots do not match the
855
- * {@link module:core/editor/editorconfig~EditorConfig#initialData `initialData` configuration}.
856
- *
857
- * This may happen for one of the two reasons:
858
- *
859
- * * Configuration error. The `sourceElementsOrData` parameter in
860
- * {@link module:editor-multi-root/multirooteditor~MultiRootEditor.create `MultiRootEditor.create()`} contains different
861
- * roots than {@link module:core/editor/editorconfig~EditorConfig#initialData `initialData` configuration}.
862
- * * As the editor was initialized, the {@link module:core/editor/editorconfig~EditorConfig#initialData `initialData`}
863
- * configuration value or the state of the editor roots has been changed.
864
- *
865
- * @error multi-root-editor-root-initial-data-mismatch
866
- */ throw new CKEditorError('multi-root-editor-root-initial-data-mismatch', null);
867
- }
868
- }
869
- // Roots that are not in the editor.
870
- for (const rootName of Object.keys(initialData)){
871
- const root = this.model.document.getRoot(rootName);
872
- if (!root || !root.isAttached()) {
873
- throw new CKEditorError('multi-root-editor-root-initial-data-mismatch', null);
874
- }
875
- }
876
- }
877
- }
251
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
252
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
253
+ */
878
254
  /**
879
- * Returns names of roots that are not lazy-loaded.
880
- */ function getNonLazyLoadRootsNames(rootsConfig) {
881
- return rootsConfig.filter(([, { lazyLoad }])=>!lazyLoad).map(([rootName])=>rootName);
255
+ * @module editor-multi-root/multirooteditor
256
+ */
257
+ /**
258
+ * The multi-root editor implementation.
259
+ *
260
+ * The multi-root editor provides multiple inline editable elements and a toolbar. All editable areas are controlled by one editor
261
+ * instance, which means that they share common configuration, document ID, or undo stack.
262
+ *
263
+ * This type of editor is dedicated to integrations which require a customized UI with an open structure, featuring multiple editable areas,
264
+ * allowing developers to have a control over the exact location of these editable areas.
265
+ *
266
+ * In order to create a multi-root editor instance, use the static
267
+ * {@link module:editor-multi-root/multirooteditor~MultiRootEditor.create `MultiRootEditor.create()`} method.
268
+ *
269
+ * Note that you will need to attach the editor toolbar to your web page manually, in a desired place, after the editor is initialized.
270
+ */
271
+ var MultiRootEditor = class extends Editor {
272
+ /**
273
+ * @inheritDoc
274
+ */
275
+ static get editorName() {
276
+ return "MultiRootEditor";
277
+ }
278
+ /**
279
+ * @inheritDoc
280
+ */
281
+ ui;
282
+ /**
283
+ * The elements on which the editor has been initialized.
284
+ */
285
+ sourceElements;
286
+ /**
287
+ * A set of lock IDs for enabling or disabling particular root.
288
+ */
289
+ _readOnlyRootLocks = /* @__PURE__ */ new Map();
290
+ constructor(sourceElementsOrDataOrConfig, config = {}) {
291
+ const { sourceElementsOrData, editorConfig } = normalizeMultiRootEditorConstructorParams(sourceElementsOrDataOrConfig, config);
292
+ super(editorConfig);
293
+ normalizeRootsConfig(sourceElementsOrData, this.config, false);
294
+ normalizeRootsAttributesConfig(this.config);
295
+ normalizeRootEditableOptionsConfig(this.config);
296
+ if (this.config.get("lazyRoots"))
297
+ /**
298
+ * Using deprecated `config.lazyRoots` configuration option.
299
+ * Use `config.roots.<rootName>.lazyLoad` instead.
300
+ *
301
+ * @error multi-root-editor-root-deprecated-config-lazy-roots
302
+ */
303
+ throw new CKEditorError("multi-root-editor-root-deprecated-config-lazy-roots", null);
304
+ const rootsConfig = Object.entries(this.config.get("roots"));
305
+ this.sourceElements = {};
306
+ const editableElements = {};
307
+ for (const [rootName, rootConfig] of rootsConfig) {
308
+ const editableElement = getRootEditableElement(rootConfig);
309
+ if (!editableElement) continue;
310
+ if (isElement$1(editableElement)) {
311
+ this.sourceElements[rootName] = editableElement;
312
+ secureSourceElement(this, editableElement);
313
+ }
314
+ editableElements[rootName] = editableElement;
315
+ }
316
+ this.editing.view.document.roots.on("add", (evt, viewRoot) => {
317
+ viewRoot.unbind("isReadOnly");
318
+ viewRoot.bind("isReadOnly").to(this.editing.view.document, "isReadOnly", (isReadOnly) => {
319
+ return isReadOnly || this._readOnlyRootLocks.has(viewRoot.rootName);
320
+ });
321
+ viewRoot.on("change:isReadOnly", (evt, prop, value) => {
322
+ const viewRange = this.editing.view.createRangeIn(viewRoot);
323
+ for (const viewItem of viewRange.getItems()) if (viewItem.is("editableElement")) {
324
+ viewItem.unbind("isReadOnly");
325
+ viewItem.isReadOnly = value;
326
+ }
327
+ });
328
+ });
329
+ for (const [rootName, rootConfig] of rootsConfig) {
330
+ const root = this.model.document.createRoot(rootConfig.modelElement, rootName);
331
+ if (rootConfig.lazyLoad) root._isLoaded = false;
332
+ }
333
+ this.registerRootAttribute("$rootEditableOptions");
334
+ registerAndInitializeRootConfigAttributes(this);
335
+ const options = {
336
+ shouldToolbarGroupWhenFull: !this.config.get("toolbar.shouldNotGroupWhenFull"),
337
+ editableElements,
338
+ label: extractRootsConfigField(this.config.get("roots"), "label")
339
+ };
340
+ const view = new MultiRootEditorUIView(this.locale, this.editing.view, getNonLazyLoadRootsNames(rootsConfig), options);
341
+ this.ui = new MultiRootEditorUI(this, view);
342
+ this.model.document.on("change:data", () => {
343
+ const changedRoots = this.model.document.differ.getChangedRoots();
344
+ for (const changes of changedRoots) {
345
+ const root = this.model.document.getRoot(changes.name);
346
+ if (changes.state == "detached") this.fire("detachRoot", root);
347
+ }
348
+ for (const changes of changedRoots) {
349
+ const root = this.model.document.getRoot(changes.name);
350
+ if (changes.state == "attached") this.fire("addRoot", root);
351
+ }
352
+ });
353
+ this.listenTo(this.model, "canEditAt", (evt, [selection]) => {
354
+ if (!selection) return;
355
+ let selectionInReadOnlyRoot = false;
356
+ for (const range of selection.getRanges()) {
357
+ const root = range.root;
358
+ if (this._readOnlyRootLocks.has(root.rootName)) {
359
+ selectionInReadOnlyRoot = true;
360
+ break;
361
+ }
362
+ }
363
+ if (selectionInReadOnlyRoot) {
364
+ evt.return = false;
365
+ evt.stop();
366
+ }
367
+ }, { priority: "high" });
368
+ this.decorate("loadRoot");
369
+ this.on("loadRoot", (evt, [rootName]) => {
370
+ const root = this.model.document.getRoot(rootName);
371
+ if (!root)
372
+ /**
373
+ * The root to load does not exist.
374
+ *
375
+ * @error multi-root-editor-load-root-no-root
376
+ */
377
+ throw new CKEditorError("multi-root-editor-load-root-no-root", this, { rootName });
378
+ if (root._isLoaded) {
379
+ /**
380
+ * The root to load was already loaded before. The `loadRoot()` call has no effect.
381
+ *
382
+ * @error multi-root-editor-load-root-already-loaded
383
+ */
384
+ logWarning("multi-root-editor-load-root-already-loaded");
385
+ evt.stop();
386
+ }
387
+ }, { priority: "highest" });
388
+ verifyLicenseKey(this);
389
+ function verifyLicenseKey(editor) {
390
+ const decodedPayload = decodeLicenseKey(editor.config.get("licenseKey"));
391
+ if (!decodedPayload) return;
392
+ if (isFeatureBlockedByLicenseKey(decodedPayload, "MRE")) {
393
+ editor.enableReadOnlyMode(Symbol("invalidLicense"));
394
+ editor._showLicenseError("featureNotAllowed", "Multi-root editor");
395
+ }
396
+ }
397
+ }
398
+ /**
399
+ * Destroys the editor instance, releasing all resources used by it.
400
+ *
401
+ * Updates the original editor element with the data if the
402
+ * {@link module:core/editor/editorconfig~EditorConfig#updateSourceElementOnDestroy `updateSourceElementOnDestroy`}
403
+ * configuration option is set to `true`.
404
+ *
405
+ * **Note**: The multi-root editor does not remove the toolbar and editable when destroyed. You can
406
+ * do that yourself in the destruction chain, if you need to:
407
+ *
408
+ * ```ts
409
+ * editor.destroy().then( () => {
410
+ * // Remove the toolbar from DOM.
411
+ * editor.ui.view.toolbar.element.remove();
412
+ *
413
+ * // Remove editable elements from DOM.
414
+ * for ( const editable of Object.values( editor.ui.view.editables ) ) {
415
+ * editable.element.remove();
416
+ * }
417
+ *
418
+ * console.log( 'Editor was destroyed' );
419
+ * } );
420
+ * ```
421
+ */
422
+ async destroy() {
423
+ const shouldUpdateSourceElement = this.config.get("updateSourceElementOnDestroy");
424
+ const data = {};
425
+ for (const rootName of Object.keys(this.sourceElements)) data[rootName] = shouldUpdateSourceElement ? this.getData({ rootName }) : "";
426
+ this.ui.destroy();
427
+ await super.destroy();
428
+ for (const rootName of Object.keys(this.sourceElements)) setDataInElement(this.sourceElements[rootName], data[rootName]);
429
+ }
430
+ addRoot(rootName, options = {}) {
431
+ const initialData = options.initialData || options.data || "";
432
+ const modelAttributes = { ...options.modelAttributes || options.attributes };
433
+ const modelElement = options.modelElement || options.elementName || "$root";
434
+ if (!this.model.schema.isLimit(modelElement))
435
+ /**
436
+ * The model root element must be a {@link module:engine/model/schema~ModelSchemaItemDefinition#isLimit limit element}.
437
+ * The element name specified in
438
+ * {@link module:editor-multi-root/multirooteditor~MultiRootEditor#addRoot:ROOT_CONFIG `addRoot()`}
439
+ * options must be registered in the schema
440
+ * with `isLimit` set to `true`.
441
+ *
442
+ * @error multi-root-editor-add-root-element-is-not-limit
443
+ * @param rootName The name of the root that uses a non-limit element.
444
+ * @param elementName The name of the model element used for the root.
445
+ */
446
+ throw new CKEditorError("multi-root-editor-add-root-element-is-not-limit", this, {
447
+ rootName,
448
+ elementName: modelElement
449
+ });
450
+ if (isElement$1(options.element))
451
+ /**
452
+ * Passing an existing DOM element as the `element` option of
453
+ * {@link module:editor-multi-root/multirooteditor~MultiRootEditor#addRoot:ROOT_CONFIG `addRoot()`}
454
+ * is not supported and will be ignored. The
455
+ * `addRoot()` method only registers the model root; the DOM editable is created later by
456
+ * {@link module:editor-multi-root/multirooteditor~MultiRootEditor#createEditable `createEditable()`}.
457
+ *
458
+ * Pass a tag name string (e.g. `'h1'`) or a
459
+ * {@link module:engine/view/elementdefinition~ViewElementDefinition view element definition}
460
+ * instead, or omit the option to create a default `<div>`.
461
+ *
462
+ * @error multi-root-editor-add-root-element-option-ignored
463
+ */
464
+ logWarning("multi-root-editor-add-root-element-option-ignored");
465
+ setRootEditableOptions(modelAttributes, options);
466
+ if (options.description != null && !("$description" in modelAttributes)) modelAttributes.$description = options.description;
467
+ if (options.title != null && !("$title" in modelAttributes)) modelAttributes.$title = options.title;
468
+ const _addRoot = (writer) => {
469
+ const root = writer.addRoot(rootName, modelElement);
470
+ if (initialData) writer.insert(this.data.parse(initialData, root), root, 0);
471
+ for (const key of Object.keys(modelAttributes)) {
472
+ this.registerRootAttribute(key);
473
+ writer.setAttribute(key, modelAttributes[key], root);
474
+ }
475
+ };
476
+ if (options.isUndoable) this.model.change(_addRoot);
477
+ else this.model.enqueueChange({ isUndoable: false }, _addRoot);
478
+ }
479
+ /**
480
+ * Detaches a root from the editor.
481
+ *
482
+ * ```ts
483
+ * editor.detachRoot( 'myRoot' );
484
+ * ```
485
+ *
486
+ * A detached root is not entirely removed from the editor model, however it can be considered removed.
487
+ *
488
+ * After a root is detached all its children are removed, all markers inside it are removed, and whenever something is inserted to it,
489
+ * it is automatically removed as well. Finally, a detached root is not returned by
490
+ * {@link module:engine/model/document~ModelDocument#getRootNames} by default.
491
+ *
492
+ * It is possible to re-add a previously detached root calling {@link #addRoot}.
493
+ *
494
+ * Whenever a root is detached, the editor instance will fire {@link #event:detachRoot `detachRoot` event}. The event is also
495
+ * called when the root is detached indirectly, e.g. by the undo feature or on a remote client during real-time collaboration.
496
+ *
497
+ * Note, that this method only detached a root in the editor model. It **does not** destroy the DOM editable element linked with
498
+ * the root and it **does not** remove the DOM element from the DOM structure of your application.
499
+ *
500
+ * To properly remove a DOM editable element after a root was detached, listen to {@link #event:detachRoot `detachRoot` event}
501
+ * and call {@link #detachEditable}. Then, remove the DOM element from your application.
502
+ *
503
+ * ```ts
504
+ * editor.on( 'detachRoot', ( evt, root ) => {
505
+ * const editableElement = editor.detachEditable( root );
506
+ *
507
+ * // You may want to do an additional DOM clean-up here.
508
+ *
509
+ * editableElement.remove();
510
+ * } );
511
+ *
512
+ * // ...
513
+ *
514
+ * editor.detachRoot( 'myRoot' ); // Will detach the root, and remove the DOM editable element.
515
+ * ```
516
+ *
517
+ * By setting `isUndoable` flag to `true`, you can allow for re-adding the root using the undo feature.
518
+ *
519
+ * Additionally, you can group detaching multiple roots in one undo step. This can be useful if the roots are combined into one,
520
+ * bigger UI element, and you want them all to be re-added together.
521
+ *
522
+ * ```ts
523
+ * editor.model.change( () => {
524
+ * editor.detachRoot( 'left-row-3', true );
525
+ * editor.detachRoot( 'center-row-3', true );
526
+ * editor.detachRoot( 'right-row-3', true );
527
+ * } );
528
+ * ```
529
+ *
530
+ * @param rootName Name of the root to detach.
531
+ * @param isUndoable Whether detaching the root can be undone (using the undo feature) or not.
532
+ */
533
+ detachRoot(rootName, isUndoable = false) {
534
+ if (isUndoable) this.model.change((writer) => writer.detachRoot(rootName));
535
+ else this.model.enqueueChange({ isUndoable: false }, (writer) => writer.detachRoot(rootName));
536
+ }
537
+ createEditable(root, optionsOrPlaceholder, label) {
538
+ let placeholder;
539
+ let element;
540
+ if (!optionsOrPlaceholder || typeof optionsOrPlaceholder === "string") placeholder = optionsOrPlaceholder;
541
+ else {
542
+ placeholder = optionsOrPlaceholder?.placeholder;
543
+ label = optionsOrPlaceholder?.label;
544
+ element = optionsOrPlaceholder?.element;
545
+ }
546
+ const rootEditableConfig = root.getAttribute("$rootEditableOptions") || {};
547
+ const editableElement = normalizeViewRootElementDefinition(element || rootEditableConfig.element);
548
+ const editable = this.ui.view.createEditable(root.rootName, editableElement, label || rootEditableConfig.label);
549
+ editable.isInlineRoot = !rootAcceptsBlocks(this, root.rootName);
550
+ this.ui.addEditable(editable, placeholder || rootEditableConfig.placeholder);
551
+ this.editing.view.forceRender();
552
+ return editable.element;
553
+ }
554
+ /**
555
+ * Detaches the DOM editable element that was attached to the given root.
556
+ *
557
+ * @param root Root for which the editable element should be detached.
558
+ * @returns The DOM element that was detached. You may want to remove it from your application DOM structure.
559
+ */
560
+ detachEditable(root) {
561
+ const rootName = root.rootName;
562
+ const editable = this.ui.view.editables[rootName];
563
+ this.ui.removeEditable(editable);
564
+ this.ui.view.removeEditable(rootName);
565
+ return editable.element;
566
+ }
567
+ /**
568
+ * Loads a root that has previously been declared in
569
+ * {@link module:core/editor/editorconfig~EditorConfig#roots `config.roots.<rootName>.lazyLoad`} configuration option.
570
+ *
571
+ * **Important! Lazy roots loading is an experimental feature, and may become deprecated. Be advised of the following
572
+ * known limitations:**
573
+ *
574
+ * * **Real-time collaboration integrations that use
575
+ * [uploaded editor bundles](https://ckeditor.com/docs/cs/latest/guides/collaboration/editor-bundle.html) are not supported. Using
576
+ * lazy roots will lead to unexpected behavior and data loss.**
577
+ * * **Revision history feature will read and process the whole document on editor initialization, possibly defeating the purpose
578
+ * of using the lazy roots loading. Additionally, when the document is loaded for the first time, all roots need to be loaded,
579
+ * to make sure that the initial revision data includes all roots. Otherwise, you may experience data loss.**
580
+ * * **Multiple features, that require full document data to be loaded, will produce incorrect or confusing results if not all
581
+ * roots are loaded. These include: bookmarks, find and replace, word count, pagination, document exports, document outline,
582
+ * and table of contents.**
583
+ *
584
+ * Only roots specified in the editor config can be loaded. A root cannot be loaded multiple times. A root cannot be unloaded and
585
+ * loading a root cannot be reverted using the undo feature.
586
+ *
587
+ * When a root becomes loaded, it will be treated by the editor as though it was just added. This, among others, means that all
588
+ * related events and mechanisms will be fired, including {@link ~MultiRootEditor#event:addRoot `addRoot` event},
589
+ * {@link module:engine/model/document~ModelDocument#event:change `model.Document` `change` event}, model post-fixers and conversion.
590
+ *
591
+ * Until the root becomes loaded, all above mechanisms are suppressed.
592
+ *
593
+ * This method is {@link module:utils/observablemixin~Observable#decorate decorated}.
594
+ *
595
+ * Note that attributes loaded together with a root are automatically registered.
596
+ *
597
+ * See also {@link ~MultiRootEditor#registerRootAttribute `MultiRootEditor#registerRootAttribute()`} and
598
+ * {@link module:core/editor/editorconfig~EditorConfig#roots `config.roots.<rootName>.modelAttributes` configuration option}.
599
+ *
600
+ * When this method is used in real-time collaboration environment, its effects become asynchronous as the editor will first synchronize
601
+ * with the remote editing session, before the root is added to the editor.
602
+ *
603
+ * If the root has been already loaded by any other client, the additional data passed in `loadRoot()` parameters will be ignored.
604
+ *
605
+ * @param rootName Name of the root to load.
606
+ * @param options Additional options for the loaded root.
607
+ * @fires loadRoot
608
+ */
609
+ loadRoot(rootName, { data = "", attributes = {} } = {}) {
610
+ const root = this.model.document.getRoot(rootName);
611
+ this.model.enqueueChange({ isUndoable: false }, (writer) => {
612
+ if (data) writer.insert(this.data.parse(data, root), root, 0);
613
+ for (const key of Object.keys(attributes)) {
614
+ this.registerRootAttribute(key);
615
+ writer.setAttribute(key, attributes[key], root);
616
+ }
617
+ root._isLoaded = true;
618
+ this.model.document.differ._bufferRootLoad(root);
619
+ });
620
+ }
621
+ /**
622
+ * Returns the document data for all attached roots.
623
+ *
624
+ * @param options Additional configuration for the retrieved data.
625
+ * Editor features may introduce more configuration options that can be set through this parameter.
626
+ * @param options.trim Whether returned data should be trimmed. This option is set to `'empty'` by default,
627
+ * which means that whenever editor content is considered empty, an empty string is returned. To turn off trimming
628
+ * use `'none'`. In such cases exact content will be returned (for example `'<p>&nbsp;</p>'` for an empty editor).
629
+ * @returns The full document data.
630
+ */
631
+ getFullData(options) {
632
+ const data = {};
633
+ for (const rootName of this.model.document.getRootNames()) data[rootName] = this.data.get({
634
+ ...options,
635
+ rootName
636
+ });
637
+ return data;
638
+ }
639
+ /**
640
+ * Returns attributes for all attached roots.
641
+ *
642
+ * Note: all and only {@link ~MultiRootEditor#registerRootAttribute registered} roots attributes will be returned.
643
+ * If a registered root attribute is not set for a given root, `null` will be returned.
644
+ *
645
+ * @returns Object with roots attributes. Keys are roots names, while values are attributes set on given root.
646
+ */
647
+ getRootsAttributes() {
648
+ const rootsAttributes = {};
649
+ for (const rootName of this.model.document.getRootNames()) rootsAttributes[rootName] = this.getRootAttributes(rootName);
650
+ return rootsAttributes;
651
+ }
652
+ /**
653
+ * Switches given editor root to the read-only mode.
654
+ *
655
+ * In contrary to {@link module:core/editor/editor~Editor#enableReadOnlyMode `enableReadOnlyMode()`}, which switches the whole editor
656
+ * to the read-only mode, this method turns only a particular root to the read-only mode. This can be useful when you want to prevent
657
+ * editing only a part of the editor content.
658
+ *
659
+ * When you switch a root to the read-only mode, you need provide a unique identifier (`lockId`) that will identify this request. You
660
+ * will need to provide the same `lockId` when you will want to
661
+ * {@link module:editor-multi-root/multirooteditor~MultiRootEditor#enableRoot re-enable} the root.
662
+ *
663
+ * ```ts
664
+ * const model = editor.model;
665
+ * const myRoot = model.document.getRoot( 'myRoot' );
666
+ *
667
+ * editor.disableRoot( 'myRoot', 'my-lock' );
668
+ * model.canEditAt( myRoot ); // `false`
669
+ *
670
+ * editor.disableRoot( 'myRoot', 'other-lock' );
671
+ * editor.disableRoot( 'myRoot', 'other-lock' ); // Multiple locks with the same ID have no effect.
672
+ * model.canEditAt( myRoot ); // `false`
673
+ *
674
+ * editor.enableRoot( 'myRoot', 'my-lock' );
675
+ * model.canEditAt( myRoot ); // `false`
676
+ *
677
+ * editor.enableRoot( 'myRoot', 'other-lock' );
678
+ * model.canEditAt( myRoot ); // `true`
679
+ * ```
680
+ *
681
+ * See also {@link module:core/editor/editor~Editor#enableReadOnlyMode `Editor#enableReadOnlyMode()`} and
682
+ * {@link module:editor-multi-root/multirooteditor~MultiRootEditor#enableRoot `MultiRootEditor#enableRoot()`}.
683
+ *
684
+ * @param rootName Name of the root to switch to read-only mode.
685
+ * @param lockId A unique ID for setting the editor to the read-only state.
686
+ */
687
+ disableRoot(rootName, lockId) {
688
+ if (rootName == "$graveyard")
689
+ /**
690
+ * You cannot disable the `$graveyard` root.
691
+ *
692
+ * @error multi-root-editor-cannot-disable-graveyard-root
693
+ */
694
+ throw new CKEditorError("multi-root-editor-cannot-disable-graveyard-root", this);
695
+ const locksForGivenRoot = this._readOnlyRootLocks.get(rootName);
696
+ if (locksForGivenRoot) locksForGivenRoot.add(lockId);
697
+ else {
698
+ this._readOnlyRootLocks.set(rootName, new Set([lockId]));
699
+ const editableRootElement = this.editing.view.document.getRoot(rootName);
700
+ editableRootElement.isReadOnly = true;
701
+ Array.from(this.commands.commands()).forEach((command) => command.affectsData && command.refresh());
702
+ }
703
+ }
704
+ /**
705
+ * Removes given read-only lock from the given root.
706
+ *
707
+ * See {@link module:editor-multi-root/multirooteditor~MultiRootEditor#disableRoot `disableRoot()`}.
708
+ *
709
+ * @param rootName Name of the root to switch back from the read-only mode.
710
+ * @param lockId A unique ID for setting the editor to the read-only state.
711
+ */
712
+ enableRoot(rootName, lockId) {
713
+ const locksForGivenRoot = this._readOnlyRootLocks.get(rootName);
714
+ if (!locksForGivenRoot || !locksForGivenRoot.has(lockId)) return;
715
+ if (locksForGivenRoot.size === 1) {
716
+ this._readOnlyRootLocks.delete(rootName);
717
+ const editableRootElement = this.editing.view.document.getRoot(rootName);
718
+ editableRootElement.isReadOnly = this.isReadOnly;
719
+ Array.from(this.commands.commands()).forEach((command) => command.affectsData && command.refresh());
720
+ } else locksForGivenRoot.delete(lockId);
721
+ }
722
+ static async create(sourceElementsOrDataOrConfig, config = {}) {
723
+ const editor = new this(sourceElementsOrDataOrConfig, config);
724
+ await editor.initPlugins();
725
+ verifyRootElements(editor);
726
+ await editor.ui.init();
727
+ const initialData = extractRootsConfigField(editor.config.get("roots"), "initialData");
728
+ editor._verifyRootsWithInitialData(initialData);
729
+ await editor.data.init(initialData);
730
+ editor.fire("ready");
731
+ return editor;
732
+ }
733
+ /**
734
+ * @internal
735
+ */
736
+ _verifyRootsWithInitialData(initialData) {
737
+ for (const rootName of this.model.document.getRootNames()) if (!(rootName in initialData))
738
+ /**
739
+ * Editor roots do not match the
740
+ * {@link module:core/editor/editorconfig~EditorConfig#initialData `initialData` configuration}.
741
+ *
742
+ * This may happen for one of the two reasons:
743
+ *
744
+ * * Configuration error. The `sourceElementsOrData` parameter in
745
+ * {@link module:editor-multi-root/multirooteditor~MultiRootEditor.create `MultiRootEditor.create()`} contains different
746
+ * roots than {@link module:core/editor/editorconfig~EditorConfig#initialData `initialData` configuration}.
747
+ * * As the editor was initialized, the {@link module:core/editor/editorconfig~EditorConfig#initialData `initialData`}
748
+ * configuration value or the state of the editor roots has been changed.
749
+ *
750
+ * @error multi-root-editor-root-initial-data-mismatch
751
+ */
752
+ throw new CKEditorError("multi-root-editor-root-initial-data-mismatch", null);
753
+ for (const rootName of Object.keys(initialData)) {
754
+ const root = this.model.document.getRoot(rootName);
755
+ if (!root || !root.isAttached()) throw new CKEditorError("multi-root-editor-root-initial-data-mismatch", null);
756
+ }
757
+ }
758
+ };
759
+ /**
760
+ * Returns names of roots that are not lazy-loaded.
761
+ */
762
+ function getNonLazyLoadRootsNames(rootsConfig) {
763
+ return rootsConfig.filter(([, { lazyLoad }]) => !lazyLoad).map(([rootName]) => rootName);
882
764
  }
883
765
  /**
884
- * Collects a specific field from the editor configuration for all roots.
885
- * Returns a simple map of root names and values of the specified field.
886
- * If a root does not have the specified field defined, it is not included in the returned object.
887
- */ function extractRootsConfigField(rootsConfig, key) {
888
- return Object.fromEntries(Object.entries(rootsConfig).filter(([, config])=>!config.lazyLoad).map(([rootName, config])=>[
889
- rootName,
890
- config[key]
891
- ]).filter(([, value])=>value !== undefined));
766
+ * Collects a specific field from the editor configuration for all roots.
767
+ * Returns a simple map of root names and values of the specified field.
768
+ * If a root does not have the specified field defined, it is not included in the returned object.
769
+ */
770
+ function extractRootsConfigField(rootsConfig, key) {
771
+ return Object.fromEntries(Object.entries(rootsConfig).filter(([, config]) => !config.lazyLoad).map(([rootName, config]) => [rootName, config[key]]).filter(([, value]) => value !== void 0));
892
772
  }
893
773
  /**
894
- * Normalize legacy `config.rootsAttributes` config option to `config.roots.<rootName>.modelAttributes`.
895
- */ function normalizeRootsAttributesConfig(config) {
896
- if (config.get('rootsAttributes')) {
897
- const rootsAttributes = config.get('rootsAttributes');
898
- const rootsConfig = config.get('roots');
899
- for (const [rootName, attributes] of Object.entries(rootsAttributes)){
900
- const rootConfig = rootsConfig[rootName];
901
- if (!rootConfig) {
902
- /**
903
- * Trying to set attributes on a non-existing root.
904
- *
905
- * Roots specified in `config.rootsAttributes` do not match initial editor roots.
906
- *
907
- * @error multi-root-editor-root-attributes-no-root
908
- */ throw new CKEditorError('multi-root-editor-root-attributes-no-root', null);
909
- }
910
- if (Object.keys(rootConfig.modelAttributes || {}).length) {
911
- /**
912
- * Trying to set attributes using deprecated `config.rootsAttributes` on a root that has already
913
- * defined attributes using `config.roots.<rootName>.modelAttributes`.
914
- *
915
- * @error multi-root-editor-root-attributes-conflict
916
- */ throw new CKEditorError('multi-root-editor-root-attributes-conflict', null);
917
- }
918
- config.set(`roots.${rootName}.modelAttributes`, attributes);
919
- }
920
- }
774
+ * Normalize legacy `config.rootsAttributes` config option to `config.roots.<rootName>.modelAttributes`.
775
+ */
776
+ function normalizeRootsAttributesConfig(config) {
777
+ if (config.get("rootsAttributes")) {
778
+ const rootsAttributes = config.get("rootsAttributes");
779
+ const rootsConfig = config.get("roots");
780
+ for (const [rootName, attributes] of Object.entries(rootsAttributes)) {
781
+ const rootConfig = rootsConfig[rootName];
782
+ if (!rootConfig)
783
+ /**
784
+ * Trying to set attributes on a non-existing root.
785
+ *
786
+ * Roots specified in `config.rootsAttributes` do not match initial editor roots.
787
+ *
788
+ * @error multi-root-editor-root-attributes-no-root
789
+ */
790
+ throw new CKEditorError("multi-root-editor-root-attributes-no-root", null);
791
+ if (Object.keys(rootConfig.modelAttributes || {}).length)
792
+ /**
793
+ * Trying to set attributes using deprecated `config.rootsAttributes` on a root that has already
794
+ * defined attributes using `config.roots.<rootName>.modelAttributes`.
795
+ *
796
+ * @error multi-root-editor-root-attributes-conflict
797
+ */
798
+ throw new CKEditorError("multi-root-editor-root-attributes-conflict", null);
799
+ config.set(`roots.${rootName}.modelAttributes`, attributes);
800
+ }
801
+ }
921
802
  }
922
803
  /**
923
- * Normalize `placeholder` and `label` from `config.roots.<rootName>` into the `$rootEditableOptions` root model attribute,
924
- * stored under `config.roots.<rootName>.modelAttributes`. This way the attribute is registered, set on initial data load
925
- * and shipped through RTC initial-data path together with the rest of `modelAttributes`.
926
- *
927
- * This is also required by the revision history feature: on editor load, RH compares the latest revision data against
928
- * `initialData` and `modelAttributes` passed to the editor and logs a warning if they do not match. Because `$rootEditableOptions`
929
- * ends up in the revision data, it must also be present in `modelAttributes` (even as an empty object when no options
930
- * are configured), otherwise the comparison reports a spurious mismatch.
931
- */ function normalizeRootEditableOptionsConfig(config) {
932
- const rootsConfig = config.get('roots');
933
- for (const [rootName, rootConfig] of Object.entries(rootsConfig)){
934
- if (rootConfig.modelAttributes?.$rootEditableOptions) {
935
- continue;
936
- }
937
- const modelAttributes = {
938
- ...rootConfig.modelAttributes
939
- };
940
- setRootEditableOptions(modelAttributes, rootConfig);
941
- config.set(`roots.${rootName}.modelAttributes`, modelAttributes);
942
- }
804
+ * Normalize `placeholder` and `label` from `config.roots.<rootName>` into the `$rootEditableOptions` root model attribute,
805
+ * stored under `config.roots.<rootName>.modelAttributes`. This way the attribute is registered, set on initial data load
806
+ * and shipped through RTC initial-data path together with the rest of `modelAttributes`.
807
+ *
808
+ * This is also required by the revision history feature: on editor load, RH compares the latest revision data against
809
+ * `initialData` and `modelAttributes` passed to the editor and logs a warning if they do not match. Because `$rootEditableOptions`
810
+ * ends up in the revision data, it must also be present in `modelAttributes` (even as an empty object when no options
811
+ * are configured), otherwise the comparison reports a spurious mismatch.
812
+ */
813
+ function normalizeRootEditableOptionsConfig(config) {
814
+ const rootsConfig = config.get("roots");
815
+ for (const [rootName, rootConfig] of Object.entries(rootsConfig)) {
816
+ if (rootConfig.modelAttributes?.$rootEditableOptions) continue;
817
+ const modelAttributes = { ...rootConfig.modelAttributes };
818
+ setRootEditableOptions(modelAttributes, rootConfig);
819
+ config.set(`roots.${rootName}.modelAttributes`, modelAttributes);
820
+ }
943
821
  }
944
822
  /**
945
- * Mutates the given `modelAttributes` map by adding the `$rootEditableOptions` entry derived from `placeholder`, `label`
946
- * and `element`. If `$rootEditableOptions` is already present, the map is left untouched.
947
- *
948
- * The `element` is normalized into canonical form ({@link module:core/editor/editorconfig~ViewRootElementDefinition})
949
- * before being persisted. A raw DOM element is local to this editor instance - it cannot be replicated through
950
- * RTC, so it is silently dropped here. Callers that want to surface a warning (e.g. `addRoot()`) should do so before
951
- * invoking this function.
952
- */ function setRootEditableOptions(modelAttributes, { placeholder, label, element }) {
953
- if ('$rootEditableOptions' in modelAttributes) {
954
- return;
955
- }
956
- // In the `else` branch `element` cannot be an `HTMLElement`, but the normalizer's return type still
957
- // includes it the cast narrows it back to the canonical descriptor form.
958
- const storageElement = isElement(element) ? undefined : normalizeViewRootElementDefinition(element);
959
- modelAttributes.$rootEditableOptions = {
960
- ...placeholder && {
961
- placeholder
962
- },
963
- ...label && {
964
- label
965
- },
966
- ...storageElement && {
967
- element: storageElement
968
- }
969
- };
823
+ * Mutates the given `modelAttributes` map by adding the `$rootEditableOptions` entry derived from `placeholder`, `label`
824
+ * and `element`. If `$rootEditableOptions` is already present, the map is left untouched.
825
+ *
826
+ * The `element` is normalized into canonical form ({@link module:core/editor/editorconfig~ViewRootElementDefinition})
827
+ * before being persisted. A raw DOM element is local to this editor instance - it cannot be replicated through
828
+ * RTC, so it is silently dropped here. Callers that want to surface a warning (e.g. `addRoot()`) should do so before
829
+ * invoking this function.
830
+ */
831
+ function setRootEditableOptions(modelAttributes, { placeholder, label, element }) {
832
+ if ("$rootEditableOptions" in modelAttributes) return;
833
+ const storageElement = isElement$1(element) ? void 0 : normalizeViewRootElementDefinition(element);
834
+ modelAttributes.$rootEditableOptions = {
835
+ ...placeholder && { placeholder },
836
+ ...label && { label },
837
+ ...storageElement && { element: storageElement }
838
+ };
970
839
  }
971
- function isElement(value) {
972
- return isElement$1(value);
840
+ function isElement$1(value) {
841
+ return isElement(value);
973
842
  }
974
843
  /**
975
- * Returns the canonical editable element descriptor for the given root config.
976
- *
977
- * Falls back to `$rootEditableOptions.element` so remote RTC clients - which do not see the originator's
978
- * `config.roots.<name>.element` - can recreate the configured editable shape from the model attributes
979
- * they receive. The result is normalized here in case the attribute was pre-supplied without going
980
- * through `setRootEditableOptions()` (e.g. a caller writing `modelAttributes.$rootEditableOptions` directly).
981
- */ function getRootEditableElement(rootConfig) {
982
- const rootEditableOptions = rootConfig.modelAttributes?.$rootEditableOptions;
983
- return normalizeViewRootElementDefinition(rootConfig.element || rootEditableOptions?.element);
844
+ * Returns the canonical editable element descriptor for the given root config.
845
+ *
846
+ * Falls back to `$rootEditableOptions.element` so remote RTC clients - which do not see the originator's
847
+ * `config.roots.<name>.element` - can recreate the configured editable shape from the model attributes
848
+ * they receive. The result is normalized here in case the attribute was pre-supplied without going
849
+ * through `setRootEditableOptions()` (e.g. a caller writing `modelAttributes.$rootEditableOptions` directly).
850
+ */
851
+ function getRootEditableElement(rootConfig) {
852
+ const rootEditableOptions = rootConfig.modelAttributes?.$rootEditableOptions;
853
+ return normalizeViewRootElementDefinition(rootConfig.element || rootEditableOptions?.element);
984
854
  }
985
855
 
856
+ /**
857
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
858
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
859
+ */
860
+
861
+ /**
862
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
863
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
864
+ */
865
+
986
866
  export { MultiRootEditor, MultiRootEditorUI, MultiRootEditorUIView };
987
- //# sourceMappingURL=index.js.map
867
+ //# sourceMappingURL=index.js.map