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,330 @@
|
|
|
1
|
+
import type { WaitForInteractiveResult } from '../../shared';
|
|
2
|
+
import type { EditorId, EditorLanguage, EditorPreset } from './typings';
|
|
3
|
+
import type { EditorCreator } from './utils';
|
|
4
|
+
import type { Editor } from 'ckeditor5';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
isEmptyObject,
|
|
8
|
+
waitFor,
|
|
9
|
+
waitForDOMReady,
|
|
10
|
+
waitForInteractiveAttribute,
|
|
11
|
+
} from '../../shared';
|
|
12
|
+
import { ContextsRegistry } from '../context';
|
|
13
|
+
import { EditorsRegistry } from './editors-registry';
|
|
14
|
+
import {
|
|
15
|
+
createDispatchEditorRootsChangeEventPlugin,
|
|
16
|
+
createSyncEditorWithInputPlugin,
|
|
17
|
+
} from './plugins';
|
|
18
|
+
import {
|
|
19
|
+
createEditorInContext,
|
|
20
|
+
isSingleRootEditor,
|
|
21
|
+
loadAllEditorTranslations,
|
|
22
|
+
loadEditorConstructor,
|
|
23
|
+
loadEditorPlugins,
|
|
24
|
+
normalizeCustomTranslations,
|
|
25
|
+
queryEditablesElements,
|
|
26
|
+
queryEditablesSnapshotContent,
|
|
27
|
+
resolveEditorConfigElementReferences,
|
|
28
|
+
resolveEditorConfigTranslations,
|
|
29
|
+
setEditorEditableHeight,
|
|
30
|
+
unwrapEditorContext,
|
|
31
|
+
unwrapEditorWatchdog,
|
|
32
|
+
wrapWithWatchdog,
|
|
33
|
+
} from './utils';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The Blazor hook that manages the lifecycle of CKEditor5 instances.
|
|
37
|
+
*/
|
|
38
|
+
export class EditorComponentElement extends HTMLElement {
|
|
39
|
+
/**
|
|
40
|
+
* The promise that resolves to the editor instance.
|
|
41
|
+
*/
|
|
42
|
+
private editorPromise: Promise<Editor> | null = null;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Wait result for the interactive attribute.
|
|
46
|
+
*/
|
|
47
|
+
private interactiveWait?: WaitForInteractiveResult;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Mounts the editor component.
|
|
51
|
+
*/
|
|
52
|
+
async connectedCallback(): Promise<void> {
|
|
53
|
+
await waitForDOMReady();
|
|
54
|
+
|
|
55
|
+
// By default, components do not bootstrap from web components.
|
|
56
|
+
// They bootstrap only when they receive the data-cke-interactive flag, which the interop sets.
|
|
57
|
+
// This is a fallback for situations where CKEditor 5 is rendered on a non-interactive page.
|
|
58
|
+
this.interactiveWait = waitForInteractiveAttribute(this);
|
|
59
|
+
|
|
60
|
+
// Let's start preloading the editor constructor. We are still waiting for the interactive attribute, but
|
|
61
|
+
// at least we will have the constructor ready when it arrives. It makes no difference which editor type is being
|
|
62
|
+
// preloaded as they are loaded from the same package, so let's just preload the classic editor constructor.
|
|
63
|
+
void loadEditorConstructor('classic');
|
|
64
|
+
|
|
65
|
+
await this.interactiveWait.promise;
|
|
66
|
+
await this.initializeEditor();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Initializes the editor instance.
|
|
71
|
+
*/
|
|
72
|
+
private async initializeEditor(): Promise<void> {
|
|
73
|
+
const editorId = this.getAttribute('data-cke-editor-id')!;
|
|
74
|
+
|
|
75
|
+
EditorsRegistry.the.resetErrors(editorId);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
this.style.display = 'block';
|
|
79
|
+
this.editorPromise = this.createEditor();
|
|
80
|
+
|
|
81
|
+
const editor = await this.editorPromise;
|
|
82
|
+
|
|
83
|
+
// Do not even try to broadcast about the registration of the editor
|
|
84
|
+
// if hook was immediately destroyed.
|
|
85
|
+
/* v8 ignore else -- @preserve */
|
|
86
|
+
if (this.isConnected) {
|
|
87
|
+
EditorsRegistry.the.register(editorId, editor);
|
|
88
|
+
|
|
89
|
+
editor.once('destroy', () => {
|
|
90
|
+
if (EditorsRegistry.the.hasItem(editorId)) {
|
|
91
|
+
EditorsRegistry.the.unregister(editorId);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (error: any) {
|
|
97
|
+
/* v8 ignore start -- @preserve */
|
|
98
|
+
console.error(`Error initializing CKEditor5 instance with ID "${editorId}":`, error);
|
|
99
|
+
this.editorPromise = null;
|
|
100
|
+
EditorsRegistry.the.error(editorId, error);
|
|
101
|
+
/* v8 ignore end */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Destroys the editor instance when the component is destroyed.
|
|
107
|
+
* This is important to prevent memory leaks and ensure that the editor is properly cleaned up.
|
|
108
|
+
*/
|
|
109
|
+
async disconnectedCallback() {
|
|
110
|
+
// Disconnect the observer if present.
|
|
111
|
+
this.interactiveWait?.disconnect();
|
|
112
|
+
|
|
113
|
+
// Let's hide the element during destruction to prevent flickering.
|
|
114
|
+
this.style.display = 'none';
|
|
115
|
+
|
|
116
|
+
// Let's wait for the mounted promise to resolve before proceeding with destruction.
|
|
117
|
+
try {
|
|
118
|
+
const editor = await this.editorPromise;
|
|
119
|
+
|
|
120
|
+
/* v8 ignore next -- @preserve */
|
|
121
|
+
if (!editor) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const editorContext = unwrapEditorContext(editor);
|
|
126
|
+
const watchdog = unwrapEditorWatchdog(editor);
|
|
127
|
+
|
|
128
|
+
if (editorContext) {
|
|
129
|
+
// If context is present, make sure it's not in unmounting phase, as it'll kill the editors.
|
|
130
|
+
// If it's being destroyed, don't do anything, as the context will take care of it.
|
|
131
|
+
if (editorContext.state !== 'unavailable') {
|
|
132
|
+
await editorContext.context.remove(editorContext.editorContextId);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (watchdog) {
|
|
136
|
+
await watchdog.destroy();
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
await editor.destroy();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
this.editorPromise = null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Creates the CKEditor instance.
|
|
149
|
+
*/
|
|
150
|
+
private async createEditor() {
|
|
151
|
+
const editorId = this.getAttribute('data-cke-editor-id')!;
|
|
152
|
+
const preset = JSON.parse(this.getAttribute('data-cke-preset')!) as EditorPreset;
|
|
153
|
+
const contextId = this.getAttribute('data-cke-context-id');
|
|
154
|
+
const editableHeight = this.getAttribute('data-cke-editable-height') ? Number.parseInt(this.getAttribute('data-cke-editable-height')!, 10) : null;
|
|
155
|
+
const saveDebounceMs = Number.parseInt(this.getAttribute('data-cke-save-debounce-ms')!, 10);
|
|
156
|
+
const language = JSON.parse(this.getAttribute('data-cke-language')!) as EditorLanguage;
|
|
157
|
+
const watchdog = this.hasAttribute('data-cke-watchdog');
|
|
158
|
+
const content = JSON.parse(this.getAttribute('data-cke-content')!) as Record<string, string>;
|
|
159
|
+
|
|
160
|
+
const {
|
|
161
|
+
customTranslations,
|
|
162
|
+
editorType,
|
|
163
|
+
licenseKey,
|
|
164
|
+
config: { plugins, ...config },
|
|
165
|
+
} = preset;
|
|
166
|
+
|
|
167
|
+
// Wrap editor creator with watchdog if needed.
|
|
168
|
+
let Constructor: EditorCreator = await loadEditorConstructor(editorType);
|
|
169
|
+
const context = await (
|
|
170
|
+
contextId
|
|
171
|
+
? ContextsRegistry.the.waitFor(contextId)
|
|
172
|
+
: null
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Do not use editor specific watchdog if context is attached, as the context is by default protected.
|
|
176
|
+
if (watchdog && !context) {
|
|
177
|
+
const wrapped = await wrapWithWatchdog(Constructor);
|
|
178
|
+
|
|
179
|
+
({ Constructor } = wrapped);
|
|
180
|
+
wrapped.watchdog.on('restart', () => {
|
|
181
|
+
const newInstance = wrapped.watchdog.editor!;
|
|
182
|
+
|
|
183
|
+
this.editorPromise = Promise.resolve(newInstance);
|
|
184
|
+
|
|
185
|
+
EditorsRegistry.the.register(editorId, newInstance);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const { loadedPlugins, hasPremium } = await loadEditorPlugins(plugins);
|
|
190
|
+
|
|
191
|
+
loadedPlugins.push(
|
|
192
|
+
await createDispatchEditorRootsChangeEventPlugin({
|
|
193
|
+
saveDebounceMs,
|
|
194
|
+
editorId,
|
|
195
|
+
targetElement: this,
|
|
196
|
+
}),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
if (isSingleRootEditor(editorType)) {
|
|
200
|
+
loadedPlugins.push(
|
|
201
|
+
await createSyncEditorWithInputPlugin(saveDebounceMs),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Mix custom translations with loaded translations.
|
|
206
|
+
const loadedTranslations = await loadAllEditorTranslations(language, hasPremium);
|
|
207
|
+
const mixedTranslations = [
|
|
208
|
+
...loadedTranslations,
|
|
209
|
+
normalizeCustomTranslations(customTranslations || {}),
|
|
210
|
+
]
|
|
211
|
+
.filter(translations => !isEmptyObject(translations));
|
|
212
|
+
|
|
213
|
+
// Let's query all elements, and create basic configuration.
|
|
214
|
+
let initialData: string | Record<string, string> = {
|
|
215
|
+
...content,
|
|
216
|
+
...queryEditablesSnapshotContent(editorId),
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
if (isSingleRootEditor(editorType)) {
|
|
220
|
+
initialData = initialData['main'] || '';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Depending of the editor type, and parent lookup for nearest context or initialize it without it.
|
|
224
|
+
const editor = await (async () => {
|
|
225
|
+
let sourceElementOrData: HTMLElement | Record<string, HTMLElement> = queryEditablesElements(editorId);
|
|
226
|
+
|
|
227
|
+
// Handle special case when user specified `initialData` of several root elements, but editable components
|
|
228
|
+
// are not yet present in the DOM. In other words - editor is initialized before attaching root elements.
|
|
229
|
+
if (!sourceElementOrData['main']) {
|
|
230
|
+
const requiredRoots = (
|
|
231
|
+
isSingleRootEditor(editorType)
|
|
232
|
+
? ['main']
|
|
233
|
+
: Object.keys(initialData as Record<string, string>)
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
if (!checkIfAllRootsArePresent(sourceElementOrData, requiredRoots)) {
|
|
237
|
+
sourceElementOrData = await waitForAllRootsToBePresent(editorId, requiredRoots);
|
|
238
|
+
initialData = {
|
|
239
|
+
...content,
|
|
240
|
+
...queryEditablesSnapshotContent(editorId),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// If single root editor, unwrap the element from the object.
|
|
246
|
+
if (isSingleRootEditor(editorType) && 'main' in sourceElementOrData) {
|
|
247
|
+
sourceElementOrData = sourceElementOrData['main'];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Construct parsed config. First resolve DOM element references in the provided configuration.
|
|
251
|
+
let resolvedConfig = resolveEditorConfigElementReferences(config);
|
|
252
|
+
|
|
253
|
+
// Then resolve translation references in the provided configuration, using the mixed translations.
|
|
254
|
+
resolvedConfig = resolveEditorConfigTranslations([...mixedTranslations].reverse(), language.ui, resolvedConfig);
|
|
255
|
+
|
|
256
|
+
// Construct parsed config.
|
|
257
|
+
const parsedConfig = {
|
|
258
|
+
...resolvedConfig,
|
|
259
|
+
initialData,
|
|
260
|
+
licenseKey,
|
|
261
|
+
plugins: loadedPlugins,
|
|
262
|
+
language,
|
|
263
|
+
...mixedTranslations.length && {
|
|
264
|
+
translations: mixedTranslations,
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
if (!context || !(sourceElementOrData instanceof HTMLElement)) {
|
|
269
|
+
return Constructor.create(sourceElementOrData as any, parsedConfig);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const result = await createEditorInContext({
|
|
273
|
+
context,
|
|
274
|
+
element: sourceElementOrData,
|
|
275
|
+
creator: Constructor,
|
|
276
|
+
config: parsedConfig,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
return result.editor;
|
|
280
|
+
})();
|
|
281
|
+
|
|
282
|
+
if (isSingleRootEditor(editorType) && editableHeight) {
|
|
283
|
+
setEditorEditableHeight(editor, editableHeight);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return editor;
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Checks if all required root elements are present in the elements object.
|
|
292
|
+
*
|
|
293
|
+
* @param elements The elements object mapping root IDs to HTMLElements.
|
|
294
|
+
* @param requiredRoots The list of required root IDs.
|
|
295
|
+
* @returns True if all required roots are present, false otherwise.
|
|
296
|
+
*/
|
|
297
|
+
function checkIfAllRootsArePresent(elements: Record<string, HTMLElement>, requiredRoots: string[]): boolean {
|
|
298
|
+
return requiredRoots.every(rootId => elements[rootId]);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Waits for all required root elements to be present in the DOM.
|
|
303
|
+
*
|
|
304
|
+
* @param editorId The editor's ID.
|
|
305
|
+
* @param requiredRoots The list of required root IDs.
|
|
306
|
+
* @returns A promise that resolves to the record of root elements.
|
|
307
|
+
*/
|
|
308
|
+
async function waitForAllRootsToBePresent(
|
|
309
|
+
editorId: EditorId,
|
|
310
|
+
requiredRoots: string[],
|
|
311
|
+
): Promise<Record<string, HTMLElement>> {
|
|
312
|
+
return waitFor(
|
|
313
|
+
() => {
|
|
314
|
+
const elements = queryEditablesElements(editorId) as unknown as Record<string, HTMLElement>;
|
|
315
|
+
|
|
316
|
+
if (!checkIfAllRootsArePresent(elements, requiredRoots)) {
|
|
317
|
+
throw new Error(
|
|
318
|
+
'It looks like not all required root elements are present yet.\n'
|
|
319
|
+
+ '* If you want to wait for them, ensure they are registered before editor initialization.\n'
|
|
320
|
+
+ '* If you want lazy initialize roots, consider removing root values from the `initialData` config '
|
|
321
|
+
+ 'and assign initial data in editable components.\n'
|
|
322
|
+
+ `Missing roots: ${requiredRoots.filter(rootId => !elements[rootId]).join(', ')}.`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return elements;
|
|
327
|
+
},
|
|
328
|
+
{ timeOutAfter: 2000, retryAfter: 100 },
|
|
329
|
+
);
|
|
330
|
+
}
|
|
@@ -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,76 @@
|
|
|
1
|
+
import type { Editor, PluginConstructor } from 'ckeditor5';
|
|
2
|
+
|
|
3
|
+
import { debounce } from '../../../shared';
|
|
4
|
+
import { getEditorRootsValues } from '../utils';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a DispatchEditorRootsChangeEvent plugin class.
|
|
8
|
+
*/
|
|
9
|
+
export async function createDispatchEditorRootsChangeEventPlugin(
|
|
10
|
+
{
|
|
11
|
+
saveDebounceMs,
|
|
12
|
+
editorId,
|
|
13
|
+
targetElement,
|
|
14
|
+
}: {
|
|
15
|
+
saveDebounceMs: number;
|
|
16
|
+
editorId: string;
|
|
17
|
+
targetElement: HTMLElement;
|
|
18
|
+
},
|
|
19
|
+
): Promise<PluginConstructor> {
|
|
20
|
+
const { Plugin } = await import('ckeditor5');
|
|
21
|
+
|
|
22
|
+
return class DispatchEditorRootsChangeEvent extends Plugin {
|
|
23
|
+
/**
|
|
24
|
+
* The name of the plugin.
|
|
25
|
+
*/
|
|
26
|
+
static get pluginName() {
|
|
27
|
+
return 'DispatchEditorRootsChangeEvent' as const;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Initializes the plugin.
|
|
32
|
+
*/
|
|
33
|
+
public afterInit(): void {
|
|
34
|
+
const { editor } = this;
|
|
35
|
+
const sync = debounce(saveDebounceMs, this.dispatch);
|
|
36
|
+
|
|
37
|
+
editor.model.document.on('change:data', sync);
|
|
38
|
+
editor.once('ready', this.dispatch);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Dispatches a custom event with all roots data.
|
|
43
|
+
*/
|
|
44
|
+
private dispatch = (): void => {
|
|
45
|
+
const { editor } = this;
|
|
46
|
+
|
|
47
|
+
targetElement.dispatchEvent(
|
|
48
|
+
new CKEditor5ChangeDataEvent({
|
|
49
|
+
editorId,
|
|
50
|
+
editor,
|
|
51
|
+
roots: getEditorRootsValues(editor),
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A custom event dispatched by the DispatchEditorRootsChangeEvent plugin, containing all editor roots data.
|
|
60
|
+
*/
|
|
61
|
+
export class CKEditor5ChangeDataEvent extends CustomEvent<CKEditor5ChangeDataEventPayload> {
|
|
62
|
+
static readonly EVENT_NAME = 'ckeditor5:change:data';
|
|
63
|
+
|
|
64
|
+
constructor(detail: CKEditor5ChangeDataEventPayload) {
|
|
65
|
+
super(CKEditor5ChangeDataEvent.EVENT_NAME, {
|
|
66
|
+
detail,
|
|
67
|
+
bubbles: true,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type CKEditor5ChangeDataEventPayload = {
|
|
73
|
+
editorId: string;
|
|
74
|
+
editor: Editor;
|
|
75
|
+
roots: Record<string, string>;
|
|
76
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
/* v8 ignore else -- @preserve */
|
|
59
|
+
if (this.input) {
|
|
60
|
+
const newValue = this.editor.getData();
|
|
61
|
+
|
|
62
|
+
this.input.value = newValue;
|
|
63
|
+
this.input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Destroys the plugin.
|
|
69
|
+
*/
|
|
70
|
+
public override destroy(): void {
|
|
71
|
+
if (this.form) {
|
|
72
|
+
this.form.removeEventListener('submit', this.sync);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.input = null;
|
|
76
|
+
this.form = null;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -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
|
+
};
|