ckeditor5-phoenix 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 (138) hide show
  1. package/dist/index.cjs +2 -0
  2. package/dist/index.cjs.map +1 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.mjs +447 -0
  5. package/dist/index.mjs.map +1 -0
  6. package/dist/src/hooks/editable.d.ts +5 -0
  7. package/dist/src/hooks/editable.d.ts.map +1 -0
  8. package/dist/src/hooks/editable.test.d.ts +2 -0
  9. package/dist/src/hooks/editable.test.d.ts.map +1 -0
  10. package/dist/src/hooks/editor/editor.d.ts +5 -0
  11. package/dist/src/hooks/editor/editor.d.ts.map +1 -0
  12. package/dist/src/hooks/editor/editor.test.d.ts +2 -0
  13. package/dist/src/hooks/editor/editor.test.d.ts.map +1 -0
  14. package/dist/src/hooks/editor/editors-registry.d.ts +68 -0
  15. package/dist/src/hooks/editor/editors-registry.d.ts.map +1 -0
  16. package/dist/src/hooks/editor/editors-registry.test.d.ts +2 -0
  17. package/dist/src/hooks/editor/editors-registry.test.d.ts.map +1 -0
  18. package/dist/src/hooks/editor/index.d.ts +2 -0
  19. package/dist/src/hooks/editor/index.d.ts.map +1 -0
  20. package/dist/src/hooks/editor/typings.d.ts +63 -0
  21. package/dist/src/hooks/editor/typings.d.ts.map +1 -0
  22. package/dist/src/hooks/editor/utils/index.d.ts +7 -0
  23. package/dist/src/hooks/editor/utils/index.d.ts.map +1 -0
  24. package/dist/src/hooks/editor/utils/is-single-editing-like-editor.d.ts +9 -0
  25. package/dist/src/hooks/editor/utils/is-single-editing-like-editor.d.ts.map +1 -0
  26. package/dist/src/hooks/editor/utils/is-single-editing-like-editor.test.d.ts +2 -0
  27. package/dist/src/hooks/editor/utils/is-single-editing-like-editor.test.d.ts.map +1 -0
  28. package/dist/src/hooks/editor/utils/load-editor-constructor.d.ts +9 -0
  29. package/dist/src/hooks/editor/utils/load-editor-constructor.d.ts.map +1 -0
  30. package/dist/src/hooks/editor/utils/load-editor-constructor.test.d.ts +2 -0
  31. package/dist/src/hooks/editor/utils/load-editor-constructor.test.d.ts.map +1 -0
  32. package/dist/src/hooks/editor/utils/load-editor-plugins.d.ts +12 -0
  33. package/dist/src/hooks/editor/utils/load-editor-plugins.d.ts.map +1 -0
  34. package/dist/src/hooks/editor/utils/load-editor-plugins.test.d.ts +2 -0
  35. package/dist/src/hooks/editor/utils/load-editor-plugins.test.d.ts.map +1 -0
  36. package/dist/src/hooks/editor/utils/query-all-editor-editables.d.ts +16 -0
  37. package/dist/src/hooks/editor/utils/query-all-editor-editables.d.ts.map +1 -0
  38. package/dist/src/hooks/editor/utils/query-all-editor-editables.test.d.ts +2 -0
  39. package/dist/src/hooks/editor/utils/query-all-editor-editables.test.d.ts.map +1 -0
  40. package/dist/src/hooks/editor/utils/read-preset-or-throw.d.ts +9 -0
  41. package/dist/src/hooks/editor/utils/read-preset-or-throw.d.ts.map +1 -0
  42. package/dist/src/hooks/editor/utils/read-preset-or-throw.test.d.ts +2 -0
  43. package/dist/src/hooks/editor/utils/read-preset-or-throw.test.d.ts.map +1 -0
  44. package/dist/src/hooks/editor/utils/set-editor-editable-height.d.ts +9 -0
  45. package/dist/src/hooks/editor/utils/set-editor-editable-height.d.ts.map +1 -0
  46. package/dist/src/hooks/editor/utils/set-editor-editable-height.test.d.ts +2 -0
  47. package/dist/src/hooks/editor/utils/set-editor-editable-height.test.d.ts.map +1 -0
  48. package/dist/src/hooks/index.d.ts +6 -0
  49. package/dist/src/hooks/index.d.ts.map +1 -0
  50. package/dist/src/hooks/ui-part.d.ts +5 -0
  51. package/dist/src/hooks/ui-part.d.ts.map +1 -0
  52. package/dist/src/hooks/ui-part.test.d.ts +2 -0
  53. package/dist/src/hooks/ui-part.test.d.ts.map +1 -0
  54. package/dist/src/index.d.ts +3 -0
  55. package/dist/src/index.d.ts.map +1 -0
  56. package/dist/src/shared/debounce.d.ts +2 -0
  57. package/dist/src/shared/debounce.d.ts.map +1 -0
  58. package/dist/src/shared/debounce.test.d.ts +2 -0
  59. package/dist/src/shared/debounce.test.d.ts.map +1 -0
  60. package/dist/src/shared/hook.d.ts +71 -0
  61. package/dist/src/shared/hook.d.ts.map +1 -0
  62. package/dist/src/shared/hook.test.d.ts +2 -0
  63. package/dist/src/shared/hook.test.d.ts.map +1 -0
  64. package/dist/src/shared/index.d.ts +5 -0
  65. package/dist/src/shared/index.d.ts.map +1 -0
  66. package/dist/src/shared/map-object-values.d.ts +11 -0
  67. package/dist/src/shared/map-object-values.d.ts.map +1 -0
  68. package/dist/src/shared/map-object-values.test.d.ts +2 -0
  69. package/dist/src/shared/map-object-values.test.d.ts.map +1 -0
  70. package/dist/src/shared/parse-int-if-not-null.d.ts +2 -0
  71. package/dist/src/shared/parse-int-if-not-null.d.ts.map +1 -0
  72. package/dist/src/shared/parse-int-if-not-null.test.d.ts +2 -0
  73. package/dist/src/shared/parse-int-if-not-null.test.d.ts.map +1 -0
  74. package/dist/src/types/index.d.ts +2 -0
  75. package/dist/src/types/index.d.ts.map +1 -0
  76. package/dist/src/types/required-by.type.d.ts +2 -0
  77. package/dist/src/types/required-by.type.d.ts.map +1 -0
  78. package/dist/test-utils/editor/create-editable-html-element.d.ts +12 -0
  79. package/dist/test-utils/editor/create-editable-html-element.d.ts.map +1 -0
  80. package/dist/test-utils/editor/create-editor-html-element.d.ts +13 -0
  81. package/dist/test-utils/editor/create-editor-html-element.d.ts.map +1 -0
  82. package/dist/test-utils/editor/create-editor-preset.d.ts +15 -0
  83. package/dist/test-utils/editor/create-editor-preset.d.ts.map +1 -0
  84. package/dist/test-utils/editor/create-ui-part-html-element.d.ts +6 -0
  85. package/dist/test-utils/editor/create-ui-part-html-element.d.ts.map +1 -0
  86. package/dist/test-utils/editor/get-test-editor-input.d.ts +5 -0
  87. package/dist/test-utils/editor/get-test-editor-input.d.ts.map +1 -0
  88. package/dist/test-utils/editor/index.d.ts +8 -0
  89. package/dist/test-utils/editor/index.d.ts.map +1 -0
  90. package/dist/test-utils/editor/is-editor-shown.d.ts +5 -0
  91. package/dist/test-utils/editor/is-editor-shown.d.ts.map +1 -0
  92. package/dist/test-utils/editor/wait-for-test-editor.d.ts +7 -0
  93. package/dist/test-utils/editor/wait-for-test-editor.d.ts.map +1 -0
  94. package/dist/test-utils/html.d.ts +28 -0
  95. package/dist/test-utils/html.d.ts.map +1 -0
  96. package/dist/test-utils/html.test.d.ts +2 -0
  97. package/dist/test-utils/html.test.d.ts.map +1 -0
  98. package/dist/test-utils/index.d.ts +3 -0
  99. package/dist/test-utils/index.d.ts.map +1 -0
  100. package/dist/vite.config.d.ts +3 -0
  101. package/dist/vite.config.d.ts.map +1 -0
  102. package/package.json +44 -0
  103. package/src/hooks/editable.test.ts +241 -0
  104. package/src/hooks/editable.ts +113 -0
  105. package/src/hooks/editor/editor.test.ts +333 -0
  106. package/src/hooks/editor/editor.ts +179 -0
  107. package/src/hooks/editor/editors-registry.test.ts +238 -0
  108. package/src/hooks/editor/editors-registry.ts +154 -0
  109. package/src/hooks/editor/index.ts +1 -0
  110. package/src/hooks/editor/typings.ts +72 -0
  111. package/src/hooks/editor/utils/index.ts +6 -0
  112. package/src/hooks/editor/utils/is-single-editing-like-editor.test.ts +40 -0
  113. package/src/hooks/editor/utils/is-single-editing-like-editor.ts +11 -0
  114. package/src/hooks/editor/utils/load-editor-constructor.test.ts +62 -0
  115. package/src/hooks/editor/utils/load-editor-constructor.ts +27 -0
  116. package/src/hooks/editor/utils/load-editor-plugins.test.ts +46 -0
  117. package/src/hooks/editor/utils/load-editor-plugins.ts +50 -0
  118. package/src/hooks/editor/utils/query-all-editor-editables.test.ts +140 -0
  119. package/src/hooks/editor/utils/query-all-editor-editables.ts +47 -0
  120. package/src/hooks/editor/utils/read-preset-or-throw.test.ts +162 -0
  121. package/src/hooks/editor/utils/read-preset-or-throw.ts +33 -0
  122. package/src/hooks/editor/utils/set-editor-editable-height.test.ts +131 -0
  123. package/src/hooks/editor/utils/set-editor-editable-height.ts +15 -0
  124. package/src/hooks/index.ts +9 -0
  125. package/src/hooks/ui-part.test.ts +151 -0
  126. package/src/hooks/ui-part.ts +89 -0
  127. package/src/index.ts +2 -0
  128. package/src/shared/debounce.test.ts +72 -0
  129. package/src/shared/debounce.ts +16 -0
  130. package/src/shared/hook.test.ts +131 -0
  131. package/src/shared/hook.ts +141 -0
  132. package/src/shared/index.ts +4 -0
  133. package/src/shared/map-object-values.test.ts +29 -0
  134. package/src/shared/map-object-values.ts +19 -0
  135. package/src/shared/parse-int-if-not-null.test.ts +25 -0
  136. package/src/shared/parse-int-if-not-null.ts +9 -0
  137. package/src/types/index.ts +1 -0
  138. package/src/types/required-by.type.ts +1 -0
