ckeditor5 1.8.0 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
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);