aboutsummaryrefslogtreecommitdiffstats
path: root/actiontext/app
diff options
context:
space:
mode:
Diffstat (limited to 'actiontext/app')
-rw-r--r--actiontext/app/helpers/action_text/content_helper.rb30
-rw-r--r--actiontext/app/helpers/action_text/tag_helper.rb75
-rw-r--r--actiontext/app/javascript/actiontext/attachment_upload.js45
-rw-r--r--actiontext/app/javascript/actiontext/index.js11
-rw-r--r--actiontext/app/models/action_text/rich_text.rb25
-rw-r--r--actiontext/app/views/action_text/attachables/_missing_attachable.html.erb1
-rw-r--r--actiontext/app/views/action_text/attachables/_remote_image.html.erb8
-rw-r--r--actiontext/app/views/action_text/attachment_galleries/_attachment_gallery.html.erb3
-rw-r--r--actiontext/app/views/action_text/content/_layout.html.erb3
-rw-r--r--actiontext/app/views/active_storage/blobs/_blob.html.erb14
10 files changed, 215 insertions, 0 deletions
diff --git a/actiontext/app/helpers/action_text/content_helper.rb b/actiontext/app/helpers/action_text/content_helper.rb
new file mode 100644
index 0000000000..b3335601f9
--- /dev/null
+++ b/actiontext/app/helpers/action_text/content_helper.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module ActionText
+ module ContentHelper
+ SANITIZER = Rails::Html::Sanitizer.white_list_sanitizer
+ ALLOWED_TAGS = SANITIZER.allowed_tags + [ ActionText::Attachment::TAG_NAME, "figure", "figcaption" ]
+ ALLOWED_ATTRIBUTES = SANITIZER.allowed_attributes + ActionText::Attachment::ATTRIBUTES
+
+ def render_action_text_content(content)
+ content = content.render_attachments do |attachment|
+ unless attachment.in?(content.gallery_attachments)
+ attachment.node.tap do |node|
+ node.inner_html = render(attachment, in_gallery: false).chomp
+ end
+ end
+ end
+
+ content = content.render_attachment_galleries do |attachment_gallery|
+ render(layout: attachment_gallery, object: attachment_gallery) do
+ attachment_gallery.attachments.map do |attachment|
+ attachment.node.inner_html = render(attachment, in_gallery: true).chomp
+ attachment.to_html
+ end.join("").html_safe
+ end.chomp
+ end
+
+ sanitize content.to_html, tags: ALLOWED_TAGS, attributes: ALLOWED_ATTRIBUTES
+ end
+ end
+end
diff --git a/actiontext/app/helpers/action_text/tag_helper.rb b/actiontext/app/helpers/action_text/tag_helper.rb
new file mode 100644
index 0000000000..837b2264b1
--- /dev/null
+++ b/actiontext/app/helpers/action_text/tag_helper.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module ActionText
+ module TagHelper
+ cattr_accessor(:id, instance_accessor: false) { 0 }
+
+ # Returns a `trix-editor` tag that instantiates the Trix JavaScript editor as well as a hidden field
+ # that Trix will write to on changes, so the content will be sent on form submissions.
+ #
+ # ==== Options
+ # * <tt>:class</tt> - Defaults to "trix-content" which ensures default styling is applied.
+ #
+ # ==== Example
+ #
+ # rich_text_area_tag "content", message.content
+ # # <input type="hidden" name="content" id="trix_input_post_1">
+ # # <trix-editor id="content" input="trix_input_post_1" class="trix-content" ...></trix-editor>
+ def rich_text_area_tag(name, value = nil, options = {})
+ options = options.symbolize_keys
+
+ options[:input] ||= "trix_input_#{ActionText::TagHelper.id += 1}"
+ options[:class] ||= "trix-content"
+
+ options[:data] ||= {}
+ options[:data][:direct_upload_url] = main_app.rails_direct_uploads_url
+ options[:data][:blob_url_template] = main_app.rails_service_blob_url(":signed_id", ":filename")
+
+ editor_tag = content_tag("trix-editor", "", options)
+ input_tag = hidden_field_tag(name, value, id: options[:input])
+
+ input_tag + editor_tag
+ end
+ end
+end
+
+module ActionView::Helpers
+ class Tags::ActionText < Tags::Base
+ delegate :dom_id, to: ActionView::RecordIdentifier
+
+ def render
+ options = @options.stringify_keys
+ add_default_name_and_id(options)
+ options["input"] ||= dom_id(object, [options["id"], :trix_input].compact.join("_")) if object
+ @template_object.rich_text_area_tag(options.delete("name"), editable_value, options)
+ end
+
+ def editable_value
+ value&.body.try(:to_trix_html)
+ end
+ end
+
+ module FormHelper
+ # Returns a `trix-editor` tag that instantiates the Trix JavaScript editor as well as a hidden field
+ # that Trix will write to on changes, so the content will be sent on form submissions.
+ #
+ # ==== Options
+ # * <tt>:class</tt> - Defaults to "trix-content" which ensures default styling is applied.
+ #
+ # ==== Example
+ # form_with(model: @message) do |form|
+ # form.rich_text_area :content
+ # end
+ # # <input type="hidden" name="message[content]" id="message_content_trix_input_message_1">
+ # # <trix-editor id="content" input="message_content_trix_input_message_1" class="trix-content" ...></trix-editor>
+ def rich_text_area(object_name, method, options = {})
+ Tags::ActionText.new(object_name, method, self, options).render
+ end
+ end
+
+ class FormBuilder
+ def rich_text_area(method, options = {})
+ @template.rich_text_area(@object_name, method, objectify_options(options))
+ end
+ end
+end
diff --git a/actiontext/app/javascript/actiontext/attachment_upload.js b/actiontext/app/javascript/actiontext/attachment_upload.js
new file mode 100644
index 0000000000..a716f1f589
--- /dev/null
+++ b/actiontext/app/javascript/actiontext/attachment_upload.js
@@ -0,0 +1,45 @@
+import { DirectUpload } from "activestorage"
+
+export class AttachmentUpload {
+ constructor(attachment, element) {
+ this.attachment = attachment
+ this.element = element
+ this.directUpload = new DirectUpload(attachment.file, this.directUploadUrl, this)
+ }
+
+ start() {
+ this.directUpload.create(this.directUploadDidComplete.bind(this))
+ }
+
+ directUploadWillStoreFileWithXHR(xhr) {
+ xhr.upload.addEventListener("progress", event => {
+ const progress = event.loaded / event.total * 100
+ this.attachment.setUploadProgress(progress)
+ })
+ }
+
+ directUploadDidComplete(error, attributes) {
+ if (error) {
+ throw new Error(`Direct upload failed: ${error}`)
+ }
+
+ this.attachment.setAttributes({
+ sgid: attributes.attachable_sgid,
+ url: this.createBlobUrl(attributes.signed_id, attributes.filename)
+ })
+ }
+
+ createBlobUrl(signedId, filename) {
+ return this.blobUrlTemplate
+ .replace(":signed_id", signedId)
+ .replace(":filename", encodeURIComponent(filename))
+ }
+
+ get directUploadUrl() {
+ return this.element.dataset.directUploadUrl
+ }
+
+ get blobUrlTemplate() {
+ return this.element.dataset.blobUrlTemplate
+ }
+}
diff --git a/actiontext/app/javascript/actiontext/index.js b/actiontext/app/javascript/actiontext/index.js
new file mode 100644
index 0000000000..c149eda952
--- /dev/null
+++ b/actiontext/app/javascript/actiontext/index.js
@@ -0,0 +1,11 @@
+import * as Trix from "trix"
+import { AttachmentUpload } from "./attachment_upload"
+
+addEventListener("trix-attachment-add", event => {
+ const { attachment, target } = event
+
+ if (attachment.file) {
+ const upload = new AttachmentUpload(attachment, target)
+ upload.start()
+ }
+})
diff --git a/actiontext/app/models/action_text/rich_text.rb b/actiontext/app/models/action_text/rich_text.rb
new file mode 100644
index 0000000000..717e93599f
--- /dev/null
+++ b/actiontext/app/models/action_text/rich_text.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+# The RichText record holds the content produced by the Trix editor in a serialized `body` attribute.
+# It also holds all the references to the embedded files, which are stored using Active Storage.
+# This record is then associated with the Active Record model the application desires to have
+# rich text content using the `has_rich_text` class method.
+class ActionText::RichText < ActiveRecord::Base
+ self.table_name = "action_text_rich_texts"
+
+ serialize :body, ActionText::Content
+ delegate :to_s, :nil?, to: :body
+
+ belongs_to :record, polymorphic: true, touch: true
+ has_many_attached :embeds
+
+ before_save do
+ self.embeds = body.attachments.map(&:attachable) if body.present?
+ end
+
+ def to_plain_text
+ body&.to_plain_text.to_s
+ end
+
+ delegate :blank?, :empty?, :present?, to: :to_plain_text
+end
diff --git a/actiontext/app/views/action_text/attachables/_missing_attachable.html.erb b/actiontext/app/views/action_text/attachables/_missing_attachable.html.erb
new file mode 100644
index 0000000000..5ffd93b89e
--- /dev/null
+++ b/actiontext/app/views/action_text/attachables/_missing_attachable.html.erb
@@ -0,0 +1 @@
+<%= "☒" -%>
diff --git a/actiontext/app/views/action_text/attachables/_remote_image.html.erb b/actiontext/app/views/action_text/attachables/_remote_image.html.erb
new file mode 100644
index 0000000000..3372f8d940
--- /dev/null
+++ b/actiontext/app/views/action_text/attachables/_remote_image.html.erb
@@ -0,0 +1,8 @@
+<figure class="attachment attachment--preview">
+ <%= image_tag(remote_image.url, width: remote_image.width, height: remote_image.height) %>
+ <% if caption = remote_image.try(:caption) %>
+ <figcaption class="attachment__caption">
+ <%= caption %>
+ </figcaption>
+ <% end %>
+</figure>
diff --git a/actiontext/app/views/action_text/attachment_galleries/_attachment_gallery.html.erb b/actiontext/app/views/action_text/attachment_galleries/_attachment_gallery.html.erb
new file mode 100644
index 0000000000..6bc8674dc5
--- /dev/null
+++ b/actiontext/app/views/action_text/attachment_galleries/_attachment_gallery.html.erb
@@ -0,0 +1,3 @@
+<div class="attachment-gallery attachment-gallery--<%= attachment_gallery.size %>">
+ <%= yield %>
+</div>
diff --git a/actiontext/app/views/action_text/content/_layout.html.erb b/actiontext/app/views/action_text/content/_layout.html.erb
new file mode 100644
index 0000000000..55cb708ac4
--- /dev/null
+++ b/actiontext/app/views/action_text/content/_layout.html.erb
@@ -0,0 +1,3 @@
+<div class="trix-content">
+ <%= render_action_text_content(content) %>
+</div>
diff --git a/actiontext/app/views/active_storage/blobs/_blob.html.erb b/actiontext/app/views/active_storage/blobs/_blob.html.erb
new file mode 100644
index 0000000000..049f57e804
--- /dev/null
+++ b/actiontext/app/views/active_storage/blobs/_blob.html.erb
@@ -0,0 +1,14 @@
+<figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
+ <% if blob.representable? %>
+ <%= image_tag blob.representation(resize_to_fit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
+ <% end %>
+
+ <figcaption class="attachment__caption">
+ <% if caption = blob.try(:caption) %>
+ <%= caption %>
+ <% else %>
+ <span class="attachment__name"><%= blob.filename %></span>
+ <span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
+ <% end %>
+ </figcaption>
+</figure>