ckeditor5 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 18736efccfe3547f18ff9df5418fd5f93aafa426423c3ca8daca99de481c6c60
4
- data.tar.gz: 6be07d38f24ba131e3a8444175b8416561706a97d74bd581d8568703ef3544f2
3
+ metadata.gz: e3e06c4a4882429855d9570658be64f10fffd97371d0ae60e9aee2ecec08df00
4
+ data.tar.gz: f48eabec0f1bcab9299be0f00094f2e1078a757582a705e57d504748943b5790
5
5
  SHA512:
6
- metadata.gz: c40e50c2093c7f8c311b8565a54ddc4835aba887ced1e36b4802c6681769a8f2e4f39812443e688f6e31ff4932eef76282664be82d7cdbb4b667c9febe76374f
7
- data.tar.gz: f9a999fb8a325323e497b225f3c8bed54cc24befc5bcd2cc90a93a25dece1d539e5e33a34fbe2a4f9570e462527492e061819563b7fad19e61d24fae8a39953e
6
+ metadata.gz: c989f7723dadc126c356b579f50160b09f3c359af8a791a919bcff0595c7387241f89f5a63a100c482878d0f067c094f45619628094d6a8c0659d797d75cb514
7
+ data.tar.gz: a411bbde271725fbaeddfc58023307837ffac5e26d4814ed13e922288846129973a2b297681076b480982485a91ed4b5c527056cf2b9b799cfbb321a5ed83762
data/README.md CHANGED
@@ -101,6 +101,9 @@ Voilà! You have CKEditor 5 integrated with your Rails application. 🎉
101
101
  - [Inline editor 📝](#inline-editor-)
102
102
  - [Balloon editor 🎈](#balloon-editor-)
103
103
  - [Decoupled editor 🌐](#decoupled-editor-)
104
+ - [Using Context 📦](#using-context-)
105
+ - [Using Context in CKEditor 5 🔄](#using-context-in-ckeditor-5-)
106
+ - [Example usage of `ckeditor5_context` helper 📝](#example-usage-of-ckeditor5_context-helper-)
104
107
  - [How to access editor instance? 🤔](#how-to-access-editor-instance-)
105
108
  - [Common Tasks and Solutions 💡](#common-tasks-and-solutions-)
106
109
  - [Setting Editor Language 🌐](#setting-editor-language-)
@@ -112,6 +115,8 @@ Voilà! You have CKEditor 5 integrated with your Rails application. 🎉
112
115
  - [Events fired by the editor 🔊](#events-fired-by-the-editor-)
113
116
  - [`editor-ready` event](#editor-ready-event)
114
117
  - [`editor-error` event](#editor-error-event)
118
+ - [`editor-change` event](#editor-change-event)
119
+ - [Inline event handling](#inline-event-handling)
115
120
  - [Trademarks 📜](#trademarks-)
116
121
  - [License 📜](#license-)
117
122
 
@@ -184,9 +189,6 @@ Configuration of the editor can be complex, and it's recommended to use the [CKE
184
189
 
185
190
  ### Available Configuration Methods ⚙️
186
191
 
187
- <details>
188
- <summary>Expand to show available methods 📖</summary>
189
-
190
192
  #### `cdn(cdn = nil, &block)` method
191
193
 
192
194
  Defines the CDN to be used for CKEditor 5 assets. The example below shows how to set the CDN to `:jsdelivr`:
@@ -569,8 +571,6 @@ CKEditor5::Rails.configure do
569
571
  end
570
572
  ```
571
573
 
572
- </details>
573
-
574
574
  ## Including CKEditor 5 assets 📦
575
575
 
576
576
  To include CKEditor 5 assets in your application, you can use the `ckeditor5_assets` helper method. This method takes the version of CKEditor 5 as an argument and includes the necessary resources of the editor. Depending on the specified configuration, it includes the JS and CSS assets from the official CKEditor 5 CDN or one of the popular CDNs.
@@ -965,6 +965,45 @@ If you want to use a decoupled editor, you can pass the `type` keyword argument
965
965
  <% end %>
966
966
  ```
967
967
 
968
+ ## Using Context 📦
969
+
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.
971
+
972
+ ![CKEditor 5 Context](docs/context.png)
973
+
974
+ ### Using Context in CKEditor 5 🔄
975
+
976
+ Format of the `ckeditor5_context` helper:
977
+
978
+ ```erb
979
+ <!-- app/views/demos/index.html.erb -->
980
+
981
+ <%= ckeditor5_context config: { ... }, plugins: [ ... ] do %>
982
+ <%= ckeditor5_editor %>
983
+ <%= ckeditor5_editor %>
984
+ <% end %>
985
+ ```
986
+
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 📝
990
+
991
+ ```erb
992
+ <!-- app/views/demos/index.html.erb -->
993
+
994
+ <% content_for :head do %>
995
+ <%= ckeditor5_assets preset: :ultrabasic %>
996
+ <% end %>
997
+
998
+ <%= ckeditor5_context do %>
999
+ <%= ckeditor5_editor initial_data: 'Hello World' %>
1000
+
1001
+ <br>
1002
+
1003
+ <%= ckeditor5_editor initial_data: 'Hello World 2' %>
1004
+ <% end %>
1005
+ ```
1006
+
968
1007
  ## How to access editor instance? 🤔
969
1008
 
970
1009
  You can access the editor instance using plain HTML and JavaScript, as CKEditor 5 is a web component with defined `instance`, `instancePromise` and `editables` properties.
@@ -1222,6 +1261,8 @@ class HighlightCommand extends Command {
1222
1261
 
1223
1262
  ## Events fired by the editor 🔊
1224
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
+
1225
1266
  ### `editor-ready` event
1226
1267
 
1227
1268
  The event is fired when the initialization of the editor is completed. You can listen to it using the `editor-ready` event.
@@ -1242,6 +1283,40 @@ document.getElementById('editor').addEventListener('editor-error', () => {
1242
1283
  });
1243
1284
  ```
1244
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
+
1245
1320
  ## Trademarks 📜
1246
1321
 
1247
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/).
@@ -1250,6 +1325,6 @@ This gem is not owned by CKSource and does not use the CKEditor® trademark for
1250
1325
 
1251
1326
  ## License 📜
1252
1327
 
1253
- 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.
1254
1329
 
1255
- 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
- module CKEditor5::Rails::Assets
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(WEBCOMPONENT_SOURCE, type: 'module', nonce: true)
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);