ckeditor5-blazor 1.9.0 → 1.10.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 (46) hide show
  1. package/dist/elements/editable.d.ts +3 -11
  2. package/dist/elements/editable.d.ts.map +1 -1
  3. package/dist/elements/editor/editor.d.ts.map +1 -1
  4. package/dist/elements/editor/typings.d.ts +2 -1
  5. package/dist/elements/editor/typings.d.ts.map +1 -1
  6. package/dist/elements/editor/utils/cleanup-orphan-editor-elements.d.ts +8 -0
  7. package/dist/elements/editor/utils/cleanup-orphan-editor-elements.d.ts.map +1 -0
  8. package/dist/elements/editor/utils/create-editor-in-context.d.ts +6 -1
  9. package/dist/elements/editor/utils/create-editor-in-context.d.ts.map +1 -1
  10. package/dist/elements/editor/utils/index.d.ts +1 -0
  11. package/dist/elements/editor/utils/index.d.ts.map +1 -1
  12. package/dist/elements/editor/utils/wrap-with-watchdog.d.ts +7 -16
  13. package/dist/elements/editor/utils/wrap-with-watchdog.d.ts.map +1 -1
  14. package/dist/elements/ui-part.d.ts +3 -3
  15. package/dist/elements/ui-part.d.ts.map +1 -1
  16. package/dist/index.cjs +2 -2
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.mjs +459 -394
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/interop/create-editable-blazor-interop.d.ts.map +1 -1
  21. package/dist/interop/create-editor-blazor-interop.d.ts.map +1 -1
  22. package/dist/shared/are-maps-equal.d.ts +11 -0
  23. package/dist/shared/are-maps-equal.d.ts.map +1 -0
  24. package/dist/shared/async-registry.d.ts +44 -16
  25. package/dist/shared/async-registry.d.ts.map +1 -1
  26. package/dist/shared/index.d.ts +1 -0
  27. package/dist/shared/index.d.ts.map +1 -1
  28. package/package.json +3 -3
  29. package/src/elements/editable.ts +38 -58
  30. package/src/elements/editor/editor.ts +122 -101
  31. package/src/elements/editor/typings.ts +3 -1
  32. package/src/elements/editor/utils/cleanup-orphan-editor-elements.test.ts +285 -0
  33. package/src/elements/editor/utils/cleanup-orphan-editor-elements.ts +60 -0
  34. package/src/elements/editor/utils/create-editor-in-context.ts +8 -2
  35. package/src/elements/editor/utils/index.ts +1 -0
  36. package/src/elements/editor/utils/wrap-with-watchdog.test.ts +34 -14
  37. package/src/elements/editor/utils/wrap-with-watchdog.ts +15 -25
  38. package/src/elements/ui-part.test.ts +1 -1
  39. package/src/elements/ui-part.ts +12 -11
  40. package/src/interop/create-editable-blazor-interop.ts +19 -16
  41. package/src/interop/create-editor-blazor-interop.ts +15 -18
  42. package/src/shared/are-maps-equal.test.ts +56 -0
  43. package/src/shared/are-maps-equal.ts +22 -0
  44. package/src/shared/async-registry.test.ts +190 -88
  45. package/src/shared/async-registry.ts +179 -107
  46. package/src/shared/index.ts +1 -0
