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,279 @@
|
|
|
1
|
+
import type { Editor } from 'ckeditor5';
|
|
2
|
+
|
|
3
|
+
import type { EditorId, EditorLanguage, EditorPreset } from './typings';
|
|
4
|
+
import type { EditorCreator } from './utils';
|
|
5
|
+
|
|
6
|
+
import { isEmptyObject, waitFor, waitForDOMReady } from '../../shared';
|
|
7
|
+
import { ContextsRegistry } from '../context';
|
|
8
|
+
import { EditorsRegistry } from './editors-registry';
|
|
9
|
+
import { createSyncEditorWithInputPlugin } from './plugins';
|
|
10
|
+
import {
|
|
11
|
+
createEditorInContext,
|
|
12
|
+
isSingleRootEditor,
|
|
13
|
+
loadAllEditorTranslations,
|
|
14
|
+
loadEditorConstructor,
|
|
15
|
+
loadEditorPlugins,
|
|
16
|
+
normalizeCustomTranslations,
|
|
17
|
+
queryEditablesElements,
|
|
18
|
+
queryEditablesSnapshotContent,
|
|
19
|
+
resolveEditorConfigElementReferences,
|
|
20
|
+
setEditorEditableHeight,
|
|
21
|
+
unwrapEditorContext,
|
|
22
|
+
unwrapEditorWatchdog,
|
|
23
|
+
wrapWithWatchdog,
|
|
24
|
+
} from './utils';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The Symfony hook that manages the lifecycle of CKEditor5 instances.
|
|
28
|
+
*/
|
|
29
|
+
export class EditorComponentElement extends HTMLElement {
|
|
30
|
+
/**
|
|
31
|
+
* The promise that resolves to the editor instance.
|
|
32
|
+
*/
|
|
33
|
+
private editorPromise: Promise<Editor> | null = null;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Mounts the editor component.
|
|
37
|
+
*/
|
|
38
|
+
async connectedCallback(): Promise<void> {
|
|
39
|
+
await waitForDOMReady();
|
|
40
|
+
|
|
41
|
+
const editorId = this.getAttribute('data-cke-editor-id')!;
|
|
42
|
+
|
|
43
|
+
EditorsRegistry.the.resetErrors(editorId);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
this.style.display = 'block';
|
|
47
|
+
this.editorPromise = this.createEditor();
|
|
48
|
+
|
|
49
|
+
const editor = await this.editorPromise;
|
|
50
|
+
|
|
51
|
+
// Do not even try to broadcast about the registration of the editor
|
|
52
|
+
// if hook was immediately destroyed.
|
|
53
|
+
if (this.isConnected) {
|
|
54
|
+
EditorsRegistry.the.register(editorId, editor);
|
|
55
|
+
|
|
56
|
+
editor.once('destroy', () => {
|
|
57
|
+
if (EditorsRegistry.the.hasItem(editorId)) {
|
|
58
|
+
EditorsRegistry.the.unregister(editorId);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/* v8 ignore next 6 */
|
|
63
|
+
}
|
|
64
|
+
catch (error: any) {
|
|
65
|
+
console.error(`Error initializing CKEditor5 instance with ID "${editorId}":`, error);
|
|
66
|
+
this.editorPromise = null;
|
|
67
|
+
EditorsRegistry.the.error(editorId, error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Destroys the editor instance when the component is destroyed.
|
|
73
|
+
* This is important to prevent memory leaks and ensure that the editor is properly cleaned up.
|
|
74
|
+
*/
|
|
75
|
+
async disconnectedCallback() {
|
|
76
|
+
// Let's hide the element during destruction to prevent flickering.
|
|
77
|
+
this.style.display = 'none';
|
|
78
|
+
|
|
79
|
+
// Let's wait for the mounted promise to resolve before proceeding with destruction.
|
|
80
|
+
try {
|
|
81
|
+
const editor = await this.editorPromise;
|
|
82
|
+
|
|
83
|
+
/* v8 ignore next 3 */
|
|
84
|
+
if (!editor) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const editorContext = unwrapEditorContext(editor);
|
|
89
|
+
const watchdog = unwrapEditorWatchdog(editor);
|
|
90
|
+
|
|
91
|
+
if (editorContext) {
|
|
92
|
+
// If context is present, make sure it's not in unmounting phase, as it'll kill the editors.
|
|
93
|
+
// If it's being destroyed, don't do anything, as the context will take care of it.
|
|
94
|
+
if (editorContext.state !== 'unavailable') {
|
|
95
|
+
await editorContext.context.remove(editorContext.editorContextId);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (watchdog) {
|
|
99
|
+
await watchdog.destroy();
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
await editor.destroy();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
this.editorPromise = null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Creates the CKEditor instance.
|
|
112
|
+
*/
|
|
113
|
+
private async createEditor() {
|
|
114
|
+
const editorId = this.getAttribute('data-cke-editor-id')!;
|
|
115
|
+
const preset = JSON.parse(this.getAttribute('data-cke-preset')!) as EditorPreset;
|
|
116
|
+
const contextId = this.getAttribute('data-cke-context-id');
|
|
117
|
+
const editableHeight = this.getAttribute('data-cke-editable-height') ? Number.parseInt(this.getAttribute('data-cke-editable-height')!, 10) : null;
|
|
118
|
+
const saveDebounceMs = Number.parseInt(this.getAttribute('data-cke-save-debounce-ms')!, 10);
|
|
119
|
+
const language = JSON.parse(this.getAttribute('data-cke-language')!) as EditorLanguage;
|
|
120
|
+
const watchdog = this.hasAttribute('data-cke-watchdog');
|
|
121
|
+
const content = JSON.parse(this.getAttribute('data-cke-content')!) as Record<string, string>;
|
|
122
|
+
|
|
123
|
+
const {
|
|
124
|
+
customTranslations,
|
|
125
|
+
editorType,
|
|
126
|
+
licenseKey,
|
|
127
|
+
config: { plugins, ...config },
|
|
128
|
+
} = preset;
|
|
129
|
+
|
|
130
|
+
// Wrap editor creator with watchdog if needed.
|
|
131
|
+
let Constructor: EditorCreator = await loadEditorConstructor(editorType);
|
|
132
|
+
const context = await (
|
|
133
|
+
contextId
|
|
134
|
+
? ContextsRegistry.the.waitFor(contextId)
|
|
135
|
+
: null
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// Do not use editor specific watchdog if context is attached, as the context is by default protected.
|
|
139
|
+
if (watchdog && !context) {
|
|
140
|
+
const wrapped = await wrapWithWatchdog(Constructor);
|
|
141
|
+
|
|
142
|
+
({ Constructor } = wrapped);
|
|
143
|
+
wrapped.watchdog.on('restart', () => {
|
|
144
|
+
const newInstance = wrapped.watchdog.editor!;
|
|
145
|
+
|
|
146
|
+
this.editorPromise = Promise.resolve(newInstance);
|
|
147
|
+
|
|
148
|
+
EditorsRegistry.the.register(editorId, newInstance);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const { loadedPlugins, hasPremium } = await loadEditorPlugins(plugins);
|
|
153
|
+
|
|
154
|
+
if (isSingleRootEditor(editorType)) {
|
|
155
|
+
loadedPlugins.push(
|
|
156
|
+
await createSyncEditorWithInputPlugin(saveDebounceMs),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Mix custom translations with loaded translations.
|
|
161
|
+
const loadedTranslations = await loadAllEditorTranslations(language, hasPremium);
|
|
162
|
+
const mixedTranslations = [
|
|
163
|
+
...loadedTranslations,
|
|
164
|
+
normalizeCustomTranslations(customTranslations || {}),
|
|
165
|
+
]
|
|
166
|
+
.filter(translations => !isEmptyObject(translations));
|
|
167
|
+
|
|
168
|
+
// Let's query all elements, and create basic configuration.
|
|
169
|
+
let initialData: string | Record<string, string> = {
|
|
170
|
+
...content,
|
|
171
|
+
...queryEditablesSnapshotContent(editorId),
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
if (isSingleRootEditor(editorType)) {
|
|
175
|
+
initialData = initialData['main'] || '';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Depending of the editor type, and parent lookup for nearest context or initialize it without it.
|
|
179
|
+
const editor = await (async () => {
|
|
180
|
+
let sourceElementOrData: HTMLElement | Record<string, HTMLElement> = queryEditablesElements(editorId);
|
|
181
|
+
|
|
182
|
+
// Handle special case when user specified `initialData` of several root elements, but editable components
|
|
183
|
+
// are not yet present in the DOM. In other words - editor is initialized before attaching root elements.
|
|
184
|
+
if (!sourceElementOrData['main']) {
|
|
185
|
+
const requiredRoots = (
|
|
186
|
+
isSingleRootEditor(editorType)
|
|
187
|
+
? ['main']
|
|
188
|
+
: Object.keys(initialData as Record<string, string>)
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (!checkIfAllRootsArePresent(sourceElementOrData, requiredRoots)) {
|
|
192
|
+
sourceElementOrData = await waitForAllRootsToBePresent(editorId, requiredRoots);
|
|
193
|
+
initialData = {
|
|
194
|
+
...content,
|
|
195
|
+
...queryEditablesSnapshotContent(editorId),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// If single root editor, unwrap the element from the object.
|
|
201
|
+
if (isSingleRootEditor(editorType) && 'main' in sourceElementOrData) {
|
|
202
|
+
sourceElementOrData = sourceElementOrData['main'];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Construct parsed config.
|
|
206
|
+
const parsedConfig = {
|
|
207
|
+
...resolveEditorConfigElementReferences(config),
|
|
208
|
+
initialData,
|
|
209
|
+
licenseKey,
|
|
210
|
+
plugins: loadedPlugins,
|
|
211
|
+
language,
|
|
212
|
+
...mixedTranslations.length && {
|
|
213
|
+
translations: mixedTranslations,
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
if (!context || !(sourceElementOrData instanceof HTMLElement)) {
|
|
218
|
+
return Constructor.create(sourceElementOrData as any, parsedConfig);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const result = await createEditorInContext({
|
|
222
|
+
context,
|
|
223
|
+
element: sourceElementOrData,
|
|
224
|
+
creator: Constructor,
|
|
225
|
+
config: parsedConfig,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return result.editor;
|
|
229
|
+
})();
|
|
230
|
+
|
|
231
|
+
if (isSingleRootEditor(editorType) && editableHeight) {
|
|
232
|
+
setEditorEditableHeight(editor, editableHeight);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return editor;
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Checks if all required root elements are present in the elements object.
|
|
241
|
+
*
|
|
242
|
+
* @param elements The elements object mapping root IDs to HTMLElements.
|
|
243
|
+
* @param requiredRoots The list of required root IDs.
|
|
244
|
+
* @returns True if all required roots are present, false otherwise.
|
|
245
|
+
*/
|
|
246
|
+
function checkIfAllRootsArePresent(elements: Record<string, HTMLElement>, requiredRoots: string[]): boolean {
|
|
247
|
+
return requiredRoots.every(rootId => elements[rootId]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Waits for all required root elements to be present in the DOM.
|
|
252
|
+
*
|
|
253
|
+
* @param editorId The editor's ID.
|
|
254
|
+
* @param requiredRoots The list of required root IDs.
|
|
255
|
+
* @returns A promise that resolves to the record of root elements.
|
|
256
|
+
*/
|
|
257
|
+
async function waitForAllRootsToBePresent(
|
|
258
|
+
editorId: EditorId,
|
|
259
|
+
requiredRoots: string[],
|
|
260
|
+
): Promise<Record<string, HTMLElement>> {
|
|
261
|
+
return waitFor(
|
|
262
|
+
() => {
|
|
263
|
+
const elements = queryEditablesElements(editorId) as unknown as Record<string, HTMLElement>;
|
|
264
|
+
|
|
265
|
+
if (!checkIfAllRootsArePresent(elements, requiredRoots)) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
'It looks like not all required root elements are present yet.\n'
|
|
268
|
+
+ '* If you want to wait for them, ensure they are registered before editor initialization.\n'
|
|
269
|
+
+ '* If you want lazy initialize roots, consider removing root values from the `initialData` config '
|
|
270
|
+
+ 'and assign initial data in editable components.\n'
|
|
271
|
+
+ `Missing roots: ${requiredRoots.filter(rootId => !elements[rootId]).join(', ')}.`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return elements;
|
|
276
|
+
},
|
|
277
|
+
{ timeOutAfter: 2000, retryAfter: 100 },
|
|
278
|
+
);
|
|
279
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { AsyncRegistry } from '../../shared/async-registry';
|
|
4
|
+
import { EditorsRegistry } from './editors-registry';
|
|
5
|
+
|
|
6
|
+
describe('editors registry', () => {
|
|
7
|
+
it('should be singleton of async registry', () => {
|
|
8
|
+
expect(EditorsRegistry.the).toBeInstanceOf(AsyncRegistry);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Editor } from 'ckeditor5';
|
|
2
|
+
|
|
3
|
+
import { AsyncRegistry } from '../../shared/async-registry';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* It provides a way to register editors and execute callbacks on them when they are available.
|
|
7
|
+
*/
|
|
8
|
+
export class EditorsRegistry extends AsyncRegistry<Editor> {
|
|
9
|
+
static readonly the = new EditorsRegistry();
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './sync-editor-with-input';
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ClassicEditor, PluginConstructor } from 'ckeditor5';
|
|
2
|
+
|
|
3
|
+
import { debounce } from '../../../shared';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a SyncEditorWithInput plugin class.
|
|
7
|
+
*/
|
|
8
|
+
export async function createSyncEditorWithInputPlugin(saveDebounceMs: number): Promise<PluginConstructor> {
|
|
9
|
+
const { Plugin } = await import('ckeditor5');
|
|
10
|
+
|
|
11
|
+
return class SyncEditorWithInput extends Plugin {
|
|
12
|
+
/**
|
|
13
|
+
* The input element to synchronize with.
|
|
14
|
+
*/
|
|
15
|
+
private input: HTMLInputElement | null = null;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The form element reference for cleanup.
|
|
19
|
+
*/
|
|
20
|
+
private form: HTMLFormElement | null = null;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The name of the plugin.
|
|
24
|
+
*/
|
|
25
|
+
static get pluginName() {
|
|
26
|
+
return 'SyncEditorWithInput' as const;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initializes the plugin.
|
|
31
|
+
*/
|
|
32
|
+
public afterInit(): void {
|
|
33
|
+
const { editor } = this;
|
|
34
|
+
const editorElement = (editor as ClassicEditor).sourceElement as HTMLElement;
|
|
35
|
+
|
|
36
|
+
// Try to find the associated input field.
|
|
37
|
+
const editorId = editorElement.id.replace(/_editor$/, '');
|
|
38
|
+
|
|
39
|
+
this.input = document.getElementById(`${editorId}_input`) as HTMLInputElement | null;
|
|
40
|
+
|
|
41
|
+
if (!this.input) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Setup handlers.
|
|
46
|
+
editor.model.document.on('change:data', debounce(saveDebounceMs, () => this.sync()));
|
|
47
|
+
editor.once('ready', this.sync);
|
|
48
|
+
|
|
49
|
+
// Setup form integration.
|
|
50
|
+
this.form = this.input.closest('form');
|
|
51
|
+
this.form?.addEventListener('submit', this.sync);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Synchronizes the editor's content with the input field.
|
|
56
|
+
*/
|
|
57
|
+
private sync = (): void => {
|
|
58
|
+
if (this.input) {
|
|
59
|
+
const newValue = this.editor.getData();
|
|
60
|
+
|
|
61
|
+
this.input.value = newValue;
|
|
62
|
+
this.input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Destroys the plugin.
|
|
68
|
+
*/
|
|
69
|
+
public override destroy(): void {
|
|
70
|
+
if (this.form) {
|
|
71
|
+
this.form.removeEventListener('submit', this.sync);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.input = null;
|
|
75
|
+
this.form = null;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a unique identifier for a CKEditor5 editor instance.
|
|
3
|
+
* This is typically the ID of the HTML element that the editor is attached to.
|
|
4
|
+
*/
|
|
5
|
+
export type EditorId = string;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Defines editor type supported by CKEditor5.
|
|
9
|
+
*/
|
|
10
|
+
export type EditorType = 'inline' | 'classic' | 'balloon' | 'decoupled' | 'multiroot';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Represents a CKEditor5 plugin as a string identifier.
|
|
14
|
+
*/
|
|
15
|
+
export type EditorPlugin = string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Configuration object for CKEditor5 editor instance.
|
|
19
|
+
*/
|
|
20
|
+
export type EditorConfig = {
|
|
21
|
+
/**
|
|
22
|
+
* Array of plugin identifiers to be loaded by the editor.
|
|
23
|
+
*/
|
|
24
|
+
plugins: EditorPlugin[];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Other configuration options are flexible and can be any key-value pairs.
|
|
28
|
+
*/
|
|
29
|
+
[key: string]: any;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Configuration object for CKEditor5 cloud services.
|
|
34
|
+
*/
|
|
35
|
+
export type EditorCloudConfig = {
|
|
36
|
+
/**
|
|
37
|
+
* The version of CKEditor5 being used.
|
|
38
|
+
*/
|
|
39
|
+
editorVersion: string;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Indicates whether the CKEditor5 instance is a premium version.
|
|
43
|
+
*/
|
|
44
|
+
premium: boolean;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* List of language codes for translations available in the CKEditor5 instance.
|
|
48
|
+
*/
|
|
49
|
+
translations: string[];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Configuration for CKEditor5's upload adapter.
|
|
53
|
+
*/
|
|
54
|
+
ckbox: {
|
|
55
|
+
version: string;
|
|
56
|
+
theme: string | null;
|
|
57
|
+
} | null;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Configuration object for the CKEditor5 hook.
|
|
62
|
+
*/
|
|
63
|
+
export type EditorPreset = {
|
|
64
|
+
/**
|
|
65
|
+
* The configuration of the cloud.
|
|
66
|
+
*/
|
|
67
|
+
cloud: EditorCloudConfig | null;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The type of CKEditor5 editor to use.
|
|
71
|
+
* Must be one of the predefined types: 'inline', 'classic', 'balloon', 'decoupled', or 'multiroot'.
|
|
72
|
+
*/
|
|
73
|
+
editorType: EditorType;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The configuration object for the CKEditor5 editor.
|
|
77
|
+
* This should match the configuration expected by CKEditor5.
|
|
78
|
+
*/
|
|
79
|
+
config: EditorConfig;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The license key for CKEditor5.
|
|
83
|
+
* This is required for using CKEditor5 with a valid license.
|
|
84
|
+
*/
|
|
85
|
+
licenseKey: string;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Optional watchdog configuration for error recovery.
|
|
89
|
+
*/
|
|
90
|
+
watchdogConfig?: Record<string, any> | null;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Optional custom translations for the editor.
|
|
94
|
+
* This allows for localization of the editor interface.
|
|
95
|
+
*/
|
|
96
|
+
customTranslations?: EditorCustomTranslationsDictionary | null;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Represents the language settings for the CKEditor5 editor.
|
|
101
|
+
*/
|
|
102
|
+
export type EditorLanguage = {
|
|
103
|
+
ui: string;
|
|
104
|
+
content: string;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Represents custom translations for the editor.
|
|
109
|
+
*/
|
|
110
|
+
export type EditorCustomTranslationsDictionary = {
|
|
111
|
+
[language: string]: {
|
|
112
|
+
[key: string]: string | ReadonlyArray<string>;
|
|
113
|
+
};
|
|
114
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Context, ContextWatchdog, Editor, EditorConfig } from 'ckeditor5';
|
|
2
|
+
|
|
3
|
+
import type { EditorCreator } from './wrap-with-watchdog';
|
|
4
|
+
|
|
5
|
+
import { uid } from '../../../shared';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Symbol used to store the context watchdog on the editor instance.
|
|
9
|
+
* Internal use only.
|
|
10
|
+
*/
|
|
11
|
+
const CONTEXT_EDITOR_WATCHDOG_SYMBOL = Symbol.for('context-editor-watchdog');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates a CKEditor 5 editor instance within a given context watchdog.
|
|
15
|
+
*
|
|
16
|
+
* @param params Parameters for editor creation.
|
|
17
|
+
* @param params.element The DOM element or data for the editor.
|
|
18
|
+
* @param params.context The context watchdog instance.
|
|
19
|
+
* @param params.creator The editor creator utility.
|
|
20
|
+
* @param params.config The editor configuration object.
|
|
21
|
+
* @returns The created editor instance.
|
|
22
|
+
*/
|
|
23
|
+
export async function createEditorInContext({ element, context, creator, config }: Attrs) {
|
|
24
|
+
const editorContextId = uid();
|
|
25
|
+
|
|
26
|
+
await context.add({
|
|
27
|
+
creator: (_element, _config) => creator.create(_element, _config),
|
|
28
|
+
id: editorContextId,
|
|
29
|
+
sourceElementOrData: element,
|
|
30
|
+
type: 'editor',
|
|
31
|
+
config,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const editor = context.getItem(editorContextId) as Editor;
|
|
35
|
+
const contextDescriptor: EditorContextDescriptor = {
|
|
36
|
+
state: 'available',
|
|
37
|
+
editorContextId,
|
|
38
|
+
context,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
(editor as any)[CONTEXT_EDITOR_WATCHDOG_SYMBOL] = contextDescriptor;
|
|
42
|
+
|
|
43
|
+
// Destroying of context is async. There can be situation when the destroy of the context
|
|
44
|
+
// and the destroy of the editor is called in parallel. It often happens during unmounting of
|
|
45
|
+
// phoenix hooks. Let's make sure that descriptor informs other components, that context is being
|
|
46
|
+
// destroyed.
|
|
47
|
+
const originalDestroy = context.destroy.bind(context);
|
|
48
|
+
context.destroy = async () => {
|
|
49
|
+
contextDescriptor.state = 'unavailable';
|
|
50
|
+
return originalDestroy();
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...contextDescriptor,
|
|
55
|
+
editor,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Retrieves the context watchdog from an editor instance, if available.
|
|
61
|
+
*
|
|
62
|
+
* @param editor The editor instance.
|
|
63
|
+
* @returns The context watchdog or null if not found.
|
|
64
|
+
*/
|
|
65
|
+
export function unwrapEditorContext(editor: Editor): EditorContextDescriptor | null {
|
|
66
|
+
if (CONTEXT_EDITOR_WATCHDOG_SYMBOL in editor) {
|
|
67
|
+
return (editor as any)[CONTEXT_EDITOR_WATCHDOG_SYMBOL];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parameters for creating an editor in a context.
|
|
75
|
+
*/
|
|
76
|
+
type Attrs = {
|
|
77
|
+
context: ContextWatchdog<Context>;
|
|
78
|
+
creator: EditorCreator;
|
|
79
|
+
element: HTMLElement;
|
|
80
|
+
config: EditorConfig;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Descriptor for an editor context.
|
|
85
|
+
*/
|
|
86
|
+
type EditorContextDescriptor = {
|
|
87
|
+
state: 'available' | 'unavailable';
|
|
88
|
+
editorContextId: string;
|
|
89
|
+
context: ContextWatchdog<Context>;
|
|
90
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './create-editor-in-context';
|
|
2
|
+
export * from './is-single-root-editor';
|
|
3
|
+
export * from './load-editor-constructor';
|
|
4
|
+
export * from './load-editor-plugins';
|
|
5
|
+
export * from './load-editor-translations';
|
|
6
|
+
export * from './normalize-custom-translations';
|
|
7
|
+
export * from './query-all-editor-ids';
|
|
8
|
+
export * from './query-editor-editables';
|
|
9
|
+
export * from './resolve-editor-config-elements-references';
|
|
10
|
+
export * from './set-editor-editable-height';
|
|
11
|
+
export * from './wrap-with-watchdog';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { EditorType } from '../typings';
|
|
4
|
+
|
|
5
|
+
import { isSingleRootEditor } from './is-single-root-editor';
|
|
6
|
+
|
|
7
|
+
describe('isSingleRootEditor', () => {
|
|
8
|
+
it('should return true for inline editor', () => {
|
|
9
|
+
expect(isSingleRootEditor('inline')).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should return true for classic editor', () => {
|
|
13
|
+
expect(isSingleRootEditor('classic')).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should return true for balloon editor', () => {
|
|
17
|
+
expect(isSingleRootEditor('balloon')).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should return false for decoupled editor', () => {
|
|
21
|
+
expect(isSingleRootEditor('decoupled')).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should return false for multiroot editor', () => {
|
|
25
|
+
expect(isSingleRootEditor('multiroot')).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should handle all valid editor types', () => {
|
|
29
|
+
const singleEditingTypes: EditorType[] = ['inline', 'classic', 'balloon', 'decoupled'];
|
|
30
|
+
const multiEditingTypes: EditorType[] = ['multiroot'];
|
|
31
|
+
|
|
32
|
+
singleEditingTypes.forEach((type) => {
|
|
33
|
+
expect(isSingleRootEditor(type)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
multiEditingTypes.forEach((type) => {
|
|
37
|
+
expect(isSingleRootEditor(type)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EditorType } from '../typings';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks if the given editor type is one of the single editing-like editors.
|
|
5
|
+
*
|
|
6
|
+
* @param editorType - The type of the editor to check.
|
|
7
|
+
* @returns `true` if the editor type is 'inline', 'classic', or 'balloon', otherwise `false`.
|
|
8
|
+
*/
|
|
9
|
+
export function isSingleRootEditor(editorType: EditorType): boolean {
|
|
10
|
+
return ['inline', 'classic', 'balloon', 'decoupled'].includes(editorType);
|
|
11
|
+
}
|