ckeditor5 1.9.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 +48 -11
- 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} +104 -479
- 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/helpers.rb +0 -2
- data/lib/ckeditor5/rails/presets/preset_builder.rb +17 -1
- data/lib/ckeditor5/rails/version.rb +1 -1
- metadata +8 -4
- data/lib/ckeditor5/rails/cloud/helpers.rb +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e3e06c4a4882429855d9570658be64f10fffd97371d0ae60e9aee2ecec08df00
|
4
|
+
data.tar.gz: f48eabec0f1bcab9299be0f00094f2e1078a757582a705e57d504748943b5790
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c989f7723dadc126c356b579f50160b09f3c359af8a791a919bcff0595c7387241f89f5a63a100c482878d0f067c094f45619628094d6a8c0659d797d75cb514
|
7
|
+
data.tar.gz: a411bbde271725fbaeddfc58023307837ffac5e26d4814ed13e922288846129973a2b297681076b480982485a91ed4b5c527056cf2b9b799cfbb321a5ed83762
|
data/README.md
CHANGED
@@ -102,8 +102,8 @@ Voilà! You have CKEditor 5 integrated with your Rails application. 🎉
|
|
102
102
|
- [Balloon editor 🎈](#balloon-editor-)
|
103
103
|
- [Decoupled editor 🌐](#decoupled-editor-)
|
104
104
|
- [Using Context 📦](#using-context-)
|
105
|
-
- [Benefits of Using Context in Collaboration 🤝](#benefits-of-using-context-in-collaboration-)
|
106
105
|
- [Using Context in CKEditor 5 🔄](#using-context-in-ckeditor-5-)
|
106
|
+
- [Example usage of `ckeditor5_context` helper 📝](#example-usage-of-ckeditor5_context-helper-)
|
107
107
|
- [How to access editor instance? 🤔](#how-to-access-editor-instance-)
|
108
108
|
- [Common Tasks and Solutions 💡](#common-tasks-and-solutions-)
|
109
109
|
- [Setting Editor Language 🌐](#setting-editor-language-)
|
@@ -115,6 +115,8 @@ Voilà! You have CKEditor 5 integrated with your Rails application. 🎉
|
|
115
115
|
- [Events fired by the editor 🔊](#events-fired-by-the-editor-)
|
116
116
|
- [`editor-ready` event](#editor-ready-event)
|
117
117
|
- [`editor-error` event](#editor-error-event)
|
118
|
+
- [`editor-change` event](#editor-change-event)
|
119
|
+
- [Inline event handling](#inline-event-handling)
|
118
120
|
- [Trademarks 📜](#trademarks-)
|
119
121
|
- [License 📜](#license-)
|
120
122
|
|
@@ -967,25 +969,24 @@ If you want to use a decoupled editor, you can pass the `type` keyword argument
|
|
967
969
|
|
968
970
|
Context CKEditor 5 is a feature that allows multiple editor instances to share a common configuration and state. This is particularly useful in collaborative environments where multiple users are editing different parts of the same document simultaneously. By using a shared context, all editor instances can synchronize their configurations, plugins, and other settings, ensuring a consistent editing experience across all users.
|
969
971
|
|
970
|
-
|
971
|
-
|
972
|
-
1. **Consistency**: Ensures that all editor instances have the same configuration, plugins, and settings, providing a uniform editing experience.
|
973
|
-
2. **Synchronization**: Allows real-time synchronization of content and changes across multiple editor instances, making collaborative editing seamless.
|
974
|
-
3. **Resource Sharing**: Reduces the overhead of loading and initializing multiple editor instances by sharing common resources and configurations.
|
975
|
-
4. **Simplified Management**: Makes it easier to manage and update the configuration and state of multiple editor instances from a single point.
|
972
|
+

|
976
973
|
|
977
974
|
### Using Context in CKEditor 5 🔄
|
978
975
|
|
979
976
|
Format of the `ckeditor5_context` helper:
|
980
977
|
|
981
978
|
```erb
|
982
|
-
|
979
|
+
<!-- app/views/demos/index.html.erb -->
|
980
|
+
|
981
|
+
<%= ckeditor5_context config: { ... }, plugins: [ ... ] do %>
|
983
982
|
<%= ckeditor5_editor %>
|
984
983
|
<%= ckeditor5_editor %>
|
985
984
|
<% end %>
|
986
985
|
```
|
987
986
|
|
988
|
-
|
987
|
+
The `ckeditor5_context` helper takes the `config` and `plugins` keyword arguments. The `config` keyword argument allows you to define the shared configuration of the editor instances, while the `plugins` keyword argument allows you to define the shared plugins. Format of these arguments is the same as in the `ckeditor5_editor` helper.
|
988
|
+
|
989
|
+
### Example usage of `ckeditor5_context` helper 📝
|
989
990
|
|
990
991
|
```erb
|
991
992
|
<!-- app/views/demos/index.html.erb -->
|
@@ -1260,6 +1261,8 @@ class HighlightCommand extends Command {
|
|
1260
1261
|
|
1261
1262
|
## Events fired by the editor 🔊
|
1262
1263
|
|
1264
|
+
CKEditor 5 provides a set of events that you can listen to in order to react to changes in the editor. You can listen to these events using the `addEventListener` method or by defining event handlers directly in the view.
|
1265
|
+
|
1263
1266
|
### `editor-ready` event
|
1264
1267
|
|
1265
1268
|
The event is fired when the initialization of the editor is completed. You can listen to it using the `editor-ready` event.
|
@@ -1280,6 +1283,40 @@ document.getElementById('editor').addEventListener('editor-error', () => {
|
|
1280
1283
|
});
|
1281
1284
|
```
|
1282
1285
|
|
1286
|
+
### `editor-change` event
|
1287
|
+
|
1288
|
+
The event is fired when the content of the editor changes. You can listen to it using the `editor-change` event.
|
1289
|
+
|
1290
|
+
```js
|
1291
|
+
document.getElementById('editor').addEventListener('editor-change', () => {
|
1292
|
+
console.log('Editor content has changed');
|
1293
|
+
});
|
1294
|
+
```
|
1295
|
+
|
1296
|
+
### Inline event handling
|
1297
|
+
|
1298
|
+
You can also define event handlers directly in the view using the `oneditorchange`, `oneditorerror`, and `oneditorready` attributes.
|
1299
|
+
|
1300
|
+
```erb
|
1301
|
+
<!-- app/views/demos/index.html.erb -->
|
1302
|
+
|
1303
|
+
<script type="text/javascript">
|
1304
|
+
function onEditorChange(event) {
|
1305
|
+
// event.detail.editor, event.detail.data
|
1306
|
+
}
|
1307
|
+
|
1308
|
+
function onEditorError(event) {
|
1309
|
+
// event.detail.editor
|
1310
|
+
}
|
1311
|
+
|
1312
|
+
function onEditorReady(event) {
|
1313
|
+
// event.detail.editor
|
1314
|
+
}
|
1315
|
+
</script>
|
1316
|
+
|
1317
|
+
<%= ckeditor5_editor style: 'width: 600px', id: 'editor', oneditorchange: 'onEditorChange', oneditorerror: 'onEditorError', oneditorready: 'onEditorReady' %>
|
1318
|
+
```
|
1319
|
+
|
1283
1320
|
## Trademarks 📜
|
1284
1321
|
|
1285
1322
|
CKEditor® is a trademark of [CKSource Holding sp. z o.o.](https://cksource.com/) All rights reserved. For more information about the license of CKEditor® please visit [CKEditor's licensing page](https://ckeditor.com/legal/ckeditor-oss-license/).
|
@@ -1288,6 +1325,6 @@ This gem is not owned by CKSource and does not use the CKEditor® trademark for
|
|
1288
1325
|
|
1289
1326
|
## License 📜
|
1290
1327
|
|
1291
|
-
This project is licensed under the terms of the [GNU General Public License v2.0](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). See the [LICENSE](LICENSE) file for details.
|
1328
|
+
This project is licensed under the terms of the [GNU General Public License v2.0 or later](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). See the [LICENSE](LICENSE) file for details.
|
1292
1329
|
|
1293
|
-
This project uses CKEditor 5 which is licensed under the terms of [GNU General Public License Version 2](https://www.gnu.org/licenses/old-licenses/gpl-2.0.html). For more information about CKEditor 5 licensing, please see their [official documentation](https://ckeditor.com/legal/ckeditor-oss-license/).
|
1330
|
+
This project uses CKEditor 5 which is licensed under the terms of [GNU General Public License Version 2 or later](https://www.gnu.org/licenses/old-licenses/gpl-2.0.html). For more information about CKEditor 5 licensing, please see their [official documentation](https://ckeditor.com/legal/ckeditor-oss-license/).
|
@@ -3,9 +3,9 @@
|
|
3
3
|
require 'uri'
|
4
4
|
require 'action_view'
|
5
5
|
|
6
|
-
|
7
|
-
WEBCOMPONENT_SOURCE = File.read(File.join(__dir__, 'webcomponent.mjs')).html_safe
|
6
|
+
require_relative 'webcomponent_bundle'
|
8
7
|
|
8
|
+
module CKEditor5::Rails::Assets
|
9
9
|
class AssetsBundleHtmlSerializer
|
10
10
|
include ActionView::Helpers::TagHelper
|
11
11
|
|
@@ -38,7 +38,7 @@ module CKEditor5::Rails::Assets
|
|
38
38
|
private
|
39
39
|
|
40
40
|
def web_component_tag
|
41
|
-
@web_component_tag ||= tag.script(
|
41
|
+
@web_component_tag ||= tag.script(WebComponentBundle.source, type: 'module', nonce: true)
|
42
42
|
end
|
43
43
|
|
44
44
|
def window_scripts_tags
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CKEditor5::Rails::Assets
|
4
|
+
module WebComponentBundle
|
5
|
+
WEBCOMPONENTS_PATH = File.join(__dir__, 'webcomponents')
|
6
|
+
WEBCOMPONENTS_MODULES = [
|
7
|
+
'utils.mjs',
|
8
|
+
'components/editable.mjs',
|
9
|
+
'components/ui-part.mjs',
|
10
|
+
'components/editor.mjs',
|
11
|
+
'components/context.mjs'
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
module_function
|
15
|
+
|
16
|
+
def source
|
17
|
+
@source ||= WEBCOMPONENTS_MODULES.map do |file|
|
18
|
+
File.read(File.join(WEBCOMPONENTS_PATH, file))
|
19
|
+
end.join("\n").html_safe
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
class CKEditorContextComponent extends HTMLElement {
|
2
|
+
static get observedAttributes() {
|
3
|
+
return ['plugins', 'config'];
|
4
|
+
}
|
5
|
+
|
6
|
+
/** @type {import('ckeditor5').Context|null} */
|
7
|
+
instance = null;
|
8
|
+
|
9
|
+
/** @type {Promise<import('ckeditor5').Context>} */
|
10
|
+
instancePromise = Promise.withResolvers();
|
11
|
+
|
12
|
+
/** @type {Set<CKEditorComponent>} */
|
13
|
+
#connectedEditors = new Set();
|
14
|
+
|
15
|
+
async connectedCallback() {
|
16
|
+
try {
|
17
|
+
execIfDOMReady(() => this.#initializeContext());
|
18
|
+
} catch (error) {
|
19
|
+
console.error('Failed to initialize context:', error);
|
20
|
+
this.dispatchEvent(new CustomEvent('context-error', { detail: error }));
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
async attributeChangedCallback(name, oldValue, newValue) {
|
25
|
+
if (oldValue !== null && oldValue !== newValue) {
|
26
|
+
await this.#initializeContext();
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
async disconnectedCallback() {
|
31
|
+
if (this.instance) {
|
32
|
+
await this.instance.destroy();
|
33
|
+
this.instance = null;
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
/**
|
38
|
+
* Register editor component with this context
|
39
|
+
*
|
40
|
+
* @param {CKEditorComponent} editor
|
41
|
+
*/
|
42
|
+
registerEditor(editor) {
|
43
|
+
this.#connectedEditors.add(editor);
|
44
|
+
}
|
45
|
+
|
46
|
+
/**
|
47
|
+
* Unregister editor component from this context
|
48
|
+
*
|
49
|
+
* @param {CKEditorComponent} editor
|
50
|
+
*/
|
51
|
+
unregisterEditor(editor) {
|
52
|
+
this.#connectedEditors.delete(editor);
|
53
|
+
}
|
54
|
+
|
55
|
+
/**
|
56
|
+
* Initialize CKEditor context with shared configuration
|
57
|
+
*
|
58
|
+
* @private
|
59
|
+
*/
|
60
|
+
async #initializeContext() {
|
61
|
+
if (this.instance) {
|
62
|
+
this.instancePromise = Promise.withResolvers();
|
63
|
+
|
64
|
+
await this.instance.destroy();
|
65
|
+
|
66
|
+
this.instance = null;
|
67
|
+
}
|
68
|
+
|
69
|
+
const { Context, ContextWatchdog } = await import('ckeditor5');
|
70
|
+
const plugins = await this.#getPlugins();
|
71
|
+
const config = this.#getConfig();
|
72
|
+
|
73
|
+
this.instance = new ContextWatchdog(Context, {
|
74
|
+
crashNumberLimit: 10
|
75
|
+
});
|
76
|
+
|
77
|
+
await this.instance.create({
|
78
|
+
...config,
|
79
|
+
plugins
|
80
|
+
});
|
81
|
+
|
82
|
+
this.instance.on('itemError', (...args) => {
|
83
|
+
console.error('Context item error:', ...args);
|
84
|
+
});
|
85
|
+
|
86
|
+
this.instancePromise.resolve(this.instance);
|
87
|
+
this.dispatchEvent(new CustomEvent('context-ready', { detail: this.instance }));
|
88
|
+
|
89
|
+
// Reinitialize connected editors.
|
90
|
+
await Promise.all(
|
91
|
+
[...this.#connectedEditors].map(editor => editor.reinitializeEditor())
|
92
|
+
);
|
93
|
+
}
|
94
|
+
|
95
|
+
async #getPlugins() {
|
96
|
+
const raw = this.getAttribute('plugins');
|
97
|
+
|
98
|
+
return loadAsyncImports(raw ? JSON.parse(raw) : []);
|
99
|
+
}
|
100
|
+
|
101
|
+
/**
|
102
|
+
* Gets context configuration with resolved element references.
|
103
|
+
*
|
104
|
+
* @private
|
105
|
+
*/
|
106
|
+
#getConfig() {
|
107
|
+
const config = JSON.parse(this.getAttribute('config') || '{}');
|
108
|
+
|
109
|
+
return resolveElementReferences(config);
|
110
|
+
}
|
111
|
+
}
|
112
|
+
|
113
|
+
customElements.define('ckeditor-context-component', CKEditorContextComponent);
|
@@ -0,0 +1,113 @@
|
|
1
|
+
class CKEditorEditableComponent 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 ['name'];
|
10
|
+
}
|
11
|
+
|
12
|
+
/**
|
13
|
+
* Gets the name of this editable region
|
14
|
+
*
|
15
|
+
* @returns {string} The name attribute value
|
16
|
+
*/
|
17
|
+
get name() {
|
18
|
+
// The default value is set mainly for decoupled editors where the name is not required.
|
19
|
+
return this.getAttribute('name') || 'editable';
|
20
|
+
}
|
21
|
+
|
22
|
+
/**
|
23
|
+
* Gets the actual editable DOM element
|
24
|
+
* @returns {HTMLDivElement|null} The div element containing editable content
|
25
|
+
*/
|
26
|
+
get editableElement() {
|
27
|
+
return this.querySelector('div');
|
28
|
+
}
|
29
|
+
|
30
|
+
/**
|
31
|
+
* Lifecycle callback when element is added to DOM
|
32
|
+
* Sets up the editable element and registers it with the parent editor
|
33
|
+
*
|
34
|
+
* @throws {Error} If not used as child of ckeditor-component
|
35
|
+
*/
|
36
|
+
connectedCallback() {
|
37
|
+
execIfDOMReady(() => {
|
38
|
+
const editorComponent = this.#queryEditorElement();
|
39
|
+
|
40
|
+
if (!editorComponent ) {
|
41
|
+
throw new Error('ckeditor-editable-component must be a child of ckeditor-component');
|
42
|
+
}
|
43
|
+
|
44
|
+
this.innerHTML = `<div>${this.innerHTML}</div>`;
|
45
|
+
this.style.display = 'block';
|
46
|
+
|
47
|
+
if (editorComponent.isDecoupled()) {
|
48
|
+
editorComponent.runAfterEditorReady(editor => {
|
49
|
+
this.appendChild(editor.ui.view[this.name].element);
|
50
|
+
});
|
51
|
+
} else {
|
52
|
+
if (!this.name) {
|
53
|
+
throw new Error('Editable component missing required "name" attribute');
|
54
|
+
}
|
55
|
+
|
56
|
+
editorComponent.editables[this.name] = this;
|
57
|
+
}
|
58
|
+
});
|
59
|
+
}
|
60
|
+
|
61
|
+
/**
|
62
|
+
* Lifecycle callback for attribute changes
|
63
|
+
* Handles name changes and propagates other attributes to editable element
|
64
|
+
*
|
65
|
+
* @param {string} name - Name of changed attribute
|
66
|
+
* @param {string|null} oldValue - Previous value
|
67
|
+
* @param {string|null} newValue - New value
|
68
|
+
*/
|
69
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
70
|
+
if (oldValue === newValue) {
|
71
|
+
return;
|
72
|
+
}
|
73
|
+
|
74
|
+
if (name === 'name') {
|
75
|
+
if (!oldValue) {
|
76
|
+
return;
|
77
|
+
}
|
78
|
+
|
79
|
+
const editorComponent = this.#queryEditorElement();
|
80
|
+
|
81
|
+
if (editorComponent) {
|
82
|
+
editorComponent.editables[newValue] = editorComponent.editables[oldValue];
|
83
|
+
delete editorComponent.editables[oldValue];
|
84
|
+
}
|
85
|
+
} else {
|
86
|
+
this.editableElement.setAttribute(name, newValue);
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
/**
|
91
|
+
* Lifecycle callback when element is removed
|
92
|
+
* Un-registers this editable from the parent editor
|
93
|
+
*/
|
94
|
+
disconnectedCallback() {
|
95
|
+
const editorComponent = this.#queryEditorElement();
|
96
|
+
|
97
|
+
if (editorComponent) {
|
98
|
+
delete editorComponent.editables[this.name];
|
99
|
+
}
|
100
|
+
}
|
101
|
+
|
102
|
+
/**
|
103
|
+
* Finds the parent editor component
|
104
|
+
*
|
105
|
+
* @private
|
106
|
+
* @returns {CKEditorComponent|null} Parent editor component or null if not found
|
107
|
+
*/
|
108
|
+
#queryEditorElement() {
|
109
|
+
return this.closest('ckeditor-component') || document.body.querySelector('ckeditor-component');
|
110
|
+
}
|
111
|
+
}
|
112
|
+
|
113
|
+
customElements.define('ckeditor-editable-component', CKEditorEditableComponent);
|
@@ -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.
|
@@ -63,6 +40,69 @@ class CKEditorComponent extends HTMLElement {
|
|
63
40
|
/** @type {String} ID of editor within context */
|
64
41
|
#contextEditorId = null;
|
65
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
|
+
|
66
106
|
/**
|
67
107
|
* Lifecycle callback when element is connected to DOM
|
68
108
|
* Initializes the editor when DOM is ready
|
@@ -83,7 +123,11 @@ class CKEditorComponent extends HTMLElement {
|
|
83
123
|
});
|
84
124
|
} catch (error) {
|
85
125
|
console.error('Failed to initialize editor:', error);
|
86
|
-
|
126
|
+
|
127
|
+
const event = new CustomEvent('editor-error', { detail: error });
|
128
|
+
|
129
|
+
this.dispatchEvent(event);
|
130
|
+
this.oneditorerror?.(event);
|
87
131
|
}
|
88
132
|
}
|
89
133
|
|
@@ -250,8 +294,6 @@ class CKEditorComponent extends HTMLElement {
|
|
250
294
|
instance = await Editor.create(content, config);
|
251
295
|
}
|
252
296
|
|
253
|
-
this.dispatchEvent(new CustomEvent('editor-ready', { detail: instance }));
|
254
|
-
|
255
297
|
return {
|
256
298
|
contextId,
|
257
299
|
instance,
|
@@ -299,14 +341,50 @@ class CKEditorComponent extends HTMLElement {
|
|
299
341
|
|
300
342
|
this.#setupContentSync();
|
301
343
|
this.#setupEditableHeight();
|
344
|
+
this.#setupDataChangeListener();
|
302
345
|
|
303
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);
|
304
353
|
} catch (err) {
|
305
354
|
this.instancePromise.reject(err);
|
306
355
|
throw err;
|
307
356
|
}
|
308
357
|
}
|
309
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
|
+
|
310
388
|
/**
|
311
389
|
* Checks if current editor is classic type
|
312
390
|
*
|
@@ -515,138 +593,6 @@ class CKEditorComponent extends HTMLElement {
|
|
515
593
|
}
|
516
594
|
}
|
517
595
|
|
518
|
-
/**
|
519
|
-
* Custom element that provides shared CKEditor context for multiple editors.
|
520
|
-
*
|
521
|
-
* @extends HTMLElement
|
522
|
-
* @example
|
523
|
-
*
|
524
|
-
* <ckeditor-context-component plugins='[ ... ]'>
|
525
|
-
* <ckeditor-component type="ClassicEditor" config='{"toolbar": ["bold", "italic"]}'>
|
526
|
-
* <ckeditor-component type="ClassicEditor" config='{"toolbar": ["bold", "italic"]}'>
|
527
|
-
* </ckeditor-component>
|
528
|
-
*/
|
529
|
-
class CKEditorContextComponent extends HTMLElement {
|
530
|
-
static get observedAttributes() {
|
531
|
-
return ['plugins', 'config'];
|
532
|
-
}
|
533
|
-
|
534
|
-
/** @type {import('ckeditor5').Context|null} */
|
535
|
-
instance = null;
|
536
|
-
|
537
|
-
/** @type {Promise<import('ckeditor5').Context>} */
|
538
|
-
instancePromise = Promise.withResolvers();
|
539
|
-
|
540
|
-
/** @type {Set<CKEditorComponent>} */
|
541
|
-
#connectedEditors = new Set();
|
542
|
-
|
543
|
-
async connectedCallback() {
|
544
|
-
try {
|
545
|
-
execIfDOMReady(() => this.#initializeContext());
|
546
|
-
} catch (error) {
|
547
|
-
console.error('Failed to initialize context:', error);
|
548
|
-
this.dispatchEvent(new CustomEvent('context-error', { detail: error }));
|
549
|
-
}
|
550
|
-
}
|
551
|
-
|
552
|
-
async attributeChangedCallback(name, oldValue, newValue) {
|
553
|
-
if (oldValue !== null && oldValue !== newValue) {
|
554
|
-
await this.#initializeContext();
|
555
|
-
}
|
556
|
-
}
|
557
|
-
|
558
|
-
async disconnectedCallback() {
|
559
|
-
if (this.instance) {
|
560
|
-
await this.instance.destroy();
|
561
|
-
this.instance = null;
|
562
|
-
}
|
563
|
-
}
|
564
|
-
|
565
|
-
/**
|
566
|
-
* Register editor component with this context
|
567
|
-
*
|
568
|
-
* @param {CKEditorComponent} editor
|
569
|
-
*/
|
570
|
-
registerEditor(editor) {
|
571
|
-
this.#connectedEditors.add(editor);
|
572
|
-
}
|
573
|
-
|
574
|
-
/**
|
575
|
-
* Unregister editor component from this context
|
576
|
-
*
|
577
|
-
* @param {CKEditorComponent} editor
|
578
|
-
*/
|
579
|
-
unregisterEditor(editor) {
|
580
|
-
this.#connectedEditors.delete(editor);
|
581
|
-
}
|
582
|
-
|
583
|
-
/**
|
584
|
-
* Initialize CKEditor context with shared configuration
|
585
|
-
*
|
586
|
-
* @private
|
587
|
-
*/
|
588
|
-
async #initializeContext() {
|
589
|
-
if (this.instance) {
|
590
|
-
this.instancePromise = Promise.withResolvers();
|
591
|
-
|
592
|
-
await this.instance.destroy();
|
593
|
-
|
594
|
-
this.instance = null;
|
595
|
-
}
|
596
|
-
|
597
|
-
const { Context, ContextWatchdog } = await import('ckeditor5');
|
598
|
-
const plugins = await this.#getPlugins();
|
599
|
-
const config = this.#getConfig();
|
600
|
-
|
601
|
-
this.instance = new ContextWatchdog(Context, {
|
602
|
-
crashNumberLimit: 10
|
603
|
-
});
|
604
|
-
|
605
|
-
await this.instance.create({
|
606
|
-
...config,
|
607
|
-
plugins
|
608
|
-
});
|
609
|
-
|
610
|
-
this.instance.on('itemError', (...args) => {
|
611
|
-
console.error('Context item error:', ...args);
|
612
|
-
});
|
613
|
-
|
614
|
-
this.instancePromise.resolve(this.instance);
|
615
|
-
this.dispatchEvent(new CustomEvent('context-ready', { detail: this.instance }));
|
616
|
-
|
617
|
-
// Reinitialize connected editors.
|
618
|
-
await Promise.all(
|
619
|
-
[...this.#connectedEditors].map(editor => editor.reinitializeEditor())
|
620
|
-
);
|
621
|
-
}
|
622
|
-
|
623
|
-
async #getPlugins() {
|
624
|
-
const raw = this.getAttribute('plugins');
|
625
|
-
|
626
|
-
return loadAsyncImports(raw ? JSON.parse(raw) : []);
|
627
|
-
}
|
628
|
-
|
629
|
-
/**
|
630
|
-
* Gets context configuration with resolved element references.
|
631
|
-
*
|
632
|
-
* @private
|
633
|
-
*/
|
634
|
-
#getConfig() {
|
635
|
-
const config = JSON.parse(this.getAttribute('config') || '{}');
|
636
|
-
|
637
|
-
return resolveElementReferences(config);
|
638
|
-
}
|
639
|
-
}
|
640
|
-
|
641
|
-
/**
|
642
|
-
* Tracks and manages editable roots for CKEditor MultiRoot editor.
|
643
|
-
* Provides a proxy-based API for dynamically managing editable elements with automatic
|
644
|
-
* attachment/detachment of editor roots.
|
645
|
-
*
|
646
|
-
* @class
|
647
|
-
* @property {CKEditorComponent} #editorElement - Reference to parent editor component
|
648
|
-
* @property {Record<string, HTMLElement>} #editables - Map of tracked editable elements
|
649
|
-
*/
|
650
596
|
class CKEditorMultiRootEditablesTracker {
|
651
597
|
#editorElement;
|
652
598
|
#editables;
|
@@ -765,325 +711,4 @@ class CKEditorMultiRootEditablesTracker {
|
|
765
711
|
}
|
766
712
|
}
|
767
713
|
|
768
|
-
/**
|
769
|
-
* Custom HTML element representing an editable region for CKEditor.
|
770
|
-
* Must be used as a child of ckeditor-component element.
|
771
|
-
*
|
772
|
-
* @customElement ckeditor-editable-component
|
773
|
-
* @extends HTMLElement
|
774
|
-
*
|
775
|
-
* @property {string} name - The name of the editable region, accessed via getAttribute
|
776
|
-
* @property {HTMLDivElement} editableElement - The div element containing editable content
|
777
|
-
*
|
778
|
-
* @fires connectedCallback - When the element is added to the DOM
|
779
|
-
* @fires attributeChangedCallback - When element attributes change
|
780
|
-
* @fires disconnectedCallback - When the element is removed from the DOM
|
781
|
-
*
|
782
|
-
* @throws {Error} Throws error if not used as child of ckeditor-component
|
783
|
-
*
|
784
|
-
* @example
|
785
|
-
* <ckeditor-component>
|
786
|
-
* <ckeditor-editable-component name="main">
|
787
|
-
* Content goes here
|
788
|
-
* </ckeditor-editable-component>
|
789
|
-
* </ckeditor-component>
|
790
|
-
*/
|
791
|
-
class CKEditorEditableComponent extends HTMLElement {
|
792
|
-
/**
|
793
|
-
* List of attributes that trigger updates when changed
|
794
|
-
*
|
795
|
-
* @static
|
796
|
-
* @returns {string[]} Array of attribute names to observe
|
797
|
-
*/
|
798
|
-
static get observedAttributes() {
|
799
|
-
return ['name'];
|
800
|
-
}
|
801
|
-
|
802
|
-
/**
|
803
|
-
* Gets the name of this editable region
|
804
|
-
*
|
805
|
-
* @returns {string} The name attribute value
|
806
|
-
*/
|
807
|
-
get name() {
|
808
|
-
// The default value is set mainly for decoupled editors where the name is not required.
|
809
|
-
return this.getAttribute('name') || 'editable';
|
810
|
-
}
|
811
|
-
|
812
|
-
/**
|
813
|
-
* Gets the actual editable DOM element
|
814
|
-
* @returns {HTMLDivElement|null} The div element containing editable content
|
815
|
-
*/
|
816
|
-
get editableElement() {
|
817
|
-
return this.querySelector('div');
|
818
|
-
}
|
819
|
-
|
820
|
-
/**
|
821
|
-
* Lifecycle callback when element is added to DOM
|
822
|
-
* Sets up the editable element and registers it with the parent editor
|
823
|
-
*
|
824
|
-
* @throws {Error} If not used as child of ckeditor-component
|
825
|
-
*/
|
826
|
-
connectedCallback() {
|
827
|
-
execIfDOMReady(() => {
|
828
|
-
const editorComponent = this.#queryEditorElement();
|
829
|
-
|
830
|
-
if (!editorComponent ) {
|
831
|
-
throw new Error('ckeditor-editable-component must be a child of ckeditor-component');
|
832
|
-
}
|
833
|
-
|
834
|
-
this.innerHTML = `<div>${this.innerHTML}</div>`;
|
835
|
-
this.style.display = 'block';
|
836
|
-
|
837
|
-
if (editorComponent.isDecoupled()) {
|
838
|
-
editorComponent.runAfterEditorReady(editor => {
|
839
|
-
this.appendChild(editor.ui.view[this.name].element);
|
840
|
-
});
|
841
|
-
} else {
|
842
|
-
if (!this.name) {
|
843
|
-
throw new Error('Editable component missing required "name" attribute');
|
844
|
-
}
|
845
|
-
|
846
|
-
editorComponent.editables[this.name] = this;
|
847
|
-
}
|
848
|
-
});
|
849
|
-
}
|
850
|
-
|
851
|
-
/**
|
852
|
-
* Lifecycle callback for attribute changes
|
853
|
-
* Handles name changes and propagates other attributes to editable element
|
854
|
-
*
|
855
|
-
* @param {string} name - Name of changed attribute
|
856
|
-
* @param {string|null} oldValue - Previous value
|
857
|
-
* @param {string|null} newValue - New value
|
858
|
-
*/
|
859
|
-
attributeChangedCallback(name, oldValue, newValue) {
|
860
|
-
if (oldValue === newValue) {
|
861
|
-
return;
|
862
|
-
}
|
863
|
-
|
864
|
-
if (name === 'name') {
|
865
|
-
if (!oldValue) {
|
866
|
-
return;
|
867
|
-
}
|
868
|
-
|
869
|
-
const editorComponent = this.#queryEditorElement();
|
870
|
-
|
871
|
-
if (editorComponent) {
|
872
|
-
editorComponent.editables[newValue] = editorComponent.editables[oldValue];
|
873
|
-
delete editorComponent.editables[oldValue];
|
874
|
-
}
|
875
|
-
} else {
|
876
|
-
this.editableElement.setAttribute(name, newValue);
|
877
|
-
}
|
878
|
-
}
|
879
|
-
|
880
|
-
/**
|
881
|
-
* Lifecycle callback when element is removed
|
882
|
-
* Un-registers this editable from the parent editor
|
883
|
-
*/
|
884
|
-
disconnectedCallback() {
|
885
|
-
const editorComponent = this.#queryEditorElement();
|
886
|
-
|
887
|
-
if (editorComponent) {
|
888
|
-
delete editorComponent.editables[this.name];
|
889
|
-
}
|
890
|
-
}
|
891
|
-
|
892
|
-
/**
|
893
|
-
* Finds the parent editor component
|
894
|
-
*
|
895
|
-
* @private
|
896
|
-
* @returns {CKEditorComponent|null} Parent editor component or null if not found
|
897
|
-
*/
|
898
|
-
#queryEditorElement() {
|
899
|
-
return this.closest('ckeditor-component') || document.body.querySelector('ckeditor-component');
|
900
|
-
}
|
901
|
-
}
|
902
|
-
|
903
|
-
/**
|
904
|
-
* Custom HTML element that represents a CKEditor UI part component.
|
905
|
-
* It helpers with management of toolbar and other elements.
|
906
|
-
*
|
907
|
-
* @extends HTMLElement
|
908
|
-
* @customElement ckeditor-ui-part-component
|
909
|
-
* @example
|
910
|
-
* <ckeditor-ui-part-component></ckeditor-ui-part-component>
|
911
|
-
*/
|
912
|
-
class CKEditorUIPartComponent extends HTMLElement {
|
913
|
-
/**
|
914
|
-
* Lifecycle callback when element is added to DOM
|
915
|
-
* Adds the toolbar to the editor UI
|
916
|
-
*/
|
917
|
-
connectedCallback() {
|
918
|
-
execIfDOMReady(async () => {
|
919
|
-
const uiPart = this.getAttribute('name');
|
920
|
-
const editor = await this.#queryEditorElement().instancePromise.promise;
|
921
|
-
|
922
|
-
this.appendChild(editor.ui.view[uiPart].element);
|
923
|
-
});
|
924
|
-
}
|
925
|
-
|
926
|
-
/**
|
927
|
-
* Finds the parent editor component
|
928
|
-
*
|
929
|
-
* @private
|
930
|
-
* @returns {CKEditorComponent|null} Parent editor component or null if not found
|
931
|
-
*/
|
932
|
-
#queryEditorElement() {
|
933
|
-
return this.closest('ckeditor-component') || document.body.querySelector('ckeditor-component');
|
934
|
-
}
|
935
|
-
}
|
936
|
-
|
937
|
-
/**
|
938
|
-
* Executes callback when DOM is ready
|
939
|
-
*
|
940
|
-
* @param {() => void} callback - Function to execute
|
941
|
-
*/
|
942
|
-
function execIfDOMReady(callback) {
|
943
|
-
switch (document.readyState) {
|
944
|
-
case 'loading':
|
945
|
-
document.addEventListener('DOMContentLoaded', callback, { once: true });
|
946
|
-
break;
|
947
|
-
|
948
|
-
case 'interactive':
|
949
|
-
case 'complete':
|
950
|
-
setTimeout(callback, 0);
|
951
|
-
break;
|
952
|
-
|
953
|
-
default:
|
954
|
-
console.warn('Unexpected document.readyState:', document.readyState);
|
955
|
-
setTimeout(callback, 0);
|
956
|
-
}
|
957
|
-
}
|
958
|
-
|
959
|
-
/**
|
960
|
-
* Dynamically imports modules based on configuration
|
961
|
-
*
|
962
|
-
* @param {Array<ImportConfig>} imports - Array of import configurations
|
963
|
-
* @returns {Promise<Array<any>>} Loaded modules
|
964
|
-
*/
|
965
|
-
function loadAsyncImports(imports = []) {
|
966
|
-
const loadInlinePlugin = async ({ name, code }) => {
|
967
|
-
const module = await import(`data:text/javascript,${encodeURIComponent(code)}`);
|
968
|
-
|
969
|
-
if (!module.default) {
|
970
|
-
throw new Error(`Inline plugin "${name}" must export a default class/function!`);
|
971
|
-
}
|
972
|
-
|
973
|
-
return module.default;
|
974
|
-
};
|
975
|
-
|
976
|
-
const loadExternalPlugin = async ({ import_name, import_as, window_name }) => {
|
977
|
-
if (window_name) {
|
978
|
-
if (!Object.prototype.hasOwnProperty.call(window, window_name)) {
|
979
|
-
throw new Error(
|
980
|
-
`Plugin window['${window_name}'] not found in global scope. ` +
|
981
|
-
'Please ensure the plugin is loaded before CKEditor initialization.'
|
982
|
-
);
|
983
|
-
}
|
984
|
-
|
985
|
-
return window[window_name];
|
986
|
-
}
|
987
|
-
|
988
|
-
const module = await import(import_name);
|
989
|
-
const imported = module[import_as || 'default'];
|
990
|
-
|
991
|
-
if (!imported) {
|
992
|
-
throw new Error(`Plugin "${import_as}" not found in the ESM module "${import_name}"!`);
|
993
|
-
}
|
994
|
-
|
995
|
-
return imported;
|
996
|
-
};
|
997
|
-
|
998
|
-
return Promise.all(imports.map(item => {
|
999
|
-
switch(item.type) {
|
1000
|
-
case 'inline':
|
1001
|
-
return loadInlinePlugin(item);
|
1002
|
-
|
1003
|
-
case 'external':
|
1004
|
-
default:
|
1005
|
-
return loadExternalPlugin(item);
|
1006
|
-
}
|
1007
|
-
}));
|
1008
|
-
}
|
1009
|
-
|
1010
|
-
|
1011
|
-
/**
|
1012
|
-
* Checks if a key is safe to use in configuration objects to prevent prototype pollution.
|
1013
|
-
*
|
1014
|
-
* @param {string} key - Key name to check
|
1015
|
-
* @returns {boolean} True if key is safe to use.
|
1016
|
-
*/
|
1017
|
-
function isSafeKey(key) {
|
1018
|
-
return typeof key === 'string' &&
|
1019
|
-
key !== '__proto__' &&
|
1020
|
-
key !== 'constructor' &&
|
1021
|
-
key !== 'prototype';
|
1022
|
-
}
|
1023
|
-
|
1024
|
-
/**
|
1025
|
-
* Resolves element references in configuration object.
|
1026
|
-
* Looks for objects with { $element: "selector" } format and replaces them with actual elements.
|
1027
|
-
*
|
1028
|
-
* @param {Object} obj - Configuration object to process
|
1029
|
-
* @returns {Object} Processed configuration object with resolved element references
|
1030
|
-
*/
|
1031
|
-
function resolveElementReferences(obj) {
|
1032
|
-
if (!obj || typeof obj !== 'object') {
|
1033
|
-
return obj;
|
1034
|
-
}
|
1035
|
-
|
1036
|
-
if (Array.isArray(obj)) {
|
1037
|
-
return obj.map(item => resolveElementReferences(item));
|
1038
|
-
}
|
1039
|
-
|
1040
|
-
const result = Object.create(null);
|
1041
|
-
|
1042
|
-
for (const key of Object.getOwnPropertyNames(obj)) {
|
1043
|
-
if (!isSafeKey(key)) {
|
1044
|
-
console.warn(`Suspicious key "${key}" detected in config, skipping`);
|
1045
|
-
continue;
|
1046
|
-
}
|
1047
|
-
|
1048
|
-
const value = obj[key];
|
1049
|
-
|
1050
|
-
if (value && typeof value === 'object') {
|
1051
|
-
if (value.$element) {
|
1052
|
-
const selector = value.$element;
|
1053
|
-
|
1054
|
-
if (typeof selector !== 'string') {
|
1055
|
-
console.warn(`Invalid selector type for "${key}", expected string`);
|
1056
|
-
continue;
|
1057
|
-
}
|
1058
|
-
|
1059
|
-
const element = document.querySelector(selector);
|
1060
|
-
|
1061
|
-
if (!element) {
|
1062
|
-
console.warn(`Element not found for selector: ${selector}`);
|
1063
|
-
}
|
1064
|
-
|
1065
|
-
result[key] = element || null;
|
1066
|
-
} else {
|
1067
|
-
result[key] = resolveElementReferences(value);
|
1068
|
-
}
|
1069
|
-
} else {
|
1070
|
-
result[key] = value;
|
1071
|
-
}
|
1072
|
-
}
|
1073
|
-
|
1074
|
-
return result;
|
1075
|
-
}
|
1076
|
-
|
1077
|
-
/**
|
1078
|
-
* Custom element that provides shared CKEditor context for multiple editors.
|
1079
|
-
*
|
1080
|
-
* @returns {String} unique id
|
1081
|
-
*/
|
1082
|
-
function uid() {
|
1083
|
-
return Math.random().toString(36).substring(2);
|
1084
|
-
}
|
1085
|
-
|
1086
714
|
customElements.define('ckeditor-component', CKEditorComponent);
|
1087
|
-
customElements.define('ckeditor-editable-component', CKEditorEditableComponent);
|
1088
|
-
customElements.define('ckeditor-ui-part-component', CKEditorUIPartComponent);
|
1089
|
-
customElements.define('ckeditor-context-component', CKEditorContextComponent);
|
@@ -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);
|
@@ -0,0 +1,155 @@
|
|
1
|
+
/**
|
2
|
+
* Executes callback when DOM is ready
|
3
|
+
*
|
4
|
+
* @param {() => void} callback - Function to execute when DOM is ready
|
5
|
+
*/
|
6
|
+
function execIfDOMReady(callback) {
|
7
|
+
switch (document.readyState) {
|
8
|
+
case 'loading':
|
9
|
+
document.addEventListener('DOMContentLoaded', callback, { once: true });
|
10
|
+
break;
|
11
|
+
|
12
|
+
case 'interactive':
|
13
|
+
case 'complete':
|
14
|
+
setTimeout(callback, 0);
|
15
|
+
break;
|
16
|
+
|
17
|
+
default:
|
18
|
+
console.warn('Unexpected document.readyState:', document.readyState);
|
19
|
+
setTimeout(callback, 0);
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
23
|
+
/**
|
24
|
+
* Dynamically imports modules based on configuration
|
25
|
+
*
|
26
|
+
* @param {Array<Object>} imports - Array of import configurations
|
27
|
+
* @param {Object} imports[].name - Name of inline plugin (for inline type)
|
28
|
+
* @param {Object} imports[].code - Source code of inline plugin (for inline type)
|
29
|
+
* @param {Object} imports[].import_name - Module path to import (for external type)
|
30
|
+
* @param {Object} imports[].import_as - Name to import as (for external type)
|
31
|
+
* @param {Object} imports[].window_name - Global window object name (for external type)
|
32
|
+
* @param {('inline'|'external')} imports[].type - Type of import
|
33
|
+
* @returns {Promise<Array<any>>} Array of loaded modules
|
34
|
+
* @throws {Error} When plugin loading fails
|
35
|
+
*/
|
36
|
+
function loadAsyncImports(imports = []) {
|
37
|
+
const loadInlinePlugin = async ({ name, code }) => {
|
38
|
+
const module = await import(`data:text/javascript,${encodeURIComponent(code)}`);
|
39
|
+
|
40
|
+
if (!module.default) {
|
41
|
+
throw new Error(`Inline plugin "${name}" must export a default class/function!`);
|
42
|
+
}
|
43
|
+
|
44
|
+
return module.default;
|
45
|
+
};
|
46
|
+
|
47
|
+
const loadExternalPlugin = async ({ import_name, import_as, window_name }) => {
|
48
|
+
if (window_name) {
|
49
|
+
if (!Object.prototype.hasOwnProperty.call(window, window_name)) {
|
50
|
+
throw new Error(
|
51
|
+
`Plugin window['${window_name}'] not found in global scope. ` +
|
52
|
+
'Please ensure the plugin is loaded before CKEditor initialization.'
|
53
|
+
);
|
54
|
+
}
|
55
|
+
|
56
|
+
return window[window_name];
|
57
|
+
}
|
58
|
+
|
59
|
+
const module = await import(import_name);
|
60
|
+
const imported = module[import_as || 'default'];
|
61
|
+
|
62
|
+
if (!imported) {
|
63
|
+
throw new Error(`Plugin "${import_as}" not found in the ESM module "${import_name}"!`);
|
64
|
+
}
|
65
|
+
|
66
|
+
return imported;
|
67
|
+
};
|
68
|
+
|
69
|
+
return Promise.all(imports.map(item => {
|
70
|
+
switch(item.type) {
|
71
|
+
case 'inline':
|
72
|
+
return loadInlinePlugin(item);
|
73
|
+
|
74
|
+
case 'external':
|
75
|
+
default:
|
76
|
+
return loadExternalPlugin(item);
|
77
|
+
}
|
78
|
+
}));
|
79
|
+
}
|
80
|
+
|
81
|
+
/**
|
82
|
+
* Checks if a key is safe to use in configuration objects to prevent prototype pollution
|
83
|
+
*
|
84
|
+
* @param {string} key - Key name to check
|
85
|
+
* @returns {boolean} True if key is safe to use
|
86
|
+
*/
|
87
|
+
function isSafeKey(key) {
|
88
|
+
return typeof key === 'string' &&
|
89
|
+
key !== '__proto__' &&
|
90
|
+
key !== 'constructor' &&
|
91
|
+
key !== 'prototype';
|
92
|
+
}
|
93
|
+
|
94
|
+
/**
|
95
|
+
* Resolves element references in configuration object.
|
96
|
+
* Looks for objects with { $element: "selector" } format and replaces them with actual DOM elements.
|
97
|
+
*
|
98
|
+
* @param {Object} obj - Configuration object to process
|
99
|
+
* @returns {Object} Processed configuration object with resolved element references
|
100
|
+
* @throws {Error} When element reference is invalid
|
101
|
+
*/
|
102
|
+
function resolveElementReferences(obj) {
|
103
|
+
if (!obj || typeof obj !== 'object') {
|
104
|
+
return obj;
|
105
|
+
}
|
106
|
+
|
107
|
+
if (Array.isArray(obj)) {
|
108
|
+
return obj.map(item => resolveElementReferences(item));
|
109
|
+
}
|
110
|
+
|
111
|
+
const result = Object.create(null);
|
112
|
+
|
113
|
+
for (const key of Object.getOwnPropertyNames(obj)) {
|
114
|
+
if (!isSafeKey(key)) {
|
115
|
+
console.warn(`Suspicious key "${key}" detected in config, skipping`);
|
116
|
+
continue;
|
117
|
+
}
|
118
|
+
|
119
|
+
const value = obj[key];
|
120
|
+
|
121
|
+
if (value && typeof value === 'object') {
|
122
|
+
if (value.$element) {
|
123
|
+
const selector = value.$element;
|
124
|
+
|
125
|
+
if (typeof selector !== 'string') {
|
126
|
+
console.warn(`Invalid selector type for "${key}", expected string`);
|
127
|
+
continue;
|
128
|
+
}
|
129
|
+
|
130
|
+
const element = document.querySelector(selector);
|
131
|
+
|
132
|
+
if (!element) {
|
133
|
+
console.warn(`Element not found for selector: ${selector}`);
|
134
|
+
}
|
135
|
+
|
136
|
+
result[key] = element || null;
|
137
|
+
} else {
|
138
|
+
result[key] = resolveElementReferences(value);
|
139
|
+
}
|
140
|
+
} else {
|
141
|
+
result[key] = value;
|
142
|
+
}
|
143
|
+
}
|
144
|
+
|
145
|
+
return result;
|
146
|
+
}
|
147
|
+
|
148
|
+
/**
|
149
|
+
* Generates a unique identifier string
|
150
|
+
*
|
151
|
+
* @returns {string} Random string that can be used as unique identifier
|
152
|
+
*/
|
153
|
+
function uid() {
|
154
|
+
return Math.random().toString(36).substring(2);
|
155
|
+
}
|
@@ -6,7 +6,7 @@ require_relative 'ckbox_bundle'
|
|
6
6
|
|
7
7
|
module CKEditor5::Rails
|
8
8
|
module Cdn::Helpers
|
9
|
-
def
|
9
|
+
def ckeditor5_assets(preset: :default, **kwargs)
|
10
10
|
merge_with_editor_preset(preset, **kwargs) => {
|
11
11
|
cdn:,
|
12
12
|
version:,
|
@@ -32,15 +32,7 @@ module CKEditor5::Rails
|
|
32
32
|
|
33
33
|
Cdn::UrlGenerator::CDN_THIRD_PARTY_GENERATORS.each_key do |key|
|
34
34
|
define_method(:"ckeditor5_#{key.to_s.parameterize}_assets") do |**kwargs|
|
35
|
-
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
def ckeditor5_assets(**kwargs)
|
40
|
-
if kwargs[:license_key] && kwargs[:license_key] != 'GPL'
|
41
|
-
ckeditor5_cloud_assets(**kwargs)
|
42
|
-
else
|
43
|
-
ckeditor5_cdn_assets(**kwargs.merge(cdn: Engine.default_preset.cdn))
|
35
|
+
ckeditor5_assets(**kwargs.merge(cdn: key))
|
44
36
|
end
|
45
37
|
end
|
46
38
|
|
@@ -62,7 +54,7 @@ module CKEditor5::Rails
|
|
62
54
|
|
63
55
|
raise ArgumentError,
|
64
56
|
"Poor thing. You forgot to define #{key}. Make sure you passed `#{key}:` parameter to " \
|
65
|
-
"`
|
57
|
+
"`ckeditor5_assets` or defined default one in your `#{preset}` preset!"
|
66
58
|
end
|
67
59
|
|
68
60
|
hash
|
@@ -1,14 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'cdn/helpers'
|
4
|
-
require_relative 'cloud/helpers'
|
5
4
|
require_relative 'editor/helpers'
|
6
5
|
require_relative 'context/helpers'
|
7
6
|
|
8
7
|
module CKEditor5::Rails
|
9
8
|
module Helpers
|
10
9
|
include Cdn::Helpers
|
11
|
-
include Cloud::Helpers
|
12
10
|
include Editor::Helpers
|
13
11
|
include Context::Helpers
|
14
12
|
end
|
@@ -22,6 +22,18 @@ module CKEditor5::Rails
|
|
22
22
|
}
|
23
23
|
end
|
24
24
|
|
25
|
+
def premium?
|
26
|
+
@premium
|
27
|
+
end
|
28
|
+
|
29
|
+
def gpl?
|
30
|
+
license_key == 'GPL'
|
31
|
+
end
|
32
|
+
|
33
|
+
def menubar?
|
34
|
+
@config.dig(:menuBar, :isVisible) || false
|
35
|
+
end
|
36
|
+
|
25
37
|
def to_h_with_overrides(**overrides)
|
26
38
|
{
|
27
39
|
version: overrides.fetch(:version, version),
|
@@ -51,6 +63,8 @@ module CKEditor5::Rails
|
|
51
63
|
return @license_key if license_key.nil?
|
52
64
|
|
53
65
|
@license_key = license_key
|
66
|
+
|
67
|
+
cdn(:cloud) unless gpl?
|
54
68
|
end
|
55
69
|
|
56
70
|
def gpl
|
@@ -134,7 +148,9 @@ module CKEditor5::Rails
|
|
134
148
|
names.each { |name| plugin(name, **kwargs) }
|
135
149
|
end
|
136
150
|
|
137
|
-
def language(ui, content: ui) # rubocop:disable Naming/MethodParameterName
|
151
|
+
def language(ui = nil, content: ui) # rubocop:disable Naming/MethodParameterName
|
152
|
+
return @config[:language] if ui.nil?
|
153
|
+
|
138
154
|
@config[:language] = {
|
139
155
|
ui: ui,
|
140
156
|
content: content
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ckeditor5
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mateusz Bagiński
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2024-11-
|
12
|
+
date: 2024-11-19 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -45,12 +45,16 @@ files:
|
|
45
45
|
- lib/ckeditor5/rails.rb
|
46
46
|
- lib/ckeditor5/rails/assets/assets_bundle.rb
|
47
47
|
- lib/ckeditor5/rails/assets/assets_bundle_html_serializer.rb
|
48
|
-
- lib/ckeditor5/rails/assets/
|
48
|
+
- lib/ckeditor5/rails/assets/webcomponent_bundle.rb
|
49
|
+
- lib/ckeditor5/rails/assets/webcomponents/components/context.mjs
|
50
|
+
- lib/ckeditor5/rails/assets/webcomponents/components/editable.mjs
|
51
|
+
- lib/ckeditor5/rails/assets/webcomponents/components/editor.mjs
|
52
|
+
- lib/ckeditor5/rails/assets/webcomponents/components/ui-part.mjs
|
53
|
+
- lib/ckeditor5/rails/assets/webcomponents/utils.mjs
|
49
54
|
- lib/ckeditor5/rails/cdn/ckbox_bundle.rb
|
50
55
|
- lib/ckeditor5/rails/cdn/ckeditor_bundle.rb
|
51
56
|
- lib/ckeditor5/rails/cdn/helpers.rb
|
52
57
|
- lib/ckeditor5/rails/cdn/url_generator.rb
|
53
|
-
- lib/ckeditor5/rails/cloud/helpers.rb
|
54
58
|
- lib/ckeditor5/rails/context/helpers.rb
|
55
59
|
- lib/ckeditor5/rails/context/props.rb
|
56
60
|
- lib/ckeditor5/rails/editor/config_helpers.rb
|
@@ -1,13 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module CKEditor5::Rails
|
4
|
-
module Cloud
|
5
|
-
module Helpers
|
6
|
-
def ckeditor5_cloud_assets(license_key:, **kwargs)
|
7
|
-
raise 'Cloud assets are not permitted in GPL license!' if license_key == 'GPL'
|
8
|
-
|
9
|
-
ckeditor5_cdn_assets(cdn: :cloud, license_key: license_key, **kwargs)
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|