ckeditor5 1.15.9 → 1.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/lib/ckeditor5/rails/assets/webcomponents/components/context.mjs +23 -0
- data/lib/ckeditor5/rails/assets/webcomponents/components/editor.mjs +24 -0
- data/lib/ckeditor5/rails/assets/webcomponents/utils.mjs +33 -10
- data/lib/ckeditor5/rails/concerns/checksum.rb +15 -0
- data/lib/ckeditor5/rails/context/props.rb +17 -2
- data/lib/ckeditor5/rails/editor/props.rb +16 -3
- data/lib/ckeditor5/rails/editor/props_base_plugin.rb +19 -0
- data/lib/ckeditor5/rails/editor/props_inline_plugin.rb +6 -3
- data/lib/ckeditor5/rails/editor/props_plugin.rb +6 -4
- data/lib/ckeditor5/rails/engine.rb +3 -2
- data/lib/ckeditor5/rails/presets/plugins_builder.rb +9 -9
- data/lib/ckeditor5/rails/presets/preset_builder.rb +4 -6
- data/lib/ckeditor5/rails/version.rb +1 -1
- data/lib/ckeditor5/rails.rb +1 -0
- data/spec/e2e/features/editor_types_spec.rb +178 -0
- data/spec/e2e/features/form_integration_spec.rb +60 -0
- data/spec/e2e/spec_helper.rb +43 -0
- data/spec/e2e/support/eventually.rb +14 -0
- data/spec/e2e/support/form_helpers.rb +37 -0
- data/spec/lib/ckeditor5/rails/assets/asset_bundle_hml_serializer_spec.rb +7 -0
- data/spec/lib/ckeditor5/rails/cdn/helpers_spec.rb +19 -1
- data/spec/lib/ckeditor5/rails/concerns/checksum_spec.rb +50 -0
- data/spec/lib/ckeditor5/rails/context/props_spec.rb +7 -1
- data/spec/lib/ckeditor5/rails/editor/props_base_plugin_spec.rb +27 -0
- data/spec/lib/ckeditor5/rails/editor/props_spec.rb +2 -0
- data/spec/lib/ckeditor5/rails/engine_spec.rb +88 -0
- data/spec/lib/ckeditor5/rails/hooks/form_spec.rb +156 -0
- data/spec/lib/ckeditor5/rails/hooks/simple_form_spec.rb +100 -0
- data/spec/lib/ckeditor5/rails/presets/plugins_builder_spec.rb +10 -10
- data/spec/lib/ckeditor5/rails/presets/preset_builder_spec.rb +10 -0
- data/spec/spec_helper.rb +2 -2
- metadata +22 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e91fe2d11179c703901756efa978fcedcd3030d0381c4e550ab8c9c15ef73a19
|
4
|
+
data.tar.gz: 4119948cdae51969b3d1e77801f20faecd81cfbf2755388fc444421e7da8bdb9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d4f236d5bc883344daa8fa9bc0b7d55e3898360dd8fabfd48265bbd1eedf3c654e8ba6baf5b0a1ba5ffa215c6b2c14a60c02f7182b81885a875b0a4902f67746
|
7
|
+
data.tar.gz: 593de9199583c1f22a38b9858967eca63b9c98eef8b7a6a1e5a06ccfa1df759424dab96ed88c1719614744b26c8a32920722156875ba748e66c80272eeb0fd78
|
data/Gemfile
CHANGED
@@ -12,7 +12,12 @@ class CKEditorContextComponent extends HTMLElement {
|
|
12
12
|
/** @type {Set<CKEditorComponent>} */
|
13
13
|
#connectedEditors = new Set();
|
14
14
|
|
15
|
+
/** @type {String} Attributes checksum hash */
|
16
|
+
#integrity = '';
|
17
|
+
|
15
18
|
async connectedCallback() {
|
19
|
+
this.#integrity = this.getAttribute('integrity');
|
20
|
+
|
16
21
|
try {
|
17
22
|
execIfDOMReady(() => this.#initializeContext());
|
18
23
|
} catch (error) {
|
@@ -52,6 +57,22 @@ class CKEditorContextComponent extends HTMLElement {
|
|
52
57
|
this.#connectedEditors.delete(editor);
|
53
58
|
}
|
54
59
|
|
60
|
+
/**
|
61
|
+
* Validates editor configuration integrity hash to prevent attacks.
|
62
|
+
*/
|
63
|
+
async #validateIntegrity() {
|
64
|
+
const integrity = await calculateChecksum({
|
65
|
+
plugins: this.getAttribute('plugins'),
|
66
|
+
});
|
67
|
+
|
68
|
+
if (integrity !== this.#integrity) {
|
69
|
+
throw new Error(
|
70
|
+
'Configuration integrity check failed. It means that #integrity attributes mismatch from attributes passed to webcomponent. ' +
|
71
|
+
'This could be a security issue. Please check if you are passing correct attributes to the webcomponent.'
|
72
|
+
);
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
55
76
|
/**
|
56
77
|
* Initialize CKEditor context with shared configuration
|
57
78
|
*
|
@@ -66,6 +87,8 @@ class CKEditorContextComponent extends HTMLElement {
|
|
66
87
|
this.instance = null;
|
67
88
|
}
|
68
89
|
|
90
|
+
await this.#validateIntegrity();
|
91
|
+
|
69
92
|
const { Context, ContextWatchdog } = await import('ckeditor5');
|
70
93
|
const plugins = await this.#getPlugins();
|
71
94
|
const config = this.#getConfig();
|
@@ -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 {String} Attributes checksum hash */
|
44
|
+
#integrity = '';
|
45
|
+
|
43
46
|
/** @type {(event: CustomEvent) => void} Event handler for editor change */
|
44
47
|
get oneditorchange() {
|
45
48
|
return this.#getEventHandler('editorchange');
|
@@ -106,9 +109,11 @@ class CKEditorComponent extends HTMLElement {
|
|
106
109
|
/**
|
107
110
|
* Lifecycle callback when element is connected to DOM
|
108
111
|
* Initializes the editor when DOM is ready
|
112
|
+
*
|
109
113
|
* @protected
|
110
114
|
*/
|
111
115
|
connectedCallback() {
|
116
|
+
this.#integrity = this.getAttribute('integrity');
|
112
117
|
this.#context = this.closest('ckeditor-context-component');
|
113
118
|
this.#initialHTML = this.innerHTML;
|
114
119
|
|
@@ -228,6 +233,23 @@ class CKEditorComponent extends HTMLElement {
|
|
228
233
|
return resolveElementReferences(config);
|
229
234
|
}
|
230
235
|
|
236
|
+
/**
|
237
|
+
* Validates editor configuration integrity hash to prevent attacks.
|
238
|
+
*/
|
239
|
+
async #validateIntegrity() {
|
240
|
+
const integrity = await calculateChecksum({
|
241
|
+
translations: this.getAttribute('translations'),
|
242
|
+
plugins: this.getAttribute('plugins'),
|
243
|
+
});
|
244
|
+
|
245
|
+
if (integrity !== this.#integrity) {
|
246
|
+
throw new Error(
|
247
|
+
'Configuration integrity check failed. It means that #integrity attributes mismatch from attributes passed to webcomponent. ' +
|
248
|
+
'This could be a security issue. Please check if you are passing correct attributes to the webcomponent.'
|
249
|
+
);
|
250
|
+
}
|
251
|
+
}
|
252
|
+
|
231
253
|
/**
|
232
254
|
* Creates a new CKEditor instance
|
233
255
|
*
|
@@ -237,6 +259,8 @@ class CKEditorComponent extends HTMLElement {
|
|
237
259
|
* @throws {Error} When initialization fails
|
238
260
|
*/
|
239
261
|
async #initializeEditor(editablesOrContent) {
|
262
|
+
await this.#validateIntegrity();
|
263
|
+
|
240
264
|
const Editor = await this.#getEditorConstructor();
|
241
265
|
const [plugins, translations] = await Promise.all([
|
242
266
|
this.#getPlugins(),
|
@@ -66,16 +66,18 @@ function loadAsyncImports(imports = []) {
|
|
66
66
|
return imported;
|
67
67
|
};
|
68
68
|
|
69
|
-
return Promise.all(
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
69
|
+
return Promise.all(
|
70
|
+
imports.map(async (item) => {
|
71
|
+
switch(item.type) {
|
72
|
+
case 'inline':
|
73
|
+
return loadInlinePlugin(item);
|
74
|
+
|
75
|
+
case 'external':
|
76
|
+
default:
|
77
|
+
return loadExternalPlugin(item);
|
78
|
+
}
|
79
|
+
})
|
80
|
+
);
|
79
81
|
}
|
80
82
|
|
81
83
|
/**
|
@@ -153,3 +155,24 @@ function resolveElementReferences(obj) {
|
|
153
155
|
function uid() {
|
154
156
|
return Math.random().toString(36).substring(2);
|
155
157
|
}
|
158
|
+
|
159
|
+
/**
|
160
|
+
* Calculates checksum for an object.
|
161
|
+
*/
|
162
|
+
async function calculateChecksum(obj) {
|
163
|
+
const objCopy = { ...obj, checksum: undefined };
|
164
|
+
|
165
|
+
return sha256(JSON.stringify(objCopy));
|
166
|
+
}
|
167
|
+
|
168
|
+
/**
|
169
|
+
* Calculates SHA-256 hash for a string
|
170
|
+
*/
|
171
|
+
async function sha256(str) {
|
172
|
+
const buffer = new TextEncoder().encode(str);
|
173
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
|
174
|
+
|
175
|
+
return Array.from(new Uint8Array(hashBuffer))
|
176
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
177
|
+
.join('');
|
178
|
+
}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module CKEditor5::Rails::Concerns
|
7
|
+
module Checksum
|
8
|
+
private
|
9
|
+
|
10
|
+
def calculate_object_checksum(obj)
|
11
|
+
json = JSON.generate(obj)
|
12
|
+
Digest::SHA256.hexdigest(json)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -3,14 +3,16 @@
|
|
3
3
|
module CKEditor5::Rails
|
4
4
|
module Context
|
5
5
|
class Props
|
6
|
+
include CKEditor5::Rails::Concerns::Checksum
|
7
|
+
|
6
8
|
def initialize(config)
|
7
9
|
@config = config
|
8
10
|
end
|
9
11
|
|
10
12
|
def to_attributes
|
11
13
|
{
|
12
|
-
|
13
|
-
|
14
|
+
**serialized_attributes,
|
15
|
+
integrity: integrity_checksum
|
14
16
|
}
|
15
17
|
end
|
16
18
|
|
@@ -18,6 +20,19 @@ module CKEditor5::Rails
|
|
18
20
|
|
19
21
|
attr_reader :config
|
20
22
|
|
23
|
+
def integrity_checksum
|
24
|
+
unsafe_attributes = serialized_attributes.slice(:plugins)
|
25
|
+
|
26
|
+
calculate_object_checksum(unsafe_attributes)
|
27
|
+
end
|
28
|
+
|
29
|
+
def serialized_attributes
|
30
|
+
@serialized_attributes ||= {
|
31
|
+
plugins: serialize_plugins,
|
32
|
+
config: serialize_config
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
21
36
|
def serialize_plugins
|
22
37
|
(config[:plugins] || []).map { |plugin| Editor::PropsPlugin.normalize(plugin).to_h }.to_json
|
23
38
|
end
|
@@ -5,6 +5,8 @@ require_relative 'editable_height_normalizer'
|
|
5
5
|
|
6
6
|
module CKEditor5::Rails::Editor
|
7
7
|
class Props
|
8
|
+
include CKEditor5::Rails::Concerns::Checksum
|
9
|
+
|
8
10
|
EDITOR_TYPES = {
|
9
11
|
classic: 'ClassicEditor',
|
10
12
|
inline: 'InlineEditor',
|
@@ -25,8 +27,9 @@ module CKEditor5::Rails::Editor
|
|
25
27
|
|
26
28
|
def to_attributes
|
27
29
|
{
|
30
|
+
**serialized_attributes,
|
28
31
|
type: EDITOR_TYPES[@type],
|
29
|
-
|
32
|
+
integrity: integrity_checksum
|
30
33
|
}
|
31
34
|
end
|
32
35
|
|
@@ -38,14 +41,24 @@ module CKEditor5::Rails::Editor
|
|
38
41
|
|
39
42
|
attr_reader :controller_context, :watchdog, :type, :config, :editable_height
|
40
43
|
|
44
|
+
def integrity_checksum
|
45
|
+
unsafe_attributes = serialized_attributes.slice(:translations, :plugins)
|
46
|
+
|
47
|
+
calculate_object_checksum(unsafe_attributes)
|
48
|
+
end
|
49
|
+
|
41
50
|
def serialized_attributes
|
42
|
-
|
51
|
+
return @serialized_attributes if defined?(@serialized_attributes)
|
52
|
+
|
53
|
+
attributes = {
|
43
54
|
translations: serialize_translations,
|
44
55
|
plugins: serialize_plugins,
|
45
56
|
config: serialize_config,
|
46
57
|
watchdog: watchdog
|
47
58
|
}
|
48
|
-
|
59
|
+
|
60
|
+
attributes.merge!(editable_height ? { 'editable-height' => editable_height } : {})
|
61
|
+
@serialized_attributes = attributes
|
49
62
|
end
|
50
63
|
|
51
64
|
def serialize_translations
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CKEditor5::Rails
|
4
|
+
module Editor
|
5
|
+
class PropsBasePlugin
|
6
|
+
include Concerns::Checksum
|
7
|
+
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
def initialize(name)
|
11
|
+
@name = name
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_h
|
15
|
+
raise NotImplementedError, 'This method must be implemented in a subclass'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -1,11 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'props_base_plugin'
|
4
|
+
|
3
5
|
module CKEditor5::Rails::Editor
|
4
|
-
class PropsInlinePlugin
|
5
|
-
attr_reader :
|
6
|
+
class PropsInlinePlugin < PropsBasePlugin
|
7
|
+
attr_reader :code
|
6
8
|
|
7
9
|
def initialize(name, code)
|
8
|
-
|
10
|
+
super(name)
|
11
|
+
|
9
12
|
@code = code
|
10
13
|
validate_code!
|
11
14
|
end
|
@@ -1,12 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
class PropsPlugin
|
5
|
-
attr_reader :name, :js_import_meta
|
3
|
+
require_relative 'props_base_plugin'
|
6
4
|
|
7
|
-
|
5
|
+
module CKEditor5::Rails::Editor
|
6
|
+
class PropsPlugin < PropsBasePlugin
|
7
|
+
attr_reader :js_import_meta
|
8
8
|
|
9
9
|
def initialize(name, premium: false, **js_import_meta)
|
10
|
+
super(name)
|
11
|
+
|
10
12
|
@name = name
|
11
13
|
@js_import_meta = if js_import_meta.empty?
|
12
14
|
{ import_name: premium ? 'ckeditor5-premium-features' : 'ckeditor5' }
|
@@ -45,7 +45,7 @@ module CKEditor5::Rails
|
|
45
45
|
def find_preset(preset)
|
46
46
|
return preset if preset.is_a?(CKEditor5::Rails::Presets::PresetBuilder)
|
47
47
|
|
48
|
-
|
48
|
+
base.presets[preset]
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
@@ -54,7 +54,8 @@ module CKEditor5::Rails
|
|
54
54
|
|
55
55
|
delegate :version, :gpl, :premium, :cdn, :translations, :license_key,
|
56
56
|
:type, :menubar, :toolbar, :plugins, :plugin, :inline_plugin,
|
57
|
-
:language, :ckbox, :configure,
|
57
|
+
:language, :ckbox, :configure, :automatic_upgrades, :simple_upload_adapter,
|
58
|
+
:editable_height, to: :default_preset
|
58
59
|
|
59
60
|
def initialize(configuration)
|
60
61
|
@configuration = configuration
|
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
module CKEditor5::Rails
|
4
4
|
class Presets::PluginsBuilder
|
5
|
-
attr_reader :
|
5
|
+
attr_reader :items
|
6
6
|
|
7
7
|
def initialize(plugins)
|
8
|
-
@
|
8
|
+
@items = plugins
|
9
9
|
end
|
10
10
|
|
11
11
|
def self.create_plugin(name, **kwargs)
|
@@ -17,19 +17,19 @@ module CKEditor5::Rails
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def remove(*names)
|
20
|
-
names.each { |name|
|
20
|
+
names.each { |name| items.delete_if { |plugin| plugin.name == name } }
|
21
21
|
end
|
22
22
|
|
23
23
|
def prepend(*names, before: nil, **kwargs)
|
24
24
|
new_plugins = names.map { |name| self.class.create_plugin(name, **kwargs) }
|
25
25
|
|
26
26
|
if before
|
27
|
-
index =
|
27
|
+
index = items.index { |p| p.name == before }
|
28
28
|
raise ArgumentError, "Plugin '#{before}' not found" unless index
|
29
29
|
|
30
|
-
|
30
|
+
items.insert(index, *new_plugins)
|
31
31
|
else
|
32
|
-
|
32
|
+
items.insert(0, *new_plugins)
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
@@ -37,12 +37,12 @@ module CKEditor5::Rails
|
|
37
37
|
new_plugins = names.map { |name| self.class.create_plugin(name, **kwargs) }
|
38
38
|
|
39
39
|
if after
|
40
|
-
index =
|
40
|
+
index = items.index { |p| p.name == after }
|
41
41
|
raise ArgumentError, "Plugin '#{after}' not found" unless index
|
42
42
|
|
43
|
-
|
43
|
+
items.insert(index + 1, *new_plugins)
|
44
44
|
else
|
45
|
-
|
45
|
+
items.push(*new_plugins)
|
46
46
|
end
|
47
47
|
end
|
48
48
|
end
|
@@ -162,10 +162,9 @@ module CKEditor5::Rails
|
|
162
162
|
}
|
163
163
|
end
|
164
164
|
|
165
|
-
return unless block
|
166
|
-
|
167
165
|
builder = ToolbarBuilder.new(@config[:toolbar][:items])
|
168
|
-
builder.instance_eval(&block)
|
166
|
+
builder.instance_eval(&block) if block_given?
|
167
|
+
builder
|
169
168
|
end
|
170
169
|
|
171
170
|
def inline_plugin(name, code)
|
@@ -183,10 +182,9 @@ module CKEditor5::Rails
|
|
183
182
|
|
184
183
|
names.each { |name| plugin(name, **kwargs) } unless names.empty?
|
185
184
|
|
186
|
-
return unless block
|
187
|
-
|
188
185
|
builder = PluginsBuilder.new(@config[:plugins])
|
189
|
-
builder.instance_eval(&block)
|
186
|
+
builder.instance_eval(&block) if block_given?
|
187
|
+
builder
|
190
188
|
end
|
191
189
|
|
192
190
|
def language(ui = nil, content: ui) # rubocop:disable Naming/MethodParameterName
|
data/lib/ckeditor5/rails.rb
CHANGED
@@ -5,6 +5,7 @@ module CKEditor5
|
|
5
5
|
require_relative 'rails/version'
|
6
6
|
require_relative 'rails/version_detector'
|
7
7
|
require_relative 'rails/semver'
|
8
|
+
require_relative 'rails/concerns/checksum'
|
8
9
|
require_relative 'rails/assets/assets_bundle'
|
9
10
|
require_relative 'rails/assets/assets_bundle_html_serializer'
|
10
11
|
require_relative 'rails/helpers'
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'e2e/spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe 'CKEditor5 Types Integration', type: :feature, js: true do
|
6
|
+
shared_examples 'an editor' do |path|
|
7
|
+
before { visit path }
|
8
|
+
|
9
|
+
it 'loads and initializes the editor' do
|
10
|
+
expect(page).to have_css('.ck-editor__editable', wait: 10)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
shared_examples 'an editor that fires change event with main payload' do |path|
|
15
|
+
before { visit path }
|
16
|
+
|
17
|
+
it 'sends properly change events with proper payload' do
|
18
|
+
editor = first('.ck-editor__editable')
|
19
|
+
|
20
|
+
# Set up detailed change event listener
|
21
|
+
page.execute_script(<<~JS)
|
22
|
+
window._editorEvents = [];
|
23
|
+
document.querySelector('ckeditor-component').addEventListener('editor-change', (e) => {
|
24
|
+
window._editorEvents.push({
|
25
|
+
data: e.detail.data,
|
26
|
+
hasEditor: !!e.detail.editor
|
27
|
+
});
|
28
|
+
});
|
29
|
+
JS
|
30
|
+
|
31
|
+
# Clear editor and type text
|
32
|
+
editor.click
|
33
|
+
editor.send_keys([[:control, 'a'], :backspace])
|
34
|
+
editor.send_keys('Hello from keyboard!')
|
35
|
+
|
36
|
+
# Wait for change events and verify the last one
|
37
|
+
eventually do
|
38
|
+
events = page.evaluate_script('window._editorEvents')
|
39
|
+
last_event = events.last
|
40
|
+
|
41
|
+
expect(last_event['data']).to eq('main' => '<p>Hello from keyboard!</p>')
|
42
|
+
expect(last_event['hasEditor']).to be true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
shared_examples 'a multiroot editor that fires change events' do |path, editables| # rubocop:disable Metrics/BlockLength
|
48
|
+
before { visit path }
|
49
|
+
|
50
|
+
it 'sends properly change events with proper payload for editables' do # rubocop:disable Metrics/BlockLength
|
51
|
+
editors = editables.map do |name|
|
52
|
+
find("[data-testid='#{name}-editable']")
|
53
|
+
end
|
54
|
+
|
55
|
+
# Set up detailed change event listener
|
56
|
+
page.execute_script(<<~JS)
|
57
|
+
window._editorEvents = [];
|
58
|
+
document.querySelector('ckeditor-component').addEventListener('editor-change', (e) => {
|
59
|
+
window._editorEvents.push({
|
60
|
+
data: e.detail.data,
|
61
|
+
hasEditor: !!e.detail.editor
|
62
|
+
});
|
63
|
+
});
|
64
|
+
JS
|
65
|
+
|
66
|
+
# Test each editable
|
67
|
+
expected_data = {}
|
68
|
+
editors.each_with_index do |editor, index|
|
69
|
+
editor.click
|
70
|
+
editor.send_keys([[:control, 'a'], :backspace])
|
71
|
+
content = "Content for #{editables[index]}"
|
72
|
+
editor.send_keys(content)
|
73
|
+
expected_data[editables[index]] = "<p>#{content}</p>"
|
74
|
+
end
|
75
|
+
|
76
|
+
# Wait for change events and verify the last one
|
77
|
+
eventually do
|
78
|
+
events = page.evaluate_script('window._editorEvents')
|
79
|
+
last_event = events.last
|
80
|
+
|
81
|
+
expect(last_event['data']).to eq(expected_data)
|
82
|
+
expect(last_event['hasEditor']).to be true
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe 'Classic Editor' do
|
88
|
+
it_behaves_like 'an editor', 'classic'
|
89
|
+
it_behaves_like 'an editor that fires change event with main payload', 'classic'
|
90
|
+
end
|
91
|
+
|
92
|
+
describe 'Decoupled Editor' do
|
93
|
+
before { visit 'decoupled' }
|
94
|
+
|
95
|
+
it_behaves_like 'an editor', 'decoupled'
|
96
|
+
it_behaves_like 'an editor that fires change event with main payload', 'decoupled'
|
97
|
+
|
98
|
+
it 'has separate toolbar' do
|
99
|
+
expect(page).to have_css('.toolbar-container .ck-toolbar')
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe 'Balloon Editor' do
|
104
|
+
before { visit 'balloon' }
|
105
|
+
|
106
|
+
it_behaves_like 'an editor', 'balloon'
|
107
|
+
it_behaves_like 'an editor that fires change event with main payload', 'balloon'
|
108
|
+
|
109
|
+
it 'shows balloon toolbar on selection' do
|
110
|
+
editor = first('.ck-editor__editable')
|
111
|
+
editor.click
|
112
|
+
|
113
|
+
expect(page).to have_css('.ck-balloon-panel', wait: 5)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
describe 'Inline Editor' do
|
118
|
+
it_behaves_like 'an editor', 'inline'
|
119
|
+
it_behaves_like 'an editor that fires change event with main payload', 'inline'
|
120
|
+
end
|
121
|
+
|
122
|
+
describe 'Multiroot Editor' do
|
123
|
+
before { visit 'multiroot' }
|
124
|
+
|
125
|
+
it_behaves_like 'an editor', 'multiroot'
|
126
|
+
it_behaves_like 'a multiroot editor that fires change events', 'multiroot', %w[toolbar content]
|
127
|
+
|
128
|
+
it 'supports multiple editable areas' do
|
129
|
+
expect(page).to have_css('.ck-editor__editable', minimum: 2)
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'shares toolbar between editables' do
|
133
|
+
expect(page).to have_css('.ck-toolbar', count: 1)
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'handles dynamically added editables' do # rubocop:disable Metrics/BlockLength
|
137
|
+
# Set up event listener
|
138
|
+
page.execute_script(<<~JS)
|
139
|
+
window._newEditableEvents = [];
|
140
|
+
document.querySelector('ckeditor-component').addEventListener('editor-change', (e) => {
|
141
|
+
window._newEditableEvents.push({
|
142
|
+
data: e.detail.data,
|
143
|
+
hasEditor: !!e.detail.editor
|
144
|
+
});
|
145
|
+
});
|
146
|
+
JS
|
147
|
+
|
148
|
+
# Add new editable component
|
149
|
+
page.execute_script(<<~JS)
|
150
|
+
const container = document.querySelector('[data-testid="multiroot-editor"]');
|
151
|
+
const newEditable = document.createElement('ckeditor-editable-component');
|
152
|
+
newEditable.setAttribute('name', 'new-root');
|
153
|
+
container.appendChild(newEditable);
|
154
|
+
JS
|
155
|
+
|
156
|
+
sleep 0.1 # Wait for component initialization
|
157
|
+
|
158
|
+
# Find and interact with new editable
|
159
|
+
new_editable = find("[name='new-root']")
|
160
|
+
new_editable.click
|
161
|
+
new_editable.send_keys('Content for new root')
|
162
|
+
|
163
|
+
# Verify the change event
|
164
|
+
eventually do
|
165
|
+
events = page.evaluate_script('window._newEditableEvents')
|
166
|
+
last_event = events.last
|
167
|
+
|
168
|
+
expect(last_event['data']).to include(
|
169
|
+
'content' => '',
|
170
|
+
'new-root' => '<p>Content for new root</p>',
|
171
|
+
'toolbar' => '<p>This is a toolbar editable</p>'
|
172
|
+
)
|
173
|
+
|
174
|
+
expect(last_event['hasEditor']).to be true
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|