ckeditor5-blazor 0.1.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 (219) hide show
  1. package/dist/ckeditor5-blazor-error.d.ts +7 -0
  2. package/dist/ckeditor5-blazor-error.d.ts.map +1 -0
  3. package/dist/elements/context/context.d.ts +26 -0
  4. package/dist/elements/context/context.d.ts.map +1 -0
  5. package/dist/elements/context/contexts-registry.d.ts +9 -0
  6. package/dist/elements/context/contexts-registry.d.ts.map +1 -0
  7. package/dist/elements/context/index.d.ts +4 -0
  8. package/dist/elements/context/index.d.ts.map +1 -0
  9. package/dist/elements/context/typings.d.ts +34 -0
  10. package/dist/elements/context/typings.d.ts.map +1 -0
  11. package/dist/elements/editable.d.ts +34 -0
  12. package/dist/elements/editable.d.ts.map +1 -0
  13. package/dist/elements/editor/custom-editor-plugins.d.ts +54 -0
  14. package/dist/elements/editor/custom-editor-plugins.d.ts.map +1 -0
  15. package/dist/elements/editor/editor.d.ts +31 -0
  16. package/dist/elements/editor/editor.d.ts.map +1 -0
  17. package/dist/elements/editor/editors-registry.d.ts +9 -0
  18. package/dist/elements/editor/editors-registry.d.ts.map +1 -0
  19. package/dist/elements/editor/index.d.ts +3 -0
  20. package/dist/elements/editor/index.d.ts.map +1 -0
  21. package/dist/elements/editor/plugins/dispatch-editor-roots-change-event.d.ts +23 -0
  22. package/dist/elements/editor/plugins/dispatch-editor-roots-change-event.d.ts.map +1 -0
  23. package/dist/elements/editor/plugins/index.d.ts +3 -0
  24. package/dist/elements/editor/plugins/index.d.ts.map +1 -0
  25. package/dist/elements/editor/plugins/sync-editor-with-input.d.ts +6 -0
  26. package/dist/elements/editor/plugins/sync-editor-with-input.d.ts.map +1 -0
  27. package/dist/elements/editor/typings.d.ts +99 -0
  28. package/dist/elements/editor/typings.d.ts.map +1 -0
  29. package/dist/elements/editor/utils/create-editor-in-context.d.ts +44 -0
  30. package/dist/elements/editor/utils/create-editor-in-context.d.ts.map +1 -0
  31. package/dist/elements/editor/utils/get-editor-roots-values.d.ts +9 -0
  32. package/dist/elements/editor/utils/get-editor-roots-values.d.ts.map +1 -0
  33. package/dist/elements/editor/utils/index.d.ts +14 -0
  34. package/dist/elements/editor/utils/index.d.ts.map +1 -0
  35. package/dist/elements/editor/utils/is-single-root-editor.d.ts +9 -0
  36. package/dist/elements/editor/utils/is-single-root-editor.d.ts.map +1 -0
  37. package/dist/elements/editor/utils/load-editor-constructor.d.ts +9 -0
  38. package/dist/elements/editor/utils/load-editor-constructor.d.ts.map +1 -0
  39. package/dist/elements/editor/utils/load-editor-plugins.d.ts +20 -0
  40. package/dist/elements/editor/utils/load-editor-plugins.d.ts.map +1 -0
  41. package/dist/elements/editor/utils/load-editor-translations.d.ts +14 -0
  42. package/dist/elements/editor/utils/load-editor-translations.d.ts.map +1 -0
  43. package/dist/elements/editor/utils/normalize-custom-translations.d.ts +11 -0
  44. package/dist/elements/editor/utils/normalize-custom-translations.d.ts.map +1 -0
  45. package/dist/elements/editor/utils/query-all-editor-ids.d.ts +5 -0
  46. package/dist/elements/editor/utils/query-all-editor-ids.d.ts.map +1 -0
  47. package/dist/elements/editor/utils/query-editor-editables.d.ts +25 -0
  48. package/dist/elements/editor/utils/query-editor-editables.d.ts.map +1 -0
  49. package/dist/elements/editor/utils/resolve-editor-config-elements-references.d.ts +9 -0
  50. package/dist/elements/editor/utils/resolve-editor-config-elements-references.d.ts.map +1 -0
  51. package/dist/elements/editor/utils/resolve-editor-config-translations.d.ts +25 -0
  52. package/dist/elements/editor/utils/resolve-editor-config-translations.d.ts.map +1 -0
  53. package/dist/elements/editor/utils/set-editor-editable-height.d.ts +9 -0
  54. package/dist/elements/editor/utils/set-editor-editable-height.d.ts.map +1 -0
  55. package/dist/elements/editor/utils/wrap-with-watchdog.d.ts +24 -0
  56. package/dist/elements/editor/utils/wrap-with-watchdog.d.ts.map +1 -0
  57. package/dist/elements/ensure-editor-elements-registered.d.ts +5 -0
  58. package/dist/elements/ensure-editor-elements-registered.d.ts.map +1 -0
  59. package/dist/elements/index.d.ts +6 -0
  60. package/dist/elements/index.d.ts.map +1 -0
  61. package/dist/elements/ui-part.d.ts +18 -0
  62. package/dist/elements/ui-part.d.ts.map +1 -0
  63. package/dist/index.cjs +5 -0
  64. package/dist/index.cjs.map +1 -0
  65. package/dist/index.d.ts +11 -0
  66. package/dist/index.d.ts.map +1 -0
  67. package/dist/index.mjs +1400 -0
  68. package/dist/index.mjs.map +1 -0
  69. package/dist/interop/create-context-blazor-interop.d.ts +10 -0
  70. package/dist/interop/create-context-blazor-interop.d.ts.map +1 -0
  71. package/dist/interop/create-editable-blazor-interop.d.ts +21 -0
  72. package/dist/interop/create-editable-blazor-interop.d.ts.map +1 -0
  73. package/dist/interop/create-editor-blazor-interop.d.ts +19 -0
  74. package/dist/interop/create-editor-blazor-interop.d.ts.map +1 -0
  75. package/dist/interop/create-ui-part-blazor-interop.d.ts +10 -0
  76. package/dist/interop/create-ui-part-blazor-interop.d.ts.map +1 -0
  77. package/dist/interop/index.d.ts +5 -0
  78. package/dist/interop/index.d.ts.map +1 -0
  79. package/dist/interop/utils/create-editor-value-sync.d.ts +63 -0
  80. package/dist/interop/utils/create-editor-value-sync.d.ts.map +1 -0
  81. package/dist/interop/utils/index.d.ts +2 -0
  82. package/dist/interop/utils/index.d.ts.map +1 -0
  83. package/dist/shared/async-registry.d.ts +136 -0
  84. package/dist/shared/async-registry.d.ts.map +1 -0
  85. package/dist/shared/camel-case.d.ts +8 -0
  86. package/dist/shared/camel-case.d.ts.map +1 -0
  87. package/dist/shared/debounce.d.ts +2 -0
  88. package/dist/shared/debounce.d.ts.map +1 -0
  89. package/dist/shared/deep-camel-case-keys.d.ts +8 -0
  90. package/dist/shared/deep-camel-case-keys.d.ts.map +1 -0
  91. package/dist/shared/filter-object-values.d.ts +9 -0
  92. package/dist/shared/filter-object-values.d.ts.map +1 -0
  93. package/dist/shared/index.d.ts +16 -0
  94. package/dist/shared/index.d.ts.map +1 -0
  95. package/dist/shared/is-empty-object.d.ts +2 -0
  96. package/dist/shared/is-empty-object.d.ts.map +1 -0
  97. package/dist/shared/is-plain-object.d.ts +8 -0
  98. package/dist/shared/is-plain-object.d.ts.map +1 -0
  99. package/dist/shared/map-object-values.d.ts +11 -0
  100. package/dist/shared/map-object-values.d.ts.map +1 -0
  101. package/dist/shared/once.d.ts +2 -0
  102. package/dist/shared/once.d.ts.map +1 -0
  103. package/dist/shared/shallow-equal.d.ts +9 -0
  104. package/dist/shared/shallow-equal.d.ts.map +1 -0
  105. package/dist/shared/timeout.d.ts +8 -0
  106. package/dist/shared/timeout.d.ts.map +1 -0
  107. package/dist/shared/uid.d.ts +7 -0
  108. package/dist/shared/uid.d.ts.map +1 -0
  109. package/dist/shared/wait-for-dom-ready.d.ts +5 -0
  110. package/dist/shared/wait-for-dom-ready.d.ts.map +1 -0
  111. package/dist/shared/wait-for-interactive-attribute.d.ts +18 -0
  112. package/dist/shared/wait-for-interactive-attribute.d.ts.map +1 -0
  113. package/dist/shared/wait-for.d.ts +20 -0
  114. package/dist/shared/wait-for.d.ts.map +1 -0
  115. package/dist/types/can-be-promise.type.d.ts +2 -0
  116. package/dist/types/can-be-promise.type.d.ts.map +1 -0
  117. package/dist/types/dot-net-interop.type.d.ts +7 -0
  118. package/dist/types/dot-net-interop.type.d.ts.map +1 -0
  119. package/dist/types/index.d.ts +4 -0
  120. package/dist/types/index.d.ts.map +1 -0
  121. package/dist/types/required-by.type.d.ts +2 -0
  122. package/dist/types/required-by.type.d.ts.map +1 -0
  123. package/package.json +49 -0
  124. package/src/ckeditor5-blazor-error.ts +9 -0
  125. package/src/elements/context/context.test.ts +323 -0
  126. package/src/elements/context/context.ts +128 -0
  127. package/src/elements/context/contexts-registry.test.ts +10 -0
  128. package/src/elements/context/contexts-registry.ts +10 -0
  129. package/src/elements/context/index.ts +3 -0
  130. package/src/elements/context/typings.ts +38 -0
  131. package/src/elements/editable.test.ts +383 -0
  132. package/src/elements/editable.ts +183 -0
  133. package/src/elements/editor/custom-editor-plugins.test.ts +103 -0
  134. package/src/elements/editor/custom-editor-plugins.ts +85 -0
  135. package/src/elements/editor/editor.test.ts +562 -0
  136. package/src/elements/editor/editor.ts +330 -0
  137. package/src/elements/editor/editors-registry.test.ts +10 -0
  138. package/src/elements/editor/editors-registry.ts +10 -0
  139. package/src/elements/editor/index.ts +2 -0
  140. package/src/elements/editor/plugins/dispatch-editor-roots-change-event.ts +76 -0
  141. package/src/elements/editor/plugins/index.ts +2 -0
  142. package/src/elements/editor/plugins/sync-editor-with-input.ts +79 -0
  143. package/src/elements/editor/typings.ts +114 -0
  144. package/src/elements/editor/utils/create-editor-in-context.ts +89 -0
  145. package/src/elements/editor/utils/get-editor-roots-values.test.ts +48 -0
  146. package/src/elements/editor/utils/get-editor-roots-values.ts +21 -0
  147. package/src/elements/editor/utils/index.ts +13 -0
  148. package/src/elements/editor/utils/is-single-root-editor.test.ts +40 -0
  149. package/src/elements/editor/utils/is-single-root-editor.ts +11 -0
  150. package/src/elements/editor/utils/load-editor-constructor.test.ts +62 -0
  151. package/src/elements/editor/utils/load-editor-constructor.ts +29 -0
  152. package/src/elements/editor/utils/load-editor-plugins.test.ts +100 -0
  153. package/src/elements/editor/utils/load-editor-plugins.ts +72 -0
  154. package/src/elements/editor/utils/load-editor-translations.ts +232 -0
  155. package/src/elements/editor/utils/normalize-custom-translations.test.ts +152 -0
  156. package/src/elements/editor/utils/normalize-custom-translations.ts +17 -0
  157. package/src/elements/editor/utils/query-all-editor-ids.ts +9 -0
  158. package/src/elements/editor/utils/query-editor-editables.ts +101 -0
  159. package/src/elements/editor/utils/resolve-editor-config-elements-references.test.ts +93 -0
  160. package/src/elements/editor/utils/resolve-editor-config-elements-references.ts +36 -0
  161. package/src/elements/editor/utils/resolve-editor-config-translations.test.ts +131 -0
  162. package/src/elements/editor/utils/resolve-editor-config-translations.ts +77 -0
  163. package/src/elements/editor/utils/set-editor-editable-height.test.ts +131 -0
  164. package/src/elements/editor/utils/set-editor-editable-height.ts +15 -0
  165. package/src/elements/editor/utils/wrap-with-watchdog.test.ts +45 -0
  166. package/src/elements/editor/utils/wrap-with-watchdog.ts +51 -0
  167. package/src/elements/ensure-editor-elements-registered.ts +24 -0
  168. package/src/elements/index.ts +14 -0
  169. package/src/elements/ui-part.test.ts +156 -0
  170. package/src/elements/ui-part.ts +84 -0
  171. package/src/index.ts +15 -0
  172. package/src/interop/create-context-blazor-interop.test.ts +30 -0
  173. package/src/interop/create-context-blazor-interop.ts +15 -0
  174. package/src/interop/create-editable-blazor-interop.test.ts +213 -0
  175. package/src/interop/create-editable-blazor-interop.ts +98 -0
  176. package/src/interop/create-editor-blazor-interop.test.ts +183 -0
  177. package/src/interop/create-editor-blazor-interop.ts +112 -0
  178. package/src/interop/create-ui-part-blazor-interop.test.ts +30 -0
  179. package/src/interop/create-ui-part-blazor-interop.ts +15 -0
  180. package/src/interop/index.ts +4 -0
  181. package/src/interop/utils/create-editor-value-sync.test.ts +302 -0
  182. package/src/interop/utils/create-editor-value-sync.ts +160 -0
  183. package/src/interop/utils/index.ts +1 -0
  184. package/src/shared/async-registry.test.ts +737 -0
  185. package/src/shared/async-registry.ts +353 -0
  186. package/src/shared/camel-case.test.ts +35 -0
  187. package/src/shared/camel-case.ts +11 -0
  188. package/src/shared/debounce.test.ts +72 -0
  189. package/src/shared/debounce.ts +16 -0
  190. package/src/shared/deep-camel-case-keys.test.ts +34 -0
  191. package/src/shared/deep-camel-case-keys.ts +26 -0
  192. package/src/shared/filter-object-values.test.ts +25 -0
  193. package/src/shared/filter-object-values.ts +17 -0
  194. package/src/shared/index.ts +15 -0
  195. package/src/shared/is-empty-object.test.ts +78 -0
  196. package/src/shared/is-empty-object.ts +3 -0
  197. package/src/shared/is-plain-object.test.ts +38 -0
  198. package/src/shared/is-plain-object.ts +15 -0
  199. package/src/shared/map-object-values.test.ts +29 -0
  200. package/src/shared/map-object-values.ts +19 -0
  201. package/src/shared/once.test.ts +116 -0
  202. package/src/shared/once.ts +12 -0
  203. package/src/shared/shallow-equal.test.ts +51 -0
  204. package/src/shared/shallow-equal.ts +30 -0
  205. package/src/shared/timeout.test.ts +65 -0
  206. package/src/shared/timeout.ts +13 -0
  207. package/src/shared/uid.test.ts +25 -0
  208. package/src/shared/uid.ts +8 -0
  209. package/src/shared/wait-for-dom-ready.test.ts +87 -0
  210. package/src/shared/wait-for-dom-ready.ts +21 -0
  211. package/src/shared/wait-for-interactive-attribute.test.ts +93 -0
  212. package/src/shared/wait-for-interactive-attribute.ts +50 -0
  213. package/src/shared/wait-for.test.ts +24 -0
  214. package/src/shared/wait-for.ts +56 -0
  215. package/src/types/can-be-promise.type.ts +1 -0
  216. package/src/types/dot-net-interop.type.ts +6 -0
  217. package/src/types/dotnet-global.d.ts +14 -0
  218. package/src/types/index.ts +3 -0
  219. package/src/types/required-by.type.ts +1 -0