@@ -1 +1 @@
1
- {"version":3,"file":"create-editable-blazor-interop.d.ts","sourceRoot":"","sources":["../../../src/interop/create-editable-blazor-interop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAS9C;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa;IAoDpF;;OAEG;;IAkBH;;;OAGG;sBACqB,MAAM;IAW9B;;;OAGG;yCACwC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;EAU5E"}
1
+ {"version":3,"file":"create-editable-blazor-interop.d.ts","sourceRoot":"","sources":["../../../src/interop/create-editable-blazor-interop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAS9C;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa;IA6DpF;;OAEG;;IAcH;;;OAGG;sBACqB,MAAM;IAS9B;;;OAGG;yCACwC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;EAU5E"}
@@ -1 +1 @@
1
- {"version":3,"file":"create-editor-blazor-interop.d.ts","sourceRoot":"","sources":["../../../src/interop/create-editor-blazor-interop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAW9C;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa;IAmElF;;OAEG;sBACqB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAS9C;;OAEG;yCACwC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAUzE;;OAEG;;IAmBH;;;;OAIG;;EAWN"}
1
+ {"version":3,"file":"create-editor-blazor-interop.d.ts","sourceRoot":"","sources":["../../../src/interop/create-editor-blazor-interop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAW9C;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa;IAqElF;;OAEG;sBACqB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAS9C;;OAEG;yCACwC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAUzE;;OAEG;;IAcH;;;;OAIG;;EAWN"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Compares two Map structures for equality based on their contents.
3
+ * The function checks if the maps have the same size, contain the exact same keys,
4
+ * and have strictly equal values (using shallow comparison).
5
+ *
6
+ * @param map1 - The first map to compare (can be null).
7
+ * @param map2 - The second map to compare.
8
+ * @returns Returns `true` if the maps are identical in terms of keys and values, otherwise `false`.
9
+ */
10
+ export declare function areMapsEqual(map1: Map<any, any> | null, map2: Map<any, any>): boolean;
11
+ //# sourceMappingURL=are-maps-equal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"are-maps-equal.d.ts","sourceRoot":"","sources":["../../../src/shared/are-maps-equal.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,OAAO,CAYrF"}
@@ -19,6 +19,15 @@ export declare class AsyncRegistry<T extends Destructible> {
19
19
  * Set of watchers that observe changes to the registry.
20
20
  */
21
21
  private readonly watchers;
22
+ /**
23
+ * Batch nesting depth. When > 0, watcher notifications are deferred.
24
+ */
25
+ private batchDepth;
26
+ /**
27
+ * Snapshot of the last state dispatched to watchers, used for change detection.
28
+ */
29
+ private lastNotifiedItems;
30
+ private lastNotifiedErrors;
22
31
  /**
23
32
  * Executes a function on an item.
24
33
  * If the item is not yet registered, it will wait for it to be registered.
@@ -29,6 +38,14 @@ export declare class AsyncRegistry<T extends Destructible> {
29
38
  * @returns A promise that resolves with the result of the function.
30
39
  */
31
40
  execute<R, E extends T = T>(id: RegistryId | null, onSuccess: (item: E) => R, onError?: (error: any) => void): Promise<Awaited<R>>;
41
+ /**
42
+ * Reactively binds a mount/unmount lifecycle to a single registry item.
43
+ *
44
+ * @param id The ID of the item to observe.
45
+ * @param onMount Function executed when the item mounts.
46
+ * @returns A function that stops observing and immediately runs any pending cleanup.
47
+ */
48
+ mountEffect<E extends T = T>(id: RegistryId | null, onMount: (item: E) => (() => void) | void): () => void;
32
49
  /**
33
50
  * Registers an item.
34
51
  *
@@ -53,14 +70,21 @@ export declare class AsyncRegistry<T extends Destructible> {
53
70
  * Un-registers an item.
54
71
  *
55
72
  * @param id The ID of the item.
73
+ * @param resetPendingCallbacks If true resets pending callbacks.
56
74
  */
57
- unregister(id: RegistryId | null): void;
75
+ unregister(id: RegistryId | null, resetPendingCallbacks?: boolean): void;
58
76
  /**
59
77
  * Gets all registered items.
60
78
  *
61
79
  * @returns An array of all registered items.
62
80
  */
63
81
  getItems(): T[];
82
+ /**
83
+ * Returns single registered item.
84
+ *
85
+ * @returns Registered item.
86
+ */
87
+ getItem(id: RegistryId | null): T | undefined;
64
88
  /**
65
89
  * Checks if an item with the given ID is registered.
66
90
  *
@@ -73,15 +97,30 @@ export declare class AsyncRegistry<T extends Destructible> {
73
97
  * If the item is not registered yet, it will wait for it to be registered.
74
98
  *
75
99
  * @param id The ID of the item.
76
- * @param timeout Optional timeout in milliseconds.
77
100
  * @returns A promise that resolves with the item instance.
78
101
  */
79
- waitFor<E extends T = T>(id: RegistryId | null, timeout?: number): Promise<E>;
102
+ waitFor<E extends T = T>(id: RegistryId | null): Promise<E>;
80
103
  /**
81
104
  * Destroys all registered items and clears the registry.
82
105
  * This will call the `destroy` method on each item.
83
106
  */
84
107
  destroyAll(): Promise<void>;
108
+ /**
109
+ * Destroys all registered editors and removes all watchers.
110
+ */
111
+ reset(): Promise<void>;
112
+ /**
113
+ * Executes a callback while deferring all watcher notifications.
114
+ * A single notification is fired synchronously after the callback returns,
115
+ * but only if the registry actually changed.
116
+ *
117
+ * Batches can be nested — watchers are notified only when the outermost
118
+ * batch completes.
119
+ *
120
+ * @param fn The callback to execute.
121
+ * @returns The return value of the callback.
122
+ */
123
+ batch<R>(fn: () => R): R;
85
124
  /**
86
125
  * Registers a watcher that will be called whenever the registry changes.
87
126
  *
@@ -96,13 +135,9 @@ export declare class AsyncRegistry<T extends Destructible> {
96
135
  */
97
136
  unwatch(watcher: RegistryWatcher<T>): void;
98
137
  /**
99
- * Resets the registry by clearing all items, errors, and pending callbacks.
138
+ * Immediately dispatches the current state to all watchers if it changed.
100
139
  */
101
- reset(): void;
102
- /**
103
- * Notifies all watchers about changes to the registry.
104
- */
105
- private notifyWatchers;
140
+ private flushWatchers;
106
141
  /**
107
142
  * Gets or creates pending callbacks for a specific ID.
108
143
  *
@@ -110,13 +145,6 @@ export declare class AsyncRegistry<T extends Destructible> {
110
145
  * @returns The pending callbacks structure.
111
146
  */
112
147
  private getPendingCallbacks;
113
- /**
114
- * Registers an item as the default (null ID) item if it's the first one.
115
- *
116
- * @param id The ID of the item being registered.
117
- * @param item The item instance.
118
- */
119
- private registerAsDefault;
120
148
  }
121
149
  /**
122
150
  * Interface for objects that can be destroyed.
@@ -1 +1 @@
1
- {"version":3,"file":"async-registry.d.ts","sourceRoot":"","sources":["../../../src/shared/async-registry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,aAAa,CAAC,CAAC,SAAS,YAAY;IAC/C;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmC;IAEzD;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAqC;IAE1E;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqD;IAEtF;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiC;IAE1D;;;;;;;;OAQG;IACH,OAAO,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,EACxB,EAAE,EAAE,UAAU,GAAG,IAAI,EACrB,SAAS,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,EACzB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,GAC7B,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAgCtB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,EAAE,UAAU,GAAG,IAAI,EAAE,IAAI,EAAE,CAAC,GAAG,IAAI;IAqB9C;;;;;OAKG;IACH,KAAK,CAAC,EAAE,EAAE,UAAU,GAAG,IAAI,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI;IAqB9C;;;;OAIG;IACH,WAAW,CAAC,EAAE,EAAE,UAAU,GAAG,IAAI,GAAG,IAAI;IAWxC;;;;OAIG;IACH,UAAU,CAAC,EAAE,EAAE,UAAU,GAAG,IAAI,GAAG,IAAI;IAgBvC;;;;OAIG;IACH,QAAQ,IAAI,CAAC,EAAE;IAIf;;;;;OAKG;IACH,OAAO,CAAC,EAAE,EAAE,UAAU,GAAG,IAAI,GAAG,OAAO;IAIvC;;;;;;;OAOG;IACH,OAAO,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,UAAU,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAwC7E;;;OAGG;IACG,UAAU;IAehB;;;;;OAKG;IACH,KAAK,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAY9C;;;;OAIG;IACH,OAAO,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,IAAI;IAI1C;;OAEG;IACH,KAAK,IAAI,IAAI;IAOb;;OAEG;IACH,OAAO,CAAC,cAAc;IAStB;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAW3B;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;CAK1B;AAED;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;CAC7B,CAAC;AAEF;;GAEG;AACH,KAAK,UAAU,GAAG,MAAM,CAAC;AAUzB;;GAEG;AACH,KAAK,eAAe,CAAC,CAAC,IAAI,CACxB,KAAK,EAAE,GAAG,CAAC,UAAU,GAAG,IAAI,EAAE,CAAC,CAAC,EAChC,MAAM,EAAE,GAAG,CAAC,UAAU,GAAG,IAAI,EAAE,KAAK,CAAC,KAClC,IAAI,CAAC"}
1
+ {"version":3,"file":"async-registry.d.ts","sourceRoot":"","sources":["../../../src/shared/async-registry.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ export * from './are-maps-equal';
1
2
  export * from './async-registry';
2
3
  export * from './camel-case';
3
4
  export * from './debounce';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,QAAQ,CAAC;AACvB,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC;AAC1B,cAAc,OAAO,CAAC;AACtB,cAAc,YAAY,CAAC;AAC3B,cAAc,sBAAsB,CAAC;AACrC,cAAc,kCAAkC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,QAAQ,CAAC;AACvB,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC;AAC1B,cAAc,OAAO,CAAC;AACtB,cAAc,YAAY,CAAC;AAC3B,cAAc,sBAAsB,CAAC;AACrC,cAAc,kCAAkC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ckeditor5-blazor",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "CKEditor 5 integration for Blazor",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,9 +30,9 @@
30
30
  "@vitest/coverage-v8": "^4.0.18",
31
31
  "ckeditor5": "^47.6.0",
32
32
  "ckeditor5-premium-features": "^47.6.0",
33
- "happy-dom": "^20.5.0",
33
+ "happy-dom": "^20.8.9",
34
34
  "typescript": "^5.9.3",
35
- "vite": "^7.3.1",
35
+ "vite": "^7.3.2",
36
36
  "vite-plugin-dts": "^4.5.4",
37
37
  "vite-tsconfig-paths": "^6.0.5",
38
38
  "vitest": "^4.0.18"
@@ -11,27 +11,15 @@ import { queryAllEditorIds } from './editor/utils';
11
11
  */
12
12
  export class EditableComponentElement extends HTMLElement {
13
13
  /**
14
- * The promise that resolves when the editable is mounted.
14
+ * Stops observing the editor registry and immediately runs any pending cleanup.
15
15
  */
16
- private editorPromise: Promise<MultiRootEditor | null> | null = null;
16
+ private unmountEffect: VoidFunction | null = null;
17
17
 
18
18
  /**
19
19
  * Wait result for the interactive attribute.
20
20
  */
21
21
  private interactiveWait?: WaitForInteractiveResult;
22
22
 
23
- /**
24
- * Callbacks to be invoked before the editable is destroyed.
25
- */
26
- private beforeDestroyCallbacks: Array<() => void> = [];
27
-
28
- /**
29
- * Registers a callback to be called before the editable is destroyed.
30
- */
31
- public onBeforeDestroy(callback: () => void): void {
32
- this.beforeDestroyCallbacks.push(callback);
33
- }
34
-
35
23
  /**
36
24
  * Mounts the editable component.
37
25
  */
@@ -62,11 +50,11 @@ export class EditableComponentElement extends HTMLElement {
62
50
  throw new CKEditor5BlazorError('Editor ID or Root Name is missing.');
63
51
  }
64
52
 
65
- // If the editor is not registered yet, we will wait for it to be registered.
66
53
  this.style.display = 'block';
67
- this.editorPromise = EditorsRegistry.the.execute(editorId, async (editor: MultiRootEditor) => {
54
+
55
+ this.unmountEffect = EditorsRegistry.the.mountEffect(editorId, (editor: MultiRootEditor) => {
68
56
  if (!this.isConnected) {
69
- return null;
57
+ return;
70
58
  }
71
59
 
72
60
  const { ui, editing, model } = editor;
@@ -95,7 +83,7 @@ export class EditableComponentElement extends HTMLElement {
95
83
  });
96
84
  }
97
85
 
98
- return editor;
86
+ return;
99
87
  }
100
88
 
101
89
  editor.addRoot(rootName, {
@@ -135,59 +123,51 @@ export class EditableComponentElement extends HTMLElement {
135
123
  const debouncedSync = debounce(saveDebounceMs, sync);
136
124
 
137
125
  editor.model.document.on('change:data', debouncedSync);
138
- this.onBeforeDestroy(() => editor.model.document.off('change:data', debouncedSync));
139
126
  sync();
140
127
 
141
- return editor;
128
+ return () => {
129
+ editor.model.document.off('change:data', debouncedSync);
130
+
131
+ if (editor.state !== 'destroyed' && rootName) {
132
+ const root = editor.model.document.getRoot(rootName);
133
+
134
+ /* v8 ignore else -- @preserve */
135
+ if (root && 'detachEditable' in editor) {
136
+ // Detaching editables seem to be buggy when something removed DOM element of the editable (e.g. Blazor re-render) before
137
+ // the editable is unmounted. To prevent errors in such cases, we will try to detach the editable if it exists, but ignore errors.
138
+ try {
139
+ /* v8 ignore else -- @preserve */
140
+ if (editor.ui.view.editables[rootName]) {
141
+ editor.detachEditable(root);
142
+ }
143
+ }
144
+ catch (err) {
145
+ // Ignore errors when detaching editable.
146
+ /* v8 ignore next -- @preserve */
147
+ console.error('Unable unmount editable from root:', err);
148
+ }
149
+
150
+ if (root.isAttached()) {
151
+ editor.detachRoot(rootName, false);
152
+ }
153
+ }
154
+ }
155
+ };
142
156
  });
143
157
  }
144
158
 
145
159
  /**
146
160
  * Destroys the editable component. Unmounts root from the editor.
147
161
  */
148
- async disconnectedCallback() {
162
+ disconnectedCallback() {
149
163
  // Disconnect the observer if present.
150
164
  this.interactiveWait?.disconnect();
151
165
 
152
- const rootName = this.getAttribute('data-cke-root-name');
153
-
154
166
  // Let's hide the element during destruction to prevent flickering.
155
167
  this.style.display = 'none';
156
168
 
157
- // Let's wait for the mounted promise to resolve before proceeding with destruction.
158
- const editor = await this.editorPromise;
159
- this.editorPromise = null;
160
-
161
- // Run all registered pre-destroy callbacks and clear the queue.
162
- for (const callback of this.beforeDestroyCallbacks) {
163
- callback();
164
- }
165
-
166
- this.beforeDestroyCallbacks = [];
167
-
168
- // Unmount root from the editor if editor is still registered.
169
- if (editor && editor.state !== 'destroyed' && rootName) {
170
- const root = editor.model.document.getRoot(rootName);
171
-
172
- /* v8 ignore else -- @preserve */
173
- if (root && 'detachEditable' in editor) {
174
- // Detaching editables seem to be buggy when something removed DOM element of the editable (e.g. Blazor re-render) before
175
- // the editable is unmounted. To prevent errors in such cases, we will try to detach the editable if it exists, but ignore errors.
176
- try {
177
- if (editor.ui.view.editables[rootName]) {
178
- editor.detachEditable(root);
179
- }
180
- }
181
- catch (err) {
182
- // Ignore errors when detaching editable.
183
- /* v8 ignore next -- @preserve */
184
- console.error('Unable unmount editable from root:', err);
185
- }
186
-
187
- if (root.isAttached()) {
188
- editor.detachRoot(rootName, false);
189
- }
190
- }
191
- }
169
+ // Stop observing the registry and run cleanup immediately.
170
+ this.unmountEffect?.();
171
+ this.unmountEffect = null;
192
172
  }
193
173
  }
@@ -1,6 +1,5 @@
1
1
  import type { WaitForInteractiveResult } from '../../shared';
2
2
  import type { EditorId, EditorLanguage, EditorPreset } from './typings';
3
- import type { EditorCreator } from './utils';
4
3
  import type { Editor } from 'ckeditor5';
5
4
 
6
5
  import {
@@ -16,6 +15,7 @@ import {
16
15
  createSyncEditorWithInputPlugin,
17
16
  } from './plugins';
18
17
  import {
18
+ cleanupOrphanEditorElements,
19
19
  createEditorInContext,
20
20
  isSingleRootEditor,
21
21
  loadAllEditorTranslations,
@@ -155,144 +155,165 @@ export class EditorComponentElement extends HTMLElement {
155
155
  const editableHeight = this.getAttribute('data-cke-editable-height') ? Number.parseInt(this.getAttribute('data-cke-editable-height')!, 10) : null;
156
156
  const saveDebounceMs = Number.parseInt(this.getAttribute('data-cke-save-debounce-ms')!, 10);
157
157
  const language = JSON.parse(this.getAttribute('data-cke-language')!) as EditorLanguage;
158
- const watchdog = this.hasAttribute('data-cke-watchdog');
158
+ const useWatchdog = this.hasAttribute('data-cke-watchdog');
159
159
  const content = JSON.parse(this.getAttribute('data-cke-content')!) as Record<string, string>;
160
160
 
161
161
  const {
162
162
  customTranslations,
163
163
  editorType,
164
164
  licenseKey,
165
+ watchdogConfig,
165
166
  config: { plugins, ...config },
166
167
  } = preset;
167
168
 
168
- // Wrap editor creator with watchdog if needed.
169
- let Constructor: EditorCreator = await loadEditorConstructor(editorType);
169
+ const Constructor = await loadEditorConstructor(editorType);
170
170
  const context = await (
171
171
  contextId
172
172
  ? ContextsRegistry.the.waitFor(contextId)
173
173
  : null
174
174
  );
175
175
 
176
- // Do not use editor specific watchdog if context is attached, as the context is by default protected.
177
- if (watchdog && !context) {
178
- const wrapped = await wrapWithWatchdog(Constructor);
176
+ /**
177
+ * Builds the full editor configuration and creates the editor instance.
178
+ */
179
+ const buildAndCreateEditor = async () => {
180
+ const { loadedPlugins, hasPremium } = await loadEditorPlugins(plugins);
179
181
 
180
- ({ Constructor } = wrapped);
181
- wrapped.watchdog.on('restart', () => {
182
- const newInstance = wrapped.watchdog.editor!;
182
+ loadedPlugins.push(
183
+ await createDispatchEditorRootsChangeEventPlugin({
184
+ saveDebounceMs,
185
+ editorId,
186
+ targetElement: this,
187
+ }),
188
+ );
183
189
 
184
- this.editorPromise = Promise.resolve(newInstance);
190
+ if (isSingleRootEditor(editorType)) {
191
+ loadedPlugins.push(
192
+ await createSyncEditorWithInputPlugin(saveDebounceMs),
193
+ );
194
+ }
185
195
 
186
- EditorsRegistry.the.register(editorId, newInstance);
187
- });
188
- }
196
+ // Mix custom translations with loaded translations.
197
+ const loadedTranslations = await loadAllEditorTranslations(language, hasPremium);
198
+ const mixedTranslations = [
199
+ ...loadedTranslations,
200
+ normalizeCustomTranslations(customTranslations || {}),
201
+ ]
202
+ .filter(translations => !isEmptyObject(translations));
203
+
204
+ // Let's query all elements, and create basic configuration.
205
+ let initialData: string | Record<string, string> = {
206
+ ...content,
207
+ ...queryEditablesSnapshotContent(editorId),
208
+ };
189
209
 
190
- const { loadedPlugins, hasPremium } = await loadEditorPlugins(plugins);
210
+ if (isSingleRootEditor(editorType)) {
211
+ initialData = initialData['main'] || '';
212
+ }
191
213
 
192
- loadedPlugins.push(
193
- await createDispatchEditorRootsChangeEventPlugin({
194
- saveDebounceMs,
195
- editorId,
196
- targetElement: this,
197
- }),
198
- );
214
+ // Depending of the editor type, and parent lookup for nearest context or initialize it without it.
215
+ const editor = await (async () => {
216
+ let sourceElementOrData: HTMLElement | Record<string, HTMLElement> = queryEditablesElements(editorId);
217
+
218
+ // Handle special case when user specified `initialData` of several root elements, but editable components
219
+ // are not yet present in the DOM. In other words - editor is initialized before attaching root elements.
220
+ if (!sourceElementOrData['main']) {
221
+ const requiredRoots = (
222
+ isSingleRootEditor(editorType)
223
+ ? ['main']
224
+ : Object.keys(initialData as Record<string, string>)
225
+ );
226
+
227
+ if (!checkIfAllRootsArePresent(sourceElementOrData, requiredRoots)) {
228
+ sourceElementOrData = await waitForAllRootsToBePresent(editorId, requiredRoots);
229
+ initialData = {
230
+ ...content,
231
+ ...queryEditablesSnapshotContent(editorId),
232
+ };
233
+ }
234
+ }
199
235
 
200
- if (isSingleRootEditor(editorType)) {
201
- loadedPlugins.push(
202
- await createSyncEditorWithInputPlugin(saveDebounceMs),
203
- );
204
- }
236
+ // If single root editor, unwrap the element from the object.
237
+ if (isSingleRootEditor(editorType) && 'main' in sourceElementOrData) {
238
+ sourceElementOrData = sourceElementOrData['main'];
239
+ }
205
240
 
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
- };
241
+ // Construct parsed config. First resolve DOM element references in the provided configuration.
242
+ let resolvedConfig = resolveEditorConfigElementReferences(config);
243
+
244
+ // Then resolve translation references in the provided configuration, using the mixed translations.
245
+ resolvedConfig = resolveEditorConfigTranslations([...mixedTranslations].reverse(), language.ui, resolvedConfig);
246
+
247
+ // Construct parsed config.
248
+ const parsedConfig = {
249
+ ...resolvedConfig,
250
+ initialData,
251
+ licenseKey,
252
+ plugins: loadedPlugins,
253
+ language,
254
+ ...mixedTranslations.length && {
255
+ translations: mixedTranslations,
256
+ },
257
+ };
258
+
259
+ if (!context || !(sourceElementOrData instanceof HTMLElement)) {
260
+ return Constructor.create(sourceElementOrData as any, parsedConfig);
261
+ }
219
262
 
220
- if (isSingleRootEditor(editorType)) {
221
- initialData = initialData['main'] || '';
222
- }
263
+ const result = await createEditorInContext({
264
+ context,
265
+ element: sourceElementOrData,
266
+ creator: Constructor,
267
+ config: parsedConfig,
268
+ });
223
269
 
224
- // Depending of the editor type, and parent lookup for nearest context or initialize it without it.
225
- const editor = await (async () => {
226
- let sourceElementOrData: 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 (!sourceElementOrData['main']) {
231
- const requiredRoots = (
232
- isSingleRootEditor(editorType)
233
- ? ['main']
234
- : Object.keys(initialData as Record<string, string>)
235
- );
270
+ return result.editor;
271
+ })();
236
272
 
237
- if (!checkIfAllRootsArePresent(sourceElementOrData, requiredRoots)) {
238
- sourceElementOrData = await waitForAllRootsToBePresent(editorId, requiredRoots);
239
- initialData = {
240
- ...content,
241
- ...queryEditablesSnapshotContent(editorId),
242
- };
243
- }
273
+ // Assign root attributes if they are not empty. This is needed to support custom attributes on the root element of the editor.
274
+ if (!isEmptyObject(rootAttributes)) {
275
+ editor.model.change((writer) => {
276
+ writer.setAttributes(rootAttributes, editor.model.document.getRoot()!);
277
+ });
244
278
  }
245
279
 
246
- // If single root editor, unwrap the element from the object.
247
- if (isSingleRootEditor(editorType) && 'main' in sourceElementOrData) {
248
- sourceElementOrData = sourceElementOrData['main'];
280
+ if (isSingleRootEditor(editorType) && editableHeight) {
281
+ setEditorEditableHeight(editor, editableHeight);
249
282
  }
250
283
 
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
- };
284
+ return editor;
285
+ };
268
286
 
269
- if (!context || !(sourceElementOrData instanceof HTMLElement)) {
270
- return Constructor.create(sourceElementOrData as any, parsedConfig);
271
- }
287
+ // Do not use editor specific watchdog if context is attached, as the context is by default protected.
288
+ if (useWatchdog && !context) {
289
+ const watchdog = await wrapWithWatchdog(buildAndCreateEditor, watchdogConfig);
272
290
 
273
- const result = await createEditorInContext({
274
- context,
275
- element: sourceElementOrData,
276
- creator: Constructor,
277
- config: parsedConfig,
291
+ watchdog.on('error', (_, { causesRestart }) => {
292
+ if (causesRestart) {
293
+ const prevEditor = EditorsRegistry.the.getItem(editorId);
294
+
295
+ /* v8 ignore next 3 */
296
+ if (prevEditor) {
297
+ cleanupOrphanEditorElements(prevEditor);
298
+
299
+ EditorsRegistry.the.unregister(editorId);
300
+ }
301
+ }
278
302
  });
279
303
 
280
- return result.editor;
281
- })();
304
+ watchdog.on('restart', () => {
305
+ const newInstance = watchdog.editor!;
282
306
 
283
- // Assign root attributes if they are not empty. This is needed to support custom attributes on the root element of the editor.
284
- if (!isEmptyObject(rootAttributes)) {
285
- editor.model.change((writer) => {
286
- writer.setAttributes(rootAttributes, editor.model.document.getRoot()!);
307
+ EditorsRegistry.the.register(editorId, newInstance);
287
308
  });
288
- }
289
309
 
290
- if (isSingleRootEditor(editorType) && editableHeight) {
291
- setEditorEditableHeight(editor, editableHeight);
310
+ await watchdog.create({});
311
+
312
+ return watchdog.editor!;
292
313
  }
293
314
 
294
- return editor;
295
- };
315
+ return buildAndCreateEditor();
316
+ }
296
317
  }
297
318
 
298
319
  /**
@@ -1,3 +1,5 @@
1
+ import type { WatchdogConfig } from 'ckeditor5';
2
+
1
3
  /**
2
4
  * Represents a unique identifier for a CKEditor5 editor instance.
3
5
  * This is typically the ID of the HTML element that the editor is attached to.
@@ -100,7 +102,7 @@ export type EditorPreset = {
100
102
  /**
101
103
  * Optional watchdog configuration for error recovery.
102
104
  */
103
- watchdogConfig?: Record<string, any> | null;
105
+ watchdogConfig?: WatchdogConfig | null;
104
106
 
105
107
  /**
106
108
  * Optional custom translations for the editor.