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,89 @@
1
+ import type { EditorCreator } from './wrap-with-watchdog';
2
+ import type { Context, ContextWatchdog, Editor, EditorConfig } from 'ckeditor5';
3
+
4
+ import { uid } from '../../../shared';
5
+
6
+ /**
7
+ * Symbol used to store the context watchdog on the editor instance.
8
+ * Internal use only.
9
+ */
10
+ const CONTEXT_EDITOR_WATCHDOG_SYMBOL = Symbol.for('context-editor-watchdog');
11
+
12
+ /**
13
+ * Creates a CKEditor 5 editor instance within a given context watchdog.
14
+ *
15
+ * @param params Parameters for editor creation.
16
+ * @param params.element The DOM element or data for the editor.
17
+ * @param params.context The context watchdog instance.
18
+ * @param params.creator The editor creator utility.
19
+ * @param params.config The editor configuration object.
20
+ * @returns The created editor instance.
21
+ */
22
+ export async function createEditorInContext({ element, context, creator, config }: Attrs) {
23
+ const editorContextId = uid();
24
+
25
+ await context.add({
26
+ creator: (_element, _config) => creator.create(_element, _config),
27
+ id: editorContextId,
28
+ sourceElementOrData: element,
29
+ type: 'editor',
30
+ config,
31
+ });
32
+
33
+ const editor = context.getItem(editorContextId) as Editor;
34
+ const contextDescriptor: EditorContextDescriptor = {
35
+ state: 'available',
36
+ editorContextId,
37
+ context,
38
+ };
39
+
40
+ (editor as any)[CONTEXT_EDITOR_WATCHDOG_SYMBOL] = contextDescriptor;
41
+
42
+ // Destroying of context is async. There can be situation when the destroy of the context
43
+ // and the destroy of the editor is called in parallel. It often happens during unmounting of
44
+ // phoenix hooks. Let's make sure that descriptor informs other components, that context is being
45
+ // destroyed.
46
+ const originalDestroy = context.destroy.bind(context);
47
+ context.destroy = async () => {
48
+ contextDescriptor.state = 'unavailable';
49
+ return originalDestroy();
50
+ };
51
+
52
+ return {
53
+ ...contextDescriptor,
54
+ editor,
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Retrieves the context watchdog from an editor instance, if available.
60
+ *
61
+ * @param editor The editor instance.
62
+ * @returns The context watchdog or null if not found.
63
+ */
64
+ export function unwrapEditorContext(editor: Editor): EditorContextDescriptor | null {
65
+ if (CONTEXT_EDITOR_WATCHDOG_SYMBOL in editor) {
66
+ return (editor as any)[CONTEXT_EDITOR_WATCHDOG_SYMBOL];
67
+ }
68
+
69
+ return null;
70
+ }
71
+
72
+ /**
73
+ * Parameters for creating an editor in a context.
74
+ */
75
+ type Attrs = {
76
+ context: ContextWatchdog<Context>;
77
+ creator: EditorCreator;
78
+ element: HTMLElement;
79
+ config: EditorConfig;
80
+ };
81
+
82
+ /**
83
+ * Descriptor for an editor context.
84
+ */
85
+ type EditorContextDescriptor = {
86
+ state: 'available' | 'unavailable';
87
+ editorContextId: string;
88
+ context: ContextWatchdog<Context>;
89
+ };
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { getEditorRootsValues } from './get-editor-roots-values';
4
+
5
+ describe('getEditorRootsValues', () => {
6
+ it('should return values for all regular roots', () => {
7
+ const editor = {
8
+ model: {
9
+ document: {
10
+ getRoots: () => [
11
+ { rootName: 'main' },
12
+ { rootName: 'header' },
13
+ ],
14
+ },
15
+ },
16
+ getData: ({ rootName }: { rootName: string; }) => ({
17
+ main: '<p>Main</p>',
18
+ header: '<p>Header</p>',
19
+ }[rootName]),
20
+ };
21
+
22
+ expect(getEditorRootsValues(editor as any)).toEqual({
23
+ main: '<p>Main</p>',
24
+ header: '<p>Header</p>',
25
+ });
26
+ });
27
+
28
+ it('should skip the graveyard root', () => {
29
+ const editor = {
30
+ model: {
31
+ document: {
32
+ getRoots: () => [
33
+ { rootName: 'main' },
34
+ { rootName: '$graveyard' },
35
+ ],
36
+ },
37
+ },
38
+ getData: ({ rootName }: { rootName: string; }) => ({
39
+ main: '<p>Main</p>',
40
+ $graveyard: '<p>Should not be included</p>',
41
+ }[rootName]),
42
+ };
43
+
44
+ expect(getEditorRootsValues(editor as any)).toEqual({
45
+ main: '<p>Main</p>',
46
+ });
47
+ });
48
+ });
@@ -0,0 +1,21 @@
1
+ import type { Editor } from 'ckeditor5';
2
+
3
+ /**
4
+ * Gets values of all editor roots.
5
+ *
6
+ * @param editor The editor instance.
7
+ * @returns A record where keys are root names and values are root HTML strings.
8
+ */
9
+ export function getEditorRootsValues(editor: Editor): Record<string, string> {
10
+ return Array
11
+ .from(editor.model.document.getRoots())
12
+ .reduce<Record<string, string>>((acc, root) => {
13
+ if (root.rootName === '$graveyard') {
14
+ return acc;
15
+ }
16
+
17
+ acc[root.rootName] = editor.getData({ rootName: root.rootName });
18
+
19
+ return acc;
20
+ }, Object.create({}));
21
+ }
@@ -0,0 +1,13 @@
1
+ export * from './create-editor-in-context';
2
+ export * from './get-editor-roots-values';
3
+ export * from './is-single-root-editor';
4
+ export * from './load-editor-constructor';
5
+ export * from './load-editor-plugins';
6
+ export * from './load-editor-translations';
7
+ export * from './normalize-custom-translations';
8
+ export * from './query-all-editor-ids';
9
+ export * from './query-editor-editables';
10
+ export * from './resolve-editor-config-elements-references';
11
+ export * from './resolve-editor-config-translations';
12
+ export * from './set-editor-editable-height';
13
+ export * from './wrap-with-watchdog';
@@ -0,0 +1,40 @@
1
+ import type { EditorType } from '../typings';
2
+
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import { isSingleRootEditor } from './is-single-root-editor';
6
+
7
+ describe('isSingleRootEditor', () => {
8
+ it('should return true for inline editor', () => {
9
+ expect(isSingleRootEditor('inline')).toBe(true);
10
+ });
11
+
12
+ it('should return true for classic editor', () => {
13
+ expect(isSingleRootEditor('classic')).toBe(true);
14
+ });
15
+
16
+ it('should return true for balloon editor', () => {
17
+ expect(isSingleRootEditor('balloon')).toBe(true);
18
+ });
19
+
20
+ it('should return false for decoupled editor', () => {
21
+ expect(isSingleRootEditor('decoupled')).toBe(true);
22
+ });
23
+
24
+ it('should return false for multiroot editor', () => {
25
+ expect(isSingleRootEditor('multiroot')).toBe(false);
26
+ });
27
+
28
+ it('should handle all valid editor types', () => {
29
+ const singleEditingTypes: EditorType[] = ['inline', 'classic', 'balloon', 'decoupled'];
30
+ const multiEditingTypes: EditorType[] = ['multiroot'];
31
+
32
+ singleEditingTypes.forEach((type) => {
33
+ expect(isSingleRootEditor(type)).toBe(true);
34
+ });
35
+
36
+ multiEditingTypes.forEach((type) => {
37
+ expect(isSingleRootEditor(type)).toBe(false);
38
+ });
39
+ });
40
+ });
@@ -0,0 +1,11 @@
1
+ import type { EditorType } from '../typings';
2
+
3
+ /**
4
+ * Checks if the given editor type is one of the single editing-like editors.
5
+ *
6
+ * @param editorType - The type of the editor to check.
7
+ * @returns `true` if the editor type is 'inline', 'classic', or 'balloon', otherwise `false`.
8
+ */
9
+ export function isSingleRootEditor(editorType: EditorType): boolean {
10
+ return ['inline', 'classic', 'balloon', 'decoupled'].includes(editorType);
11
+ }
@@ -0,0 +1,62 @@
1
+ import type { EditorType } from '../typings';
2
+
3
+ import { describe, expect, it, vi } from 'vitest';
4
+
5
+ import { loadEditorConstructor } from './load-editor-constructor';
6
+
7
+ // Mock the ckeditor5 package
8
+ vi.mock('ckeditor5', async () => {
9
+ const mockEditorConstructor = vi.fn();
10
+
11
+ return {
12
+ InlineEditor: mockEditorConstructor,
13
+ BalloonEditor: mockEditorConstructor,
14
+ ClassicEditor: mockEditorConstructor,
15
+ DecoupledEditor: mockEditorConstructor,
16
+ MultiRootEditor: mockEditorConstructor,
17
+ };
18
+ });
19
+
20
+ describe('loadEditorConstructor', () => {
21
+ it('should load InlineEditor for inline type', async () => {
22
+ const constructor = await loadEditorConstructor('inline');
23
+ expect(constructor).toBeDefined();
24
+ });
25
+
26
+ it('should load BalloonEditor for balloon type', async () => {
27
+ const constructor = await loadEditorConstructor('balloon');
28
+ expect(constructor).toBeDefined();
29
+ });
30
+
31
+ it('should load ClassicEditor for classic type', async () => {
32
+ const constructor = await loadEditorConstructor('classic');
33
+ expect(constructor).toBeDefined();
34
+ });
35
+
36
+ it('should load DecoupledEditor for decoupled type', async () => {
37
+ const constructor = await loadEditorConstructor('decoupled');
38
+ expect(constructor).toBeDefined();
39
+ });
40
+
41
+ it('should load MultiRootEditor for multiroot type', async () => {
42
+ const constructor = await loadEditorConstructor('multiroot');
43
+ expect(constructor).toBeDefined();
44
+ });
45
+
46
+ it('should throw error for unsupported editor type', async () => {
47
+ const invalidType = 'invalid' as EditorType;
48
+
49
+ await expect(loadEditorConstructor(invalidType)).rejects.toThrow(
50
+ 'Unsupported editor type: invalid',
51
+ );
52
+ });
53
+
54
+ it('should return the correct constructor for each editor type', async () => {
55
+ const editorTypes: EditorType[] = ['inline', 'balloon', 'classic', 'decoupled', 'multiroot'];
56
+
57
+ for (const type of editorTypes) {
58
+ const constructor = await loadEditorConstructor(type);
59
+ expect(constructor).toBeDefined();
60
+ }
61
+ });
62
+ });
@@ -0,0 +1,29 @@
1
+ import type { EditorType } from '../typings';
2
+
3
+ import { CKEditor5BlazorError } from '../../../ckeditor5-blazor-error';
4
+
5
+ /**
6
+ * Returns the constructor for the specified CKEditor5 editor type.
7
+ *
8
+ * @param type - The type of the editor to load.
9
+ * @returns A promise that resolves to the editor constructor.
10
+ */
11
+ export async function loadEditorConstructor(type: EditorType) {
12
+ const PKG = await import('ckeditor5');
13
+
14
+ const editorMap = {
15
+ inline: PKG.InlineEditor,
16
+ balloon: PKG.BalloonEditor,
17
+ classic: PKG.ClassicEditor,
18
+ decoupled: PKG.DecoupledEditor,
19
+ multiroot: PKG.MultiRootEditor,
20
+ } as const;
21
+
22
+ const EditorConstructor = editorMap[type];
23
+
24
+ if (!EditorConstructor) {
25
+ throw new CKEditor5BlazorError(`Unsupported editor type: ${type}`);
26
+ }
27
+
28
+ return EditorConstructor;
29
+ }
@@ -0,0 +1,100 @@
1
+ import type { EditorPlugin } from '../typings';
2
+
3
+ import { afterEach, describe, expect, it } from 'vitest';
4
+
5
+ import { CustomEditorPluginsRegistry } from '../custom-editor-plugins';
6
+ import { loadEditorPlugins } from './load-editor-plugins';
7
+
8
+ class CustomPlugin {
9
+ static get pluginName() {
10
+ return 'CustomPlugin';
11
+ }
12
+ }
13
+
14
+ describe('loadEditorPlugins', () => {
15
+ it('should load plugins from base package', async () => {
16
+ const plugins: EditorPlugin[] = ['Bold', 'Italic'];
17
+ const { loadedPlugins } = await loadEditorPlugins(plugins);
18
+
19
+ expect(loadedPlugins).toHaveLength(2);
20
+ expect(loadedPlugins[0]).toBeDefined();
21
+ expect(loadedPlugins[1]).toBeDefined();
22
+ });
23
+
24
+ it('should load plugins from premium package', async () => {
25
+ const plugins: EditorPlugin[] = ['ExportInlineStyles', 'Comments'];
26
+ const { loadedPlugins } = await loadEditorPlugins(plugins);
27
+
28
+ expect(loadedPlugins).toHaveLength(2);
29
+ expect(loadedPlugins[0]).toBeDefined();
30
+ expect(loadedPlugins[1]).toBeDefined();
31
+ });
32
+
33
+ it('should handle empty plugin array', async () => {
34
+ const plugins: EditorPlugin[] = [];
35
+ const { loadedPlugins } = await loadEditorPlugins(plugins);
36
+
37
+ expect(loadedPlugins).toHaveLength(0);
38
+ });
39
+
40
+ it('should prioritize base package over premium package', async () => {
41
+ const plugins: EditorPlugin[] = ['Bold'];
42
+ const { loadedPlugins } = await loadEditorPlugins(plugins);
43
+
44
+ expect(loadedPlugins).toHaveLength(1);
45
+ expect(loadedPlugins[0]).toBeDefined();
46
+ });
47
+
48
+ it('should return plugin constructors', async () => {
49
+ const plugins: EditorPlugin[] = ['Bold'];
50
+ const { loadedPlugins } = await loadEditorPlugins(plugins);
51
+
52
+ expect(loadedPlugins).toHaveLength(1);
53
+ expect(typeof loadedPlugins[0]).toBe('function');
54
+ });
55
+
56
+ it('should throw an error for unknown plugins', async () => {
57
+ const plugins: EditorPlugin[] = ['UnknownPlugin'];
58
+ await expect(loadEditorPlugins(plugins)).rejects.toThrowError(/not found/);
59
+ });
60
+
61
+ describe('custom plugins', () => {
62
+ afterEach(() => {
63
+ CustomEditorPluginsRegistry.the.unregisterAll();
64
+ });
65
+
66
+ it('should load custom plugins', async () => {
67
+ CustomEditorPluginsRegistry.the.register('CustomPlugin', () => CustomPlugin);
68
+
69
+ const plugins: EditorPlugin[] = ['CustomPlugin'];
70
+ const { loadedPlugins } = await loadEditorPlugins(plugins);
71
+
72
+ expect(loadedPlugins).toHaveLength(1);
73
+ expect(loadedPlugins[0]).toEqual(CustomPlugin);
74
+ });
75
+
76
+ it('should prefer loading custom plugins over base package', async () => {
77
+ CustomEditorPluginsRegistry.the.register('Bold', () => CustomPlugin);
78
+
79
+ const plugins: EditorPlugin[] = ['Bold'];
80
+ const { loadedPlugins } = await loadEditorPlugins(plugins);
81
+
82
+ expect(loadedPlugins).toHaveLength(1);
83
+ expect(loadedPlugins[0]).toEqual(CustomPlugin);
84
+ });
85
+
86
+ it('should be possible to load async custom plugins', async () => {
87
+ CustomEditorPluginsRegistry.the.register('AsyncPlugin', async () => {
88
+ return new Promise((resolve) => {
89
+ setTimeout(() => resolve(CustomPlugin), 20);
90
+ });
91
+ });
92
+
93
+ const plugins: EditorPlugin[] = ['AsyncPlugin'];
94
+ const { loadedPlugins } = await loadEditorPlugins(plugins);
95
+
96
+ expect(loadedPlugins).toHaveLength(1);
97
+ expect(loadedPlugins[0]).toEqual(CustomPlugin);
98
+ });
99
+ });
100
+ });
@@ -0,0 +1,72 @@
1
+ import type { EditorPlugin } from '../typings';
2
+ import type { PluginConstructor } from 'ckeditor5';
3
+
4
+ import { CKEditor5BlazorError } from '../../../ckeditor5-blazor-error';
5
+ import { CustomEditorPluginsRegistry } from '../custom-editor-plugins';
6
+
7
+ /**
8
+ * Loads CKEditor plugins from base and premium packages.
9
+ * First tries to load from the base 'ckeditor5' package, then falls back to 'ckeditor5-premium-features'.
10
+ *
11
+ * @param plugins - Array of plugin names to load
12
+ * @returns Promise that resolves to an array of loaded Plugin instances
13
+ * @throws Error if a plugin is not found in either package
14
+ */
15
+ export async function loadEditorPlugins(plugins: EditorPlugin[]): Promise<LoadedPlugins> {
16
+ const basePackage = await import('ckeditor5');
17
+ let premiumPackage: Record<string, any> | null = null;
18
+
19
+ const loaders = plugins.map(async (plugin) => {
20
+ // Let's first try to load the plugin from the base package.
21
+ // Coverage is disabled due to Vitest issues with mocking dynamic imports.
22
+
23
+ // If the plugin is not found in the base package, try custom plugins.
24
+ const customPlugin = await CustomEditorPluginsRegistry.the.get(plugin);
25
+
26
+ if (customPlugin) {
27
+ return customPlugin;
28
+ }
29
+
30
+ // If not found, try to load from the base package.
31
+ const { [plugin]: basePkgImport } = basePackage as Record<string, unknown>;
32
+
33
+ if (basePkgImport) {
34
+ return basePkgImport as PluginConstructor;
35
+ }
36
+
37
+ // Plugin not found in base package, try premium package.
38
+ /* v8 ignore start -- @preserve */
39
+ if (!premiumPackage) {
40
+ try {
41
+ premiumPackage = await import('ckeditor5-premium-features');
42
+ }
43
+ catch (error) {
44
+ console.error(`Failed to load premium package: ${error}`);
45
+ throw new CKEditor5BlazorError(`Plugin "${plugin}" not found in base package and failed to load premium package.`);
46
+ }
47
+ }
48
+ /* v8 ignore end */
49
+
50
+ const { [plugin]: premiumPkgImport } = premiumPackage || {};
51
+
52
+ if (premiumPkgImport) {
53
+ return premiumPkgImport as PluginConstructor;
54
+ }
55
+
56
+ // Plugin not found in either package, throw an error.
57
+ throw new CKEditor5BlazorError(`Plugin "${plugin}" not found in base or premium packages.`);
58
+ });
59
+
60
+ return {
61
+ loadedPlugins: await Promise.all(loaders),
62
+ hasPremium: !!premiumPackage,
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Type representing the loaded plugins and whether premium features are available.
68
+ */
69
+ type LoadedPlugins = {
70
+ loadedPlugins: PluginConstructor<any>[];
71
+ hasPremium: boolean;
72
+ };