@@ -0,0 +1,323 @@
1
+ import { Context, ContextPlugin, ContextWatchdog } from 'ckeditor5';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import {
5
+ createContextSnapshot,
6
+ DEFAULT_TEST_CONTEXT_ID,
7
+ renderTestContext,
8
+ renderTestEditor,
9
+ waitForDestroyAllEditors,
10
+ waitForTestContext,
11
+ waitForTestEditor,
12
+ } from '~/test-utils';
13
+
14
+ import { timeout } from '../../shared';
15
+ import { CustomEditorPluginsRegistry } from '../editor/custom-editor-plugins';
16
+ import { EditorsRegistry } from '../editor/editors-registry';
17
+ import { ensureEditorElementsRegistered } from '../ensure-editor-elements-registered';
18
+ import { ContextsRegistry } from './contexts-registry';
19
+
20
+ describe('context component', () => {
21
+ beforeEach(() => {
22
+ document.body.innerHTML = '';
23
+ ensureEditorElementsRegistered();
24
+ });
25
+
26
+ afterEach(async () => {
27
+ vi.useRealTimers();
28
+ vi.resetAllMocks();
29
+
30
+ document.body.innerHTML = '';
31
+
32
+ await waitForDestroyAllEditors();
33
+ });
34
+
35
+ describe('mount', () => {
36
+ it('should save the context instance in the registry with provided id', async () => {
37
+ renderTestContext({ contextId: 'my-context' });
38
+
39
+ const watchdog = await ContextsRegistry.the.waitFor('my-context');
40
+
41
+ expect(watchdog).toBeInstanceOf(ContextWatchdog);
42
+ expect(watchdog.context).toBeInstanceOf(Context);
43
+ });
44
+
45
+ it('should initialize context with empty creator config', async () => {
46
+ renderTestContext(createContextSnapshot(DEFAULT_TEST_CONTEXT_ID, {
47
+ config: {},
48
+ }));
49
+
50
+ expect(await waitForTestContext()).toBeInstanceOf(ContextWatchdog);
51
+ });
52
+
53
+ it('should print console error if `itemError` event is fired', async () => {
54
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
55
+
56
+ renderTestContext();
57
+
58
+ const watchdog = await waitForTestContext();
59
+
60
+ (watchdog as any)._fire('itemError', 'test-error', { some: 'data' });
61
+
62
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Context item error:', null, 'test-error', { some: 'data' });
63
+
64
+ consoleErrorSpy.mockRestore();
65
+ });
66
+
67
+ it('should initialize custom plugins passed to context', async () => {
68
+ class CustomPlugin extends ContextPlugin {
69
+ static get pluginName() {
70
+ return 'CustomPlugin';
71
+ }
72
+ }
73
+
74
+ CustomEditorPluginsRegistry.the.register('CustomPlugin', () => CustomPlugin);
75
+
76
+ renderTestContext(createContextSnapshot(DEFAULT_TEST_CONTEXT_ID, {
77
+ config: {
78
+ plugins: ['CustomPlugin'],
79
+ },
80
+ }));
81
+
82
+ const { context } = await waitForTestContext();
83
+
84
+ expect(context?.plugins.get('CustomPlugin')).toBeInstanceOf(CustomPlugin);
85
+ });
86
+
87
+ it('registered plugins should support custom translations', async () => {
88
+ class CustomPlugin extends ContextPlugin {
89
+ static get pluginName() {
90
+ return 'CustomPlugin';
91
+ }
92
+
93
+ getHelloTitle() {
94
+ return this.context.t('HELLO');
95
+ }
96
+ }
97
+
98
+ CustomEditorPluginsRegistry.the.register('CustomPlugin', () => CustomPlugin);
99
+
100
+ renderTestContext(createContextSnapshot(DEFAULT_TEST_CONTEXT_ID, {
101
+ customTranslations: {
102
+ en: {
103
+ HELLO: 'Hello from CustomPlugin',
104
+ },
105
+ },
106
+ config: {
107
+ plugins: ['CustomPlugin'],
108
+ },
109
+ }));
110
+
111
+ const { context } = await waitForTestContext();
112
+ const plugin = context?.plugins.get('CustomPlugin') as CustomPlugin;
113
+
114
+ expect(plugin.getHelloTitle()).toBe('Hello from CustomPlugin');
115
+ });
116
+
117
+ it('should support custom language for context translations', async () => {
118
+ class CustomPlugin extends ContextPlugin {
119
+ static get pluginName() {
120
+ return 'CustomPlugin';
121
+ }
122
+
123
+ getHelloTitle() {
124
+ return this.context.t('HELLO');
125
+ }
126
+ }
127
+
128
+ CustomEditorPluginsRegistry.the.register('CustomPlugin', () => CustomPlugin);
129
+
130
+ renderTestContext(createContextSnapshot(
131
+ DEFAULT_TEST_CONTEXT_ID,
132
+ {
133
+ customTranslations: {
134
+ en: {
135
+ HELLO: 'Hello from CustomPlugin',
136
+ },
137
+ pl: {
138
+ HELLO: 'Witaj z CustomPlugin',
139
+ },
140
+ },
141
+ config: {
142
+ plugins: ['CustomPlugin'],
143
+ },
144
+ },
145
+ {
146
+ ui: 'pl',
147
+ content: 'pl',
148
+ },
149
+ ));
150
+
151
+ const { context } = await waitForTestContext();
152
+ const plugin = context?.plugins.get('CustomPlugin') as CustomPlugin;
153
+
154
+ expect(plugin.getHelloTitle()).toBe('Witaj z CustomPlugin');
155
+ });
156
+
157
+ it('should support translations references in config', async () => {
158
+ renderTestContext(createContextSnapshot(
159
+ DEFAULT_TEST_CONTEXT_ID,
160
+ {
161
+ customTranslations: {
162
+ en: {
163
+ HELLO: 'Hello from CustomPlugin',
164
+ },
165
+ pl: {
166
+ HELLO: 'Witaj z CustomPlugin',
167
+ },
168
+ },
169
+ config: {
170
+ customConfig: { $translation: 'HELLO' },
171
+ },
172
+ },
173
+ {
174
+ ui: 'pl',
175
+ content: 'pl',
176
+ },
177
+ ));
178
+
179
+ const { context } = await waitForTestContext();
180
+ const translation = context?.config.get('customConfig') as string;
181
+
182
+ expect(translation).toBe('Witaj z CustomPlugin');
183
+ });
184
+ });
185
+
186
+ describe('attaching editor', () => {
187
+ it('should not attach editor to the context (editor has no specified context, context initialized)', async () => {
188
+ renderTestContext();
189
+
190
+ const { context } = await waitForTestContext();
191
+
192
+ renderTestEditor();
193
+
194
+ await waitForTestEditor();
195
+
196
+ expect(context?.editors.first).toBeNull();
197
+ });
198
+
199
+ it('should attach editor to the context (editor has specified context, context initialized)', async () => {
200
+ renderTestContext();
201
+
202
+ const { context } = await waitForTestContext();
203
+
204
+ renderTestEditor({
205
+ contextId: DEFAULT_TEST_CONTEXT_ID,
206
+ });
207
+
208
+ const editor = await waitForTestEditor();
209
+
210
+ expect(context?.editors.first).toEqual(editor);
211
+ });
212
+
213
+ it('should pause editor initialization when context is not yet initialized', async () => {
214
+ renderTestEditor({
215
+ contextId: DEFAULT_TEST_CONTEXT_ID,
216
+ });
217
+
218
+ await timeout(50);
219
+
220
+ renderTestContext();
221
+
222
+ const { context } = await waitForTestContext();
223
+ const editor = await waitForTestEditor();
224
+
225
+ expect(context?.editors.first).toEqual(editor);
226
+ });
227
+
228
+ it('should be possible to attach multiple editors to the same context', async () => {
229
+ renderTestContext();
230
+
231
+ const { context } = await waitForTestContext();
232
+
233
+ renderTestEditor({
234
+ editorId: 'editor-1',
235
+ contextId: DEFAULT_TEST_CONTEXT_ID,
236
+ });
237
+
238
+ renderTestEditor({
239
+ editorId: 'editor-2',
240
+ contextId: DEFAULT_TEST_CONTEXT_ID,
241
+ });
242
+
243
+ const editors = await Promise.all([
244
+ waitForTestEditor('editor-1'),
245
+ waitForTestEditor('editor-2'),
246
+ ]);
247
+
248
+ expect(context?.editors.length).toBe(2);
249
+ expect([...context!.editors]).toEqual(editors);
250
+ });
251
+ });
252
+
253
+ describe('destroy', () => {
254
+ it('destroyed editor is removed from the context editors collection', async () => {
255
+ renderTestContext();
256
+
257
+ const { context } = await waitForTestContext();
258
+
259
+ const editorEl = renderTestEditor({
260
+ contextId: DEFAULT_TEST_CONTEXT_ID,
261
+ });
262
+
263
+ const editor = await waitForTestEditor();
264
+
265
+ expect(context?.editors.first).toEqual(editor);
266
+
267
+ editorEl.remove();
268
+
269
+ await expect.poll(() => context?.editors.first).toBeNull();
270
+ });
271
+
272
+ it('should remove the context instance from the registry on destroy', async () => {
273
+ const el = renderTestContext({ contextId: 'my-context' });
274
+
275
+ const watchdog = await ContextsRegistry.the.waitFor('my-context');
276
+
277
+ expect(watchdog).toBeInstanceOf(ContextWatchdog);
278
+
279
+ el.remove();
280
+
281
+ await expect.poll(() => ContextsRegistry.the.hasItem('my-context')).toBe(false);
282
+ expect(watchdog.state).toBe('destroyed');
283
+ });
284
+
285
+ it('should hide element during destruction', async () => {
286
+ const el = renderTestContext();
287
+
288
+ el.remove();
289
+
290
+ expect(el.style.display).toBe('none');
291
+ });
292
+
293
+ it('should handle destruction when mounted promise is not resolved yet', async () => {
294
+ const el = renderTestContext();
295
+
296
+ el.remove();
297
+
298
+ expect(el.style.display).toBe('none');
299
+ });
300
+
301
+ it('destroying context should destroy all attached editors', async () => {
302
+ const contextEl = renderTestContext();
303
+
304
+ await waitForTestContext();
305
+
306
+ const editorEl = renderTestEditor({
307
+ contextId: DEFAULT_TEST_CONTEXT_ID,
308
+ });
309
+ const editorId = editorEl.getAttribute('data-cke-editor-id')!;
310
+
311
+ const editor = await waitForTestEditor();
312
+
313
+ expect(editor.state).toBe('ready');
314
+
315
+ contextEl.remove();
316
+
317
+ await expect.poll(() => editor.state).toBe('destroyed');
318
+
319
+ expect(ContextsRegistry.the.hasItem(DEFAULT_TEST_CONTEXT_ID)).toBe(false);
320
+ await expect.poll(() => EditorsRegistry.the.hasItem(editorId)).toBe(false);
321
+ });
322
+ });
323
+ });
@@ -0,0 +1,128 @@
1
+ import type { WaitForInteractiveResult } from '../../shared';
2
+ import type { EditorLanguage } from '../editor';
3
+ import type { ContextConfig } from './typings';
4
+ import type { Context, ContextWatchdog } from 'ckeditor5';
5
+
6
+ import { isEmptyObject, waitForDOMReady, waitForInteractiveAttribute } from '../../shared';
7
+ import {
8
+ loadAllEditorTranslations,
9
+ loadEditorPlugins,
10
+ normalizeCustomTranslations,
11
+ resolveEditorConfigElementReferences,
12
+ resolveEditorConfigTranslations,
13
+ } from '../editor/utils';
14
+ import { ContextsRegistry } from './contexts-registry';
15
+
16
+ /**
17
+ * The Blazor hook that mounts CKEditor context instances.
18
+ */
19
+ export class ContextComponentElement extends HTMLElement {
20
+ /**
21
+ * The promise that resolves to the context instance.
22
+ */
23
+ private contextPromise: Promise<ContextWatchdog<Context>> | null = null;
24
+
25
+ /**
26
+ * Wait result for the interactive attribute.
27
+ */
28
+ private interactiveWait?: WaitForInteractiveResult;
29
+
30
+ /**
31
+ * Mounts the context component.
32
+ */
33
+ async connectedCallback() {
34
+ await waitForDOMReady();
35
+
36
+ // By default, components do not bootstrap from web components.
37
+ // They bootstrap only when they receive the data-cke-interactive flag, which the interop sets.
38
+ // This is a fallback for situations where CKEditor 5 is rendered on a non-interactive page.
39
+ this.interactiveWait = waitForInteractiveAttribute(this);
40
+
41
+ await this.interactiveWait.promise;
42
+ await this.initializeContext();
43
+ }
44
+
45
+ /**
46
+ * Initializes the context component.
47
+ */
48
+ private async initializeContext(): Promise<void> {
49
+ const contextId = this.getAttribute('data-cke-context-id')!;
50
+ const language = JSON.parse(this.getAttribute('data-cke-language')!) as EditorLanguage;
51
+ const contextConfig = JSON.parse(this.getAttribute('data-cke-context')!) as ContextConfig;
52
+
53
+ const { customTranslations, watchdogConfig, config: { plugins, ...config } } = contextConfig;
54
+ const { loadedPlugins, hasPremium } = await loadEditorPlugins(plugins ?? []);
55
+
56
+ // Mix custom translations with loaded translations.
57
+ const loadedTranslations = await loadAllEditorTranslations(language, hasPremium);
58
+ const mixedTranslations = [
59
+ ...loadedTranslations,
60
+ normalizeCustomTranslations(customTranslations || {}),
61
+ ]
62
+ .filter(translations => !isEmptyObject(translations));
63
+
64
+ // Initialize context with watchdog.
65
+ this.contextPromise = (async () => {
66
+ const { ContextWatchdog, Context } = await import('ckeditor5');
67
+
68
+ const instance = new ContextWatchdog(Context, {
69
+ crashNumberLimit: 10,
70
+ ...watchdogConfig,
71
+ });
72
+
73
+ // Construct parsed config. First resolve DOM element references in the provided configuration.
74
+ let resolvedConfig = resolveEditorConfigElementReferences(config);
75
+
76
+ // Then resolve translation references in the provided configuration, using the mixed translations.
77
+ resolvedConfig = resolveEditorConfigTranslations([...mixedTranslations].reverse(), language.ui, resolvedConfig);
78
+
79
+ await instance.create({
80
+ ...resolvedConfig,
81
+ language,
82
+ plugins: loadedPlugins,
83
+ ...mixedTranslations.length && {
84
+ translations: mixedTranslations,
85
+ },
86
+ });
87
+
88
+ instance.on('itemError', (...args) => {
89
+ console.error('Context item error:', ...args);
90
+ });
91
+
92
+ return instance;
93
+ })();
94
+
95
+ const context = await this.contextPromise;
96
+
97
+ if (this.isConnected) {
98
+ ContextsRegistry.the.register(contextId, context);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Destroys the context component. Unmounts root from the editor.
104
+ */
105
+ async disconnectedCallback() {
106
+ // Disconnect the observer if present.
107
+ this.interactiveWait?.disconnect();
108
+
109
+ const contextId = this.getAttribute('data-cke-context-id');
110
+
111
+ // Let's hide the element during destruction to prevent flickering.
112
+ this.style.display = 'none';
113
+
114
+ // Let's wait for the mounted promise to resolve before proceeding with destruction.
115
+ try {
116
+ const context = await this.contextPromise;
117
+
118
+ await context?.destroy();
119
+ }
120
+ finally {
121
+ this.contextPromise = null;
122
+
123
+ if (contextId && ContextsRegistry.the.hasItem(contextId)) {
124
+ ContextsRegistry.the.unregister(contextId);
125
+ }
126
+ }
127
+ }
128
+ }
@@ -0,0 +1,10 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { AsyncRegistry } from '../../shared/async-registry';
4
+ import { ContextsRegistry } from './contexts-registry';
5
+
6
+ describe('contexts registry', () => {
7
+ it('should be singleton of async registry', () => {
8
+ expect(ContextsRegistry.the).toBeInstanceOf(AsyncRegistry);
9
+ });
10
+ });
@@ -0,0 +1,10 @@
1
+ import type { Context, ContextWatchdog } from 'ckeditor5';
2
+
3
+ import { AsyncRegistry } from '../../shared';
4
+
5
+ /**
6
+ * It provides a way to register contexts and execute callbacks on them when they are available.
7
+ */
8
+ export class ContextsRegistry extends AsyncRegistry<ContextWatchdog<Context>> {
9
+ static readonly the = new ContextsRegistry();
10
+ }
@@ -0,0 +1,3 @@
1
+ export * from './context';
2
+ export * from './contexts-registry';
3
+ export * from './typings';
@@ -0,0 +1,38 @@
1
+ import type { EditorCustomTranslationsDictionary, EditorPlugin } from '../editor';
2
+ import type { WatchdogConfig } from 'ckeditor5';
3
+
4
+ /**
5
+ * Configuration object for CKEditor5 context instance.
6
+ */
7
+ export type ContextConfig = {
8
+ /**
9
+ * Optional custom translations for the editor.
10
+ * This allows for localization of the editor interface.
11
+ */
12
+ customTranslations?: EditorCustomTranslationsDictionary | null;
13
+
14
+ /**
15
+ * Watchdog configuration for the context.
16
+ */
17
+ watchdogConfig: WatchdogConfig | null;
18
+
19
+ /**
20
+ * Configuration options for the context instance.
21
+ */
22
+ config: ContextCreatorConfig;
23
+ };
24
+
25
+ /**
26
+ * Configuration options for the context instance.
27
+ */
28
+ export type ContextCreatorConfig = {
29
+ /**
30
+ * Array of plugin identifiers to be loaded by the editor.
31
+ */
32
+ plugins?: EditorPlugin[];
33
+
34
+ /**
35
+ * Other configuration options are flexible and can be any key-value pairs.
36
+ */
37
+ [key: string]: any;
38
+ };