ckeditor5 1.20.1 → 1.22.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +99 -2
- data/lib/ckeditor5/rails/assets/assets_bundle.rb +11 -1
- data/lib/ckeditor5/rails/assets/assets_bundle_html_serializer.rb +5 -10
- data/lib/ckeditor5/rails/assets/webcomponent_bundle.rb +10 -3
- data/lib/ckeditor5/rails/assets/webcomponents/components/editor.mjs +35 -2
- data/lib/ckeditor5/rails/assets/webcomponents/utils.mjs +36 -2
- data/lib/ckeditor5/rails/cdn/concerns/bundle_builder.rb +57 -0
- data/lib/ckeditor5/rails/cdn/helpers.rb +62 -55
- data/lib/ckeditor5/rails/editor/helpers/editor_helpers.rb +25 -19
- data/lib/ckeditor5/rails/editor/props.rb +7 -14
- data/lib/ckeditor5/rails/engine.rb +13 -0
- data/lib/ckeditor5/rails/presets/manager.rb +2 -0
- data/lib/ckeditor5/rails/presets/preset_builder.rb +1 -1
- data/lib/ckeditor5/rails/presets/toolbar_builder.rb +117 -3
- data/lib/ckeditor5/rails/version.rb +1 -1
- data/spec/e2e/features/ajax_form_integration_spec.rb +78 -0
- data/spec/e2e/features/lazy_assets_spec.rb +54 -0
- data/spec/e2e/support/form_helpers.rb +4 -1
- data/spec/lib/ckeditor5/rails/assets/assets_bundle_hml_serializer_spec.rb +53 -0
- data/spec/lib/ckeditor5/rails/assets/assets_bundle_spec.rb +2 -1
- data/spec/lib/ckeditor5/rails/cdn/helpers_spec.rb +95 -3
- data/spec/lib/ckeditor5/rails/editor/helpers/editor_helpers_spec.rb +85 -25
- data/spec/lib/ckeditor5/rails/editor/props_spec.rb +14 -34
- data/spec/lib/ckeditor5/rails/engine_spec.rb +10 -0
- data/spec/lib/ckeditor5/rails/hooks/form_spec.rb +15 -2
- data/spec/lib/ckeditor5/rails/plugins/wproofreader_spec.rb +2 -0
- data/spec/lib/ckeditor5/rails/presets/toolbar_builder_spec.rb +119 -0
- data/spec/lib/ckeditor5/rails/version_detector_spec.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 541773f43272493bbe29d3baf236cab3e85231f03b31c5d070b4917e6ecdea26
|
4
|
+
data.tar.gz: cb06989fd313768d6c1a5990f3dd49525fd1482b29a6b542c6d2778092eb3ad5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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>
|
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({
|
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
|
-
|
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
|
-
|
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
|
561
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
100
|
-
|
134
|
+
def combined_bundle
|
135
|
+
acc = Assets::AssetsBundle.new(scripts: [], stylesheets: [])
|
101
136
|
|
102
|
-
|
103
|
-
|
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
|
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
|
-
|
68
|
+
context = ckeditor5_context_or_fallback(preset)
|
71
69
|
|
72
|
-
preset = find_preset(preset ||
|
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
|
-
|
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
|
152
|
-
|
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
|
-
|
159
|
-
|
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
|
-
|
162
|
-
|
163
|
-
|
166
|
+
{
|
167
|
+
bundle: nil,
|
168
|
+
preset: Engine.default_preset
|
169
|
+
}
|
164
170
|
end
|
165
171
|
end
|
166
172
|
end
|