Skip to content

Commit

Permalink
Import Action Text
Browse files Browse the repository at this point in the history
  • Loading branch information
georgeclaghorn committed Jan 5, 2019
2 parents 8a23a0e + cfe4674 commit 0decd2d
Show file tree
Hide file tree
Showing 144 changed files with 9,447 additions and 5 deletions.
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ AllCops:
- 'actionpack/lib/action_dispatch/journey/parser.rb'
- 'railties/test/fixtures/tmp/**/*'
- 'actionmailbox/test/dummy/**/*'
- 'actiontext/test/dummy/**/*'
- 'node_modules/**/*'

Performance:
Expand Down Expand Up @@ -135,6 +136,7 @@ Style/FrozenStringLiteralComment:
- 'actionpack/test/**/*.ruby'
- 'activestorage/db/migrate/**/*.rb'
- 'actionmailbox/db/migrate/**/*.rb'
- 'actiontext/db/migrate/**/*.rb'

Style/RedundantFreeze:
Enabled: true
Expand Down
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ env:
- "JRUBY_OPTS='--dev -J-Xmx1024M'"
matrix:
- "GEM=actionpack,actioncable"
- "GEM=actionmailer,activemodel,activesupport,actionview,activejob,activestorage,actionmailbox"
- "GEM=actionmailer,activemodel,activesupport,actionview,activejob,activestorage,actionmailbox,actiontext"
- "GEM=activesupport PRESERVE_TIMEZONES=1"
- "GEM=activerecord:sqlite3"
- "GEM=guides"
Expand Down
7 changes: 7 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ PATH
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actiontext (6.0.0.alpha)
actionpack (= 6.0.0.alpha)
activerecord (= 6.0.0.alpha)
activestorage (= 6.0.0.alpha)
activesupport (= 6.0.0.alpha)
nokogiri (>= 1.8.5)
actionview (6.0.0.alpha)
activesupport (= 6.0.0.alpha)
builder (~> 3.1)
Expand Down Expand Up @@ -79,6 +85,7 @@ PATH
actionmailbox (= 6.0.0.alpha)
actionmailer (= 6.0.0.alpha)
actionpack (= 6.0.0.alpha)
actiontext (= 6.0.0.alpha)
actionview (= 6.0.0.alpha)
activejob (= 6.0.0.alpha)
activemodel (= 6.0.0.alpha)
Expand Down
5 changes: 5 additions & 0 deletions actiontext/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/test/dummy/db/*.sqlite3
/test/dummy/db/*.sqlite3-journal
/test/dummy/log/*.log
/test/dummy/tmp/
/tmp/
3 changes: 3 additions & 0 deletions actiontext/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* Added to Rails.

*DHH*
21 changes: 21 additions & 0 deletions actiontext/MIT-LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2019 Basecamp, LLC

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
67 changes: 67 additions & 0 deletions actiontext/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Action Text

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.

## Trix compared to other rich text editors

Most WYSIWYG editors are wrappers around HTML’s `contenteditable` and `execCommand` APIs, designed by Microsoft to support live editing of web pages in Internet Explorer 5.5, and [eventually reverse-engineered](https://blog.whatwg.org/the-road-to-html-5-contenteditable#history) and copied by other browsers.

Because these APIs were never fully specified or documented, and because WYSIWYG HTML editors are enormous in scope, each browser’s implementation has its own set of bugs and quirks, and JavaScript developers are left to resolve the inconsistencies.

Trix sidesteps these inconsistencies by treating contenteditable as an I/O device: when input makes its way to the editor, Trix converts that input into an editing operation on its internal document model, then re-renders that document back into the editor. This gives Trix complete control over what happens after every keystroke, and avoids the need to use execCommand at all.

## Installation

Run `rails action_text:install` to add the Yarn package and copy over the necessary migration.

## Examples

Adding a rich text field to an existing model:

```ruby
# app/models/message.rb
class Message < ApplicationRecord
has_rich_text :content
end
```

Then refer to this field in the form for the model:

```erb
<%# app/views/messages/_form.html.erb %>
<%= form_with(model: message) do |form| %>
<div class="field">
<%= form.label :content %>
<%= form.rich_text_area :content %>
</div>
<% end %>
```

And finally display the sanitized rich text on a page:

```erb
<%= @message.content %>
```

To accept the rich text content, all you have to do is permit the referenced attribute:

```ruby
class MessagesController < ApplicationController
def create
message = Message.create! params.require(:message).permit(:title, :content)
redirect_to message
end
end
```

## Custom styling

By default, the Action Text editor and content is styled by the Trix defaults. If you want to change these defaults, you'll want to remove the `app/assets/stylesheets/actiontext.css` linker and base your stylings on the [contents of that file](https://raw.githubusercontent.com/basecamp/trix/master/dist/trix.css).

You can also style the HTML used for embedded images and other attachments (known as blobs). On installation, Action Text will copy over a partial to `app/views/active_storage/blobs/_blob.html.erb`, which you can specialize.

## License

Action Text is released under the [MIT License](https://opensource.org/licenses/MIT).
13 changes: 13 additions & 0 deletions actiontext/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

require "bundler/setup"
require "bundler/gem_tasks"
require "rake/testtask"

Rake::TestTask.new do |t|
t.libs << "test"
t.pattern = "test/**/*_test.rb"
t.verbose = true
end

task default: :test
38 changes: 38 additions & 0 deletions actiontext/actiontext.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip

# Describe your gem and declare its dependencies:
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.name = "actiontext"
s.version = version
s.summary = "Rich text framework."
s.description = "Edit and display rich text in Rails applications."

s.required_ruby_version = ">= 2.5.0"

s.license = "MIT"

s.authors = ["Javan Makhmali", "Sam Stephenson", "David Heinemeier Hansson"]
s.email = ["[email protected]", "[email protected]", "[email protected]"]
s.homepage = "https://rubyonrails.org"

s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*", "app/**/*", "config/**/*", "db/**/*"]
s.require_path = "lib"

s.metadata = {
"source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/actiontext",
"changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actiontext/CHANGELOG.md"
}

# NOTE: Please read our dependency guidelines before updating versions:
# https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves

s.add_dependency "activesupport", version
s.add_dependency "activerecord", version
s.add_dependency "activestorage", version
s.add_dependency "actionpack", version

s.add_dependency "nokogiri", ">= 1.8.5"
end
30 changes: 30 additions & 0 deletions actiontext/app/helpers/action_text/content_helper.rb
Original file line number Diff line number Diff line change
@@ -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
75 changes: 75 additions & 0 deletions actiontext/app/helpers/action_text/tag_helper.rb
Original file line number Diff line number Diff line change
@@ -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
45 changes: 45 additions & 0 deletions actiontext/app/javascript/actiontext/attachment_upload.js
Original file line number Diff line number Diff line change
@@ -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
}
}
11 changes: 11 additions & 0 deletions actiontext/app/javascript/actiontext/index.js
Original file line number Diff line number Diff line change
@@ -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()
}
})
25 changes: 25 additions & 0 deletions actiontext/app/models/action_text/rich_text.rb
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= "☒" -%>
Loading

0 comments on commit 0decd2d

Please sign in to comment.