ckeditor5 0.0.1 → 1.0.1
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/Gemfile +21 -0
- data/README.md +94 -13
- data/lib/ckeditor5/rails/assets/assets_bundle.rb +94 -0
- data/lib/ckeditor5/rails/assets/assets_bundle_html_serializer.rb +76 -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 +99 -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
|