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.
- package/dist/elements/editable.d.ts +3 -11
- package/dist/elements/editable.d.ts.map +1 -1
- package/dist/elements/editor/editor.d.ts.map +1 -1
- package/dist/elements/editor/typings.d.ts +2 -1
- package/dist/elements/editor/typings.d.ts.map +1 -1
- package/dist/elements/editor/utils/cleanup-orphan-editor-elements.d.ts +8 -0
- package/dist/elements/editor/utils/cleanup-orphan-editor-elements.d.ts.map +1 -0
- package/dist/elements/editor/utils/create-editor-in-context.d.ts +6 -1
- package/dist/elements/editor/utils/create-editor-in-context.d.ts.map +1 -1
- package/dist/elements/editor/utils/index.d.ts +1 -0
- package/dist/elements/editor/utils/index.d.ts.map +1 -1
- package/dist/elements/editor/utils/wrap-with-watchdog.d.ts +7 -16
- package/dist/elements/editor/utils/wrap-with-watchdog.d.ts.map +1 -1
- package/dist/elements/ui-part.d.ts +3 -3
- package/dist/elements/ui-part.d.ts.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +459 -394
- package/dist/index.mjs.map +1 -1
- package/dist/interop/create-editable-blazor-interop.d.ts.map +1 -1
- package/dist/interop/create-editor-blazor-interop.d.ts.map +1 -1
- package/dist/shared/are-maps-equal.d.ts +11 -0
- package/dist/shared/are-maps-equal.d.ts.map +1 -0
- package/dist/shared/async-registry.d.ts +44 -16
- package/dist/shared/async-registry.d.ts.map +1 -1
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/elements/editable.ts +38 -58
- package/src/elements/editor/editor.ts +122 -101
- package/src/elements/editor/typings.ts +3 -1
- package/src/elements/editor/utils/cleanup-orphan-editor-elements.test.ts +285 -0
- package/src/elements/editor/utils/cleanup-orphan-editor-elements.ts +60 -0
- package/src/elements/editor/utils/create-editor-in-context.ts +8 -2
- package/src/elements/editor/utils/index.ts +1 -0
- package/src/elements/editor/utils/wrap-with-watchdog.test.ts +34 -14
- package/src/elements/editor/utils/wrap-with-watchdog.ts +15 -25
- package/src/elements/ui-part.test.ts +1 -1
- package/src/elements/ui-part.ts +12 -11
- package/src/interop/create-editable-blazor-interop.ts +19 -16
- package/src/interop/create-editor-blazor-interop.ts +15 -18
- package/src/shared/are-maps-equal.test.ts +56 -0
- package/src/shared/are-maps-equal.ts +22 -0
- package/src/shared/async-registry.test.ts +190 -88
- package/src/shared/async-registry.ts +179 -107
- package/src/shared/index.ts +1 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import type { Editor } from 'ckeditor5';
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { cleanupOrphanEditorElements } from './cleanup-orphan-editor-elements';
|
|
6
|
+
|
|
7
|
+
describe('cleanupOrphanEditorElements', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
document.body.innerHTML = '';
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should remove uiElement from the DOM if it is connected', () => {
|
|
13
|
+
const uiElement = document.createElement('div');
|
|
14
|
+
document.body.appendChild(uiElement);
|
|
15
|
+
|
|
16
|
+
const mockEditor = {
|
|
17
|
+
ui: { element: uiElement },
|
|
18
|
+
} as unknown as Editor;
|
|
19
|
+
|
|
20
|
+
expect(uiElement.isConnected).toBe(true);
|
|
21
|
+
|
|
22
|
+
cleanupOrphanEditorElements(mockEditor);
|
|
23
|
+
|
|
24
|
+
expect(uiElement.isConnected).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should not remove uiElement from the DOM if it has proper attribute', () => {
|
|
28
|
+
const uiElement = document.createElement('div');
|
|
29
|
+
|
|
30
|
+
uiElement.setAttribute('data-cke-controlled', '');
|
|
31
|
+
document.body.appendChild(uiElement);
|
|
32
|
+
|
|
33
|
+
const mockEditor = {
|
|
34
|
+
ui: { element: uiElement },
|
|
35
|
+
} as unknown as Editor;
|
|
36
|
+
|
|
37
|
+
expect(uiElement.isConnected).toBe(true);
|
|
38
|
+
|
|
39
|
+
cleanupOrphanEditorElements(mockEditor);
|
|
40
|
+
|
|
41
|
+
expect(uiElement.isConnected).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should not throw an error if uiElement is not connected or does not exist', () => {
|
|
45
|
+
const uiElement = document.createElement('div');
|
|
46
|
+
|
|
47
|
+
const mockEditor = {
|
|
48
|
+
ui: { element: uiElement },
|
|
49
|
+
} as unknown as Editor;
|
|
50
|
+
|
|
51
|
+
expect(() => cleanupOrphanEditorElements(mockEditor)).not.toThrow();
|
|
52
|
+
expect(() => cleanupOrphanEditorElements({ ui: {} } as Editor)).not.toThrow();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should remove toolbar element from the DOM if it is connected', () => {
|
|
56
|
+
const toolbarElement = document.createElement('div');
|
|
57
|
+
document.body.appendChild(toolbarElement);
|
|
58
|
+
|
|
59
|
+
const mockEditor = {
|
|
60
|
+
ui: {
|
|
61
|
+
view: {
|
|
62
|
+
toolbar: { element: toolbarElement },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
} as unknown as Editor;
|
|
66
|
+
|
|
67
|
+
expect(toolbarElement.isConnected).toBe(true);
|
|
68
|
+
|
|
69
|
+
cleanupOrphanEditorElements(mockEditor);
|
|
70
|
+
|
|
71
|
+
expect(toolbarElement.isConnected).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should clear toolbar element instead of removing it when it has data-cke-controlled', () => {
|
|
75
|
+
const toolbarElement = document.createElement('div');
|
|
76
|
+
toolbarElement.setAttribute('data-cke-controlled', '');
|
|
77
|
+
toolbarElement.innerHTML = '<button>Bold</button>';
|
|
78
|
+
document.body.appendChild(toolbarElement);
|
|
79
|
+
|
|
80
|
+
const mockEditor = {
|
|
81
|
+
ui: {
|
|
82
|
+
view: {
|
|
83
|
+
toolbar: { element: toolbarElement },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
} as unknown as Editor;
|
|
87
|
+
|
|
88
|
+
expect(toolbarElement.isConnected).toBe(true);
|
|
89
|
+
|
|
90
|
+
cleanupOrphanEditorElements(mockEditor);
|
|
91
|
+
|
|
92
|
+
expect(toolbarElement.isConnected).toBe(true);
|
|
93
|
+
expect(toolbarElement.innerHTML).toBe('');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should not throw if toolbar element is absent', () => {
|
|
97
|
+
const mockEditor = {
|
|
98
|
+
ui: {
|
|
99
|
+
view: {
|
|
100
|
+
toolbar: {},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
} as unknown as Editor;
|
|
104
|
+
|
|
105
|
+
expect(() => cleanupOrphanEditorElements(mockEditor)).not.toThrow();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should remove menuBarView element from the DOM if it is connected', () => {
|
|
109
|
+
const menuBarElement = document.createElement('div');
|
|
110
|
+
document.body.appendChild(menuBarElement);
|
|
111
|
+
|
|
112
|
+
const mockEditor = {
|
|
113
|
+
ui: {
|
|
114
|
+
view: {
|
|
115
|
+
menuBarView: { element: menuBarElement },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
} as unknown as Editor;
|
|
119
|
+
|
|
120
|
+
expect(menuBarElement.isConnected).toBe(true);
|
|
121
|
+
|
|
122
|
+
cleanupOrphanEditorElements(mockEditor);
|
|
123
|
+
|
|
124
|
+
expect(menuBarElement.isConnected).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should clear menuBarView element instead of removing it when it has data-cke-controlled', () => {
|
|
128
|
+
const menuBarElement = document.createElement('div');
|
|
129
|
+
menuBarElement.setAttribute('data-cke-controlled', '');
|
|
130
|
+
menuBarElement.innerHTML = '<nav>File Edit</nav>';
|
|
131
|
+
document.body.appendChild(menuBarElement);
|
|
132
|
+
|
|
133
|
+
const mockEditor = {
|
|
134
|
+
ui: {
|
|
135
|
+
view: {
|
|
136
|
+
menuBarView: { element: menuBarElement },
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
} as unknown as Editor;
|
|
140
|
+
|
|
141
|
+
expect(menuBarElement.isConnected).toBe(true);
|
|
142
|
+
|
|
143
|
+
cleanupOrphanEditorElements(mockEditor);
|
|
144
|
+
|
|
145
|
+
expect(menuBarElement.isConnected).toBe(true);
|
|
146
|
+
expect(menuBarElement.innerHTML).toBe('');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should not throw if menuBarView element is absent', () => {
|
|
150
|
+
const mockEditor = {
|
|
151
|
+
ui: {
|
|
152
|
+
view: {
|
|
153
|
+
menuBarView: {},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
} as unknown as Editor;
|
|
157
|
+
|
|
158
|
+
expect(() => cleanupOrphanEditorElements(mockEditor)).not.toThrow();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should remove all three ui elements when all are connected', () => {
|
|
162
|
+
const uiElement = document.createElement('div');
|
|
163
|
+
const toolbarElement = document.createElement('div');
|
|
164
|
+
const menuBarElement = document.createElement('div');
|
|
165
|
+
|
|
166
|
+
document.body.append(uiElement, toolbarElement, menuBarElement);
|
|
167
|
+
|
|
168
|
+
const mockEditor = {
|
|
169
|
+
ui: {
|
|
170
|
+
element: uiElement,
|
|
171
|
+
view: {
|
|
172
|
+
toolbar: { element: toolbarElement },
|
|
173
|
+
menuBarView: { element: menuBarElement },
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
} as unknown as Editor;
|
|
177
|
+
|
|
178
|
+
cleanupOrphanEditorElements(mockEditor);
|
|
179
|
+
|
|
180
|
+
expect(uiElement.isConnected).toBe(false);
|
|
181
|
+
expect(toolbarElement.isConnected).toBe(false);
|
|
182
|
+
expect(menuBarElement.isConnected).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should remove _bodyCollectionContainer from the DOM if it is connected', () => {
|
|
186
|
+
const container = document.createElement('div');
|
|
187
|
+
document.body.appendChild(container);
|
|
188
|
+
|
|
189
|
+
const mockEditor = {
|
|
190
|
+
ui: {
|
|
191
|
+
view: {
|
|
192
|
+
body: {
|
|
193
|
+
_bodyCollectionContainer: container,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
} as unknown as Editor;
|
|
198
|
+
|
|
199
|
+
expect(container.isConnected).toBe(true);
|
|
200
|
+
cleanupOrphanEditorElements(mockEditor);
|
|
201
|
+
expect(container.isConnected).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should clean up corresponding attributes and classes from domRoots', () => {
|
|
205
|
+
const rootElement = document.createElement('div');
|
|
206
|
+
|
|
207
|
+
rootElement.setAttribute('contenteditable', 'true');
|
|
208
|
+
rootElement.setAttribute('role', 'textbox');
|
|
209
|
+
rootElement.setAttribute('aria-label', 'Rich Text Editor');
|
|
210
|
+
rootElement.setAttribute('aria-multiline', 'true');
|
|
211
|
+
rootElement.setAttribute('spellcheck', 'false');
|
|
212
|
+
|
|
213
|
+
rootElement.classList.add(
|
|
214
|
+
'ck',
|
|
215
|
+
'ck-content',
|
|
216
|
+
'ck-editor__editable',
|
|
217
|
+
'ck-rounded-corners',
|
|
218
|
+
'ck-editor__editable_inline',
|
|
219
|
+
'ck-blurred',
|
|
220
|
+
'ck-focused',
|
|
221
|
+
'my-custom-class',
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const domRoots = new Map();
|
|
225
|
+
domRoots.set('main', rootElement);
|
|
226
|
+
|
|
227
|
+
const mockEditor = {
|
|
228
|
+
editing: {
|
|
229
|
+
view: {
|
|
230
|
+
domRoots,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
} as unknown as Editor;
|
|
234
|
+
|
|
235
|
+
cleanupOrphanEditorElements(mockEditor);
|
|
236
|
+
|
|
237
|
+
expect(rootElement.hasAttribute('contenteditable')).toBe(false);
|
|
238
|
+
expect(rootElement.hasAttribute('role')).toBe(false);
|
|
239
|
+
expect(rootElement.hasAttribute('aria-label')).toBe(false);
|
|
240
|
+
expect(rootElement.hasAttribute('aria-multiline')).toBe(false);
|
|
241
|
+
expect(rootElement.hasAttribute('spellcheck')).toBe(false);
|
|
242
|
+
|
|
243
|
+
const removedClasses = [
|
|
244
|
+
'ck',
|
|
245
|
+
'ck-content',
|
|
246
|
+
'ck-editor__editable',
|
|
247
|
+
'ck-rounded-corners',
|
|
248
|
+
'ck-editor__editable_inline',
|
|
249
|
+
'ck-blurred',
|
|
250
|
+
'ck-focused',
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
removedClasses.forEach((className) => {
|
|
254
|
+
expect(rootElement.classList.contains(className)).toBe(false);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect(rootElement.classList.contains('my-custom-class')).toBe(true);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should ignore objects in domRoots that are not instances of HTMLElement', () => {
|
|
261
|
+
const fakeRoot = {
|
|
262
|
+
removeAttribute: () => {},
|
|
263
|
+
classList: { remove: () => {} },
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const domRoots = new Map();
|
|
267
|
+
domRoots.set('main', fakeRoot);
|
|
268
|
+
|
|
269
|
+
const mockEditor = {
|
|
270
|
+
editing: {
|
|
271
|
+
view: {
|
|
272
|
+
domRoots,
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
} as unknown as Editor;
|
|
276
|
+
|
|
277
|
+
expect(() => cleanupOrphanEditorElements(mockEditor)).not.toThrow();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should fail gracefully on an empty editor object', () => {
|
|
281
|
+
const emptyEditor = {} as unknown as Editor;
|
|
282
|
+
|
|
283
|
+
expect(() => cleanupOrphanEditorElements(emptyEditor)).not.toThrow();
|
|
284
|
+
});
|
|
285
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Editor } from 'ckeditor5';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Removes all DOM elements injected by a specific CKEditor instance.
|
|
5
|
+
* Call this before assigning a new instance (e.g. in the 'restart' watchdog handler),
|
|
6
|
+
* because the watchdog does not clean up the previous editor's DOM on its own.
|
|
7
|
+
*/
|
|
8
|
+
export function cleanupOrphanEditorElements(editor: Editor): void {
|
|
9
|
+
const uiElements = [
|
|
10
|
+
editor.ui?.element,
|
|
11
|
+
editor.ui?.view?.toolbar?.element,
|
|
12
|
+
editor.ui?.view?.menuBarView?.element,
|
|
13
|
+
].filter(Boolean) as HTMLElement[];
|
|
14
|
+
|
|
15
|
+
for (const uiElement of uiElements) {
|
|
16
|
+
removeOrReset(uiElement);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const bodyCollectionContainer = (editor.ui as any)?.view?.body?._bodyCollectionContainer;
|
|
20
|
+
|
|
21
|
+
if (bodyCollectionContainer?.isConnected) {
|
|
22
|
+
removeOrReset(bodyCollectionContainer);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const editingView = editor.editing?.view;
|
|
26
|
+
|
|
27
|
+
if (editingView) {
|
|
28
|
+
for (const domRoot of editingView.domRoots.values()) {
|
|
29
|
+
if (!(domRoot instanceof HTMLElement)) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
domRoot.removeAttribute('contenteditable');
|
|
34
|
+
domRoot.removeAttribute('role');
|
|
35
|
+
domRoot.removeAttribute('aria-label');
|
|
36
|
+
domRoot.removeAttribute('aria-multiline');
|
|
37
|
+
domRoot.removeAttribute('spellcheck');
|
|
38
|
+
domRoot.classList.remove(
|
|
39
|
+
'ck',
|
|
40
|
+
'ck-content',
|
|
41
|
+
'ck-editor__editable',
|
|
42
|
+
'ck-rounded-corners',
|
|
43
|
+
'ck-editor__editable_inline',
|
|
44
|
+
'ck-blurred',
|
|
45
|
+
'ck-focused',
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
removeOrReset(domRoot);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function removeOrReset(element: HTMLElement) {
|
|
53
|
+
if (element.hasAttribute('data-cke-controlled')) {
|
|
54
|
+
element.innerHTML = '';
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
element.remove();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { EditorCreator } from './wrap-with-watchdog';
|
|
2
1
|
import type { Context, ContextWatchdog, Editor, EditorConfig } from 'ckeditor5';
|
|
3
2
|
|
|
4
3
|
import { uid } from '../../../shared';
|
|
@@ -41,7 +40,7 @@ export async function createEditorInContext({ element, context, creator, config
|
|
|
41
40
|
|
|
42
41
|
// Destroying of context is async. There can be situation when the destroy of the context
|
|
43
42
|
// and the destroy of the editor is called in parallel. It often happens during unmounting of
|
|
44
|
-
//
|
|
43
|
+
// blazor hooks. Let's make sure that descriptor informs other components, that context is being
|
|
45
44
|
// destroyed.
|
|
46
45
|
const originalDestroy = context.destroy.bind(context);
|
|
47
46
|
context.destroy = async () => {
|
|
@@ -87,3 +86,10 @@ type EditorContextDescriptor = {
|
|
|
87
86
|
editorContextId: string;
|
|
88
87
|
context: ContextWatchdog<Context>;
|
|
89
88
|
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Type representing an Editor creator with a create method.
|
|
92
|
+
*/
|
|
93
|
+
type EditorCreator = {
|
|
94
|
+
create: (...args: any) => Promise<Editor>;
|
|
95
|
+
};
|
|
@@ -15,31 +15,51 @@ describe('wrap with watchdog', () => {
|
|
|
15
15
|
element.remove();
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
it('returns editor instance after
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
licenseKey: 'GPL',
|
|
22
|
-
});
|
|
18
|
+
it('returns editor instance after starting the watchdog', async () => {
|
|
19
|
+
const factory = () => ClassicEditor.create(element, { licenseKey: 'GPL' });
|
|
20
|
+
const watchdog = await wrapWithWatchdog(factory, null);
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
await watchdog.create({});
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
expect(watchdog.editor).toBeInstanceOf(ClassicEditor);
|
|
25
|
+
|
|
26
|
+
await watchdog.destroy();
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
it('returns instance of watchdog', async () => {
|
|
30
|
-
const
|
|
30
|
+
const factory = () => ClassicEditor.create(element, { licenseKey: 'GPL' });
|
|
31
|
+
const watchdog = await wrapWithWatchdog(factory, null);
|
|
31
32
|
|
|
32
33
|
expect(watchdog).toBeInstanceOf(EditorWatchdog);
|
|
33
34
|
});
|
|
34
35
|
|
|
35
36
|
it('should be possible to unwrap watchdog from editor instance', async () => {
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
});
|
|
37
|
+
const factory = () => ClassicEditor.create(element, { licenseKey: 'GPL' });
|
|
38
|
+
const watchdog = await wrapWithWatchdog(factory, null);
|
|
39
|
+
|
|
40
|
+
await watchdog.create({});
|
|
41
|
+
|
|
42
|
+
expect(unwrapEditorWatchdog(watchdog.editor!)).toBeInstanceOf(EditorWatchdog);
|
|
43
|
+
|
|
44
|
+
await watchdog.destroy();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('rebuilds config by calling factory again on restart', async () => {
|
|
48
|
+
let callCount = 0;
|
|
49
|
+
const factory = async () => {
|
|
50
|
+
callCount++;
|
|
51
|
+
return ClassicEditor.create(element, { licenseKey: 'GPL' });
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const watchdog = await wrapWithWatchdog(factory, null);
|
|
55
|
+
await watchdog.create({});
|
|
56
|
+
|
|
57
|
+
expect(callCount).toBe(1);
|
|
58
|
+
|
|
59
|
+
await (watchdog as any)._restart();
|
|
40
60
|
|
|
41
|
-
expect(
|
|
61
|
+
expect(callCount).toBe(2);
|
|
42
62
|
|
|
43
|
-
await
|
|
63
|
+
await watchdog.destroy();
|
|
44
64
|
});
|
|
45
65
|
});
|
|
@@ -1,35 +1,32 @@
|
|
|
1
|
-
import type { Editor, EditorWatchdog } from 'ckeditor5';
|
|
1
|
+
import type { Editor, EditorWatchdog, WatchdogConfig } from 'ckeditor5';
|
|
2
2
|
|
|
3
3
|
const EDITOR_WATCHDOG_SYMBOL = Symbol.for('elixir-editor-watchdog');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Wraps an
|
|
6
|
+
* Wraps an editor factory with a watchdog for automatic recovery.
|
|
7
|
+
* The factory is invoked on each (re)start, so configuration is rebuilt every time.
|
|
7
8
|
*
|
|
8
|
-
* @param
|
|
9
|
-
* @
|
|
9
|
+
* @param factory Async function that creates and returns an Editor instance.
|
|
10
|
+
* @param watchdogConfig Configuration of the watchdog.
|
|
11
|
+
* @returns The watchdog instance.
|
|
10
12
|
*/
|
|
11
|
-
export async function wrapWithWatchdog(
|
|
13
|
+
export async function wrapWithWatchdog(factory: () => Promise<Editor>, watchdogConfig?: WatchdogConfig | null) {
|
|
12
14
|
const { EditorWatchdog } = await import('ckeditor5');
|
|
13
|
-
const watchdog = new EditorWatchdog(Editor);
|
|
14
15
|
|
|
15
|
-
watchdog
|
|
16
|
-
|
|
16
|
+
const watchdog = new EditorWatchdog(null, watchdogConfig ?? {
|
|
17
|
+
crashNumberLimit: 10,
|
|
18
|
+
minimumNonErrorTimePeriod: 5000,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
watchdog.setCreator(async () => {
|
|
22
|
+
const editor = await factory();
|
|
17
23
|
|
|
18
24
|
(editor as any)[EDITOR_WATCHDOG_SYMBOL] = watchdog;
|
|
19
25
|
|
|
20
26
|
return editor;
|
|
21
27
|
});
|
|
22
28
|
|
|
23
|
-
return
|
|
24
|
-
watchdog,
|
|
25
|
-
Constructor: {
|
|
26
|
-
create: async (...args: Parameters<typeof Editor['create']>) => {
|
|
27
|
-
await watchdog.create(...args);
|
|
28
|
-
|
|
29
|
-
return watchdog.editor!;
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
};
|
|
29
|
+
return watchdog;
|
|
33
30
|
}
|
|
34
31
|
|
|
35
32
|
/**
|
|
@@ -42,10 +39,3 @@ export function unwrapEditorWatchdog(editor: Editor): EditorWatchdog | null {
|
|
|
42
39
|
|
|
43
40
|
return null;
|
|
44
41
|
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Type representing an Editor creator with a create method.
|
|
48
|
-
*/
|
|
49
|
-
export type EditorCreator = {
|
|
50
|
-
create: (...args: any) => Promise<Editor>;
|
|
51
|
-
};
|
|
@@ -136,7 +136,7 @@ describe('ui-part component', () => {
|
|
|
136
136
|
|
|
137
137
|
it('should handle destruction when mounted promise is not resolved yet', async () => {
|
|
138
138
|
document.body.innerHTML = '';
|
|
139
|
-
EditorsRegistry.the.reset();
|
|
139
|
+
await EditorsRegistry.the.reset();
|
|
140
140
|
|
|
141
141
|
const el = renderTestUIPart(createUIPartSnapshot('toolbar'));
|
|
142
142
|
|
package/src/elements/ui-part.ts
CHANGED
|
@@ -8,9 +8,9 @@ import { queryAllEditorIds } from './editor/utils';
|
|
|
8
8
|
*/
|
|
9
9
|
export class UIPartComponentElement extends HTMLElement {
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* Stops observing the editor registry and immediately runs any pending cleanup.
|
|
12
12
|
*/
|
|
13
|
-
private
|
|
13
|
+
private unmountEffect: VoidFunction | null = null;
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Mounts the UI part component.
|
|
@@ -26,9 +26,9 @@ export class UIPartComponentElement extends HTMLElement {
|
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
// If the editor is not registered yet, we will wait for it to be registered.
|
|
30
29
|
this.style.display = 'block';
|
|
31
|
-
|
|
30
|
+
|
|
31
|
+
this.unmountEffect = EditorsRegistry.the.mountEffect(editorId, (editor) => {
|
|
32
32
|
if (!this.isConnected) {
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
@@ -44,22 +44,23 @@ export class UIPartComponentElement extends HTMLElement {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
this.appendChild(uiPart.element);
|
|
47
|
+
|
|
48
|
+
return () => {
|
|
49
|
+
this.innerHTML = '';
|
|
50
|
+
};
|
|
47
51
|
});
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
/**
|
|
51
55
|
* Destroys the UI part component. Unmounts UI parts from the editor.
|
|
52
56
|
*/
|
|
53
|
-
|
|
57
|
+
disconnectedCallback() {
|
|
54
58
|
// Let's hide the element during destruction to prevent flickering.
|
|
55
59
|
this.style.display = 'none';
|
|
56
60
|
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
this.
|
|
60
|
-
|
|
61
|
-
// Unmount all UI parts from the editor.
|
|
62
|
-
this.innerHTML = '';
|
|
61
|
+
// Stop observing the registry and run cleanup immediately.
|
|
62
|
+
this.unmountEffect?.();
|
|
63
|
+
this.unmountEffect = null;
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
|
|
@@ -20,6 +20,8 @@ export function createEditableBlazorInterop(element: HTMLElement, interop: DotNe
|
|
|
20
20
|
const rootName = element.getAttribute('data-cke-root-name') ?? 'main';
|
|
21
21
|
|
|
22
22
|
let unmounted = false;
|
|
23
|
+
let stopEffect: VoidFunction | null = null;
|
|
24
|
+
|
|
23
25
|
let editorRef: unknown | null = null;
|
|
24
26
|
|
|
25
27
|
let sync = createNoopSync<string>();
|
|
@@ -46,12 +48,8 @@ export function createEditableBlazorInterop(element: HTMLElement, interop: DotNe
|
|
|
46
48
|
}
|
|
47
49
|
};
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
*/
|
|
52
|
-
const initializeSynchronization = async () => {
|
|
53
|
-
const editor = await EditorsRegistry.the.waitFor(editorId);
|
|
54
|
-
editorRef = DotNet.createJSObjectReference(editor);
|
|
51
|
+
stopEffect = EditorsRegistry.the.mountEffect(editorId, (editor) => {
|
|
52
|
+
editorRef = globalThis.DotNet.createJSObjectReference(editor);
|
|
55
53
|
|
|
56
54
|
sync = createEditorValueSync(editor, {
|
|
57
55
|
getCurrentValue: () => editor.getData({ rootName }) ?? '',
|
|
@@ -60,9 +58,20 @@ export function createEditableBlazorInterop(element: HTMLElement, interop: DotNe
|
|
|
60
58
|
});
|
|
61
59
|
|
|
62
60
|
syncRootAttributes = createRootAttributesUpdater(editor, rootName);
|
|
63
|
-
};
|
|
64
61
|
|
|
65
|
-
|
|
62
|
+
return () => {
|
|
63
|
+
sync.unmount();
|
|
64
|
+
|
|
65
|
+
/* v8 ignore else -- @preserve */
|
|
66
|
+
if (editorRef) {
|
|
67
|
+
globalThis.DotNet?.disposeJSObjectReference(editorRef);
|
|
68
|
+
editorRef = null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
syncRootAttributes = null;
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
|
|
66
75
|
document.body.addEventListener(CKEditor5ChangeDataEvent.EVENT_NAME, onDataChange);
|
|
67
76
|
markElementAsInteractive(element);
|
|
68
77
|
|
|
@@ -76,14 +85,10 @@ export function createEditableBlazorInterop(element: HTMLElement, interop: DotNe
|
|
|
76
85
|
}
|
|
77
86
|
|
|
78
87
|
document.body.removeEventListener(CKEditor5ChangeDataEvent.EVENT_NAME, onDataChange);
|
|
79
|
-
sync.unmount();
|
|
80
88
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
editorRef = null;
|
|
84
|
-
}
|
|
89
|
+
stopEffect?.();
|
|
90
|
+
stopEffect = null;
|
|
85
91
|
|
|
86
|
-
syncRootAttributes = null;
|
|
87
92
|
unmounted = true;
|
|
88
93
|
},
|
|
89
94
|
|
|
@@ -97,8 +102,6 @@ export function createEditableBlazorInterop(element: HTMLElement, interop: DotNe
|
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
await EditorsRegistry.the.waitFor(editorId);
|
|
100
|
-
|
|
101
|
-
// Ensure sync is initialized before forwarding (waitFor guarantees the editor exists)
|
|
102
105
|
sync.setValue(value);
|
|
103
106
|
},
|
|
104
107
|
|