ckeditor5 1.8.0 → 1.10.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.
- checksums.yaml +4 -4
- data/README.md +82 -7
- data/lib/ckeditor5/rails/assets/assets_bundle_html_serializer.rb +3 -3
- data/lib/ckeditor5/rails/assets/webcomponent_bundle.rb +22 -0
- data/lib/ckeditor5/rails/assets/webcomponents/components/context.mjs +113 -0
- data/lib/ckeditor5/rails/assets/webcomponents/components/editable.mjs +113 -0
- data/lib/ckeditor5/rails/assets/{webcomponent.mjs → webcomponents/components/editor.mjs} +164 -349
- data/lib/ckeditor5/rails/assets/webcomponents/components/ui-part.mjs +26 -0
- data/lib/ckeditor5/rails/assets/webcomponents/utils.mjs +155 -0
- data/lib/ckeditor5/rails/cdn/helpers.rb +3 -11
- data/lib/ckeditor5/rails/context/helpers.rb +13 -0
- data/lib/ckeditor5/rails/context/props.rb +30 -0
- data/lib/ckeditor5/rails/editor/helpers.rb +7 -14
- data/lib/ckeditor5/rails/helpers.rb +2 -2
- data/lib/ckeditor5/rails/presets/preset_builder.rb +17 -1
- data/lib/ckeditor5/rails/version.rb +1 -1
- metadata +10 -4
- data/lib/ckeditor5/rails/cloud/helpers.rb +0 -13
@@ -1,26 +1,3 @@
|
|
1
|
-
/**
|
2
|
-
* Custom web component for integrating CKEditor 5 into web applications.
|
3
|
-
*
|
4
|
-
* @class
|
5
|
-
* @extends HTMLElement
|
6
|
-
*
|
7
|
-
* @property {import('ckeditor5').Editor|null} instance - The current CKEditor instance
|
8
|
-
* @property {Record<string, HTMLElement>} editables - Object containing editable elements
|
9
|
-
*
|
10
|
-
* @fires editor-ready - Fired when editor is initialized with the editor instance as detail
|
11
|
-
* @fires editor-error - Fired when initialization fails with the error as detail
|
12
|
-
*
|
13
|
-
* @example
|
14
|
-
* // Basic usage with Classic Editor
|
15
|
-
* <ckeditor-component type="ClassicEditor" config='{"toolbar": ["bold", "italic"]}'>
|
16
|
-
* </ckeditor-component>
|
17
|
-
*
|
18
|
-
* // Multiroot editor with multiple editables
|
19
|
-
* <ckeditor-component type="MultirootEditor">
|
20
|
-
* <ckeditor-editable-component name="title">Title content</ckeditor-editable-component>
|
21
|
-
* <ckeditor-editable-component name="content">Main content</ckeditor-editable-component>
|
22
|
-
* </ckeditor-component>
|
23
|
-
*/
|
24
1
|
class CKEditorComponent extends HTMLElement {
|
25
2
|
/**
|
26
3
|
* List of attributes that trigger updates when changed.
|
@@ -57,19 +34,100 @@ class CKEditorComponent extends HTMLElement {
|
|
57
34
|
/** @type {String} Initial HTML passed to component */
|
58
35
|
#initialHTML = '';
|
59
36
|
|
37
|
+
/** @type {CKEditorContextComponent|null} */
|
38
|
+
#context = null;
|
39
|
+
|
40
|
+
/** @type {String} ID of editor within context */
|
41
|
+
#contextEditorId = null;
|
42
|
+
|
43
|
+
/** @type {(event: CustomEvent) => void} Event handler for editor change */
|
44
|
+
get oneditorchange() {
|
45
|
+
return this.#getEventHandler('editorchange');
|
46
|
+
}
|
47
|
+
|
48
|
+
set oneditorchange(handler) {
|
49
|
+
this.#setEventHandler('editorchange', handler);
|
50
|
+
}
|
51
|
+
|
52
|
+
/** @type {(event: CustomEvent) => void} Event handler for editor ready */
|
53
|
+
get oneditorready() {
|
54
|
+
return this.#getEventHandler('editorready');
|
55
|
+
}
|
56
|
+
|
57
|
+
set oneditorready(handler) {
|
58
|
+
this.#setEventHandler('editorready', handler);
|
59
|
+
}
|
60
|
+
|
61
|
+
/** @type {(event: CustomEvent) => void} Event handler for editor error */
|
62
|
+
get oneditorerror() {
|
63
|
+
return this.#getEventHandler('editorerror');
|
64
|
+
}
|
65
|
+
|
66
|
+
set oneditorerror(handler) {
|
67
|
+
this.#setEventHandler('editorerror', handler);
|
68
|
+
}
|
69
|
+
|
70
|
+
/**
|
71
|
+
* Gets event handler function from attribute or property
|
72
|
+
*
|
73
|
+
* @private
|
74
|
+
* @param {string} name - Event name without 'on' prefix
|
75
|
+
* @returns {Function|null} Event handler or null
|
76
|
+
*/
|
77
|
+
#getEventHandler(name) {
|
78
|
+
if (this.hasAttribute(`on${name}`)) {
|
79
|
+
const handler = this.getAttribute(`on${name}`);
|
80
|
+
|
81
|
+
if (!isSafeKey(handler)) {
|
82
|
+
throw new Error(`Unsafe event handler attribute value: ${handler}`);
|
83
|
+
}
|
84
|
+
|
85
|
+
return window[handler] || new Function('event', handler);
|
86
|
+
}
|
87
|
+
return this[`#${name}Handler`];
|
88
|
+
}
|
89
|
+
|
90
|
+
/**
|
91
|
+
* Sets event handler function
|
92
|
+
*
|
93
|
+
* @private
|
94
|
+
* @param {string} name - Event name without 'on' prefix
|
95
|
+
* @param {Function|string|null} handler - Event handler
|
96
|
+
*/
|
97
|
+
#setEventHandler(name, handler) {
|
98
|
+
if (typeof handler === 'string') {
|
99
|
+
this.setAttribute(`on${name}`, handler);
|
100
|
+
} else {
|
101
|
+
this.removeAttribute(`on${name}`);
|
102
|
+
this[`#${name}Handler`] = handler;
|
103
|
+
}
|
104
|
+
}
|
105
|
+
|
60
106
|
/**
|
61
107
|
* Lifecycle callback when element is connected to DOM
|
62
108
|
* Initializes the editor when DOM is ready
|
63
109
|
* @protected
|
64
110
|
*/
|
65
111
|
connectedCallback() {
|
112
|
+
this.#context = this.closest('ckeditor-context-component');
|
66
113
|
this.#initialHTML = this.innerHTML;
|
67
114
|
|
68
115
|
try {
|
69
|
-
execIfDOMReady(() =>
|
116
|
+
execIfDOMReady(async () => {
|
117
|
+
if (this.#context) {
|
118
|
+
await this.#context.instancePromise.promise;
|
119
|
+
this.#context.registerEditor(this);
|
120
|
+
}
|
121
|
+
|
122
|
+
await this.reinitializeEditor();
|
123
|
+
});
|
70
124
|
} catch (error) {
|
71
125
|
console.error('Failed to initialize editor:', error);
|
72
|
-
|
126
|
+
|
127
|
+
const event = new CustomEvent('editor-error', { detail: error });
|
128
|
+
|
129
|
+
this.dispatchEvent(event);
|
130
|
+
this.oneditorerror?.(event);
|
73
131
|
}
|
74
132
|
}
|
75
133
|
|
@@ -84,7 +142,7 @@ class CKEditorComponent extends HTMLElement {
|
|
84
142
|
if (oldValue !== null &&
|
85
143
|
oldValue !== newValue &&
|
86
144
|
CKEditorComponent.observedAttributes.includes(name) && this.isConnected) {
|
87
|
-
await this
|
145
|
+
await this.reinitializeEditor();
|
88
146
|
}
|
89
147
|
}
|
90
148
|
|
@@ -94,9 +152,12 @@ class CKEditorComponent extends HTMLElement {
|
|
94
152
|
* @protected
|
95
153
|
*/
|
96
154
|
async disconnectedCallback() {
|
155
|
+
if (this.#context) {
|
156
|
+
this.#context.unregisterEditor(this);
|
157
|
+
}
|
158
|
+
|
97
159
|
try {
|
98
|
-
await this
|
99
|
-
await this.watchdog?.destroy();
|
160
|
+
await this.#destroy();
|
100
161
|
} catch (error) {
|
101
162
|
console.error('Failed to destroy editor:', error);
|
102
163
|
}
|
@@ -119,6 +180,7 @@ class CKEditorComponent extends HTMLElement {
|
|
119
180
|
|
120
181
|
/**
|
121
182
|
* Determines appropriate editor element tag based on editor type
|
183
|
+
*
|
122
184
|
* @private
|
123
185
|
* @returns {string} HTML tag name to use
|
124
186
|
*/
|
@@ -133,57 +195,25 @@ class CKEditorComponent extends HTMLElement {
|
|
133
195
|
}
|
134
196
|
|
135
197
|
/**
|
136
|
-
*
|
137
|
-
* Looks for objects with { $element: "selector" } format and replaces them with actual elements.
|
198
|
+
* Gets the CKEditor context instance if available.
|
138
199
|
*
|
139
200
|
* @private
|
140
|
-
* @
|
141
|
-
* @returns {Object} Processed configuration object with resolved element references
|
201
|
+
* @returns {import('ckeditor5').ContextWatchdog|null}
|
142
202
|
*/
|
143
|
-
#
|
144
|
-
|
145
|
-
|
146
|
-
}
|
147
|
-
|
148
|
-
if (Array.isArray(obj)) {
|
149
|
-
return obj.map(item => this.#resolveElementReferences(item));
|
150
|
-
}
|
151
|
-
|
152
|
-
const result = Object.create(null);
|
153
|
-
|
154
|
-
for (const key of Object.getOwnPropertyNames(obj)) {
|
155
|
-
if (!isSafeKey(key)) {
|
156
|
-
console.warn(`Suspicious key "${key}" detected in config, skipping`);
|
157
|
-
continue;
|
158
|
-
}
|
159
|
-
|
160
|
-
const value = obj[key];
|
161
|
-
|
162
|
-
if (value && typeof value === 'object') {
|
163
|
-
if (value.$element) {
|
164
|
-
const selector = value.$element;
|
165
|
-
|
166
|
-
if (typeof selector !== 'string') {
|
167
|
-
console.warn(`Invalid selector type for "${key}", expected string`);
|
168
|
-
continue;
|
169
|
-
}
|
170
|
-
|
171
|
-
const element = document.querySelector(selector);
|
172
|
-
|
173
|
-
if (!element) {
|
174
|
-
console.warn(`Element not found for selector: ${selector}`);
|
175
|
-
}
|
203
|
+
get #contextWatchdog() {
|
204
|
+
return this.#context?.instance;
|
205
|
+
}
|
176
206
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
}
|
207
|
+
/**
|
208
|
+
* Destroys the editor instance and watchdog if available
|
209
|
+
*/
|
210
|
+
async #destroy() {
|
211
|
+
if (this.#contextEditorId) {
|
212
|
+
await this.#contextWatchdog.remove(this.#contextEditorId);
|
184
213
|
}
|
185
214
|
|
186
|
-
|
215
|
+
await this.instance?.destroy();
|
216
|
+
await this.watchdog?.destroy();
|
187
217
|
}
|
188
218
|
|
189
219
|
/**
|
@@ -195,7 +225,7 @@ class CKEditorComponent extends HTMLElement {
|
|
195
225
|
#getConfig() {
|
196
226
|
const config = JSON.parse(this.getAttribute('config') || '{}');
|
197
227
|
|
198
|
-
return
|
228
|
+
return resolveElementReferences(config);
|
199
229
|
}
|
200
230
|
|
201
231
|
/**
|
@@ -232,13 +262,27 @@ class CKEditorComponent extends HTMLElement {
|
|
232
262
|
plugins,
|
233
263
|
};
|
234
264
|
|
235
|
-
console.warn('Initializing CKEditor with:', { config, watchdog: this.hasWatchdog() });
|
265
|
+
console.warn('Initializing CKEditor with:', { config, watchdog: this.hasWatchdog(), context: this.#context });
|
236
266
|
|
237
267
|
// Initialize watchdog if needed
|
238
268
|
let watchdog = null;
|
239
269
|
let instance = null;
|
240
|
-
|
241
|
-
|
270
|
+
let contextId = null;
|
271
|
+
|
272
|
+
if (this.#context) {
|
273
|
+
contextId = uid();
|
274
|
+
|
275
|
+
await this.#contextWatchdog.add( {
|
276
|
+
creator: (_element, _config) => Editor.create(_element, _config),
|
277
|
+
id: contextId,
|
278
|
+
sourceElementOrData: content,
|
279
|
+
type: 'editor',
|
280
|
+
config,
|
281
|
+
} );
|
282
|
+
|
283
|
+
instance = this.#contextWatchdog.getItem(contextId);
|
284
|
+
} else if (this.hasWatchdog()) {
|
285
|
+
// Let's create use with plain watchdog.
|
242
286
|
const { EditorWatchdog } = await import('ckeditor5');
|
243
287
|
const watchdog = new EditorWatchdog(Editor);
|
244
288
|
|
@@ -246,12 +290,12 @@ class CKEditorComponent extends HTMLElement {
|
|
246
290
|
|
247
291
|
instance = watchdog.editor;
|
248
292
|
} else {
|
293
|
+
// Let's create the editor without watchdog.
|
249
294
|
instance = await Editor.create(content, config);
|
250
295
|
}
|
251
296
|
|
252
|
-
this.dispatchEvent(new CustomEvent('editor-ready', { detail: instance }));
|
253
|
-
|
254
297
|
return {
|
298
|
+
contextId,
|
255
299
|
instance,
|
256
300
|
watchdog,
|
257
301
|
};
|
@@ -263,11 +307,12 @@ class CKEditorComponent extends HTMLElement {
|
|
263
307
|
* @private
|
264
308
|
* @returns {Promise<void>}
|
265
309
|
*/
|
266
|
-
async
|
310
|
+
async reinitializeEditor() {
|
267
311
|
if (this.instance) {
|
268
312
|
this.instancePromise = Promise.withResolvers();
|
269
313
|
|
270
|
-
await this
|
314
|
+
await this.#destroy();
|
315
|
+
|
271
316
|
this.instance = null;
|
272
317
|
}
|
273
318
|
|
@@ -288,21 +333,58 @@ class CKEditorComponent extends HTMLElement {
|
|
288
333
|
}
|
289
334
|
|
290
335
|
try {
|
291
|
-
const { watchdog, instance } = await this.#initializeEditor(this.editables || this.#getConfig().initialData || '');
|
336
|
+
const { watchdog, instance, contextId } = await this.#initializeEditor(this.editables || this.#getConfig().initialData || '');
|
292
337
|
|
293
338
|
this.watchdog = watchdog;
|
294
339
|
this.instance = instance;
|
340
|
+
this.#contextEditorId = contextId;
|
295
341
|
|
296
342
|
this.#setupContentSync();
|
297
343
|
this.#setupEditableHeight();
|
344
|
+
this.#setupDataChangeListener();
|
298
345
|
|
299
346
|
this.instancePromise.resolve(this.instance);
|
347
|
+
|
348
|
+
// Broadcast editor ready event
|
349
|
+
const event = new CustomEvent('editor-ready', { detail: this.instance });
|
350
|
+
|
351
|
+
this.dispatchEvent(event);
|
352
|
+
this.oneditorready?.(event);
|
300
353
|
} catch (err) {
|
301
354
|
this.instancePromise.reject(err);
|
302
355
|
throw err;
|
303
356
|
}
|
304
357
|
}
|
305
358
|
|
359
|
+
/**
|
360
|
+
* Sets up data change listener that broadcasts content changes
|
361
|
+
*
|
362
|
+
* @private
|
363
|
+
*/
|
364
|
+
#setupDataChangeListener() {
|
365
|
+
const getRootContent = rootName => this.instance.getData({ rootName });
|
366
|
+
const getAllRoots = () =>
|
367
|
+
this.instance.model.document
|
368
|
+
.getRootNames()
|
369
|
+
.reduce((acc, rootName) => ({
|
370
|
+
...acc,
|
371
|
+
[rootName]: getRootContent(rootName)
|
372
|
+
}), {});
|
373
|
+
|
374
|
+
this.instance?.model.document.on('change:data', () => {
|
375
|
+
const event = new CustomEvent('editor-change', {
|
376
|
+
detail: {
|
377
|
+
editor: this.instance,
|
378
|
+
data: getAllRoots(),
|
379
|
+
},
|
380
|
+
bubbles: true
|
381
|
+
});
|
382
|
+
|
383
|
+
this.dispatchEvent(event);
|
384
|
+
this.oneditorchange?.(event);
|
385
|
+
});
|
386
|
+
}
|
387
|
+
|
306
388
|
/**
|
307
389
|
* Checks if current editor is classic type
|
308
390
|
*
|
@@ -511,15 +593,6 @@ class CKEditorComponent extends HTMLElement {
|
|
511
593
|
}
|
512
594
|
}
|
513
595
|
|
514
|
-
/**
|
515
|
-
* Tracks and manages editable roots for CKEditor MultiRoot editor.
|
516
|
-
* Provides a proxy-based API for dynamically managing editable elements with automatic
|
517
|
-
* attachment/detachment of editor roots.
|
518
|
-
*
|
519
|
-
* @class
|
520
|
-
* @property {CKEditorComponent} #editorElement - Reference to parent editor component
|
521
|
-
* @property {Record<string, HTMLElement>} #editables - Map of tracked editable elements
|
522
|
-
*/
|
523
596
|
class CKEditorMultiRootEditablesTracker {
|
524
597
|
#editorElement;
|
525
598
|
#editables;
|
@@ -638,262 +711,4 @@ class CKEditorMultiRootEditablesTracker {
|
|
638
711
|
}
|
639
712
|
}
|
640
713
|
|
641
|
-
/**
|
642
|
-
* Custom HTML element representing an editable region for CKEditor.
|
643
|
-
* Must be used as a child of ckeditor-component element.
|
644
|
-
*
|
645
|
-
* @customElement ckeditor-editable-component
|
646
|
-
* @extends HTMLElement
|
647
|
-
*
|
648
|
-
* @property {string} name - The name of the editable region, accessed via getAttribute
|
649
|
-
* @property {HTMLDivElement} editableElement - The div element containing editable content
|
650
|
-
*
|
651
|
-
* @fires connectedCallback - When the element is added to the DOM
|
652
|
-
* @fires attributeChangedCallback - When element attributes change
|
653
|
-
* @fires disconnectedCallback - When the element is removed from the DOM
|
654
|
-
*
|
655
|
-
* @throws {Error} Throws error if not used as child of ckeditor-component
|
656
|
-
*
|
657
|
-
* @example
|
658
|
-
* <ckeditor-component>
|
659
|
-
* <ckeditor-editable-component name="main">
|
660
|
-
* Content goes here
|
661
|
-
* </ckeditor-editable-component>
|
662
|
-
* </ckeditor-component>
|
663
|
-
*/
|
664
|
-
class CKEditorEditableComponent extends HTMLElement {
|
665
|
-
/**
|
666
|
-
* List of attributes that trigger updates when changed
|
667
|
-
*
|
668
|
-
* @static
|
669
|
-
* @returns {string[]} Array of attribute names to observe
|
670
|
-
*/
|
671
|
-
static get observedAttributes() {
|
672
|
-
return ['name'];
|
673
|
-
}
|
674
|
-
|
675
|
-
/**
|
676
|
-
* Gets the name of this editable region
|
677
|
-
*
|
678
|
-
* @returns {string} The name attribute value
|
679
|
-
*/
|
680
|
-
get name() {
|
681
|
-
// The default value is set mainly for decoupled editors where the name is not required.
|
682
|
-
return this.getAttribute('name') || 'editable';
|
683
|
-
}
|
684
|
-
|
685
|
-
/**
|
686
|
-
* Gets the actual editable DOM element
|
687
|
-
* @returns {HTMLDivElement|null} The div element containing editable content
|
688
|
-
*/
|
689
|
-
get editableElement() {
|
690
|
-
return this.querySelector('div');
|
691
|
-
}
|
692
|
-
|
693
|
-
/**
|
694
|
-
* Lifecycle callback when element is added to DOM
|
695
|
-
* Sets up the editable element and registers it with the parent editor
|
696
|
-
*
|
697
|
-
* @throws {Error} If not used as child of ckeditor-component
|
698
|
-
*/
|
699
|
-
connectedCallback() {
|
700
|
-
execIfDOMReady(() => {
|
701
|
-
const editorComponent = this.#queryEditorElement();
|
702
|
-
|
703
|
-
if (!editorComponent ) {
|
704
|
-
throw new Error('ckeditor-editable-component must be a child of ckeditor-component');
|
705
|
-
}
|
706
|
-
|
707
|
-
this.innerHTML = `<div>${this.innerHTML}</div>`;
|
708
|
-
this.style.display = 'block';
|
709
|
-
|
710
|
-
if (editorComponent.isDecoupled()) {
|
711
|
-
editorComponent.runAfterEditorReady(editor => {
|
712
|
-
this.appendChild(editor.ui.view[this.name].element);
|
713
|
-
});
|
714
|
-
} else {
|
715
|
-
if (!this.name) {
|
716
|
-
throw new Error('Editable component missing required "name" attribute');
|
717
|
-
}
|
718
|
-
|
719
|
-
editorComponent.editables[this.name] = this;
|
720
|
-
}
|
721
|
-
});
|
722
|
-
}
|
723
|
-
|
724
|
-
/**
|
725
|
-
* Lifecycle callback for attribute changes
|
726
|
-
* Handles name changes and propagates other attributes to editable element
|
727
|
-
*
|
728
|
-
* @param {string} name - Name of changed attribute
|
729
|
-
* @param {string|null} oldValue - Previous value
|
730
|
-
* @param {string|null} newValue - New value
|
731
|
-
*/
|
732
|
-
attributeChangedCallback(name, oldValue, newValue) {
|
733
|
-
if (oldValue === newValue) {
|
734
|
-
return;
|
735
|
-
}
|
736
|
-
|
737
|
-
if (name === 'name') {
|
738
|
-
if (!oldValue) {
|
739
|
-
return;
|
740
|
-
}
|
741
|
-
|
742
|
-
const editorComponent = this.#queryEditorElement();
|
743
|
-
|
744
|
-
if (editorComponent) {
|
745
|
-
editorComponent.editables[newValue] = editorComponent.editables[oldValue];
|
746
|
-
delete editorComponent.editables[oldValue];
|
747
|
-
}
|
748
|
-
} else {
|
749
|
-
this.editableElement.setAttribute(name, newValue);
|
750
|
-
}
|
751
|
-
}
|
752
|
-
|
753
|
-
/**
|
754
|
-
* Lifecycle callback when element is removed
|
755
|
-
* Un-registers this editable from the parent editor
|
756
|
-
*/
|
757
|
-
disconnectedCallback() {
|
758
|
-
const editorComponent = this.#queryEditorElement();
|
759
|
-
|
760
|
-
if (editorComponent) {
|
761
|
-
delete editorComponent.editables[this.name];
|
762
|
-
}
|
763
|
-
}
|
764
|
-
|
765
|
-
/**
|
766
|
-
* Finds the parent editor component
|
767
|
-
*
|
768
|
-
* @private
|
769
|
-
* @returns {CKEditorComponent|null} Parent editor component or null if not found
|
770
|
-
*/
|
771
|
-
#queryEditorElement() {
|
772
|
-
return this.closest('ckeditor-component') || document.body.querySelector('ckeditor-component');
|
773
|
-
}
|
774
|
-
}
|
775
|
-
|
776
|
-
/**
|
777
|
-
* Custom HTML element that represents a CKEditor UI part component.
|
778
|
-
* It helpers with management of toolbar and other elements.
|
779
|
-
*
|
780
|
-
* @extends HTMLElement
|
781
|
-
* @customElement ckeditor-ui-part-component
|
782
|
-
* @example
|
783
|
-
* <ckeditor-ui-part-component></ckeditor-ui-part-component>
|
784
|
-
*/
|
785
|
-
class CKEditorUIPartComponent extends HTMLElement {
|
786
|
-
/**
|
787
|
-
* Lifecycle callback when element is added to DOM
|
788
|
-
* Adds the toolbar to the editor UI
|
789
|
-
*/
|
790
|
-
connectedCallback() {
|
791
|
-
execIfDOMReady(async () => {
|
792
|
-
const uiPart = this.getAttribute('name');
|
793
|
-
const editor = await this.#queryEditorElement().instancePromise.promise;
|
794
|
-
|
795
|
-
this.appendChild(editor.ui.view[uiPart].element);
|
796
|
-
});
|
797
|
-
}
|
798
|
-
|
799
|
-
/**
|
800
|
-
* Finds the parent editor component
|
801
|
-
*
|
802
|
-
* @private
|
803
|
-
* @returns {CKEditorComponent|null} Parent editor component or null if not found
|
804
|
-
*/
|
805
|
-
#queryEditorElement() {
|
806
|
-
return this.closest('ckeditor-component') || document.body.querySelector('ckeditor-component');
|
807
|
-
}
|
808
|
-
}
|
809
|
-
|
810
|
-
/**
|
811
|
-
* Executes callback when DOM is ready
|
812
|
-
*
|
813
|
-
* @param {() => void} callback - Function to execute
|
814
|
-
*/
|
815
|
-
function execIfDOMReady(callback) {
|
816
|
-
switch (document.readyState) {
|
817
|
-
case 'loading':
|
818
|
-
document.addEventListener('DOMContentLoaded', callback, { once: true });
|
819
|
-
break;
|
820
|
-
|
821
|
-
case 'interactive':
|
822
|
-
case 'complete':
|
823
|
-
setTimeout(callback, 0);
|
824
|
-
break;
|
825
|
-
|
826
|
-
default:
|
827
|
-
console.warn('Unexpected document.readyState:', document.readyState);
|
828
|
-
setTimeout(callback, 0);
|
829
|
-
}
|
830
|
-
}
|
831
|
-
|
832
|
-
/**
|
833
|
-
* Dynamically imports modules based on configuration
|
834
|
-
*
|
835
|
-
* @param {Array<ImportConfig>} imports - Array of import configurations
|
836
|
-
* @returns {Promise<Array<any>>} Loaded modules
|
837
|
-
*/
|
838
|
-
function loadAsyncImports(imports = []) {
|
839
|
-
const loadInlinePlugin = async ({ name, code }) => {
|
840
|
-
const module = await import(`data:text/javascript,${encodeURIComponent(code)}`);
|
841
|
-
|
842
|
-
if (!module.default) {
|
843
|
-
throw new Error(`Inline plugin "${name}" must export a default class/function!`);
|
844
|
-
}
|
845
|
-
|
846
|
-
return module.default;
|
847
|
-
};
|
848
|
-
|
849
|
-
const loadExternalPlugin = async ({ import_name, import_as, window_name }) => {
|
850
|
-
if (window_name) {
|
851
|
-
if (!Object.prototype.hasOwnProperty.call(window, window_name)) {
|
852
|
-
throw new Error(
|
853
|
-
`Plugin window['${window_name}'] not found in global scope. ` +
|
854
|
-
'Please ensure the plugin is loaded before CKEditor initialization.'
|
855
|
-
);
|
856
|
-
}
|
857
|
-
|
858
|
-
return window[window_name];
|
859
|
-
}
|
860
|
-
|
861
|
-
const module = await import(import_name);
|
862
|
-
const imported = module[import_as || 'default'];
|
863
|
-
|
864
|
-
if (!imported) {
|
865
|
-
throw new Error(`Plugin "${import_as}" not found in the ESM module "${import_name}"!`);
|
866
|
-
}
|
867
|
-
|
868
|
-
return imported;
|
869
|
-
};
|
870
|
-
|
871
|
-
return Promise.all(imports.map(item => {
|
872
|
-
switch(item.type) {
|
873
|
-
case 'inline':
|
874
|
-
return loadInlinePlugin(item);
|
875
|
-
|
876
|
-
case 'external':
|
877
|
-
default:
|
878
|
-
return loadExternalPlugin(item);
|
879
|
-
}
|
880
|
-
}));
|
881
|
-
}
|
882
|
-
|
883
|
-
|
884
|
-
/**
|
885
|
-
* Checks if a key is safe to use in configuration objects to prevent prototype pollution.
|
886
|
-
*
|
887
|
-
* @param {string} key - Key name to check
|
888
|
-
* @returns {boolean} True if key is safe to use.
|
889
|
-
*/
|
890
|
-
function isSafeKey(key) {
|
891
|
-
return typeof key === 'string' &&
|
892
|
-
key !== '__proto__' &&
|
893
|
-
key !== 'constructor' &&
|
894
|
-
key !== 'prototype';
|
895
|
-
}
|
896
|
-
|
897
714
|
customElements.define('ckeditor-component', CKEditorComponent);
|
898
|
-
customElements.define('ckeditor-editable-component', CKEditorEditableComponent);
|
899
|
-
customElements.define('ckeditor-ui-part-component', CKEditorUIPartComponent);
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class CKEditorUIPartComponent extends HTMLElement {
|
2
|
+
/**
|
3
|
+
* Lifecycle callback when element is added to DOM
|
4
|
+
* Adds the toolbar to the editor UI
|
5
|
+
*/
|
6
|
+
connectedCallback() {
|
7
|
+
execIfDOMReady(async () => {
|
8
|
+
const uiPart = this.getAttribute('name');
|
9
|
+
const editor = await this.#queryEditorElement().instancePromise.promise;
|
10
|
+
|
11
|
+
this.appendChild(editor.ui.view[uiPart].element);
|
12
|
+
});
|
13
|
+
}
|
14
|
+
|
15
|
+
/**
|
16
|
+
* Finds the parent editor component
|
17
|
+
*
|
18
|
+
* @private
|
19
|
+
* @returns {CKEditorComponent|null} Parent editor component or null if not found
|
20
|
+
*/
|
21
|
+
#queryEditorElement() {
|
22
|
+
return this.closest('ckeditor-component') || document.body.querySelector('ckeditor-component');
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
customElements.define('ckeditor-ui-part-component', CKEditorUIPartComponent);
|