actiontext 6.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.md +9 -0
  5. data/app/helpers/action_text/content_helper.rb +30 -0
  6. data/app/helpers/action_text/tag_helper.rb +75 -0
  7. data/app/javascript/actiontext/attachment_upload.js +45 -0
  8. data/app/javascript/actiontext/index.js +10 -0
  9. data/app/models/action_text/rich_text.rb +29 -0
  10. data/app/views/action_text/attachables/_missing_attachable.html.erb +1 -0
  11. data/app/views/action_text/attachables/_remote_image.html.erb +8 -0
  12. data/app/views/action_text/attachment_galleries/_attachment_gallery.html.erb +3 -0
  13. data/app/views/action_text/content/_layout.html.erb +3 -0
  14. data/app/views/active_storage/blobs/_blob.html.erb +14 -0
  15. data/db/migrate/201805281641_create_action_text_tables.rb +14 -0
  16. data/lib/action_text.rb +37 -0
  17. data/lib/action_text/attachable.rb +82 -0
  18. data/lib/action_text/attachables/content_attachment.rb +38 -0
  19. data/lib/action_text/attachables/missing_attachable.rb +13 -0
  20. data/lib/action_text/attachables/remote_image.rb +46 -0
  21. data/lib/action_text/attachment.rb +103 -0
  22. data/lib/action_text/attachment_gallery.rb +65 -0
  23. data/lib/action_text/attachments/caching.rb +16 -0
  24. data/lib/action_text/attachments/minification.rb +17 -0
  25. data/lib/action_text/attachments/trix_conversion.rb +34 -0
  26. data/lib/action_text/attribute.rb +48 -0
  27. data/lib/action_text/content.rb +132 -0
  28. data/lib/action_text/engine.rb +50 -0
  29. data/lib/action_text/fragment.rb +57 -0
  30. data/lib/action_text/gem_version.rb +17 -0
  31. data/lib/action_text/html_conversion.rb +24 -0
  32. data/lib/action_text/plain_text_conversion.rb +81 -0
  33. data/lib/action_text/serialization.rb +34 -0
  34. data/lib/action_text/trix_attachment.rb +92 -0
  35. data/lib/action_text/version.rb +10 -0
  36. data/lib/tasks/actiontext.rake +20 -0
  37. data/lib/templates/actiontext.scss +36 -0
  38. data/lib/templates/fixtures.yml +4 -0
  39. data/lib/templates/installer.rb +32 -0
  40. data/package.json +29 -0
  41. metadata +158 -0
@@ -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
@@ -0,0 +1,5 @@
1
+ ## Rails 6.0.0.beta1 (January 18, 2019) ##
2
+
3
+ * Added to Rails.
4
+
5
+ *DHH*
@@ -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.
@@ -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,10 @@
1
+ import { AttachmentUpload } from "./attachment_upload"
2
+
3
+ addEventListener("trix-attachment-add", event => {
4
+ const { attachment, target } = event
5
+
6
+ if (attachment.file) {
7
+ const upload = new AttachmentUpload(attachment, target)
8
+ upload.start()
9
+ }
10
+ })
@@ -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,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,3 @@
1
+ <div class="attachment-gallery attachment-gallery--<%= attachment_gallery.size %>">
2
+ <%= yield %>
3
+ </div>
@@ -0,0 +1,3 @@
1
+ <div class="trix-content">
2
+ <%= render_action_text_content(content) %>
3
+ </div>
@@ -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
@@ -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