actiontext 6.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/MIT-LICENSE +21 -0
- data/README.md +9 -0
- data/app/helpers/action_text/content_helper.rb +30 -0
- data/app/helpers/action_text/tag_helper.rb +75 -0
- data/app/javascript/actiontext/attachment_upload.js +45 -0
- data/app/javascript/actiontext/index.js +10 -0
- data/app/models/action_text/rich_text.rb +29 -0
- data/app/views/action_text/attachables/_missing_attachable.html.erb +1 -0
- data/app/views/action_text/attachables/_remote_image.html.erb +8 -0
- data/app/views/action_text/attachment_galleries/_attachment_gallery.html.erb +3 -0
- data/app/views/action_text/content/_layout.html.erb +3 -0
- data/app/views/active_storage/blobs/_blob.html.erb +14 -0
- data/db/migrate/201805281641_create_action_text_tables.rb +14 -0
- data/lib/action_text.rb +37 -0
- data/lib/action_text/attachable.rb +82 -0
- data/lib/action_text/attachables/content_attachment.rb +38 -0
- data/lib/action_text/attachables/missing_attachable.rb +13 -0
- data/lib/action_text/attachables/remote_image.rb +46 -0
- data/lib/action_text/attachment.rb +103 -0
- data/lib/action_text/attachment_gallery.rb +65 -0
- data/lib/action_text/attachments/caching.rb +16 -0
- data/lib/action_text/attachments/minification.rb +17 -0
- data/lib/action_text/attachments/trix_conversion.rb +34 -0
- data/lib/action_text/attribute.rb +48 -0
- data/lib/action_text/content.rb +132 -0
- data/lib/action_text/engine.rb +50 -0
- data/lib/action_text/fragment.rb +57 -0
- data/lib/action_text/gem_version.rb +17 -0
- data/lib/action_text/html_conversion.rb +24 -0
- data/lib/action_text/plain_text_conversion.rb +81 -0
- data/lib/action_text/serialization.rb +34 -0
- data/lib/action_text/trix_attachment.rb +92 -0
- data/lib/action_text/version.rb +10 -0
- data/lib/tasks/actiontext.rake +20 -0
- data/lib/templates/actiontext.scss +36 -0
- data/lib/templates/fixtures.yml +4 -0
- data/lib/templates/installer.rb +32 -0
- data/package.json +29 -0
- metadata +158 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9eb946145656ee7d27e4ee9ce733a3e0f55e1b63ab16e4cea8741fecaac697f6
|
4
|
+
data.tar.gz: 658a5373916fd11abfdbd569746f1f4ed23fb71cc52294d513d063de47a148ab
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8418aebfa611943071b773ec51544e039f4f3e8220a594c4a66bfced904754da4f2ee1fc7d0472e7d6e0e207f4aad70f8828509fd0f6a95df5db1557e42f34cb
|
7
|
+
data.tar.gz: 427541191977534c0d36bf0b4cfb0615a5525e0ef645a939b31e066dac689ec0288bc7b6dca919523c8adae6ffee56888bc0d0cb232cd592c51bd8f06daee357
|
data/CHANGELOG.md
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2019 Basecamp, LLC
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# Action Text
|
2
|
+
|
3
|
+
Action Text brings rich text content and editing to Rails. It includes the [Trix editor](https://trix-editor.org) that handles everything from formatting to links to quotes to lists to embedded images and galleries. The rich text content generated by the Trix editor is saved in its own RichText model that's associated with any existing Active Record model in the application. Any embedded images (or other attachments) are automatically stored using Active Storage and associated with the included RichText model.
|
4
|
+
|
5
|
+
You can read more about Action Text in the [Action Text Overview](https://edgeguides.rubyonrails.org/action_text_overview.html) guide.
|
6
|
+
|
7
|
+
## License
|
8
|
+
|
9
|
+
Action Text is released under the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionText
|
4
|
+
module ContentHelper
|
5
|
+
SANITIZER = Rails::Html::Sanitizer.white_list_sanitizer
|
6
|
+
ALLOWED_TAGS = SANITIZER.allowed_tags + [ ActionText::Attachment::TAG_NAME, "figure", "figcaption" ]
|
7
|
+
ALLOWED_ATTRIBUTES = SANITIZER.allowed_attributes + ActionText::Attachment::ATTRIBUTES
|
8
|
+
|
9
|
+
def render_action_text_content(content)
|
10
|
+
content = content.render_attachments do |attachment|
|
11
|
+
unless attachment.in?(content.gallery_attachments)
|
12
|
+
attachment.node.tap do |node|
|
13
|
+
node.inner_html = render(attachment, in_gallery: false).chomp
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
content = content.render_attachment_galleries do |attachment_gallery|
|
19
|
+
render(layout: attachment_gallery, object: attachment_gallery) do
|
20
|
+
attachment_gallery.attachments.map do |attachment|
|
21
|
+
attachment.node.inner_html = render(attachment, in_gallery: true).chomp
|
22
|
+
attachment.to_html
|
23
|
+
end.join("").html_safe
|
24
|
+
end.chomp
|
25
|
+
end
|
26
|
+
|
27
|
+
sanitize content.to_html, tags: ALLOWED_TAGS, attributes: ALLOWED_ATTRIBUTES
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionText
|
4
|
+
module TagHelper
|
5
|
+
cattr_accessor(:id, instance_accessor: false) { 0 }
|
6
|
+
|
7
|
+
# Returns a `trix-editor` tag that instantiates the Trix JavaScript editor as well as a hidden field
|
8
|
+
# that Trix will write to on changes, so the content will be sent on form submissions.
|
9
|
+
#
|
10
|
+
# ==== Options
|
11
|
+
# * <tt>:class</tt> - Defaults to "trix-content" which ensures default styling is applied.
|
12
|
+
#
|
13
|
+
# ==== Example
|
14
|
+
#
|
15
|
+
# rich_text_area_tag "content", message.content
|
16
|
+
# # <input type="hidden" name="content" id="trix_input_post_1">
|
17
|
+
# # <trix-editor id="content" input="trix_input_post_1" class="trix-content" ...></trix-editor>
|
18
|
+
def rich_text_area_tag(name, value = nil, options = {})
|
19
|
+
options = options.symbolize_keys
|
20
|
+
|
21
|
+
options[:input] ||= "trix_input_#{ActionText::TagHelper.id += 1}"
|
22
|
+
options[:class] ||= "trix-content"
|
23
|
+
|
24
|
+
options[:data] ||= {}
|
25
|
+
options[:data][:direct_upload_url] = main_app.rails_direct_uploads_url
|
26
|
+
options[:data][:blob_url_template] = main_app.rails_service_blob_url(":signed_id", ":filename")
|
27
|
+
|
28
|
+
editor_tag = content_tag("trix-editor", "", options)
|
29
|
+
input_tag = hidden_field_tag(name, value, id: options[:input])
|
30
|
+
|
31
|
+
input_tag + editor_tag
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module ActionView::Helpers
|
37
|
+
class Tags::ActionText < Tags::Base
|
38
|
+
delegate :dom_id, to: ActionView::RecordIdentifier
|
39
|
+
|
40
|
+
def render
|
41
|
+
options = @options.stringify_keys
|
42
|
+
add_default_name_and_id(options)
|
43
|
+
options["input"] ||= dom_id(object, [options["id"], :trix_input].compact.join("_")) if object
|
44
|
+
@template_object.rich_text_area_tag(options.delete("name"), editable_value, options)
|
45
|
+
end
|
46
|
+
|
47
|
+
def editable_value
|
48
|
+
value&.body.try(:to_trix_html)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
module FormHelper
|
53
|
+
# Returns a `trix-editor` tag that instantiates the Trix JavaScript editor as well as a hidden field
|
54
|
+
# that Trix will write to on changes, so the content will be sent on form submissions.
|
55
|
+
#
|
56
|
+
# ==== Options
|
57
|
+
# * <tt>:class</tt> - Defaults to "trix-content" which ensures default styling is applied.
|
58
|
+
#
|
59
|
+
# ==== Example
|
60
|
+
# form_with(model: @message) do |form|
|
61
|
+
# form.rich_text_area :content
|
62
|
+
# end
|
63
|
+
# # <input type="hidden" name="message[content]" id="message_content_trix_input_message_1">
|
64
|
+
# # <trix-editor id="content" input="message_content_trix_input_message_1" class="trix-content" ...></trix-editor>
|
65
|
+
def rich_text_area(object_name, method, options = {})
|
66
|
+
Tags::ActionText.new(object_name, method, self, options).render
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class FormBuilder
|
71
|
+
def rich_text_area(method, options = {})
|
72
|
+
@template.rich_text_area(@object_name, method, objectify_options(options))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
import { DirectUpload } from "@rails/activestorage"
|
2
|
+
|
3
|
+
export class AttachmentUpload {
|
4
|
+
constructor(attachment, element) {
|
5
|
+
this.attachment = attachment
|
6
|
+
this.element = element
|
7
|
+
this.directUpload = new DirectUpload(attachment.file, this.directUploadUrl, this)
|
8
|
+
}
|
9
|
+
|
10
|
+
start() {
|
11
|
+
this.directUpload.create(this.directUploadDidComplete.bind(this))
|
12
|
+
}
|
13
|
+
|
14
|
+
directUploadWillStoreFileWithXHR(xhr) {
|
15
|
+
xhr.upload.addEventListener("progress", event => {
|
16
|
+
const progress = event.loaded / event.total * 100
|
17
|
+
this.attachment.setUploadProgress(progress)
|
18
|
+
})
|
19
|
+
}
|
20
|
+
|
21
|
+
directUploadDidComplete(error, attributes) {
|
22
|
+
if (error) {
|
23
|
+
throw new Error(`Direct upload failed: ${error}`)
|
24
|
+
}
|
25
|
+
|
26
|
+
this.attachment.setAttributes({
|
27
|
+
sgid: attributes.attachable_sgid,
|
28
|
+
url: this.createBlobUrl(attributes.signed_id, attributes.filename)
|
29
|
+
})
|
30
|
+
}
|
31
|
+
|
32
|
+
createBlobUrl(signedId, filename) {
|
33
|
+
return this.blobUrlTemplate
|
34
|
+
.replace(":signed_id", signedId)
|
35
|
+
.replace(":filename", encodeURIComponent(filename))
|
36
|
+
}
|
37
|
+
|
38
|
+
get directUploadUrl() {
|
39
|
+
return this.element.dataset.directUploadUrl
|
40
|
+
}
|
41
|
+
|
42
|
+
get blobUrlTemplate() {
|
43
|
+
return this.element.dataset.blobUrlTemplate
|
44
|
+
}
|
45
|
+
}
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionText
|
4
|
+
# The RichText record holds the content produced by the Trix editor in a serialized `body` attribute.
|
5
|
+
# It also holds all the references to the embedded files, which are stored using Active Storage.
|
6
|
+
# This record is then associated with the Active Record model the application desires to have
|
7
|
+
# rich text content using the `has_rich_text` class method.
|
8
|
+
class RichText < ActiveRecord::Base
|
9
|
+
self.table_name = "action_text_rich_texts"
|
10
|
+
|
11
|
+
serialize :body, ActionText::Content
|
12
|
+
delegate :to_s, :nil?, to: :body
|
13
|
+
|
14
|
+
belongs_to :record, polymorphic: true, touch: true
|
15
|
+
has_many_attached :embeds
|
16
|
+
|
17
|
+
before_save do
|
18
|
+
self.embeds = body.attachments.map(&:attachable) if body.present?
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_plain_text
|
22
|
+
body&.to_plain_text.to_s
|
23
|
+
end
|
24
|
+
|
25
|
+
delegate :blank?, :empty?, :present?, to: :to_plain_text
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
ActiveSupport.run_load_hooks :action_text_rich_text, ActionText::RichText
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= "☒" -%>
|
@@ -0,0 +1,8 @@
|
|
1
|
+
<figure class="attachment attachment--preview">
|
2
|
+
<%= image_tag(remote_image.url, width: remote_image.width, height: remote_image.height) %>
|
3
|
+
<% if caption = remote_image.try(:caption) %>
|
4
|
+
<figcaption class="attachment__caption">
|
5
|
+
<%= caption %>
|
6
|
+
</figcaption>
|
7
|
+
<% end %>
|
8
|
+
</figure>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
|
2
|
+
<% if blob.representable? %>
|
3
|
+
<%= image_tag blob.representation(resize_to_fit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
|
4
|
+
<% end %>
|
5
|
+
|
6
|
+
<figcaption class="attachment__caption">
|
7
|
+
<% if caption = blob.try(:caption) %>
|
8
|
+
<%= caption %>
|
9
|
+
<% else %>
|
10
|
+
<span class="attachment__name"><%= blob.filename %></span>
|
11
|
+
<span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
|
12
|
+
<% end %>
|
13
|
+
</figcaption>
|
14
|
+
</figure>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class CreateActionTextTables < ActiveRecord::Migration[6.0]
|
2
|
+
def change
|
3
|
+
create_table :action_text_rich_texts do |t|
|
4
|
+
t.string :name, null: false
|
5
|
+
t.text :body, limit: 16777215
|
6
|
+
t.references :record, null: false, polymorphic: true, index: false
|
7
|
+
|
8
|
+
t.datetime :created_at, null: false
|
9
|
+
t.datetime :updated_at, null: false
|
10
|
+
|
11
|
+
t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/action_text.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support"
|
4
|
+
require "active_support/rails"
|
5
|
+
|
6
|
+
require "nokogiri"
|
7
|
+
|
8
|
+
module ActionText
|
9
|
+
extend ActiveSupport::Autoload
|
10
|
+
|
11
|
+
autoload :Attachable
|
12
|
+
autoload :AttachmentGallery
|
13
|
+
autoload :Attachment
|
14
|
+
autoload :Attribute
|
15
|
+
autoload :Content
|
16
|
+
autoload :Fragment
|
17
|
+
autoload :HtmlConversion
|
18
|
+
autoload :PlainTextConversion
|
19
|
+
autoload :Serialization
|
20
|
+
autoload :TrixAttachment
|
21
|
+
|
22
|
+
module Attachables
|
23
|
+
extend ActiveSupport::Autoload
|
24
|
+
|
25
|
+
autoload :ContentAttachment
|
26
|
+
autoload :MissingAttachable
|
27
|
+
autoload :RemoteImage
|
28
|
+
end
|
29
|
+
|
30
|
+
module Attachments
|
31
|
+
extend ActiveSupport::Autoload
|
32
|
+
|
33
|
+
autoload :Caching
|
34
|
+
autoload :Minification
|
35
|
+
autoload :TrixConversion
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionText
|
4
|
+
module Attachable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
LOCATOR_NAME = "attachable"
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def from_node(node)
|
11
|
+
if attachable = attachable_from_sgid(node["sgid"])
|
12
|
+
attachable
|
13
|
+
elsif attachable = ActionText::Attachables::ContentAttachment.from_node(node)
|
14
|
+
attachable
|
15
|
+
elsif attachable = ActionText::Attachables::RemoteImage.from_node(node)
|
16
|
+
attachable
|
17
|
+
else
|
18
|
+
ActionText::Attachables::MissingAttachable
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def from_attachable_sgid(sgid, options = {})
|
23
|
+
method = sgid.is_a?(Array) ? :locate_many_signed : :locate_signed
|
24
|
+
record = GlobalID::Locator.public_send(method, sgid, options.merge(for: LOCATOR_NAME))
|
25
|
+
record || raise(ActiveRecord::RecordNotFound)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def attachable_from_sgid(sgid)
|
30
|
+
from_attachable_sgid(sgid)
|
31
|
+
rescue ActiveRecord::RecordNotFound
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class_methods do
|
37
|
+
def from_attachable_sgid(sgid)
|
38
|
+
ActionText::Attachable.from_attachable_sgid(sgid, only: self)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def attachable_sgid
|
43
|
+
to_sgid(expires_in: nil, for: LOCATOR_NAME).to_s
|
44
|
+
end
|
45
|
+
|
46
|
+
def attachable_content_type
|
47
|
+
try(:content_type) || "application/octet-stream"
|
48
|
+
end
|
49
|
+
|
50
|
+
def attachable_filename
|
51
|
+
filename.to_s if respond_to?(:filename)
|
52
|
+
end
|
53
|
+
|
54
|
+
def attachable_filesize
|
55
|
+
try(:byte_size) || try(:filesize)
|
56
|
+
end
|
57
|
+
|
58
|
+
def attachable_metadata
|
59
|
+
try(:metadata) || {}
|
60
|
+
end
|
61
|
+
|
62
|
+
def previewable_attachable?
|
63
|
+
false
|
64
|
+
end
|
65
|
+
|
66
|
+
def as_json(*)
|
67
|
+
super.merge(attachable_sgid: attachable_sgid)
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_rich_text_attributes(attributes = {})
|
71
|
+
attributes.dup.tap do |attrs|
|
72
|
+
attrs[:sgid] = attachable_sgid
|
73
|
+
attrs[:content_type] = attachable_content_type
|
74
|
+
attrs[:previewable] = true if previewable_attachable?
|
75
|
+
attrs[:filename] = attachable_filename
|
76
|
+
attrs[:filesize] = attachable_filesize
|
77
|
+
attrs[:width] = attachable_metadata[:width]
|
78
|
+
attrs[:height] = attachable_metadata[:height]
|
79
|
+
end.compact
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionText
|
4
|
+
module Attachables
|
5
|
+
class ContentAttachment
|
6
|
+
include ActiveModel::Model
|
7
|
+
|
8
|
+
def self.from_node(node)
|
9
|
+
if node["content-type"]
|
10
|
+
if matches = node["content-type"].match(/vnd\.rubyonrails\.(.+)\.html/)
|
11
|
+
attachment = new(name: matches[1])
|
12
|
+
attachment if attachment.valid?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_accessor :name
|
18
|
+
validates_inclusion_of :name, in: %w( horizontal-rule )
|
19
|
+
|
20
|
+
def attachable_plain_text_representation(caption)
|
21
|
+
case name
|
22
|
+
when "horizontal-rule"
|
23
|
+
" ┄ "
|
24
|
+
else
|
25
|
+
" "
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_partial_path
|
30
|
+
"action_text/attachables/content_attachment"
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_trix_content_attachment_partial_path
|
34
|
+
"action_text/attachables/content_attachments/#{name.underscore}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|