ckeditor5-livewire 0.0.1
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/hooks/context/context.d.ts +39 -0
- package/dist/hooks/context/context.d.ts.map +1 -0
- package/dist/hooks/context/contexts-registry.d.ts +9 -0
- package/dist/hooks/context/contexts-registry.d.ts.map +1 -0
- package/dist/hooks/context/index.d.ts +4 -0
- package/dist/hooks/context/index.d.ts.map +1 -0
- package/dist/hooks/context/typings.d.ts +34 -0
- package/dist/hooks/context/typings.d.ts.map +1 -0
- package/dist/hooks/editable.d.ts +40 -0
- package/dist/hooks/editable.d.ts.map +1 -0
- package/dist/hooks/editor/custom-editor-plugins.d.ts +54 -0
- package/dist/hooks/editor/custom-editor-plugins.d.ts.map +1 -0
- package/dist/hooks/editor/editor.d.ts +69 -0
- package/dist/hooks/editor/editor.d.ts.map +1 -0
- package/dist/hooks/editor/editors-registry.d.ts +9 -0
- package/dist/hooks/editor/editors-registry.d.ts.map +1 -0
- package/dist/hooks/editor/index.d.ts +3 -0
- package/dist/hooks/editor/index.d.ts.map +1 -0
- package/dist/hooks/editor/plugins/index.d.ts +3 -0
- package/dist/hooks/editor/plugins/index.d.ts.map +1 -0
- package/dist/hooks/editor/plugins/livewire-sync.d.ts +19 -0
- package/dist/hooks/editor/plugins/livewire-sync.d.ts.map +1 -0
- package/dist/hooks/editor/plugins/sync-editor-with-input.d.ts +6 -0
- package/dist/hooks/editor/plugins/sync-editor-with-input.d.ts.map +1 -0
- package/dist/hooks/editor/typings.d.ts +99 -0
- package/dist/hooks/editor/typings.d.ts.map +1 -0
- package/dist/hooks/editor/utils/create-editor-in-context.d.ts +44 -0
- package/dist/hooks/editor/utils/create-editor-in-context.d.ts.map +1 -0
- package/dist/hooks/editor/utils/get-editor-roots-values.d.ts +9 -0
- package/dist/hooks/editor/utils/get-editor-roots-values.d.ts.map +1 -0
- package/dist/hooks/editor/utils/index.d.ts +12 -0
- package/dist/hooks/editor/utils/index.d.ts.map +1 -0
- package/dist/hooks/editor/utils/is-single-editing-like-editor.d.ts +9 -0
- package/dist/hooks/editor/utils/is-single-editing-like-editor.d.ts.map +1 -0
- package/dist/hooks/editor/utils/load-editor-constructor.d.ts +9 -0
- package/dist/hooks/editor/utils/load-editor-constructor.d.ts.map +1 -0
- package/dist/hooks/editor/utils/load-editor-plugins.d.ts +20 -0
- package/dist/hooks/editor/utils/load-editor-plugins.d.ts.map +1 -0
- package/dist/hooks/editor/utils/load-editor-translations.d.ts +14 -0
- package/dist/hooks/editor/utils/load-editor-translations.d.ts.map +1 -0
- package/dist/hooks/editor/utils/normalize-custom-translations.d.ts +11 -0
- package/dist/hooks/editor/utils/normalize-custom-translations.d.ts.map +1 -0
- package/dist/hooks/editor/utils/query-editor-editables.d.ts +34 -0
- package/dist/hooks/editor/utils/query-editor-editables.d.ts.map +1 -0
- package/dist/hooks/editor/utils/resolve-editor-config-elements-references.d.ts +9 -0
- package/dist/hooks/editor/utils/resolve-editor-config-elements-references.d.ts.map +1 -0
- package/dist/hooks/editor/utils/set-editor-editable-height.d.ts +9 -0
- package/dist/hooks/editor/utils/set-editor-editable-height.d.ts.map +1 -0
- package/dist/hooks/editor/utils/wrap-with-watchdog.d.ts +24 -0
- package/dist/hooks/editor/utils/wrap-with-watchdog.d.ts.map +1 -0
- package/dist/hooks/hook.d.ts +58 -0
- package/dist/hooks/hook.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +5 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/ui-part.d.ts +32 -0
- package/dist/hooks/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 +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +1146 -0
- package/dist/index.mjs.map +1 -0
- package/dist/shared/async-registry.d.ts +131 -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 +13 -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/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.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/hooks/context/context.test.ts +394 -0
- package/src/hooks/context/context.ts +116 -0
- package/src/hooks/context/contexts-registry.test.ts +10 -0
- package/src/hooks/context/contexts-registry.ts +10 -0
- package/src/hooks/context/index.ts +3 -0
- package/src/hooks/context/typings.ts +39 -0
- package/src/hooks/editable.test.ts +276 -0
- package/src/hooks/editable.ts +122 -0
- package/src/hooks/editor/custom-editor-plugins.test.ts +103 -0
- package/src/hooks/editor/custom-editor-plugins.ts +84 -0
- package/src/hooks/editor/editor.test.ts +782 -0
- package/src/hooks/editor/editor.ts +357 -0
- package/src/hooks/editor/editors-registry.test.ts +10 -0
- package/src/hooks/editor/editors-registry.ts +10 -0
- package/src/hooks/editor/index.ts +2 -0
- package/src/hooks/editor/plugins/index.ts +2 -0
- package/src/hooks/editor/plugins/livewire-sync.ts +85 -0
- package/src/hooks/editor/plugins/sync-editor-with-input.ts +76 -0
- package/src/hooks/editor/typings.ts +114 -0
- package/src/hooks/editor/utils/create-editor-in-context.ts +90 -0
- package/src/hooks/editor/utils/get-editor-roots-values.ts +16 -0
- package/src/hooks/editor/utils/index.ts +11 -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 +100 -0
- package/src/hooks/editor/utils/load-editor-plugins.ts +71 -0
- package/src/hooks/editor/utils/load-editor-translations.ts +233 -0
- package/src/hooks/editor/utils/normalize-custom-translations.test.ts +152 -0
- package/src/hooks/editor/utils/normalize-custom-translations.ts +18 -0
- package/src/hooks/editor/utils/query-editor-editables.ts +102 -0
- package/src/hooks/editor/utils/resolve-editor-config-elements-references.test.ts +93 -0
- package/src/hooks/editor/utils/resolve-editor-config-elements-references.ts +36 -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/editor/utils/wrap-with-watchdog.test.ts +45 -0
- package/src/hooks/editor/utils/wrap-with-watchdog.ts +51 -0
- package/src/hooks/hook.ts +87 -0
- package/src/hooks/index.ts +21 -0
- package/src/hooks/ui-part.test.ts +161 -0
- package/src/hooks/ui-part.ts +80 -0
- package/src/index.ts +5 -0
- package/src/livewire.d.ts +42 -0
- package/src/shared/async-registry.test.ts +658 -0
- package/src/shared/async-registry.ts +308 -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 +12 -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/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.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,276 @@
|
|
|
1
|
+
import type { MultiRootEditor } from 'ckeditor5';
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createEditableHtmlElement,
|
|
7
|
+
createEditableSnapshot,
|
|
8
|
+
createEditorHtmlElement,
|
|
9
|
+
createEditorPreset,
|
|
10
|
+
createEditorSnapshot,
|
|
11
|
+
LivewireStub,
|
|
12
|
+
queryEditableInput,
|
|
13
|
+
waitForTestEditor,
|
|
14
|
+
} from '~/test-utils';
|
|
15
|
+
|
|
16
|
+
import type { Snapshot as EditorSnapshot } from './editor';
|
|
17
|
+
|
|
18
|
+
import { EditableComponentHook } from './editable';
|
|
19
|
+
import { EditorComponentHook } from './editor';
|
|
20
|
+
import { registerLivewireComponentHook } from './hook';
|
|
21
|
+
|
|
22
|
+
describe('editable component', () => {
|
|
23
|
+
let livewireStub: LivewireStub;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
document.body.innerHTML = '';
|
|
27
|
+
livewireStub = window.Livewire = new LivewireStub();
|
|
28
|
+
|
|
29
|
+
registerLivewireComponentHook('ckeditor5', EditorComponentHook);
|
|
30
|
+
registerLivewireComponentHook('ckeditor5-editable', EditableComponentHook);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
await livewireStub.$internal.destroy();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('mounting editable', () => {
|
|
38
|
+
it('should add editable root to the editor after mounting editor (empty editor)', async () => {
|
|
39
|
+
appendMultirootEditor();
|
|
40
|
+
|
|
41
|
+
const editor = await waitForTestEditor();
|
|
42
|
+
|
|
43
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
44
|
+
name: 'ckeditor5-editable',
|
|
45
|
+
el: createEditableHtmlElement(),
|
|
46
|
+
ephemeral: createEditableSnapshot('foo', '<p>Initial foo component</p>'),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(editor.getData({ rootName: 'foo' })).toBe('<p>Initial foo component</p>');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should add editable root to the editor after mounting editor (non-empty editor, other editable defined before)', async () => {
|
|
53
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
54
|
+
name: 'ckeditor5-editable',
|
|
55
|
+
el: createEditableHtmlElement(),
|
|
56
|
+
ephemeral: createEditableSnapshot('bar', '<p>Initial bar content</p>'),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
appendMultirootEditor({
|
|
60
|
+
bar: '<p>Initial bar content</p>',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const editor = await waitForTestEditor();
|
|
64
|
+
|
|
65
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
66
|
+
name: 'ckeditor5-editable',
|
|
67
|
+
el: createEditableHtmlElement(),
|
|
68
|
+
ephemeral: createEditableSnapshot('foo', '<p>Initial foo content</p>'),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(editor.getData({ rootName: 'foo' })).toBe('<p>Initial foo content</p>');
|
|
72
|
+
expect(editor.getData({ rootName: 'bar' })).toBe('<p>Initial bar content</p>');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should add editable root to the editor after mounting editor (non-empty editor, other editable defined after)', async () => {
|
|
76
|
+
appendMultirootEditor({
|
|
77
|
+
bar: '<p>Initial bar content</p>',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
81
|
+
name: 'ckeditor5-editable',
|
|
82
|
+
el: createEditableHtmlElement(),
|
|
83
|
+
ephemeral: createEditableSnapshot('bar', '<p>Initial bar content</p>'),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const editor = await waitForTestEditor();
|
|
87
|
+
|
|
88
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
89
|
+
name: 'ckeditor5-editable',
|
|
90
|
+
el: createEditableHtmlElement(),
|
|
91
|
+
ephemeral: createEditableSnapshot('foo', '<p>Initial foo content</p>'),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(editor.getData({ rootName: 'foo' })).toBe('<p>Initial foo content</p>');
|
|
95
|
+
expect(editor.getData({ rootName: 'bar' })).toBe('<p>Initial bar content</p>');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should do nothing if adding existing root (without provided content)', async () => {
|
|
99
|
+
appendMultirootEditor({
|
|
100
|
+
main: '<p>Initial main content</p>',
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
104
|
+
name: 'ckeditor5-editable',
|
|
105
|
+
el: createEditableHtmlElement(),
|
|
106
|
+
ephemeral: createEditableSnapshot('main'),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const editor = await waitForTestEditor();
|
|
110
|
+
|
|
111
|
+
expect(editor.getData({ rootName: 'main' })).toBe('<p>Initial main content</p>');
|
|
112
|
+
|
|
113
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
114
|
+
name: 'ckeditor5-editable',
|
|
115
|
+
el: createEditableHtmlElement(),
|
|
116
|
+
ephemeral: createEditableSnapshot('main'),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(editor.getData({ rootName: 'main' })).toBe('<p>Initial main content</p>');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should update existing root content if added existing root with provided content', async () => {
|
|
123
|
+
appendMultirootEditor({
|
|
124
|
+
main: '<p>Initial main content</p>',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
128
|
+
name: 'ckeditor5-editable',
|
|
129
|
+
el: createEditableHtmlElement(),
|
|
130
|
+
ephemeral: createEditableSnapshot('main'),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const editor = await waitForTestEditor();
|
|
134
|
+
|
|
135
|
+
expect(editor.getData({ rootName: 'main' })).toBe('<p>Initial main content</p>');
|
|
136
|
+
|
|
137
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
138
|
+
name: 'ckeditor5-editable',
|
|
139
|
+
el: createEditableHtmlElement(),
|
|
140
|
+
ephemeral: createEditableSnapshot('main', '<p>Updated main content</p>'),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(editor.getData({ rootName: 'main' })).toBe('<p>Updated main content</p>');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('input value synchronization', () => {
|
|
148
|
+
let editor: MultiRootEditor;
|
|
149
|
+
|
|
150
|
+
beforeEach(async () => {
|
|
151
|
+
appendMultirootEditor();
|
|
152
|
+
editor = await waitForTestEditor();
|
|
153
|
+
vi.useFakeTimers();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
afterEach(() => {
|
|
157
|
+
vi.useRealTimers();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should not crash if input is not present', () => {
|
|
161
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
162
|
+
name: 'ckeditor5-editable',
|
|
163
|
+
el: createEditableHtmlElement({
|
|
164
|
+
withInput: false,
|
|
165
|
+
}),
|
|
166
|
+
ephemeral: createEditableSnapshot('foo', '<p>Initial foo component</p>'),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(editor.getData({ rootName: 'foo' })).toBe('<p>Initial foo component</p>');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should synchronize input value after mounting editable', async () => {
|
|
173
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
174
|
+
name: 'ckeditor5-editable',
|
|
175
|
+
el: createEditableHtmlElement({
|
|
176
|
+
id: 'editable-foo',
|
|
177
|
+
}),
|
|
178
|
+
ephemeral: createEditableSnapshot('foo', '<p>Initial foo component</p>'),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const input = queryEditableInput('editable-foo')!;
|
|
182
|
+
|
|
183
|
+
expect(input.value).toBe('<p>Initial foo component</p>');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should debounce input value synchronization', async () => {
|
|
187
|
+
livewireStub.$internal.appendComponentToDOM({
|
|
188
|
+
name: 'ckeditor5-editable',
|
|
189
|
+
el: createEditableHtmlElement({
|
|
190
|
+
id: 'editable-foo',
|
|
191
|
+
}),
|
|
192
|
+
ephemeral: {
|
|
193
|
+
...createEditableSnapshot('foo', '<p>Initial foo component</p>'),
|
|
194
|
+
saveDebounceMs: 500,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const input = queryEditableInput('editable-foo')!;
|
|
199
|
+
|
|
200
|
+
expect(input.value).toBe('<p>Initial foo component</p>');
|
|
201
|
+
|
|
202
|
+
editor.setData({
|
|
203
|
+
foo: '<p>Modified foo content</p>',
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
vi.advanceTimersByTime(300);
|
|
207
|
+
|
|
208
|
+
expect(input.value).toBe('<p>Initial foo component</p>');
|
|
209
|
+
|
|
210
|
+
vi.advanceTimersByTime(300);
|
|
211
|
+
|
|
212
|
+
expect(input.value).toBe('<p>Modified foo content</p>');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('socket synchronization', () => {
|
|
217
|
+
let editor: MultiRootEditor;
|
|
218
|
+
|
|
219
|
+
beforeEach(async () => {
|
|
220
|
+
appendMultirootEditor();
|
|
221
|
+
editor = await waitForTestEditor();
|
|
222
|
+
vi.useFakeTimers();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
afterEach(() => {
|
|
226
|
+
vi.useRealTimers();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should synchronize socket after mounting editable', async () => {
|
|
230
|
+
const { $wire } = livewireStub.$internal.appendComponentToDOM({
|
|
231
|
+
name: 'ckeditor5-editable',
|
|
232
|
+
el: createEditableHtmlElement(),
|
|
233
|
+
ephemeral: createEditableSnapshot('foo', '<p>Initial foo component</p>'),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect($wire.set).toHaveBeenCalledWith('content', '<p>Initial foo component</p>');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should debounce socket synchronization', async () => {
|
|
240
|
+
const { $wire } = livewireStub.$internal.appendComponentToDOM({
|
|
241
|
+
name: 'ckeditor5-editable',
|
|
242
|
+
el: createEditableHtmlElement(),
|
|
243
|
+
ephemeral: {
|
|
244
|
+
...createEditableSnapshot('foo', '<p>Initial foo component</p>'),
|
|
245
|
+
saveDebounceMs: 500,
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
expect($wire.set).toHaveBeenCalledWith('content', '<p>Initial foo component</p>');
|
|
250
|
+
|
|
251
|
+
editor.setData({
|
|
252
|
+
foo: '<p>Modified foo content</p>',
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
vi.advanceTimersByTime(300);
|
|
256
|
+
|
|
257
|
+
expect($wire.set).not.toHaveBeenCalledWith('content', '<p>Modified foo content</p>');
|
|
258
|
+
|
|
259
|
+
vi.advanceTimersByTime(300);
|
|
260
|
+
|
|
261
|
+
expect($wire.set).toHaveBeenCalledWith('content', '<p>Modified foo content</p>');
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
function appendMultirootEditor(initialContent: Record<string, string> = {}) {
|
|
266
|
+
livewireStub.$internal.appendComponentToDOM<EditorSnapshot>({
|
|
267
|
+
name: 'ckeditor5',
|
|
268
|
+
el: createEditorHtmlElement(),
|
|
269
|
+
ephemeral: {
|
|
270
|
+
...createEditorSnapshot(),
|
|
271
|
+
preset: createEditorPreset('multiroot'),
|
|
272
|
+
content: initialContent,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { MultiRootEditor } from 'ckeditor5';
|
|
2
|
+
|
|
3
|
+
import { debounce } from '../shared';
|
|
4
|
+
import { EditorsRegistry } from './editor/editors-registry';
|
|
5
|
+
import { ClassHook } from './hook';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Editable hook for Livewire. It allows you to create editables for multi-root editors.
|
|
9
|
+
*/
|
|
10
|
+
export class EditableComponentHook extends ClassHook<Snapshot> {
|
|
11
|
+
/**
|
|
12
|
+
* The promise that resolves when the editable is mounted.
|
|
13
|
+
*/
|
|
14
|
+
private editorPromise: Promise<MultiRootEditor> | null = null;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Mounts the editable component.
|
|
18
|
+
*/
|
|
19
|
+
override mounted() {
|
|
20
|
+
const { editorId, rootName, content, saveDebounceMs } = this.ephemeral;
|
|
21
|
+
const input = this.element.querySelector<HTMLInputElement>('input');
|
|
22
|
+
|
|
23
|
+
// If the editor is not registered yet, we will wait for it to be registered.
|
|
24
|
+
this.editorPromise = EditorsRegistry.the.execute(editorId, (editor: MultiRootEditor) => {
|
|
25
|
+
const { ui, editing, model } = editor;
|
|
26
|
+
|
|
27
|
+
if (model.document.getRoot(rootName)) {
|
|
28
|
+
// If the newly added root already exists, but the newly added editable has content,
|
|
29
|
+
// we need to update the root data with the editable content.
|
|
30
|
+
if (content !== null) {
|
|
31
|
+
const data = editor.getData({ rootName });
|
|
32
|
+
|
|
33
|
+
if (data && data !== content) {
|
|
34
|
+
editor.setData({
|
|
35
|
+
[rootName]: content,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return editor;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
editor.addRoot(rootName, {
|
|
44
|
+
isUndoable: false,
|
|
45
|
+
...content !== null && {
|
|
46
|
+
data: content,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const contentElement = this.element.querySelector('[data-cke-editable-content]') as HTMLElement | null;
|
|
51
|
+
const editable = ui.view.createEditable(rootName, contentElement!);
|
|
52
|
+
|
|
53
|
+
ui.addEditable(editable);
|
|
54
|
+
editing.view.forceRender();
|
|
55
|
+
|
|
56
|
+
// Sync data with socket and input element.
|
|
57
|
+
const sync = () => {
|
|
58
|
+
const html = editor.getData({ rootName });
|
|
59
|
+
|
|
60
|
+
if (input) {
|
|
61
|
+
input.value = html;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.$wire.set('content', html);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
editor.model.document.on('change:data', debounce(saveDebounceMs, sync));
|
|
68
|
+
sync();
|
|
69
|
+
|
|
70
|
+
return editor;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Destroys the editable component. Unmounts root from the editor.
|
|
76
|
+
*/
|
|
77
|
+
override async destroyed() {
|
|
78
|
+
const { rootName } = this.ephemeral;
|
|
79
|
+
|
|
80
|
+
// Let's hide the element during destruction to prevent flickering.
|
|
81
|
+
this.element.style.display = 'none';
|
|
82
|
+
|
|
83
|
+
// Let's wait for the mounted promise to resolve before proceeding with destruction.
|
|
84
|
+
const editor = await this.editorPromise;
|
|
85
|
+
this.editorPromise = null;
|
|
86
|
+
|
|
87
|
+
// Unmount root from the editor if editor is still registered.
|
|
88
|
+
if (editor && editor.state !== 'destroyed') {
|
|
89
|
+
const root = editor.model.document.getRoot(rootName);
|
|
90
|
+
|
|
91
|
+
if (root && 'detachEditable' in editor) {
|
|
92
|
+
editor.detachEditable(root);
|
|
93
|
+
editor.detachRoot(rootName, false);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A snapshot of the Livewire component's state relevant to the CKEditor5 editable hook.
|
|
101
|
+
*/
|
|
102
|
+
export type Snapshot = {
|
|
103
|
+
/**
|
|
104
|
+
* The ID of the editor instance this editable belongs to.
|
|
105
|
+
*/
|
|
106
|
+
editorId: string;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The name of the root element in the editor.
|
|
110
|
+
*/
|
|
111
|
+
rootName: string;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The initial content value for the editable.
|
|
115
|
+
*/
|
|
116
|
+
content: string | null;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The debounce time in milliseconds for saving changes.
|
|
120
|
+
*/
|
|
121
|
+
saveDebounceMs: number;
|
|
122
|
+
};
|
|
@@ -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,84 @@
|
|
|
1
|
+
import type { PluginConstructor } from 'ckeditor5';
|
|
2
|
+
|
|
3
|
+
import type { CanBePromise } from '../../types';
|
|
4
|
+
|
|
5
|
+
type PluginReader = () => CanBePromise<PluginConstructor>;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Registry for custom CKEditor plugins.
|
|
9
|
+
* Allows registration and retrieval of custom plugins that can be used alongside built-in plugins.
|
|
10
|
+
*/
|
|
11
|
+
export class CustomEditorPluginsRegistry {
|
|
12
|
+
static readonly the = new CustomEditorPluginsRegistry();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Map of registered custom plugins.
|
|
16
|
+
*/
|
|
17
|
+
private readonly plugins = new Map<string, PluginReader>();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Private constructor to enforce singleton pattern.
|
|
21
|
+
*/
|
|
22
|
+
private constructor() {}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Registers a custom plugin for the CKEditor.
|
|
26
|
+
*
|
|
27
|
+
* @param name The name of the plugin.
|
|
28
|
+
* @param reader The plugin reader function that returns the plugin constructor.
|
|
29
|
+
* @returns A function to unregister the plugin.
|
|
30
|
+
*/
|
|
31
|
+
register(name: string, reader: PluginReader): () => void {
|
|
32
|
+
if (this.plugins.has(name)) {
|
|
33
|
+
throw new Error(`Plugin with name "${name}" is already registered.`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.plugins.set(name, reader);
|
|
37
|
+
|
|
38
|
+
return this.unregister.bind(this, name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Removes a custom plugin by its name.
|
|
43
|
+
*
|
|
44
|
+
* @param name The name of the plugin to unregister.
|
|
45
|
+
* @throws Will throw an error if the plugin is not registered.
|
|
46
|
+
*/
|
|
47
|
+
unregister(name: string): void {
|
|
48
|
+
if (!this.plugins.has(name)) {
|
|
49
|
+
throw new Error(`Plugin with name "${name}" is not registered.`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.plugins.delete(name);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Removes all custom editor plugins.
|
|
57
|
+
* This is useful for cleanup in tests or when reloading plugins.
|
|
58
|
+
*/
|
|
59
|
+
unregisterAll(): void {
|
|
60
|
+
this.plugins.clear();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Retrieves a custom plugin by its name.
|
|
65
|
+
*
|
|
66
|
+
* @param name The name of the plugin.
|
|
67
|
+
* @returns The plugin constructor or undefined if not found.
|
|
68
|
+
*/
|
|
69
|
+
async get(name: string): Promise<PluginConstructor | undefined> {
|
|
70
|
+
const reader = this.plugins.get(name);
|
|
71
|
+
|
|
72
|
+
return reader?.();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Checks if a plugin with the given name is registered.
|
|
77
|
+
*
|
|
78
|
+
* @param name The name of the plugin.
|
|
79
|
+
* @returns `true` if the plugin is registered, `false` otherwise.
|
|
80
|
+
*/
|
|
81
|
+
has(name: string): boolean {
|
|
82
|
+
return this.plugins.has(name);
|
|
83
|
+
}
|
|
84
|
+
}
|