ckeditor5 1.31.7 → 1.32.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 +34 -12
- data/lib/ckeditor5/rails/assets/webcomponent_bundle.rb +2 -21
- data/lib/ckeditor5/rails/version.rb +2 -2
- data/npm_package/dist/index.cjs +2 -0
- data/npm_package/dist/index.cjs.map +1 -0
- data/npm_package/dist/index.d.ts +1 -0
- data/npm_package/dist/index.mjs +723 -0
- data/npm_package/dist/index.mjs.map +1 -0
- data/npm_package/dist/src/components/context.d.ts +24 -0
- data/npm_package/dist/src/components/context.d.ts.map +1 -0
- data/npm_package/dist/src/components/editable.d.ts +34 -0
- data/npm_package/dist/src/components/editable.d.ts.map +1 -0
- data/npm_package/dist/src/components/editor/editor.d.ts +79 -0
- data/npm_package/dist/src/components/editor/editor.d.ts.map +1 -0
- data/npm_package/dist/src/components/editor/index.d.ts +3 -0
- data/npm_package/dist/src/components/editor/index.d.ts.map +1 -0
- data/npm_package/dist/src/components/editor/multiroot-editables-tracker.d.ts +36 -0
- data/npm_package/dist/src/components/editor/multiroot-editables-tracker.d.ts.map +1 -0
- data/npm_package/dist/src/components/index.d.ts +5 -0
- data/npm_package/dist/src/components/index.d.ts.map +1 -0
- data/npm_package/dist/src/components/ui-part.d.ts +2 -0
- data/npm_package/dist/src/components/ui-part.d.ts.map +1 -0
- data/npm_package/dist/src/helpers/exec-if-dom-ready.d.ts +7 -0
- data/npm_package/dist/src/helpers/exec-if-dom-ready.d.ts.map +1 -0
- data/npm_package/dist/src/helpers/index.d.ts +8 -0
- data/npm_package/dist/src/helpers/index.d.ts.map +1 -0
- data/npm_package/dist/src/helpers/inject-script.d.ts +8 -0
- data/npm_package/dist/src/helpers/inject-script.d.ts.map +1 -0
- data/npm_package/dist/src/helpers/is-safe-key.d.ts +8 -0
- data/npm_package/dist/src/helpers/is-safe-key.d.ts.map +1 -0
- data/npm_package/dist/src/helpers/load-async-css.d.ts +9 -0
- data/npm_package/dist/src/helpers/load-async-css.d.ts.map +1 -0
- data/npm_package/dist/src/helpers/load-async-imports.d.ts +25 -0
- data/npm_package/dist/src/helpers/load-async-imports.d.ts.map +1 -0
- data/npm_package/dist/src/helpers/resolve-config-element-references.d.ts +9 -0
- data/npm_package/dist/src/helpers/resolve-config-element-references.d.ts.map +1 -0
- data/npm_package/dist/src/helpers/uid.d.ts +7 -0
- data/npm_package/dist/src/helpers/uid.d.ts.map +1 -0
- data/npm_package/dist/src/index.d.ts +1 -0
- data/npm_package/dist/src/index.d.ts.map +1 -0
- data/npm_package/dist/vite.config.d.ts +3 -0
- data/npm_package/dist/vite.config.d.ts.map +1 -0
- data/npm_package/package.json +37 -0
- metadata +42 -7
- data/lib/ckeditor5/rails/assets/webcomponents/components/context.mjs +0 -123
- data/lib/ckeditor5/rails/assets/webcomponents/components/editable.mjs +0 -113
- data/lib/ckeditor5/rails/assets/webcomponents/components/editor.mjs +0 -778
- data/lib/ckeditor5/rails/assets/webcomponents/components/ui-part.mjs +0 -26
- data/lib/ckeditor5/rails/assets/webcomponents/utils.mjs +0 -235
@@ -1,778 +0,0 @@
|
|
1
|
-
class CKEditorComponent extends HTMLElement {
|
2
|
-
/**
|
3
|
-
* List of attributes that trigger updates when changed.
|
4
|
-
*
|
5
|
-
* @static
|
6
|
-
* @returns {string[]} Array of attribute names to observe
|
7
|
-
*/
|
8
|
-
static get observedAttributes() {
|
9
|
-
return ['config', 'plugins', 'translations', 'type'];
|
10
|
-
}
|
11
|
-
|
12
|
-
/**
|
13
|
-
* List of input attributes that trigger updates when changed.
|
14
|
-
*
|
15
|
-
* @static
|
16
|
-
* @returns {string[]} Array of input attribute names to observe
|
17
|
-
*/
|
18
|
-
static get inputAttributes() {
|
19
|
-
return ['name', 'required', 'value'];
|
20
|
-
}
|
21
|
-
|
22
|
-
/** @type {Promise<import('ckeditor5').Editor>|null} Promise to initialize editor instance */
|
23
|
-
instancePromise = Promise.withResolvers();
|
24
|
-
|
25
|
-
/** @type {import('ckeditor5').Watchdog|null} Editor watchdog */
|
26
|
-
watchdog = null;
|
27
|
-
|
28
|
-
/** @type {import('ckeditor5').Editor|null} Current editor instance */
|
29
|
-
instance = null;
|
30
|
-
|
31
|
-
/** @type {Record<string, HTMLElement>} Map of editable elements by name */
|
32
|
-
editables = {};
|
33
|
-
|
34
|
-
/** @type {String} Initial HTML passed to component */
|
35
|
-
#initialHTML = '';
|
36
|
-
|
37
|
-
/** @type {CKEditorContextComponent|null} */
|
38
|
-
#context = null;
|
39
|
-
|
40
|
-
/** @type {String} ID of editor within context */
|
41
|
-
#contextEditorId = null;
|
42
|
-
|
43
|
-
/** @type {Object} Description of ckeditor bundle */
|
44
|
-
#bundle = null;
|
45
|
-
|
46
|
-
/** @type {(event: CustomEvent) => void} Event handler for editor change */
|
47
|
-
get oneditorchange() {
|
48
|
-
return this.#getEventHandler('editorchange');
|
49
|
-
}
|
50
|
-
|
51
|
-
set oneditorchange(handler) {
|
52
|
-
this.#setEventHandler('editorchange', handler);
|
53
|
-
}
|
54
|
-
|
55
|
-
/** @type {(event: CustomEvent) => void} Event handler for editor ready */
|
56
|
-
get oneditorready() {
|
57
|
-
return this.#getEventHandler('editorready');
|
58
|
-
}
|
59
|
-
|
60
|
-
set oneditorready(handler) {
|
61
|
-
this.#setEventHandler('editorready', handler);
|
62
|
-
}
|
63
|
-
|
64
|
-
/** @type {(event: CustomEvent) => void} Event handler for editor error */
|
65
|
-
get oneditorerror() {
|
66
|
-
return this.#getEventHandler('editorerror');
|
67
|
-
}
|
68
|
-
|
69
|
-
set oneditorerror(handler) {
|
70
|
-
this.#setEventHandler('editorerror', handler);
|
71
|
-
}
|
72
|
-
|
73
|
-
/**
|
74
|
-
* Gets event handler function from attribute or property
|
75
|
-
*
|
76
|
-
* @private
|
77
|
-
* @param {string} name - Event name without 'on' prefix
|
78
|
-
* @returns {Function|null} Event handler or null
|
79
|
-
*/
|
80
|
-
#getEventHandler(name) {
|
81
|
-
if (this.hasAttribute(`on${name}`)) {
|
82
|
-
const handler = this.getAttribute(`on${name}`);
|
83
|
-
|
84
|
-
if (!isSafeKey(handler)) {
|
85
|
-
throw new Error(`Unsafe event handler attribute value: ${handler}`);
|
86
|
-
}
|
87
|
-
|
88
|
-
return window[handler] || new Function('event', handler);
|
89
|
-
}
|
90
|
-
return this[`#${name}Handler`];
|
91
|
-
}
|
92
|
-
|
93
|
-
/**
|
94
|
-
* Sets event handler function
|
95
|
-
*
|
96
|
-
* @private
|
97
|
-
* @param {string} name - Event name without 'on' prefix
|
98
|
-
* @param {Function|string|null} handler - Event handler
|
99
|
-
*/
|
100
|
-
#setEventHandler(name, handler) {
|
101
|
-
if (typeof handler === 'string') {
|
102
|
-
this.setAttribute(`on${name}`, handler);
|
103
|
-
} else {
|
104
|
-
this.removeAttribute(`on${name}`);
|
105
|
-
this[`#${name}Handler`] = handler;
|
106
|
-
}
|
107
|
-
}
|
108
|
-
|
109
|
-
/**
|
110
|
-
* Lifecycle callback when element is connected to DOM
|
111
|
-
* Initializes the editor when DOM is ready
|
112
|
-
* @protected
|
113
|
-
*/
|
114
|
-
connectedCallback() {
|
115
|
-
this.#context = this.closest('ckeditor-context-component');
|
116
|
-
this.#initialHTML = this.innerHTML;
|
117
|
-
|
118
|
-
try {
|
119
|
-
execIfDOMReady(async () => {
|
120
|
-
if (this.#context) {
|
121
|
-
await this.#context.instancePromise.promise;
|
122
|
-
this.#context.registerEditor(this);
|
123
|
-
}
|
124
|
-
|
125
|
-
await this.reinitializeEditor();
|
126
|
-
});
|
127
|
-
} catch (error) {
|
128
|
-
console.error('Failed to initialize editor:', error);
|
129
|
-
|
130
|
-
const event = new CustomEvent('editor-error', { detail: error });
|
131
|
-
|
132
|
-
this.dispatchEvent(event);
|
133
|
-
this.oneditorerror?.(event);
|
134
|
-
}
|
135
|
-
}
|
136
|
-
|
137
|
-
/**
|
138
|
-
* Handles attribute changes and reinitializes editor if needed
|
139
|
-
* @protected
|
140
|
-
* @param {string} name - Name of changed attribute
|
141
|
-
* @param {string|null} oldValue - Previous attribute value
|
142
|
-
* @param {string|null} newValue - New attribute value
|
143
|
-
*/
|
144
|
-
async attributeChangedCallback(name, oldValue, newValue) {
|
145
|
-
if (oldValue !== null &&
|
146
|
-
oldValue !== newValue &&
|
147
|
-
CKEditorComponent.observedAttributes.includes(name) && this.isConnected) {
|
148
|
-
await this.reinitializeEditor();
|
149
|
-
}
|
150
|
-
}
|
151
|
-
|
152
|
-
/**
|
153
|
-
* Lifecycle callback when element is removed from DOM
|
154
|
-
* Destroys the editor instance
|
155
|
-
* @protected
|
156
|
-
*/
|
157
|
-
async disconnectedCallback() {
|
158
|
-
if (this.#context) {
|
159
|
-
this.#context.unregisterEditor(this);
|
160
|
-
}
|
161
|
-
|
162
|
-
try {
|
163
|
-
await this.#destroy();
|
164
|
-
} catch (error) {
|
165
|
-
console.error('Failed to destroy editor:', error);
|
166
|
-
}
|
167
|
-
}
|
168
|
-
|
169
|
-
/**
|
170
|
-
* Runs a callback after the editor is ready. It waits for editor
|
171
|
-
* initialization if needed.
|
172
|
-
*
|
173
|
-
* @param {(editor: import('ckeditor5').Editor) => void} callback - Callback to run
|
174
|
-
* @returns {Promise<void>}
|
175
|
-
*/
|
176
|
-
runAfterEditorReady(callback) {
|
177
|
-
if (this.instance) {
|
178
|
-
return Promise.resolve(callback(this.instance));
|
179
|
-
}
|
180
|
-
|
181
|
-
return this.instancePromise.promise.then(callback);
|
182
|
-
}
|
183
|
-
|
184
|
-
/**
|
185
|
-
* Determines appropriate editor element tag based on editor type
|
186
|
-
*
|
187
|
-
* @private
|
188
|
-
* @returns {string} HTML tag name to use
|
189
|
-
*/
|
190
|
-
get #editorElementTag() {
|
191
|
-
switch (this.getAttribute('type')) {
|
192
|
-
case 'ClassicEditor':
|
193
|
-
return 'textarea';
|
194
|
-
|
195
|
-
default:
|
196
|
-
return 'div';
|
197
|
-
}
|
198
|
-
}
|
199
|
-
|
200
|
-
/**
|
201
|
-
* Gets the CKEditor context instance if available.
|
202
|
-
*
|
203
|
-
* @private
|
204
|
-
* @returns {import('ckeditor5').ContextWatchdog|null}
|
205
|
-
*/
|
206
|
-
get #contextWatchdog() {
|
207
|
-
return this.#context?.instance;
|
208
|
-
}
|
209
|
-
|
210
|
-
/**
|
211
|
-
* Destroys the editor instance and watchdog if available
|
212
|
-
*/
|
213
|
-
async #destroy() {
|
214
|
-
if (this.#contextEditorId) {
|
215
|
-
await this.#contextWatchdog.remove(this.#contextEditorId);
|
216
|
-
}
|
217
|
-
|
218
|
-
await this.instance?.destroy();
|
219
|
-
await this.watchdog?.destroy();
|
220
|
-
}
|
221
|
-
|
222
|
-
/**
|
223
|
-
* Gets editor configuration with resolved element references
|
224
|
-
*
|
225
|
-
* @private
|
226
|
-
* @returns {EditorConfig}
|
227
|
-
*/
|
228
|
-
#getConfig() {
|
229
|
-
const config = JSON.parse(this.getAttribute('config') || '{}');
|
230
|
-
|
231
|
-
return resolveElementReferences(config);
|
232
|
-
}
|
233
|
-
|
234
|
-
/**
|
235
|
-
* Creates a new CKEditor instance
|
236
|
-
*
|
237
|
-
* @private
|
238
|
-
* @param {Record<string, HTMLElement>|CKEditorMultiRootEditablesTracker} editablesOrContent - Editable or content
|
239
|
-
* @returns {Promise<{ editor: import('ckeditor5').Editor, watchdog: editor: import('ckeditor5').EditorWatchdog }>} Initialized editor instance
|
240
|
-
* @throws {Error} When initialization fails
|
241
|
-
*/
|
242
|
-
async #initializeEditor(editablesOrContent) {
|
243
|
-
await Promise.all([
|
244
|
-
this.#ensureStylesheetsInjected(),
|
245
|
-
this.#ensureWindowScriptsInjected(),
|
246
|
-
]);
|
247
|
-
|
248
|
-
// Depending on the type of the editor the content supplied on the first
|
249
|
-
// argument is different. For ClassicEditor it's a element or string, for MultiRootEditor
|
250
|
-
// it's an object with editables, for DecoupledEditor it's string.
|
251
|
-
let content = editablesOrContent;
|
252
|
-
|
253
|
-
if (editablesOrContent instanceof CKEditorMultiRootEditablesTracker) {
|
254
|
-
content = editablesOrContent.getAll();
|
255
|
-
} else if (typeof editablesOrContent !== 'string') {
|
256
|
-
content = editablesOrContent.main;
|
257
|
-
}
|
258
|
-
|
259
|
-
// Broadcast editor initialization event. It's good time to load add inline window plugins.
|
260
|
-
const beforeInitEventDetails = {
|
261
|
-
...content instanceof HTMLElement && { element: content },
|
262
|
-
...content instanceof String && { data: content },
|
263
|
-
...content instanceof Object && { editables: content }
|
264
|
-
};
|
265
|
-
|
266
|
-
window.dispatchEvent(
|
267
|
-
new CustomEvent('ckeditor:attach:before', { detail: beforeInitEventDetails})
|
268
|
-
);
|
269
|
-
|
270
|
-
// Start fetching constructor.
|
271
|
-
const Editor = await this.#getEditorConstructor();
|
272
|
-
const [plugins, translations] = await Promise.all([
|
273
|
-
this.#getPlugins(),
|
274
|
-
this.#getTranslations()
|
275
|
-
]);
|
276
|
-
|
277
|
-
const config = {
|
278
|
-
...this.#getConfig(),
|
279
|
-
...translations.length && {
|
280
|
-
translations
|
281
|
-
},
|
282
|
-
plugins,
|
283
|
-
};
|
284
|
-
|
285
|
-
// Broadcast editor mounting event. It's good time to map configuration.
|
286
|
-
window.dispatchEvent(
|
287
|
-
new CustomEvent('ckeditor:attach', { detail: { config, ...beforeInitEventDetails } })
|
288
|
-
);
|
289
|
-
|
290
|
-
console.warn('Initializing CKEditor with:', { config, watchdog: this.hasWatchdog(), context: this.#context });
|
291
|
-
|
292
|
-
// Initialize watchdog if needed
|
293
|
-
let watchdog = null;
|
294
|
-
let instance = null;
|
295
|
-
let contextId = null;
|
296
|
-
|
297
|
-
if (this.#context) {
|
298
|
-
contextId = uid();
|
299
|
-
|
300
|
-
await this.#contextWatchdog.add( {
|
301
|
-
creator: (_element, _config) => Editor.create(_element, _config),
|
302
|
-
id: contextId,
|
303
|
-
sourceElementOrData: content,
|
304
|
-
type: 'editor',
|
305
|
-
config,
|
306
|
-
} );
|
307
|
-
|
308
|
-
instance = this.#contextWatchdog.getItem(contextId);
|
309
|
-
} else if (this.hasWatchdog()) {
|
310
|
-
// Let's create use with plain watchdog.
|
311
|
-
const { EditorWatchdog } = await import('ckeditor5');
|
312
|
-
const watchdog = new EditorWatchdog(Editor);
|
313
|
-
|
314
|
-
await watchdog.create(content, config);
|
315
|
-
|
316
|
-
instance = watchdog.editor;
|
317
|
-
} else {
|
318
|
-
// Let's create the editor without watchdog.
|
319
|
-
instance = await Editor.create(content, config);
|
320
|
-
}
|
321
|
-
|
322
|
-
console.warn('CKEditor initialized:', {
|
323
|
-
instance,
|
324
|
-
watchdog, config: instance.config._config,
|
325
|
-
});
|
326
|
-
|
327
|
-
return {
|
328
|
-
contextId,
|
329
|
-
instance,
|
330
|
-
watchdog,
|
331
|
-
};
|
332
|
-
}
|
333
|
-
|
334
|
-
/**
|
335
|
-
* Re-initializes the editor by destroying existing instance and creating new one
|
336
|
-
*
|
337
|
-
* @private
|
338
|
-
* @returns {Promise<void>}
|
339
|
-
*/
|
340
|
-
async reinitializeEditor() {
|
341
|
-
if (this.instance) {
|
342
|
-
this.instancePromise = Promise.withResolvers();
|
343
|
-
|
344
|
-
await this.#destroy();
|
345
|
-
|
346
|
-
this.instance = null;
|
347
|
-
}
|
348
|
-
|
349
|
-
this.style.display = 'block';
|
350
|
-
|
351
|
-
if (!this.isMultiroot() && !this.isDecoupled()) {
|
352
|
-
this.innerHTML = `<${this.#editorElementTag}>${this.#initialHTML}</${this.#editorElementTag}>`;
|
353
|
-
this.#assignInputAttributes();
|
354
|
-
}
|
355
|
-
|
356
|
-
// Let's track changes in editables if it's a multiroot editor.
|
357
|
-
if(this.isMultiroot()) {
|
358
|
-
this.editables = new CKEditorMultiRootEditablesTracker(this, this.#queryEditables());
|
359
|
-
} else if (this.isDecoupled()) {
|
360
|
-
this.editables = null;
|
361
|
-
} else {
|
362
|
-
this.editables = this.#queryEditables();
|
363
|
-
}
|
364
|
-
|
365
|
-
try {
|
366
|
-
const { watchdog, instance, contextId } = await this.#initializeEditor(this.editables || this.#getConfig().initialData || '');
|
367
|
-
|
368
|
-
this.watchdog = watchdog;
|
369
|
-
this.instance = instance;
|
370
|
-
this.#contextEditorId = contextId;
|
371
|
-
|
372
|
-
this.#setupContentSync();
|
373
|
-
this.#setupEditableHeight();
|
374
|
-
this.#setupDataChangeListener();
|
375
|
-
|
376
|
-
this.instancePromise.resolve(this.instance);
|
377
|
-
|
378
|
-
// Broadcast editor ready event
|
379
|
-
const event = new CustomEvent('editor-ready', { detail: this.instance });
|
380
|
-
|
381
|
-
this.dispatchEvent(event);
|
382
|
-
this.oneditorready?.(event);
|
383
|
-
} catch (err) {
|
384
|
-
this.instancePromise.reject(err);
|
385
|
-
throw err;
|
386
|
-
}
|
387
|
-
}
|
388
|
-
|
389
|
-
/**
|
390
|
-
* Sets up data change listener that broadcasts content changes
|
391
|
-
*
|
392
|
-
* @private
|
393
|
-
*/
|
394
|
-
#setupDataChangeListener() {
|
395
|
-
const getRootContent = rootName => this.instance.getData({ rootName });
|
396
|
-
const getAllRoots = () =>
|
397
|
-
this.instance.model.document
|
398
|
-
.getRootNames()
|
399
|
-
.reduce((acc, rootName) => ({
|
400
|
-
...acc,
|
401
|
-
[rootName]: getRootContent(rootName)
|
402
|
-
}), {});
|
403
|
-
|
404
|
-
this.instance?.model.document.on('change:data', () => {
|
405
|
-
const event = new CustomEvent('editor-change', {
|
406
|
-
detail: {
|
407
|
-
editor: this.instance,
|
408
|
-
data: getAllRoots(),
|
409
|
-
},
|
410
|
-
bubbles: true
|
411
|
-
});
|
412
|
-
|
413
|
-
this.dispatchEvent(event);
|
414
|
-
this.oneditorchange?.(event);
|
415
|
-
});
|
416
|
-
}
|
417
|
-
|
418
|
-
/**
|
419
|
-
* Checks if current editor is classic type
|
420
|
-
*
|
421
|
-
* @returns {boolean}
|
422
|
-
*/
|
423
|
-
isClassic() {
|
424
|
-
return this.getAttribute('type') === 'ClassicEditor';
|
425
|
-
}
|
426
|
-
|
427
|
-
/**
|
428
|
-
* Checks if current editor is balloon type
|
429
|
-
*
|
430
|
-
* @returns {boolean}
|
431
|
-
*/
|
432
|
-
isBallon() {
|
433
|
-
return this.getAttribute('type') === 'BalloonEditor';
|
434
|
-
}
|
435
|
-
|
436
|
-
/**
|
437
|
-
* Checks if current editor is multiroot type
|
438
|
-
*
|
439
|
-
* @returns {boolean}
|
440
|
-
*/
|
441
|
-
isMultiroot() {
|
442
|
-
return this.getAttribute('type') === 'MultiRootEditor';
|
443
|
-
}
|
444
|
-
|
445
|
-
/**
|
446
|
-
* Checks if current editor is decoupled type
|
447
|
-
*
|
448
|
-
* @returns {boolean}
|
449
|
-
*/
|
450
|
-
isDecoupled() {
|
451
|
-
return this.getAttribute('type') === 'DecoupledEditor';
|
452
|
-
}
|
453
|
-
|
454
|
-
/**
|
455
|
-
* Checks if current editor has watchdog enabled
|
456
|
-
*
|
457
|
-
* @returns {boolean}
|
458
|
-
*/
|
459
|
-
hasWatchdog() {
|
460
|
-
return this.getAttribute('watchdog') === 'true';
|
461
|
-
}
|
462
|
-
|
463
|
-
/**
|
464
|
-
* Queries and validates editable elements
|
465
|
-
*
|
466
|
-
* @private
|
467
|
-
* @returns {Record<string, HTMLElement>}
|
468
|
-
* @throws {Error} When required editables are missing
|
469
|
-
*/
|
470
|
-
#queryEditables() {
|
471
|
-
if (this.isDecoupled()) {
|
472
|
-
return {};
|
473
|
-
}
|
474
|
-
|
475
|
-
if (this.isMultiroot()) {
|
476
|
-
const editables = [...this.querySelectorAll('ckeditor-editable-component')];
|
477
|
-
|
478
|
-
return editables.reduce((acc, element) => {
|
479
|
-
if (!element.name) {
|
480
|
-
throw new Error('Editable component missing required "name" attribute');
|
481
|
-
}
|
482
|
-
acc[element.name] = element;
|
483
|
-
return acc;
|
484
|
-
}, Object.create(null));
|
485
|
-
}
|
486
|
-
|
487
|
-
const mainEditable = this.querySelector(this.#editorElementTag);
|
488
|
-
|
489
|
-
if (!mainEditable) {
|
490
|
-
throw new Error(`No ${this.#editorElementTag} element found`);
|
491
|
-
}
|
492
|
-
|
493
|
-
return { main: mainEditable };
|
494
|
-
}
|
495
|
-
|
496
|
-
/**
|
497
|
-
* Copies input-related attributes from component to the main editable element
|
498
|
-
*
|
499
|
-
* @private
|
500
|
-
*/
|
501
|
-
#assignInputAttributes() {
|
502
|
-
const textarea = this.querySelector('textarea');
|
503
|
-
|
504
|
-
if (!textarea) {
|
505
|
-
return;
|
506
|
-
}
|
507
|
-
|
508
|
-
for (const attr of CKEditorComponent.inputAttributes) {
|
509
|
-
if (this.hasAttribute(attr)) {
|
510
|
-
textarea.setAttribute(attr, this.getAttribute(attr));
|
511
|
-
}
|
512
|
-
}
|
513
|
-
}
|
514
|
-
|
515
|
-
/**
|
516
|
-
* Sets up content sync between editor and textarea element.
|
517
|
-
*
|
518
|
-
* @private
|
519
|
-
*/
|
520
|
-
#setupContentSync() {
|
521
|
-
if (!this.instance) {
|
522
|
-
return;
|
523
|
-
}
|
524
|
-
|
525
|
-
const textarea = this.querySelector('textarea');
|
526
|
-
|
527
|
-
if (!textarea) {
|
528
|
-
return;
|
529
|
-
}
|
530
|
-
|
531
|
-
// Initial sync
|
532
|
-
const syncInput = () => {
|
533
|
-
this.style.position = 'relative';
|
534
|
-
|
535
|
-
textarea.innerHTML = '';
|
536
|
-
textarea.value = this.instance.getData();
|
537
|
-
textarea.tabIndex = -1;
|
538
|
-
|
539
|
-
Object.assign(textarea.style, {
|
540
|
-
display: 'flex',
|
541
|
-
position: 'absolute',
|
542
|
-
bottom: '0',
|
543
|
-
left: '50%',
|
544
|
-
width: '1px',
|
545
|
-
height: '1px',
|
546
|
-
opacity: '0',
|
547
|
-
pointerEvents: 'none',
|
548
|
-
margin: '0',
|
549
|
-
padding: '0',
|
550
|
-
border: 'none'
|
551
|
-
});
|
552
|
-
};
|
553
|
-
|
554
|
-
syncInput();
|
555
|
-
|
556
|
-
// Listen for changes
|
557
|
-
this.instance.model.document.on('change:data', () => {
|
558
|
-
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
559
|
-
textarea.dispatchEvent(new Event('change', { bubbles: true }));
|
560
|
-
|
561
|
-
syncInput();
|
562
|
-
});
|
563
|
-
}
|
564
|
-
|
565
|
-
/**
|
566
|
-
* Sets up editable height for ClassicEditor
|
567
|
-
*
|
568
|
-
* @private
|
569
|
-
*/
|
570
|
-
#setupEditableHeight() {
|
571
|
-
if (!this.isClassic() && !this.isBallon()) {
|
572
|
-
return;
|
573
|
-
}
|
574
|
-
|
575
|
-
const { instance } = this;
|
576
|
-
const height = Number.parseInt(this.getAttribute('editable-height'), 10);
|
577
|
-
|
578
|
-
if (Number.isNaN(height)) {
|
579
|
-
return;
|
580
|
-
}
|
581
|
-
|
582
|
-
instance.editing.view.change((writer) => {
|
583
|
-
writer.setStyle('height', `${height}px`, instance.editing.view.document.getRoot());
|
584
|
-
});
|
585
|
-
}
|
586
|
-
|
587
|
-
/**
|
588
|
-
* Gets bundle JSON description from translations attribute
|
589
|
-
*/
|
590
|
-
#getBundle() {
|
591
|
-
return this.#bundle ||= JSON.parse(this.getAttribute('bundle'));
|
592
|
-
}
|
593
|
-
|
594
|
-
|
595
|
-
/**
|
596
|
-
* Checks if all required stylesheets are injected. If not, inject.
|
597
|
-
*/
|
598
|
-
async #ensureStylesheetsInjected() {
|
599
|
-
await loadAsyncCSS(this.#getBundle().stylesheets || []);
|
600
|
-
}
|
601
|
-
|
602
|
-
/**
|
603
|
-
* Checks if all required scripts are injected. If not, inject.
|
604
|
-
*/
|
605
|
-
async #ensureWindowScriptsInjected() {
|
606
|
-
const windowScripts = (this.#getBundle().scripts || []).filter(script => !!script.window_name);
|
607
|
-
|
608
|
-
await loadAsyncImports(windowScripts);
|
609
|
-
}
|
610
|
-
|
611
|
-
/**
|
612
|
-
* Loads translation modules
|
613
|
-
*
|
614
|
-
* @private
|
615
|
-
* @returns {Promise<Array<any>>}
|
616
|
-
*/
|
617
|
-
async #getTranslations() {
|
618
|
-
const translations = this.#getBundle().scripts.filter(script => script.translation);
|
619
|
-
|
620
|
-
return loadAsyncImports(translations);
|
621
|
-
}
|
622
|
-
|
623
|
-
/**
|
624
|
-
* Loads plugin modules
|
625
|
-
*
|
626
|
-
* @private
|
627
|
-
* @returns {Promise<Array<any>>}
|
628
|
-
*/
|
629
|
-
async #getPlugins() {
|
630
|
-
const raw = this.getAttribute('plugins');
|
631
|
-
const items = raw ? JSON.parse(raw) : [];
|
632
|
-
const mappedItems = items.map(item =>
|
633
|
-
typeof item === 'string'
|
634
|
-
? { import_name: 'ckeditor5', import_as: item }
|
635
|
-
: item
|
636
|
-
);
|
637
|
-
|
638
|
-
return loadAsyncImports(mappedItems);
|
639
|
-
}
|
640
|
-
|
641
|
-
/**
|
642
|
-
* Gets editor constructor based on type attribute
|
643
|
-
*
|
644
|
-
* @private
|
645
|
-
* @returns {Promise<typeof import('ckeditor5').Editor>}
|
646
|
-
* @throws {Error} When editor type is invalid
|
647
|
-
*/
|
648
|
-
async #getEditorConstructor() {
|
649
|
-
const CKEditor = await import('ckeditor5');
|
650
|
-
const editorType = this.getAttribute('type');
|
651
|
-
|
652
|
-
if (!editorType || !Object.prototype.hasOwnProperty.call(CKEditor, editorType)) {
|
653
|
-
throw new Error(`Invalid editor type: ${editorType}`);
|
654
|
-
}
|
655
|
-
|
656
|
-
return CKEditor[editorType];
|
657
|
-
}
|
658
|
-
}
|
659
|
-
|
660
|
-
class CKEditorMultiRootEditablesTracker {
|
661
|
-
#editorElement;
|
662
|
-
#editables;
|
663
|
-
|
664
|
-
/**
|
665
|
-
* Creates new tracker instance wrapped in a Proxy for dynamic property access
|
666
|
-
*
|
667
|
-
* @param {CKEditorComponent} editorElement - Parent editor component reference
|
668
|
-
* @param {Record<string, HTMLElement>} initialEditables - Initial editable elements
|
669
|
-
* @returns {Proxy<CKEditorMultiRootEditablesTracker>} Proxy wrapping the tracker
|
670
|
-
*/
|
671
|
-
constructor(editorElement, initialEditables = {}) {
|
672
|
-
this.#editorElement = editorElement;
|
673
|
-
this.#editables = initialEditables;
|
674
|
-
|
675
|
-
return new Proxy(this, {
|
676
|
-
/**
|
677
|
-
* Handles property access, returns class methods or editable elements
|
678
|
-
*
|
679
|
-
* @param {CKEditorMultiRootEditablesTracker} target - The tracker instance
|
680
|
-
* @param {string|symbol} name - Property name being accessed
|
681
|
-
*/
|
682
|
-
get(target, name) {
|
683
|
-
if (typeof target[name] === 'function') {
|
684
|
-
return target[name].bind(target);
|
685
|
-
}
|
686
|
-
|
687
|
-
return target.#editables[name];
|
688
|
-
},
|
689
|
-
|
690
|
-
/**
|
691
|
-
* Handles setting new editable elements, triggers root attachment
|
692
|
-
*
|
693
|
-
* @param {CKEditorMultiRootEditablesTracker} target - The tracker instance
|
694
|
-
* @param {string} name - Name of the editable root
|
695
|
-
* @param {HTMLElement} element - Element to attach as editable
|
696
|
-
*/
|
697
|
-
set(target, name, element) {
|
698
|
-
if (target.#editables[name] !== element) {
|
699
|
-
target.attachRoot(name, element);
|
700
|
-
target.#editables[name] = element;
|
701
|
-
}
|
702
|
-
return true;
|
703
|
-
},
|
704
|
-
|
705
|
-
/**
|
706
|
-
* Handles removing editable elements, triggers root detachment
|
707
|
-
*
|
708
|
-
* @param {CKEditorMultiRootEditablesTracker} target - The tracker instance
|
709
|
-
* @param {string} name - Name of the root to remove
|
710
|
-
*/
|
711
|
-
deleteProperty(target, name) {
|
712
|
-
target.detachRoot(name);
|
713
|
-
delete target.#editables[name];
|
714
|
-
return true;
|
715
|
-
}
|
716
|
-
});
|
717
|
-
}
|
718
|
-
|
719
|
-
/**
|
720
|
-
* Attaches a new editable root to the editor.
|
721
|
-
* Creates new editor root and binds UI elements.
|
722
|
-
*
|
723
|
-
* @param {string} name - Name of the editable root
|
724
|
-
* @param {HTMLElement} element - DOM element to use as editable
|
725
|
-
* @returns {Promise<void>} Resolves when root is attached
|
726
|
-
*/
|
727
|
-
async attachRoot(name, element) {
|
728
|
-
await this.detachRoot(name);
|
729
|
-
|
730
|
-
return this.#editorElement.runAfterEditorReady((editor) => {
|
731
|
-
const { ui, editing, model } = editor;
|
732
|
-
|
733
|
-
editor.addRoot(name, {
|
734
|
-
isUndoable: false,
|
735
|
-
data: element.innerHTML
|
736
|
-
});
|
737
|
-
|
738
|
-
const root = model.document.getRoot(name);
|
739
|
-
|
740
|
-
if (ui.getEditableElement(name)) {
|
741
|
-
editor.detachEditable(root);
|
742
|
-
}
|
743
|
-
|
744
|
-
const editable = ui.view.createEditable(name, element);
|
745
|
-
ui.addEditable(editable);
|
746
|
-
editing.view.forceRender();
|
747
|
-
});
|
748
|
-
}
|
749
|
-
|
750
|
-
/**
|
751
|
-
* Detaches an editable root from the editor.
|
752
|
-
* Removes editor root and cleans up UI bindings.
|
753
|
-
*
|
754
|
-
* @param {string} name - Name of root to detach
|
755
|
-
* @returns {Promise<void>} Resolves when root is detached
|
756
|
-
*/
|
757
|
-
async detachRoot(name) {
|
758
|
-
return this.#editorElement.runAfterEditorReady(editor => {
|
759
|
-
const root = editor.model.document.getRoot(name);
|
760
|
-
|
761
|
-
if (root) {
|
762
|
-
editor.detachEditable(root);
|
763
|
-
editor.detachRoot(name, true);
|
764
|
-
}
|
765
|
-
});
|
766
|
-
}
|
767
|
-
|
768
|
-
/**
|
769
|
-
* Gets all currently tracked editable elements
|
770
|
-
*
|
771
|
-
* @returns {Record<string, HTMLElement>} Map of all editable elements
|
772
|
-
*/
|
773
|
-
getAll() {
|
774
|
-
return this.#editables;
|
775
|
-
}
|
776
|
-
}
|
777
|
-
|
778
|
-
customElements.define('ckeditor-component', CKEditorComponent);
|