ckeditor5-livewire 1.10.0 → 1.12.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.
Files changed (58) hide show
  1. package/dist/hooks/editable.d.ts +3 -5
  2. package/dist/hooks/editable.d.ts.map +1 -1
  3. package/dist/hooks/editor/editor.d.ts +0 -4
  4. package/dist/hooks/editor/editor.d.ts.map +1 -1
  5. package/dist/hooks/editor/plugins/livewire-sync.d.ts.map +1 -1
  6. package/dist/hooks/editor/types/editor-relaxed-constructor.type.d.ts +6 -0
  7. package/dist/hooks/editor/types/editor-relaxed-constructor.type.d.ts.map +1 -0
  8. package/dist/hooks/editor/types/index.d.ts +2 -0
  9. package/dist/hooks/editor/types/index.d.ts.map +1 -0
  10. package/dist/hooks/editor/utils/assign-initial-data-to-editor-config.d.ts +10 -0
  11. package/dist/hooks/editor/utils/assign-initial-data-to-editor-config.d.ts.map +1 -0
  12. package/dist/hooks/editor/utils/assign-source-elements-to-editor-config.d.ts +12 -0
  13. package/dist/hooks/editor/utils/assign-source-elements-to-editor-config.d.ts.map +1 -0
  14. package/dist/hooks/editor/utils/cleanup-orphan-editor-elements.d.ts +8 -0
  15. package/dist/hooks/editor/utils/cleanup-orphan-editor-elements.d.ts.map +1 -0
  16. package/dist/hooks/editor/utils/create-editor-in-context.d.ts +7 -4
  17. package/dist/hooks/editor/utils/create-editor-in-context.d.ts.map +1 -1
  18. package/dist/hooks/editor/utils/index.d.ts +4 -0
  19. package/dist/hooks/editor/utils/index.d.ts.map +1 -1
  20. package/dist/hooks/editor/utils/is-multiroot-editor-instance.d.ts +6 -0
  21. package/dist/hooks/editor/utils/is-multiroot-editor-instance.d.ts.map +1 -0
  22. package/dist/hooks/editor/utils/wrap-with-watchdog.d.ts +7 -16
  23. package/dist/hooks/editor/utils/wrap-with-watchdog.d.ts.map +1 -1
  24. package/dist/hooks/ui-part.d.ts +2 -6
  25. package/dist/hooks/ui-part.d.ts.map +1 -1
  26. package/dist/index.cjs +2 -2
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.mjs +349 -232
  29. package/dist/index.mjs.map +1 -1
  30. package/dist/shared/are-maps-equal.d.ts +11 -0
  31. package/dist/shared/are-maps-equal.d.ts.map +1 -0
  32. package/dist/shared/async-registry.d.ts +43 -10
  33. package/dist/shared/async-registry.d.ts.map +1 -1
  34. package/dist/shared/index.d.ts +1 -0
  35. package/dist/shared/index.d.ts.map +1 -1
  36. package/package.json +5 -5
  37. package/src/hooks/context/context.test.ts +3 -1
  38. package/src/hooks/editable.ts +74 -47
  39. package/src/hooks/editor/editor.test.ts +44 -9
  40. package/src/hooks/editor/editor.ts +161 -149
  41. package/src/hooks/editor/plugins/livewire-sync.ts +17 -8
  42. package/src/hooks/editor/types/editor-relaxed-constructor.type.ts +6 -0
  43. package/src/hooks/editor/types/index.ts +1 -0
  44. package/src/hooks/editor/utils/assign-initial-data-to-editor-config.ts +48 -0
  45. package/src/hooks/editor/utils/assign-source-elements-to-editor-config.ts +61 -0
  46. package/src/hooks/editor/utils/cleanup-orphan-editor-elements.test.ts +285 -0
  47. package/src/hooks/editor/utils/cleanup-orphan-editor-elements.ts +60 -0
  48. package/src/hooks/editor/utils/create-editor-in-context.ts +8 -7
  49. package/src/hooks/editor/utils/index.ts +4 -0
  50. package/src/hooks/editor/utils/is-multiroot-editor-instance.ts +8 -0
  51. package/src/hooks/editor/utils/wrap-with-watchdog.test.ts +34 -14
  52. package/src/hooks/editor/utils/wrap-with-watchdog.ts +16 -26
  53. package/src/hooks/ui-part.ts +10 -16
  54. package/src/shared/are-maps-equal.test.ts +56 -0
  55. package/src/shared/are-maps-equal.ts +22 -0
  56. package/src/shared/async-registry.test.ts +212 -31
  57. package/src/shared/async-registry.ts +178 -61
  58. package/src/shared/index.ts +1 -0
