ckeditor5 1.20.1 → 1.22.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +99 -2
  3. data/lib/ckeditor5/rails/assets/assets_bundle.rb +11 -1
  4. data/lib/ckeditor5/rails/assets/assets_bundle_html_serializer.rb +5 -10
  5. data/lib/ckeditor5/rails/assets/webcomponent_bundle.rb +10 -3
  6. data/lib/ckeditor5/rails/assets/webcomponents/components/editor.mjs +35 -2
  7. data/lib/ckeditor5/rails/assets/webcomponents/utils.mjs +36 -2
  8. data/lib/ckeditor5/rails/cdn/concerns/bundle_builder.rb +57 -0
  9. data/lib/ckeditor5/rails/cdn/helpers.rb +62 -55
  10. data/lib/ckeditor5/rails/editor/helpers/editor_helpers.rb +25 -19
  11. data/lib/ckeditor5/rails/editor/props.rb +7 -14
  12. data/lib/ckeditor5/rails/engine.rb +13 -0
  13. data/lib/ckeditor5/rails/presets/manager.rb +2 -0
  14. data/lib/ckeditor5/rails/presets/preset_builder.rb +1 -1
  15. data/lib/ckeditor5/rails/presets/toolbar_builder.rb +117 -3
  16. data/lib/ckeditor5/rails/version.rb +1 -1
  17. data/spec/e2e/features/ajax_form_integration_spec.rb +78 -0
  18. data/spec/e2e/features/lazy_assets_spec.rb +54 -0
  19. data/spec/e2e/support/form_helpers.rb +4 -1
  20. data/spec/lib/ckeditor5/rails/assets/assets_bundle_hml_serializer_spec.rb +53 -0
  21. data/spec/lib/ckeditor5/rails/assets/assets_bundle_spec.rb +2 -1
  22. data/spec/lib/ckeditor5/rails/cdn/helpers_spec.rb +95 -3
  23. data/spec/lib/ckeditor5/rails/editor/helpers/editor_helpers_spec.rb +85 -25
  24. data/spec/lib/ckeditor5/rails/editor/props_spec.rb +14 -34
  25. data/spec/lib/ckeditor5/rails/engine_spec.rb +10 -0
  26. data/spec/lib/ckeditor5/rails/hooks/form_spec.rb +15 -2
  27. data/spec/lib/ckeditor5/rails/plugins/wproofreader_spec.rb +2 -0
  28. data/spec/lib/ckeditor5/rails/presets/toolbar_builder_spec.rb +119 -0
  29. data/spec/lib/ckeditor5/rails/version_detector_spec.rb +1 -1
  30. metadata +7 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5a5f9a3ade293a0c872a6fa77b20acd227133d66728bafcc0e6e20e7e55e3b2
4
- data.tar.gz: d6cb12217a822993bf1fe8487143c2debdea4061dc1c72424dbef052f78cb216
3
+ metadata.gz: 541773f43272493bbe29d3baf236cab3e85231f03b31c5d070b4917e6ecdea26
4
+ data.tar.gz: cb06989fd313768d6c1a5990f3dd49525fd1482b29a6b542c6d2778092eb3ad5
5
5
  SHA512:
6
- metadata.gz: d985a26adaaf6a86490ab603a4a50fb7c9c06fdd749f48501763dde0c8eaed14bc6989464bc55589b5be90ecb2ac5a8d4b95f259c71df91957799b80e67c0d04
7
- data.tar.gz: 149b7da93c090b45cf1c687b96d3579505ba9c131dd3cca2baae6ac995eaff5da99696be3944197e49012abd2c34140abc9de889703018cc4a6505669c2a7ee5
6
+ metadata.gz: 5855d32cb3fcfd5da47b650683c215bda3dc1842f8e9d10af599eecec515d00ca120adc0be299d6410c5bd5a12df7d8df6c4c49b0d0d5d6477e65ccb3841a99a
7
+ data.tar.gz: 437632ab7d5599ddcd452b3dfefa218d2541db59896c50cba8ade94081c40ac4c9a9e144c82daa684174f88d50ac300d341eae07e5d675fe6a90f7ef15d667db
data/README.md CHANGED
@@ -36,7 +36,7 @@ In your layout:
36
36
  be included in the head section to ensure proper loading order.
