action_mosaico 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|