ckeditor5 1.9.0 → 1.11.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: bb520612d3f2847e8d70c48a8ffb3d1ad4be25800dd5f141a6948caf30b07978
4
- data.tar.gz: 5f9850fb6a39774ab796f67b5dc95fa0ec1e6a5fe6f04fb3481f6ff17c76e026
3
+ metadata.gz: cbd754782a22b999b5843eedf90b499ba82f44dfac2ceb51f3c46898800482e2
4
+ data.tar.gz: c7945b1c9efeb3579a70ac0aaa832bac46147c512b9de98dff3dbf5a3187afad
5
5
  SHA512:
6
- metadata.gz: 7057a3e537f5626b4754b815c1f418ddac64301350aa3b0299d5051dcda474a5f397018389a0e7c773bc1d15687043498a2bcd44518581e4233f8ab4ae85dc52
7
- data.tar.gz: 0526b5810f411971ce3adafa1bad252eabaffc566f3d637b6585c2a16a6d6c938078e8b88ed88b34e610477c87c66f5dfc74d092fab2f46f183327f2fca9d287
6
+ metadata.gz: e1263373ca9e36adf9d63b8ff23d88933231cdacb8378cedc9f17819ae59924064e04bec14731e3907808af138ecd2afc4cd80cb2db9b943f221d1b8d9ccdf43
7
+ data.tar.gz: a8c822abfc9f0f191785d2cea17d5eb7d5d9696c2d3337c9fbdd4ff302f9ef1bf2b46fca90cc9b221f00c1a37c0d124c92fb74dde125dc5e8cd2aaf9b683f8bc
data/Gemfile CHANGED
@@ -6,11 +6,21 @@ gem 'bundler', '~> 2.5', '>= 2.5.21'
6
6
 
7
7
  group :development do
8
8
  gem 'brakeman', '~> 6.1', '>= 6.1.1', require: false
9
+ gem 'guard', '~> 2.19', '>= 2.19.0'
10
+ gem 'guard-process', '~> 1.2'
11
+ gem 'pry', '~> 0.15', '>= 0.15.0'
12
+ gem 'rails', '~> 7.0', '>= 7.0.0'
9
13
  gem 'rake', '~> 13.2', '>= 13.2.1'
10
14
  gem 'rspec', '~> 3.13'
15
+ gem 'rspec-expectations', '~> 3.13'
16
+ gem 'rspec-rails', '~> 7.0'
11
17
  gem 'rubocop', '~> 1.66', require: false
12
18
  gem 'rubocop-rails', '~> 2.26', '>= 2.26.2', require: false
13
19
  gem 'rubocop-rails-omakase', '~> 1.0.0', require: false
20
+ gem 'simple_form', '~> 5.3', '>= 5.3.0'
21
+ gem 'slim', '~> 5.2', '>= 5.2.0'
22
+ gem 'sprockets-rails', '~> 3.2', '>= 3.2.2'
23
+ gem 'sqlite3', '>= 1.4'
14
24
  end
15
25
 
16
26
  group :test, :development do
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,9 @@ 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)
120
+ - [Gem Development 🛠](#gem-development-)
118
121
  - [Trademarks 📜](#trademarks-)
119
122
  - [License 📜](#license-)
120
123
 
@@ -967,25 +970,24 @@ If you want to use a decoupled editor, you can pass the `type` keyword argument
967
970
 
968
971
  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
972
 
970
- ### Benefits of Using Context in Collaboration 🤝
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.
973
+ ![CKEditor 5 Context](docs/context.png)
976
974
 
977
975
  ### Using Context in CKEditor 5 🔄
978
976
 
979
977
  Format of the `ckeditor5_context` helper:
980
978
 
981
979
  ```erb
982
- <%= ckeditor5_context config: { ...you context config... }, plugins: [ ...your context plugins... ] do %>
980
+ <!-- app/views/demos/index.html.erb -->
981
+
982
+ <%= ckeditor5_context config: { ... }, plugins: [ ... ] do %>
983
983
  <%= ckeditor5_editor %>
984
984
  <%= ckeditor5_editor %>
985
985
  <% end %>
986
986
  ```
987
987
 
988
- Example usage:
988
+ 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.
989
+
990
+ ### Example usage of `ckeditor5_context` helper 📝
989
991
 
990
992
  ```erb
991
993
  <!-- app/views/demos/index.html.erb -->
@@ -1260,6 +1262,8 @@ class HighlightCommand extends Command {
1260
1262
 
1261
1263
  ## Events fired by the editor 🔊
1262
1264
 
1265
+ 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.
1266
+
1263
1267
  ### `editor-ready` event
1264
1268
 
1265
1269
  The event is fired when the initialization of the editor is completed. You can listen to it using the `editor-ready` event.
@@ -1280,6 +1284,54 @@ document.getElementById('editor').addEventListener('editor-error', () => {
1280
1284
  });
1281
1285
  ```
1282
1286
 
1287
+ ### `editor-change` event
1288
+
1289
+ The event is fired when the content of the editor changes. You can listen to it using the `editor-change` event.
1290
+
1291
+ ```js
1292
+ document.getElementById('editor').addEventListener('editor-change', () => {
1293
+ console.log('Editor content has changed');
1294
+ });
1295
+ ```
1296
+
1297
+ ### Inline event handling
1298
+
1299
+ You can also define event handlers directly in the view using the `oneditorchange`, `oneditorerror`, and `oneditorready` attributes.
1300
+
1301
+ ```erb
1302
+ <!-- app/views/demos/index.html.erb -->
1303
+
1304
+ <script type="text/javascript">
1305
+ function onEditorChange(event) {
1306
+ // event.detail.editor, event.detail.data
1307
+ }
1308
+
1309
+ function onEditorError(event) {
1310
+ // event.detail.editor
1311
+ }
1312
+
1313
+ function onEditorReady(event) {
1314
+ // event.detail.editor
1315
+ }
1316
+ </script>
1317
+
1318
+ <%= ckeditor5_editor id: 'editor',
1319
+ oneditorchange: 'onEditorChange',
1320
+ oneditorerror: 'onEditorError',
1321
+ oneditorready: 'onEditorReady'
1322
+ %>
1323
+ ```
1324
+
1325
+ ## Gem Development 🛠
1326
+
1327
+ If you want to contribute to the gem, you can clone the repository and run the following commands:
1328
+
1329
+ ```sh
1330
+ gem install bundler -v 2.5.22
1331
+ bundle install
1332
+ bundle exec guard
1333
+ ```
1334
+
1283
1335
  ## Trademarks 📜
1284
1336
 
1285
1337
  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 +1340,6 @@ This gem is not owned by CKSource and does not use the CKEditor® trademark for
1288
1340
 
1289
1341
  ## License 📜
1290
1342
 
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.
1343
+ 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
1344
 
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/).
1345
+ 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,12 +38,12 @@ 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
45
45
  @window_scripts_tags ||= safe_join(bundle.scripts.filter_map do |script|
46
- tag.script(src: script.url, nonce: true, async: true) if script.window?
46
+ tag.script(src: script.url, nonce: true, async: true, crossorigin: 'anonymous') if script.window?
47
47
  end)
48
48
  end
49
49
 
@@ -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);