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,131 @@
1
+ import type { Translations } from 'ckeditor5';
2
+
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { resolveEditorConfigTranslations } from './resolve-editor-config-translations';
6
+
7
+ describe('resolveEditorConfigTranslations', () => {
8
+ let translationsPacks: Translations[];
9
+
10
+ beforeEach(() => {
11
+ translationsPacks = [
12
+ makePack('en', {
13
+ HELLO: 'Hello world',
14
+ NESTED: 'nested value',
15
+ ARRAY: 'array value',
16
+ }),
17
+ ];
18
+ });
19
+
20
+ afterEach(() => {
21
+ vi.restoreAllMocks();
22
+ });
23
+
24
+ it('resolves a single translation reference', () => {
25
+ const config = {
26
+ foo: { $translation: 'HELLO' },
27
+ };
28
+
29
+ const result = resolveEditorConfigTranslations(translationsPacks, 'en', config);
30
+ expect(result.foo).toBe('Hello world');
31
+ });
32
+
33
+ it('respects specified language when multiple packs present', () => {
34
+ const packs: Translations[] = [
35
+ makePack('en', { KEY: 'english' }),
36
+ makePack('pl', { KEY: 'polish' }),
37
+ ];
38
+
39
+ const result = resolveEditorConfigTranslations(packs, 'pl', { foo: { $translation: 'KEY' } });
40
+ expect(result.foo).toBe('polish');
41
+ });
42
+
43
+ it('returns null if translation key not found', () => {
44
+ const config = {
45
+ foo: { $translation: 'MISSING' },
46
+ };
47
+
48
+ const result = resolveEditorConfigTranslations(translationsPacks, 'en', config);
49
+ expect(result.foo).toBeNull();
50
+ });
51
+
52
+ it('recursively resolves nested translation references', () => {
53
+ const config = {
54
+ nested: {
55
+ bar: { $translation: 'NESTED' },
56
+ },
57
+ };
58
+
59
+ const result = resolveEditorConfigTranslations(translationsPacks, 'en', config);
60
+ expect(result.nested.bar).toBe('nested value');
61
+ });
62
+
63
+ it('resolves translation references in arrays', () => {
64
+ const config = [
65
+ { $translation: 'HELLO' },
66
+ { $translation: 'ARRAY' },
67
+ { notTranslation: 123 },
68
+ ];
69
+
70
+ const result = resolveEditorConfigTranslations(translationsPacks, 'en', config);
71
+
72
+ expect(result[0]).toBe('Hello world');
73
+ expect(result[1]).toBe('array value');
74
+ expect(result[2]).toEqual({ notTranslation: 123 });
75
+ });
76
+
77
+ it('returns primitives as is', () => {
78
+ expect(resolveEditorConfigTranslations(translationsPacks, 'en', 42 as any)).toBe(42);
79
+ expect(resolveEditorConfigTranslations(translationsPacks, 'en', 'foo' as any)).toBe('foo');
80
+ expect(resolveEditorConfigTranslations(translationsPacks, 'en', null as any)).toBe(null);
81
+ expect(resolveEditorConfigTranslations(translationsPacks, 'en', undefined as any)).toBe(undefined);
82
+ });
83
+
84
+ it('warns for missing translation key', () => {
85
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
86
+ const config = { foo: { $translation: 'UNKNOWN' } };
87
+
88
+ resolveEditorConfigTranslations(translationsPacks, 'en', config);
89
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Translation not found for key: UNKNOWN'));
90
+ });
91
+
92
+ it('selects translations based on the provided language when multiple packs are given', () => {
93
+ const packs: Translations[] = [
94
+ makePack('en', { HELLO: 'Hello world', ARRAY: 'array value' }),
95
+ makePack('pl', { NESTED: 'nested value' }),
96
+ ];
97
+
98
+ const config = {
99
+ foo: { $translation: 'HELLO' },
100
+ nested: { bar: { $translation: 'NESTED' } },
101
+ arr: [{ $translation: 'ARRAY' }],
102
+ missing: { $translation: 'NOTHING' },
103
+ };
104
+
105
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
106
+ const resultEn = resolveEditorConfigTranslations(packs, 'en', config);
107
+
108
+ expect(resultEn.foo).toBe('Hello world');
109
+ expect(resultEn.nested.bar).toBeNull();
110
+ expect(resultEn.arr[0]).toBe('array value');
111
+ expect(resultEn.missing).toBeNull();
112
+
113
+ const resultPl = resolveEditorConfigTranslations(packs, 'pl', config);
114
+
115
+ expect(resultPl.foo).toBeNull();
116
+ expect(resultPl.nested.bar).toBe('nested value');
117
+ expect(resultPl.arr[0]).toBeNull();
118
+ expect(resultPl.missing).toBeNull();
119
+
120
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Translation not found for key: NOTHING'));
121
+ });
122
+ });
123
+
124
+ function makePack(lang: string, dict: Record<string, string>): Translations {
125
+ return {
126
+ [lang]: {
127
+ dictionary: dict,
128
+ getPluralForm: null,
129
+ },
130
+ };
131
+ }
@@ -0,0 +1,77 @@
1
+ import type { Translations } from 'ckeditor5';
2
+
3
+ /**
4
+ * Resolves translation references in a configuration object.
5
+ *
6
+ * The configuration may contain objects with the form `{ $translation: "some.key" }`.
7
+ * These are replaced with the actual string from the provided translations map.
8
+ *
9
+ * The function will walk the provided object recursively, handling arrays and
10
+ * nested objects. Primitive values are returned as-is. If a translation key is
11
+ * not present in the map, a warning is logged and `null` is returned for that
12
+ * value.
13
+ *
14
+ * @param translations - An array of CKEditor `Translations` objects. Each translation
15
+ * pack will be searched in order for the requested key, and the
16
+ * first matching value will be returned. This mirrors the format
17
+ * returned by `loadAllEditorTranslations` and simplifies the
18
+ * caller's API.
19
+ * @param language - Language identifier to look up in the packs. Only this locale
20
+ * will be consulted, ensuring that keys from other languages are
21
+ * ignored even if they appear earlier in the array.
22
+ * @param obj - Configuration object to process
23
+ * @returns Processed configuration object with resolved translations.
24
+ */
25
+ export function resolveEditorConfigTranslations<T>(
26
+ translations: Translations[],
27
+ language: string,
28
+ obj: T,
29
+ ): T {
30
+ if (!obj || typeof obj !== 'object') {
31
+ return obj;
32
+ }
33
+
34
+ if (Array.isArray(obj)) {
35
+ return obj.map(item => resolveEditorConfigTranslations(translations, language, item)) as T;
36
+ }
37
+
38
+ const anyObj = obj as any;
39
+
40
+ if (anyObj.$translation && typeof anyObj.$translation === 'string') {
41
+ const key: string = anyObj.$translation;
42
+ const value = getTranslationValue(translations, key, language);
43
+
44
+ if (value === undefined) {
45
+ console.warn(`Translation not found for key: ${key}`);
46
+ }
47
+
48
+ return (value !== undefined ? value : null) as T;
49
+ }
50
+
51
+ const result = Object.create(null);
52
+
53
+ for (const [key, value] of Object.entries(obj)) {
54
+ result[key] = resolveEditorConfigTranslations(translations, language, value);
55
+ }
56
+
57
+ return result as T;
58
+ }
59
+
60
+ /**
61
+ * Look up a translation value inside the provided map or array of CKEditor packs.
62
+ */
63
+ function getTranslationValue(
64
+ translations: Translations[],
65
+ key: string,
66
+ language: string,
67
+ ): string | ReadonlyArray<string> | undefined {
68
+ for (const pack of translations) {
69
+ const langData = pack[language];
70
+
71
+ if (langData?.dictionary && key in langData.dictionary) {
72
+ return langData.dictionary[key] as string | ReadonlyArray<string>;
73
+ }
74
+ }
75
+
76
+ return undefined;
77
+ }
@@ -0,0 +1,131 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { setEditorEditableHeight } from './set-editor-editable-height';
4
+
5
+ // Mock CKEditor5 types and interfaces
6
+ const mockWriter = {
7
+ setStyle: vi.fn(),
8
+ };
9
+
10
+ const mockRoot = {
11
+ // Mock root element
12
+ };
13
+
14
+ const mockViewDocument = {
15
+ getRoot: vi.fn(() => mockRoot),
16
+ };
17
+
18
+ const mockView = {
19
+ change: vi.fn(callback => callback(mockWriter)),
20
+ document: mockViewDocument,
21
+ };
22
+
23
+ const mockEditing = {
24
+ view: mockView,
25
+ };
26
+
27
+ const mockEditor = {
28
+ editing: mockEditing,
29
+ };
30
+
31
+ describe('setEditorEditableHeight', () => {
32
+ beforeEach(() => {
33
+ vi.clearAllMocks();
34
+ });
35
+
36
+ it('should set height style on editor root element', () => {
37
+ const height = 300;
38
+
39
+ setEditorEditableHeight(mockEditor as any, height);
40
+
41
+ expect(mockView.change).toHaveBeenCalledWith(expect.any(Function));
42
+ expect(mockViewDocument.getRoot).toHaveBeenCalled();
43
+ expect(mockWriter.setStyle).toHaveBeenCalledWith('height', '300px', mockRoot);
44
+ });
45
+
46
+ it('should handle different height values', () => {
47
+ const heights = [100, 200, 500, 1000];
48
+
49
+ heights.forEach((height) => {
50
+ vi.clearAllMocks();
51
+
52
+ setEditorEditableHeight(mockEditor as any, height);
53
+
54
+ expect(mockWriter.setStyle).toHaveBeenCalledWith('height', `${height}px`, mockRoot);
55
+ });
56
+ });
57
+
58
+ it('should handle zero height', () => {
59
+ const height = 0;
60
+
61
+ setEditorEditableHeight(mockEditor as any, height);
62
+
63
+ expect(mockWriter.setStyle).toHaveBeenCalledWith('height', '0px', mockRoot);
64
+ });
65
+
66
+ it('should handle negative height values', () => {
67
+ const height = -100;
68
+
69
+ setEditorEditableHeight(mockEditor as any, height);
70
+
71
+ expect(mockWriter.setStyle).toHaveBeenCalledWith('height', '-100px', mockRoot);
72
+ });
73
+
74
+ it('should handle decimal height values', () => {
75
+ const height = 250.5;
76
+
77
+ setEditorEditableHeight(mockEditor as any, height);
78
+
79
+ expect(mockWriter.setStyle).toHaveBeenCalledWith('height', '250.5px', mockRoot);
80
+ });
81
+
82
+ it('should call view.change with correct callback', () => {
83
+ const height = 400;
84
+
85
+ setEditorEditableHeight(mockEditor as any, height);
86
+
87
+ expect(mockView.change).toHaveBeenCalledTimes(1);
88
+ expect(mockView.change).toHaveBeenCalledWith(expect.any(Function));
89
+ });
90
+
91
+ it('should work with different editor instances', () => {
92
+ const anotherMockEditor = {
93
+ editing: {
94
+ view: {
95
+ change: vi.fn(callback => callback(mockWriter)),
96
+ document: {
97
+ getRoot: vi.fn(() => mockRoot),
98
+ },
99
+ },
100
+ },
101
+ };
102
+
103
+ const height = 350;
104
+
105
+ setEditorEditableHeight(anotherMockEditor as any, height);
106
+
107
+ expect(anotherMockEditor.editing.view.change).toHaveBeenCalledWith(expect.any(Function));
108
+ expect(anotherMockEditor.editing.view.document.getRoot).toHaveBeenCalled();
109
+ expect(mockWriter.setStyle).toHaveBeenCalledWith('height', '350px', mockRoot);
110
+ });
111
+
112
+ it('should handle editor with null root gracefully', () => {
113
+ const mockEditorWithNullRoot = {
114
+ editing: {
115
+ view: {
116
+ change: vi.fn(callback => callback(mockWriter)),
117
+ document: {
118
+ getRoot: vi.fn(() => null),
119
+ },
120
+ },
121
+ },
122
+ };
123
+
124
+ const height = 200;
125
+
126
+ // Should not throw error even with null root
127
+ expect(() => setEditorEditableHeight(mockEditorWithNullRoot as any, height)).not.toThrow();
128
+
129
+ expect(mockWriter.setStyle).toHaveBeenCalledWith('height', '200px', null);
130
+ });
131
+ });
@@ -0,0 +1,15 @@
1
+ import type { Editor } from 'ckeditor5';
2
+
3
+ /**
4
+ * Sets the height of the editable area in the CKEditor instance.
5
+ *
6
+ * @param instance - The CKEditor instance to modify.
7
+ * @param height - The height in pixels to set for the editable area.
8
+ */
9
+ export function setEditorEditableHeight(instance: Editor, height: number): void {
10
+ const { editing } = instance;
11
+
12
+ editing.view.change((writer) => {
13
+ writer.setStyle('height', `${height}px`, editing.view.document.getRoot()!);
14
+ });
15
+ }
@@ -0,0 +1,45 @@
1
+ import { ClassicEditor, EditorWatchdog } from 'ckeditor5';
2
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
3
+
4
+ import { unwrapEditorWatchdog, wrapWithWatchdog } from './wrap-with-watchdog';
5
+
6
+ describe('wrap with watchdog', () => {
7
+ let element: HTMLElement;
8
+
9
+ beforeEach(async () => {
10
+ element = document.createElement('div');
11
+ document.body.appendChild(element);
12
+ });
13
+
14
+ afterEach(() => {
15
+ element.remove();
16
+ });
17
+
18
+ it('returns editor instance after calling Constructor.create', async () => {
19
+ const { Constructor } = await wrapWithWatchdog(ClassicEditor);
20
+ const editor = await Constructor.create(element, {
21
+ licenseKey: 'GPL',
22
+ });
23
+
24
+ expect(editor).toBeInstanceOf(ClassicEditor);
25
+
26
+ await editor.destroy();
27
+ });
28
+
29
+ it('returns instance of watchdog', async () => {
30
+ const { watchdog } = await wrapWithWatchdog(ClassicEditor);
31
+
32
+ expect(watchdog).toBeInstanceOf(EditorWatchdog);
33
+ });
34
+
35
+ it('should be possible to unwrap watchdog from editor instance', async () => {
36
+ const { Constructor } = await wrapWithWatchdog(ClassicEditor);
37
+ const editor = await Constructor.create(element, {
38
+ licenseKey: 'GPL',
39
+ });
40
+
41
+ expect(unwrapEditorWatchdog(editor)).toBeInstanceOf(EditorWatchdog);
42
+
43
+ await editor.destroy();
44
+ });
45
+ });
@@ -0,0 +1,51 @@
1
+ import type { Editor, EditorWatchdog } from 'ckeditor5';
2
+
3
+ const EDITOR_WATCHDOG_SYMBOL = Symbol.for('elixir-editor-watchdog');
4
+
5
+ /**
6
+ * Wraps an Editor creator with a watchdog for automatic recovery.
7
+ *
8
+ * @param Editor - The Editor creator to wrap.
9
+ * @returns The Editor creator wrapped with a watchdog.
10
+ */
11
+ export async function wrapWithWatchdog(Editor: EditorCreator) {
12
+ const { EditorWatchdog } = await import('ckeditor5');
13
+ const watchdog = new EditorWatchdog(Editor);
14
+
15
+ watchdog.setCreator(async (...args: Parameters<typeof Editor['create']>) => {
16
+ const editor = await Editor.create(...args);
17
+
18
+ (editor as any)[EDITOR_WATCHDOG_SYMBOL] = watchdog;
19
+
20
+ return editor;
21
+ });
22
+
23
+ return {
24
+ watchdog,
25
+ Constructor: {
26
+ create: async (...args: Parameters<typeof Editor['create']>) => {
27
+ await watchdog.create(...args);
28
+
29
+ return watchdog.editor!;
30
+ },
31
+ },
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Unwraps the EditorWatchdog from the editor instance.
37
+ */
38
+ export function unwrapEditorWatchdog(editor: Editor): EditorWatchdog | null {
39
+ if (EDITOR_WATCHDOG_SYMBOL in editor) {
40
+ return (editor as any)[EDITOR_WATCHDOG_SYMBOL] as EditorWatchdog;
41
+ }
42
+
43
+ return null;
44
+ }
45
+
46
+ /**
47
+ * Type representing an Editor creator with a create method.
48
+ */
49
+ export type EditorCreator = {
50
+ create: (...args: any) => Promise<Editor>;
51
+ };
@@ -0,0 +1,24 @@
1
+ import { ContextComponentElement } from './context';
2
+ import { EditableComponentElement } from './editable';
3
+ import { EditorComponentElement } from './editor';
4
+ import { UIPartComponentElement } from './ui-part';
5
+
6
+ const CUSTOM_ELEMENTS = {
7
+ 'cke5-editor': EditorComponentElement,
8
+ 'cke5-context': ContextComponentElement,
9
+ 'cke5-ui-part': UIPartComponentElement,
10
+ 'cke5-editable': EditableComponentElement,
11
+ };
12
+
13
+ /**
14
+ * Registers all available Blazor component hooks.
15
+ */
16
+ export function ensureEditorElementsRegistered() {
17
+ for (const [name, CustomElement] of Object.entries(CUSTOM_ELEMENTS)) {
18
+ if (window.customElements.get(name)) {
19
+ continue;
20
+ }
21
+
22
+ window.customElements.define(name, CustomElement);
23
+ }
24
+ }
@@ -0,0 +1,14 @@
1
+ export {
2
+ ContextComponentElement,
3
+ type ContextConfig,
4
+ type ContextCreatorConfig,
5
+ ContextsRegistry,
6
+ } from './context';
7
+ export { EditableComponentElement } from './editable';
8
+ export {
9
+ type EditorCloudConfig,
10
+ EditorComponentElement,
11
+ type EditorConfig,
12
+ } from './editor';
13
+ export { ensureEditorElementsRegistered } from './ensure-editor-elements-registered';
14
+ export { UIPartComponentElement } from './ui-part';
@@ -0,0 +1,156 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import {
4
+ createEditorPreset,
5
+ createUIPartSnapshot,
6
+ renderTestEditor,
7
+ renderTestUIPart,
8
+ waitForDestroyAllEditors,
9
+ waitForTestEditor,
10
+ } from '~/test-utils';
11
+
12
+ import { timeout } from '../shared';
13
+ import { EditorsRegistry } from './editor/editors-registry';
14
+ import { ensureEditorElementsRegistered } from './ensure-editor-elements-registered';
15
+
16
+ describe('ui-part component', () => {
17
+ beforeEach(() => {
18
+ document.body.innerHTML = '';
19
+ ensureEditorElementsRegistered();
20
+ });
21
+
22
+ afterEach(async () => {
23
+ vi.useRealTimers();
24
+ vi.resetAllMocks();
25
+
26
+ await waitForDestroyAllEditors();
27
+ document.body.innerHTML = '';
28
+ });
29
+
30
+ describe('mounting ui part', () => {
31
+ it('should mount toolbar to the editor after mounting editor', async () => {
32
+ appendMultirootEditor();
33
+
34
+ const editor = await waitForTestEditor();
35
+ const toolbarElement = editor.ui.view.toolbar?.element;
36
+
37
+ const el = renderTestUIPart(createUIPartSnapshot('toolbar'));
38
+
39
+ await vi.waitFor(() => {
40
+ expect(el.contains(toolbarElement!)).toBe(true);
41
+ });
42
+
43
+ expect(toolbarElement).toBeTruthy();
44
+ });
45
+
46
+ it('should mount menubar to the editor after mounting editor', async () => {
47
+ appendMultirootEditor();
48
+
49
+ const editor = await waitForTestEditor();
50
+ const menubarElement = (editor.ui.view as any).menuBarView.element;
51
+
52
+ const el = renderTestUIPart(createUIPartSnapshot('menubar'));
53
+
54
+ await vi.waitFor(() => {
55
+ expect(el.children.length).toBeGreaterThan(0);
56
+ });
57
+
58
+ expect(el.contains(menubarElement)).toBe(true);
59
+ });
60
+
61
+ it('should mount UI part before editor is created', async () => {
62
+ const el = renderTestUIPart(createUIPartSnapshot('toolbar'));
63
+
64
+ appendMultirootEditor();
65
+
66
+ const editor = await waitForTestEditor();
67
+ const toolbarElement = editor.ui.view.toolbar?.element;
68
+
69
+ await vi.waitFor(() => {
70
+ expect(el.contains(toolbarElement!)).toBe(true);
71
+ });
72
+
73
+ expect(toolbarElement).toBeTruthy();
74
+ });
75
+
76
+ it('should default to first editor ID if data-cke-editor-id is missing', async () => {
77
+ appendMultirootEditor();
78
+
79
+ const editor = await waitForTestEditor();
80
+ const toolbarElement = editor.ui.view.toolbar?.element;
81
+
82
+ // Render UI part without editorId
83
+ const el = renderTestUIPart({ editorId: undefined } as any);
84
+
85
+ await vi.waitFor(() => {
86
+ expect(el.contains(toolbarElement!)).toBe(true);
87
+ });
88
+
89
+ expect(toolbarElement).toBeTruthy();
90
+ });
91
+
92
+ it('should not mount UI part if element is disconnected before editor is ready', async () => {
93
+ const el = renderTestUIPart(createUIPartSnapshot('toolbar'));
94
+ el.remove();
95
+
96
+ appendMultirootEditor();
97
+
98
+ await waitForTestEditor();
99
+ await timeout(10);
100
+
101
+ expect(el.innerHTML).toBe('');
102
+ });
103
+ });
104
+
105
+ describe('destroying ui part', () => {
106
+ beforeEach(async () => {
107
+ appendMultirootEditor();
108
+ await waitForTestEditor();
109
+ });
110
+
111
+ it('should clear UI part element on destruction', async () => {
112
+ const el = renderTestUIPart(createUIPartSnapshot('toolbar'));
113
+
114
+ await vi.waitFor(() => {
115
+ expect(el.children.length).toBeGreaterThan(0);
116
+ });
117
+
118
+ el.remove();
119
+
120
+ await vi.waitFor(() => {
121
+ expect(el.innerHTML).toBe('');
122
+ });
123
+
124
+ expect(el.style.display).toBe('none');
125
+ });
126
+
127
+ it('should hide element during destruction', async () => {
128
+ const el = renderTestUIPart(createUIPartSnapshot('toolbar'));
129
+
130
+ // If we remove immediately, disconnectedCallback should hide it.
131
+ el.remove();
132
+
133
+ // Ensure style is updated synchronously or microtask
134
+ expect(el.style.display).toBe('none');
135
+ });
136
+
137
+ it('should handle destruction when mounted promise is not resolved yet', async () => {
138
+ document.body.innerHTML = '';
139
+ EditorsRegistry.the.reset();
140
+
141
+ const el = renderTestUIPart(createUIPartSnapshot('toolbar'));
142
+
143
+ el.remove();
144
+
145
+ expect(el.innerHTML).toBe('');
146
+ expect(el.style.display).toBe('none');
147
+ });
148
+ });
149
+
150
+ function appendMultirootEditor(initialContent: Record<string, string> = {}) {
151
+ renderTestEditor({
152
+ preset: createEditorPreset('multiroot'),
153
+ content: initialContent,
154
+ });
155
+ }
156
+ });