ckeditor5 1.0.5 → 1.1.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 +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
|