actiontext 7.2.2.1 → 8.1.2
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/CHANGELOG.md +44 -56
- data/app/assets/javascripts/actiontext.esm.js +114 -14
- data/app/assets/javascripts/actiontext.js +120 -17
- data/app/helpers/action_text/tag_helper.rb +30 -14
- data/app/javascript/actiontext/attachment_upload.js +78 -12
- data/app/javascript/actiontext/index.js +10 -1
- data/app/models/action_text/rich_text.rb +5 -2
- data/lib/action_text/attribute.rb +25 -5
- data/lib/action_text/content.rb +4 -3
- data/lib/action_text/engine.rb +5 -2
- data/lib/action_text/fixture_set.rb +1 -1
- data/lib/action_text/gem_version.rb +3 -3
- data/lib/action_text/plain_text_conversion.rb +63 -28
- data/lib/action_text/rendering.rb +0 -1
- data/lib/action_text/system_test_helper.rb +37 -21
- data/lib/generators/action_text/install/install_generator.rb +0 -24
- data/lib/generators/action_text/install/templates/actiontext.css +414 -5
- data/package.json +3 -2
- metadata +29 -20
- data/app/assets/javascripts/trix.js +0 -13720
- data/app/assets/stylesheets/trix.css +0 -412
|
@@ -1,32 +1,86 @@
|
|
|
1
|
-
import { DirectUpload } from "@rails/activestorage"
|
|
1
|
+
import { DirectUpload, dispatchEvent } from "@rails/activestorage"
|
|
2
2
|
|
|
3
3
|
export class AttachmentUpload {
|
|
4
|
-
constructor(attachment, element) {
|
|
4
|
+
constructor(attachment, element, file = attachment.file) {
|
|
5
5
|
this.attachment = attachment
|
|
6
6
|
this.element = element
|
|
7
|
-
this.directUpload = new DirectUpload(
|
|
7
|
+
this.directUpload = new DirectUpload(file, this.directUploadUrl, this)
|
|
8
|
+
this.file = file
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
start() {
|
|
11
|
-
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
this.directUpload.create((error, attributes) => this.directUploadDidComplete(error, attributes, resolve, reject))
|
|
14
|
+
this.dispatch("start")
|
|
15
|
+
})
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
directUploadWillStoreFileWithXHR(xhr) {
|
|
15
19
|
xhr.upload.addEventListener("progress", event => {
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
// Scale upload progress to 0-90% range
|
|
21
|
+
const progress = (event.loaded / event.total) * 90
|
|
22
|
+
if (progress) {
|
|
23
|
+
this.dispatch("progress", { progress: progress })
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Start simulating progress after upload completes
|
|
28
|
+
xhr.upload.addEventListener("loadend", () => {
|
|
29
|
+
this.simulateResponseProgress(xhr)
|
|
18
30
|
})
|
|
19
31
|
}
|
|
20
32
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
simulateResponseProgress(xhr) {
|
|
34
|
+
let progress = 90
|
|
35
|
+
const startTime = Date.now()
|
|
36
|
+
|
|
37
|
+
const updateProgress = () => {
|
|
38
|
+
// Simulate progress from 90% to 99% over estimated time
|
|
39
|
+
const elapsed = Date.now() - startTime
|
|
40
|
+
const estimatedResponseTime = this.estimateResponseTime()
|
|
41
|
+
const responseProgress = Math.min(elapsed / estimatedResponseTime, 1)
|
|
42
|
+
progress = 90 + (responseProgress * 9) // 90% to 99%
|
|
43
|
+
|
|
44
|
+
this.dispatch("progress", { progress })
|
|
45
|
+
|
|
46
|
+
// Continue until response arrives or we hit 99%
|
|
47
|
+
if (xhr.readyState !== XMLHttpRequest.DONE && progress < 99) {
|
|
48
|
+
requestAnimationFrame(updateProgress)
|
|
49
|
+
}
|
|
24
50
|
}
|
|
25
51
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
52
|
+
// Stop simulation when response arrives
|
|
53
|
+
xhr.addEventListener("loadend", () => {
|
|
54
|
+
this.dispatch("progress", { progress: 100 })
|
|
29
55
|
})
|
|
56
|
+
|
|
57
|
+
requestAnimationFrame(updateProgress)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
estimateResponseTime() {
|
|
61
|
+
// Base estimate: 1 second for small files, scaling up for larger files
|
|
62
|
+
const fileSize = this.file.size
|
|
63
|
+
const MB = 1024 * 1024
|
|
64
|
+
|
|
65
|
+
if (fileSize < MB) {
|
|
66
|
+
return 1000 // 1 second for files under 1MB
|
|
67
|
+
} else if (fileSize < 10 * MB) {
|
|
68
|
+
return 2000 // 2 seconds for files 1-10MB
|
|
69
|
+
} else {
|
|
70
|
+
return 3000 + (fileSize / MB * 50) // 3+ seconds for larger files
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
directUploadDidComplete(error, attributes, resolve, reject) {
|
|
75
|
+
if (error) {
|
|
76
|
+
this.dispatchError(error, reject)
|
|
77
|
+
} else {
|
|
78
|
+
resolve({
|
|
79
|
+
sgid: attributes.attachable_sgid,
|
|
80
|
+
url: this.createBlobUrl(attributes.signed_id, attributes.filename)
|
|
81
|
+
})
|
|
82
|
+
this.dispatch("end")
|
|
83
|
+
}
|
|
30
84
|
}
|
|
31
85
|
|
|
32
86
|
createBlobUrl(signedId, filename) {
|
|
@@ -35,6 +89,18 @@ export class AttachmentUpload {
|
|
|
35
89
|
.replace(":filename", encodeURIComponent(filename))
|
|
36
90
|
}
|
|
37
91
|
|
|
92
|
+
dispatch(name, detail = {}) {
|
|
93
|
+
detail.attachment = this.attachment
|
|
94
|
+
return dispatchEvent(this.element, `direct-upload:${name}`, { detail })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
dispatchError(error, reject) {
|
|
98
|
+
const event = this.dispatch("error", { error })
|
|
99
|
+
if (!event.defaultPrevented) {
|
|
100
|
+
reject(error)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
38
104
|
get directUploadUrl() {
|
|
39
105
|
return this.element.dataset.directUploadUrl
|
|
40
106
|
}
|
|
@@ -4,7 +4,16 @@ addEventListener("trix-attachment-add", event => {
|
|
|
4
4
|
const { attachment, target } = event
|
|
5
5
|
|
|
6
6
|
if (attachment.file) {
|
|
7
|
-
const upload = new AttachmentUpload(attachment, target)
|
|
7
|
+
const upload = new AttachmentUpload(attachment, target, attachment.file)
|
|
8
|
+
const onProgress = event => attachment.setUploadProgress(event.detail.progress)
|
|
9
|
+
|
|
10
|
+
target.addEventListener("direct-upload:progress", onProgress)
|
|
11
|
+
|
|
8
12
|
upload.start()
|
|
13
|
+
.then(attributes => attachment.setAttributes(attributes))
|
|
14
|
+
.catch(error => alert(error))
|
|
15
|
+
.finally(() => target.removeEventListener("direct-upload:progress", onProgress))
|
|
9
16
|
}
|
|
10
17
|
})
|
|
18
|
+
|
|
19
|
+
export { AttachmentUpload }
|
|
@@ -48,10 +48,13 @@ module ActionText
|
|
|
48
48
|
##
|
|
49
49
|
# :method: embeds
|
|
50
50
|
#
|
|
51
|
-
# Returns the
|
|
51
|
+
# Returns the ActiveStorage::Attachment records from the embedded files.
|
|
52
|
+
#
|
|
53
|
+
# Attached ActiveStorage::Blob records are extracted from the `body`
|
|
54
|
+
# in a {before_validation}[rdoc-ref:ActiveModel::Validations::Callbacks::ClassMethods#before_validation] callback.
|
|
52
55
|
has_many_attached :embeds
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
before_validation do
|
|
55
58
|
self.embeds = body.attachables.grep(ActiveStorage::Blob).uniq if body.present?
|
|
56
59
|
end
|
|
57
60
|
|
|
@@ -41,13 +41,16 @@ module ActionText
|
|
|
41
41
|
# `strict_loading:` will be set to the value of the
|
|
42
42
|
# `strict_loading_by_default` class attribute (false by default).
|
|
43
43
|
#
|
|
44
|
+
# * `:store_if_blank` - Pass false to not create RichText records with empty values,
|
|
45
|
+
# if a blank value is provided. Default: true.
|
|
46
|
+
#
|
|
44
47
|
#
|
|
45
48
|
# Note: Action Text relies on polymorphic associations, which in turn store
|
|
46
49
|
# class names in the database. When renaming classes that use `has_rich_text`,
|
|
47
50
|
# make sure to also update the class names in the
|
|
48
51
|
# `action_text_rich_texts.record_type` polymorphic type column of the
|
|
49
52
|
# corresponding rows.
|
|
50
|
-
def has_rich_text(name, encrypted: false, strict_loading: strict_loading_by_default)
|
|
53
|
+
def has_rich_text(name, encrypted: false, strict_loading: strict_loading_by_default, store_if_blank: true)
|
|
51
54
|
class_eval <<-CODE, __FILE__, __LINE__ + 1
|
|
52
55
|
def #{name}
|
|
53
56
|
rich_text_#{name} || build_rich_text_#{name}
|
|
@@ -56,12 +59,29 @@ module ActionText
|
|
|
56
59
|
def #{name}?
|
|
57
60
|
rich_text_#{name}.present?
|
|
58
61
|
end
|
|
59
|
-
|
|
60
|
-
def #{name}=(body)
|
|
61
|
-
self.#{name}.body = body
|
|
62
|
-
end
|
|
63
62
|
CODE
|
|
64
63
|
|
|
64
|
+
if store_if_blank
|
|
65
|
+
class_eval <<-CODE, __FILE__, __LINE__ + 1
|
|
66
|
+
def #{name}=(body)
|
|
67
|
+
self.#{name}.body = body
|
|
68
|
+
end
|
|
69
|
+
CODE
|
|
70
|
+
else
|
|
71
|
+
class_eval <<-CODE, __FILE__, __LINE__ + 1
|
|
72
|
+
def #{name}=(body)
|
|
73
|
+
if body.present?
|
|
74
|
+
self.#{name}.body = body
|
|
75
|
+
else
|
|
76
|
+
if #{name}?
|
|
77
|
+
self.#{name}.body = body
|
|
78
|
+
self.#{name}.mark_for_destruction
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
CODE
|
|
83
|
+
end
|
|
84
|
+
|
|
65
85
|
rich_text_class_name = encrypted ? "ActionText::EncryptedRichText" : "ActionText::RichText"
|
|
66
86
|
has_one :"rich_text_#{name}", -> { where(name: name) },
|
|
67
87
|
class_name: rich_text_class_name, as: :record, inverse_of: :record, autosave: true, dependent: :destroy,
|
data/lib/action_text/content.rb
CHANGED
|
@@ -56,7 +56,7 @@ module ActionText
|
|
|
56
56
|
@links ||= fragment.find_all("a[href]").map { |a| a["href"] }.uniq
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
# Extracts
|
|
59
|
+
# Extracts ActionText::Attachment objects from the HTML fragment:
|
|
60
60
|
#
|
|
61
61
|
# attachable = ActiveStorage::Blob.first
|
|
62
62
|
# html = %Q(<action-text-attachment sgid="#{attachable.attachable_sgid}" caption="Captioned"></action-text-attachment>)
|
|
@@ -78,7 +78,7 @@ module ActionText
|
|
|
78
78
|
@gallery_attachments ||= attachment_galleries.flat_map(&:attachments)
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
-
# Extracts
|
|
81
|
+
# Extracts ActionText::Attachable objects from the HTML fragment:
|
|
82
82
|
#
|
|
83
83
|
# attachable = ActiveStorage::Blob.first
|
|
84
84
|
# html = %Q(<action-text-attachment sgid="#{attachable.attachable_sgid}" caption="Captioned"></action-text-attachment>)
|
|
@@ -123,10 +123,11 @@ module ActionText
|
|
|
123
123
|
# content.to_plain_text # => "safeunsafe"
|
|
124
124
|
#
|
|
125
125
|
# NOTE: that the returned string is not HTML safe and should not be rendered in
|
|
126
|
-
# browsers.
|
|
126
|
+
# browsers without additional sanitization.
|
|
127
127
|
#
|
|
128
128
|
# content = ActionText::Content.new("<script>alert()</script>")
|
|
129
129
|
# content.to_plain_text # => "<script>alert()</script>"
|
|
130
|
+
# ActionText::ContentHelper.sanitizer.sanitize(content.to_plain_text) # => ""
|
|
130
131
|
def to_plain_text
|
|
131
132
|
render_attachments(with_full_attributes: false, &:to_plain_text).fragment.to_plain_text
|
|
132
133
|
end
|
data/lib/action_text/engine.rb
CHANGED
|
@@ -8,6 +8,7 @@ require "active_record/railtie"
|
|
|
8
8
|
require "active_storage/engine"
|
|
9
9
|
|
|
10
10
|
require "action_text"
|
|
11
|
+
require "action_text/trix"
|
|
11
12
|
|
|
12
13
|
module ActionText
|
|
13
14
|
class Engine < Rails::Engine
|
|
@@ -34,7 +35,7 @@ module ActionText
|
|
|
34
35
|
|
|
35
36
|
initializer "action_text.asset" do
|
|
36
37
|
if Rails.application.config.respond_to?(:assets)
|
|
37
|
-
Rails.application.config.assets.precompile += %w( actiontext.js actiontext.esm.js
|
|
38
|
+
Rails.application.config.assets.precompile += %w( actiontext.js actiontext.esm.js )
|
|
38
39
|
end
|
|
39
40
|
end
|
|
40
41
|
|
|
@@ -87,7 +88,9 @@ module ActionText
|
|
|
87
88
|
|
|
88
89
|
config.after_initialize do |app|
|
|
89
90
|
if klass = app.config.action_text.sanitizer_vendor
|
|
90
|
-
|
|
91
|
+
ActiveSupport.on_load(:action_view) do
|
|
92
|
+
ActionText::ContentHelper.sanitizer = klass.safe_list_sanitizer.new
|
|
93
|
+
end
|
|
91
94
|
end
|
|
92
95
|
end
|
|
93
96
|
end
|
|
@@ -62,7 +62,7 @@ module ActionText
|
|
|
62
62
|
signed_global_id = ActiveRecord::FixtureSet.signed_global_id fixture_set_name, label,
|
|
63
63
|
column_type: column_type, for: ActionText::Attachable::LOCATOR_NAME
|
|
64
64
|
|
|
65
|
-
%(
|
|
65
|
+
%(<#{Attachment.tag_name} sgid="#{signed_global_id}"></#{Attachment.tag_name}>)
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
end
|
|
@@ -7,64 +7,70 @@ module ActionText
|
|
|
7
7
|
extend self
|
|
8
8
|
|
|
9
9
|
def node_to_plain_text(node)
|
|
10
|
-
|
|
10
|
+
BottomUpReducer.new(node).reduce do |n, child_values|
|
|
11
|
+
plain_text_for_node(n, child_values)
|
|
12
|
+
end.then(&method(:remove_trailing_newlines))
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
private
|
|
14
|
-
def plain_text_for_node(node,
|
|
16
|
+
def plain_text_for_node(node, child_values)
|
|
15
17
|
if respond_to?(plain_text_method_for_node(node), true)
|
|
16
|
-
send(plain_text_method_for_node(node), node,
|
|
18
|
+
send(plain_text_method_for_node(node), node, child_values)
|
|
17
19
|
else
|
|
18
|
-
|
|
20
|
+
plain_text_for_child_values(child_values)
|
|
19
21
|
end
|
|
20
22
|
end
|
|
21
23
|
|
|
22
|
-
def plain_text_for_node_children(node)
|
|
23
|
-
texts = []
|
|
24
|
-
node.children.each_with_index do |child, index|
|
|
25
|
-
texts << plain_text_for_node(child, index)
|
|
26
|
-
end
|
|
27
|
-
texts.join
|
|
28
|
-
end
|
|
29
|
-
|
|
30
24
|
def plain_text_method_for_node(node)
|
|
31
25
|
:"plain_text_for_#{node.name}_node"
|
|
32
26
|
end
|
|
33
27
|
|
|
34
|
-
def
|
|
35
|
-
|
|
28
|
+
def plain_text_for_child_values(child_values)
|
|
29
|
+
child_values.join
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def plain_text_for_unsupported_node(node, _child_values)
|
|
33
|
+
""
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
%i[ script style].each do |element|
|
|
37
|
+
alias_method :"plain_text_for_#{element}_node", :plain_text_for_unsupported_node
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def plain_text_for_block(node, child_values)
|
|
41
|
+
"#{remove_trailing_newlines(plain_text_for_child_values(child_values))}\n\n"
|
|
36
42
|
end
|
|
37
43
|
|
|
38
44
|
%i[ h1 p ].each do |element|
|
|
39
45
|
alias_method :"plain_text_for_#{element}_node", :plain_text_for_block
|
|
40
46
|
end
|
|
41
47
|
|
|
42
|
-
def plain_text_for_list(node,
|
|
43
|
-
"#{break_if_nested_list(node, plain_text_for_block(node))}"
|
|
48
|
+
def plain_text_for_list(node, child_values)
|
|
49
|
+
"#{break_if_nested_list(node, plain_text_for_block(node, child_values))}"
|
|
44
50
|
end
|
|
45
51
|
|
|
46
52
|
%i[ ul ol ].each do |element|
|
|
47
53
|
alias_method :"plain_text_for_#{element}_node", :plain_text_for_list
|
|
48
54
|
end
|
|
49
55
|
|
|
50
|
-
def plain_text_for_br_node(node,
|
|
56
|
+
def plain_text_for_br_node(node, _child_values)
|
|
51
57
|
"\n"
|
|
52
58
|
end
|
|
53
59
|
|
|
54
|
-
def plain_text_for_text_node(node,
|
|
60
|
+
def plain_text_for_text_node(node, _child_values)
|
|
55
61
|
remove_trailing_newlines(node.text)
|
|
56
62
|
end
|
|
57
63
|
|
|
58
|
-
def plain_text_for_div_node(node,
|
|
59
|
-
"#{remove_trailing_newlines(
|
|
64
|
+
def plain_text_for_div_node(node, child_values)
|
|
65
|
+
"#{remove_trailing_newlines(plain_text_for_child_values(child_values))}\n"
|
|
60
66
|
end
|
|
61
67
|
|
|
62
|
-
def plain_text_for_figcaption_node(node,
|
|
63
|
-
"[#{remove_trailing_newlines(
|
|
68
|
+
def plain_text_for_figcaption_node(node, child_values)
|
|
69
|
+
"[#{remove_trailing_newlines(plain_text_for_child_values(child_values))}]"
|
|
64
70
|
end
|
|
65
71
|
|
|
66
|
-
def plain_text_for_blockquote_node(node,
|
|
67
|
-
text = plain_text_for_block(node)
|
|
72
|
+
def plain_text_for_blockquote_node(node, child_values)
|
|
73
|
+
text = plain_text_for_block(node, child_values)
|
|
68
74
|
return "“”" if text.blank?
|
|
69
75
|
|
|
70
76
|
text = text.dup
|
|
@@ -73,9 +79,9 @@ module ActionText
|
|
|
73
79
|
text
|
|
74
80
|
end
|
|
75
81
|
|
|
76
|
-
def plain_text_for_li_node(node,
|
|
77
|
-
bullet = bullet_for_li_node(node
|
|
78
|
-
text = remove_trailing_newlines(
|
|
82
|
+
def plain_text_for_li_node(node, child_values)
|
|
83
|
+
bullet = bullet_for_li_node(node)
|
|
84
|
+
text = remove_trailing_newlines(plain_text_for_child_values(child_values))
|
|
79
85
|
indentation = indentation_for_li_node(node)
|
|
80
86
|
|
|
81
87
|
"#{indentation}#{bullet} #{text}\n"
|
|
@@ -85,8 +91,9 @@ module ActionText
|
|
|
85
91
|
text.chomp("")
|
|
86
92
|
end
|
|
87
93
|
|
|
88
|
-
def bullet_for_li_node(node
|
|
94
|
+
def bullet_for_li_node(node)
|
|
89
95
|
if list_node_name_for_li_node(node) == "ol"
|
|
96
|
+
index = node.parent.elements.index(node)
|
|
90
97
|
"#{index + 1}."
|
|
91
98
|
else
|
|
92
99
|
"•"
|
|
@@ -115,5 +122,33 @@ module ActionText
|
|
|
115
122
|
text
|
|
116
123
|
end
|
|
117
124
|
end
|
|
125
|
+
|
|
126
|
+
class BottomUpReducer # :nodoc:
|
|
127
|
+
def initialize(node)
|
|
128
|
+
@node = node
|
|
129
|
+
@values = {}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def reduce(&block)
|
|
133
|
+
traverse_bottom_up(@node) do |n|
|
|
134
|
+
child_values = @values.values_at(*n.children)
|
|
135
|
+
@values[n] = block.call(n, child_values)
|
|
136
|
+
end
|
|
137
|
+
@values[@node]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
def traverse_bottom_up(node, &block)
|
|
142
|
+
call_stack, processing_stack = [ node ], []
|
|
143
|
+
|
|
144
|
+
until call_stack.empty?
|
|
145
|
+
node = call_stack.pop
|
|
146
|
+
processing_stack.push(node)
|
|
147
|
+
call_stack.concat node.children
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
processing_stack.reverse_each(&block)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
118
153
|
end
|
|
119
154
|
end
|
|
@@ -7,52 +7,68 @@ module ActionText
|
|
|
7
7
|
# Locates a Trix editor and fills it in with the given HTML.
|
|
8
8
|
#
|
|
9
9
|
# The editor can be found by:
|
|
10
|
+
#
|
|
10
11
|
# * its `id`
|
|
11
12
|
# * its `placeholder`
|
|
12
13
|
# * the text from its `label` element
|
|
13
14
|
# * its `aria-label`
|
|
14
15
|
# * the `name` of its input
|
|
15
16
|
#
|
|
17
|
+
# Additional options are forwarded to Capybara as filters
|
|
16
18
|
#
|
|
17
19
|
# Examples:
|
|
18
20
|
#
|
|
19
21
|
# # <trix-editor id="message_content" ...></trix-editor>
|
|
20
|
-
#
|
|
22
|
+
# fill_in_rich_textarea "message_content", with: "Hello <em>world!</em>"
|
|
21
23
|
#
|
|
22
24
|
# # <trix-editor placeholder="Your message here" ...></trix-editor>
|
|
23
|
-
#
|
|
25
|
+
# fill_in_rich_textarea "Your message here", with: "Hello <em>world!</em>"
|
|
24
26
|
#
|
|
25
27
|
# # <label for="message_content">Message content</label>
|
|
26
28
|
# # <trix-editor id="message_content" ...></trix-editor>
|
|
27
|
-
#
|
|
29
|
+
# fill_in_rich_textarea "Message content", with: "Hello <em>world!</em>"
|
|
28
30
|
#
|
|
29
31
|
# # <trix-editor aria-label="Message content" ...></trix-editor>
|
|
30
|
-
#
|
|
32
|
+
# fill_in_rich_textarea "Message content", with: "Hello <em>world!</em>"
|
|
31
33
|
#
|
|
32
34
|
# # <input id="trix_input_1" name="message[content]" type="hidden">
|
|
33
35
|
# # <trix-editor input="trix_input_1"></trix-editor>
|
|
34
|
-
#
|
|
35
|
-
def
|
|
36
|
-
find(:
|
|
36
|
+
# fill_in_rich_textarea "message[content]", with: "Hello <em>world!</em>"
|
|
37
|
+
def fill_in_rich_textarea(locator = nil, with:, **)
|
|
38
|
+
find(:rich_textarea, locator, **).execute_script(<<~JS, with.to_s)
|
|
39
|
+
if ("value" in this) {
|
|
40
|
+
this.value = arguments[0]
|
|
41
|
+
} else {
|
|
42
|
+
this.editor.loadHTML(arguments[0])
|
|
43
|
+
}
|
|
44
|
+
JS
|
|
37
45
|
end
|
|
46
|
+
alias_method :fill_in_rich_text_area, :fill_in_rich_textarea
|
|
38
47
|
end
|
|
39
48
|
end
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
XPath.descendant
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
%i[rich_textarea rich_text_area].each do |rich_textarea|
|
|
51
|
+
Capybara.add_selector rich_textarea do
|
|
52
|
+
label "rich-text area"
|
|
53
|
+
xpath do |locator|
|
|
54
|
+
xpath = XPath.descendant[[
|
|
55
|
+
XPath.attribute(:role) == "textbox",
|
|
56
|
+
(XPath.attribute(:contenteditable) == "") | (XPath.attribute(:contenteditable) == "true")
|
|
57
|
+
].reduce(:&)]
|
|
58
|
+
|
|
59
|
+
if locator.nil?
|
|
60
|
+
xpath
|
|
61
|
+
else
|
|
62
|
+
input_located_by_name = XPath.anywhere(:input).where(XPath.attr(:name) == locator).attr(:id)
|
|
63
|
+
input_located_by_label = XPath.anywhere(:label).where(XPath.string.n.is(locator)).attr(:for)
|
|
49
64
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
xpath.where \
|
|
66
|
+
XPath.attr(:id).equals(locator) |
|
|
67
|
+
XPath.attr(:placeholder).equals(locator) |
|
|
68
|
+
XPath.attr(:"aria-label").equals(locator) |
|
|
69
|
+
XPath.attr(:input).equals(input_located_by_name) |
|
|
70
|
+
XPath.attr(:id).equals(input_located_by_label)
|
|
71
|
+
end
|
|
56
72
|
end
|
|
57
73
|
end
|
|
58
74
|
end
|
|
@@ -36,20 +36,8 @@ module ActionText
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def create_actiontext_files
|
|
39
|
-
destination = Pathname(destination_root)
|
|
40
|
-
|
|
41
39
|
template "actiontext.css", "app/assets/stylesheets/actiontext.css"
|
|
42
40
|
|
|
43
|
-
unless destination.join("app/assets/application.css").exist?
|
|
44
|
-
if (stylesheets = Dir.glob "#{destination_root}/app/assets/stylesheets/application.*.{scss,css}").length > 0
|
|
45
|
-
insert_into_file stylesheets.first.to_s, %(@import 'actiontext.css';)
|
|
46
|
-
else
|
|
47
|
-
say <<~INSTRUCTIONS, :green
|
|
48
|
-
To use the Trix editor, you must require 'app/assets/stylesheets/actiontext.css' in your base stylesheet.
|
|
49
|
-
INSTRUCTIONS
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
41
|
gem_root = "#{__dir__}/../../../.."
|
|
54
42
|
|
|
55
43
|
copy_file "#{gem_root}/app/views/active_storage/blobs/_blob.html.erb",
|
|
@@ -59,18 +47,6 @@ module ActionText
|
|
|
59
47
|
"app/views/layouts/action_text/contents/_content.html.erb"
|
|
60
48
|
end
|
|
61
49
|
|
|
62
|
-
def enable_image_processing_gem
|
|
63
|
-
if (gemfile_path = Pathname(destination_root).join("Gemfile")).exist?
|
|
64
|
-
say "Ensure image_processing gem has been enabled so image uploads will work (remember to bundle!)"
|
|
65
|
-
image_processing_regex = /gem ["']image_processing["']/
|
|
66
|
-
if File.readlines(gemfile_path).grep(image_processing_regex).any?
|
|
67
|
-
uncomment_lines gemfile_path, image_processing_regex
|
|
68
|
-
else
|
|
69
|
-
run "bundle add --skip-install image_processing"
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
50
|
def create_migrations
|
|
75
51
|
rails_command "railties:install:migrations FROM=active_storage,action_text", inline: true
|
|
76
52
|
end
|