ckeditor5-symfony 1.0.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 (174) hide show
  1. package/dist/ckeditor5-symfony-error.d.ts +7 -0
  2. package/dist/ckeditor5-symfony-error.d.ts.map +1 -0
  3. package/dist/elements/context/context.d.ts +18 -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 +18 -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 +23 -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/index.d.ts +2 -0
  22. package/dist/elements/editor/plugins/index.d.ts.map +1 -0
  23. package/dist/elements/editor/plugins/sync-editor-with-input.d.ts +6 -0
  24. package/dist/elements/editor/plugins/sync-editor-with-input.d.ts.map +1 -0
  25. package/dist/elements/editor/typings.d.ts +99 -0
  26. package/dist/elements/editor/typings.d.ts.map +1 -0
  27. package/dist/elements/editor/utils/create-editor-in-context.d.ts +44 -0
  28. package/dist/elements/editor/utils/create-editor-in-context.d.ts.map +1 -0
  29. package/dist/elements/editor/utils/index.d.ts +12 -0
  30. package/dist/elements/editor/utils/index.d.ts.map +1 -0
  31. package/dist/elements/editor/utils/is-single-root-editor.d.ts +9 -0
  32. package/dist/elements/editor/utils/is-single-root-editor.d.ts.map +1 -0
  33. package/dist/elements/editor/utils/load-editor-constructor.d.ts +9 -0
  34. package/dist/elements/editor/utils/load-editor-constructor.d.ts.map +1 -0
  35. package/dist/elements/editor/utils/load-editor-plugins.d.ts +20 -0
  36. package/dist/elements/editor/utils/load-editor-plugins.d.ts.map +1 -0
  37. package/dist/elements/editor/utils/load-editor-translations.d.ts +14 -0
  38. package/dist/elements/editor/utils/load-editor-translations.d.ts.map +1 -0
  39. package/dist/elements/editor/utils/normalize-custom-translations.d.ts +11 -0
  40. package/dist/elements/editor/utils/normalize-custom-translations.d.ts.map +1 -0
  41. package/dist/elements/editor/utils/query-all-editor-ids.d.ts +5 -0
  42. package/dist/elements/editor/utils/query-all-editor-ids.d.ts.map +1 -0
  43. package/dist/elements/editor/utils/query-editor-editables.d.ts +25 -0
  44. package/dist/elements/editor/utils/query-editor-editables.d.ts.map +1 -0
  45. package/dist/elements/editor/utils/resolve-editor-config-elements-references.d.ts +9 -0
  46. package/dist/elements/editor/utils/resolve-editor-config-elements-references.d.ts.map +1 -0
  47. package/dist/elements/editor/utils/set-editor-editable-height.d.ts +9 -0
  48. package/dist/elements/editor/utils/set-editor-editable-height.d.ts.map +1 -0
  49. package/dist/elements/editor/utils/wrap-with-watchdog.d.ts +24 -0
  50. package/dist/elements/editor/utils/wrap-with-watchdog.d.ts.map +1 -0
  51. package/dist/elements/index.d.ts +6 -0
  52. package/dist/elements/index.d.ts.map +1 -0
  53. package/dist/elements/register-custom-elements.d.ts +5 -0
  54. package/dist/elements/register-custom-elements.d.ts.map +1 -0
  55. package/dist/elements/ui-part.d.ts +18 -0
  56. package/dist/elements/ui-part.d.ts.map +1 -0
  57. package/dist/index.cjs +5 -0
  58. package/dist/index.cjs.map +1 -0
  59. package/dist/index.d.ts +3 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.mjs +1089 -0
  62. package/dist/index.mjs.map +1 -0
  63. package/dist/shared/async-registry.d.ts +136 -0
  64. package/dist/shared/async-registry.d.ts.map +1 -0
  65. package/dist/shared/camel-case.d.ts +8 -0
  66. package/dist/shared/camel-case.d.ts.map +1 -0
  67. package/dist/shared/debounce.d.ts +2 -0
  68. package/dist/shared/debounce.d.ts.map +1 -0
  69. package/dist/shared/deep-camel-case-keys.d.ts +8 -0
  70. package/dist/shared/deep-camel-case-keys.d.ts.map +1 -0
  71. package/dist/shared/filter-object-values.d.ts +9 -0
  72. package/dist/shared/filter-object-values.d.ts.map +1 -0
  73. package/dist/shared/index.d.ts +15 -0
  74. package/dist/shared/index.d.ts.map +1 -0
  75. package/dist/shared/is-empty-object.d.ts +2 -0
  76. package/dist/shared/is-empty-object.d.ts.map +1 -0
  77. package/dist/shared/is-plain-object.d.ts +8 -0
  78. package/dist/shared/is-plain-object.d.ts.map +1 -0
  79. package/dist/shared/map-object-values.d.ts +11 -0
  80. package/dist/shared/map-object-values.d.ts.map +1 -0
  81. package/dist/shared/once.d.ts +2 -0
  82. package/dist/shared/once.d.ts.map +1 -0
  83. package/dist/shared/shallow-equal.d.ts +9 -0
  84. package/dist/shared/shallow-equal.d.ts.map +1 -0
  85. package/dist/shared/timeout.d.ts +8 -0
  86. package/dist/shared/timeout.d.ts.map +1 -0
  87. package/dist/shared/uid.d.ts +7 -0
  88. package/dist/shared/uid.d.ts.map +1 -0
  89. package/dist/shared/wait-for-dom-ready.d.ts +5 -0
  90. package/dist/shared/wait-for-dom-ready.d.ts.map +1 -0
  91. package/dist/shared/wait-for.d.ts +20 -0
  92. package/dist/shared/wait-for.d.ts.map +1 -0
  93. package/dist/types/can-be-promise.type.d.ts +2 -0
  94. package/dist/types/can-be-promise.type.d.ts.map +1 -0
  95. package/dist/types/index.d.ts +3 -0
  96. package/dist/types/index.d.ts.map +1 -0
  97. package/dist/types/required-by.type.d.ts +2 -0
  98. package/dist/types/required-by.type.d.ts.map +1 -0
  99. package/package.json +40 -0
  100. package/src/ckeditor5-symfony-error.ts +9 -0
  101. package/src/elements/context/context.test.ts +291 -0
  102. package/src/elements/context/context.ts +99 -0
  103. package/src/elements/context/contexts-registry.test.ts +10 -0
  104. package/src/elements/context/contexts-registry.ts +10 -0
  105. package/src/elements/context/index.ts +3 -0
  106. package/src/elements/context/typings.ts +39 -0
  107. package/src/elements/editable.test.ts +334 -0
  108. package/src/elements/editable.ts +114 -0
  109. package/src/elements/editor/custom-editor-plugins.test.ts +103 -0
  110. package/src/elements/editor/custom-editor-plugins.ts +86 -0
  111. package/src/elements/editor/editor.test.ts +438 -0
  112. package/src/elements/editor/editor.ts +279 -0
  113. package/src/elements/editor/editors-registry.test.ts +10 -0
  114. package/src/elements/editor/editors-registry.ts +10 -0
  115. package/src/elements/editor/index.ts +2 -0
  116. package/src/elements/editor/plugins/index.ts +1 -0
  117. package/src/elements/editor/plugins/sync-editor-with-input.ts +78 -0
  118. package/src/elements/editor/typings.ts +114 -0
  119. package/src/elements/editor/utils/create-editor-in-context.ts +90 -0
  120. package/src/elements/editor/utils/index.ts +11 -0
  121. package/src/elements/editor/utils/is-single-root-editor.test.ts +40 -0
  122. package/src/elements/editor/utils/is-single-root-editor.ts +11 -0
  123. package/src/elements/editor/utils/load-editor-constructor.test.ts +62 -0
  124. package/src/elements/editor/utils/load-editor-constructor.ts +29 -0
  125. package/src/elements/editor/utils/load-editor-plugins.test.ts +100 -0
  126. package/src/elements/editor/utils/load-editor-plugins.ts +73 -0
  127. package/src/elements/editor/utils/load-editor-translations.ts +233 -0
  128. package/src/elements/editor/utils/normalize-custom-translations.test.ts +152 -0
  129. package/src/elements/editor/utils/normalize-custom-translations.ts +18 -0
  130. package/src/elements/editor/utils/query-all-editor-ids.ts +9 -0
  131. package/src/elements/editor/utils/query-editor-editables.ts +101 -0
  132. package/src/elements/editor/utils/resolve-editor-config-elements-references.test.ts +93 -0
  133. package/src/elements/editor/utils/resolve-editor-config-elements-references.ts +36 -0
  134. package/src/elements/editor/utils/set-editor-editable-height.test.ts +131 -0
  135. package/src/elements/editor/utils/set-editor-editable-height.ts +15 -0
  136. package/src/elements/editor/utils/wrap-with-watchdog.test.ts +45 -0
  137. package/src/elements/editor/utils/wrap-with-watchdog.ts +51 -0
  138. package/src/elements/index.ts +14 -0
  139. package/src/elements/register-custom-elements.ts +24 -0
  140. package/src/elements/ui-part.test.ts +142 -0
  141. package/src/elements/ui-part.ts +80 -0
  142. package/src/index.ts +6 -0
  143. package/src/shared/async-registry.test.ts +737 -0
  144. package/src/shared/async-registry.ts +353 -0
  145. package/src/shared/camel-case.test.ts +35 -0
  146. package/src/shared/camel-case.ts +11 -0
  147. package/src/shared/debounce.test.ts +72 -0
  148. package/src/shared/debounce.ts +16 -0
  149. package/src/shared/deep-camel-case-keys.test.ts +34 -0
  150. package/src/shared/deep-camel-case-keys.ts +26 -0
  151. package/src/shared/filter-object-values.test.ts +25 -0
  152. package/src/shared/filter-object-values.ts +17 -0
  153. package/src/shared/index.ts +14 -0
  154. package/src/shared/is-empty-object.test.ts +78 -0
  155. package/src/shared/is-empty-object.ts +3 -0
  156. package/src/shared/is-plain-object.test.ts +38 -0
  157. package/src/shared/is-plain-object.ts +15 -0
  158. package/src/shared/map-object-values.test.ts +29 -0
  159. package/src/shared/map-object-values.ts +19 -0
  160. package/src/shared/once.test.ts +116 -0
  161. package/src/shared/once.ts +12 -0
  162. package/src/shared/shallow-equal.test.ts +51 -0
  163. package/src/shared/shallow-equal.ts +30 -0
  164. package/src/shared/timeout.test.ts +65 -0
  165. package/src/shared/timeout.ts +13 -0
  166. package/src/shared/uid.test.ts +25 -0
  167. package/src/shared/uid.ts +8 -0
  168. package/src/shared/wait-for-dom-ready.test.ts +87 -0
  169. package/src/shared/wait-for-dom-ready.ts +21 -0
  170. package/src/shared/wait-for.test.ts +24 -0
  171. package/src/shared/wait-for.ts +56 -0
  172. package/src/types/can-be-promise.type.ts +1 -0
  173. package/src/types/index.ts +2 -0
  174. package/src/types/required-by.type.ts +1 -0
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Queries all CKEditor 5 editor IDs present in the document.
3
+ */
4
+ export function queryAllEditorIds(): string[] {
5
+ return Array
6
+ .from(document.querySelectorAll<HTMLElement>('cke5-editor'))
7
+ .map(element => element.getAttribute('data-cke-editor-id'))
8
+ .filter((id): id is string => id !== null);
9
+ }
@@ -0,0 +1,101 @@
1
+ import type { EditorId } from '../typings';
2
+
3
+ import { filterObjectValues, mapObjectValues } from '../../../shared';
4
+
5
+ /**
6
+ * Gets the initial root elements for the editor based on its type.
7
+ *
8
+ * @param editorId The editor's ID.
9
+ * @returns The root element(s) for the editor.
10
+ */
11
+ export function queryEditablesElements(editorId: EditorId) {
12
+ const editables = queryAllEditorEditables(editorId);
13
+
14
+ return mapObjectValues(editables, ({ element }) => element);
15
+ }
16
+
17
+ /**
18
+ * Gets the initial data for the roots of the editor. If the editor is a single editing-like editor,
19
+ * it retrieves the initial value from the element's attribute. Otherwise, it returns an object mapping
20
+ * editable names to their initial values.
21
+ *
22
+ * @param editorId The editor's ID.
23
+ * @returns The initial values for the editor's roots.
24
+ */
25
+ export function queryEditablesSnapshotContent(editorId: EditorId) {
26
+ const editables = queryAllEditorEditables(editorId);
27
+ const values = mapObjectValues(editables, ({ content }) => content);
28
+
29
+ return filterObjectValues(values, value => typeof value === 'string') as Record<string, string>;
30
+ }
31
+
32
+ /**
33
+ * Queries all editable elements within a specific editor instance. It picks
34
+ * initial values from actually rendered elements or from the editor container's.
35
+ *
36
+ * It may differ from the `initialData` used during editor creation, as it might
37
+ * not set all roots or set different values.
38
+ *
39
+ * @param editorId The ID of the editor to query.
40
+ * @returns An object mapping editable names to their corresponding elements and initial values.
41
+ */
42
+ function queryAllEditorEditables(editorId: EditorId) {
43
+ const acc = (
44
+ Array
45
+ .from(document.querySelectorAll<HTMLElement>(`cke5-editable[data-cke-editor-id="${editorId}"]`))
46
+ .reduce<Record<string, EditableItem>>((acc, element) => {
47
+ const rootName = element.getAttribute('data-cke-root-name')!;
48
+ const content = element.getAttribute('data-cke-content');
49
+
50
+ acc[rootName] = {
51
+ element: element.querySelector<HTMLElement>('[data-cke-editable-content]')!,
52
+ content,
53
+ };
54
+
55
+ return acc;
56
+ }, Object.create({}))
57
+ );
58
+
59
+ const editor = document.querySelector<HTMLElement>(`cke5-editor[data-cke-editor-id="${editorId}"]`);
60
+
61
+ /* v8 ignore next 3 */
62
+ if (!editor) {
63
+ return acc;
64
+ }
65
+
66
+ const currentMain = acc['main'];
67
+ const initialRootEditableValue = JSON.parse(editor.getAttribute('data-cke-content')!);
68
+ const contentElement = document.querySelector<HTMLElement>(`#${editorId}_editor `);
69
+
70
+ // If found `main` editable, but it has no content, try to fill it from the editor container.
71
+ if (currentMain && initialRootEditableValue?.['main']) {
72
+ return {
73
+ ...acc,
74
+ main: {
75
+ ...currentMain,
76
+ content: currentMain.content || initialRootEditableValue['main'],
77
+ },
78
+ };
79
+ }
80
+
81
+ // If no `main` editable found, try to create it from the editor container.
82
+ if (contentElement) {
83
+ return {
84
+ ...acc,
85
+ main: {
86
+ element: contentElement,
87
+ content: initialRootEditableValue?.['main'] || null,
88
+ },
89
+ };
90
+ }
91
+
92
+ return acc;
93
+ }
94
+
95
+ /**
96
+ * Type representing an editable item within an editor.
97
+ */
98
+ export type EditableItem = {
99
+ element: HTMLElement;
100
+ content: string | null;
101
+ };
@@ -0,0 +1,93 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { resolveEditorConfigElementReferences } from './resolve-editor-config-elements-references';
4
+
5
+ describe('resolveEditorConfigElementReferences', () => {
6
+ afterEach(() => {
7
+ document.body.innerHTML = '';
8
+ });
9
+
10
+ it('resolves a single element reference', () => {
11
+ const div = document.createElement('div');
12
+ div.id = 'test-div';
13
+ document.body.appendChild(div);
14
+
15
+ const config = {
16
+ foo: { $element: '#test-div' },
17
+ };
18
+
19
+ const result = resolveEditorConfigElementReferences(config);
20
+ expect(result.foo).toBe(div);
21
+ });
22
+
23
+ it('returns null if element not found', () => {
24
+ const config = {
25
+ foo: { $element: '#not-exist' },
26
+ };
27
+ const result = resolveEditorConfigElementReferences(config);
28
+
29
+ expect(result.foo).toBeNull();
30
+ });
31
+
32
+ it('recursively resolves nested element references', () => {
33
+ const span = document.createElement('span');
34
+ span.className = 'my-span';
35
+ document.body.appendChild(span);
36
+
37
+ const config = {
38
+ nested: {
39
+ bar: { $element: '.my-span' },
40
+ },
41
+ };
42
+
43
+ const result = resolveEditorConfigElementReferences(config);
44
+ expect(result.nested.bar).toBe(span);
45
+ });
46
+
47
+ it('resolves element references in arrays', () => {
48
+ const el1 = document.createElement('div');
49
+ el1.id = 'el1';
50
+ document.body.appendChild(el1);
51
+
52
+ const el2 = document.createElement('div');
53
+ el2.id = 'el2';
54
+ document.body.appendChild(el2);
55
+
56
+ const config = [
57
+ { $element: '#el1' },
58
+ { $element: '#el2' },
59
+ { notElement: 123 },
60
+ ];
61
+
62
+ const result = resolveEditorConfigElementReferences(config);
63
+
64
+ expect(result[0]).toBe(el1);
65
+ expect(result[1]).toBe(el2);
66
+ expect(result[2]).toEqual({ notElement: 123 });
67
+ });
68
+
69
+ it('returns primitives as is', () => {
70
+ expect(resolveEditorConfigElementReferences(42)).toBe(42);
71
+ expect(resolveEditorConfigElementReferences('foo')).toBe('foo');
72
+ expect(resolveEditorConfigElementReferences(null)).toBe(null);
73
+ expect(resolveEditorConfigElementReferences(undefined)).toBe(undefined);
74
+ });
75
+
76
+ it('warns for invalid selector type', () => {
77
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
78
+ const config = { foo: { $element: '.foo' } };
79
+
80
+ resolveEditorConfigElementReferences(config);
81
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Element not found for selector: .foo'));
82
+ warnSpy.mockRestore();
83
+ });
84
+
85
+ it('warns if element not found', () => {
86
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
87
+ const config = { foo: { $element: '#not-found' } };
88
+
89
+ resolveEditorConfigElementReferences(config);
90
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Element not found'));
91
+ warnSpy.mockRestore();
92
+ });
93
+ });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Resolves element references in configuration object.
3
+ * Looks for objects with { $element: "selector" } format and replaces them with actual DOM elements.
4
+ *
5
+ * @param obj - Configuration object to process
6
+ * @returns Processed configuration object with resolved element references
7
+ */
8
+ export function resolveEditorConfigElementReferences<T>(obj: T): T {
9
+ if (!obj || typeof obj !== 'object') {
10
+ return obj;
11
+ }
12
+
13
+ if (Array.isArray(obj)) {
14
+ return obj.map(item => resolveEditorConfigElementReferences(item)) as T;
15
+ }
16
+
17
+ const anyObj = obj as any;
18
+
19
+ if (anyObj.$element && typeof anyObj.$element === 'string') {
20
+ const element = document.querySelector(anyObj.$element);
21
+
22
+ if (!element) {
23
+ console.warn(`Element not found for selector: ${anyObj.$element}`);
24
+ }
25
+
26
+ return (element || null) as T;
27
+ }
28
+
29
+ const result = Object.create(null);
30
+
31
+ for (const [key, value] of Object.entries(obj)) {
32
+ result[key] = resolveEditorConfigElementReferences(value);
33
+ }
34
+
35
+ return result as T;
36
+ }
@@ -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,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 { registerCustomElements } from './register-custom-elements';
14
+ export { UIPartComponentElement } from './ui-part';
@@ -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 Symfony component hooks.
15
+ */
16
+ export function registerCustomElements() {
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,142 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import {
4
+ createEditorPreset,
5
+ createUIPartSnapshot,
6
+ renderTestEditor,
7
+ renderTestUIPart,
8
+ waitForTestEditor,
9
+ } from '~/test-utils';
10
+
11
+ import { CustomEditorPluginsRegistry } from './editor/custom-editor-plugins';
12
+ import { EditorsRegistry } from './editor/editors-registry';
13
+ import { registerCustomElements } from './register-custom-elements';
14
+
15
+ describe('ui-part component', () => {
16
+ beforeEach(() => {
17
+ document.body.innerHTML = '';
18
+ registerCustomElements();
19
+ });
20
+
21
+ afterEach(async () => {
22
+ document.body.innerHTML = '';
23
+ CustomEditorPluginsRegistry.the.unregisterAll();
24
+ EditorsRegistry.the.reset();
25
+ vi.restoreAllMocks();
26
+ });
27
+
28
+ describe('mounting ui part', () => {
29
+ it('should mount toolbar to the editor after mounting editor', async () => {
30
+ appendMultirootEditor();
31
+
32
+ const editor = await waitForTestEditor();
33
+ const toolbarElement = editor.ui.view.toolbar?.element;
34
+
35
+ const el = renderTestUIPart(createUIPartSnapshot('toolbar'));
36
+
37
+ await vi.waitFor(() => {
38
+ expect(el.contains(toolbarElement!)).toBe(true);
39
+ });
40
+
41
+ expect(toolbarElement).toBeTruthy();
42
+ });
43
+
44
+ it('should mount menubar to the editor after mounting editor', async () => {
45
+ appendMultirootEditor();
46
+
47
+ const editor = await waitForTestEditor();
48
+ const menubarElement = (editor.ui.view as any).menuBarView.element;
49
+
50
+ const el = renderTestUIPart(createUIPartSnapshot('menubar'));
51
+
52
+ await vi.waitFor(() => {
53
+ expect(el.children.length).toBeGreaterThan(0);
54
+ });
55
+
56
+ expect(el.contains(menubarElement)).toBe(true);
57
+ });
58
+
59
+ it('should mount UI part before editor is created', async () => {
60
+ const el = renderTestUIPart(createUIPartSnapshot('toolbar'));
61
+
62
+ appendMultirootEditor();
63
+
64
+ const editor = await waitForTestEditor();
65
+ const toolbarElement = editor.ui.view.toolbar?.element;
66
+
67
+ await vi.waitFor(() => {
68
+ expect(el.contains(toolbarElement!)).toBe(true);
69
+ });
70
+
71
+ expect(toolbarElement).toBeTruthy();
72
+ });
73
+
74
+ it('should default to first editor ID if data-cke-editor-id is missing', async () => {
75
+ appendMultirootEditor();
76
+
77
+ const editor = await waitForTestEditor();
78
+ const toolbarElement = editor.ui.view.toolbar?.element;
79
+
80
+ // Render UI part without editorId
81
+ const el = renderTestUIPart({ editorId: undefined } as any);
82
+
83
+ await vi.waitFor(() => {
84
+ expect(el.contains(toolbarElement!)).toBe(true);
85
+ });
86
+
87
+ expect(toolbarElement).toBeTruthy();
88
+ });
89
+ });
90
+
91
+ describe('destroying ui part', () => {
92
+ beforeEach(async () => {
93
+ appendMultirootEditor();
94
+ await waitForTestEditor();
95
+ });
96
+
97
+ it('should clear UI part element on destruction', async () => {
98
+ const el = renderTestUIPart(createUIPartSnapshot('toolbar'));
99
+
100
+ await vi.waitFor(() => {
101
+ expect(el.children.length).toBeGreaterThan(0);
102
+ });
103
+
104
+ el.remove();
105
+
106
+ await vi.waitFor(() => {
107
+ expect(el.innerHTML).toBe('');
108
+ });
109
+
110
+ expect(el.style.display).toBe('none');
111
+ });
112
+
113
+ it('should hide element during destruction', async () => {
114
+ const el = renderTestUIPart(createUIPartSnapshot('toolbar'));
115
+
116
+ // If we remove immediately, disconnectedCallback should hide it.
117
+ el.remove();
118
+
119
+ // Ensure style is updated synchronously or microtask
120
+ expect(el.style.display).toBe('none');
121
+ });
122
+
123
+ it('should handle destruction when mounted promise is not resolved yet', async () => {
124
+ document.body.innerHTML = '';
125
+ EditorsRegistry.the.reset();
126
+
127
+ const el = renderTestUIPart(createUIPartSnapshot('toolbar'));
128
+
129
+ el.remove();
130
+
131
+ expect(el.innerHTML).toBe('');
132
+ expect(el.style.display).toBe('none');
133
+ });
134
+ });
135
+
136
+ function appendMultirootEditor(initialContent: Record<string, string> = {}) {
137
+ renderTestEditor({
138
+ preset: createEditorPreset('multiroot'),
139
+ content: initialContent,
140
+ });
141
+ }
142
+ });