37
37
  This is crucial for CKEditor 5 to work correctly.
38
38
  -->
39
- <%= javascript_importmap_tags %>
39
+ <!-- javascript_importmap_tags -->
40
40
  <%= yield :head %>
41
41
  </head>
42
42
  <body>
@@ -148,6 +148,7 @@ For extending CKEditor's functionality, refer to the [plugins directory](https:/
148
148
  - [Custom preset](#custom-preset)
149
149
  - [Inline preset definition](#inline-preset-definition)
150
150
  - [Lazy loading 🚀](#lazy-loading-)
151
+ - [`ckeditor5_lazy_javascript_tags` helper](#ckeditor5_lazy_javascript_tags-helper)
151
152
  - [GPL usage 🆓](#gpl-usage-)
152
153
  - [Commercial usage 💰](#commercial-usage-)
153
154
  - [Editor placement 🏗️](#editor-placement-️)
@@ -172,6 +173,7 @@ For extending CKEditor's functionality, refer to the [plugins directory](https:/
172
173
  - [Integrating with Forms 📋](#integrating-with-forms-)
173
174
  - [Rails form builder integration](#rails-form-builder-integration)
174
175
  - [Simple form integration](#simple-form-integration)
176
+ - [Integration with Turbolinks 🚀](#integration-with-turbolinks-)
175
177
  - [Custom Styling 🎨](#custom-styling-)
176
178
  - [Custom plugins 🧩](#custom-plugins-)
177
179
  - [Events fired by the editor 🔊](#events-fired-by-the-editor-)
@@ -608,6 +610,39 @@ CKEditor5::Rails.configure do
608
610
  end
609
611
  end
610
612
  ```
613
+
614
+ If you want to append groups of items, you can use the `group` method:
615
+
616
+ ```rb
617
+ # config/initializers/ckeditor5.rb
618
+
619
+ CKEditor5::Rails.configure do
620
+ # ... other configuration
621
+
622
+ toolbar do
623
+ group :text_formatting, label: 'Text Formatting', icon: 'threeVerticalDots' do
624
+ append :bold, :italic, :underline, :strikethrough, separator,
625
+ :subscript, :superscript, :removeFormat
626
+ end
627
+ end
628
+ end
629
+ ```
630
+
631
+ If you want add new line or the separator, you can use the `break_line` or `separator` methods:
632
+
633
+ ```rb
634
+ # config/initializers/ckeditor5.rb
635
+
636
+ CKEditor5::Rails.configure do
637
+ # ... other configuration
638
+
639
+ toolbar do
640
+ append :bold, break_line
641
+ append separator, :italic
642
+ end
643
+ end
644
+ ```
645
+
611
646
  </details>
612
647
 
613
648
  #### `menubar(visible: true)` method
@@ -1136,12 +1171,49 @@ It's possible to define the preset directly in the `ckeditor5_assets` helper met
1136
1171
  ### Lazy loading 🚀
1137
1172
 
1138
1173
  <details>
1139
- <summary>Loading JS and CSS Assets</summary>
1174
+ <summary>Expand to show more information about lazy loading</summary>
1140
1175
 
1141
1176
  All JS assets defined by the `ckeditor5_assets` helper method are loaded **asynchronously**. It means that the assets are loaded in the background without blocking the rendering of the page. However, the CSS assets are loaded **synchronously** to prevent the flash of unstyled content and ensure that the editor is styled correctly.
1142
1177
 
1143
1178
  It has been achieved by using web components, together with import maps, which are supported by modern browsers. The web components are used to define the editor and its plugins, while the import maps are used to define the dependencies between the assets.
1144
1179
 
1180
+ #### `ckeditor5_lazy_javascript_tags` helper
1181
+
1182
+ **This method is slow as content is being loaded on the fly on the client side. Use it only when necessary.**
1183
+
1184
+ If you want to include the CKEditor 5 JavaScripts and Stylesheets when the editor is being appended to the DOM using Turbolinks, Stimulus, or other JavaScript frameworks, you can use the `ckeditor5_lazy_javascript_tags` helper method.
1185
+
1186
+ This method does not preload the assets, and it's appending web component that loads the assets when the editor is being appended to the DOM. It's useful when turbolinks frame is being replaced or when the editor is being appended to the DOM dynamically.
1187
+
1188
+ The example below shows how to include the CKEditor 5 assets lazily:
1189
+
1190
+ ```erb
1191
+ <!-- app/views/demos/index.html.erb -->
1192
+
1193
+ <% content_for :head do %>
1194
+ <%= ckeditor5_lazy_javascript_tags %>
1195
+ <% end %>
1196
+
1197
+ <%= turbo_frame_tag 'editor' do %>
1198
+ <%= ckeditor5_editor %>
1199
+ <% end %>
1200
+ ```
1201
+
1202
+ ⚠️ Keep in mind that the `ckeditor5_lazy_javascript_tags` helper method should be included in the `head` section of the layout and it does not create controller context for the editors. In other words, you have to specify `preset` every time you use `ckeditor5_editor` helper (in `ckeditor5_assets` it's not necessary, as it's inherited by all editors).
1203
+
1204
+ If you want to keep inheritance of the presets and enforce integration to inject CKEditor 5 files on the fly, you can use the `lazy` keyword argument in the `ckeditor5_assets` helper method:
1205
+
1206
+ ```erb
1207
+ <!-- app/views/demos/index.html.erb -->
1208
+
1209
+ <% content_for :head do %>
1210
+ <%= ckeditor5_assets preset: :custom, lazy: true %>
1211
+ <% end %>
1212
+
1213
+ <!-- This time preset will be inherited but stylesheets and js files will be injected on the client side. -->
1214
+ <%= ckeditor5_editor %>
1215
+ ```
1216
+
1145
1217
  </details>
1146
1218
 
1147
1219
  ### GPL usage 🆓
@@ -1658,6 +1730,31 @@ You can integrate CKEditor 5 with Rails form builders like `form_for` or `simple
1658
1730
  <% end %>
1659
1731
  ```
1660
1732
 
1733
+ ### Integration with Turbolinks 🚀
1734
+
1735
+ If you're using Turbolinks in your Rails application, you may need to load CKEditor 5 in embeds that are loaded dynamically and not on the initial page load. In this case, you can use the `ckeditor5_lazy_javascript_tags` helper method to load CKEditor 5 assets when the editor is appended to the DOM. This method is useful when you're using Turbolinks or Stimulus to load CKEditor 5 dynamically.
1736
+
1737
+ Your view should look like this:
1738
+
1739
+ ```erb
1740
+ <!-- app/views/demos/index.html.erb -->
1741
+
1742
+ <% content_for :head do %>
1743
+ <%= ckeditor5_lazy_javascript_tags %>
1744
+ <% end %>
1745
+ ```
1746
+
1747
+ Your ajax partial should look like this:
1748
+
1749
+ ```erb
1750
+ <!-- app/views/demos/_form.html.erb -->
1751
+
1752
+ <!-- Presets and other configuration as usual -->
1753
+ <%= ckeditor5_editor %>
1754
+ ```
1755
+
1756
+ This method does not preload the assets, and it's appending web component that loads the assets when the editor is being appended to the DOM. Please see the [Lazy Loading](#lazy-loading) section for more information and [demos](https://github.com/Mati365/ckeditor5-rails/blob/main/sandbox/app/views/demos/form_ajax.slim) on how to use this method.
1757
+
1661
1758
  ### Custom Styling 🎨
1662
1759
 
1663
1760
  You can pass the `style`, `class` and `id` keyword arguments to the `ckeditor5_editor` helper to define the styling of the editor. The example below shows how to set the height, margin, and CSS class of the editor:
@@ -30,6 +30,13 @@ module CKEditor5::Rails::Assets
30
30
  stylesheets + scripts.map(&:preloads)
31
31
  end
32
32
 
33
+ def to_json(*_args)
34
+ {
35
+ scripts: scripts.map(&:to_h),
36
+ stylesheets: stylesheets
37
+ }.to_json
38
+ end
39
+
33
40
  def <<(other)
34
41
  raise TypeError, 'other must be an instance of AssetsBundle' unless other.is_a?(AssetsBundle)
35
42
 
@@ -54,7 +61,10 @@ module CKEditor5::Rails::Assets
54
61
  end
55
62
 
56
63
  def to_h
57
- import_meta.to_h.merge({ url: url })
64
+ import_meta.to_h.merge({
65
+ url: url,
66
+ translation: translation?
67
+ })
58
68
  end
59
69
 
60
70
  def preloads
@@ -9,23 +9,22 @@ module CKEditor5::Rails::Assets
9
9
  class AssetsBundleHtmlSerializer
10
10
  include ActionView::Helpers::TagHelper
11
11
 
12
- attr_reader :bundle, :importmap
12
+ attr_reader :bundle, :importmap, :lazy
13
13
 
14
- def initialize(bundle, importmap: true)
14
+ def initialize(bundle, importmap: true, lazy: false)
15
15
  raise TypeError, 'bundle must be an instance of AssetsBundle' unless bundle.is_a?(AssetsBundle)
16
16
 
17
17
  @importmap = importmap
18
18
  @bundle = bundle
19
+ @lazy = lazy
19
20
  end
20
21
 
21
22
  def to_html
22
23
  tags = [
23
- preload_tags,
24
- styles_tags,
25
- window_scripts_tags,
26
- web_component_tag
24
+ WebComponentBundle.instance.to_html
27
25
  ]
28
26
 
27
+ tags.prepend(preload_tags, styles_tags, window_scripts_tags) unless lazy
29
28
  tags.prepend(AssetsImportMap.new(bundle).to_html) if importmap
30
29
 
31
30
  safe_join(tags)
@@ -41,10 +40,6 @@ module CKEditor5::Rails::Assets
41
40
 
42
41
  private
43
42
 
44
- def web_component_tag
45
- @web_component_tag ||= tag.script(WebComponentBundle.source, type: 'module', nonce: true)
46
- end
47
-
48
43
  def window_scripts_tags
49
44
  @window_scripts_tags ||= safe_join(bundle.scripts.filter_map do |script|
50
45
  tag.script(src: script.url, nonce: true, crossorigin: 'anonymous') if script.window?
@@ -1,7 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'singleton'
4
+
3
5
  module CKEditor5::Rails::Assets
4
- module WebComponentBundle
6
+ class WebComponentBundle
7
+ include ActionView::Helpers::TagHelper
8
+ include Singleton
9
+
5
10
  WEBCOMPONENTS_PATH = File.join(__dir__, 'webcomponents')
6
11
  WEBCOMPONENTS_MODULES = [
7
12
  'utils.mjs',
@@ -11,12 +16,14 @@ module CKEditor5::Rails::Assets
11
16
  'components/context.mjs'
12
17
  ].freeze
13
18
 
14
- module_function
15
-
16
19
  def source
17
20
  @source ||= WEBCOMPONENTS_MODULES.map do |file|
18
21
  File.read(File.join(WEBCOMPONENTS_PATH, file))
19
22
  end.join("\n").html_safe
20
23
  end
24
+
25
+ def to_html
26
+ @to_html ||= tag.script(source, type: 'module', nonce: true)
27
+ end
21
28
  end
22
29
  end
@@ -40,6 +40,9 @@ class CKEditorComponent extends HTMLElement {
40
40
  /** @type {String} ID of editor within context */
41
41
  #contextEditorId = null;
42
42
 
43
+ /** @type {Object} Description of ckeditor bundle */
44
+ #bundle = null;
45
+
43
46
  /** @type {(event: CustomEvent) => void} Event handler for editor change */
44
47
  get oneditorchange() {
45
48
  return this.#getEventHandler('editorchange');
@@ -237,6 +240,11 @@ class CKEditorComponent extends HTMLElement {
237
240
  * @throws {Error} When initialization fails
238
241
  */
239
242
  async #initializeEditor(editablesOrContent) {
243
+ await Promise.all([
244
+ this.#ensureStylesheetsInjected(),
245
+ this.#ensureWindowScriptsInjected(),
246
+ ]);
247
+
240
248
  const Editor = await this.#getEditorConstructor();
241
249
  const [plugins, translations] = await Promise.all([
242
250
  this.#getPlugins(),
@@ -550,6 +558,30 @@ class CKEditorComponent extends HTMLElement {
550
558
  });
551
559
  }
552
560
 
561
+ /**
562
+ * Gets bundle JSON description from translations attribute
563
+ */
564
+ #getBundle() {
565
+ return this.#bundle ||= JSON.parse(this.getAttribute('bundle'));
566
+ }
567
+
568
+
569
+ /**
570
+ * Checks if all required stylesheets are injected. If not, inject.
571
+ */
572
+ async #ensureStylesheetsInjected() {
573
+ await loadAsyncCSS(this.#getBundle().stylesheets || []);
574
+ }
575
+
576
+ /**
577
+ * Checks if all required scripts are injected. If not, inject.
578
+ */
579
+ async #ensureWindowScriptsInjected() {
580
+ const windowScripts = (this.#getBundle().scripts || []).filter(script => !!script.window_name);
581
+
582
+ await loadAsyncImports(windowScripts);
583
+ }
584
+
553
585
  /**
554
586
  * Loads translation modules
555
587
  *
@@ -557,8 +589,9 @@ class CKEditorComponent extends HTMLElement {
557
589
  * @returns {Promise<Array<any>>}
558
590
  */
559
591
  async #getTranslations() {
560
- const raw = this.getAttribute('translations');
561
- return loadAsyncImports(raw ? JSON.parse(raw) : []);
592
+ const translations = this.#getBundle().scripts.filter(script => script.translation);
593
+
594
+ return loadAsyncImports(translations);
562
595
  }
563
596
 
564
597
  /**
@@ -44,13 +44,21 @@ function loadAsyncImports(imports = []) {
44
44
  return module.default;
45
45
  };
46
46
 
47
- const loadExternalPlugin = async ({ import_name, import_as, window_name, stylesheets }) => {
47
+ const loadExternalPlugin = async ({ url, import_name, import_as, window_name, stylesheets }) => {
48
48
  if (stylesheets?.length) {
49
49
  await loadAsyncCSS(stylesheets);
50
50
  }
51
51
 
52
52
  if (window_name) {
53
- if (!Object.prototype.hasOwnProperty.call(window, window_name)) {
53
+ function isScriptPresent() {
54
+ return Object.prototype.hasOwnProperty.call(window, window_name);
55
+ }
56
+
57
+ if (url && !isScriptPresent()) {
58
+ await injectScript(url);
59
+ }
60
+
61
+ if (!isScriptPresent()) {
54
62
  throw new Error(
55
63
  `Plugin window['${window_name}'] not found in global scope. ` +
56
64
  'Please ensure the plugin is loaded before CKEditor initialization.'
@@ -129,6 +137,32 @@ function loadAsyncCSS(stylesheets = []) {
129
137
  return Promise.all(promises);
130
138
  }
131
139
 
140
+ const SCRIPT_LOAD_PROMISES = new Map();
141
+
142
+ /**
143
+ * Dynamically loads script files based on configuration.
144
+ * Uses caching to avoid loading the same script multiple times.
145
+ *
146
+ * @param {string} url - URL of the script to load
147
+ */
148
+ function injectScript(url) {
149
+ if (SCRIPT_LOAD_PROMISES.has(url)) {
150
+ return SCRIPT_LOAD_PROMISES.get(url);
151
+ }
152
+
153
+ const promise = new Promise((resolve, reject) => {
154
+ const script = document.createElement('script');
155
+ script.src = url;
156
+ script.onload = resolve;
157
+ script.onerror = reject;
158
+
159
+ document.head.appendChild(script);
160
+ });
161
+
162
+ SCRIPT_LOAD_PROMISES.set(url, promise);
163
+ return promise;
164
+ }
165
+
132
166
  /**
133
167
  * Checks if a key is safe to use in configuration objects to prevent prototype pollution
134
168
  *
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CKEditor5::Rails
4
+ module Cdn::Concerns
5
+ module BundleBuilder
6
+ def create_preset_bundle(preset)
7
+ preset => {
8
+ cdn:,
9
+ version:,
10
+ translations:,
11
+ ckbox:,
12
+ premium:
13
+ }
14
+
15
+ bundle = build_base_cdn_bundle(cdn, version, translations)
16
+ bundle << build_premium_cdn_bundle(cdn, version, translations) if premium
17
+ bundle << build_ckbox_cdn_bundle(ckbox) if ckbox
18
+ bundle << build_plugins_cdn_bundle(preset.plugins.items)
19
+ bundle
20
+ end
21
+
22
+ private
23
+
24
+ def build_base_cdn_bundle(cdn, version, translations)
25
+ Cdn::CKEditorBundle.new(
26
+ Semver.new(version),
27
+ 'ckeditor5',
28
+ translations: translations,
29
+ cdn: cdn
30
+ )
31
+ end
32
+
33
+ def build_premium_cdn_bundle(cdn, version, translations)
34
+ Cdn::CKEditorBundle.new(
35
+ Semver.new(version),
36
+ 'ckeditor5-premium-features',
37
+ translations: translations,
38
+ cdn: cdn
39
+ )
40
+ end
41
+
42
+ def build_ckbox_cdn_bundle(ckbox)
43
+ Cdn::CKBoxBundle.new(
44
+ Semver.new(ckbox[:version]),
45
+ theme: ckbox[:theme] || :lark,
46
+ cdn: ckbox[:cdn] || :ckbox
47
+ )
48
+ end
49
+
50
+ def build_plugins_cdn_bundle(plugins)
51
+ plugins.each_with_object(Assets::AssetsBundle.new(scripts: [], stylesheets: [])) do |plugin, bundle|
52
+ bundle << plugin.preload_assets_bundle if plugin.preload_assets_bundle.present?
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -9,9 +9,13 @@ require_relative '../assets/assets_bundle_html_serializer'
9
9
  require_relative 'url_generator'
10
10
  require_relative 'ckeditor_bundle'
11
11
  require_relative 'ckbox_bundle'
12
+ require_relative 'concerns/bundle_builder'
12
13
 
13
14
  module CKEditor5::Rails
14
15
  module Cdn::Helpers
16
+ include Cdn::Concerns::BundleBuilder
17
+ include ActionView::Helpers::TagHelper
18
+
15
19
  class ImportmapAlreadyRenderedError < ArgumentError; end
16
20
 
17
21
  # The `ckeditor5_assets` helper includes CKEditor 5 assets in your application.
@@ -28,6 +32,8 @@ module CKEditor5::Rails
28
32
  # - license_key: Commercial license key
29
33
  # - premium: Enable premium features
30
34
  # - language: Set editor UI language (e.g. :pl, :es)
35
+ # - lazy: Enable lazy loading of dependencies (slower but useful for async partials)
36
+ # - importmap: Whether to use importmap for dependencies (default: true)
31
37
  #
32
38
  # @example Basic usage with default preset
33
39
  # <%= ckeditor5_assets %>
@@ -60,34 +66,63 @@ module CKEditor5::Rails
60
66
  def ckeditor5_assets(
61
67
  preset: :default,
62
68
  importmap: true,
69
+ lazy: false,
63
70
  **kwargs
64
71
  )
65
72
  ensure_importmap_not_rendered!
66
73
 
67
74
  mapped_preset = merge_with_editor_preset(preset, **kwargs)
68
- mapped_preset => {
69
- cdn:,
70
- version:,
71
- translations:,
72
- ckbox:,
73
- license_key:,
74
- premium:
75
- }
76
-
77
- bundle = build_base_cdn_bundle(cdn, version, translations)
78
- bundle << build_premium_cdn_bundle(cdn, version, translations) if premium
79
- bundle << build_ckbox_cdn_bundle(ckbox) if ckbox
80
- bundle << build_plugins_cdn_bundle(mapped_preset.plugins.items)
75
+ bundle = create_preset_bundle(mapped_preset)
81
76
 
82
77
  @__ckeditor_context = {
83
- license_key: license_key,
78
+ license_key: mapped_preset.license_key,
84
79
  bundle: bundle,
85
80
  preset: mapped_preset
86
81
  }
87
82
 
88
- build_html_tags(bundle, importmap)
83
+ build_assets_html_tags(bundle, importmap: importmap, lazy: lazy)
89
84
  end
90
85
 
86
+ # Helper for dynamically loading CKEditor assets when working with Turbo/Stimulus.
87
+ # Adds importmap containing imports from all presets and includes only web component
88
+ # initialization code. Useful when dynamically adding editors to the page with
89
+ # unknown preset configuration.
90
+ #
91
+ # @note Do not use this helper if ckeditor5_assets is already included on the page
92
+ # as it will cause duplicate imports.
93
+ #
94
+ # @example With Turbo/Stimulus dynamic editor loading
95
+ # <%= ckeditor5_lazy_javascript_tags %>
96
+ #
97
+ def ckeditor5_lazy_javascript_tags
98
+ ensure_importmap_not_rendered!
99
+
100
+ if importmap_available?
101
+ @__ckeditor_context = {
102
+ bundle: combined_bundle
103
+ }
104
+
105
+ return Assets::WebComponentBundle.instance.to_html
106
+ end
107
+
108
+ safe_join([
109
+ Assets::AssetsImportMap.new(combined_bundle).to_html,
110
+ Assets::WebComponentBundle.instance.to_html
111
+ ])
112
+ end
113
+
114
+ # Dynamically generates helper methods for each third-party CDN provider.
115
+ # These methods are shortcuts for including CKEditor assets from specific CDNs.
116
+ # Generated methods follow the pattern: ckeditor5_<cdn>_assets
117
+ #
118
+ # @example Using JSDelivr CDN
119
+ # <%= ckeditor5_jsdelivr_assets %>
120
+ #
121
+ # @example Using UNPKG CDN with version
122
+ # <%= ckeditor5_unpkg_assets version: '34.1.0' %>
123
+ #
124
+ # @example Using JSDelivr CDN with custom options
125
+ # <%= ckeditor5_jsdelivr_assets preset: :custom, translations: [:pl] %>
91
126
  Cdn::UrlGenerator::CDN_THIRD_PARTY_GENERATORS.each_key do |key|
92
127
  define_method(:"ckeditor5_#{key.to_s.parameterize}_assets") do |**kwargs|
93
128
  ckeditor5_assets(**kwargs.merge(cdn: key))
@@ -96,15 +131,18 @@ module CKEditor5::Rails
96
131
 
97
132
  private
98
133
 
99
- def merge_with_editor_preset(preset, language: nil, **kwargs)
100
- found_preset = Engine.find_preset(preset)
134
+ def combined_bundle
135
+ acc = Assets::AssetsBundle.new(scripts: [], stylesheets: [])
101
136
 
102
- if found_preset.blank?
103
- raise ArgumentError,
104
- "Poor thing. You forgot to define your #{preset} preset. " \
105
- 'Please define it in initializer. Thank you!'
137
+ Engine.presets.to_h.values.each_with_object(acc) do |preset, bundle|
138
+ bundle << create_preset_bundle(preset)
106
139
  end
107
140
 
141
+ acc
142
+ end
143
+
144
+ def merge_with_editor_preset(preset, language: nil, **kwargs)
145
+ found_preset = Engine.find_preset!(preset)
108
146
  new_preset = found_preset.clone.merge_with_hash!(**kwargs)
109
147
 
110
148
  # Assign default language if not present
@@ -125,38 +163,6 @@ module CKEditor5::Rails
125
163
  new_preset
126
164
  end
127
165
 
128
- def build_base_cdn_bundle(cdn, version, translations)
129
- Cdn::CKEditorBundle.new(
130
- Semver.new(version),
131
- 'ckeditor5',
132
- translations: translations,
133
- cdn: cdn
134
- )
135
- end
136
-
137
- def build_premium_cdn_bundle(cdn, version, translations)
138
- Cdn::CKEditorBundle.new(
139
- Semver.new(version),
140
- 'ckeditor5-premium-features',
141
- translations: translations,
142
- cdn: cdn
143
- )
144
- end
145
-
146
- def build_ckbox_cdn_bundle(ckbox)
147
- Cdn::CKBoxBundle.new(
148
- Semver.new(ckbox[:version]),
149
- theme: ckbox[:theme] || :lark,
150
- cdn: ckbox[:cdn] || :ckbox
151
- )
152
- end
153
-
154
- def build_plugins_cdn_bundle(plugins)
155
- plugins.each_with_object(Assets::AssetsBundle.new(scripts: [], stylesheets: [])) do |plugin, bundle|
156
- bundle << plugin.preload_assets_bundle if plugin.preload_assets_bundle.present?
157
- end
158
- end
159
-
160
166
  def importmap_available?
161
167
  respond_to?(:importmap_rendered?)
162
168
  end
@@ -169,10 +175,11 @@ module CKEditor5::Rails
169
175
  'Please move ckeditor5_assets helper before javascript_importmap_tags in your layout.'
170
176
  end
171
177
 
172
- def build_html_tags(bundle, importmap)
178
+ def build_assets_html_tags(bundle, importmap:, lazy: nil)
173
179
  serializer = Assets::AssetsBundleHtmlSerializer.new(
174
180
  bundle,
175
- importmap: importmap && !importmap_available?
181
+ importmap: importmap && !importmap_available?,
182
+ lazy: lazy
176
183
  )
177
184
 
178
185
  html = serializer.to_html
@@ -7,9 +7,7 @@ require_relative 'config_helpers'
7
7
  module CKEditor5::Rails
8
8
  module Editor::Helpers::Editor
9
9
  include Editor::Helpers::Config
10
-
11
- class EditorContextError < StandardError; end
12
- class PresetNotFoundError < ArgumentError; end
10
+ include Cdn::Concerns::BundleBuilder
13
11
 
14
12
  # Creates a CKEditor 5 editor instance in the view.
15
13
  #
@@ -67,17 +65,22 @@ module CKEditor5::Rails
67
65
  )
68
66
  validate_editor_input!(initial_data, block)
69
67
 
70
- controller_context = validate_and_get_editor_context!
68
+ context = ckeditor5_context_or_fallback(preset)
71
69
 
72
- preset = find_preset(preset || controller_context[:preset] || :default)
70
+ preset = Engine.find_preset!(preset || context[:preset] || :default)
73
71
  config = build_editor_config(preset, config, extra_config, initial_data)
72
+
74
73
  type ||= preset.type
75
74
 
75
+ # Add some fallbacks
76
+ config[:licenseKey] ||= context[:license_key]
77
+ config[:language] = { ui: language } if language
78
+
76
79
  editor_props = Editor::Props.new(
77
- controller_context, type, config,
80
+ type, config,
81
+ bundle: context[:bundle],
78
82
  watchdog: watchdog,
79
- editable_height: editable_height,
80
- language: language
83
+ editable_height: editable_height || preset.editable_height
81
84
  )
82
85
 
83
86
  tag_attributes = html_attributes.merge(editor_props.to_attributes)
@@ -148,19 +151,22 @@ module CKEditor5::Rails
148
151
  editor_config
149
152
  end
150
153
 
151
- def validate_and_get_editor_context!
152
- unless defined?(@__ckeditor_context)
153
- raise EditorContextError,
154
- 'CKEditor installation context is not defined. ' \
155
- 'Ensure ckeditor5_assets is called in the head section.'
156
- end
154
+ def ckeditor5_context_or_fallback(preset)
155
+ return @__ckeditor_context if @__ckeditor_context.present?
157
156
 
158
- @__ckeditor_context
159
- end
157
+ if preset.present?
158
+ found_preset = Engine.find_preset(preset)
159
+
160
+ return {
161
+ bundle: create_preset_bundle(found_preset),
162
+ preset: found_preset
163
+ }
164
+ end
160
165
 
161
- def find_preset(preset)
162
- Engine.find_preset(preset) or
163
- raise PresetNotFoundError, "Preset #{preset} is not defined."
166
+ {
167
+ bundle: nil,
168
+ preset: Engine.default_preset
169
+ }
164
170
  end
165
171
  end
166
172
  end