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,85 @@
1
+ import type { CanBePromise } from '../../types';
2
+ import type { PluginConstructor } from 'ckeditor5';
3
+
4
+ import { CKEditor5BlazorError } from '../../ckeditor5-blazor-error';
5
+
6
+ type PluginReader = () => CanBePromise<PluginConstructor>;
7
+
8
+ /**
9
+ * Registry for custom CKEditor plugins.
10
+ * Allows registration and retrieval of custom plugins that can be used alongside built-in plugins.
11
+ */
12
+ export class CustomEditorPluginsRegistry {
13
+ static readonly the = new CustomEditorPluginsRegistry();
14
+
15
+ /**
16
+ * Map of registered custom plugins.
17
+ */
18
+ private readonly plugins = new Map<string, PluginReader>();
19
+
20
+ /**
21
+ * Private constructor to enforce singleton pattern.
22
+ */
23
+ private constructor() {}
24
+
25
+ /**
26
+ * Registers a custom plugin for the CKEditor.
27
+ *
28
+ * @param name The name of the plugin.
29
+ * @param reader The plugin reader function that returns the plugin constructor.
30
+ * @returns A function to unregister the plugin.
31
+ */
32
+ register(name: string, reader: PluginReader): () => void {
33
+ if (this.plugins.has(name)) {
34
+ throw new CKEditor5BlazorError(`Plugin with name "${name}" is already registered.`);
35
+ }
36
+
37
+ this.plugins.set(name, reader);
38
+
39
+ return this.unregister.bind(this, name);
40
+ }
41
+
42
+ /**
43
+ * Removes a custom plugin by its name.
44
+ *
45
+ * @param name The name of the plugin to unregister.
46
+ * @throws Will throw an error if the plugin is not registered.
47
+ */
48
+ unregister(name: string): void {
49
+ if (!this.plugins.has(name)) {
50
+ throw new CKEditor5BlazorError(`Plugin with name "${name}" is not registered.`);
51
+ }
52
+
53
+ this.plugins.delete(name);
54
+ }
55
+
56
+ /**
57
+ * Removes all custom editor plugins.
58
+ * This is useful for cleanup in tests or when reloading plugins.
59
+ */
60
+ unregisterAll(): void {
61
+ this.plugins.clear();
62
+ }
63
+
64
+ /**
65
+ * Retrieves a custom plugin by its name.
66
+ *
67
+ * @param name The name of the plugin.
68
+ * @returns The plugin constructor or undefined if not found.
69
+ */
70
+ async get(name: string): Promise<PluginConstructor | undefined> {
71
+ const reader = this.plugins.get(name);
72
+
73
+ return reader?.();
74
+ }
75
+
76
+ /**
77
+ * Checks if a plugin with the given name is registered.
78
+ *
79
+ * @param name The name of the plugin.
80
+ * @returns `true` if the plugin is registered, `false` otherwise.
81
+ */
82
+ has(name: string): boolean {
83
+ return this.plugins.has(name);
84
+ }
85
+ }
@@ -0,0 +1,562 @@
1
+ import type { LanguageConfig } from 'ckeditor5';
2
+
3
+ import {
4
+ BalloonEditor,
5
+ ClassicEditor,
6
+ DecoupledEditor,
7
+ Editor,
8
+ InlineEditor,
9
+ MultiRootEditor,
10
+ Plugin,
11
+ } from 'ckeditor5';
12
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
13
+
14
+ import {
15
+ createEditableSnapshot,
16
+ createEditorPreset,
17
+ getTestEditorInput,
18
+ html,
19
+ isEditorShown,
20
+ renderTestEditable,
21
+ renderTestEditor,
22
+ waitForDestroyAllEditors,
23
+ waitForTestEditor,
24
+ } from '~/test-utils';
25
+
26
+ import { timeout } from '../../shared/timeout';
27
+ import { ensureEditorElementsRegistered } from '../ensure-editor-elements-registered';
28
+ import { CustomEditorPluginsRegistry } from './custom-editor-plugins';
29
+ import { unwrapEditorWatchdog } from './utils';
30
+
31
+ describe('editor component', () => {
32
+ beforeEach(async () => {
33
+ document.body.innerHTML = '';
34
+ ensureEditorElementsRegistered();
35
+ });
36
+
37
+ afterEach(async () => {
38
+ vi.useRealTimers();
39
+ vi.resetAllMocks();
40
+
41
+ document.body.innerHTML = '';
42
+
43
+ await waitForDestroyAllEditors();
44
+ });
45
+
46
+ it('should save the editor instance in the registry with provided editorId', async () => {
47
+ renderTestEditor();
48
+
49
+ const editor = await waitForTestEditor();
50
+
51
+ expect(editor).toBeInstanceOf(Editor);
52
+ });
53
+
54
+ it('should be possible to pass custom plugins to the editor', async () => {
55
+ class MyCustomPlugin extends Plugin {
56
+ static pluginName = 'MyCustomPlugin';
57
+ }
58
+
59
+ const preset = createEditorPreset('classic', {
60
+ toolbar: [],
61
+ plugins: ['MyCustomPlugin'],
62
+ });
63
+
64
+ CustomEditorPluginsRegistry.the.register('MyCustomPlugin', () => MyCustomPlugin);
65
+ renderTestEditor({ preset });
66
+
67
+ const editor = await waitForTestEditor();
68
+
69
+ expect(editor.plugins.get('MyCustomPlugin')).toBeInstanceOf(MyCustomPlugin);
70
+ });
71
+
72
+ describe('editor types', () => {
73
+ describe('classic', () => {
74
+ it('should create an classic editor with default preset', async () => {
75
+ renderTestEditor();
76
+
77
+ const editor = await waitForTestEditor();
78
+
79
+ expect(editor).to.toBeInstanceOf(ClassicEditor);
80
+ expect(isEditorShown()).toBe(true);
81
+ });
82
+
83
+ it('should assign default value from `content` snapshot property', async () => {
84
+ const initialValue = `<p>Hello World! Today is ${new Date().toLocaleDateString()}</p>`;
85
+
86
+ renderTestEditor({
87
+ content: {
88
+ main: initialValue,
89
+ },
90
+ });
91
+
92
+ const editor = await waitForTestEditor();
93
+
94
+ expect(editor.getData()).toBe(initialValue);
95
+ });
96
+
97
+ it('should assign empty main value if initialized editor with empty `content` snapshot property', async () => {
98
+ renderTestEditor({
99
+ content: {},
100
+ });
101
+
102
+ const editor = await waitForTestEditor();
103
+
104
+ expect(editor.getData()).toBe('');
105
+ });
106
+ });
107
+
108
+ describe('inline', () => {
109
+ it('should create an inline editor with default preset', async () => {
110
+ renderTestEditor({ preset: createEditorPreset('inline') });
111
+
112
+ const editor = await waitForTestEditor();
113
+
114
+ expect(editor).to.toBeInstanceOf(InlineEditor);
115
+ expect(isEditorShown()).toBe(true);
116
+ });
117
+ });
118
+
119
+ describe('decoupled', () => {
120
+ it('should create a decoupled editor with `main` editable and default preset', async () => {
121
+ renderTestEditor({ preset: createEditorPreset('decoupled') });
122
+ renderTestEditable(createEditableSnapshot('main', null));
123
+
124
+ const editor = await waitForTestEditor();
125
+
126
+ expect(editor).to.toBeInstanceOf(DecoupledEditor);
127
+ expect(isEditorShown()).toBe(true);
128
+ });
129
+
130
+ it('should pick initial content from the editable snapshot if provided', async () => {
131
+ const initialEditableContent = '<p>Initial editable content</p>';
132
+
133
+ renderTestEditor({ preset: createEditorPreset('decoupled') });
134
+ renderTestEditable(createEditableSnapshot('main', initialEditableContent));
135
+
136
+ const editor = await waitForTestEditor();
137
+
138
+ expect(editor).to.toBeInstanceOf(DecoupledEditor);
139
+ expect(editor.getData()).toBe(initialEditableContent);
140
+ });
141
+
142
+ it('should wait for root element to be present in DOM if it is not', async () => {
143
+ renderTestEditor({ preset: createEditorPreset('decoupled') });
144
+
145
+ await timeout(200);
146
+
147
+ renderTestEditable(createEditableSnapshot('main', null));
148
+
149
+ const editor = await waitForTestEditor();
150
+
151
+ expect(editor).to.toBeInstanceOf(DecoupledEditor);
152
+ expect(isEditorShown()).toBe(true);
153
+ });
154
+ });
155
+
156
+ describe('balloon', () => {
157
+ it('should create a balloon editor with default preset', async () => {
158
+ renderTestEditor({ preset: createEditorPreset('balloon') });
159
+
160
+ const editor = await waitForTestEditor();
161
+
162
+ expect(editor).to.toBeInstanceOf(BalloonEditor);
163
+ expect(isEditorShown()).toBe(true);
164
+ });
165
+ });
166
+
167
+ describe('multiroot', () => {
168
+ it('should create a multiroot editor without editables in the DOM and initial content', async () => {
169
+ renderTestEditor({
170
+ preset: createEditorPreset('multiroot'),
171
+ content: {},
172
+ });
173
+
174
+ const editor = await waitForTestEditor();
175
+
176
+ expect(editor).toBeInstanceOf(MultiRootEditor);
177
+ });
178
+
179
+ it('should wait and for root elements to be present in DOM if they are not (with content=null value)', async () => {
180
+ renderTestEditor({
181
+ preset: createEditorPreset('multiroot'),
182
+ content: {
183
+ header: '<p>Header root initial content</p>',
184
+ },
185
+ });
186
+
187
+ await timeout(500); // Simulate some delay before adding the root.
188
+
189
+ renderTestEditable(createEditableSnapshot('header'));
190
+
191
+ const editor = await waitForTestEditor();
192
+
193
+ expect(editor).toBeInstanceOf(MultiRootEditor);
194
+ expect(editor.getData({ rootName: 'header' })).toBe('<p>Header root initial content</p>');
195
+ });
196
+
197
+ it('should wait and for root elements to be present in DOM if they are not (with content=\'\' value)', async () => {
198
+ renderTestEditor({
199
+ preset: createEditorPreset('multiroot'),
200
+ content: {
201
+ header: '<p>Header root initial content</p>',
202
+ },
203
+ });
204
+
205
+ await timeout(500); // Simulate some delay before adding the root.
206
+
207
+ renderTestEditable(createEditableSnapshot('header', ''));
208
+
209
+ const editor = await waitForTestEditor();
210
+
211
+ expect(editor).toBeInstanceOf(MultiRootEditor);
212
+ expect(editor.getData({ rootName: 'header' })).toBe('');
213
+ });
214
+
215
+ it('should wait and for root elements to be present in DOM if they are not (with set content value)', async () => {
216
+ renderTestEditor({
217
+ preset: createEditorPreset('multiroot'),
218
+ content: {
219
+ header: '<p>Header root initial content</p>',
220
+ },
221
+ });
222
+
223
+ renderTestEditable(
224
+ createEditableSnapshot('header', '<p>Editable content overrides snapshot content</p>'),
225
+ );
226
+
227
+ const editor = await waitForTestEditor();
228
+
229
+ expect(editor).toBeInstanceOf(MultiRootEditor);
230
+
231
+ await vi.waitFor(() => {
232
+ expect(editor.getData({ rootName: 'header' })).toBe('<p>Editable content overrides snapshot content</p>');
233
+ });
234
+ });
235
+
236
+ it('should not crash after setting content using `setData`', async () => {
237
+ renderTestEditor({
238
+ content: {
239
+ main: '<p>Initial content</p>',
240
+ },
241
+ });
242
+
243
+ const editor = await waitForTestEditor();
244
+
245
+ expect(() => {
246
+ editor.setData('<p>New content</p>');
247
+ }).not.toThrow();
248
+ });
249
+
250
+ it('should update root data if root already exists but editable has different content', async () => {
251
+ renderTestEditor({
252
+ preset: createEditorPreset('multiroot'),
253
+ content: {},
254
+ });
255
+
256
+ const editor = await waitForTestEditor<MultiRootEditor>();
257
+
258
+ editor.addRoot('existingRoot', { data: '<p>Old content</p>' });
259
+
260
+ renderTestEditable(createEditableSnapshot('existingRoot', '<p>New content</p>'));
261
+
262
+ await vi.waitFor(() => {
263
+ expect(editor.getData({ rootName: 'existingRoot' })).toBe('<p>New content</p>');
264
+ });
265
+ });
266
+ });
267
+ });
268
+
269
+ describe('`editableHeight` snapshot parameter`', () => {
270
+ it('should not set any height if `editableHeight` parameter is `null`', async () => {
271
+ renderTestEditor({ editableHeight: null });
272
+
273
+ const editor = await waitForTestEditor();
274
+
275
+ const editableElement = editor.ui.getEditableElement()!;
276
+
277
+ expect(getComputedStyle(editableElement).height).toBe('');
278
+ });
279
+
280
+ it('should set the editable height if `editableHeight` snapshot parameter is provided', async () => {
281
+ renderTestEditor({ editableHeight: 400 });
282
+
283
+ const editor = await waitForTestEditor();
284
+
285
+ const editableElement = editor.ui.getEditableElement()!;
286
+
287
+ expect(getComputedStyle(editableElement).height).toBe('400px');
288
+ });
289
+ });
290
+
291
+ describe('`saveDebounceMs` snapshot parameter`', () => {
292
+ beforeEach(() => {
293
+ vi.useFakeTimers();
294
+ });
295
+
296
+ it('should use parameter to debounce input sync', async () => {
297
+ renderTestEditor({ saveDebounceMs: 400 }, { withInput: true });
298
+
299
+ const editor = await waitForTestEditor();
300
+ const input = getTestEditorInput();
301
+
302
+ editor.setData('<p>Debounce test</p>');
303
+ expect(input.value).to.be.equal('<p>Initial content</p>');
304
+
305
+ await vi.advanceTimersByTimeAsync(399);
306
+ expect(input.value).to.be.equal('<p>Initial content</p>');
307
+
308
+ await vi.advanceTimersByTimeAsync(1);
309
+ expect(input.value).to.be.equal('<p>Debounce test</p>');
310
+ });
311
+ });
312
+
313
+ describe('`language` snapshot parameter`', () => {
314
+ it('should be possible to pass language configuration to the editor configuration', async () => {
315
+ renderTestEditor({
316
+ language: {
317
+ ui: 'pl',
318
+ content: 'de',
319
+ },
320
+ });
321
+
322
+ const editor = await waitForTestEditor();
323
+ const configLanguage = editor.config.get('language') as LanguageConfig;
324
+
325
+ expect(configLanguage.ui).toBe('pl');
326
+ expect(configLanguage.content).toBe('de');
327
+ });
328
+
329
+ it('should have buttons translated to the selected UI language', async () => {
330
+ renderTestEditor({
331
+ language: {
332
+ ui: 'pl',
333
+ content: 'pl',
334
+ },
335
+ });
336
+
337
+ const editor = await waitForTestEditor();
338
+
339
+ expect(editor.t('Bold')).toBe('Pogrubienie');
340
+ });
341
+
342
+ it('should be possible to pass custom translations to the editor', async () => {
343
+ const preset = createEditorPreset('classic', {}, {
344
+ pl: {
345
+ Bold: 'Czcionka grubaśna',
346
+ },
347
+ });
348
+
349
+ renderTestEditor({
350
+ preset,
351
+ language: {
352
+ ui: 'pl',
353
+ content: 'pl',
354
+ },
355
+ });
356
+
357
+ const editor = await waitForTestEditor();
358
+
359
+ expect(editor.t('Bold')).toBe('Czcionka grubaśna');
360
+ });
361
+
362
+ it('should resolve $translation references in the editor configuration', async () => {
363
+ const preset = createEditorPreset(
364
+ 'classic',
365
+ {
366
+ customPlugin: {
367
+ label: { $translation: 'Custom' },
368
+ },
369
+ },
370
+ {
371
+ pl: {
372
+ Custom: 'Mocarna czcionka',
373
+ },
374
+ },
375
+ );
376
+
377
+ renderTestEditor({
378
+ preset,
379
+ language: {
380
+ ui: 'pl',
381
+ content: 'pl',
382
+ },
383
+ });
384
+
385
+ const editor = await waitForTestEditor();
386
+
387
+ expect((editor.config.get('customPlugin') as any).label).toBe('Mocarna czcionka');
388
+ });
389
+ });
390
+
391
+ describe('`watchdog` snapshot parameter`', () => {
392
+ it('should not wrap editor with watchdog if `watchdog` is false', async () => {
393
+ renderTestEditor({
394
+ watchdog: false,
395
+ });
396
+
397
+ const editor = await waitForTestEditor();
398
+ const watchdog = unwrapEditorWatchdog(editor);
399
+
400
+ expect(watchdog).toBeNull();
401
+ });
402
+
403
+ it('should resurrect editor after crashing and broadcast the new instance when `watchdog` is enabled', async () => {
404
+ renderTestEditor({
405
+ editorId: 'editor-with-watchdog',
406
+ watchdog: true,
407
+ });
408
+
409
+ const originalEditor = await waitForTestEditor('editor-with-watchdog');
410
+ const watchdog = unwrapEditorWatchdog(originalEditor)!;
411
+
412
+ (watchdog as any)._restart();
413
+
414
+ await vi.waitFor(async () => {
415
+ const newEditor = await waitForTestEditor('editor-with-watchdog');
416
+
417
+ expect(newEditor).not.equals(originalEditor);
418
+ });
419
+ });
420
+ });
421
+
422
+ describe('form integration', () => {
423
+ beforeEach(() => {
424
+ vi.useFakeTimers();
425
+ });
426
+
427
+ it('should sync editor data to the associated input field', async () => {
428
+ renderTestEditor({ saveDebounceMs: 0 }, { withInput: true });
429
+
430
+ const editor = await waitForTestEditor();
431
+ const input = getTestEditorInput();
432
+
433
+ editor.setData('<p>Form integration test</p>');
434
+ await vi.advanceTimersByTimeAsync(1);
435
+
436
+ expect(input.value).to.be.equal('<p>Form integration test</p>');
437
+ });
438
+
439
+ it('should not crash if hidden input is not present', async () => {
440
+ renderTestEditor({ saveDebounceMs: 0 }, { withInput: false });
441
+
442
+ const editor = await waitForTestEditor();
443
+
444
+ expect(() => {
445
+ editor.setData('<p>Form integration test</p>');
446
+ vi.advanceTimersByTime(1);
447
+ }).not.to.throw();
448
+ });
449
+
450
+ it('should immediately sync editor data to the associated input field on form submit', async () => {
451
+ const form = html.form({ id: 'form' });
452
+ document.body.appendChild(form);
453
+
454
+ renderTestEditor({ saveDebounceMs: 5000 }, { withInput: true, container: form });
455
+
456
+ const editor = await waitForTestEditor();
457
+ const input = getTestEditorInput();
458
+
459
+ editor.setData('<p>Form integration test</p>');
460
+ await vi.advanceTimersByTimeAsync(1000);
461
+
462
+ // Value should not be synced yet due to debounce.
463
+ expect(input.value).to.be.equal('<p>Initial content</p>');
464
+
465
+ // Submit the form.
466
+ form.dispatchEvent(new Event('submit', { bubbles: true }));
467
+ await vi.advanceTimersByTimeAsync(1);
468
+
469
+ // Value should be synced immediately on form submit.
470
+ expect(input.value).to.be.equal('<p>Form integration test</p>');
471
+ });
472
+ });
473
+
474
+ describe('roots change event integration', () => {
475
+ beforeEach(() => {
476
+ vi.useFakeTimers();
477
+ });
478
+
479
+ it('should dispatch `ckeditor5:change:data` event with all roots values for single-root editor', async () => {
480
+ const component = renderTestEditor({ saveDebounceMs: 0 });
481
+ const changeSpy = vi.fn();
482
+ const bodyChangeSpy = vi.fn();
483
+
484
+ component.addEventListener('ckeditor5:change:data', changeSpy);
485
+ document.body.addEventListener('ckeditor5:change:data', bodyChangeSpy);
486
+
487
+ const editor = await waitForTestEditor();
488
+
489
+ editor.setData('<p>Updated main root content</p>');
490
+ await vi.advanceTimersByTimeAsync(1);
491
+
492
+ expect(changeSpy).toHaveBeenCalled();
493
+ expect((changeSpy.mock.lastCall![0] as CustomEvent).detail).toEqual({
494
+ editor,
495
+ editorId: 'test-editor',
496
+ roots: {
497
+ main: '<p>Updated main root content</p>',
498
+ },
499
+ });
500
+
501
+ expect(bodyChangeSpy).toHaveBeenCalled();
502
+ expect((bodyChangeSpy.mock.lastCall![0] as CustomEvent).detail).toEqual({
503
+ editor,
504
+ editorId: 'test-editor',
505
+ roots: {
506
+ main: '<p>Updated main root content</p>',
507
+ },
508
+ });
509
+ });
510
+
511
+ it('should dispatch `ckeditor5:change:data` event with all roots values for multiroot editor', async () => {
512
+ const component = renderTestEditor({
513
+ saveDebounceMs: 0,
514
+ preset: createEditorPreset('multiroot'),
515
+ content: {
516
+ header: '<p>Header root initial content</p>',
517
+ footer: '<p>Footer root initial content</p>',
518
+ },
519
+ });
520
+
521
+ const changeSpy = vi.fn();
522
+
523
+ component.addEventListener('ckeditor5:change:data', changeSpy);
524
+
525
+ renderTestEditable(createEditableSnapshot('header'));
526
+ renderTestEditable(createEditableSnapshot('footer'));
527
+
528
+ const editor = await waitForTestEditor<MultiRootEditor>();
529
+
530
+ editor.setData({
531
+ header: '<p>Updated header content</p>',
532
+ footer: '<p>Updated footer content</p>',
533
+ });
534
+
535
+ await vi.advanceTimersByTimeAsync(1);
536
+
537
+ expect(changeSpy).toHaveBeenCalled();
538
+ expect((changeSpy.mock.lastCall![0] as CustomEvent).detail).toEqual({
539
+ editor,
540
+ editorId: 'test-editor',
541
+ roots: {
542
+ header: '<p>Updated header content</p>',
543
+ footer: '<p>Updated footer content</p>',
544
+ },
545
+ });
546
+ });
547
+ });
548
+
549
+ describe('destroy', () => {
550
+ it('should destroy editor on component unmount', async () => {
551
+ const component = renderTestEditor();
552
+
553
+ const editor = await waitForTestEditor();
554
+
555
+ component.remove();
556
+
557
+ await vi.waitFor(() => {
558
+ expect(editor.state).toBe('destroyed');
559
+ });
560
+ });
561
+ });
562
+ });