ckeditor5 1.9.0 → 1.11.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: 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);