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,84 @@
1
+ import { CKEditor5BlazorError } from '../ckeditor5-blazor-error';
2
+ import { waitForDOMReady } from '../shared';
3
+ import { EditorsRegistry } from './editor/editors-registry';
4
+ import { queryAllEditorIds } from './editor/utils';
5
+
6
+ /**
7
+ * UI Part hook for Blazor. It allows you to create UI parts for multi-root editors.
8
+ */
9
+ export class UIPartComponentElement extends HTMLElement {
10
+ /**
11
+ * The promise that resolves when the UI part is mounted.
12
+ */
13
+ private mountedPromise: Promise<void> | null = null;
14
+
15
+ /**
16
+ * Mounts the UI part component.
17
+ */
18
+ async connectedCallback() {
19
+ await waitForDOMReady();
20
+
21
+ const editorId = this.getAttribute('data-cke-editor-id') || queryAllEditorIds()[0]!;
22
+ const name = this.getAttribute('data-cke-name');
23
+
24
+ /* v8 ignore next if -- @preserve */
25
+ if (!editorId || !name) {
26
+ return;
27
+ }
28
+
29
+ // If the editor is not registered yet, we will wait for it to be registered.
30
+ this.style.display = 'block';
31
+ this.mountedPromise = EditorsRegistry.the.execute(editorId, (editor) => {
32
+ if (!this.isConnected) {
33
+ return;
34
+ }
35
+
36
+ const { ui } = editor;
37
+
38
+ const uiViewName = mapUIPartView(name);
39
+ const uiPart = (ui.view as any)[uiViewName!];
40
+
41
+ /* v8 ignore next if -- @preserve */
42
+ if (!uiPart) {
43
+ throw new CKEditor5BlazorError(`Unknown UI part name: "${name}". Supported names are "toolbar" and "menubar".`);
44
+ }
45
+
46
+ this.appendChild(uiPart.element);
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Destroys the UI part component. Unmounts UI parts from the editor.
52
+ */
53
+ async disconnectedCallback() {
54
+ // Let's hide the element during destruction to prevent flickering.
55
+ this.style.display = 'none';
56
+
57
+ // Let's wait for the mounted promise to resolve before proceeding with destruction.
58
+ await this.mountedPromise;
59
+ this.mountedPromise = null;
60
+
61
+ // Unmount all UI parts from the editor.
62
+ this.innerHTML = '';
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Maps the UI part name to the corresponding view in the editor.
68
+ *
69
+ * @param name The name of the UI part.
70
+ * @returns The name of the view in the editor.
71
+ */
72
+ function mapUIPartView(name: string): string | null {
73
+ switch (name) {
74
+ case 'toolbar':
75
+ return 'toolbar';
76
+
77
+ case 'menubar':
78
+ return 'menuBarView';
79
+
80
+ /* v8 ignore next -- @preserve */
81
+ default:
82
+ return null;
83
+ }
84
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ export { CKEditor5BlazorError } from './ckeditor5-blazor-error';
2
+ export { ensureEditorElementsRegistered } from './elements';
3
+ export { ContextsRegistry } from './elements/context/contexts-registry';
4
+ export { EditableComponentElement } from './elements/editable';
5
+ export { EditorComponentElement } from './elements/editor';
6
+ export { CustomEditorPluginsRegistry } from './elements/editor/custom-editor-plugins';
7
+ export { EditorsRegistry } from './elements/editor/editors-registry';
8
+ export { CKEditor5ChangeDataEvent } from './elements/editor/plugins/dispatch-editor-roots-change-event';
9
+ export { UIPartComponentElement } from './elements/ui-part';
10
+ export {
11
+ createContextBlazorInterop,
12
+ createEditableBlazorInterop,
13
+ createEditorBlazorInterop,
14
+ createUIPartBlazorInterop,
15
+ } from './interop';
@@ -0,0 +1,30 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+
3
+ import { createContextBlazorInterop } from './create-context-blazor-interop';
4
+
5
+ describe('createContextBlazorInterop', () => {
6
+ let element: HTMLElement;
7
+
8
+ beforeEach(() => {
9
+ element = document.createElement('ckeditor5-editable');
10
+ document.body.appendChild(element);
11
+ });
12
+
13
+ afterEach(() => {
14
+ element.remove();
15
+ });
16
+
17
+ it('should mark the element as interactive after initialization', () => {
18
+ expect(element.hasAttribute('data-cke-interactive')).toBe(false);
19
+
20
+ createContextBlazorInterop(element);
21
+
22
+ expect(element.getAttribute('data-cke-interactive')).toBe('true');
23
+ });
24
+
25
+ it('unmount should not throw', () => {
26
+ const interop = createContextBlazorInterop(element);
27
+
28
+ expect(() => interop.unmount()).not.toThrow();
29
+ });
30
+ });
@@ -0,0 +1,15 @@
1
+ import { markElementAsInteractive } from '../shared';
2
+
3
+ /**
4
+ * Creates a simple interop layer for CKEditor 5 context to set the interactive attribute.
5
+ *
6
+ * @param element - The root HTML element of the context component, used to identify
7
+ * the context instance and attach necessary attributes.
8
+ */
9
+ export function createContextBlazorInterop(element: HTMLElement) {
10
+ markElementAsInteractive(element);
11
+
12
+ return {
13
+ unmount() {},
14
+ };
15
+ }
@@ -0,0 +1,213 @@
1
+ import type { DotNetInterop } from '../types';
2
+ import type { Mock } from 'vitest';
3
+
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+
6
+ import {
7
+ createDotnet,
8
+ createDotNetInteropMock,
9
+ DEFAULT_TEST_EDITOR_ID,
10
+ renderTestEditable,
11
+ renderTestEditor,
12
+ waitForDestroyAllEditors,
13
+ waitForTestEditor,
14
+ } from '~/test-utils';
15
+
16
+ import { CKEditor5ChangeDataEvent } from '../elements/editor/plugins/dispatch-editor-roots-change-event';
17
+ import { ensureEditorElementsRegistered } from '../elements/ensure-editor-elements-registered';
18
+ import { createEditableBlazorInterop } from './create-editable-blazor-interop';
19
+
20
+ describe('createEditableBlazorInterop', () => {
21
+ let element: HTMLElement;
22
+ let dotnetInterop: DotNetInterop;
23
+
24
+ beforeEach(async () => {
25
+ document.body.innerHTML = '';
26
+ ensureEditorElementsRegistered();
27
+
28
+ dotnetInterop = createDotNetInteropMock();
29
+ globalThis.DotNet = createDotnet();
30
+
31
+ element = renderTestEditable({}, {
32
+ interactive: false,
33
+ });
34
+
35
+ renderTestEditor();
36
+ await waitForTestEditor();
37
+ });
38
+
39
+ afterEach(async () => {
40
+ vi.useRealTimers();
41
+ vi.resetAllMocks();
42
+
43
+ globalThis.DotNet = undefined as unknown as typeof DotNet;
44
+ document.body.innerHTML = '';
45
+
46
+ await waitForDestroyAllEditors();
47
+ });
48
+
49
+ it('sets [data-cke-interactive=true] attribute on the element', async () => {
50
+ expect(element.hasAttribute('data-cke-interactive')).toBe(false);
51
+
52
+ createEditableBlazorInterop(element, dotnetInterop);
53
+
54
+ const editor = await waitForTestEditor();
55
+
56
+ expect(editor).toBeDefined();
57
+ expect(element.getAttribute('data-cke-interactive')).toBe('true');
58
+ });
59
+
60
+ it('defaults rootName to "main" when attribute is missing', async () => {
61
+ element.removeAttribute('data-cke-root-name');
62
+
63
+ createEditableBlazorInterop(element, dotnetInterop);
64
+
65
+ const editor = await waitForTestEditor();
66
+ const changeEvent = new CKEditor5ChangeDataEvent({
67
+ editorId: DEFAULT_TEST_EDITOR_ID,
68
+ editor,
69
+ roots: { main: 'changed' },
70
+ });
71
+
72
+ document.body.dispatchEvent(changeEvent);
73
+
74
+ expect(dotnetInterop.invokeMethodAsync).toHaveBeenCalledWith(
75
+ 'OnChangeEditableData',
76
+ expect.anything(),
77
+ 'changed',
78
+ );
79
+ });
80
+
81
+ it('should ignore ckeditor5 change events from other editors', async () => {
82
+ createEditableBlazorInterop(element, dotnetInterop);
83
+
84
+ const editor = await waitForTestEditor();
85
+
86
+ const changeEvent = new CKEditor5ChangeDataEvent({
87
+ editorId: 'other-editor',
88
+ editor,
89
+ roots: { main: 'changed' },
90
+ });
91
+
92
+ document.body.dispatchEvent(changeEvent);
93
+
94
+ expect(dotnetInterop.invokeMethodAsync).not.toHaveBeenCalledWith(
95
+ 'OnChangeEditableData',
96
+ expect.anything(),
97
+ expect.anything(),
98
+ );
99
+ });
100
+
101
+ it('should ignore events for an unknown root name', async () => {
102
+ const custom = renderTestEditable({ rootName: 'other' }, { interactive: false });
103
+
104
+ const interop = createEditableBlazorInterop(custom, dotnetInterop);
105
+ const editor = await waitForTestEditor();
106
+
107
+ const changeEvent = new CKEditor5ChangeDataEvent({
108
+ editorId: DEFAULT_TEST_EDITOR_ID,
109
+ editor,
110
+ roots: { unknown: 'value' },
111
+ });
112
+
113
+ (dotnetInterop.invokeMethodAsync as Mock).mockClear();
114
+ document.body.dispatchEvent(changeEvent);
115
+ expect(dotnetInterop.invokeMethodAsync).not.toHaveBeenCalled();
116
+
117
+ interop.unmount();
118
+ });
119
+
120
+ describe('setValue', () => {
121
+ it('should be possible to call `setValue` while editor is initializing', async () => {
122
+ const { setValue } = createEditableBlazorInterop(element, dotnetInterop);
123
+
124
+ expect(() => setValue('test')).not.toThrow();
125
+
126
+ const editor = await waitForTestEditor();
127
+
128
+ expect(editor.getData()).toBe('<p>test</p>');
129
+ });
130
+
131
+ it('should not set data if the interop is unmounted before the editor is ready', async () => {
132
+ const { setValue, unmount } = createEditableBlazorInterop(element, dotnetInterop);
133
+
134
+ unmount();
135
+
136
+ expect(() => setValue('test')).not.toThrow();
137
+
138
+ const editor = await waitForTestEditor();
139
+
140
+ expect(editor.getData()).toBe('<p>Initial content</p>');
141
+ });
142
+
143
+ it('should delay setting data if the editor is focused', async () => {
144
+ const { setValue } = createEditableBlazorInterop(element, dotnetInterop);
145
+ const editor = await waitForTestEditor();
146
+
147
+ editor.ui.focusTracker.isFocused = true;
148
+
149
+ await setValue('focused update');
150
+
151
+ expect(editor.getData()).toBe('<p>Initial content</p>');
152
+
153
+ editor.ui.focusTracker.isFocused = false;
154
+
155
+ await vi.waitFor(() => expect(editor.getData()).toBe('<p>focused update</p>'));
156
+ });
157
+
158
+ it('treats null/undefined editor data as empty string when syncing after focus', async () => {
159
+ const { setValue } = createEditableBlazorInterop(element, dotnetInterop);
160
+
161
+ const editor = await waitForTestEditor();
162
+ const originalGetData = editor.getData;
163
+
164
+ (editor as any).getData = () => null;
165
+
166
+ editor.ui.focusTracker.isFocused = true;
167
+ await setValue('from-blazor');
168
+ expect(editor.getData()).toBe(null);
169
+
170
+ editor.ui.focusTracker.isFocused = false;
171
+ (editor as any).getData = originalGetData;
172
+ expect(editor.getData()).toBe('<p>from-blazor</p>');
173
+ });
174
+ });
175
+
176
+ describe('unmount', () => {
177
+ it('should remove event listeners and prevent future updates', async () => {
178
+ const interop = createEditableBlazorInterop(element, dotnetInterop);
179
+ const editor = await waitForTestEditor();
180
+
181
+ const changeEvent = new CKEditor5ChangeDataEvent({
182
+ editorId: DEFAULT_TEST_EDITOR_ID,
183
+ editor,
184
+ roots: { main: 'changed' },
185
+ });
186
+
187
+ document.body.dispatchEvent(changeEvent);
188
+
189
+ expect(dotnetInterop.invokeMethodAsync).toHaveBeenCalledWith(
190
+ 'OnChangeEditableData',
191
+ expect.anything(),
192
+ 'changed',
193
+ );
194
+
195
+ (dotnetInterop.invokeMethodAsync as Mock).mockClear();
196
+
197
+ interop.unmount();
198
+ document.body.dispatchEvent(changeEvent);
199
+
200
+ expect(dotnetInterop.invokeMethodAsync).not.toBeCalled();
201
+ });
202
+
203
+ it('should not crash if called twice', async () => {
204
+ const interop = createEditableBlazorInterop(element, dotnetInterop);
205
+ await waitForTestEditor();
206
+
207
+ expect(() => {
208
+ interop.unmount();
209
+ interop.unmount();
210
+ }).not.toThrow();
211
+ });
212
+ });
213
+ });
@@ -0,0 +1,98 @@
1
+ import type { DotNetInterop } from '../types';
2
+
3
+ import { EditorsRegistry } from '../elements/editor/editors-registry';
4
+ import { CKEditor5ChangeDataEvent } from '../elements/editor/plugins/dispatch-editor-roots-change-event';
5
+ import { markElementAsInteractive } from '../shared';
6
+ import { createEditorValueSync, createNoopSync } from './utils/create-editor-value-sync';
7
+
8
+ /**
9
+ * Creates an interop layer to synchronize a single CKEditor 5 editable root with a Blazor component.
10
+ *
11
+ * @param element - The root HTML element of the editable component, used to identify
12
+ * the editable instance and attach necessary attributes.
13
+ * @param interop - The .NET object reference to trigger Blazor methods.
14
+ * @returns An object containing lifecycle and synchronization methods.
15
+ */
16
+ export function createEditableBlazorInterop(element: HTMLElement, interop: DotNetInterop) {
17
+ const editorId = element.getAttribute('data-cke-editor-id');
18
+ const rootName = element.getAttribute('data-cke-root-name') ?? 'main';
19
+
20
+ let unmounted = false;
21
+ let sync = createNoopSync<string>();
22
+ let editorRef: unknown | null = null;
23
+
24
+ /**
25
+ * Handles data change events dispatched by the CKEditor plugin.
26
+ * Filters by both editorId and rootName, then notifies Blazor if the root value changed.
27
+ * The callback now includes a JS object reference to the underlying editor instance.
28
+ */
29
+ const onDataChange = (event: Event) => {
30
+ if (!(event instanceof CKEditor5ChangeDataEvent) || event.detail.editorId !== editorId) {
31
+ return;
32
+ }
33
+
34
+ const newValue = event.detail.roots[rootName];
35
+
36
+ if (newValue === undefined) {
37
+ return;
38
+ }
39
+
40
+ if (sync.shouldNotify(newValue)) {
41
+ void interop.invokeMethodAsync('OnChangeEditableData', editorRef, newValue);
42
+ }
43
+ };
44
+
45
+ /**
46
+ * Initializes the focus tracker and model listeners for the editor owning this editable.
47
+ */
48
+ const initializeSynchronization = async () => {
49
+ const editor = await EditorsRegistry.the.waitFor(editorId);
50
+ editorRef = DotNet.createJSObjectReference(editor);
51
+
52
+ sync = createEditorValueSync(editor, {
53
+ getCurrentValue: () => editor.getData({ rootName }) ?? '',
54
+ applyValue: value => editor.setData({ [rootName]: value }),
55
+ isEqual: (a, b) => a === b,
56
+ });
57
+ };
58
+
59
+ void initializeSynchronization();
60
+ document.body.addEventListener(CKEditor5ChangeDataEvent.EVENT_NAME, onDataChange);
61
+ markElementAsInteractive(element);
62
+
63
+ return {
64
+ /**
65
+ * Cleans up all event listeners when the Blazor component is disposed.
66
+ */
67
+ unmount() {
68
+ if (unmounted) {
69
+ return;
70
+ }
71
+
72
+ document.body.removeEventListener(CKEditor5ChangeDataEvent.EVENT_NAME, onDataChange);
73
+ sync.unmount();
74
+
75
+ if (editorRef) {
76
+ DotNet.disposeJSObjectReference(editorRef);
77
+ editorRef = null;
78
+ }
79
+
80
+ unmounted = true;
81
+ },
82
+
83
+ /**
84
+ * Updates this editable root's data from Blazor.
85
+ * If the editor is focused, the update is deferred until blur to avoid interrupting the user.
86
+ */
87
+ setValue: async (value: string) => {
88
+ if (unmounted) {
89
+ return;
90
+ }
91
+
92
+ await EditorsRegistry.the.waitFor(editorId);
93
+
94
+ // Ensure sync is initialized before forwarding (waitFor guarantees the editor exists)
95
+ sync.setValue(value);
96
+ },
97
+ };
98
+ }
@@ -0,0 +1,183 @@
1
+ import type { DotNetInterop } from '../types';
2
+ import type { Mock } from 'vitest';
3
+
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+
6
+ import {
7
+ createDotnet,
8
+ createDotNetInteropMock,
9
+ DEFAULT_TEST_EDITOR_ID,
10
+ renderTestEditor,
11
+ waitForDestroyAllEditors,
12
+ waitForTestEditor,
13
+ } from '~/test-utils';
14
+
15
+ import { timeout } from '../../src/shared';
16
+ import { CKEditor5ChangeDataEvent } from '../elements/editor/plugins/dispatch-editor-roots-change-event';
17
+ import { ensureEditorElementsRegistered } from '../elements/ensure-editor-elements-registered';
18
+ import { createEditorBlazorInterop } from './create-editor-blazor-interop';
19
+
20
+ describe('createEditorBlazorInterop', () => {
21
+ let element: HTMLElement;
22
+ let dotnetInterop: DotNetInterop;
23
+
24
+ beforeEach(() => {
25
+ document.body.innerHTML = '';
26
+ ensureEditorElementsRegistered();
27
+
28
+ dotnetInterop = createDotNetInteropMock();
29
+ globalThis.DotNet = createDotnet();
30
+
31
+ element = renderTestEditor({}, {
32
+ interactive: false,
33
+ });
34
+ });
35
+
36
+ afterEach(async () => {
37
+ vi.useRealTimers();
38
+ vi.resetAllMocks();
39
+
40
+ globalThis.DotNet = undefined as unknown as typeof DotNet;
41
+ document.body.innerHTML = '';
42
+
43
+ await waitForDestroyAllEditors();
44
+ });
45
+
46
+ it('sets [data-cke-interactive=true] attribute on the element', async () => {
47
+ expect(element.hasAttribute('data-cke-interactive')).toBe(false);
48
+
49
+ createEditorBlazorInterop(element, dotnetInterop);
50
+
51
+ const editor = await waitForTestEditor();
52
+
53
+ expect(editor).toBeDefined();
54
+ expect(element.getAttribute('data-cke-interactive')).toBe('true');
55
+ });
56
+
57
+ it('should ignore ckeditor5 change events from other editors', async () => {
58
+ createEditorBlazorInterop(element, dotnetInterop);
59
+
60
+ const editor = await waitForTestEditor();
61
+
62
+ const changeEvent = new CKEditor5ChangeDataEvent({
63
+ editorId: 'other-editor',
64
+ editor,
65
+ roots: { main: 'changed' },
66
+ });
67
+
68
+ document.body.dispatchEvent(changeEvent);
69
+
70
+ expect(dotnetInterop.invokeMethodAsync).not.toHaveBeenCalledWith(
71
+ 'OnChangeEditorData',
72
+ expect.anything(),
73
+ expect.anything(),
74
+ );
75
+ });
76
+
77
+ describe('setValue', () => {
78
+ it('should be possible to call `setValue` while editor is initializing', async () => {
79
+ const { setValue } = createEditorBlazorInterop(element, dotnetInterop);
80
+
81
+ expect(() => setValue({ main: 'test' })).not.toThrow();
82
+
83
+ const editor = await waitForTestEditor();
84
+
85
+ expect(editor.getData()).toBe('<p>test</p>');
86
+ });
87
+
88
+ it('should not set data if the interop is unmounted before the editor is ready', async () => {
89
+ const { setValue, unmount } = createEditorBlazorInterop(element, dotnetInterop);
90
+
91
+ unmount();
92
+
93
+ expect(() => setValue({ main: 'test' })).not.toThrow();
94
+
95
+ const editor = await waitForTestEditor();
96
+
97
+ expect(editor.getData()).toBe('<p>Initial content</p>');
98
+ });
99
+
100
+ it('should delay setting data if the editor is focused', async () => {
101
+ const { setValue } = createEditorBlazorInterop(element, dotnetInterop);
102
+ const editor = await waitForTestEditor();
103
+
104
+ editor.ui.focusTracker.isFocused = true;
105
+
106
+ await setValue({ main: 'focused update' });
107
+
108
+ expect(editor.getData()).toBe('<p>Initial content</p>');
109
+
110
+ editor.ui.focusTracker.isFocused = false;
111
+
112
+ await vi.waitFor(() => expect(editor.getData()).toBe('<p>focused update</p>'));
113
+ });
114
+ });
115
+
116
+ describe('focus tracking', () => {
117
+ it('should call OnEditorFocus if editor gets focused', async () => {
118
+ createEditorBlazorInterop(element, dotnetInterop);
119
+
120
+ const editor = await waitForTestEditor();
121
+
122
+ expect(dotnetInterop.invokeMethodAsync).not.toHaveBeenCalledWith('OnEditorFocus', expect.anything());
123
+
124
+ editor.ui.focusTracker.isFocused = true;
125
+
126
+ expect(dotnetInterop.invokeMethodAsync).toHaveBeenCalledWith('OnEditorFocus', expect.anything());
127
+ });
128
+
129
+ it('should call OnEditorBlur if editor gets blurred', async () => {
130
+ createEditorBlazorInterop(element, dotnetInterop);
131
+
132
+ const editor = await waitForTestEditor();
133
+
134
+ expect(dotnetInterop.invokeMethodAsync).not.toHaveBeenCalledWith('OnEditorBlur', expect.anything());
135
+
136
+ editor.ui.focusTracker.isFocused = true;
137
+
138
+ await timeout(0);
139
+
140
+ editor.ui.focusTracker.isFocused = false;
141
+
142
+ await vi.waitFor(() => expect(dotnetInterop.invokeMethodAsync).toHaveBeenCalledWith('OnEditorBlur', expect.anything()));
143
+ });
144
+ });
145
+
146
+ describe('unmount', () => {
147
+ it('should remove event listeners and prevent future updates', async () => {
148
+ const interop = createEditorBlazorInterop(element, dotnetInterop);
149
+ const editor = await waitForTestEditor();
150
+
151
+ const changeEvent = new CKEditor5ChangeDataEvent({
152
+ editorId: DEFAULT_TEST_EDITOR_ID,
153
+ editor,
154
+ roots: { main: 'changed' },
155
+ });
156
+
157
+ document.body.dispatchEvent(changeEvent);
158
+
159
+ expect(dotnetInterop.invokeMethodAsync).toHaveBeenCalledWith(
160
+ 'OnChangeEditorData',
161
+ expect.anything(),
162
+ { main: 'changed' },
163
+ );
164
+
165
+ (dotnetInterop.invokeMethodAsync as Mock).mockClear();
166
+
167
+ interop.unmount();
168
+ document.body.dispatchEvent(changeEvent);
169
+
170
+ expect(dotnetInterop.invokeMethodAsync).not.toBeCalled();
171
+ });
172
+
173
+ it('should not crash if called twice', async () => {
174
+ const interop = createEditorBlazorInterop(element, dotnetInterop);
175
+ await waitForTestEditor();
176
+
177
+ expect(() => {
178
+ interop.unmount();
179
+ interop.unmount();
180
+ }).not.toThrow();
181
+ });
182
+ });
183
+ });