ckeditor5 1.8.0 → 1.9.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: bb520612d3f2847e8d70c48a8ffb3d1ad4be25800dd5f141a6948caf30b07978
4
+ data.tar.gz: 5f9850fb6a39774ab796f67b5dc95fa0ec1e6a5fe6f04fb3481f6ff17c76e026
5
5
  SHA512:
6
- metadata.gz: c40e50c2093c7f8c311b8565a54ddc4835aba887ced1e36b4802c6681769a8f2e4f39812443e688f6e31ff4932eef76282664be82d7cdbb4b667c9febe76374f
7
- data.tar.gz: f9a999fb8a325323e497b225f3c8bed54cc24befc5bcd2cc90a93a25dece1d539e5e33a34fbe2a4f9570e462527492e061819563b7fad19e61d24fae8a39953e
6
+ metadata.gz: 7057a3e537f5626b4754b815c1f418ddac64301350aa3b0299d5051dcda474a5f397018389a0e7c773bc1d15687043498a2bcd44518581e4233f8ab4ae85dc52
7
+ data.tar.gz: 0526b5810f411971ce3adafa1bad252eabaffc566f3d637b6585c2a16a6d6c938078e8b88ed88b34e610477c87c66f5dfc74d092fab2f46f183327f2fca9d287
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
+ - [Benefits of Using Context in Collaboration 🤝](#benefits-of-using-context-in-collaboration-)
106
+ - [Using Context in CKEditor 5 🔄](#using-context-in-ckeditor-5-)
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-)
@@ -184,9 +187,6 @@ Configuration of the editor can be complex, and it's recommended to use the [CKE
184
187
 
185
188
  ### Available Configuration Methods ⚙️
186
189
 
187
- <details>
188
- <summary>Expand to show available methods 📖</summary>
189
-
190
190
  #### `cdn(cdn = nil, &block)` method
191
191
 
192
192
  Defines the CDN to be used for CKEditor 5 assets. The example below shows how to set the CDN to `:jsdelivr`:
@@ -569,8 +569,6 @@ CKEditor5::Rails.configure do
569
569
  end
570
570
  ```
571
571
 
572
- </details>
573
-
574
572
  ## Including CKEditor 5 assets 📦
575
573
 
576
574
  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 +963,46 @@ If you want to use a decoupled editor, you can pass the `type` keyword argument
965
963
  <% end %>
966
964
  ```
967
965
 
966
+ ## Using Context 📦
967
+
968
+ 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
+
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.
976
+
977
+ ### Using Context in CKEditor 5 🔄
978
+
979
+ Format of the `ckeditor5_context` helper:
980
+
981
+ ```erb
982
+ <%= ckeditor5_context config: { ...you context config... }, plugins: [ ...your context plugins... ] do %>
983
+ <%= ckeditor5_editor %>
984
+ <%= ckeditor5_editor %>
985
+ <% end %>
986
+ ```
987
+
988
+ Example usage:
989
+
990
+ ```erb
991
+ <!-- app/views/demos/index.html.erb -->
992
+
993
+ <% content_for :head do %>
994
+ <%= ckeditor5_assets preset: :ultrabasic %>
995
+ <% end %>
996
+
997
+ <%= ckeditor5_context do %>
998
+ <%= ckeditor5_editor initial_data: 'Hello World' %>
999
+
1000
+ <br>
1001
+
1002
+ <%= ckeditor5_editor initial_data: 'Hello World 2' %>
1003
+ <% end %>
1004
+ ```
1005
+
968
1006
  ## How to access editor instance? 🤔
969
1007
 
970
1008
  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.
@@ -57,16 +57,30 @@ class CKEditorComponent extends HTMLElement {
57
57
  /** @type {String} Initial HTML passed to component */
58
58
  #initialHTML = '';
59
59
 
60
+ /** @type {CKEditorContextComponent|null} */
61
+ #context = null;
62
+
63
+ /** @type {String} ID of editor within context */
64
+ #contextEditorId = null;
65
+
60
66
  /**
61
67
  * Lifecycle callback when element is connected to DOM
62
68
  * Initializes the editor when DOM is ready
63
69
  * @protected
64
70
  */
65
71
  connectedCallback() {
72
+ this.#context = this.closest('ckeditor-context-component');
66
73
  this.#initialHTML = this.innerHTML;
67
74
 
68
75
  try {
69
- execIfDOMReady(() => this.#reinitializeEditor());
76
+ execIfDOMReady(async () => {
77
+ if (this.#context) {
78
+ await this.#context.instancePromise.promise;
79
+ this.#context.registerEditor(this);
80
+ }
81
+
82
+ await this.reinitializeEditor();
83
+ });
70
84
  } catch (error) {
71
85
  console.error('Failed to initialize editor:', error);
72
86
  this.dispatchEvent(new CustomEvent('editor-error', { detail: error }));
@@ -84,7 +98,7 @@ class CKEditorComponent extends HTMLElement {
84
98
  if (oldValue !== null &&
85
99
  oldValue !== newValue &&
86
100
  CKEditorComponent.observedAttributes.includes(name) && this.isConnected) {
87
- await this.#reinitializeEditor();
101
+ await this.reinitializeEditor();
88
102
  }
89
103
  }
90
104
 
@@ -94,9 +108,12 @@ class CKEditorComponent extends HTMLElement {
94
108
  * @protected
95
109
  */
96
110
  async disconnectedCallback() {
111
+ if (this.#context) {
112
+ this.#context.unregisterEditor(this);
113
+ }
114
+
97
115
  try {
98
- await this.instance?.destroy();
99
- await this.watchdog?.destroy();
116
+ await this.#destroy();
100
117
  } catch (error) {
101
118
  console.error('Failed to destroy editor:', error);
102
119
  }
@@ -119,6 +136,7 @@ class CKEditorComponent extends HTMLElement {
119
136
 
120
137
  /**
121
138
  * Determines appropriate editor element tag based on editor type
139
+ *
122
140
  * @private
123
141
  * @returns {string} HTML tag name to use
124
142
  */
@@ -133,57 +151,25 @@ class CKEditorComponent extends HTMLElement {
133
151
  }
134
152
 
135
153
  /**
136
- * Resolves element references in configuration object.
137
- * Looks for objects with { $element: "selector" } format and replaces them with actual elements.
154
+ * Gets the CKEditor context instance if available.
138
155
  *
139
156
  * @private
140
- * @param {Object} obj - Configuration object to process
141
- * @returns {Object} Processed configuration object with resolved element references
157
+ * @returns {import('ckeditor5').ContextWatchdog|null}
142
158
  */
143
- #resolveElementReferences(obj) {
144
- if (!obj || typeof obj !== 'object') {
145
- return obj;
146
- }
147
-
148
- if (Array.isArray(obj)) {
149
- return obj.map(item => this.#resolveElementReferences(item));
150
- }
151
-
152
- const result = Object.create(null);
153
-
154
- for (const key of Object.getOwnPropertyNames(obj)) {
155
- if (!isSafeKey(key)) {
156
- console.warn(`Suspicious key "${key}" detected in config, skipping`);
157
- continue;
158
- }
159
-
160
- const value = obj[key];
161
-
162
- if (value && typeof value === 'object') {
163
- if (value.$element) {
164
- const selector = value.$element;
165
-
166
- if (typeof selector !== 'string') {
167
- console.warn(`Invalid selector type for "${key}", expected string`);
168
- continue;
169
- }
170
-
171
- const element = document.querySelector(selector);
172
-
173
- if (!element) {
174
- console.warn(`Element not found for selector: ${selector}`);
175
- }
159
+ get #contextWatchdog() {
160
+ return this.#context?.instance;
161
+ }
176
162
 
177
- result[key] = element || null;
178
- } else {
179
- result[key] = this.#resolveElementReferences(value);
180
- }
181
- } else {
182
- result[key] = value;
183
- }
163
+ /**
164
+ * Destroys the editor instance and watchdog if available
165
+ */
166
+ async #destroy() {
167
+ if (this.#contextEditorId) {
168
+ await this.#contextWatchdog.remove(this.#contextEditorId);
184
169
  }
185
170
 
186
- return result;
171
+ await this.instance?.destroy();
172
+ await this.watchdog?.destroy();
187
173
  }
188
174
 
189
175
  /**
@@ -195,7 +181,7 @@ class CKEditorComponent extends HTMLElement {
195
181
  #getConfig() {
196
182
  const config = JSON.parse(this.getAttribute('config') || '{}');
197
183
 
198
- return this.#resolveElementReferences(config);
184
+ return resolveElementReferences(config);
199
185
  }
200
186
 
201
187
  /**
@@ -232,13 +218,27 @@ class CKEditorComponent extends HTMLElement {
232
218
  plugins,
233
219
  };
234
220
 
235
- console.warn('Initializing CKEditor with:', { config, watchdog: this.hasWatchdog() });
221
+ console.warn('Initializing CKEditor with:', { config, watchdog: this.hasWatchdog(), context: this.#context });
236
222
 
237
223
  // Initialize watchdog if needed
238
224
  let watchdog = null;
239
225
  let instance = null;
240
-
241
- if (this.hasWatchdog()) {
226
+ let contextId = null;
227
+
228
+ if (this.#context) {
229
+ contextId = uid();
230
+
231
+ await this.#contextWatchdog.add( {
232
+ creator: (_element, _config) => Editor.create(_element, _config),
233
+ id: contextId,
234
+ sourceElementOrData: content,
235
+ type: 'editor',
236
+ config,
237
+ } );
238
+
239
+ instance = this.#contextWatchdog.getItem(contextId);
240
+ } else if (this.hasWatchdog()) {
241
+ // Let's create use with plain watchdog.
242
242
  const { EditorWatchdog } = await import('ckeditor5');
243
243
  const watchdog = new EditorWatchdog(Editor);
244
244
 
@@ -246,12 +246,14 @@ class CKEditorComponent extends HTMLElement {
246
246
 
247
247
  instance = watchdog.editor;
248
248
  } else {
249
+ // Let's create the editor without watchdog.
249
250
  instance = await Editor.create(content, config);
250
251
  }
251
252
 
252
253
  this.dispatchEvent(new CustomEvent('editor-ready', { detail: instance }));
253
254
 
254
255
  return {
256
+ contextId,
255
257
  instance,
256
258
  watchdog,
257
259
  };
@@ -263,11 +265,12 @@ class CKEditorComponent extends HTMLElement {
263
265
  * @private
264
266
  * @returns {Promise<void>}
265
267
  */
266
- async #reinitializeEditor() {
268
+ async reinitializeEditor() {
267
269
  if (this.instance) {
268
270
  this.instancePromise = Promise.withResolvers();
269
271
 
270
- await this.instance.destroy();
272
+ await this.#destroy();
273
+
271
274
  this.instance = null;
272
275
  }
273
276
 
@@ -288,10 +291,11 @@ class CKEditorComponent extends HTMLElement {
288
291
  }
289
292
 
290
293
  try {
291
- const { watchdog, instance } = await this.#initializeEditor(this.editables || this.#getConfig().initialData || '');
294
+ const { watchdog, instance, contextId } = await this.#initializeEditor(this.editables || this.#getConfig().initialData || '');
292
295
 
293
296
  this.watchdog = watchdog;
294
297
  this.instance = instance;
298
+ this.#contextEditorId = contextId;
295
299
 
296
300
  this.#setupContentSync();
297
301
  this.#setupEditableHeight();
@@ -511,6 +515,129 @@ class CKEditorComponent extends HTMLElement {
511
515
  }
512
516
  }
513
517
 
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
+
514
641
  /**
515
642
  * Tracks and manages editable roots for CKEditor MultiRoot editor.
516
643
  * Provides a proxy-based API for dynamically managing editable elements with automatic
@@ -894,6 +1021,69 @@ function isSafeKey(key) {
894
1021
  key !== 'prototype';
895
1022
  }
896
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
+
897
1086
  customElements.define('ckeditor-component', CKEditorComponent);
898
1087
  customElements.define('ckeditor-editable-component', CKEditorEditableComponent);
899
1088
  customElements.define('ckeditor-ui-part-component', CKEditorUIPartComponent);
1089
+ customElements.define('ckeditor-context-component', CKEditorContextComponent);
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'props'
4
+
5
+ module CKEditor5::Rails::Context
6
+ module Helpers
7
+ def ckeditor5_context(**config, &block)
8
+ context_props = Props.new(config)
9
+
10
+ tag.send(:'ckeditor-context-component', **context_props.to_attributes, &block)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CKEditor5::Rails
4
+ module Context
5
+ class Props
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ def to_attributes
11
+ {
12
+ plugins: serialize_plugins,
13
+ config: serialize_config
14
+ }
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :config
20
+
21
+ def serialize_plugins
22
+ (config[:plugins] || []).map { |plugin| Editor::PropsPlugin.normalize(plugin).to_h }.to_json
23
+ end
24
+
25
+ def serialize_config
26
+ config.except(:plugins).to_json
27
+ end
28
+ end
29
+ end
30
+ end
@@ -29,16 +29,13 @@ module CKEditor5::Rails
29
29
  type ||= preset.type
30
30
 
31
31
  validated_height = validate_editable_height(type, editable_height) || preset.editable_height
32
- editor_props = Editor::Props.new(
33
- controller_context, type, config,
34
- watchdog: watchdog
35
- )
36
-
37
- render_editor_component(
38
- editor_props,
39
- html_attributes.merge(validated_height ? { 'editable-height' => validated_height } : {}),
40
- &block
41
- )
32
+ editor_props = Editor::Props.new(controller_context, type, config, watchdog: watchdog)
33
+
34
+ tag_attributes = html_attributes
35
+ .merge(editor_props.to_attributes)
36
+ .merge(validated_height ? { 'editable-height' => validated_height } : {})
37
+
38
+ tag.send(:'ckeditor-component', **tag_attributes, &block)
42
39
  end
43
40
 
44
41
  def ckeditor5_editable(name = nil, **kwargs, &block)
@@ -91,10 +88,6 @@ module CKEditor5::Rails
91
88
  raise PresetNotFoundError, "Preset #{preset} is not defined."
92
89
  end
93
90
 
94
- def render_editor_component(props, html_attributes, &block)
95
- tag.send(:'ckeditor-component', **props.to_attributes, **html_attributes, &block)
96
- end
97
-
98
91
  def validate_editable_height(type, height)
99
92
  return nil if height.nil?
100
93
 
@@ -3,11 +3,13 @@
3
3
  require_relative 'cdn/helpers'
4
4
  require_relative 'cloud/helpers'
5
5
  require_relative 'editor/helpers'
6
+ require_relative 'context/helpers'
6
7
 
7
8
  module CKEditor5::Rails
8
9
  module Helpers
9
10
  include Cdn::Helpers
10
11
  include Cloud::Helpers
11
12
  include Editor::Helpers
13
+ include Context::Helpers
12
14
  end
13
15
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module CKEditor5
4
4
  module Rails
5
- VERSION = '1.8.0'
5
+ VERSION = '1.9.0'
6
6
  end
7
7
  end
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.8.0
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mateusz Bagiński
@@ -51,6 +51,8 @@ files:
51
51
  - lib/ckeditor5/rails/cdn/helpers.rb
52
52
  - lib/ckeditor5/rails/cdn/url_generator.rb
53
53
  - lib/ckeditor5/rails/cloud/helpers.rb
54
+ - lib/ckeditor5/rails/context/helpers.rb
55
+ - lib/ckeditor5/rails/context/props.rb
54
56
  - lib/ckeditor5/rails/editor/config_helpers.rb
55
57
  - lib/ckeditor5/rails/editor/helpers.rb
56
58
  - lib/ckeditor5/rails/editor/props.rb