ckeditor5 0.0.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +20 -0
- data/README.md +93 -12
- data/lib/ckeditor5/rails/assets/assets_bundle.rb +94 -0
- data/lib/ckeditor5/rails/assets/assets_bundle_html_serializer.rb +75 -0
- data/lib/ckeditor5/rails/assets/webcomponent.mjs +603 -0
- data/lib/ckeditor5/rails/cdn/ckbox_bundle.rb +49 -0
- data/lib/ckeditor5/rails/cdn/ckeditor_bundle.rb +58 -0
- data/lib/ckeditor5/rails/cdn/helpers.rb +64 -0
- data/lib/ckeditor5/rails/cdn/url_generator.rb +38 -0
- data/lib/ckeditor5/rails/cloud/helpers.rb +13 -0
- data/lib/ckeditor5/rails/editor/helpers.rb +64 -0
- data/lib/ckeditor5/rails/editor/props.rb +61 -0
- data/lib/ckeditor5/rails/editor/props_plugin.rb +31 -0
- data/lib/ckeditor5/rails/engine.rb +33 -0
- data/lib/ckeditor5/rails/helpers.rb +13 -0
- data/lib/ckeditor5/rails/presets.rb +95 -0
- data/lib/ckeditor5/rails/semver.rb +19 -0
- data/lib/ckeditor5/rails/version.rb +5 -0
- data/lib/ckeditor5/rails.rb +12 -0
- data/lib/{ckeditor5.rb → ckeditor5_rails.rb} +1 -1
- metadata +39 -12
- data/.github/workflows/ruby.yml +0 -28
- data/.gitignore +0 -56
- data/Rakefile +0 -3
- data/ckeditor5.gemspec +0 -23
- data/lib/ckeditor5/version.rb +0 -5
@@ -0,0 +1,603 @@
|
|
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
|
+
class CKEditorComponent extends HTMLElement {
|
25
|
+
/**
|
26
|
+
* List of attributes that trigger updates when changed
|
27
|
+
* @static
|
28
|
+
* @returns {string[]} Array of attribute names to observe
|
29
|
+
*/
|
30
|
+
static get observedAttributes() {
|
31
|
+
return ['config', 'plugins', 'translations', 'type'];
|
32
|
+
}
|
33
|
+
|
34
|
+
/** @type {Promise<import('ckeditor5').Editor>|null} Promise to initialize editor instance */
|
35
|
+
instancePromise = Promise.withResolvers();
|
36
|
+
|
37
|
+
/** @type {import('ckeditor5').Editor|null} Current editor instance */
|
38
|
+
instance = null;
|
39
|
+
|
40
|
+
/** @type {Record<string, HTMLElement>} Map of editable elements by name */
|
41
|
+
editables = {};
|
42
|
+
|
43
|
+
/**
|
44
|
+
* Lifecycle callback when element is connected to DOM
|
45
|
+
* Initializes the editor when DOM is ready
|
46
|
+
* @protected
|
47
|
+
*/
|
48
|
+
connectedCallback() {
|
49
|
+
try {
|
50
|
+
execIfDOMReady(() => this.#reinitializeEditor());
|
51
|
+
} catch (error) {
|
52
|
+
console.error('Failed to initialize editor:', error);
|
53
|
+
this.dispatchEvent(new CustomEvent('editor-error', { detail: error }));
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
/**
|
58
|
+
* Handles attribute changes and reinitializes editor if needed
|
59
|
+
* @protected
|
60
|
+
* @param {string} name - Name of changed attribute
|
61
|
+
* @param {string|null} oldValue - Previous attribute value
|
62
|
+
* @param {string|null} newValue - New attribute value
|
63
|
+
*/
|
64
|
+
async attributeChangedCallback(name, oldValue, newValue) {
|
65
|
+
if (oldValue !== null &&
|
66
|
+
oldValue !== newValue &&
|
67
|
+
CKEditorComponent.observedAttributes.includes(name) && this.isConnected) {
|
68
|
+
await this.#reinitializeEditor();
|
69
|
+
}
|
70
|
+
}
|
71
|
+
|
72
|
+
/**
|
73
|
+
* Lifecycle callback when element is removed from DOM
|
74
|
+
* Destroys the editor instance
|
75
|
+
* @protected
|
76
|
+
*/
|
77
|
+
async disconnectedCallback() {
|
78
|
+
try {
|
79
|
+
await this.instance?.destroy();
|
80
|
+
} catch (error) {
|
81
|
+
console.error('Failed to destroy editor:', error);
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
85
|
+
/**
|
86
|
+
* Runs a callback after the editor is ready. It waits for editor
|
87
|
+
* initialization if needed.
|
88
|
+
*
|
89
|
+
* @param {(editor: import('ckeditor5').Editor) => void} callback - Callback to run
|
90
|
+
* @returns {Promise<void>}
|
91
|
+
*/
|
92
|
+
runAfterEditorReady(callback) {
|
93
|
+
if (this.instance) {
|
94
|
+
return Promise.resolve(callback(this.instance));
|
95
|
+
}
|
96
|
+
|
97
|
+
return this.instancePromise.then(callback);
|
98
|
+
}
|
99
|
+
|
100
|
+
/**
|
101
|
+
* Determines appropriate editor element tag based on editor type
|
102
|
+
* @private
|
103
|
+
* @returns {string} HTML tag name to use
|
104
|
+
*/
|
105
|
+
get #editorElementTag() {
|
106
|
+
switch (this.getAttribute('type')) {
|
107
|
+
case 'ClassicEditor':
|
108
|
+
return 'textarea';
|
109
|
+
|
110
|
+
default:
|
111
|
+
return 'div';
|
112
|
+
}
|
113
|
+
}
|
114
|
+
|
115
|
+
/**
|
116
|
+
* Initializes a new CKEditor instance
|
117
|
+
* @private
|
118
|
+
* @param {Record<string, HTMLElement>|CKEditorMultiRootEditablesTracker} editables - Editable elements
|
119
|
+
* @returns {Promise<import('ckeditor5').Editor>} Initialized editor instance
|
120
|
+
* @throws {Error} When initialization fails
|
121
|
+
*/
|
122
|
+
async #initializeEditor(editables) {
|
123
|
+
const Editor = await this.#getEditorConstructor();
|
124
|
+
const [plugins, translations] = await Promise.all([
|
125
|
+
this.#getPlugins(),
|
126
|
+
this.#getTranslations()
|
127
|
+
]);
|
128
|
+
|
129
|
+
const instance = await Editor.create(
|
130
|
+
editables instanceof CKEditorMultiRootEditablesTracker
|
131
|
+
? editables.getAll()
|
132
|
+
: editables.main,
|
133
|
+
{
|
134
|
+
...this.#getConfig(),
|
135
|
+
plugins,
|
136
|
+
translations
|
137
|
+
}
|
138
|
+
);
|
139
|
+
|
140
|
+
this.dispatchEvent(new CustomEvent('editor-ready', { detail: instance }));
|
141
|
+
|
142
|
+
return instance;
|
143
|
+
}
|
144
|
+
|
145
|
+
/**
|
146
|
+
* Re-initializes the editor by destroying existing instance and creating new one
|
147
|
+
*
|
148
|
+
* @private
|
149
|
+
* @returns {Promise<void>}
|
150
|
+
*/
|
151
|
+
async #reinitializeEditor() {
|
152
|
+
if (this.instance) {
|
153
|
+
this.instancePromise = Promise.withResolvers();
|
154
|
+
|
155
|
+
await this.instance.destroy();
|
156
|
+
this.instance = null;
|
157
|
+
}
|
158
|
+
|
159
|
+
this.style.display = 'block';
|
160
|
+
|
161
|
+
if (!this.#isMultiroot()) {
|
162
|
+
this.innerHTML = `<${this.#editorElementTag}></${this.#editorElementTag}>`;
|
163
|
+
}
|
164
|
+
|
165
|
+
// Let's track changes in editables if it's a multiroot editor.
|
166
|
+
const editables = this.#queryEditables();
|
167
|
+
|
168
|
+
if(this.#isMultiroot()) {
|
169
|
+
this.editables = new CKEditorMultiRootEditablesTracker(this, editables);
|
170
|
+
} else {
|
171
|
+
this.editables = editables;
|
172
|
+
}
|
173
|
+
|
174
|
+
try {
|
175
|
+
this.instance = await this.#initializeEditor(this.editables);
|
176
|
+
this.instancePromise.resolve(this.instance);
|
177
|
+
} catch (err) {
|
178
|
+
this.instancePromise.reject(err);
|
179
|
+
throw err;
|
180
|
+
}
|
181
|
+
}
|
182
|
+
|
183
|
+
/**
|
184
|
+
* Checks if current editor is multiroot type
|
185
|
+
*
|
186
|
+
* @private
|
187
|
+
* @returns {boolean}
|
188
|
+
*/
|
189
|
+
#isMultiroot() {
|
190
|
+
return this.getAttribute('type') === 'MultiRootEditor';
|
191
|
+
}
|
192
|
+
|
193
|
+
/**
|
194
|
+
* Parses editor configuration from config attribute
|
195
|
+
*
|
196
|
+
* @private
|
197
|
+
* @returns {EditorConfig}
|
198
|
+
*/
|
199
|
+
#getConfig() {
|
200
|
+
return JSON.parse(this.getAttribute('config') || '{}');
|
201
|
+
}
|
202
|
+
|
203
|
+
/**
|
204
|
+
* Queries and validates editable elements
|
205
|
+
*
|
206
|
+
* @private
|
207
|
+
* @returns {Record<string, HTMLElement>}
|
208
|
+
* @throws {Error} When required editables are missing
|
209
|
+
*/
|
210
|
+
#queryEditables() {
|
211
|
+
if (this.#isMultiroot()) {
|
212
|
+
const editables = [...this.querySelectorAll('ckeditor-editable-component')];
|
213
|
+
|
214
|
+
return editables.reduce((acc, element) => {
|
215
|
+
if (!element.name) {
|
216
|
+
throw new Error('Editable component missing required "name" attribute');
|
217
|
+
}
|
218
|
+
acc[element.name] = element;
|
219
|
+
return acc;
|
220
|
+
}, Object.create(null));
|
221
|
+
}
|
222
|
+
|
223
|
+
const mainEditable = this.querySelector(this.#editorElementTag);
|
224
|
+
|
225
|
+
if (!mainEditable) {
|
226
|
+
throw new Error(`No ${this.#editorElementTag} element found`);
|
227
|
+
}
|
228
|
+
|
229
|
+
return { main: mainEditable };
|
230
|
+
}
|
231
|
+
|
232
|
+
/**
|
233
|
+
* Loads translation modules
|
234
|
+
*
|
235
|
+
* @private
|
236
|
+
* @returns {Promise<Array<any>>}
|
237
|
+
*/
|
238
|
+
async #getTranslations() {
|
239
|
+
const raw = this.getAttribute('translations');
|
240
|
+
return loadAsyncImports(raw ? JSON.parse(raw) : []);
|
241
|
+
}
|
242
|
+
|
243
|
+
/**
|
244
|
+
* Loads plugin modules
|
245
|
+
*
|
246
|
+
* @private
|
247
|
+
* @returns {Promise<Array<any>>}
|
248
|
+
*/
|
249
|
+
async #getPlugins() {
|
250
|
+
const raw = this.getAttribute('plugins');
|
251
|
+
const items = raw ? JSON.parse(raw) : [];
|
252
|
+
const mappedItems = items.map(item =>
|
253
|
+
typeof item === 'string'
|
254
|
+
? { import_name: 'ckeditor5', import_as: item }
|
255
|
+
: item
|
256
|
+
);
|
257
|
+
|
258
|
+
return loadAsyncImports(mappedItems);
|
259
|
+
}
|
260
|
+
|
261
|
+
/**
|
262
|
+
* Gets editor constructor based on type attribute
|
263
|
+
*
|
264
|
+
* @private
|
265
|
+
* @returns {Promise<typeof import('ckeditor5').Editor>}
|
266
|
+
* @throws {Error} When editor type is invalid
|
267
|
+
*/
|
268
|
+
async #getEditorConstructor() {
|
269
|
+
const CKEditor = await import('ckeditor5');
|
270
|
+
const editorType = this.getAttribute('type');
|
271
|
+
|
272
|
+
if (!editorType || !Object.prototype.hasOwnProperty.call(CKEditor, editorType)) {
|
273
|
+
throw new Error(`Invalid editor type: ${editorType}`);
|
274
|
+
}
|
275
|
+
|
276
|
+
return CKEditor[editorType];
|
277
|
+
}
|
278
|
+
}
|
279
|
+
|
280
|
+
/**
|
281
|
+
* Tracks and manages editable roots for CKEditor MultiRoot editor.
|
282
|
+
* Provides a proxy-based API for dynamically managing editable elements with automatic
|
283
|
+
* attachment/detachment of editor roots.
|
284
|
+
*
|
285
|
+
* @class
|
286
|
+
* @property {CKEditorComponent} #editorElement - Reference to parent editor component
|
287
|
+
* @property {Record<string, HTMLElement>} #editables - Map of tracked editable elements
|
288
|
+
*/
|
289
|
+
class CKEditorMultiRootEditablesTracker {
|
290
|
+
#editorElement;
|
291
|
+
#editables;
|
292
|
+
|
293
|
+
/**
|
294
|
+
* Creates new tracker instance wrapped in a Proxy for dynamic property access
|
295
|
+
*
|
296
|
+
* @param {CKEditorComponent} editorElement - Parent editor component reference
|
297
|
+
* @param {Record<string, HTMLElement>} initialEditables - Initial editable elements
|
298
|
+
* @returns {Proxy<CKEditorMultiRootEditablesTracker>} Proxy wrapping the tracker
|
299
|
+
*/
|
300
|
+
constructor(editorElement, initialEditables = {}) {
|
301
|
+
this.#editorElement = editorElement;
|
302
|
+
this.#editables = initialEditables;
|
303
|
+
|
304
|
+
return new Proxy(this, {
|
305
|
+
/**
|
306
|
+
* Handles property access, returns class methods or editable elements
|
307
|
+
*
|
308
|
+
* @param {CKEditorMultiRootEditablesTracker} target - The tracker instance
|
309
|
+
* @param {string|symbol} name - Property name being accessed
|
310
|
+
*/
|
311
|
+
get(target, name) {
|
312
|
+
if (typeof target[name] === 'function') {
|
313
|
+
return target[name].bind(target);
|
314
|
+
}
|
315
|
+
|
316
|
+
return target.#editables[name];
|
317
|
+
},
|
318
|
+
|
319
|
+
/**
|
320
|
+
* Handles setting new editable elements, triggers root attachment
|
321
|
+
*
|
322
|
+
* @param {CKEditorMultiRootEditablesTracker} target - The tracker instance
|
323
|
+
* @param {string} name - Name of the editable root
|
324
|
+
* @param {HTMLElement} element - Element to attach as editable
|
325
|
+
*/
|
326
|
+
set(target, name, element) {
|
327
|
+
if (target.#editables[name] !== element) {
|
328
|
+
target.attachRoot(name, element);
|
329
|
+
target.#editables[name] = element;
|
330
|
+
}
|
331
|
+
return true;
|
332
|
+
},
|
333
|
+
|
334
|
+
/**
|
335
|
+
* Handles removing editable elements, triggers root detachment
|
336
|
+
*
|
337
|
+
* @param {CKEditorMultiRootEditablesTracker} target - The tracker instance
|
338
|
+
* @param {string} name - Name of the root to remove
|
339
|
+
*/
|
340
|
+
deleteProperty(target, name) {
|
341
|
+
target.detachRoot(name);
|
342
|
+
delete target.#editables[name];
|
343
|
+
return true;
|
344
|
+
}
|
345
|
+
});
|
346
|
+
}
|
347
|
+
|
348
|
+
/**
|
349
|
+
* Attaches a new editable root to the editor.
|
350
|
+
* Creates new editor root and binds UI elements.
|
351
|
+
*
|
352
|
+
* @param {string} name - Name of the editable root
|
353
|
+
* @param {HTMLElement} element - DOM element to use as editable
|
354
|
+
* @returns {Promise<void>} Resolves when root is attached
|
355
|
+
*/
|
356
|
+
async attachRoot(name, element) {
|
357
|
+
await this.detachRoot(name);
|
358
|
+
|
359
|
+
return this.#editorElement.runAfterEditorReady((editor) => {
|
360
|
+
const { ui, editing, model } = editor;
|
361
|
+
|
362
|
+
editor.addRoot(name, {
|
363
|
+
isUndoable: false,
|
364
|
+
data: element.innerHTML
|
365
|
+
});
|
366
|
+
|
367
|
+
const root = model.document.getRoot(name);
|
368
|
+
|
369
|
+
if (ui.getEditableElement(name)) {
|
370
|
+
editor.detachEditable(root);
|
371
|
+
}
|
372
|
+
|
373
|
+
const editable = ui.view.createEditable(name, element);
|
374
|
+
ui.addEditable(editable);
|
375
|
+
editing.view.forceRender();
|
376
|
+
});
|
377
|
+
}
|
378
|
+
|
379
|
+
/**
|
380
|
+
* Detaches an editable root from the editor.
|
381
|
+
* Removes editor root and cleans up UI bindings.
|
382
|
+
*
|
383
|
+
* @param {string} name - Name of root to detach
|
384
|
+
* @returns {Promise<void>} Resolves when root is detached
|
385
|
+
*/
|
386
|
+
async detachRoot(name) {
|
387
|
+
return this.#editorElement.runAfterEditorReady(editor => {
|
388
|
+
const root = editor.model.document.getRoot(name);
|
389
|
+
|
390
|
+
if (root) {
|
391
|
+
editor.detachEditable(root);
|
392
|
+
editor.detachRoot(name, true);
|
393
|
+
}
|
394
|
+
});
|
395
|
+
}
|
396
|
+
|
397
|
+
/**
|
398
|
+
* Gets all currently tracked editable elements
|
399
|
+
*
|
400
|
+
* @returns {Record<string, HTMLElement>} Map of all editable elements
|
401
|
+
*/
|
402
|
+
getAll() {
|
403
|
+
return this.#editables;
|
404
|
+
}
|
405
|
+
}
|
406
|
+
|
407
|
+
/**
|
408
|
+
* Custom HTML element representing an editable region for CKEditor.
|
409
|
+
* Must be used as a child of ckeditor-component element.
|
410
|
+
*
|
411
|
+
* @customElement ckeditor-editable-component
|
412
|
+
* @extends HTMLElement
|
413
|
+
*
|
414
|
+
* @property {string} name - The name of the editable region, accessed via getAttribute
|
415
|
+
* @property {HTMLDivElement} editableElement - The div element containing editable content
|
416
|
+
*
|
417
|
+
* @fires connectedCallback - When the element is added to the DOM
|
418
|
+
* @fires attributeChangedCallback - When element attributes change
|
419
|
+
* @fires disconnectedCallback - When the element is removed from the DOM
|
420
|
+
*
|
421
|
+
* @throws {Error} Throws error if not used as child of ckeditor-component
|
422
|
+
*
|
423
|
+
* @example
|
424
|
+
* <ckeditor-component>
|
425
|
+
* <ckeditor-editable-component name="main">
|
426
|
+
* Content goes here
|
427
|
+
* </ckeditor-editable-component>
|
428
|
+
* </ckeditor-component>
|
429
|
+
*/
|
430
|
+
class CKEditorEditableComponent extends HTMLElement {
|
431
|
+
/**
|
432
|
+
* List of attributes that trigger updates when changed
|
433
|
+
*
|
434
|
+
* @static
|
435
|
+
* @returns {string[]} Array of attribute names to observe
|
436
|
+
*/
|
437
|
+
static get observedAttributes() {
|
438
|
+
return ['name'];
|
439
|
+
}
|
440
|
+
|
441
|
+
/**
|
442
|
+
* Gets the name of this editable region
|
443
|
+
*
|
444
|
+
* @returns {string} The name attribute value
|
445
|
+
*/
|
446
|
+
get name() {
|
447
|
+
return this.getAttribute('name');
|
448
|
+
}
|
449
|
+
|
450
|
+
/**
|
451
|
+
* Gets the actual editable DOM element
|
452
|
+
* @returns {HTMLDivElement|null} The div element containing editable content
|
453
|
+
*/
|
454
|
+
get editableElement() {
|
455
|
+
return this.querySelector('div');
|
456
|
+
}
|
457
|
+
|
458
|
+
/**
|
459
|
+
* Lifecycle callback when element is added to DOM
|
460
|
+
* Sets up the editable element and registers it with the parent editor
|
461
|
+
*
|
462
|
+
* @throws {Error} If not used as child of ckeditor-component
|
463
|
+
*/
|
464
|
+
connectedCallback() {
|
465
|
+
const editorComponent = this.#queryEditorElement();
|
466
|
+
|
467
|
+
if (!editorComponent ) {
|
468
|
+
throw new Error('ckeditor-editable-component must be a child of ckeditor-component');
|
469
|
+
}
|
470
|
+
|
471
|
+
this.innerHTML = `<div>${this.innerHTML}</div>`;
|
472
|
+
this.style.display = 'block';
|
473
|
+
|
474
|
+
editorComponent.editables[this.name] = this;
|
475
|
+
}
|
476
|
+
|
477
|
+
/**
|
478
|
+
* Lifecycle callback for attribute changes
|
479
|
+
* Handles name changes and propagates other attributes to editable element
|
480
|
+
*
|
481
|
+
* @param {string} name - Name of changed attribute
|
482
|
+
* @param {string|null} oldValue - Previous value
|
483
|
+
* @param {string|null} newValue - New value
|
484
|
+
*/
|
485
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
486
|
+
if (oldValue === newValue) {
|
487
|
+
return;
|
488
|
+
}
|
489
|
+
|
490
|
+
if (name === 'name') {
|
491
|
+
if (!oldValue) {
|
492
|
+
return;
|
493
|
+
}
|
494
|
+
|
495
|
+
const editorComponent = this.#queryEditorElement();
|
496
|
+
|
497
|
+
if (editorComponent) {
|
498
|
+
editorComponent.editables[newValue] = editorComponent.editables[oldValue];
|
499
|
+
delete editorComponent.editables[oldValue];
|
500
|
+
}
|
501
|
+
} else {
|
502
|
+
this.editableElement.setAttribute(name, newValue);
|
503
|
+
}
|
504
|
+
}
|
505
|
+
|
506
|
+
/**
|
507
|
+
* Lifecycle callback when element is removed
|
508
|
+
* Un-registers this editable from the parent editor
|
509
|
+
*/
|
510
|
+
disconnectedCallback() {
|
511
|
+
const editorComponent = this.#queryEditorElement();
|
512
|
+
|
513
|
+
if (editorComponent) {
|
514
|
+
delete editorComponent.editables[this.name];
|
515
|
+
}
|
516
|
+
}
|
517
|
+
|
518
|
+
/**
|
519
|
+
* Finds the parent editor component
|
520
|
+
*
|
521
|
+
* @private
|
522
|
+
* @returns {CKEditorComponent|null} Parent editor component or null if not found
|
523
|
+
*/
|
524
|
+
#queryEditorElement() {
|
525
|
+
return this.closest('ckeditor-component');
|
526
|
+
}
|
527
|
+
}
|
528
|
+
|
529
|
+
/**
|
530
|
+
* Custom HTML element that represents a CKEditor toolbar component.
|
531
|
+
* Manages the toolbar placement and integration with the main editor component.
|
532
|
+
*
|
533
|
+
* @extends HTMLElement
|
534
|
+
* @customElement ckeditor-toolbar
|
535
|
+
* @example
|
536
|
+
* <ckeditor-toolbar></ckeditor-toolbar>
|
537
|
+
*/
|
538
|
+
class CKEditorToolbarComponent extends HTMLElement {
|
539
|
+
/**
|
540
|
+
* Lifecycle callback when element is added to DOM
|
541
|
+
* Adds the toolbar to the editor UI
|
542
|
+
*/
|
543
|
+
async connectedCallback() {
|
544
|
+
const editor = await this.#queryEditorElement().instancePromise.promise;
|
545
|
+
|
546
|
+
this.appendChild(editor.ui.view.toolbar.element);
|
547
|
+
}
|
548
|
+
|
549
|
+
/**
|
550
|
+
* Finds the parent editor component
|
551
|
+
*
|
552
|
+
* @private
|
553
|
+
* @returns {CKEditorComponent|null} Parent editor component or null if not found
|
554
|
+
*/
|
555
|
+
#queryEditorElement() {
|
556
|
+
return this.closest('ckeditor-component');
|
557
|
+
}
|
558
|
+
}
|
559
|
+
|
560
|
+
/**
|
561
|
+
* Executes callback when DOM is ready
|
562
|
+
*
|
563
|
+
* @param {() => void} callback - Function to execute
|
564
|
+
*/
|
565
|
+
function execIfDOMReady(callback) {
|
566
|
+
switch (document.readyState) {
|
567
|
+
case 'loading':
|
568
|
+
document.addEventListener('DOMContentLoaded', callback, { once: true });
|
569
|
+
break;
|
570
|
+
|
571
|
+
case 'interactive':
|
572
|
+
case 'complete':
|
573
|
+
setTimeout(callback, 0);
|
574
|
+
break;
|
575
|
+
|
576
|
+
default:
|
577
|
+
console.warn('Unexpected document.readyState:', document.readyState);
|
578
|
+
setTimeout(callback, 0);
|
579
|
+
}
|
580
|
+
}
|
581
|
+
|
582
|
+
/**
|
583
|
+
* Dynamically imports modules based on configuration
|
584
|
+
*
|
585
|
+
* @param {Array<ImportConfig>} imports - Array of import configurations
|
586
|
+
* @returns {Promise<Array<any>>} Loaded modules
|
587
|
+
*/
|
588
|
+
function loadAsyncImports(imports = []) {
|
589
|
+
return Promise.all(
|
590
|
+
imports.map(async ({ import_name, import_as, window_name }) => {
|
591
|
+
if (window_name && Object.prototype.hasOwnProperty.call(window, window_name)) {
|
592
|
+
return window[window_name];
|
593
|
+
}
|
594
|
+
|
595
|
+
const module = await import(import_name);
|
596
|
+
return import_as ? module[import_as] : module.default;
|
597
|
+
})
|
598
|
+
);
|
599
|
+
}
|
600
|
+
|
601
|
+
customElements.define('ckeditor-component', CKEditorComponent);
|
602
|
+
customElements.define('ckeditor-editable-component', CKEditorEditableComponent);
|
603
|
+
customElements.define('ckeditor-toolbar-component', CKEditorToolbarComponent);
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CKEditor5::Rails
|
4
|
+
module Cdn
|
5
|
+
class CKBoxBundle < Assets::AssetsBundle
|
6
|
+
include Cdn::UrlGenerator
|
7
|
+
|
8
|
+
attr_reader :cdn, :version, :theme, :translations
|
9
|
+
|
10
|
+
def initialize(version, theme: :lark, cdn: Engine.base.default_cdn, translations: [])
|
11
|
+
raise ArgumentError, 'version must be semver' unless version.is_a?(Semver)
|
12
|
+
raise ArgumentError, 'theme must be a string' unless theme.is_a?(String)
|
13
|
+
raise ArgumentError, 'translations must be an array' unless translations.is_a?(Array)
|
14
|
+
|
15
|
+
super()
|
16
|
+
|
17
|
+
@cdn = cdn
|
18
|
+
@version = version
|
19
|
+
@theme = theme
|
20
|
+
@translations = translations
|
21
|
+
end
|
22
|
+
|
23
|
+
def scripts
|
24
|
+
@scripts ||= [
|
25
|
+
Assets::JSExportsMeta.new(
|
26
|
+
create_cdn_url('ckbox', 'ckbox.js', version),
|
27
|
+
*translations_js_exports_meta
|
28
|
+
)
|
29
|
+
]
|
30
|
+
end
|
31
|
+
|
32
|
+
def stylesheets
|
33
|
+
@stylesheets ||= [
|
34
|
+
create_cdn_url('ckbox', "styles/themes/#{theme}.css", version)
|
35
|
+
]
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def translations_js_exports_meta
|
41
|
+
translations.map do |lang|
|
42
|
+
url = create_cdn_url('ckbox', "translations/#{lang}.js", version)
|
43
|
+
|
44
|
+
Assets::JSExportsMeta.new(url, window_name: 'CKBOX_TRANSLATIONS', translation: true)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CKEditor5::Rails
|
4
|
+
module Cdn
|
5
|
+
class CKEditorBundle < Assets::AssetsBundle
|
6
|
+
include Cdn::UrlGenerator
|
7
|
+
|
8
|
+
attr_reader :version, :translations, :import_name
|
9
|
+
|
10
|
+
def initialize(version, import_name, cdn: Engine.base.default_cdn, translations: [])
|
11
|
+
raise ArgumentError, 'version must be semver' unless version.is_a?(Semver)
|
12
|
+
raise ArgumentError, 'import_name must be a string' unless import_name.is_a?(String)
|
13
|
+
raise ArgumentError, 'translations must be an array' unless translations.is_a?(Array)
|
14
|
+
|
15
|
+
super()
|
16
|
+
|
17
|
+
@cdn = cdn
|
18
|
+
@version = version
|
19
|
+
@import_name = import_name
|
20
|
+
@translations = translations
|
21
|
+
end
|
22
|
+
|
23
|
+
def scripts
|
24
|
+
@scripts ||= [
|
25
|
+
js_exports_meta,
|
26
|
+
*translations_js_exports_meta
|
27
|
+
]
|
28
|
+
end
|
29
|
+
|
30
|
+
def stylesheets
|
31
|
+
@stylesheets ||= [
|
32
|
+
create_cdn_url(import_name, version, "#{import_name}.css")
|
33
|
+
]
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def js_exports_meta
|
39
|
+
Assets::JSExportsMeta.new(
|
40
|
+
create_cdn_url(import_name, version, "#{import_name}.js"),
|
41
|
+
import_name: import_name
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
def translations_js_exports_meta
|
46
|
+
translations.map do |lang|
|
47
|
+
url = create_cdn_url(import_name, version, "translations/#{lang}.js")
|
48
|
+
|
49
|
+
Assets::JSExportsMeta.new(
|
50
|
+
url,
|
51
|
+
import_name: "#{import_name}/translations/#{lang}.js",
|
52
|
+
translation: true
|
53
|
+
)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|