@ckeditor/ckeditor5-editor-multi-root 48.1.1 → 48.2.0-alpha.1

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,7 +2,7 @@
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 { Editor, normalizeMultiRootEditorConstructorParams, normalizeRootsConfig, secureSourceElement, registerAndInitializeRootConfigAttributes, verifyRootElements } from '@ckeditor/ckeditor5-core/dist/index.js';
5
+ import { rootAcceptsBlocks, Editor, normalizeMultiRootEditorConstructorParams, normalizeRootsConfig, secureSourceElement, registerAndInitializeRootConfigAttributes, normalizeViewRootElementDefinition, verifyRootElements } from '@ckeditor/ckeditor5-core/dist/index.js';
6
6
  import { CKEditorError, logWarning, decodeLicenseKey, isFeatureBlockedByLicenseKey, setDataInElement } from '@ckeditor/ckeditor5-utils/dist/index.js';
7
7
  import { EditorUI, EditorUIView, ToolbarView, MenuBarView, InlineEditableUIView } from '@ckeditor/ckeditor5-ui/dist/index.js';
8
8
  import { enableViewPlaceholder } from '@ckeditor/ckeditor5-engine/dist/index.js';
@@ -31,6 +31,14 @@ import { isElement as isElement$1 } from 'es-toolkit/compat';
31
31
  * Initializes the UI.
32
32
  */ init() {
33
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
+ }
34
42
  view.render();
35
43
  // Keep track of the last focused editable element. Knowing which one was focused
36
44
  // is useful when the focus moves from editable to other UI components like balloons
@@ -157,7 +165,7 @@ import { isElement as isElement$1 } from 'es-toolkit/compat';
157
165
  enableViewPlaceholder({
158
166
  view: editingView,
159
167
  element: editingRoot,
160
- isDirectHost: false,
168
+ isDirectHost: editable.isInlineRoot,
161
169
  keepOnFocus: true
162
170
  });
163
171
  }
@@ -318,6 +326,7 @@ import { isElement as isElement$1 } from 'es-toolkit/compat';
318
326
  super(editorConfig);
319
327
  normalizeRootsConfig(sourceElementsOrData, this.config, false);
320
328
  normalizeRootsAttributesConfig(this.config);
329
+ normalizeRootEditableOptionsConfig(this.config);
321
330
  if (this.config.get('lazyRoots')) {
322
331
  /**
323
332
  * Using deprecated `config.lazyRoots` configuration option.
@@ -329,16 +338,17 @@ import { isElement as isElement$1 } from 'es-toolkit/compat';
329
338
  // From this point use only normalized `roots.<rootName>.element`, etc.
330
339
  const rootsConfig = Object.entries(this.config.get('roots'));
331
340
  this.sourceElements = {};
332
- for (const [rootName, { element }] of rootsConfig){
333
- if (isElement(element)) {
334
- if (element.tagName === 'TEXTAREA') {
335
- // Documented in core/editor/editor.js
336
- // eslint-disable-next-line ckeditor5-rules/ckeditor-error-message
337
- throw new CKEditorError('editor-wrong-element', null);
338
- }
339
- this.sourceElements[rootName] = element;
340
- secureSourceElement(this, element);
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);
341
350
  }
351
+ editableElements[rootName] = editableElement;
342
352
  }
343
353
  this.editing.view.document.roots.on('add', (evt, viewRoot)=>{
344
354
  // Here we change the standard binding of readOnly flag by adding
@@ -366,37 +376,14 @@ import { isElement as isElement$1 } from 'es-toolkit/compat';
366
376
  root._isLoaded = false;
367
377
  }
368
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');
369
383
  registerAndInitializeRootConfigAttributes(this);
370
- // Registering `$rootEditableOptions` attribute to make it available in the editor model.
371
- // This allows to store editable options for each root in the model, and make them available on other RTC clients.
372
- // We do not use `registerRootAttribute()` method here, as this attribute is used internally
373
- // and should not be returned by `getRootsAttributes()` method.
374
- this.editing.model.schema.extend('$root', {
375
- allowAttributes: '$rootEditableOptions'
376
- });
377
- this.data.on('init', ()=>{
378
- this.model.enqueueChange({
379
- isUndoable: false
380
- }, (writer)=>{
381
- for (const [rootName, rootConfig] of rootsConfig){
382
- const root = this.model.document.getRoot(rootName);
383
- // Set editable config for consistency with `addRoot()` method. This will allow features
384
- // to use the same configuration for both initially loaded and dynamically added roots.
385
- const rootEditableOptions = {
386
- ...rootConfig.placeholder && {
387
- placeholder: rootConfig.placeholder
388
- },
389
- ...rootConfig.label && {
390
- label: rootConfig.label
391
- }
392
- };
393
- writer.setAttribute('$rootEditableOptions', rootEditableOptions, root);
394
- }
395
- });
396
- });
397
384
  const options = {
398
385
  shouldToolbarGroupWhenFull: !this.config.get('toolbar.shouldNotGroupWhenFull'),
399
- editableElements: this.sourceElements,
386
+ editableElements,
400
387
  label: extractRootsConfigField(this.config.get('roots'), 'label')
401
388
  };
402
389
  const view = new MultiRootEditorUIView(this.locale, this.editing.view, getNonLazyLoadRootsNames(rootsConfig), options);
@@ -524,12 +511,16 @@ import { isElement as isElement$1 } from 'es-toolkit/compat';
524
511
  }
525
512
  addRoot(rootName, options = {}) {
526
513
  const initialData = options.initialData || options.data || '';
527
- const modelAttributes = options.modelAttributes || options.attributes || {};
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()`
528
518
  const modelElement = options.modelElement || options.elementName || '$root';
529
519
  if (!this.model.schema.isLimit(modelElement)) {
530
520
  /**
531
521
  * The model root element must be a {@link module:engine/model/schema~ModelSchemaItemDefinition#isLimit limit element}.
532
- * The element name specified in {@link ~MultiRootEditor#addRoot `addRoot()`} options must be registered in the schema
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
533
524
  * with `isLimit` set to `true`.
534
525
  *
535
526
  * @error multi-root-editor-add-root-element-is-not-limit
@@ -542,9 +533,20 @@ import { isElement as isElement$1 } from 'es-toolkit/compat';
542
533
  }
543
534
  if (isElement(options.element)) {
544
535
  /**
545
- * The `element` option is not supported in {@link #addRoot `addRoot()`} method, and will be ignored.
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
546
  */ logWarning('multi-root-editor-add-root-element-option-ignored');
547
547
  }
