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.
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.mjs +447 -0
- package/dist/index.mjs.map +1 -0
- package/dist/src/hooks/editable.d.ts +5 -0
- package/dist/src/hooks/editable.d.ts.map +1 -0
- package/dist/src/hooks/editable.test.d.ts +2 -0
- package/dist/src/hooks/editable.test.d.ts.map +1 -0
- package/dist/src/hooks/editor/editor.d.ts +5 -0
- package/dist/src/hooks/editor/editor.d.ts.map +1 -0
- package/dist/src/hooks/editor/editor.test.d.ts +2 -0
- package/dist/src/hooks/editor/editor.test.d.ts.map +1 -0
- package/dist/src/hooks/editor/editors-registry.d.ts +68 -0
- package/dist/src/hooks/editor/editors-registry.d.ts.map +1 -0
- package/dist/src/hooks/editor/editors-registry.test.d.ts +2 -0
- package/dist/src/hooks/editor/editors-registry.test.d.ts.map +1 -0
- package/dist/src/hooks/editor/index.d.ts +2 -0
- package/dist/src/hooks/editor/index.d.ts.map +1 -0
- package/dist/src/hooks/editor/typings.d.ts +63 -0
- package/dist/src/hooks/editor/typings.d.ts.map +1 -0
- package/dist/src/hooks/editor/utils/index.d.ts +7 -0
- package/dist/src/hooks/editor/utils/index.d.ts.map +1 -0
- package/dist/src/hooks/editor/utils/is-single-editing-like-editor.d.ts +9 -0
- package/dist/src/hooks/editor/utils/is-single-editing-like-editor.d.ts.map +1 -0
- package/dist/src/hooks/editor/utils/is-single-editing-like-editor.test.d.ts +2 -0
- package/dist/src/hooks/editor/utils/is-single-editing-like-editor.test.d.ts.map +1 -0
- package/dist/src/hooks/editor/utils/load-editor-constructor.d.ts +9 -0
- package/dist/src/hooks/editor/utils/load-editor-constructor.d.ts.map +1 -0
- package/dist/src/hooks/editor/utils/load-editor-constructor.test.d.ts +2 -0
- package/dist/src/hooks/editor/utils/load-editor-constructor.test.d.ts.map +1 -0
- package/dist/src/hooks/editor/utils/load-editor-plugins.d.ts +12 -0
- package/dist/src/hooks/editor/utils/load-editor-plugins.d.ts.map +1 -0
- package/dist/src/hooks/editor/utils/load-editor-plugins.test.d.ts +2 -0
- package/dist/src/hooks/editor/utils/load-editor-plugins.test.d.ts.map +1 -0
- package/dist/src/hooks/editor/utils/query-all-editor-editables.d.ts +16 -0
- package/dist/src/hooks/editor/utils/query-all-editor-editables.d.ts.map +1 -0
- package/dist/src/hooks/editor/utils/query-all-editor-editables.test.d.ts +2 -0
- package/dist/src/hooks/editor/utils/query-all-editor-editables.test.d.ts.map +1 -0
- package/dist/src/hooks/editor/utils/read-preset-or-throw.d.ts +9 -0
- package/dist/src/hooks/editor/utils/read-preset-or-throw.d.ts.map +1 -0
- package/dist/src/hooks/editor/utils/read-preset-or-throw.test.d.ts +2 -0
- package/dist/src/hooks/editor/utils/read-preset-or-throw.test.d.ts.map +1 -0
- package/dist/src/hooks/editor/utils/set-editor-editable-height.d.ts +9 -0
- package/dist/src/hooks/editor/utils/set-editor-editable-height.d.ts.map +1 -0
- package/dist/src/hooks/editor/utils/set-editor-editable-height.test.d.ts +2 -0
- package/dist/src/hooks/editor/utils/set-editor-editable-height.test.d.ts.map +1 -0
- package/dist/src/hooks/index.d.ts +6 -0
- package/dist/src/hooks/index.d.ts.map +1 -0
- package/dist/src/hooks/ui-part.d.ts +5 -0
- package/dist/src/hooks/ui-part.d.ts.map +1 -0
- package/dist/src/hooks/ui-part.test.d.ts +2 -0
- package/dist/src/hooks/ui-part.test.d.ts.map +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/shared/debounce.d.ts +2 -0
- package/dist/src/shared/debounce.d.ts.map +1 -0
- package/dist/src/shared/debounce.test.d.ts +2 -0
- package/dist/src/shared/debounce.test.d.ts.map +1 -0
- package/dist/src/shared/hook.d.ts +71 -0
- package/dist/src/shared/hook.d.ts.map +1 -0
- package/dist/src/shared/hook.test.d.ts +2 -0
- package/dist/src/shared/hook.test.d.ts.map +1 -0
- package/dist/src/shared/index.d.ts +5 -0
- package/dist/src/shared/index.d.ts.map +1 -0
- package/dist/src/shared/map-object-values.d.ts +11 -0
- package/dist/src/shared/map-object-values.d.ts.map +1 -0
- package/dist/src/shared/map-object-values.test.d.ts +2 -0
- package/dist/src/shared/map-object-values.test.d.ts.map +1 -0
- package/dist/src/shared/parse-int-if-not-null.d.ts +2 -0
- package/dist/src/shared/parse-int-if-not-null.d.ts.map +1 -0
- package/dist/src/shared/parse-int-if-not-null.test.d.ts +2 -0
- package/dist/src/shared/parse-int-if-not-null.test.d.ts.map +1 -0
- package/dist/src/types/index.d.ts +2 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/required-by.type.d.ts +2 -0
- package/dist/src/types/required-by.type.d.ts.map +1 -0
- package/dist/test-utils/editor/create-editable-html-element.d.ts +12 -0
- package/dist/test-utils/editor/create-editable-html-element.d.ts.map +1 -0
- package/dist/test-utils/editor/create-editor-html-element.d.ts +13 -0
- package/dist/test-utils/editor/create-editor-html-element.d.ts.map +1 -0
- package/dist/test-utils/editor/create-editor-preset.d.ts +15 -0
- package/dist/test-utils/editor/create-editor-preset.d.ts.map +1 -0
- package/dist/test-utils/editor/create-ui-part-html-element.d.ts +6 -0
- package/dist/test-utils/editor/create-ui-part-html-element.d.ts.map +1 -0
- package/dist/test-utils/editor/get-test-editor-input.d.ts +5 -0
- package/dist/test-utils/editor/get-test-editor-input.d.ts.map +1 -0
- package/dist/test-utils/editor/index.d.ts +8 -0
- package/dist/test-utils/editor/index.d.ts.map +1 -0
- package/dist/test-utils/editor/is-editor-shown.d.ts +5 -0
- package/dist/test-utils/editor/is-editor-shown.d.ts.map +1 -0
- package/dist/test-utils/editor/wait-for-test-editor.d.ts +7 -0
- package/dist/test-utils/editor/wait-for-test-editor.d.ts.map +1 -0
- package/dist/test-utils/html.d.ts +28 -0
- package/dist/test-utils/html.d.ts.map +1 -0
- package/dist/test-utils/html.test.d.ts +2 -0
- package/dist/test-utils/html.test.d.ts.map +1 -0
- package/dist/test-utils/index.d.ts +3 -0
- package/dist/test-utils/index.d.ts.map +1 -0
- package/dist/vite.config.d.ts +3 -0
- package/dist/vite.config.d.ts.map +1 -0
- package/package.json +44 -0
- package/src/hooks/editable.test.ts +241 -0
- package/src/hooks/editable.ts +113 -0
- package/src/hooks/editor/editor.test.ts +333 -0
- package/src/hooks/editor/editor.ts +179 -0
- package/src/hooks/editor/editors-registry.test.ts +238 -0
- package/src/hooks/editor/editors-registry.ts +154 -0
- package/src/hooks/editor/index.ts +1 -0
- package/src/hooks/editor/typings.ts +72 -0
- package/src/hooks/editor/utils/index.ts +6 -0
- package/src/hooks/editor/utils/is-single-editing-like-editor.test.ts +40 -0
- package/src/hooks/editor/utils/is-single-editing-like-editor.ts +11 -0
- package/src/hooks/editor/utils/load-editor-constructor.test.ts +62 -0
- package/src/hooks/editor/utils/load-editor-constructor.ts +27 -0
- package/src/hooks/editor/utils/load-editor-plugins.test.ts +46 -0
- package/src/hooks/editor/utils/load-editor-plugins.ts +50 -0
- package/src/hooks/editor/utils/query-all-editor-editables.test.ts +140 -0
- package/src/hooks/editor/utils/query-all-editor-editables.ts +47 -0
- package/src/hooks/editor/utils/read-preset-or-throw.test.ts +162 -0
- package/src/hooks/editor/utils/read-preset-or-throw.ts +33 -0
- package/src/hooks/editor/utils/set-editor-editable-height.test.ts +131 -0
- package/src/hooks/editor/utils/set-editor-editable-height.ts +15 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/ui-part.test.ts +151 -0
- package/src/hooks/ui-part.ts +89 -0
- package/src/index.ts +2 -0
- package/src/shared/debounce.test.ts +72 -0
- package/src/shared/debounce.ts +16 -0
- package/src/shared/hook.test.ts +131 -0
- package/src/shared/hook.ts +141 -0
- package/src/shared/index.ts +4 -0
- package/src/shared/map-object-values.test.ts +29 -0
- package/src/shared/map-object-values.ts +19 -0
- package/src/shared/parse-int-if-not-null.test.ts +25 -0
- package/src/shared/parse-int-if-not-null.ts +9 -0
- package/src/types/index.ts +1 -0
- package/src/types/required-by.type.ts +1 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { ClassicEditor, DecoupledEditor, InlineEditor, MultiRootEditor } from 'ckeditor5';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createEditableHtmlElement,
|
|
6
|
+
createEditorHtmlElement,
|
|
7
|
+
createEditorPreset,
|
|
8
|
+
getTestEditorInput,
|
|
9
|
+
isEditorShown,
|
|
10
|
+
waitForTestEditor,
|
|
11
|
+
} from '~/test-utils';
|
|
12
|
+
|
|
13
|
+
import { EditorHook } from './editor';
|
|
14
|
+
import { EditorsRegistry } from './editors-registry';
|
|
15
|
+
|
|
16
|
+
describe('editor hook', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
document.body.innerHTML = '';
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
await EditorsRegistry.the.destroyAllEditors();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('mount', () => {
|
|
26
|
+
it('should save the editor instance in the registry with provided editorId', async () => {
|
|
27
|
+
const hookElement = createEditorHtmlElement({
|
|
28
|
+
id: 'magic-editor',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
document.body.appendChild(hookElement);
|
|
32
|
+
EditorHook.mounted.call({ el: hookElement });
|
|
33
|
+
|
|
34
|
+
expect(await EditorsRegistry.the.waitForEditor('magic-editor')).toBeInstanceOf(ClassicEditor);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('classic', () => {
|
|
38
|
+
it('should create an classic editor with default preset', async () => {
|
|
39
|
+
const hookElement = createEditorHtmlElement();
|
|
40
|
+
|
|
41
|
+
document.body.appendChild(hookElement);
|
|
42
|
+
EditorHook.mounted.call({ el: hookElement });
|
|
43
|
+
|
|
44
|
+
const editor = await waitForTestEditor();
|
|
45
|
+
|
|
46
|
+
expect(editor).to.toBeInstanceOf(ClassicEditor);
|
|
47
|
+
expect(isEditorShown()).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should assign default value to the editor using "cke-initial-value" attribute', async () => {
|
|
51
|
+
const initialValue = `<p>Hello World! Today is ${new Date().toLocaleDateString()}</p>`;
|
|
52
|
+
const hookElement = createEditorHtmlElement({
|
|
53
|
+
initialValue,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
document.body.appendChild(hookElement);
|
|
57
|
+
EditorHook.mounted.call({ el: hookElement });
|
|
58
|
+
|
|
59
|
+
const editor = await waitForTestEditor();
|
|
60
|
+
|
|
61
|
+
expect(editor.getData()).toBe(initialValue);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should create an editor even if `cke-initial-value` is not set', async () => {
|
|
65
|
+
const hookElement = createEditorHtmlElement({
|
|
66
|
+
initialValue: null,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
document.body.appendChild(hookElement);
|
|
70
|
+
EditorHook.mounted.call({ el: hookElement });
|
|
71
|
+
|
|
72
|
+
const editor = await waitForTestEditor();
|
|
73
|
+
|
|
74
|
+
expect(editor).toBeInstanceOf(ClassicEditor);
|
|
75
|
+
expect(editor.getData()).toBe('');
|
|
76
|
+
|
|
77
|
+
expect(isEditorShown()).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('decoupled', () => {
|
|
82
|
+
it('should be possible to create decoupled editor with editable', async () => {
|
|
83
|
+
const hookElement = createEditorHtmlElement({
|
|
84
|
+
preset: createEditorPreset('decoupled'),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
document.body.appendChild(hookElement);
|
|
88
|
+
document.body.appendChild(createEditableHtmlElement());
|
|
89
|
+
|
|
90
|
+
EditorHook.mounted.call({ el: hookElement });
|
|
91
|
+
|
|
92
|
+
const editor = await waitForTestEditor();
|
|
93
|
+
|
|
94
|
+
expect(editor).toBeInstanceOf(DecoupledEditor);
|
|
95
|
+
expect(editor.getData()).toBe('<p>Test content</p>');
|
|
96
|
+
|
|
97
|
+
expect(isEditorShown()).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('if the initial value is specified on the editable, it should ignore initial value set on the editor', async () => {
|
|
101
|
+
const hookElement = createEditorHtmlElement({
|
|
102
|
+
preset: createEditorPreset('decoupled'),
|
|
103
|
+
initialValue: '<p>Ignored content</p>',
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
document.body.appendChild(hookElement);
|
|
107
|
+
document.body.appendChild(createEditableHtmlElement({
|
|
108
|
+
initialValue: '<p>Editable value</p>',
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
EditorHook.mounted.call({ el: hookElement });
|
|
112
|
+
|
|
113
|
+
const editor = await waitForTestEditor();
|
|
114
|
+
|
|
115
|
+
expect(editor).toBeInstanceOf(DecoupledEditor);
|
|
116
|
+
expect(editor.getData()).toBe('<p>Editable value</p>');
|
|
117
|
+
|
|
118
|
+
expect(isEditorShown()).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should not match editables form the other editors', async () => {
|
|
122
|
+
const hookElement = createEditorHtmlElement({
|
|
123
|
+
preset: createEditorPreset('decoupled'),
|
|
124
|
+
initialValue: null,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const editables = {
|
|
128
|
+
current: createEditableHtmlElement({
|
|
129
|
+
id: 'test-editor',
|
|
130
|
+
initialValue: '<p>XD</p>',
|
|
131
|
+
}),
|
|
132
|
+
other: createEditableHtmlElement({
|
|
133
|
+
id: 'other-editor',
|
|
134
|
+
name: 'main',
|
|
135
|
+
initialValue: '<p>Other content</p>',
|
|
136
|
+
editorId: 'other-editor-1',
|
|
137
|
+
}),
|
|
138
|
+
other1: createEditableHtmlElement({
|
|
139
|
+
id: 'other-editor-1',
|
|
140
|
+
name: 'main',
|
|
141
|
+
initialValue: '<p>Other content 1</p>',
|
|
142
|
+
editorId: 'other-editor-1',
|
|
143
|
+
}),
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
document.body.appendChild(hookElement);
|
|
147
|
+
document.body.appendChild(editables.other1);
|
|
148
|
+
document.body.appendChild(editables.current);
|
|
149
|
+
document.body.appendChild(editables.other);
|
|
150
|
+
|
|
151
|
+
EditorHook.mounted.call({ el: hookElement });
|
|
152
|
+
|
|
153
|
+
const editor = await waitForTestEditor();
|
|
154
|
+
|
|
155
|
+
expect(editor).toBeInstanceOf(DecoupledEditor);
|
|
156
|
+
expect(editor.getData()).toBe('<p>XD</p>');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('inline', () => {
|
|
161
|
+
it('should create an inline editor with default preset', async () => {
|
|
162
|
+
const hookElement = createEditorHtmlElement({
|
|
163
|
+
preset: createEditorPreset('inline'),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
document.body.appendChild(hookElement);
|
|
167
|
+
EditorHook.mounted.call({ el: hookElement });
|
|
168
|
+
|
|
169
|
+
const editor = await waitForTestEditor();
|
|
170
|
+
|
|
171
|
+
expect(editor).toBeInstanceOf(InlineEditor);
|
|
172
|
+
expect(isEditorShown()).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('multiroot', () => {
|
|
177
|
+
it('should create a multiroot editor without editables', async () => {
|
|
178
|
+
const hookElement = createEditorHtmlElement({
|
|
179
|
+
preset: createEditorPreset('multiroot'),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
document.body.appendChild(hookElement);
|
|
183
|
+
EditorHook.mounted.call({ el: hookElement });
|
|
184
|
+
|
|
185
|
+
const editor = await waitForTestEditor();
|
|
186
|
+
|
|
187
|
+
expect(editor).toBeInstanceOf(MultiRootEditor);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should create a multiroot editor with editables', async () => {
|
|
191
|
+
const hookElement = createEditorHtmlElement({
|
|
192
|
+
preset: createEditorPreset('multiroot'),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
document.body.appendChild(hookElement);
|
|
196
|
+
document.body.appendChild(createEditableHtmlElement({
|
|
197
|
+
name: 'main',
|
|
198
|
+
initialValue: '<p>Main root</p>',
|
|
199
|
+
}));
|
|
200
|
+
|
|
201
|
+
document.body.appendChild(createEditableHtmlElement({
|
|
202
|
+
name: 'second',
|
|
203
|
+
initialValue: '<p>Second root</p>',
|
|
204
|
+
}));
|
|
205
|
+
|
|
206
|
+
EditorHook.mounted.call({ el: hookElement });
|
|
207
|
+
|
|
208
|
+
const editor = await waitForTestEditor();
|
|
209
|
+
|
|
210
|
+
expect(editor).toBeInstanceOf(MultiRootEditor);
|
|
211
|
+
expect(editor.getData({ rootName: 'main' })).toBe('<p>Main root</p>');
|
|
212
|
+
expect(editor.getData({ rootName: 'second' })).toBe('<p>Second root</p>');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('destroy', () => {
|
|
218
|
+
it('should destroy editor on hook destruction', async () => {
|
|
219
|
+
const hookElement = createEditorHtmlElement();
|
|
220
|
+
|
|
221
|
+
document.body.appendChild(hookElement);
|
|
222
|
+
EditorHook.mounted.call({ el: hookElement });
|
|
223
|
+
|
|
224
|
+
const editor = await waitForTestEditor();
|
|
225
|
+
|
|
226
|
+
EditorHook.destroyed.call({ el: hookElement });
|
|
227
|
+
|
|
228
|
+
expect(EditorsRegistry.the.getEditors()).toContain(editor);
|
|
229
|
+
|
|
230
|
+
await new Promise((resolve) => {
|
|
231
|
+
editor.once('destroy', resolve);
|
|
232
|
+
editor.destroy();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(EditorsRegistry.the.getEditors()).not.toContain(editor);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should mark the element as hidden during destruction', async () => {
|
|
239
|
+
const hookElement = createEditorHtmlElement();
|
|
240
|
+
|
|
241
|
+
document.body.appendChild(hookElement);
|
|
242
|
+
EditorHook.mounted.call({ el: hookElement });
|
|
243
|
+
|
|
244
|
+
await waitForTestEditor();
|
|
245
|
+
|
|
246
|
+
expect(hookElement.style.display).toBe('');
|
|
247
|
+
|
|
248
|
+
EditorHook.destroyed.call({ el: hookElement });
|
|
249
|
+
|
|
250
|
+
expect(hookElement.style.display).toBe('none');
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('value synchronization', () => {
|
|
255
|
+
beforeEach(() => {
|
|
256
|
+
vi.useFakeTimers();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
afterEach(() => {
|
|
260
|
+
vi.useRealTimers();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should not crash if input is not present', async () => {
|
|
264
|
+
const initialValue = `<p>Test content</p>`;
|
|
265
|
+
const hookElement = createEditorHtmlElement({
|
|
266
|
+
initialValue,
|
|
267
|
+
withInput: false,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
document.body.appendChild(hookElement);
|
|
271
|
+
EditorHook.mounted.call({ el: hookElement });
|
|
272
|
+
|
|
273
|
+
await waitForTestEditor();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should initialize the input with the initial value', async () => {
|
|
277
|
+
const initialValue = `<p>Test content</p>`;
|
|
278
|
+
const hookElement = createEditorHtmlElement({
|
|
279
|
+
initialValue,
|
|
280
|
+
withInput: true,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
document.body.appendChild(hookElement);
|
|
284
|
+
EditorHook.mounted.call({ el: hookElement });
|
|
285
|
+
|
|
286
|
+
await waitForTestEditor();
|
|
287
|
+
|
|
288
|
+
const input = getTestEditorInput();
|
|
289
|
+
|
|
290
|
+
expect(input.value).toBe(initialValue);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should sync editor data with a hidden input', async () => {
|
|
294
|
+
const initialValue = `<p>Initial content</p>`;
|
|
295
|
+
const hookElement = createEditorHtmlElement({
|
|
296
|
+
initialValue,
|
|
297
|
+
withInput: true,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
document.body.appendChild(hookElement);
|
|
301
|
+
EditorHook.mounted.call({ el: hookElement });
|
|
302
|
+
|
|
303
|
+
const editor = await waitForTestEditor();
|
|
304
|
+
const input = getTestEditorInput();
|
|
305
|
+
|
|
306
|
+
editor.setData('<p>Updated content</p>');
|
|
307
|
+
|
|
308
|
+
// Test if sync is debounced
|
|
309
|
+
expect(input.value).toBe(initialValue);
|
|
310
|
+
|
|
311
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
312
|
+
|
|
313
|
+
expect(input.value).toBe('<p>Updated content</p>');
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe('`cke-editable-height` attribute', () => {
|
|
318
|
+
it('should set the height of the editable area', async () => {
|
|
319
|
+
const editableHeight = 255;
|
|
320
|
+
const hookElement = createEditorHtmlElement({
|
|
321
|
+
editableHeight,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
document.body.appendChild(hookElement);
|
|
325
|
+
EditorHook.mounted.call({ el: hookElement });
|
|
326
|
+
|
|
327
|
+
const editor = await waitForTestEditor();
|
|
328
|
+
const editable = editor.editing.view.document.getRoot('main');
|
|
329
|
+
|
|
330
|
+
expect(editable?.getStyle('height')).toBe(`${editableHeight}px`);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { Editor } from 'ckeditor5';
|
|
2
|
+
|
|
3
|
+
import type { EditorId, EditorType } from './typings';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
debounce,
|
|
7
|
+
mapObjectValues,
|
|
8
|
+
parseIntIfNotNull,
|
|
9
|
+
} from '../../shared';
|
|
10
|
+
import { ClassHook, makeHook } from '../../shared/hook';
|
|
11
|
+
import { EditorsRegistry } from './editors-registry';
|
|
12
|
+
import {
|
|
13
|
+
isSingleEditingLikeEditor,
|
|
14
|
+
loadEditorConstructor,
|
|
15
|
+
loadEditorPlugins,
|
|
16
|
+
queryAllEditorEditables,
|
|
17
|
+
readPresetOrThrow,
|
|
18
|
+
setEditorEditableHeight,
|
|
19
|
+
} from './utils';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Editor hook for Phoenix LiveView.
|
|
23
|
+
*
|
|
24
|
+
* This class is a hook that can be used with Phoenix LiveView to integrate
|
|
25
|
+
* the CKEditor 5 WYSIWYG editor.
|
|
26
|
+
*/
|
|
27
|
+
class EditorHookImpl extends ClassHook {
|
|
28
|
+
/**
|
|
29
|
+
* The name of the hook.
|
|
30
|
+
*/
|
|
31
|
+
private editorPromise: Promise<Editor> | null = null;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Attributes for the editor instance.
|
|
35
|
+
*/
|
|
36
|
+
private get attrs() {
|
|
37
|
+
const value = {
|
|
38
|
+
editorId: this.el.getAttribute('id')!,
|
|
39
|
+
preset: readPresetOrThrow(this.el),
|
|
40
|
+
editableHeight: parseIntIfNotNull(this.el.getAttribute('cke-editable-height')),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
Object.defineProperty(this, 'attrs', {
|
|
44
|
+
value,
|
|
45
|
+
writable: false,
|
|
46
|
+
configurable: false,
|
|
47
|
+
enumerable: true,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Mounts the editor component.
|
|
55
|
+
*/
|
|
56
|
+
override async mounted() {
|
|
57
|
+
this.editorPromise = this.createEditor();
|
|
58
|
+
|
|
59
|
+
EditorsRegistry.the.register(this.attrs.editorId, await this.editorPromise);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Destroys the editor instance when the component is destroyed.
|
|
64
|
+
* This is important to prevent memory leaks and ensure that the editor is properly cleaned up.
|
|
65
|
+
*/
|
|
66
|
+
override async destroyed() {
|
|
67
|
+
// Let's hide the element during destruction to prevent flickering.
|
|
68
|
+
this.el.style.display = 'none';
|
|
69
|
+
|
|
70
|
+
// Let's wait for the mounted promise to resolve before proceeding with destruction.
|
|
71
|
+
(await this.editorPromise)?.destroy();
|
|
72
|
+
this.editorPromise = null;
|
|
73
|
+
|
|
74
|
+
EditorsRegistry.the.unregister(this.attrs.editorId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Creates the CKEditor instance.
|
|
79
|
+
*/
|
|
80
|
+
private createEditor = async () => {
|
|
81
|
+
const { preset, editorId, editableHeight } = this.attrs;
|
|
82
|
+
const { type, license, config: { plugins, ...config } } = preset;
|
|
83
|
+
|
|
84
|
+
const Constructor = await loadEditorConstructor(type);
|
|
85
|
+
const rootEditables = getInitialRootsContentElements(editorId, type);
|
|
86
|
+
|
|
87
|
+
const editor = await Constructor.create(
|
|
88
|
+
rootEditables as any,
|
|
89
|
+
{
|
|
90
|
+
...config,
|
|
91
|
+
initialData: getInitialRootsValues(editorId, type),
|
|
92
|
+
licenseKey: license.key,
|
|
93
|
+
plugins: await loadEditorPlugins(plugins),
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (isSingleEditingLikeEditor(type)) {
|
|
98
|
+
const input = document.getElementById(`${editorId}_input`) as HTMLInputElement | null;
|
|
99
|
+
|
|
100
|
+
if (input) {
|
|
101
|
+
syncEditorToInput(input, editor);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (editableHeight) {
|
|
105
|
+
setEditorEditableHeight(editor, editableHeight);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return editor;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Synchronizes the editor's content with a hidden input field.
|
|
115
|
+
*
|
|
116
|
+
* @param input The input element to synchronize with the editor.
|
|
117
|
+
* @param editor The CKEditor instance.
|
|
118
|
+
*/
|
|
119
|
+
function syncEditorToInput(input: HTMLInputElement, editor: Editor) {
|
|
120
|
+
const sync = () => {
|
|
121
|
+
input.value = editor.getData();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
editor.model.document.on('change:data', debounce(250, sync));
|
|
125
|
+
sync();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Gets the initial root elements for the editor based on its type.
|
|
130
|
+
*
|
|
131
|
+
* @param editorId The editor's ID.
|
|
132
|
+
* @param type The type of the editor.
|
|
133
|
+
* @returns The root element(s) for the editor.
|
|
134
|
+
*/
|
|
135
|
+
function getInitialRootsContentElements(editorId: EditorId, type: EditorType) {
|
|
136
|
+
if (isSingleEditingLikeEditor(type)) {
|
|
137
|
+
return document.getElementById(`${editorId}_editor`)!;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const editables = queryAllEditorEditables(editorId);
|
|
141
|
+
|
|
142
|
+
return mapObjectValues(editables, ({ content }) => content);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Gets the initial data for the roots of the editor. If the editor is a single editing-like editor,
|
|
147
|
+
* it retrieves the initial value from the element's attribute. Otherwise, it returns an object mapping
|
|
148
|
+
* editable names to their initial values.
|
|
149
|
+
*
|
|
150
|
+
* @param editorId The editor's ID.
|
|
151
|
+
* @param type The type of the editor.
|
|
152
|
+
* @returns The initial values for the editor's roots.
|
|
153
|
+
*/
|
|
154
|
+
function getInitialRootsValues(editorId: EditorId, type: EditorType) {
|
|
155
|
+
// If the editor is decoupled, the initial value might be specified in the `main` editable.
|
|
156
|
+
if (type === 'decoupled') {
|
|
157
|
+
const mainEditableValue = queryAllEditorEditables(editorId)['main']?.initialValue;
|
|
158
|
+
|
|
159
|
+
if (mainEditableValue) {
|
|
160
|
+
return mainEditableValue;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Let's check initial value assigned to the editor element.
|
|
165
|
+
if (isSingleEditingLikeEditor(type)) {
|
|
166
|
+
const initialValue = document.getElementById(editorId)?.getAttribute('cke-initial-value') || '';
|
|
167
|
+
|
|
168
|
+
return initialValue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const editables = queryAllEditorEditables(editorId);
|
|
172
|
+
|
|
173
|
+
return mapObjectValues(editables, ({ initialValue }) => initialValue);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Phoenix LiveView hook for CKEditor 5.
|
|
178
|
+
*/
|
|
179
|
+
export const EditorHook = makeHook(EditorHookImpl);
|