ckeditor5 1.0.5 → 1.1.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 +4 -4
- data/README.md +541 -86
- data/lib/ckeditor5/rails/assets/assets_bundle.rb +1 -1
- data/lib/ckeditor5/rails/assets/assets_bundle_html_serializer.rb +3 -2
- data/lib/ckeditor5/rails/assets/webcomponent.mjs +134 -19
- data/lib/ckeditor5/rails/cdn/ckbox_bundle.rb +1 -1
- data/lib/ckeditor5/rails/cdn/ckeditor_bundle.rb +1 -1
- data/lib/ckeditor5/rails/cdn/helpers.rb +34 -2
- data/lib/ckeditor5/rails/editor/helpers.rb +6 -1
- data/lib/ckeditor5/rails/editor/props.rb +2 -2
- data/lib/ckeditor5/rails/editor/props_inline_plugin.rb +32 -0
- data/lib/ckeditor5/rails/editor/props_plugin.rb +18 -13
- data/lib/ckeditor5/rails/engine.rb +21 -6
- data/lib/ckeditor5/rails/hooks/form.rb +23 -0
- data/lib/ckeditor5/rails/hooks/simple_form.rb +25 -0
- data/lib/ckeditor5/rails/presets.rb +122 -11
- data/lib/ckeditor5/rails/semver.rb +1 -1
- data/lib/ckeditor5/rails/version.rb +1 -1
- metadata +5 -2
@@ -41,7 +41,7 @@ module CKEditor5::Rails::Assets
|
|
41
41
|
class JSExportsMeta
|
42
42
|
attr_reader :url, :import_meta
|
43
43
|
|
44
|
-
delegate :esm?, :window?, :import_name, :window_name, :import_as, to: :import_meta
|
44
|
+
delegate :esm?, :window?, :import_name, :window_name, :import_as, :to_h, to: :import_meta
|
45
45
|
|
46
46
|
def initialize(url, translation: false, **import_options)
|
47
47
|
@url = url
|
@@ -63,13 +63,14 @@ module CKEditor5::Rails::Assets
|
|
63
63
|
|
64
64
|
def styles_tags
|
65
65
|
@styles_tags ||= safe_join(bundle.stylesheets.map do |url|
|
66
|
-
tag.link(href: url, rel: 'stylesheet')
|
66
|
+
tag.link(href: url, rel: 'stylesheet', crossorigin: 'anonymous')
|
67
67
|
end)
|
68
68
|
end
|
69
69
|
|
70
70
|
def preload_tags
|
71
71
|
@preload_tags ||= safe_join(bundle.preloads.map do |url|
|
72
|
-
tag.link(href: url, rel: 'preload', as: self.class.url_resource_preload_type(url)
|
72
|
+
tag.link(href: url, rel: 'preload', as: self.class.url_resource_preload_type(url),
|
73
|
+
crossorigin: 'anonymous')
|
73
74
|
end)
|
74
75
|
end
|
75
76
|
end
|
@@ -23,7 +23,8 @@
|
|
23
23
|
*/
|
24
24
|
class CKEditorComponent extends HTMLElement {
|
25
25
|
/**
|
26
|
-
* List of attributes that trigger updates when changed
|
26
|
+
* List of attributes that trigger updates when changed.
|
27
|
+
*
|
27
28
|
* @static
|
28
29
|
* @returns {string[]} Array of attribute names to observe
|
29
30
|
*/
|
@@ -31,6 +32,16 @@ class CKEditorComponent extends HTMLElement {
|
|
31
32
|
return ['config', 'plugins', 'translations', 'type'];
|
32
33
|
}
|
33
34
|
|
35
|
+
/**
|
36
|
+
* List of input attributes that trigger updates when changed.
|
37
|
+
*
|
38
|
+
* @static
|
39
|
+
* @returns {string[]} Array of input attribute names to observe
|
40
|
+
*/
|
41
|
+
static get inputAttributes() {
|
42
|
+
return ['name', 'required', 'value'];
|
43
|
+
}
|
44
|
+
|
34
45
|
/** @type {Promise<import('ckeditor5').Editor>|null} Promise to initialize editor instance */
|
35
46
|
instancePromise = Promise.withResolvers();
|
36
47
|
|
@@ -134,16 +145,17 @@ class CKEditorComponent extends HTMLElement {
|
|
134
145
|
content = editablesOrContent.main;
|
135
146
|
}
|
136
147
|
|
137
|
-
const
|
138
|
-
|
139
|
-
{
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
148
|
+
const config = {
|
149
|
+
...this.#getConfig(),
|
150
|
+
...translations.length && {
|
151
|
+
translations
|
152
|
+
},
|
153
|
+
plugins,
|
154
|
+
};
|
155
|
+
|
156
|
+
console.warn('Initializing CKEditor with config:', config);
|
157
|
+
|
158
|
+
const instance = await Editor.create(content, config);
|
147
159
|
|
148
160
|
this.dispatchEvent(new CustomEvent('editor-ready', { detail: instance }));
|
149
161
|
|
@@ -168,6 +180,7 @@ class CKEditorComponent extends HTMLElement {
|
|
168
180
|
|
169
181
|
if (!this.isMultiroot() && !this.isDecoupled()) {
|
170
182
|
this.innerHTML = `<${this.#editorElementTag}></${this.#editorElementTag}>`;
|
183
|
+
this.#assignInputAttributes();
|
171
184
|
}
|
172
185
|
|
173
186
|
// Let's track changes in editables if it's a multiroot editor.
|
@@ -181,6 +194,8 @@ class CKEditorComponent extends HTMLElement {
|
|
181
194
|
|
182
195
|
try {
|
183
196
|
this.instance = await this.#initializeEditor(this.editables || this.#getConfig().initialData || '');
|
197
|
+
this.#setupContentSync();
|
198
|
+
|
184
199
|
this.instancePromise.resolve(this.instance);
|
185
200
|
} catch (err) {
|
186
201
|
this.instancePromise.reject(err);
|
@@ -251,6 +266,74 @@ class CKEditorComponent extends HTMLElement {
|
|
251
266
|
return { main: mainEditable };
|
252
267
|
}
|
253
268
|
|
269
|
+
/**
|
270
|
+
* Copies input-related attributes from component to the main editable element
|
271
|
+
*
|
272
|
+
* @private
|
273
|
+
*/
|
274
|
+
#assignInputAttributes() {
|
275
|
+
const textarea = this.querySelector('textarea');
|
276
|
+
|
277
|
+
if (!textarea) {
|
278
|
+
return;
|
279
|
+
}
|
280
|
+
|
281
|
+
for (const attr of CKEditorComponent.inputAttributes) {
|
282
|
+
if (this.hasAttribute(attr)) {
|
283
|
+
textarea.setAttribute(attr, this.getAttribute(attr));
|
284
|
+
}
|
285
|
+
}
|
286
|
+
}
|
287
|
+
|
288
|
+
/**
|
289
|
+
* Sets up content sync between editor and textarea element.
|
290
|
+
*
|
291
|
+
* @private
|
292
|
+
*/
|
293
|
+
#setupContentSync() {
|
294
|
+
if (!this.instance) {
|
295
|
+
return;
|
296
|
+
}
|
297
|
+
|
298
|
+
const textarea = this.querySelector('textarea');
|
299
|
+
|
300
|
+
if (!textarea) {
|
301
|
+
return;
|
302
|
+
}
|
303
|
+
|
304
|
+
// Initial sync
|
305
|
+
const syncInput = () => {
|
306
|
+
this.style.position = 'relative';
|
307
|
+
|
308
|
+
textarea.value = this.instance.getData();
|
309
|
+
textarea.tabIndex = -1;
|
310
|
+
|
311
|
+
Object.assign(textarea.style, {
|
312
|
+
display: 'flex',
|
313
|
+
position: 'absolute',
|
314
|
+
bottom: '0',
|
315
|
+
left: '50%',
|
316
|
+
width: '1px',
|
317
|
+
height: '1px',
|
318
|
+
opacity: '0',
|
319
|
+
pointerEvents: 'none',
|
320
|
+
margin: '0',
|
321
|
+
padding: '0',
|
322
|
+
border: 'none'
|
323
|
+
});
|
324
|
+
};
|
325
|
+
|
326
|
+
syncInput();
|
327
|
+
|
328
|
+
// Listen for changes
|
329
|
+
this.instance.model.document.on('change:data', () => {
|
330
|
+
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
331
|
+
textarea.dispatchEvent(new Event('change', { bubbles: true }));
|
332
|
+
|
333
|
+
syncInput();
|
334
|
+
});
|
335
|
+
}
|
336
|
+
|
254
337
|
/**
|
255
338
|
* Loads translation modules
|
256
339
|
*
|
@@ -624,16 +707,48 @@ function execIfDOMReady(callback) {
|
|
624
707
|
* @returns {Promise<Array<any>>} Loaded modules
|
625
708
|
*/
|
626
709
|
function loadAsyncImports(imports = []) {
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
710
|
+
const loadInlinePlugin = async ({ name, code }) => {
|
711
|
+
const module = await import(`data:text/javascript,${encodeURIComponent(code)}`);
|
712
|
+
|
713
|
+
if (!module.default) {
|
714
|
+
throw new Error(`Inline plugin "${name}" must export a default class/function!`);
|
715
|
+
}
|
716
|
+
|
717
|
+
return module.default;
|
718
|
+
};
|
719
|
+
|
720
|
+
const loadExternalPlugin = async ({ import_name, import_as, window_name }) => {
|
721
|
+
if (window_name) {
|
722
|
+
if (!Object.prototype.hasOwnProperty.call(window, window_name)) {
|
723
|
+
throw new Error(
|
724
|
+
`Plugin window['${window_name}'] not found in global scope. ` +
|
725
|
+
'Please ensure the plugin is loaded before CKEditor initialization.'
|
726
|
+
);
|
631
727
|
}
|
632
728
|
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
729
|
+
return window[window_name];
|
730
|
+
}
|
731
|
+
|
732
|
+
const module = await import(import_name);
|
733
|
+
const imported = module[import_as || 'default'];
|
734
|
+
|
735
|
+
if (!imported) {
|
736
|
+
throw new Error(`Plugin "${import_as}" not found in the ESM module "${import_name}"!`);
|
737
|
+
}
|
738
|
+
|
739
|
+
return imported;
|
740
|
+
};
|
741
|
+
|
742
|
+
return Promise.all(imports.map(item => {
|
743
|
+
switch(item.type) {
|
744
|
+
case 'inline':
|
745
|
+
return loadInlinePlugin(item);
|
746
|
+
|
747
|
+
case 'external':
|
748
|
+
default:
|
749
|
+
return loadExternalPlugin(item);
|
750
|
+
}
|
751
|
+
}));
|
637
752
|
}
|
638
753
|
|
639
754
|
customElements.define('ckeditor-component', CKEditorComponent);
|
@@ -7,7 +7,7 @@ module CKEditor5::Rails
|
|
7
7
|
|
8
8
|
attr_reader :cdn, :version, :theme, :translations
|
9
9
|
|
10
|
-
def initialize(version, theme: :lark, cdn: Engine.
|
10
|
+
def initialize(version, theme: :lark, cdn: Engine.default_preset.cdn, translations: [])
|
11
11
|
raise ArgumentError, 'version must be semver' unless version.is_a?(Semver)
|
12
12
|
raise ArgumentError, 'theme must be a string' unless theme.is_a?(String)
|
13
13
|
raise ArgumentError, 'translations must be an array' unless translations.is_a?(Array)
|
@@ -7,7 +7,7 @@ module CKEditor5::Rails
|
|
7
7
|
|
8
8
|
attr_reader :version, :translations, :import_name
|
9
9
|
|
10
|
-
def initialize(version, import_name, cdn: Engine.
|
10
|
+
def initialize(version, import_name, cdn: Engine.default_preset.cdn, translations: [])
|
11
11
|
raise ArgumentError, 'version must be semver' unless version.is_a?(Semver)
|
12
12
|
raise ArgumentError, 'import_name must be a string' unless import_name.is_a?(String)
|
13
13
|
raise ArgumentError, 'translations must be an array' unless translations.is_a?(Array)
|
@@ -6,7 +6,17 @@ require_relative 'ckbox_bundle'
|
|
6
6
|
|
7
7
|
module CKEditor5::Rails
|
8
8
|
module Cdn::Helpers
|
9
|
-
def ckeditor5_cdn_assets(
|
9
|
+
def ckeditor5_cdn_assets(preset: :default, **kwargs)
|
10
|
+
merge_with_editor_preset(preset, **kwargs) => {
|
11
|
+
cdn:,
|
12
|
+
version:,
|
13
|
+
translations:,
|
14
|
+
ckbox:,
|
15
|
+
license_key:,
|
16
|
+
premium:,
|
17
|
+
**kwargs
|
18
|
+
}
|
19
|
+
|
10
20
|
bundle = build_base_cdn_bundle(cdn, version, translations)
|
11
21
|
bundle << build_premium_cdn_bundle(cdn, version, translations) if premium
|
12
22
|
bundle << build_ckbox_cdn_bundle(ckbox) if ckbox
|
@@ -29,12 +39,34 @@ module CKEditor5::Rails
|
|
29
39
|
if kwargs[:license_key] && kwargs[:license_key] != 'GPL'
|
30
40
|
ckeditor5_cloud_assets(**kwargs)
|
31
41
|
else
|
32
|
-
ckeditor5_cdn_assets(**kwargs.merge(cdn: Engine.
|
42
|
+
ckeditor5_cdn_assets(**kwargs.merge(cdn: Engine.default_preset.cdn))
|
33
43
|
end
|
34
44
|
end
|
35
45
|
|
36
46
|
private
|
37
47
|
|
48
|
+
def merge_with_editor_preset(preset, **kwargs)
|
49
|
+
found_preset = Engine.base.presets[preset]
|
50
|
+
|
51
|
+
if found_preset.blank?
|
52
|
+
raise ArgumentError,
|
53
|
+
"Poor thing. You forgot to define your #{preset} preset. " \
|
54
|
+
'Please define it in initializer. Thank you!'
|
55
|
+
end
|
56
|
+
|
57
|
+
hash = found_preset.to_h_with_overrides(**kwargs)
|
58
|
+
|
59
|
+
%i[version type].each do |key|
|
60
|
+
next if hash[key].present?
|
61
|
+
|
62
|
+
raise ArgumentError,
|
63
|
+
"Poor thing. You forgot to define #{key}. Make sure you passed `#{key}:` parameter to " \
|
64
|
+
"`ckeditor5_cdn_assets` or defined default one in your `#{preset}` preset!"
|
65
|
+
end
|
66
|
+
|
67
|
+
hash
|
68
|
+
end
|
69
|
+
|
38
70
|
def build_base_cdn_bundle(cdn, version, translations)
|
39
71
|
Cdn::CKEditorBundle.new(
|
40
72
|
Semver.new(version),
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'props_plugin'
|
4
|
+
require_relative 'props_inline_plugin'
|
4
5
|
require_relative 'props'
|
5
6
|
|
6
7
|
module CKEditor5::Rails
|
@@ -11,6 +12,7 @@ module CKEditor5::Rails
|
|
11
12
|
def ckeditor5_editor(
|
12
13
|
config: nil, extra_config: {},
|
13
14
|
type: nil, preset: :default,
|
15
|
+
initial_data: nil,
|
14
16
|
**html_attributes, &block
|
15
17
|
)
|
16
18
|
context = validate_and_get_editor_context!
|
@@ -19,8 +21,11 @@ module CKEditor5::Rails
|
|
19
21
|
config ||= preset.config
|
20
22
|
type ||= preset.type
|
21
23
|
|
24
|
+
config = config.deep_merge(extra_config)
|
25
|
+
config[:initialData] = initial_data if initial_data
|
26
|
+
|
22
27
|
editor_props = build_editor_props(
|
23
|
-
config: config
|
28
|
+
config: config,
|
24
29
|
type: type,
|
25
30
|
context: context
|
26
31
|
)
|
@@ -44,7 +44,7 @@ module CKEditor5::Rails::Editor
|
|
44
44
|
end
|
45
45
|
|
46
46
|
def serialize_translations
|
47
|
-
context[:bundle].translations_scripts.map
|
47
|
+
context[:bundle].translations_scripts.map(&:to_h).to_json
|
48
48
|
end
|
49
49
|
|
50
50
|
def serialize_plugins
|
@@ -54,7 +54,7 @@ module CKEditor5::Rails::Editor
|
|
54
54
|
def serialize_config
|
55
55
|
config
|
56
56
|
.except(:plugins)
|
57
|
-
.
|
57
|
+
.tap { |cfg| cfg[:licenseKey] = context[:license_key] if context[:license_key] }
|
58
58
|
.to_json
|
59
59
|
end
|
60
60
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CKEditor5::Rails::Editor
|
4
|
+
class PropsInlinePlugin
|
5
|
+
def initialize(name, code)
|
6
|
+
@name = name
|
7
|
+
@code = code
|
8
|
+
validate_code!
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_h
|
12
|
+
{
|
13
|
+
type: :inline,
|
14
|
+
name: name,
|
15
|
+
code: code
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_reader :name, :code
|
22
|
+
|
23
|
+
def validate_code!
|
24
|
+
raise ArgumentError, 'Code must be a String' unless code.is_a?(String)
|
25
|
+
|
26
|
+
return if code.include?('export default')
|
27
|
+
|
28
|
+
raise ArgumentError,
|
29
|
+
'Code must include `export default` that exports plugin definition!'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -4,30 +4,35 @@ module CKEditor5::Rails::Editor
|
|
4
4
|
class PropsPlugin
|
5
5
|
delegate :to_h, to: :import_meta
|
6
6
|
|
7
|
-
def initialize(name, premium: false,
|
7
|
+
def initialize(name, premium: false, **js_import_meta)
|
8
8
|
@name = name
|
9
|
-
@
|
10
|
-
|
11
|
-
|
9
|
+
@js_import_meta = if js_import_meta.empty?
|
10
|
+
{ import_name: premium ? 'ckeditor5-premium-features' : 'ckeditor5' }
|
11
|
+
else
|
12
|
+
js_import_meta
|
13
|
+
end
|
12
14
|
end
|
13
15
|
|
14
16
|
def self.normalize(plugin)
|
15
17
|
case plugin
|
16
18
|
when String, Symbol then new(plugin)
|
17
|
-
when PropsPlugin then plugin
|
19
|
+
when PropsPlugin, PropsInlinePlugin then plugin
|
18
20
|
else raise ArgumentError, "Invalid plugin: #{plugin}"
|
19
21
|
end
|
20
22
|
end
|
21
23
|
|
22
|
-
|
23
|
-
|
24
|
-
|
24
|
+
def to_h
|
25
|
+
meta = ::CKEditor5::Rails::Assets::JSImportMeta.new(
|
26
|
+
import_as: js_import_meta[:window_name] ? nil : name,
|
27
|
+
**js_import_meta
|
28
|
+
).to_h
|
25
29
|
|
26
|
-
|
27
|
-
|
28
|
-
import_as: name,
|
29
|
-
import_name: import_name
|
30
|
-
)
|
30
|
+
meta.merge!({ type: :external })
|
31
|
+
meta
|
31
32
|
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
attr_reader :name, :js_import_meta
|
32
37
|
end
|
33
38
|
end
|
@@ -2,17 +2,12 @@
|
|
2
2
|
|
3
3
|
require 'rails/engine'
|
4
4
|
require_relative 'presets'
|
5
|
+
require_relative 'hooks/form'
|
5
6
|
|
6
7
|
module CKEditor5::Rails
|
7
8
|
class Engine < ::Rails::Engine
|
8
9
|
config.ckeditor5 = ActiveSupport::OrderedOptions.new
|
9
10
|
|
10
|
-
# Specifies which CDN should be used to load CKEditor 5 using the ckeditor5_assets helper.
|
11
|
-
# It is possible to use the following CDNs:
|
12
|
-
# - :unpkg
|
13
|
-
# - :jsdelivr (default)
|
14
|
-
config.ckeditor5.default_cdn = :jsdelivr
|
15
|
-
|
16
11
|
# Specifies configuration of editors generated by gem.
|
17
12
|
config.ckeditor5.presets = PresetsManager.new
|
18
13
|
|
@@ -22,10 +17,30 @@ module CKEditor5::Rails
|
|
22
17
|
end
|
23
18
|
end
|
24
19
|
|
20
|
+
initializer 'ckeditor5.simple_form' do
|
21
|
+
next unless defined?(::SimpleForm)
|
22
|
+
|
23
|
+
require_relative 'hooks/simple_form'
|
24
|
+
|
25
|
+
::SimpleForm::FormBuilder.map_type :ckeditor5, to: Hooks::SimpleForm::CKEditor5Input
|
26
|
+
end
|
27
|
+
|
28
|
+
initializer 'ckeditor5.form_builder' do
|
29
|
+
require_relative 'hooks/form'
|
30
|
+
|
31
|
+
ActionView::Helpers::FormBuilder.include(
|
32
|
+
Hooks::Form::FormBuilderExtension
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
25
36
|
def self.base
|
26
37
|
config.ckeditor5
|
27
38
|
end
|
28
39
|
|
40
|
+
def self.default_preset
|
41
|
+
config.ckeditor5.presets.default
|
42
|
+
end
|
43
|
+
|
29
44
|
def self.configure
|
30
45
|
yield config.ckeditor5
|
31
46
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CKEditor5::Rails::Hooks
|
4
|
+
module Form
|
5
|
+
module FormBuilderExtension
|
6
|
+
def ckeditor5(method, options = {})
|
7
|
+
value = if object.respond_to?(method)
|
8
|
+
object.send(method)
|
9
|
+
else
|
10
|
+
options[:initial_data]
|
11
|
+
end
|
12
|
+
|
13
|
+
html_options = options.merge(
|
14
|
+
name: object_name,
|
15
|
+
required: options.delete(:required),
|
16
|
+
initial_data: value
|
17
|
+
)
|
18
|
+
|
19
|
+
@template.ckeditor5_editor(**html_options)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CKEditor5::Rails::Hooks
|
4
|
+
module SimpleForm
|
5
|
+
class CKEditor5Input < ::SimpleForm::Inputs::Base
|
6
|
+
def input(wrapper_options = nil)
|
7
|
+
merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)
|
8
|
+
@builder.template.ckeditor5_editor(**editor_options(merged_input_options))
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def editor_options(merged_input_options)
|
14
|
+
{
|
15
|
+
preset: input_options.fetch(:preset, :default),
|
16
|
+
type: input_options.fetch(:type, :classic),
|
17
|
+
config: input_options[:config],
|
18
|
+
initial_data: object.try(attribute_name) || input_options[:initial_data],
|
19
|
+
name: "#{object_name}[#{attribute_name}]",
|
20
|
+
**merged_input_options
|
21
|
+
}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|