ckeditor5-blazor 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/ckeditor5-blazor-error.d.ts +7 -0
- package/dist/ckeditor5-blazor-error.d.ts.map +1 -0
- package/dist/elements/context/context.d.ts +26 -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 +34 -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 +31 -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/dispatch-editor-roots-change-event.d.ts +23 -0
- package/dist/elements/editor/plugins/dispatch-editor-roots-change-event.d.ts.map +1 -0
- package/dist/elements/editor/plugins/index.d.ts +3 -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/get-editor-roots-values.d.ts +9 -0
- package/dist/elements/editor/utils/get-editor-roots-values.d.ts.map +1 -0
- package/dist/elements/editor/utils/index.d.ts +14 -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/resolve-editor-config-translations.d.ts +25 -0
- package/dist/elements/editor/utils/resolve-editor-config-translations.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/ensure-editor-elements-registered.d.ts +5 -0
- package/dist/elements/ensure-editor-elements-registered.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/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 +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +1400 -0
- package/dist/index.mjs.map +1 -0
- package/dist/interop/create-context-blazor-interop.d.ts +10 -0
- package/dist/interop/create-context-blazor-interop.d.ts.map +1 -0
- package/dist/interop/create-editable-blazor-interop.d.ts +21 -0
- package/dist/interop/create-editable-blazor-interop.d.ts.map +1 -0
- package/dist/interop/create-editor-blazor-interop.d.ts +19 -0
- package/dist/interop/create-editor-blazor-interop.d.ts.map +1 -0
- package/dist/interop/create-ui-part-blazor-interop.d.ts +10 -0
- package/dist/interop/create-ui-part-blazor-interop.d.ts.map +1 -0
- package/dist/interop/index.d.ts +5 -0
- package/dist/interop/index.d.ts.map +1 -0
- package/dist/interop/utils/create-editor-value-sync.d.ts +63 -0
- package/dist/interop/utils/create-editor-value-sync.d.ts.map +1 -0
- package/dist/interop/utils/index.d.ts +2 -0
- package/dist/interop/utils/index.d.ts.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 +16 -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-interactive-attribute.d.ts +18 -0
- package/dist/shared/wait-for-interactive-attribute.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/dot-net-interop.type.d.ts +7 -0
- package/dist/types/dot-net-interop.type.d.ts.map +1 -0
- package/dist/types/index.d.ts +4 -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 +49 -0
- package/src/ckeditor5-blazor-error.ts +9 -0
- package/src/elements/context/context.test.ts +323 -0
- package/src/elements/context/context.ts +128 -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 +38 -0
- package/src/elements/editable.test.ts +383 -0
- package/src/elements/editable.ts +183 -0
- package/src/elements/editor/custom-editor-plugins.test.ts +103 -0
- package/src/elements/editor/custom-editor-plugins.ts +85 -0
- package/src/elements/editor/editor.test.ts +562 -0
- package/src/elements/editor/editor.ts +330 -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/dispatch-editor-roots-change-event.ts +76 -0
- package/src/elements/editor/plugins/index.ts +2 -0
- package/src/elements/editor/plugins/sync-editor-with-input.ts +79 -0
- package/src/elements/editor/typings.ts +114 -0
- package/src/elements/editor/utils/create-editor-in-context.ts +89 -0
- package/src/elements/editor/utils/get-editor-roots-values.test.ts +48 -0
- package/src/elements/editor/utils/get-editor-roots-values.ts +21 -0
- package/src/elements/editor/utils/index.ts +13 -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 +72 -0
- package/src/elements/editor/utils/load-editor-translations.ts +232 -0
- package/src/elements/editor/utils/normalize-custom-translations.test.ts +152 -0
- package/src/elements/editor/utils/normalize-custom-translations.ts +17 -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/resolve-editor-config-translations.test.ts +131 -0
- package/src/elements/editor/utils/resolve-editor-config-translations.ts +77 -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/ensure-editor-elements-registered.ts +24 -0
- package/src/elements/index.ts +14 -0
- package/src/elements/ui-part.test.ts +156 -0
- package/src/elements/ui-part.ts +84 -0
- package/src/index.ts +15 -0
- package/src/interop/create-context-blazor-interop.test.ts +30 -0
- package/src/interop/create-context-blazor-interop.ts +15 -0
- package/src/interop/create-editable-blazor-interop.test.ts +213 -0
- package/src/interop/create-editable-blazor-interop.ts +98 -0
- package/src/interop/create-editor-blazor-interop.test.ts +183 -0
- package/src/interop/create-editor-blazor-interop.ts +112 -0
- package/src/interop/create-ui-part-blazor-interop.test.ts +30 -0
- package/src/interop/create-ui-part-blazor-interop.ts +15 -0
- package/src/interop/index.ts +4 -0
- package/src/interop/utils/create-editor-value-sync.test.ts +302 -0
- package/src/interop/utils/create-editor-value-sync.ts +160 -0
- package/src/interop/utils/index.ts +1 -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 +15 -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-interactive-attribute.test.ts +93 -0
- package/src/shared/wait-for-interactive-attribute.ts +50 -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/dot-net-interop.type.ts +6 -0
- package/src/types/dotnet-global.d.ts +14 -0
- package/src/types/index.ts +3 -0
- package/src/types/required-by.type.ts +1 -0
|
@@ -0,0 +1,383 @@
|
|
|
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
|
+
waitForDestroyAllEditors,
|
|
10
|
+
waitForTestEditor,
|
|
11
|
+
} from '~/test-utils';
|
|
12
|
+
|
|
13
|
+
import { timeout } from '../shared';
|
|
14
|
+
import { ensureEditorElementsRegistered } from './ensure-editor-elements-registered';
|
|
15
|
+
|
|
16
|
+
describe('editable component', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
document.body.innerHTML = '';
|
|
19
|
+
ensureEditorElementsRegistered();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
vi.useRealTimers();
|
|
24
|
+
vi.resetAllMocks();
|
|
25
|
+
|
|
26
|
+
await waitForDestroyAllEditors();
|
|
27
|
+
|
|
28
|
+
document.body.innerHTML = '';
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('mounting editable', () => {
|
|
32
|
+
it('should add editable root to the editor after mounting editor (empty editor)', async () => {
|
|
33
|
+
renderTestEditor({
|
|
34
|
+
preset: createEditorPreset('multiroot'),
|
|
35
|
+
content: {},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const editor = await waitForTestEditor<MultiRootEditor>();
|
|
39
|
+
|
|
40
|
+
renderTestEditable({
|
|
41
|
+
rootName: 'foo',
|
|
42
|
+
content: '<p>Initial foo component</p>',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await vi.waitFor(() => {
|
|
46
|
+
expect(editor.getData({ rootName: 'foo' })).toBe('<p>Initial foo component</p>');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should add editable root to the editor after mounting editor (non-empty editor, other editable defined before)', async () => {
|
|
51
|
+
renderTestEditable({
|
|
52
|
+
rootName: 'bar',
|
|
53
|
+
content: '<p>Initial bar content</p>',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
renderTestEditor({
|
|
57
|
+
preset: createEditorPreset('multiroot'),
|
|
58
|
+
content: {
|
|
59
|
+
bar: '<p>Initial bar content</p>',
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const editor = await waitForTestEditor<MultiRootEditor>();
|
|
64
|
+
|
|
65
|
+
renderTestEditable({
|
|
66
|
+
rootName: 'foo',
|
|
67
|
+
content: '<p>Initial foo content</p>',
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await vi.waitFor(() => {
|
|
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
|
+
|
|
76
|
+
it('should add editable root to the editor after mounting editor (non-empty editor, other editable defined after)', async () => {
|
|
77
|
+
renderTestEditor({
|
|
78
|
+
preset: createEditorPreset('multiroot'),
|
|
79
|
+
content: {
|
|
80
|
+
bar: '<p>Initial bar content</p>',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
renderTestEditable({
|
|
85
|
+
rootName: 'bar',
|
|
86
|
+
content: '<p>Initial bar content</p>',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const editor = await waitForTestEditor<MultiRootEditor>();
|
|
90
|
+
|
|
91
|
+
renderTestEditable({
|
|
92
|
+
rootName: 'foo',
|
|
93
|
+
content: '<p>Initial foo content</p>',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await vi.waitFor(() => {
|
|
97
|
+
expect(editor.getData({ rootName: 'foo' })).toBe('<p>Initial foo content</p>');
|
|
98
|
+
expect(editor.getData({ rootName: 'bar' })).toBe('<p>Initial bar content</p>');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should do nothing if adding existing root (without provided content)', async () => {
|
|
103
|
+
renderTestEditor({
|
|
104
|
+
preset: createEditorPreset('multiroot'),
|
|
105
|
+
content: {
|
|
106
|
+
main: '<p>Initial main content</p>',
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
renderTestEditable({
|
|
111
|
+
rootName: 'main',
|
|
112
|
+
content: null,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const editor = await waitForTestEditor<MultiRootEditor>();
|
|
116
|
+
|
|
117
|
+
await vi.waitFor(() => {
|
|
118
|
+
expect(editor.getData({ rootName: 'main' })).toBe('<p>Initial main content</p>');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should update existing root content if added existing root with provided content', async () => {
|
|
123
|
+
renderTestEditor({
|
|
124
|
+
preset: createEditorPreset('multiroot'),
|
|
125
|
+
content: {
|
|
126
|
+
main: '<p>Initial main content</p>',
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
renderTestEditable({
|
|
131
|
+
rootName: 'main',
|
|
132
|
+
content: '<p>Updated main content</p>',
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const editor = await waitForTestEditor<MultiRootEditor>();
|
|
136
|
+
|
|
137
|
+
await vi.waitFor(() => {
|
|
138
|
+
expect(editor.getData({ rootName: 'main' })).toBe('<p>Updated main content</p>');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should auto-assign editor ID if not provided', async () => {
|
|
143
|
+
renderTestEditor({
|
|
144
|
+
preset: createEditorPreset('multiroot'),
|
|
145
|
+
content: {},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await waitForTestEditor<MultiRootEditor>();
|
|
149
|
+
|
|
150
|
+
const editable = renderTestEditable({
|
|
151
|
+
editorId: '',
|
|
152
|
+
rootName: 'foo',
|
|
153
|
+
content: '<p>Initial foo component</p>',
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await vi.waitFor(() => {
|
|
157
|
+
expect(editable.getAttribute('data-cke-editor-id')).toBe('test-editor');
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should not initialize editable if element is disconnected before editor is ready', async () => {
|
|
162
|
+
const el = renderTestEditable({
|
|
163
|
+
rootName: 'foo',
|
|
164
|
+
content: '<p>Foo</p>',
|
|
165
|
+
});
|
|
166
|
+
el.remove();
|
|
167
|
+
|
|
168
|
+
renderTestEditor({
|
|
169
|
+
preset: createEditorPreset('multiroot'),
|
|
170
|
+
content: {},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const editor = await waitForTestEditor<MultiRootEditor>();
|
|
174
|
+
|
|
175
|
+
await timeout(10);
|
|
176
|
+
|
|
177
|
+
expect(editor.model.document.getRoot('foo')).toBe(null);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('input value synchronization', () => {
|
|
182
|
+
let editor: MultiRootEditor;
|
|
183
|
+
|
|
184
|
+
beforeEach(async () => {
|
|
185
|
+
renderTestEditor({
|
|
186
|
+
preset: createEditorPreset('multiroot'),
|
|
187
|
+
content: {},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
editor = await waitForTestEditor<MultiRootEditor>();
|
|
191
|
+
vi.useFakeTimers();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should not crash if input is not present', async () => {
|
|
195
|
+
renderTestEditable({
|
|
196
|
+
rootName: 'foo',
|
|
197
|
+
content: '<p>Initial foo component</p>',
|
|
198
|
+
}, { withInput: false });
|
|
199
|
+
|
|
200
|
+
await vi.waitFor(() => {
|
|
201
|
+
expect(editor.getData({ rootName: 'foo' })).toBe('<p>Initial foo component</p>');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should synchronize input value after mounting editable', async () => {
|
|
206
|
+
const element = renderTestEditable({
|
|
207
|
+
rootName: 'foo',
|
|
208
|
+
content: '<p>Initial foo component</p>',
|
|
209
|
+
}, { withInput: true });
|
|
210
|
+
|
|
211
|
+
const input = element.querySelector('input')!;
|
|
212
|
+
|
|
213
|
+
await vi.waitFor(() => {
|
|
214
|
+
expect(input.value).toBe('<p>Initial foo component</p>');
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should debounce input value synchronization', async () => {
|
|
219
|
+
const element = renderTestEditable({
|
|
220
|
+
rootName: 'foo',
|
|
221
|
+
content: '<p>Initial foo component</p>',
|
|
222
|
+
saveDebounceMs: 500,
|
|
223
|
+
}, { withInput: true });
|
|
224
|
+
|
|
225
|
+
const input = element.querySelector('input')!;
|
|
226
|
+
|
|
227
|
+
await vi.waitFor(() => {
|
|
228
|
+
expect(input.value).toBe('<p>Initial foo component</p>');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
editor.setData({
|
|
232
|
+
foo: '<p>Modified foo content</p>',
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
vi.advanceTimersByTime(300);
|
|
236
|
+
expect(input.value).toBe('<p>Initial foo component</p>');
|
|
237
|
+
|
|
238
|
+
vi.advanceTimersByTime(300);
|
|
239
|
+
expect(input.value).toBe('<p>Modified foo content</p>');
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('web component events', () => {
|
|
244
|
+
let editor: MultiRootEditor;
|
|
245
|
+
|
|
246
|
+
beforeEach(async () => {
|
|
247
|
+
vi.useFakeTimers();
|
|
248
|
+
renderTestEditor({
|
|
249
|
+
preset: createEditorPreset('multiroot'),
|
|
250
|
+
content: {},
|
|
251
|
+
});
|
|
252
|
+
editor = await waitForTestEditor<MultiRootEditor>();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should emit change event when editor data changes', async () => {
|
|
256
|
+
const element = renderTestEditable({
|
|
257
|
+
rootName: 'foo',
|
|
258
|
+
content: '<p>Initial content</p>',
|
|
259
|
+
saveDebounceMs: 100,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const changeSpy = vi.fn();
|
|
263
|
+
element.addEventListener('change', changeSpy);
|
|
264
|
+
|
|
265
|
+
await vi.waitFor(() => {
|
|
266
|
+
expect(editor.getData({ rootName: 'foo' })).toBe('<p>Initial content</p>');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Clear spy after initial sync
|
|
270
|
+
changeSpy.mockClear();
|
|
271
|
+
|
|
272
|
+
editor.setData({ foo: '<p>New content</p>' });
|
|
273
|
+
|
|
274
|
+
vi.advanceTimersByTime(150);
|
|
275
|
+
|
|
276
|
+
expect(changeSpy).toHaveBeenCalledTimes(1);
|
|
277
|
+
expect((changeSpy.mock.lastCall![0] as CustomEvent).detail.value).toBe('<p>New content</p>');
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe('destroy', () => {
|
|
282
|
+
it('should detach editable root from editor on component unmount', async () => {
|
|
283
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
284
|
+
|
|
285
|
+
renderTestEditor({
|
|
286
|
+
preset: createEditorPreset('multiroot'),
|
|
287
|
+
content: {},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const editor = await waitForTestEditor<MultiRootEditor>();
|
|
291
|
+
const element = renderTestEditable({
|
|
292
|
+
rootName: 'foo',
|
|
293
|
+
content: '<p>Initial foo content</p>',
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
await vi.waitFor(() => {
|
|
297
|
+
expect(editor.model.document.getRoot('foo')!.isAttached()).toBe(true);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
element.remove();
|
|
301
|
+
|
|
302
|
+
await vi.waitFor(() => {
|
|
303
|
+
expect(editor.model.document.getRoot('foo')?.isAttached()).toBe(false);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
consoleSpy.mockRestore();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should hide element during destruction', async () => {
|
|
310
|
+
renderTestEditor({
|
|
311
|
+
preset: createEditorPreset('multiroot'),
|
|
312
|
+
content: {},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const editor = await waitForTestEditor<MultiRootEditor>();
|
|
316
|
+
const element = renderTestEditable({
|
|
317
|
+
rootName: 'foo',
|
|
318
|
+
content: '<p>Initial foo content</p>',
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await vi.waitFor(() => {
|
|
322
|
+
expect(editor.getData({ rootName: 'foo' })).toBeDefined();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
element.remove();
|
|
326
|
+
|
|
327
|
+
expect(element.style.display).toBe('none');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should not crash if editor was destroyed before editable', async () => {
|
|
331
|
+
renderTestEditor({
|
|
332
|
+
preset: createEditorPreset('multiroot'),
|
|
333
|
+
content: {},
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const editorElement = document.querySelector('cke5-editor')!;
|
|
337
|
+
const editor = await waitForTestEditor<MultiRootEditor>();
|
|
338
|
+
|
|
339
|
+
const element = renderTestEditable({
|
|
340
|
+
rootName: 'foo',
|
|
341
|
+
content: '<p>Initial foo content</p>',
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
await vi.waitFor(() => {
|
|
345
|
+
expect(editor.getData({ rootName: 'foo' })).toBeDefined();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
editorElement.remove();
|
|
349
|
+
|
|
350
|
+
await vi.waitFor(() => expect(editor.state).toBe('destroyed'));
|
|
351
|
+
|
|
352
|
+
expect(() => element.remove()).not.toThrow();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should not attempt to detach root again if it is already detached', async () => {
|
|
356
|
+
renderTestEditor({
|
|
357
|
+
preset: createEditorPreset('multiroot'),
|
|
358
|
+
content: {},
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const editor = await waitForTestEditor<MultiRootEditor>();
|
|
362
|
+
const element = renderTestEditable({
|
|
363
|
+
rootName: 'foo',
|
|
364
|
+
content: '<p>Initial foo content</p>',
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
await vi.waitFor(() => {
|
|
368
|
+
expect(editor.model.document.getRoot('foo')!.isAttached()).toBe(true);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const detachSpy = vi.spyOn(editor, 'detachRoot');
|
|
372
|
+
editor.detachRoot('foo', false);
|
|
373
|
+
expect(detachSpy).toHaveBeenCalledTimes(1);
|
|
374
|
+
|
|
375
|
+
detachSpy.mockClear();
|
|
376
|
+
|
|
377
|
+
element.remove();
|
|
378
|
+
await timeout(10);
|
|
379
|
+
|
|
380
|
+
expect(detachSpy).not.toHaveBeenCalled();
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { WaitForInteractiveResult } from '../shared';
|
|
2
|
+
import type { MultiRootEditor } from 'ckeditor5';
|
|
3
|
+
|
|
4
|
+
import { CKEditor5BlazorError } from '../ckeditor5-blazor-error';
|
|
5
|
+
import { debounce, waitForDOMReady, waitForInteractiveAttribute } from '../shared';
|
|
6
|
+
import { EditorsRegistry } from './editor/editors-registry';
|
|
7
|
+
import { queryAllEditorIds } from './editor/utils';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Editable hook for Blazor. It allows you to create editables for multi-root editors.
|
|
11
|
+
*/
|
|
12
|
+
export class EditableComponentElement extends HTMLElement {
|
|
13
|
+
/**
|
|
14
|
+
* The promise that resolves when the editable is mounted.
|
|
15
|
+
*/
|
|
16
|
+
private editorPromise: Promise<MultiRootEditor | null> | null = null;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Wait result for the interactive attribute.
|
|
20
|
+
*/
|
|
21
|
+
private interactiveWait?: WaitForInteractiveResult;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Callbacks to be invoked before the editable is destroyed.
|
|
25
|
+
*/
|
|
26
|
+
private beforeDestroyCallbacks: Array<() => void> = [];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Registers a callback to be called before the editable is destroyed.
|
|
30
|
+
*/
|
|
31
|
+
public onBeforeDestroy(callback: () => void): void {
|
|
32
|
+
this.beforeDestroyCallbacks.push(callback);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Mounts the editable component.
|
|
37
|
+
*/
|
|
38
|
+
async connectedCallback() {
|
|
39
|
+
await waitForDOMReady();
|
|
40
|
+
|
|
41
|
+
this.interactiveWait = waitForInteractiveAttribute(this);
|
|
42
|
+
await this.interactiveWait.promise;
|
|
43
|
+
await this.initializeEditable();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Initializes the editable instance.
|
|
48
|
+
*/
|
|
49
|
+
private async initializeEditable(): Promise<void> {
|
|
50
|
+
if (!this.hasAttribute('data-cke-editor-id')) {
|
|
51
|
+
this.setAttribute('data-cke-editor-id', queryAllEditorIds()[0]!);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const editorId = this.getAttribute('data-cke-editor-id');
|
|
55
|
+
const rootName = this.getAttribute('data-cke-root-name');
|
|
56
|
+
const content = this.getAttribute('data-cke-content');
|
|
57
|
+
const saveDebounceMs = Number.parseInt(this.getAttribute('data-cke-save-debounce-ms')!, 10);
|
|
58
|
+
|
|
59
|
+
/* v8 ignore next if -- @preserve */
|
|
60
|
+
if (!editorId || !rootName) {
|
|
61
|
+
throw new CKEditor5BlazorError('Editor ID or Root Name is missing.');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// If the editor is not registered yet, we will wait for it to be registered.
|
|
65
|
+
this.style.display = 'block';
|
|
66
|
+
this.editorPromise = EditorsRegistry.the.execute(editorId, async (editor: MultiRootEditor) => {
|
|
67
|
+
if (!this.isConnected) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const { ui, editing, model } = editor;
|
|
72
|
+
|
|
73
|
+
const input = this.querySelector('input') as HTMLInputElement | null;
|
|
74
|
+
const root = model.document.getRoot(rootName);
|
|
75
|
+
|
|
76
|
+
if (root?.isAttached()) {
|
|
77
|
+
// If the newly added root already exists, but the newly added editable has content,
|
|
78
|
+
// we need to update the root data with the editable content.
|
|
79
|
+
if (content !== null) {
|
|
80
|
+
const data = editor.getData({ rootName });
|
|
81
|
+
|
|
82
|
+
if (data && data !== content) {
|
|
83
|
+
editor.setData({
|
|
84
|
+
[rootName]: content,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return editor;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
editor.addRoot(rootName, {
|
|
93
|
+
isUndoable: false,
|
|
94
|
+
...content !== null && {
|
|
95
|
+
data: content,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const contentElement = this.querySelector('[data-cke-editable-content]') as HTMLElement | null;
|
|
100
|
+
const editable = ui.view.createEditable(rootName, contentElement!);
|
|
101
|
+
|
|
102
|
+
ui.addEditable(editable);
|
|
103
|
+
editing.view.forceRender();
|
|
104
|
+
|
|
105
|
+
// Sync data with socket and input element.
|
|
106
|
+
const sync = () => {
|
|
107
|
+
if (!model.document.getRoot(rootName)?.isAttached()) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const html = editor.getData({ rootName });
|
|
112
|
+
|
|
113
|
+
if (input) {
|
|
114
|
+
input.value = html;
|
|
115
|
+
input.dispatchEvent(new Event('input'));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
119
|
+
detail: {
|
|
120
|
+
value: html,
|
|
121
|
+
},
|
|
122
|
+
}));
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const debouncedSync = debounce(saveDebounceMs, sync);
|
|
126
|
+
|
|
127
|
+
editor.model.document.on('change:data', debouncedSync);
|
|
128
|
+
this.onBeforeDestroy(() => editor.model.document.off('change:data', debouncedSync));
|
|
129
|
+
sync();
|
|
130
|
+
|
|
131
|
+
return editor;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Destroys the editable component. Unmounts root from the editor.
|
|
137
|
+
*/
|
|
138
|
+
async disconnectedCallback() {
|
|
139
|
+
// Disconnect the observer if present.
|
|
140
|
+
this.interactiveWait?.disconnect();
|
|
141
|
+
|
|
142
|
+
const rootName = this.getAttribute('data-cke-root-name');
|
|
143
|
+
|
|
144
|
+
// Let's hide the element during destruction to prevent flickering.
|
|
145
|
+
this.style.display = 'none';
|
|
146
|
+
|
|
147
|
+
// Let's wait for the mounted promise to resolve before proceeding with destruction.
|
|
148
|
+
const editor = await this.editorPromise;
|
|
149
|
+
this.editorPromise = null;
|
|
150
|
+
|
|
151
|
+
// Run all registered pre-destroy callbacks and clear the queue.
|
|
152
|
+
for (const callback of this.beforeDestroyCallbacks) {
|
|
153
|
+
callback();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this.beforeDestroyCallbacks = [];
|
|
157
|
+
|
|
158
|
+
// Unmount root from the editor if editor is still registered.
|
|
159
|
+
if (editor && editor.state !== 'destroyed' && rootName) {
|
|
160
|
+
const root = editor.model.document.getRoot(rootName);
|
|
161
|
+
|
|
162
|
+
/* v8 ignore else -- @preserve */
|
|
163
|
+
if (root && 'detachEditable' in editor) {
|
|
164
|
+
// Detaching editables seem to be buggy when something removed DOM element of the editable (e.g. Blazor re-render) before
|
|
165
|
+
// the editable is unmounted. To prevent errors in such cases, we will try to detach the editable if it exists, but ignore errors.
|
|
166
|
+
try {
|
|
167
|
+
if (editor.ui.view.editables[rootName]) {
|
|
168
|
+
editor.detachEditable(root);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
// Ignore errors when detaching editable.
|
|
173
|
+
/* v8 ignore next -- @preserve */
|
|
174
|
+
console.error('Unable unmount editable from root:', err);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (root.isAttached()) {
|
|
178
|
+
editor.detachRoot(rootName, false);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -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
|
+
});
|