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.
- package/dist/ckeditor5-symfony-error.d.ts +7 -0
- package/dist/ckeditor5-symfony-error.d.ts.map +1 -0
- package/dist/elements/context/context.d.ts +18 -0
- package/dist/elements/context/context.d.ts.map +1 -0
- package/dist/elements/context/contexts-registry.d.ts +9 -0
- package/dist/elements/context/contexts-registry.d.ts.map +1 -0
- package/dist/elements/context/index.d.ts +4 -0
- package/dist/elements/context/index.d.ts.map +1 -0
- package/dist/elements/context/typings.d.ts +34 -0
- package/dist/elements/context/typings.d.ts.map +1 -0
- package/dist/elements/editable.d.ts +18 -0
- package/dist/elements/editable.d.ts.map +1 -0
- package/dist/elements/editor/custom-editor-plugins.d.ts +54 -0
- package/dist/elements/editor/custom-editor-plugins.d.ts.map +1 -0
- package/dist/elements/editor/editor.d.ts +23 -0
- package/dist/elements/editor/editor.d.ts.map +1 -0
- package/dist/elements/editor/editors-registry.d.ts +9 -0
- package/dist/elements/editor/editors-registry.d.ts.map +1 -0
- package/dist/elements/editor/index.d.ts +3 -0
- package/dist/elements/editor/index.d.ts.map +1 -0
- package/dist/elements/editor/plugins/index.d.ts +2 -0
- package/dist/elements/editor/plugins/index.d.ts.map +1 -0
- package/dist/elements/editor/plugins/sync-editor-with-input.d.ts +6 -0
- package/dist/elements/editor/plugins/sync-editor-with-input.d.ts.map +1 -0
- package/dist/elements/editor/typings.d.ts +99 -0
- package/dist/elements/editor/typings.d.ts.map +1 -0
- package/dist/elements/editor/utils/create-editor-in-context.d.ts +44 -0
- package/dist/elements/editor/utils/create-editor-in-context.d.ts.map +1 -0
- package/dist/elements/editor/utils/index.d.ts +12 -0
- package/dist/elements/editor/utils/index.d.ts.map +1 -0
- package/dist/elements/editor/utils/is-single-root-editor.d.ts +9 -0
- package/dist/elements/editor/utils/is-single-root-editor.d.ts.map +1 -0
- package/dist/elements/editor/utils/load-editor-constructor.d.ts +9 -0
- package/dist/elements/editor/utils/load-editor-constructor.d.ts.map +1 -0
- package/dist/elements/editor/utils/load-editor-plugins.d.ts +20 -0
- package/dist/elements/editor/utils/load-editor-plugins.d.ts.map +1 -0
- package/dist/elements/editor/utils/load-editor-translations.d.ts +14 -0
- package/dist/elements/editor/utils/load-editor-translations.d.ts.map +1 -0
- package/dist/elements/editor/utils/normalize-custom-translations.d.ts +11 -0
- package/dist/elements/editor/utils/normalize-custom-translations.d.ts.map +1 -0
- package/dist/elements/editor/utils/query-all-editor-ids.d.ts +5 -0
- package/dist/elements/editor/utils/query-all-editor-ids.d.ts.map +1 -0
- package/dist/elements/editor/utils/query-editor-editables.d.ts +25 -0
- package/dist/elements/editor/utils/query-editor-editables.d.ts.map +1 -0
- package/dist/elements/editor/utils/resolve-editor-config-elements-references.d.ts +9 -0
- package/dist/elements/editor/utils/resolve-editor-config-elements-references.d.ts.map +1 -0
- package/dist/elements/editor/utils/set-editor-editable-height.d.ts +9 -0
- package/dist/elements/editor/utils/set-editor-editable-height.d.ts.map +1 -0
- package/dist/elements/editor/utils/wrap-with-watchdog.d.ts +24 -0
- package/dist/elements/editor/utils/wrap-with-watchdog.d.ts.map +1 -0
- package/dist/elements/index.d.ts +6 -0
- package/dist/elements/index.d.ts.map +1 -0
- package/dist/elements/register-custom-elements.d.ts +5 -0
- package/dist/elements/register-custom-elements.d.ts.map +1 -0
- package/dist/elements/ui-part.d.ts +18 -0
- package/dist/elements/ui-part.d.ts.map +1 -0
- package/dist/index.cjs +5 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +1089 -0
- package/dist/index.mjs.map +1 -0
- package/dist/shared/async-registry.d.ts +136 -0
- package/dist/shared/async-registry.d.ts.map +1 -0
- package/dist/shared/camel-case.d.ts +8 -0
- package/dist/shared/camel-case.d.ts.map +1 -0
- package/dist/shared/debounce.d.ts +2 -0
- package/dist/shared/debounce.d.ts.map +1 -0
- package/dist/shared/deep-camel-case-keys.d.ts +8 -0
- package/dist/shared/deep-camel-case-keys.d.ts.map +1 -0
- package/dist/shared/filter-object-values.d.ts +9 -0
- package/dist/shared/filter-object-values.d.ts.map +1 -0
- package/dist/shared/index.d.ts +15 -0
- package/dist/shared/index.d.ts.map +1 -0
- package/dist/shared/is-empty-object.d.ts +2 -0
- package/dist/shared/is-empty-object.d.ts.map +1 -0
- package/dist/shared/is-plain-object.d.ts +8 -0
- package/dist/shared/is-plain-object.d.ts.map +1 -0
- package/dist/shared/map-object-values.d.ts +11 -0
- package/dist/shared/map-object-values.d.ts.map +1 -0
- package/dist/shared/once.d.ts +2 -0
- package/dist/shared/once.d.ts.map +1 -0
- package/dist/shared/shallow-equal.d.ts +9 -0
- package/dist/shared/shallow-equal.d.ts.map +1 -0
- package/dist/shared/timeout.d.ts +8 -0
- package/dist/shared/timeout.d.ts.map +1 -0
- package/dist/shared/uid.d.ts +7 -0
- package/dist/shared/uid.d.ts.map +1 -0
- package/dist/shared/wait-for-dom-ready.d.ts +5 -0
- package/dist/shared/wait-for-dom-ready.d.ts.map +1 -0
- package/dist/shared/wait-for.d.ts +20 -0
- package/dist/shared/wait-for.d.ts.map +1 -0
- package/dist/types/can-be-promise.type.d.ts +2 -0
- package/dist/types/can-be-promise.type.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/required-by.type.d.ts +2 -0
- package/dist/types/required-by.type.d.ts.map +1 -0
- package/package.json +40 -0
- package/src/ckeditor5-symfony-error.ts +9 -0
- package/src/elements/context/context.test.ts +291 -0
- package/src/elements/context/context.ts +99 -0
- package/src/elements/context/contexts-registry.test.ts +10 -0
- package/src/elements/context/contexts-registry.ts +10 -0
- package/src/elements/context/index.ts +3 -0
- package/src/elements/context/typings.ts +39 -0
- package/src/elements/editable.test.ts +334 -0
- package/src/elements/editable.ts +114 -0
- package/src/elements/editor/custom-editor-plugins.test.ts +103 -0
- package/src/elements/editor/custom-editor-plugins.ts +86 -0
- package/src/elements/editor/editor.test.ts +438 -0
- package/src/elements/editor/editor.ts +279 -0
- package/src/elements/editor/editors-registry.test.ts +10 -0
- package/src/elements/editor/editors-registry.ts +10 -0
- package/src/elements/editor/index.ts +2 -0
- package/src/elements/editor/plugins/index.ts +1 -0
- package/src/elements/editor/plugins/sync-editor-with-input.ts +78 -0
- package/src/elements/editor/typings.ts +114 -0
- package/src/elements/editor/utils/create-editor-in-context.ts +90 -0
- package/src/elements/editor/utils/index.ts +11 -0
- package/src/elements/editor/utils/is-single-root-editor.test.ts +40 -0
- package/src/elements/editor/utils/is-single-root-editor.ts +11 -0
- package/src/elements/editor/utils/load-editor-constructor.test.ts +62 -0
- package/src/elements/editor/utils/load-editor-constructor.ts +29 -0
- package/src/elements/editor/utils/load-editor-plugins.test.ts +100 -0
- package/src/elements/editor/utils/load-editor-plugins.ts +73 -0
- package/src/elements/editor/utils/load-editor-translations.ts +233 -0
- package/src/elements/editor/utils/normalize-custom-translations.test.ts +152 -0
- package/src/elements/editor/utils/normalize-custom-translations.ts +18 -0
- package/src/elements/editor/utils/query-all-editor-ids.ts +9 -0
- package/src/elements/editor/utils/query-editor-editables.ts +101 -0
- package/src/elements/editor/utils/resolve-editor-config-elements-references.test.ts +93 -0
- package/src/elements/editor/utils/resolve-editor-config-elements-references.ts +36 -0
- package/src/elements/editor/utils/set-editor-editable-height.test.ts +131 -0
- package/src/elements/editor/utils/set-editor-editable-height.ts +15 -0
- package/src/elements/editor/utils/wrap-with-watchdog.test.ts +45 -0
- package/src/elements/editor/utils/wrap-with-watchdog.ts +51 -0
- package/src/elements/index.ts +14 -0
- package/src/elements/register-custom-elements.ts +24 -0
- package/src/elements/ui-part.test.ts +142 -0
- package/src/elements/ui-part.ts +80 -0
- package/src/index.ts +6 -0
- package/src/shared/async-registry.test.ts +737 -0
- package/src/shared/async-registry.ts +353 -0
- package/src/shared/camel-case.test.ts +35 -0
- package/src/shared/camel-case.ts +11 -0
- package/src/shared/debounce.test.ts +72 -0
- package/src/shared/debounce.ts +16 -0
- package/src/shared/deep-camel-case-keys.test.ts +34 -0
- package/src/shared/deep-camel-case-keys.ts +26 -0
- package/src/shared/filter-object-values.test.ts +25 -0
- package/src/shared/filter-object-values.ts +17 -0
- package/src/shared/index.ts +14 -0
- package/src/shared/is-empty-object.test.ts +78 -0
- package/src/shared/is-empty-object.ts +3 -0
- package/src/shared/is-plain-object.test.ts +38 -0
- package/src/shared/is-plain-object.ts +15 -0
- package/src/shared/map-object-values.test.ts +29 -0
- package/src/shared/map-object-values.ts +19 -0
- package/src/shared/once.test.ts +116 -0
- package/src/shared/once.ts +12 -0
- package/src/shared/shallow-equal.test.ts +51 -0
- package/src/shared/shallow-equal.ts +30 -0
- package/src/shared/timeout.test.ts +65 -0
- package/src/shared/timeout.ts +13 -0
- package/src/shared/uid.test.ts +25 -0
- package/src/shared/uid.ts +8 -0
- package/src/shared/wait-for-dom-ready.test.ts +87 -0
- package/src/shared/wait-for-dom-ready.ts +21 -0
- package/src/shared/wait-for.test.ts +24 -0
- package/src/shared/wait-for.ts +56 -0
- package/src/types/can-be-promise.type.ts +1 -0
- package/src/types/index.ts +2 -0
- package/src/types/required-by.type.ts +1 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import type { MultiRootEditor } from 'ckeditor5';
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createEditorPreset,
|
|
7
|
+
renderTestEditable,
|
|
8
|
+
renderTestEditor,
|
|
9
|
+
waitForTestEditor,
|
|
10
|
+
} from '~/test-utils';
|
|
11
|
+
|
|
12
|
+
import { EditorsRegistry } from './editor/editors-registry';
|
|
13
|
+
import { registerCustomElements } from './register-custom-elements';
|
|
14
|
+
|
|
15
|
+
describe('editable component', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
document.body.innerHTML = '';
|
|
18
|
+
registerCustomElements();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
vi.useRealTimers();
|
|
23
|
+
vi.resetAllMocks();
|
|
24
|
+
|
|
25
|
+
document.body.innerHTML = '';
|
|
26
|
+
EditorsRegistry.the.reset();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('mounting editable', () => {
|
|
30
|
+
it('should add editable root to the editor after mounting editor (empty editor)', async () => {
|
|
31
|
+
renderTestEditor({
|
|
32
|
+
preset: createEditorPreset('multiroot'),
|
|
33
|
+
content: {},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const editor = await waitForTestEditor<MultiRootEditor>();
|
|
37
|
+
|
|
38
|
+
renderTestEditable({
|
|
39
|
+
rootName: 'foo',
|
|
40
|
+
content: '<p>Initial foo component</p>',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await vi.waitFor(() => {
|
|
44
|
+
expect(editor.getData({ rootName: 'foo' })).toBe('<p>Initial foo component</p>');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should add editable root to the editor after mounting editor (non-empty editor, other editable defined before)', async () => {
|
|
49
|
+
renderTestEditable({
|
|
50
|
+
rootName: 'bar',
|
|
51
|
+
content: '<p>Initial bar content</p>',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
renderTestEditor({
|
|
55
|
+
preset: createEditorPreset('multiroot'),
|
|
56
|
+
content: {
|
|
57
|
+
bar: '<p>Initial bar content</p>',
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const editor = await waitForTestEditor<MultiRootEditor>();
|
|
62
|
+
|
|
63
|
+
renderTestEditable({
|
|
64
|
+
rootName: 'foo',
|
|
65
|
+
content: '<p>Initial foo content</p>',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await vi.waitFor(() => {
|
|
69
|
+
expect(editor.getData({ rootName: 'foo' })).toBe('<p>Initial foo content</p>');
|
|
70
|
+
expect(editor.getData({ rootName: 'bar' })).toBe('<p>Initial bar content</p>');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should add editable root to the editor after mounting editor (non-empty editor, other editable defined after)', async () => {
|
|
75
|
+
renderTestEditor({
|
|
76
|
+
preset: createEditorPreset('multiroot'),
|
|
77
|
+
content: {
|
|
78
|
+
bar: '<p>Initial bar content</p>',
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
renderTestEditable({
|
|
83
|
+
rootName: 'bar',
|
|
84
|
+
content: '<p>Initial bar content</p>',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const editor = await waitForTestEditor<MultiRootEditor>();
|
|
88
|
+
|
|
89
|
+
renderTestEditable({
|
|
90
|
+
rootName: 'foo',
|
|
91
|
+
content: '<p>Initial foo content</p>',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await vi.waitFor(() => {
|
|
95
|
+
expect(editor.getData({ rootName: 'foo' })).toBe('<p>Initial foo content</p>');
|
|
96
|
+
expect(editor.getData({ rootName: 'bar' })).toBe('<p>Initial bar content</p>');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should do nothing if adding existing root (without provided content)', async () => {
|
|
101
|
+
renderTestEditor({
|
|
102
|
+
preset: createEditorPreset('multiroot'),
|
|
103
|
+
content: {
|
|
104
|
+
main: '<p>Initial main content</p>',
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
renderTestEditable({
|
|
109
|
+
rootName: 'main',
|
|
110
|
+
content: null,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const editor = await waitForTestEditor<MultiRootEditor>();
|
|
114
|
+
|
|
115
|
+
await vi.waitFor(() => {
|
|
116
|
+
expect(editor.getData({ rootName: 'main' })).toBe('<p>Initial main content</p>');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should update existing root content if added existing root with provided content', async () => {
|
|
121
|
+
renderTestEditor({
|
|
122
|
+
preset: createEditorPreset('multiroot'),
|
|
123
|
+
content: {
|
|
124
|
+
main: '<p>Initial main content</p>',
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
renderTestEditable({
|
|
129
|
+
rootName: 'main',
|
|
130
|
+
content: '<p>Updated main content</p>',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const editor = await waitForTestEditor<MultiRootEditor>();
|
|
134
|
+
|
|
135
|
+
await vi.waitFor(() => {
|
|
136
|
+
expect(editor.getData({ rootName: 'main' })).toBe('<p>Updated main content</p>');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should auto-assign editor ID if not provided', async () => {
|
|
141
|
+
renderTestEditor({
|
|
142
|
+
preset: createEditorPreset('multiroot'),
|
|
143
|
+
content: {},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await waitForTestEditor<MultiRootEditor>();
|
|
147
|
+
|
|
148
|
+
const editable = renderTestEditable({
|
|
149
|
+
editorId: '',
|
|
150
|
+
rootName: 'foo',
|
|
151
|
+
content: '<p>Initial foo component</p>',
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await vi.waitFor(() => {
|
|
155
|
+
expect(editable.getAttribute('data-cke-editor-id')).toBe('test-editor');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('input value synchronization', () => {
|
|
161
|
+
let editor: MultiRootEditor;
|
|
162
|
+
|
|
163
|
+
beforeEach(async () => {
|
|
164
|
+
renderTestEditor({
|
|
165
|
+
preset: createEditorPreset('multiroot'),
|
|
166
|
+
content: {},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
editor = await waitForTestEditor<MultiRootEditor>();
|
|
170
|
+
vi.useFakeTimers();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should not crash if input is not present', async () => {
|
|
174
|
+
renderTestEditable({
|
|
175
|
+
rootName: 'foo',
|
|
176
|
+
content: '<p>Initial foo component</p>',
|
|
177
|
+
}, { withInput: false });
|
|
178
|
+
|
|
179
|
+
await vi.waitFor(() => {
|
|
180
|
+
expect(editor.getData({ rootName: 'foo' })).toBe('<p>Initial foo component</p>');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should synchronize input value after mounting editable', async () => {
|
|
185
|
+
const element = renderTestEditable({
|
|
186
|
+
rootName: 'foo',
|
|
187
|
+
content: '<p>Initial foo component</p>',
|
|
188
|
+
}, { withInput: true });
|
|
189
|
+
|
|
190
|
+
const input = element.querySelector('input')!;
|
|
191
|
+
|
|
192
|
+
await vi.waitFor(() => {
|
|
193
|
+
expect(input.value).toBe('<p>Initial foo component</p>');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should debounce input value synchronization', async () => {
|
|
198
|
+
const element = renderTestEditable({
|
|
199
|
+
rootName: 'foo',
|
|
200
|
+
content: '<p>Initial foo component</p>',
|
|
201
|
+
saveDebounceMs: 500,
|
|
202
|
+
}, { withInput: true });
|
|
203
|
+
|
|
204
|
+
const input = element.querySelector('input')!;
|
|
205
|
+
|
|
206
|
+
await vi.waitFor(() => {
|
|
207
|
+
expect(input.value).toBe('<p>Initial foo component</p>');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
editor.setData({
|
|
211
|
+
foo: '<p>Modified foo content</p>',
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
vi.advanceTimersByTime(300);
|
|
215
|
+
expect(input.value).toBe('<p>Initial foo component</p>');
|
|
216
|
+
|
|
217
|
+
vi.advanceTimersByTime(300);
|
|
218
|
+
expect(input.value).toBe('<p>Modified foo content</p>');
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('web component events', () => {
|
|
223
|
+
let editor: MultiRootEditor;
|
|
224
|
+
|
|
225
|
+
beforeEach(async () => {
|
|
226
|
+
vi.useFakeTimers();
|
|
227
|
+
renderTestEditor({
|
|
228
|
+
preset: createEditorPreset('multiroot'),
|
|
229
|
+
content: {},
|
|
230
|
+
});
|
|
231
|
+
editor = await waitForTestEditor<MultiRootEditor>();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should emit change event when editor data changes', async () => {
|
|
235
|
+
const element = renderTestEditable({
|
|
236
|
+
rootName: 'foo',
|
|
237
|
+
content: '<p>Initial content</p>',
|
|
238
|
+
saveDebounceMs: 100,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const changeSpy = vi.fn();
|
|
242
|
+
element.addEventListener('change', changeSpy);
|
|
243
|
+
|
|
244
|
+
await vi.waitFor(() => {
|
|
245
|
+
expect(editor.getData({ rootName: 'foo' })).toBe('<p>Initial content</p>');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Clear spy after initial sync
|
|
249
|
+
changeSpy.mockClear();
|
|
250
|
+
|
|
251
|
+
editor.setData({ foo: '<p>New content</p>' });
|
|
252
|
+
|
|
253
|
+
vi.advanceTimersByTime(150);
|
|
254
|
+
|
|
255
|
+
expect(changeSpy).toHaveBeenCalledTimes(1);
|
|
256
|
+
expect((changeSpy.mock.lastCall![0] as CustomEvent).detail.value).toBe('<p>New content</p>');
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('destroy', () => {
|
|
261
|
+
it('should detach editable root from editor on component unmount', async () => {
|
|
262
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
263
|
+
|
|
264
|
+
renderTestEditor({
|
|
265
|
+
preset: createEditorPreset('multiroot'),
|
|
266
|
+
content: {},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const editor = await waitForTestEditor<MultiRootEditor>();
|
|
270
|
+
const element = renderTestEditable({
|
|
271
|
+
rootName: 'foo',
|
|
272
|
+
content: '<p>Initial foo content</p>',
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await vi.waitFor(() => {
|
|
276
|
+
expect(editor.model.document.getRoot('foo')!.isAttached()).toBe(true);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
element.remove();
|
|
280
|
+
|
|
281
|
+
await vi.waitFor(() => {
|
|
282
|
+
expect(editor.model.document.getRoot('foo')?.isAttached()).toBe(false);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
consoleSpy.mockRestore();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should hide element during destruction', async () => {
|
|
289
|
+
renderTestEditor({
|
|
290
|
+
preset: createEditorPreset('multiroot'),
|
|
291
|
+
content: {},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const editor = await waitForTestEditor<MultiRootEditor>();
|
|
295
|
+
const element = renderTestEditable({
|
|
296
|
+
rootName: 'foo',
|
|
297
|
+
content: '<p>Initial foo content</p>',
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
await vi.waitFor(() => {
|
|
301
|
+
expect(editor.getData({ rootName: 'foo' })).toBeDefined();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
element.remove();
|
|
305
|
+
|
|
306
|
+
expect(element.style.display).toBe('none');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should not crash if editor was destroyed before editable', async () => {
|
|
310
|
+
renderTestEditor({
|
|
311
|
+
preset: createEditorPreset('multiroot'),
|
|
312
|
+
content: {},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const editorElement = document.querySelector('cke5-editor')!;
|
|
316
|
+
const editor = await waitForTestEditor<MultiRootEditor>();
|
|
317
|
+
|
|
318
|
+
const element = renderTestEditable({
|
|
319
|
+
rootName: 'foo',
|
|
320
|
+
content: '<p>Initial foo content</p>',
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
await vi.waitFor(() => {
|
|
324
|
+
expect(editor.getData({ rootName: 'foo' })).toBeDefined();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
editorElement.remove();
|
|
328
|
+
|
|
329
|
+
await vi.waitFor(() => expect(editor.state).toBe('destroyed'));
|
|
330
|
+
|
|
331
|
+
expect(() => element.remove()).not.toThrow();
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { MultiRootEditor } from 'ckeditor5';
|
|
2
|
+
|
|
3
|
+
import { CKEditor5SymfonyError } from '../ckeditor5-symfony-error';
|
|
4
|
+
import { debounce, waitForDOMReady } from '../shared';
|
|
5
|
+
import { EditorsRegistry } from './editor/editors-registry';
|
|
6
|
+
import { queryAllEditorIds } from './editor/utils';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Editable hook for Symfony. It allows you to create editables for multi-root editors.
|
|
10
|
+
*/
|
|
11
|
+
export class EditableComponentElement extends HTMLElement {
|
|
12
|
+
/**
|
|
13
|
+
* The promise that resolves when the editable is mounted.
|
|
14
|
+
*/
|
|
15
|
+
private editorPromise: Promise<MultiRootEditor> | null = null;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Mounts the editable component.
|
|
19
|
+
*/
|
|
20
|
+
async connectedCallback() {
|
|
21
|
+
await waitForDOMReady();
|
|
22
|
+
|
|
23
|
+
if (!this.hasAttribute('data-cke-editor-id')) {
|
|
24
|
+
this.setAttribute('data-cke-editor-id', queryAllEditorIds()[0]!);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const editorId = this.getAttribute('data-cke-editor-id');
|
|
28
|
+
const rootName = this.getAttribute('data-cke-root-name');
|
|
29
|
+
const content = this.getAttribute('data-cke-content');
|
|
30
|
+
const saveDebounceMs = Number.parseInt(this.getAttribute('data-cke-save-debounce-ms')!, 10);
|
|
31
|
+
|
|
32
|
+
/* v8 ignore next 3 */
|
|
33
|
+
if (!editorId || !rootName) {
|
|
34
|
+
throw new CKEditor5SymfonyError('Editor ID or Root Name is missing.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// If the editor is not registered yet, we will wait for it to be registered.
|
|
38
|
+
this.style.display = 'block';
|
|
39
|
+
this.editorPromise = EditorsRegistry.the.execute(editorId, async (editor: MultiRootEditor) => {
|
|
40
|
+
const input = this.querySelector('input') as HTMLInputElement | null;
|
|
41
|
+
const { ui, editing, model } = editor;
|
|
42
|
+
|
|
43
|
+
if (model.document.getRoot(rootName)) {
|
|
44
|
+
// If the newly added root already exists, but the newly added editable has content,
|
|
45
|
+
// we need to update the root data with the editable content.
|
|
46
|
+
if (content !== null) {
|
|
47
|
+
const data = editor.getData({ rootName });
|
|
48
|
+
|
|
49
|
+
if (data && data !== content) {
|
|
50
|
+
editor.setData({
|
|
51
|
+
[rootName]: content,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return editor;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
editor.addRoot(rootName, {
|
|
60
|
+
isUndoable: false,
|
|
61
|
+
...content !== null && {
|
|
62
|
+
data: content,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const contentElement = this.querySelector('[data-cke-editable-content]') as HTMLElement | null;
|
|
67
|
+
const editable = ui.view.createEditable(rootName, contentElement!);
|
|
68
|
+
|
|
69
|
+
ui.addEditable(editable);
|
|
70
|
+
editing.view.forceRender();
|
|
71
|
+
|
|
72
|
+
// Sync data with socket and input element.
|
|
73
|
+
const sync = () => {
|
|
74
|
+
const html = editor.getData({ rootName });
|
|
75
|
+
|
|
76
|
+
if (input) {
|
|
77
|
+
input.value = html;
|
|
78
|
+
input.dispatchEvent(new Event('input'));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.dispatchEvent(new CustomEvent('change', { detail: { value: html } }));
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
editor.model.document.on('change:data', debounce(saveDebounceMs, sync));
|
|
85
|
+
sync();
|
|
86
|
+
|
|
87
|
+
return editor;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Destroys the editable component. Unmounts root from the editor.
|
|
93
|
+
*/
|
|
94
|
+
async disconnectedCallback() {
|
|
95
|
+
const rootName = this.getAttribute('data-cke-root-name');
|
|
96
|
+
|
|
97
|
+
// Let's hide the element during destruction to prevent flickering.
|
|
98
|
+
this.style.display = 'none';
|
|
99
|
+
|
|
100
|
+
// Let's wait for the mounted promise to resolve before proceeding with destruction.
|
|
101
|
+
const editor = await this.editorPromise;
|
|
102
|
+
this.editorPromise = null;
|
|
103
|
+
|
|
104
|
+
// Unmount root from the editor if editor is still registered.
|
|
105
|
+
if (editor && editor.state !== 'destroyed' && rootName) {
|
|
106
|
+
const root = editor.model.document.getRoot(rootName);
|
|
107
|
+
|
|
108
|
+
if (root && 'detachEditable' in editor) {
|
|
109
|
+
editor.detachEditable(root);
|
|
110
|
+
editor.detachRoot(rootName, false);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Plugin } from 'ckeditor5';
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { CustomEditorPluginsRegistry } from './custom-editor-plugins';
|
|
5
|
+
|
|
6
|
+
class MockPlugin1 extends Plugin {
|
|
7
|
+
static get pluginName() {
|
|
8
|
+
return 'MockPlugin1';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class MockPlugin2 extends Plugin {
|
|
13
|
+
static get pluginName() {
|
|
14
|
+
return 'MockPlugin2';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('custom-editor-plugins', () => {
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
CustomEditorPluginsRegistry.the.unregisterAll();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('register', () => {
|
|
24
|
+
it('should register a plugin successfully', async () => {
|
|
25
|
+
const unregister = CustomEditorPluginsRegistry.the.register('test-plugin', () => MockPlugin1);
|
|
26
|
+
|
|
27
|
+
expect(await CustomEditorPluginsRegistry.the.get('test-plugin')).toBe(MockPlugin1);
|
|
28
|
+
expect(typeof unregister).toBe('function');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should throw an error when registering a plugin with the same name', () => {
|
|
32
|
+
CustomEditorPluginsRegistry.the.register('duplicate-plugin', () => MockPlugin1);
|
|
33
|
+
|
|
34
|
+
expect(() => {
|
|
35
|
+
CustomEditorPluginsRegistry.the.register('duplicate-plugin', () => MockPlugin2);
|
|
36
|
+
}).toThrow('Plugin with name "duplicate-plugin" is already registered.');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should return an unregister function that removes the plugin', async () => {
|
|
40
|
+
const unregister = CustomEditorPluginsRegistry.the.register('removable-plugin', () => MockPlugin1);
|
|
41
|
+
|
|
42
|
+
expect(await CustomEditorPluginsRegistry.the.get('removable-plugin')).toBe(MockPlugin1);
|
|
43
|
+
|
|
44
|
+
unregister();
|
|
45
|
+
|
|
46
|
+
expect(await CustomEditorPluginsRegistry.the.get('removable-plugin')).toBeUndefined();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('unregister', () => {
|
|
51
|
+
it('should unregister an existing plugin', async () => {
|
|
52
|
+
CustomEditorPluginsRegistry.the.register('temp-plugin', () => MockPlugin1);
|
|
53
|
+
|
|
54
|
+
expect(await CustomEditorPluginsRegistry.the.get('temp-plugin')).toBe(MockPlugin1);
|
|
55
|
+
|
|
56
|
+
CustomEditorPluginsRegistry.the.unregister('temp-plugin');
|
|
57
|
+
|
|
58
|
+
expect(await CustomEditorPluginsRegistry.the.get('temp-plugin')).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should throw an error when unregistering a non-existent plugin', () => {
|
|
62
|
+
expect(() => {
|
|
63
|
+
CustomEditorPluginsRegistry.the.unregister('non-existent');
|
|
64
|
+
}).toThrow('Plugin with name "non-existent" is not registered.');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('unregisterAll', () => {
|
|
69
|
+
it('should clear all registered plugins', async () => {
|
|
70
|
+
CustomEditorPluginsRegistry.the.register('plugin-1', () => MockPlugin1);
|
|
71
|
+
CustomEditorPluginsRegistry.the.register('plugin-2', () => MockPlugin2);
|
|
72
|
+
|
|
73
|
+
expect(await CustomEditorPluginsRegistry.the.get('plugin-1')).toBe(MockPlugin1);
|
|
74
|
+
expect(await CustomEditorPluginsRegistry.the.get('plugin-2')).toBe(MockPlugin2);
|
|
75
|
+
|
|
76
|
+
CustomEditorPluginsRegistry.the.unregisterAll();
|
|
77
|
+
|
|
78
|
+
expect(await CustomEditorPluginsRegistry.the.get('plugin-1')).toBeUndefined();
|
|
79
|
+
expect(await CustomEditorPluginsRegistry.the.get('plugin-2')).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('get', () => {
|
|
84
|
+
it('should return the correct plugin when it exists', async () => {
|
|
85
|
+
CustomEditorPluginsRegistry.the.register('existing-plugin', () => MockPlugin1);
|
|
86
|
+
|
|
87
|
+
expect(await CustomEditorPluginsRegistry.the.get('existing-plugin')).toBe(MockPlugin1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should return undefined for non-existent plugins', async () => {
|
|
91
|
+
expect(await CustomEditorPluginsRegistry.the.get('non-existent-plugin')).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('has', () => {
|
|
96
|
+
it('should return true for registered plugins', () => {
|
|
97
|
+
CustomEditorPluginsRegistry.the.register('check-plugin', () => MockPlugin1);
|
|
98
|
+
|
|
99
|
+
expect(CustomEditorPluginsRegistry.the.has('check-plugin')).toBe(true);
|
|
100
|
+
expect(CustomEditorPluginsRegistry.the.has('non-existent')).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { PluginConstructor } from 'ckeditor5';
|
|
2
|
+
|
|
3
|
+
import type { CanBePromise } from '../../types';
|
|
4
|
+
|
|
5
|
+
import { CKEditor5SymfonyError } from '../../ckeditor5-symfony-error';
|
|
6
|
+
|
|
7
|
+
type PluginReader = () => CanBePromise<PluginConstructor>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Registry for custom CKEditor plugins.
|
|
11
|
+
* Allows registration and retrieval of custom plugins that can be used alongside built-in plugins.
|
|
12
|
+
*/
|
|
13
|
+
export class CustomEditorPluginsRegistry {
|
|
14
|
+
static readonly the = new CustomEditorPluginsRegistry();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Map of registered custom plugins.
|
|
18
|
+
*/
|
|
19
|
+
private readonly plugins = new Map<string, PluginReader>();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Private constructor to enforce singleton pattern.
|
|
23
|
+
*/
|
|
24
|
+
private constructor() {}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Registers a custom plugin for the CKEditor.
|
|
28
|
+
*
|
|
29
|
+
* @param name The name of the plugin.
|
|
30
|
+
* @param reader The plugin reader function that returns the plugin constructor.
|
|
31
|
+
* @returns A function to unregister the plugin.
|
|
32
|
+
*/
|
|
33
|
+
register(name: string, reader: PluginReader): () => void {
|
|
34
|
+
if (this.plugins.has(name)) {
|
|
35
|
+
throw new CKEditor5SymfonyError(`Plugin with name "${name}" is already registered.`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.plugins.set(name, reader);
|
|
39
|
+
|
|
40
|
+
return this.unregister.bind(this, name);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Removes a custom plugin by its name.
|
|
45
|
+
*
|
|
46
|
+
* @param name The name of the plugin to unregister.
|
|
47
|
+
* @throws Will throw an error if the plugin is not registered.
|
|
48
|
+
*/
|
|
49
|
+
unregister(name: string): void {
|
|
50
|
+
if (!this.plugins.has(name)) {
|
|
51
|
+
throw new CKEditor5SymfonyError(`Plugin with name "${name}" is not registered.`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.plugins.delete(name);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Removes all custom editor plugins.
|
|
59
|
+
* This is useful for cleanup in tests or when reloading plugins.
|
|
60
|
+
*/
|
|
61
|
+
unregisterAll(): void {
|
|
62
|
+
this.plugins.clear();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Retrieves a custom plugin by its name.
|
|
67
|
+
*
|
|
68
|
+
* @param name The name of the plugin.
|
|
69
|
+
* @returns The plugin constructor or undefined if not found.
|
|
70
|
+
*/
|
|
71
|
+
async get(name: string): Promise<PluginConstructor | undefined> {
|
|
72
|
+
const reader = this.plugins.get(name);
|
|
73
|
+
|
|
74
|
+
return reader?.();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Checks if a plugin with the given name is registered.
|
|
79
|
+
*
|
|
80
|
+
* @param name The name of the plugin.
|
|
81
|
+
* @returns `true` if the plugin is registered, `false` otherwise.
|
|
82
|
+
*/
|
|
83
|
+
has(name: string): boolean {
|
|
84
|
+
return this.plugins.has(name);
|
|
85
|
+
}
|
|
86
|
+
}
|