548
+ // Persist editable options as a root attribute so they are available on other RTC clients.
549
+ setRootEditableOptions(modelAttributes, options);
548
550
  const _addRoot = (writer)=>{
549
551
  const root = writer.addRoot(rootName, modelElement);
550
552
  if (initialData) {
@@ -554,16 +556,6 @@ import { isElement as isElement$1 } from 'es-toolkit/compat';
554
556
  this.registerRootAttribute(key);
555
557
  writer.setAttribute(key, modelAttributes[key], root);
556
558
  }
557
- // Storing editable options as a root attribute to make them available on other RTC clients.
558
- const rootEditableOptions = {
559
- ...options.placeholder && {
560
- placeholder: options.placeholder
561
- },
562
- ...options.label && {
563
- label: options.label
564
- }
565
- };
566
- writer.setAttribute('$rootEditableOptions', rootEditableOptions, root);
567
559
  };
568
560
  if (options.isUndoable) {
569
561
  this.model.change(_addRoot);
@@ -637,14 +629,21 @@ import { isElement as isElement$1 } from 'es-toolkit/compat';
637
629
  }
638
630
  createEditable(root, optionsOrPlaceholder, label) {
639
631
  let placeholder;
632
+ let element;
640
633
  if (!optionsOrPlaceholder || typeof optionsOrPlaceholder === 'string') {
641
634
  placeholder = optionsOrPlaceholder;
642
635
  } else {
643
636
  placeholder = optionsOrPlaceholder?.placeholder;
644
637
  label = optionsOrPlaceholder?.label;
638
+ element = optionsOrPlaceholder?.element;
645
639
  }
646
640
  const rootEditableConfig = root.getAttribute('$rootEditableOptions') || {};
647
- const editable = this.ui.view.createEditable(root.rootName, undefined, label || rootEditableConfig.label);
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);
648
647
  this.ui.addEditable(editable, placeholder || rootEditableConfig.placeholder);
649
648
  this.editing.view.forceRender();
650
649
  return editable.element;
@@ -920,9 +919,69 @@ import { isElement as isElement$1 } from 'es-toolkit/compat';
920
919
  }
921
920
  }
922
921
  }
922
+ /**
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
+ }
943
+ }
944
+ /**
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
+ };
970
+ }
923
971
  function isElement(value) {
924
972
  return isElement$1(value);
925
973
  }
974
+ /**
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);
984
+ }
926
985
 
927
986
  export { MultiRootEditor, MultiRootEditorUI, MultiRootEditorUIView };
928
987
  //# sourceMappingURL=index.js.map