ckeditor5 1.8.0 → 1.9.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: 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