@@ -0,0 +1,50 @@
1
+ import type { PluginConstructor } from 'ckeditor5';
2
+
3
+ import type { EditorPlugin } from '../typings';
4
+
5
+ /**
6
+ * Loads CKEditor plugins from base and premium packages.
7
+ * First tries to load from the base 'ckeditor5' package, then falls back to 'ckeditor5-premium-features'.
8
+ *
9
+ * @param plugins - Array of plugin names to load
10
+ * @returns Promise that resolves to an array of loaded Plugin instances
11
+ * @throws Error if a plugin is not found in either package
12
+ */
13
+ export async function loadEditorPlugins(plugins: EditorPlugin[]): Promise<PluginConstructor[]> {
14
+ const basePackage: Record<string, any> = await import('ckeditor5');
15
+ let premiumPackage: Record<string, any> | null = null;
16
+
17
+ const loaders = plugins.map(async (plugin) => {
18
+ // Let's first try to load the plugin from the base package.
19
+ // Coverage is disabled due to Vitest issues with mocking dynamic imports.
20
+
21
+ /* v8 ignore start */
22
+ const { [plugin]: basePkgImport } = basePackage;
23
+
24
+ if (basePkgImport) {
25
+ return basePkgImport as PluginConstructor;
26
+ }
27
+
28
+ // Plugin not found in base package, try premium package.
29
+ if (!premiumPackage) {
30
+ try {
31
+ premiumPackage = await import('ckeditor5-premium-features');
32
+ }
33
+ catch (error) {
34
+ console.error(`Failed to load premium package: ${error}`);
35
+ }
36
+ }
37
+
38
+ const { [plugin]: premiumPkgImport } = premiumPackage || {};
39
+
40
+ if (premiumPkgImport) {
41
+ return premiumPkgImport as PluginConstructor;
42
+ }
43
+
44
+ // Plugin not found in either package, throw an error.
45
+ throw new Error(`Plugin "${plugin}" not found in base or premium packages.`);
46
+ /* v8 ignore end */
47
+ });
48
+
49
+ return Promise.all(loaders);
50
+ }
@@ -0,0 +1,140 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+
3
+ import { queryAllEditorEditables } from './query-all-editor-editables';
4
+
5
+ describe('queryAllEditorEditables', () => {
6
+ beforeEach(() => {
7
+ document.body.innerHTML = '';
8
+ });
9
+
10
+ it('should return empty object when no editables found', () => {
11
+ const result = queryAllEditorEditables('test-editor');
12
+ expect(result).toEqual({});
13
+ });
14
+
15
+ it('should find editables with specific editor ID', () => {
16
+ document.body.innerHTML = `
17
+ <div data-cke-editor-id="test-editor" data-cke-editable-root-name="main" data-cke-editable-initial-value="Initial content">
18
+ <div data-cke-editable-content>Content area</div>
19
+ </div>
20
+ `;
21
+
22
+ const result = queryAllEditorEditables('test-editor');
23
+
24
+ expect(result).toHaveProperty('main');
25
+ expect(result['main']?.content).toBeInstanceOf(HTMLElement);
26
+ expect(result['main']?.initialValue).toBe('Initial content');
27
+ });
28
+
29
+ it('should find editables without editor ID (fallback)', () => {
30
+ document.body.innerHTML = `
31
+ <div data-cke-editable-root-name="main" data-cke-editable-initial-value="Initial content">
32
+ <div data-cke-editable-content>Content area</div>
33
+ </div>
34
+ `;
35
+
36
+ const result = queryAllEditorEditables('any-editor');
37
+
38
+ expect(result).toHaveProperty('main');
39
+ expect(result['main']?.content).toBeInstanceOf(HTMLElement);
40
+ expect(result['main']?.initialValue).toBe('Initial content');
41
+ });
42
+
43
+ it('should handle multiple editables', () => {
44
+ document.body.innerHTML = `
45
+ <div data-cke-editor-id="test-editor" data-cke-editable-root-name="main" data-cke-editable-initial-value="Main content">
46
+ <div data-cke-editable-content>Main area</div>
47
+ </div>
48
+ <div data-cke-editor-id="test-editor" data-cke-editable-root-name="sidebar" data-cke-editable-initial-value="Sidebar content">
49
+ <div data-cke-editable-content>Sidebar area</div>
50
+ </div>
51
+ `;
52
+
53
+ const result = queryAllEditorEditables('test-editor');
54
+
55
+ expect(Object.keys(result)).toHaveLength(2);
56
+ expect(result).toHaveProperty('main');
57
+ expect(result).toHaveProperty('sidebar');
58
+ expect(result['main']?.initialValue).toBe('Main content');
59
+ expect(result['sidebar']?.initialValue).toBe('Sidebar content');
60
+ });
61
+
62
+ it('should ignore editables without content element', () => {
63
+ document.body.innerHTML = `
64
+ <div data-cke-editor-id="test-editor" data-cke-editable-root-name="main" data-cke-editable-initial-value="Initial content">
65
+ <!-- No content element -->
66
+ </div>
67
+ `;
68
+
69
+ const result = queryAllEditorEditables('test-editor');
70
+
71
+ expect(result).toEqual({});
72
+ });
73
+
74
+ it('should ignore editables without name attribute', () => {
75
+ document.body.innerHTML = `
76
+ <div data-cke-editor-id="test-editor" data-cke-editable-initial-value="Initial content">
77
+ <div data-cke-editable-content>Content area</div>
78
+ </div>
79
+ `;
80
+
81
+ const result = queryAllEditorEditables('test-editor');
82
+
83
+ expect(result).toEqual({});
84
+ });
85
+
86
+ it('should use empty string as default initial value', () => {
87
+ document.body.innerHTML = `
88
+ <div data-cke-editor-id="test-editor" data-cke-editable-root-name="main">
89
+ <div data-cke-editable-content>Content area</div>
90
+ </div>
91
+ `;
92
+
93
+ const result = queryAllEditorEditables('test-editor');
94
+
95
+ expect(result).toHaveProperty('main');
96
+ expect(result['main']?.initialValue).toBe('');
97
+ });
98
+
99
+ it('should filter by specific editor ID', () => {
100
+ document.body.innerHTML = `
101
+ <div data-cke-editor-id="editor1" data-cke-editable-root-name="main" data-cke-editable-initial-value="Editor 1 content">
102
+ <div data-cke-editable-content>Editor 1 area</div>
103
+ </div>
104
+ <div data-cke-editor-id="editor2" data-cke-editable-root-name="main" data-cke-editable-initial-value="Editor 2 content">
105
+ <div data-cke-editable-content>Editor 2 area</div>
106
+ </div>
107
+ `;
108
+
109
+ const result1 = queryAllEditorEditables('editor1');
110
+ const result2 = queryAllEditorEditables('editor2');
111
+
112
+ expect(result1).toHaveProperty('main');
113
+ expect(result1['main']?.initialValue).toBe('Editor 1 content');
114
+
115
+ expect(result2).toHaveProperty('main');
116
+ expect(result2['main']?.initialValue).toBe('Editor 2 content');
117
+ });
118
+
119
+ it('should handle complex nested structures', () => {
120
+ document.body.innerHTML = `
121
+ <div class="editor-container">
122
+ <div data-cke-editor-id="test-editor" data-cke-editable-root-name="header" data-cke-editable-initial-value="Header content">
123
+ <div class="wrapper">
124
+ <div data-cke-editable-content class="content-area">Header area</div>
125
+ </div>
126
+ </div>
127
+ <div data-cke-editor-id="test-editor" data-cke-editable-root-name="body" data-cke-editable-initial-value="Body content">
128
+ <div data-cke-editable-content>Body area</div>
129
+ </div>
130
+ </div>
131
+ `;
132
+
133
+ const result = queryAllEditorEditables('test-editor');
134
+
135
+ expect(Object.keys(result)).toHaveLength(2);
136
+ expect(result).toHaveProperty('header');
137
+ expect(result).toHaveProperty('body');
138
+ expect(result['header']?.content.className).toBe('content-area');
139
+ });
140
+ });
@@ -0,0 +1,47 @@
1
+ import type { EditorId } from '../typings';
2
+
3
+ /**
4
+ * Queries all editable elements within a specific editor instance.
5
+ *
6
+ * @param editorId The ID of the editor to query.
7
+ * @returns An object mapping editable names to their corresponding elements and initial values.
8
+ */
9
+ export function queryAllEditorEditables(editorId: EditorId): Record<string, EditableItem> {
10
+ const iterator = document.querySelectorAll<HTMLElement>(
11
+ [
12
+ `[data-cke-editor-id="${editorId}"][data-cke-editable-root-name]`,
13
+ '[data-cke-editable-root-name]:not([data-cke-editor-id])',
14
+ ]
15
+ .join(', '),
16
+ );
17
+
18
+ return (
19
+ Array
20
+ .from(iterator)
21
+ .reduce<Record<string, EditableItem>>((acc, element) => {
22
+ const name = element.getAttribute('data-cke-editable-root-name');
23
+ const initialValue = element.getAttribute('data-cke-editable-initial-value') || '';
24
+ const content = element.querySelector('[data-cke-editable-content]') as HTMLElement;
25
+
26
+ if (!name || !content) {
27
+ return acc;
28
+ }
29
+
30
+ return {
31
+ ...acc,
32
+ [name]: {
33
+ content,
34
+ initialValue,
35
+ },
36
+ };
37
+ }, Object.create({}))
38
+ );
39
+ }
40
+
41
+ /**
42
+ * Type representing an editable item within an editor.
43
+ */
44
+ export type EditableItem = {
45
+ content: HTMLElement;
46
+ initialValue: string;
47
+ };
@@ -0,0 +1,162 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { readPresetOrThrow } from './read-preset-or-throw';
4
+
5
+ describe('readPresetOrThrow', () => {
6
+ it('should parse valid preset configuration', () => {
7
+ const element = document.createElement('div');
8
+ const preset = {
9
+ type: 'classic',
10
+ config: {
11
+ plugins: ['Plugin1', 'Plugin2'],
12
+ toolbar: ['bold', 'italic'],
13
+ },
14
+ license: {
15
+ key: 'test-license-key',
16
+ },
17
+ };
18
+ element.setAttribute('cke-preset', JSON.stringify(preset));
19
+
20
+ const result = readPresetOrThrow(element);
21
+
22
+ expect(result).toEqual(preset);
23
+ });
24
+
25
+ it('should throw error when cke-preset attribute is missing', () => {
26
+ const element = document.createElement('div');
27
+
28
+ expect(() => readPresetOrThrow(element)).toThrow(
29
+ 'CKEditor5 hook requires a "cke-preset" attribute on the element.',
30
+ );
31
+ });
32
+
33
+ it('should throw error when type is missing', () => {
34
+ const element = document.createElement('div');
35
+ const preset = {
36
+ config: { plugins: [] },
37
+ license: { key: 'test-key' },
38
+ };
39
+ element.setAttribute('cke-preset', JSON.stringify(preset));
40
+
41
+ expect(() => readPresetOrThrow(element)).toThrow(
42
+ 'CKEditor5 hook configuration must include "editor", "config", and "license" properties.',
43
+ );
44
+ });
45
+
46
+ it('should throw error when config is missing', () => {
47
+ const element = document.createElement('div');
48
+ const preset = {
49
+ type: 'classic',
50
+ license: { key: 'test-key' },
51
+ };
52
+ element.setAttribute('cke-preset', JSON.stringify(preset));
53
+
54
+ expect(() => readPresetOrThrow(element)).toThrow(
55
+ 'CKEditor5 hook configuration must include "editor", "config", and "license" properties.',
56
+ );
57
+ });
58
+
59
+ it('should throw error when license is missing', () => {
60
+ const element = document.createElement('div');
61
+ const preset = {
62
+ type: 'classic',
63
+ config: { plugins: [] },
64
+ };
65
+ element.setAttribute('cke-preset', JSON.stringify(preset));
66
+
67
+ expect(() => readPresetOrThrow(element)).toThrow(
68
+ 'CKEditor5 hook configuration must include "editor", "config", and "license" properties.',
69
+ );
70
+ });
71
+
72
+ it('should throw error for invalid editor type', () => {
73
+ const element = document.createElement('div');
74
+ const preset = {
75
+ type: 'invalid-type',
76
+ config: { plugins: [] },
77
+ license: { key: 'test-key' },
78
+ };
79
+ element.setAttribute('cke-preset', JSON.stringify(preset));
80
+
81
+ expect(() => readPresetOrThrow(element)).toThrow(
82
+ 'Invalid editor type: invalid-type. Must be one of: inline, classic, balloon, decoupled, multiroot.',
83
+ );
84
+ });
85
+
86
+ it('should handle all valid editor types', () => {
87
+ const validTypes = ['inline', 'classic', 'balloon', 'decoupled', 'multiroot'];
88
+
89
+ validTypes.forEach((type) => {
90
+ const element = document.createElement('div');
91
+ const preset = {
92
+ type,
93
+ config: { plugins: [] },
94
+ license: { key: 'test-key' },
95
+ };
96
+ element.setAttribute('cke-preset', JSON.stringify(preset));
97
+
98
+ expect(() => readPresetOrThrow(element)).not.toThrow();
99
+ const result = readPresetOrThrow(element);
100
+ expect(result.type).toBe(type);
101
+ });
102
+ });
103
+
104
+ it('should throw error for invalid JSON', () => {
105
+ const element = document.createElement('div');
106
+ element.setAttribute('cke-preset', 'invalid-json');
107
+
108
+ expect(() => readPresetOrThrow(element)).toThrow();
109
+ });
110
+
111
+ it('should handle complex configuration objects', () => {
112
+ const element = document.createElement('div');
113
+ const preset = {
114
+ type: 'classic',
115
+ config: {
116
+ plugins: ['Plugin1', 'Plugin2'],
117
+ toolbar: {
118
+ items: ['bold', 'italic', 'link'],
119
+ shouldNotGroupWhenFull: true,
120
+ },
121
+ image: {
122
+ toolbar: ['imageStyle:full', 'imageStyle:side'],
123
+ },
124
+ customProperty: 'custom-value',
125
+ },
126
+ license: {
127
+ key: 'complex-license-key',
128
+ },
129
+ };
130
+
131
+ element.setAttribute('cke-preset', JSON.stringify(preset));
132
+
133
+ const result = readPresetOrThrow(element);
134
+
135
+ expect(result).toEqual(preset);
136
+ expect(result.config['toolbar']['items']).toEqual(['bold', 'italic', 'link']);
137
+ expect(result.config['customProperty']).toBe('custom-value');
138
+ });
139
+
140
+ it('should handle empty attribute gracefully', () => {
141
+ const element = document.createElement('div');
142
+ element.setAttribute('cke-preset', '');
143
+
144
+ expect(() => readPresetOrThrow(element)).toThrow(
145
+ 'CKEditor5 hook requires a "cke-preset" attribute on the element.',
146
+ );
147
+ });
148
+
149
+ it('should handle null values in configuration', () => {
150
+ const element = document.createElement('div');
151
+ const preset = {
152
+ type: 'classic',
153
+ config: null,
154
+ license: { key: 'test-key' },
155
+ };
156
+ element.setAttribute('cke-preset', JSON.stringify(preset));
157
+
158
+ expect(() => readPresetOrThrow(element)).toThrow(
159
+ 'CKEditor5 hook configuration must include "editor", "config", and "license" properties.',
160
+ );
161
+ });
162
+ });
@@ -0,0 +1,33 @@
1
+ import type { EditorPreset } from '../typings';
2
+
3
+ import { EDITOR_TYPES } from '../typings';
4
+
5
+ /**
6
+ * Reads the hook configuration from the element's attribute and parses it as JSON.
7
+ *
8
+ * @param element - The HTML element that contains the hook configuration.
9
+ * @returns The parsed hook configuration.
10
+ */
11
+ export function readPresetOrThrow(element: HTMLElement): EditorPreset {
12
+ const attributeValue = element.getAttribute('cke-preset');
13
+
14
+ if (!attributeValue) {
15
+ throw new Error('CKEditor5 hook requires a "cke-preset" attribute on the element.');
16
+ }
17
+
18
+ const { type, config, license } = JSON.parse(attributeValue);
19
+
20
+ if (!type || !config || !license) {
21
+ throw new Error('CKEditor5 hook configuration must include "editor", "config", and "license" properties.');
22
+ }
23
+
24
+ if (!EDITOR_TYPES.includes(type)) {
25
+ throw new Error(`Invalid editor type: ${type}. Must be one of: ${EDITOR_TYPES.join(', ')}.`);
26
+ }
27
+
28
+ return {
29
+ type,
30
+ config,
31
+ license,
32
+ };
33
+ }
@@ -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,9 @@
1
+ import { EditableHook } from './editable';
2
+ import { EditorHook } from './editor';
3
+ import { UIPartHook } from './ui-part';
4
+
5
+ export const Hooks = {
6
+ CKEditor5: EditorHook,
7
+ CKEditable: EditableHook,
8
+ CKUIPart: UIPartHook,
9
+ };