action_mosaico 0.1.0
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 +7 -0
- data/.rubocop.yml +6 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +19 -0
- data/Gemfile.lock +114 -0
- data/LICENSE.txt +21 -0
- data/README.md +45 -0
- data/Rakefile +16 -0
- data/action_mosaico.gemspec +45 -0
- data/app/assets/javascripts/action_mosaico.js +0 -0
- data/app/assets/stylesheets/action_mosaico.css +0 -0
- data/app/helpers/action_mosaico/content_helper.rb +51 -0
- data/app/helpers/action_mosaico/tag_helper.rb +95 -0
- data/app/javascript/action_mosaico/attachment_upload.js +45 -0
- data/app/javascript/action_mosaico/index.js +10 -0
- data/app/models/action_mosaico/encrypted_rich_text.rb +9 -0
- data/app/models/action_mosaico/record.rb +9 -0
- data/app/models/action_mosaico/rich_text.rb +33 -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/contents/_content.html.erb +1 -0
- data/app/views/active_storage/blobs/_blob.html.erb +14 -0
- data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/db/migrate/20180528164100_create_action_mosaico_tables.rb +28 -0
- data/lib/action_mosaico/attachable.rb +91 -0
- data/lib/action_mosaico/attachables/content_attachment.rb +37 -0
- data/lib/action_mosaico/attachables/missing_attachable.rb +13 -0
- data/lib/action_mosaico/attachables/remote_image.rb +45 -0
- data/lib/action_mosaico/attachment.rb +109 -0
- data/lib/action_mosaico/attachment_gallery.rb +70 -0
- data/lib/action_mosaico/attachments/caching.rb +17 -0
- data/lib/action_mosaico/attachments/minification.rb +17 -0
- data/lib/action_mosaico/attachments/mosaico_conversion.rb +37 -0
- data/lib/action_mosaico/attribute.rb +66 -0
- data/lib/action_mosaico/content.rb +132 -0
- data/lib/action_mosaico/encryption.rb +39 -0
- data/lib/action_mosaico/engine.rb +86 -0
- data/lib/action_mosaico/fixture_set.rb +61 -0
- data/lib/action_mosaico/fragment.rb +57 -0
- data/lib/action_mosaico/html_conversion.rb +25 -0
- data/lib/action_mosaico/mosaico_attachment.rb +95 -0
- data/lib/action_mosaico/plain_text_conversion.rb +84 -0
- data/lib/action_mosaico/rendering.rb +30 -0
- data/lib/action_mosaico/serialization.rb +36 -0
- data/lib/action_mosaico/system_test_helper.rb +55 -0
- data/lib/action_mosaico/version.rb +5 -0
- data/lib/action_mosaico.rb +40 -0
- data/lib/generators/action_mosaico/install/install_generator.rb +60 -0
- data/lib/generators/action_mosaico/install/templates/actiontext.css +35 -0
- data/lib/tasks/action_mosaico.rake +6 -0
- data/package.json +32 -0
- metadata +172 -0
@@ -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_limit: 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>
|
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'action_mosaico'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require 'irb'
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateActionMosaicoTables < ActiveRecord::Migration[7.0]
|
4
|
+
def change
|
5
|
+
# Use Active Record's configured type for primary and foreign keys
|
6
|
+
primary_key_type, foreign_key_type = primary_and_foreign_key_types
|
7
|
+
|
8
|
+
create_table :action_mosaico_rich_texts, id: primary_key_type do |t|
|
9
|
+
t.string :name, null: false
|
10
|
+
t.text :body, size: :long
|
11
|
+
t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
|
12
|
+
|
13
|
+
t.timestamps
|
14
|
+
|
15
|
+
t.index %i[record_type record_id name], name: 'index_action_mosaico_rich_texts_uniqueness', unique: true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def primary_and_foreign_key_types
|
22
|
+
config = Rails.configuration.generators
|
23
|
+
setting = config.options[config.orm][:primary_key_type]
|
24
|
+
primary_key_type = setting || :primary_key
|
25
|
+
foreign_key_type = setting || :bigint
|
26
|
+
[primary_key_type, foreign_key_type]
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMosaico
|
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 = ActionMosaico::Attachables::ContentAttachment.from_node(node)
|
14
|
+
attachable
|
15
|
+
elsif attachable = ActionMosaico::Attachables::RemoteImage.from_node(node)
|
16
|
+
attachable
|
17
|
+
else
|
18
|
+
ActionMosaico::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
|
+
|
30
|
+
def attachable_from_sgid(sgid)
|
31
|
+
from_attachable_sgid(sgid)
|
32
|
+
rescue ActiveRecord::RecordNotFound
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class_methods do
|
38
|
+
def from_attachable_sgid(sgid)
|
39
|
+
ActionMosaico::Attachable.from_attachable_sgid(sgid, only: self)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def attachable_sgid
|
44
|
+
to_sgid(expires_in: nil, for: LOCATOR_NAME).to_s
|
45
|
+
end
|
46
|
+
|
47
|
+
def attachable_content_type
|
48
|
+
try(:content_type) || 'application/octet-stream'
|
49
|
+
end
|
50
|
+
|
51
|
+
def attachable_filename
|
52
|
+
filename.to_s if respond_to?(:filename)
|
53
|
+
end
|
54
|
+
|
55
|
+
def attachable_filesize
|
56
|
+
try(:byte_size) || try(:filesize)
|
57
|
+
end
|
58
|
+
|
59
|
+
def attachable_metadata
|
60
|
+
try(:metadata) || {}
|
61
|
+
end
|
62
|
+
|
63
|
+
def previewable_attachable?
|
64
|
+
false
|
65
|
+
end
|
66
|
+
|
67
|
+
def as_json(*)
|
68
|
+
super.merge(attachable_sgid: attachable_sgid)
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_mosaico_content_attachment_partial_path
|
72
|
+
to_partial_path
|
73
|
+
end
|
74
|
+
|
75
|
+
def to_attachable_partial_path
|
76
|
+
to_partial_path
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_rich_text_attributes(attributes = {})
|
80
|
+
attributes.dup.tap do |attrs|
|
81
|
+
attrs[:sgid] = attachable_sgid
|
82
|
+
attrs[:content_type] = attachable_content_type
|
83
|
+
attrs[:previewable] = true if previewable_attachable?
|
84
|
+
attrs[:filename] = attachable_filename
|
85
|
+
attrs[:filesize] = attachable_filesize
|
86
|
+
attrs[:width] = attachable_metadata[:width]
|
87
|
+
attrs[:height] = attachable_metadata[:height]
|
88
|
+
end.compact
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMosaico
|
4
|
+
module Attachables
|
5
|
+
class ContentAttachment
|
6
|
+
include ActiveModel::Model
|
7
|
+
|
8
|
+
def self.from_node(node)
|
9
|
+
if node['content-type'] && matches = node['content-type'].match(/vnd\.rubyonrails\.(.+)\.html/)
|
10
|
+
attachment = new(name: matches[1])
|
11
|
+
attachment if attachment.valid?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_accessor :name
|
16
|
+
|
17
|
+
validates_inclusion_of :name, in: %w[horizontal-rule]
|
18
|
+
|
19
|
+
def attachable_plain_text_representation(_caption)
|
20
|
+
case name
|
21
|
+
when 'horizontal-rule'
|
22
|
+
' ┄ '
|
23
|
+
else
|
24
|
+
' '
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_partial_path
|
29
|
+
'action_mosaico/attachables/content_attachment'
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_mosaico_content_attachment_partial_path
|
33
|
+
"action_mosaico/attachables/content_attachments/#{name.underscore}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMosaico
|
4
|
+
module Attachables
|
5
|
+
class RemoteImage
|
6
|
+
extend ActiveModel::Naming
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def from_node(node)
|
10
|
+
new(attributes_from_node(node)) if node['url'] && content_type_is_image?(node['content-type'])
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def content_type_is_image?(content_type)
|
16
|
+
content_type.to_s.match?(%r{^image(/.+|$)})
|
17
|
+
end
|
18
|
+
|
19
|
+
def attributes_from_node(node)
|
20
|
+
{ url: node['url'],
|
21
|
+
content_type: node['content-type'],
|
22
|
+
width: node['width'],
|
23
|
+
height: node['height'] }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_reader :url, :content_type, :width, :height
|
28
|
+
|
29
|
+
def initialize(attributes = {})
|
30
|
+
@url = attributes[:url]
|
31
|
+
@content_type = attributes[:content_type]
|
32
|
+
@width = attributes[:width]
|
33
|
+
@height = attributes[:height]
|
34
|
+
end
|
35
|
+
|
36
|
+
def attachable_plain_text_representation(caption)
|
37
|
+
"[#{caption || 'Image'}]"
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_partial_path
|
41
|
+
'action_mosaico/attachables/remote_image'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/object/try'
|
4
|
+
|
5
|
+
module ActionMosaico
|
6
|
+
class Attachment
|
7
|
+
include Attachments::Caching
|
8
|
+
include Attachments::Minification
|
9
|
+
include Attachments::TrixConversion
|
10
|
+
|
11
|
+
mattr_accessor :tag_name, default: 'action-text-attachment'
|
12
|
+
|
13
|
+
ATTRIBUTES = %w[sgid content-type url href filename filesize width height previewable presentation caption].freeze
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def fragment_by_canonicalizing_attachments(content)
|
17
|
+
fragment_by_minifying_attachments(fragment_by_converting_mosaico_attachments(content))
|
18
|
+
end
|
19
|
+
|
20
|
+
def from_node(node, attachable = nil)
|
21
|
+
new(node, attachable || ActionMosaico::Attachable.from_node(node))
|
22
|
+
end
|
23
|
+
|
24
|
+
def from_attachables(attachables)
|
25
|
+
Array(attachables).filter_map { |attachable| from_attachable(attachable) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def from_attachable(attachable, attributes = {})
|
29
|
+
if node = node_from_attributes(attachable.to_rich_text_attributes(attributes))
|
30
|
+
new(node, attachable)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def from_attributes(attributes, attachable = nil)
|
35
|
+
if node = node_from_attributes(attributes)
|
36
|
+
from_node(node, attachable)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def node_from_attributes(attributes)
|
43
|
+
if attributes = process_attributes(attributes).presence
|
44
|
+
ActionMosaico::HtmlConversion.create_element(tag_name, attributes)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def process_attributes(attributes)
|
49
|
+
attributes.transform_keys { |key| key.to_s.underscore.dasherize }.slice(*ATTRIBUTES)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
attr_reader :node, :attachable
|
54
|
+
|
55
|
+
delegate :to_param, to: :attachable
|
56
|
+
delegate_missing_to :attachable
|
57
|
+
|
58
|
+
def initialize(node, attachable)
|
59
|
+
@node = node
|
60
|
+
@attachable = attachable
|
61
|
+
end
|
62
|
+
|
63
|
+
def caption
|
64
|
+
node_attributes['caption'].presence
|
65
|
+
end
|
66
|
+
|
67
|
+
def full_attributes
|
68
|
+
node_attributes.merge(attachable_attributes).merge(sgid_attributes)
|
69
|
+
end
|
70
|
+
|
71
|
+
def with_full_attributes
|
72
|
+
self.class.from_attributes(full_attributes, attachable)
|
73
|
+
end
|
74
|
+
|
75
|
+
def to_plain_text
|
76
|
+
if respond_to?(:attachable_plain_text_representation)
|
77
|
+
attachable_plain_text_representation(caption)
|
78
|
+
else
|
79
|
+
caption.to_s
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_html
|
84
|
+
HtmlConversion.node_to_html(node)
|
85
|
+
end
|
86
|
+
|
87
|
+
def to_s
|
88
|
+
to_html
|
89
|
+
end
|
90
|
+
|
91
|
+
def inspect
|
92
|
+
"#<#{self.class.name} attachable=#{attachable.inspect}>"
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def node_attributes
|
98
|
+
@node_attributes ||= ATTRIBUTES.map { |name| [name.underscore, node[name]] }.to_h.compact
|
99
|
+
end
|
100
|
+
|
101
|
+
def attachable_attributes
|
102
|
+
@attachable_attributes ||= (attachable.try(:to_rich_text_attributes) || {}).stringify_keys
|
103
|
+
end
|
104
|
+
|
105
|
+
def sgid_attributes
|
106
|
+
@sgid_attributes ||= node_attributes.slice('sgid').presence || attachable_attributes.slice('sgid')
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMosaico
|
4
|
+
class AttachmentGallery
|
5
|
+
include ActiveModel::Model
|
6
|
+
|
7
|
+
TAG_NAME = 'div'
|
8
|
+
private_constant :TAG_NAME
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def fragment_by_canonicalizing_attachment_galleries(content)
|
12
|
+
fragment_by_replacing_attachment_gallery_nodes(content) do |node|
|
13
|
+
"<#{TAG_NAME}>#{node.inner_html}</#{TAG_NAME}>"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def fragment_by_replacing_attachment_gallery_nodes(content)
|
18
|
+
Fragment.wrap(content).update do |source|
|
19
|
+
find_attachment_gallery_nodes(source).each do |node|
|
20
|
+
node.replace(yield(node).to_s)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def find_attachment_gallery_nodes(content)
|
26
|
+
Fragment.wrap(content).find_all(selector).select do |node|
|
27
|
+
node.children.all? do |child|
|
28
|
+
if child.text?
|
29
|
+
/\A(\n|\ )*\z/.match?(child.text)
|
30
|
+
else
|
31
|
+
child.matches? attachment_selector
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def from_node(node)
|
38
|
+
new(node)
|
39
|
+
end
|
40
|
+
|
41
|
+
def attachment_selector
|
42
|
+
"#{ActionMosaico::Attachment.tag_name}[presentation=gallery]"
|
43
|
+
end
|
44
|
+
|
45
|
+
def selector
|
46
|
+
"#{TAG_NAME}:has(#{attachment_selector} + #{attachment_selector})"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
attr_reader :node
|
51
|
+
|
52
|
+
def initialize(node)
|
53
|
+
@node = node
|
54
|
+
end
|
55
|
+
|
56
|
+
def attachments
|
57
|
+
@attachments ||= node.css(ActionMosaico::AttachmentGallery.attachment_selector).map do |node|
|
58
|
+
ActionMosaico::Attachment.from_node(node).with_full_attributes
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def size
|
63
|
+
attachments.size
|
64
|
+
end
|
65
|
+
|
66
|
+
def inspect
|
67
|
+
"#<#{self.class.name} size=#{size.inspect}>"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMosaico
|
4
|
+
module Attachments
|
5
|
+
module Caching
|
6
|
+
def cache_key(*args)
|
7
|
+
[self.class.name, cache_digest, *attachable.cache_key(*args)].join('/')
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def cache_digest
|
13
|
+
OpenSSL::Digest::SHA256.hexdigest(node.to_s)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMosaico
|
4
|
+
module Attachments
|
5
|
+
module Minification
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
class_methods do
|
9
|
+
def fragment_by_minifying_attachments(content)
|
10
|
+
Fragment.wrap(content).replace(ActionMosaico::Attachment.tag_name) do |node|
|
11
|
+
node.tap { |n| n.inner_html = '' }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/object/try'
|
4
|
+
|
5
|
+
module ActionMosaico
|
6
|
+
module Attachments
|
7
|
+
module TrixConversion
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
def fragment_by_converting_mosaico_attachments(content)
|
12
|
+
Fragment.wrap(content).replace(TrixAttachment::SELECTOR) do |node|
|
13
|
+
from_mosaico_attachment(TrixAttachment.new(node))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def from_mosaico_attachment(mosaico_attachment)
|
18
|
+
from_attributes(mosaico_attachment.attributes)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_mosaico_attachment(content = mosaico_attachment_content)
|
23
|
+
attributes = full_attributes.dup
|
24
|
+
attributes['content'] = content if content
|
25
|
+
TrixAttachment.from_attributes(attributes)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def mosaico_attachment_content
|
31
|
+
if partial_path = attachable.try(:to_mosaico_content_attachment_partial_path)
|
32
|
+
ActionMosaico::Content.render(partial: partial_path, formats: :html, object: self, as: model_name.element)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMosaico
|
4
|
+
module Attribute
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
class_methods do
|
8
|
+
# Provides access to a dependent RichText model that holds the body and attachments for a single named rich text attribute.
|
9
|
+
# This dependent attribute is lazily instantiated and will be auto-saved when it's been changed. Example:
|
10
|
+
#
|
11
|
+
# class Message < ActiveRecord::Base
|
12
|
+
# has_rich_text :content
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# message = Message.create!(content: "<h1>Funny times!</h1>")
|
16
|
+
# message.content? #=> true
|
17
|
+
# message.content.to_s # => "<h1>Funny times!</h1>"
|
18
|
+
# message.content.to_plain_text # => "Funny times!"
|
19
|
+
#
|
20
|
+
# The dependent RichText model will also automatically process attachments links as sent via the Trix-powered editor.
|
21
|
+
# These attachments are associated with the RichText model using Active Storage.
|
22
|
+
#
|
23
|
+
# If you wish to preload the dependent RichText model, you can use the named scope:
|
24
|
+
#
|
25
|
+
# Message.all.with_rich_text_content # Avoids N+1 queries when you just want the body, not the attachments.
|
26
|
+
# Message.all.with_rich_text_content_and_embeds # Avoids N+1 queries when you just want the body and attachments.
|
27
|
+
# Message.all.with_all_rich_text # Loads all rich text associations.
|
28
|
+
#
|
29
|
+
# === Options
|
30
|
+
#
|
31
|
+
# * <tt>:encrypted</tt> - Pass true to encrypt the rich text attribute. The encryption will be non-deterministic. See
|
32
|
+
# +ActiveRecord::Encryption::EncryptableRecord.encrypts+. Default: false.
|
33
|
+
def has_rich_text(name, encrypted: false)
|
34
|
+
class_eval <<-CODE, __FILE__, __LINE__ + 1
|
35
|
+
def #{name}
|
36
|
+
rich_text_#{name} || build_rich_text_#{name}
|
37
|
+
end
|
38
|
+
|
39
|
+
def #{name}?
|
40
|
+
rich_text_#{name}.present?
|
41
|
+
end
|
42
|
+
|
43
|
+
def #{name}=(body)
|
44
|
+
self.#{name}.body = body
|
45
|
+
end
|
46
|
+
CODE
|
47
|
+
|
48
|
+
rich_text_class_name = encrypted ? 'ActionMosaico::EncryptedRichText' : 'ActionMosaico::RichText'
|
49
|
+
has_one :"rich_text_#{name}", -> { where(name: name) },
|
50
|
+
class_name: rich_text_class_name, as: :record, inverse_of: :record, autosave: true, dependent: :destroy
|
51
|
+
|
52
|
+
scope :"with_rich_text_#{name}", -> { includes("rich_text_#{name}") }
|
53
|
+
scope :"with_rich_text_#{name}_and_embeds", -> { includes("rich_text_#{name}": { embeds_attachments: :blob }) }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Eager load all dependent RichText models in bulk.
|
57
|
+
def with_all_rich_text
|
58
|
+
eager_load(rich_text_association_names)
|
59
|
+
end
|
60
|
+
|
61
|
+
def rich_text_association_names
|
62
|
+
reflect_on_all_associations(:has_one).collect(&:name).select { |n| n.start_with?('rich_text_') }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|