@@ -2,7 +2,6 @@ import type { Editor } from 'ckeditor5';
2
2
 
3
3
  import type { RootAttributesUpdater } from '../utils';
4
4
  import type { EditorId, EditorLanguage, EditorPreset } from './typings';
5
- import type { EditorCreator } from './utils';
6
5
 
7
6
  import { ContextsRegistry } from '../../hooks/context';
8
7
  import { isEmptyObject, waitFor } from '../../shared';
@@ -14,6 +13,9 @@ import {
14
13
  createSyncEditorWithInputPlugin,
15
14
  } from './plugins';
16
15
  import {
16
+ assignInitialDataToEditorConfig,
17
+ assignSourceElementsToEditorConfig,
18
+ cleanupOrphanEditorElements,
17
19
  createEditorInContext,
18
20
  isSingleRootEditor,
19
21
  loadAllEditorTranslations,
@@ -34,11 +36,6 @@ import {
34
36
  * The Livewire hook that manages the lifecycle of CKEditor5 instances.
35
37
  */
36
38
  export class EditorComponentHook extends ClassHook<Snapshot> {
37
- /**
38
- * The promise that resolves to the editor instance.
39
- */
40
- private editorPromise: Promise<Editor> | null = null;
41
-
42
39
  /**
43
40
  * Root attributes updater for the main editor root.
44
41
  */
@@ -53,27 +50,51 @@ export class EditorComponentHook extends ClassHook<Snapshot> {
53
50
  EditorsRegistry.the.resetErrors(editorId);
54
51
 
55
52
  try {
56
- this.editorPromise = this.createEditor();
57
-
58
- const editor = await this.editorPromise;
53
+ const editor = await this.createEditor();
54
+ const editorContext = unwrapEditorContext(editor);
55
+ const watchdog = unwrapEditorWatchdog(editor);
59
56
 
60
- // Do not even try to broadcast about the registration of the editor
61
- // if hook was immediately destroyed.
62
- /* v8 ignore next if -- @preserve */
63
- if (!this.isBeingDestroyed()) {
64
- EditorsRegistry.the.register(editorId, editor);
57
+ // Do not even try to broadcast about the registration of the editor if hook was immediately destroyed.
58
+ /* v8 ignore next 3 */
59
+ if (this.isBeingDestroyed()) {
60
+ return;
61
+ }
65
62
 
63
+ // Run some stuff that have to be reinitialized every-time editor is being restarted.
64
+ const unmountDestroyWatcher = EditorsRegistry.the.mountEffect(editorId, (editor) => {
65
+ // Enforce deregistration of the editor when it's being destroyed by watchdog.
66
66
  editor.once('destroy', () => {
67
- /* v8 ignore next if -- @preserve */
68
- if (EditorsRegistry.the.hasItem(editorId)) {
69
- EditorsRegistry.the.unregister(editorId);
67
+ // Let's handle case when watchdog (or context watchdog) destroyed editor "externally"
68
+ // user might also manually kill the editor using `.destroy()` method.
69
+ // Keep pending callbacks though. Someone might register new callbacks just before calling `.destroy()`.
70
+ EditorsRegistry.the.unregister(editorId, false);
71
+ }, { priority: 'highest' });
72
+ });
73
+
74
+ this.onBeforeDestroy(async () => {
75
+ // If for some reason editor not fired `destroy`, enforce deregistration.
76
+ EditorsRegistry.the.unregister(editorId);
77
+ unmountDestroyWatcher();
78
+
79
+ if (editorContext) {
80
+ // If context is present, make sure it's not in unmounting phase, as it'll kill the editors.
81
+ // If it's being destroyed, don't do anything, as the context will take care of it.
82
+ if (editorContext.state !== 'unavailable') {
83
+ await editorContext.context.remove(editorContext.editorContextId);
70
84
  }
71
- });
72
- }
85
+ }
86
+ else if (watchdog) {
87
+ await watchdog.destroy();
88
+ }
89
+ else {
90
+ await editor.destroy();
91
+ }
92
+ });
93
+
94
+ EditorsRegistry.the.register(editorId, editor);
73
95
  }
74
96
  catch (error: any) {
75
97
  console.error(`Error initializing CKEditor5 instance with ID "${editorId}":`, error);
76
- this.editorPromise = null;
77
98
  EditorsRegistry.the.error(editorId, error);
78
99
  }
79
100
  }
@@ -85,42 +106,13 @@ export class EditorComponentHook extends ClassHook<Snapshot> {
85
106
  override async destroyed() {
86
107
  // Let's hide the element during destruction to prevent flickering.
87
108
  this.element.style.display = 'none';
88
-
89
- // Let's wait for the mounted promise to resolve before proceeding with destruction.
90
- try {
91
- const editor = await this.editorPromise;
92
-
93
- if (!editor) {
94
- return;
95
- }
96
-
97
- const editorContext = unwrapEditorContext(editor);
98
- const watchdog = unwrapEditorWatchdog(editor);
99
-
100
- if (editorContext) {
101
- // If context is present, make sure it's not in unmounting phase, as it'll kill the editors.
102
- // If it's being destroyed, don't do anything, as the context will take care of it.
103
- if (editorContext.state !== 'unavailable') {
104
- await editorContext.context.remove(editorContext.editorContextId);
105
- }
106
- }
107
- else if (watchdog) {
108
- await watchdog.destroy();
109
- }
110
- else {
111
- await editor.destroy();
112
- }
113
- }
114
- finally {
115
- this.editorPromise = null;
116
- }
117
109
  }
118
110
 
119
111
  /**
120
112
  * Updates the editor content when the component is updated after commit changes.
121
113
  */
122
114
  override async afterCommitSynced(): Promise<void> {
123
- const editor = await this.editorPromise;
115
+ const editor = await EditorsRegistry.the.waitFor(this.canonical.editorId);
124
116
 
125
117
  /* v8 ignore if -- @preserve */
126
118
  if (editor) {
@@ -152,7 +144,7 @@ export class EditorComponentHook extends ClassHook<Snapshot> {
152
144
  editableHeight,
153
145
  saveDebounceMs,
154
146
  language,
155
- watchdog,
147
+ watchdog: useWatchdog,
156
148
  content,
157
149
  } = this.canonical;
158
150
 
@@ -160,133 +152,153 @@ export class EditorComponentHook extends ClassHook<Snapshot> {
160
152
  customTranslations,
161
153
  editorType,
162
154
  licenseKey,
155
+ watchdogConfig,
163
156
  config: { plugins, ...config },
164
157
  } = preset;
165
158
 
166
- // Wrap editor creator with watchdog if needed.
167
- let Constructor: EditorCreator = await loadEditorConstructor(editorType);
159
+ const Constructor = await loadEditorConstructor(editorType);
168
160
  const context = await (
169
161
  contextId
170
162
  ? ContextsRegistry.the.waitFor(contextId)
171
163
  : null
172
164
  );
173
165
 
174
- // Do not use editor specific watchdog if context is attached, as the context is by default protected.
175
- if (watchdog && !context) {
176
- const wrapped = await wrapWithWatchdog(Constructor);
166
+ /**
167
+ * Builds the full editor configuration and creates the editor instance.
168
+ */
169
+ const buildAndCreateEditor = async () => {
170
+ const { loadedPlugins, hasPremium } = await loadEditorPlugins(plugins);
177
171
 
178
- ({ Constructor } = wrapped);
179
- wrapped.watchdog.on('restart', () => {
180
- const newInstance = wrapped.watchdog.editor!;
172
+ // Add integration specific plugins.
173
+ loadedPlugins.push(
174
+ await createLivewireSyncPlugin(
175
+ {
176
+ saveDebounceMs,
177
+ component: this,
178
+ },
179
+ ),
180
+ );
181
181
 
182
- this.editorPromise = Promise.resolve(newInstance);
182
+ if (isSingleRootEditor(editorType)) {
183
+ loadedPlugins.push(
184
+ await createSyncEditorWithInputPlugin(saveDebounceMs),
185
+ );
186
+ }
183
187
 
184
- EditorsRegistry.the.register(editorId, newInstance);
185
- });
186
- }
188
+ // Mix custom translations with loaded translations.
189
+ const loadedTranslations = await loadAllEditorTranslations(language, hasPremium);
190
+ const mixedTranslations = [
191
+ ...loadedTranslations,
192
+ normalizeCustomTranslations(customTranslations || {}),
193
+ ]
194
+ .filter(translations => !isEmptyObject(translations));
195
+
196
+ // Let's query all elements, and create basic configuration.
197
+ let initialData: string | Record<string, string> = {
198
+ ...content,
199
+ ...queryEditablesSnapshotContent(editorId),
200
+ };
187
201
 
188
- const { loadedPlugins, hasPremium } = await loadEditorPlugins(plugins);
202
+ if (isSingleRootEditor(editorType)) {
203
+ initialData = initialData['main'] || '';
204
+ }
189
205
 
190
- // Add integration specific plugins.
191
- loadedPlugins.push(
192
- await createLivewireSyncPlugin(
193
- {
194
- saveDebounceMs,
195
- component: this,
196
- },
197
- ),
198
- );
206
+ // Depending of the editor type, and parent lookup for nearest context or initialize it without it.
207
+ const editor = await (async () => {
208
+ let sourceElements: HTMLElement | Record<string, HTMLElement> = queryEditablesElements(editorId);
209
+
210
+ // Handle special case when user specified `initialData` of several root elements, but editable components
211
+ // are not yet present in the DOM. In other words - editor is initialized before attaching root elements.
212
+ if (!(sourceElements instanceof HTMLElement) && !('main' in sourceElements)) {
213
+ const requiredRoots = (
214
+ editorType === 'decoupled'
215
+ ? ['main']
216
+ : Object.keys(initialData as Record<string, string>)
217
+ );
218
+
219
+ if (!checkIfAllRootsArePresent(sourceElements, requiredRoots)) {
220
+ sourceElements = await waitForAllRootsToBePresent(editorId, requiredRoots);
221
+ initialData = {
222
+ ...content,
223
+ ...queryEditablesSnapshotContent(editorId),
224
+ };
225
+ }
226
+ }
199
227
 
200
- if (isSingleRootEditor(editorType)) {
201
- loadedPlugins.push(
202
- await createSyncEditorWithInputPlugin(saveDebounceMs),
203
- );
204
- }
228
+ // If single root editor, unwrap the element from the object.
229
+ if (isSingleRootEditor(editorType) && 'main' in sourceElements) {
230
+ sourceElements = sourceElements['main'];
231
+ }
205
232
 
206
- // Mix custom translations with loaded translations.
207
- const loadedTranslations = await loadAllEditorTranslations(language, hasPremium);
208
- const mixedTranslations = [
209
- ...loadedTranslations,
210
- normalizeCustomTranslations(customTranslations || {}),
211
- ]
212
- .filter(translations => !isEmptyObject(translations));
213
-
214
- // Let's query all elements, and create basic configuration.
215
- let initialData: string | Record<string, string> = {
216
- ...content,
217
- ...queryEditablesSnapshotContent(editorId),
218
- };
233
+ let resolvedConfig = { ...config };
234
+
235
+ // Do some postprocessing on received configuration.
236
+ resolvedConfig = resolveEditorConfigElementReferences(resolvedConfig);
237
+ resolvedConfig = resolveEditorConfigTranslations([...mixedTranslations].reverse(), language.ui, resolvedConfig);
238
+ resolvedConfig = assignSourceElementsToEditorConfig(Constructor, sourceElements, resolvedConfig);
239
+ resolvedConfig = assignInitialDataToEditorConfig(initialData, resolvedConfig);
240
+
241
+ // Construct parsed config.
242
+ const parsedConfig = {
243
+ ...resolvedConfig,
244
+ licenseKey,
245
+ plugins: loadedPlugins,
246
+ language,
247
+ ...mixedTranslations.length && {
248
+ translations: mixedTranslations,
249
+ },
250
+ };
251
+
252
+ if (!context || !(sourceElements instanceof HTMLElement)) {
253
+ return Constructor.create(parsedConfig);
254
+ }
219
255
 
220
- if (isSingleRootEditor(editorType)) {
221
- initialData = initialData['main'] || '';
222
- }
256
+ const result = await createEditorInContext({
257
+ context,
258
+ creator: Constructor,
259
+ config: parsedConfig,
260
+ });
223
261
 
224
- // Depending of the editor type, and parent lookup for nearest context or initialize it without it.
225
- const editor = await (async () => {
226
- let sourceElements: HTMLElement | Record<string, HTMLElement> = queryEditablesElements(editorId);
227
-
228
- // Handle special case when user specified `initialData` of several root elements, but editable components
229
- // are not yet present in the DOM. In other words - editor is initialized before attaching root elements.
230
- if (!(sourceElements instanceof HTMLElement) && !('main' in sourceElements)) {
231
- const requiredRoots = (
232
- editorType === 'decoupled'
233
- ? ['main']
234
- : Object.keys(initialData as Record<string, string>)
235
- );
262
+ return result.editor;
263
+ })();
236
264
 
237
- if (!checkIfAllRootsArePresent(sourceElements, requiredRoots)) {
238
- sourceElements = await waitForAllRootsToBePresent(editorId, requiredRoots);
239
- initialData = {
240
- ...content,
241
- ...queryEditablesSnapshotContent(editorId),
242
- };
243
- }
265
+ if (isSingleRootEditor(editorType) && editableHeight) {
266
+ setEditorEditableHeight(editor, editableHeight);
244
267
  }
245
268
 
246
- // If single root editor, unwrap the element from the object.
247
- if (isSingleRootEditor(editorType) && 'main' in sourceElements) {
248
- sourceElements = sourceElements['main'];
249
- }
269
+ this.applyRootAttributes(editor);
250
270
 
251
- // Construct parsed config. First resolve DOM element references in the provided configuration.
252
- let resolvedConfig = resolveEditorConfigElementReferences(config);
253
-
254
- // Then resolve translation references in the provided configuration, using the mixed translations.
255
- resolvedConfig = resolveEditorConfigTranslations([...mixedTranslations].reverse(), language.ui, resolvedConfig);
256
-
257
- // Construct parsed config.
258
- const parsedConfig = {
259
- ...resolvedConfig,
260
- initialData,
261
- licenseKey,
262
- plugins: loadedPlugins,
263
- language,
264
- ...mixedTranslations.length && {
265
- translations: mixedTranslations,
266
- },
267
- };
271
+ return editor;
272
+ };
268
273
 
269
- if (!context || !(sourceElements instanceof HTMLElement)) {
270
- return Constructor.create(sourceElements as any, parsedConfig);
271
- }
274
+ // Do not use editor specific watchdog if context is attached, as the context is by default protected.
275
+ if (useWatchdog && !context) {
276
+ const watchdog = await wrapWithWatchdog(buildAndCreateEditor, watchdogConfig);
277
+
278
+ // Cleanup editor registry before restart of the editor (restart might fail too).
279
+ watchdog.on('error', (_, { causesRestart }) => {
280
+ if (causesRestart) {
281
+ const prevEditor = EditorsRegistry.the.getItem(editorId);
282
+
283
+ /* v8 ignore next 3 */
284
+ if (prevEditor) {
285
+ cleanupOrphanEditorElements(prevEditor);
286
+ EditorsRegistry.the.unregister(editorId);
287
+ }
288
+ }
289
+ });
272
290
 
273
- const result = await createEditorInContext({
274
- context,
275
- element: sourceElements,
276
- creator: Constructor,
277
- config: parsedConfig,
291
+ // Register new instance after editor restarted.
292
+ watchdog.on('restart', () => {
293
+ EditorsRegistry.the.register(editorId, watchdog.editor!);
278
294
  });
279
295
 
280
- return result.editor;
281
- })();
296
+ await watchdog.create({});
282
297
 
283
- if (isSingleRootEditor(editorType) && editableHeight) {
284
- setEditorEditableHeight(editor, editableHeight);
298
+ return watchdog.editor!;
285
299
  }
286
300
 
287
- this.applyRootAttributes(editor);
288
-
289
- return editor;
301
+ return buildAndCreateEditor();
290
302
  };
291
303
  }
292
304
 
@@ -108,7 +108,9 @@ export async function createLivewireSyncPlugin(
108
108
  * Setups the content sync from Livewire to the editor when Livewire emits an event.
109
109
  */
110
110
  private setupSetEditorContentHandler() {
111
- Livewire.on('set-editor-content', ({ editorId, content }: SetContentPayload) => {
111
+ const { editor } = this;
112
+
113
+ const handler = ({ editorId, content }: SetContentPayload) => {
112
114
  if (editorId !== component.canonical.editorId) {
113
115
  return;
114
116
  }
@@ -116,9 +118,16 @@ export async function createLivewireSyncPlugin(
116
118
  const currentValues = this.getEditorRootsValues();
117
119
 
118
120
  if (!shallowEqual(currentValues, content)) {
119
- this.editor.setData(content);
121
+ editor.setData(content);
120
122
  }
121
- });
123
+ };
124
+
125
+ const clean: any = Livewire.on('set-editor-content', handler);
126
+
127
+ /* v8 ignore next if -- @preserve */
128
+ if (typeof clean === 'function') {
129
+ editor.once('destroy', clean);
130
+ }
122
131
  }
123
132
 
124
133
  /**
@@ -150,21 +159,21 @@ export async function createLivewireSyncPlugin(
150
159
  };
151
160
 
152
161
  const debouncedSync = debounce(saveDebounceMs, syncContentChange);
153
- const onChangeData = () => {
162
+
163
+ // Apply small debounce to avoid race conditions during re-mount of editables during watchdog restart.
164
+ // CKEditor tends to reset roots map and re-assign value in the same tick which may confuse two way binding.
165
+ model.document.on('change:data', debounce(10, () => {
154
166
  if (ui.focusTracker.isFocused) {
155
167
  debouncedSync();
156
168
  }
157
169
  else {
158
170
  syncContentChange();
159
171
  }
160
- };
161
-
162
- model.document.on('change:data', onChangeData);
172
+ }));
163
173
 
164
174
  editor.once('ready', syncContentChange);
165
175
  editor.once('destroy', () => {
166
176
  isDestroyed = true;
167
- model.document.off('change:data', onChangeData);
168
177
  });
169
178
  }
170
179
 
@@ -0,0 +1,6 @@
1
+ import type { Editor } from 'ckeditor5';
2
+
3
+ export type EditorRelaxedConstructor<TEditor extends Editor = Editor> = {
4
+ create: (...args: any) => Promise<TEditor>;
5
+ editorName?: string;
6
+ };
@@ -0,0 +1 @@
1
+ export * from './editor-relaxed-constructor.type';
@@ -0,0 +1,48 @@
1
+ import type { EditorConfig } from 'ckeditor5';
2
+
3
+ /**
4
+ * Assigns initial data to specified editor config.
5
+ *
6
+ * @param dataOrMap Initial data to be assigned to config.
7
+ * @param config Config of the editor.
8
+ * @returns The updated configuration object.
9
+ */
10
+ export function assignInitialDataToEditorConfig<C extends EditorConfig>(
11
+ dataOrMap: string | Record<string, string>,
12
+ config: C,
13
+ ): C {
14
+ const dataMap = toDataMap(dataOrMap);
15
+ const allRootsKeys = new Set([
16
+ ...Object.keys(dataMap),
17
+ ...Object.keys(config.roots ?? {}),
18
+ ]);
19
+
20
+ const rootsConfig = Array.from(allRootsKeys).reduce((acc, rootKey) => ({
21
+ ...acc,
22
+ [rootKey]: {
23
+ ...config.roots?.[rootKey],
24
+ ...rootKey === 'main' ? config.root : {},
25
+
26
+ /* v8 ignore start -- @preserve */
27
+ ...rootKey in dataMap
28
+ ? {
29
+ initialData: dataMap[rootKey],
30
+ }
31
+ : {},
32
+ /* v8 ignore stop -- @preserve */
33
+ },
34
+ }), Object.create(config.roots || {}));
35
+
36
+ const mappedConfig: C = {
37
+ ...config,
38
+ roots: rootsConfig,
39
+ };
40
+
41
+ delete mappedConfig.root;
42
+
43
+ return mappedConfig;
44
+ }
45
+
46
+ function toDataMap(element: string | Record<string, string>): Record<string, string> {
47
+ return typeof element === 'string' ? { main: element } : { ...element };
48
+ }
@@ -0,0 +1,61 @@
1
+ import type { EditorConfig } from 'ckeditor5';
2
+
3
+ import type { EditorRelaxedConstructor } from '../types/editor-relaxed-constructor.type';
4
+
5
+ /**
6
+ * Assigns a DOM element to the editor configuration in a way that is compatible with the specific editor type.
7
+ *
8
+ * @param Editor Constructor of the editor used to determine the location of element config entry.
9
+ * @param elementOrMap Element to be assigned to config.
10
+ * @param config Config of the editor.
11
+ * @returns The updated configuration object.
12
+ */
13
+ export function assignSourceElementsToEditorConfig<C extends EditorConfig>(
14
+ Editor: EditorRelaxedConstructor,
15
+ elementOrMap: HTMLElement | Record<string, HTMLElement>,
16
+ config: C,
17
+ ): C {
18
+ const elementsMap = toElementsMap(elementOrMap);
19
+
20
+ if (!Editor.editorName || Editor.editorName === 'ClassicEditor') {
21
+ return {
22
+ ...config,
23
+ attachTo: elementsMap['main'],
24
+ };
25
+ }
26
+
27
+ const allRootsKeys = new Set([
28
+ ...Object.keys(elementsMap),
29
+ ...Object.keys(config.roots ?? {}),
30
+ ]);
31
+
32
+ const rootsConfig = Array.from(allRootsKeys).reduce((acc, rootKey) => ({
33
+ ...acc,
34
+ [rootKey]: {
35
+ /* v8 ignore next */
36
+ ...config.roots?.[rootKey],
37
+ ...rootKey === 'main' ? config.root : {},
38
+
39
+ /* v8 ignore start -- @preserve */
40
+ ...rootKey in elementsMap
41
+ ? {
42
+ element: elementsMap[rootKey],
43
+ }
44
+ : {},
45
+ /* v8 ignore stop -- @preserve */
46
+ },
47
+ }), Object.create(config.roots || {}));
48
+
49
+ const mappedConfig: C = {
50
+ ...config,
51
+ roots: rootsConfig,
52
+ };
53
+
54
+ delete mappedConfig.root;
55
+
56
+ return mappedConfig;
57
+ }
58
+
59
+ function toElementsMap(element: HTMLElement | Record<string, HTMLElement>): Record<string, HTMLElement> {
60
+ return element instanceof HTMLElement ? { main: element } : { ...element };
61
+ }