action_mosaico 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +6 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +5 -0
  6. data/CODE_OF_CONDUCT.md +84 -0
  7. data/Gemfile +19 -0
  8. data/Gemfile.lock +114 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +45 -0
  11. data/Rakefile +16 -0
  12. data/action_mosaico.gemspec +45 -0
  13. data/app/assets/javascripts/action_mosaico.js +0 -0
  14. data/app/assets/stylesheets/action_mosaico.css +0 -0
  15. data/app/helpers/action_mosaico/content_helper.rb +51 -0
  16. data/app/helpers/action_mosaico/tag_helper.rb +95 -0
  17. data/app/javascript/action_mosaico/attachment_upload.js +45 -0
  18. data/app/javascript/action_mosaico/index.js +10 -0
  19. data/app/models/action_mosaico/encrypted_rich_text.rb +9 -0
  20. data/app/models/action_mosaico/record.rb +9 -0
  21. data/app/models/action_mosaico/rich_text.rb +33 -0
  22. data/app/views/action_text/attachables/_missing_attachable.html.erb +1 -0
  23. data/app/views/action_text/attachables/_remote_image.html.erb +8 -0
  24. data/app/views/action_text/attachment_galleries/_attachment_gallery.html.erb +3 -0
  25. data/app/views/action_text/contents/_content.html.erb +1 -0
  26. data/app/views/active_storage/blobs/_blob.html.erb +14 -0
  27. data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
  28. data/bin/console +15 -0
  29. data/bin/setup +8 -0
  30. data/db/migrate/20180528164100_create_action_mosaico_tables.rb +28 -0
  31. data/lib/action_mosaico/attachable.rb +91 -0
  32. data/lib/action_mosaico/attachables/content_attachment.rb +37 -0
  33. data/lib/action_mosaico/attachables/missing_attachable.rb +13 -0
  34. data/lib/action_mosaico/attachables/remote_image.rb +45 -0
  35. data/lib/action_mosaico/attachment.rb +109 -0
  36. data/lib/action_mosaico/attachment_gallery.rb +70 -0
  37. data/lib/action_mosaico/attachments/caching.rb +17 -0
  38. data/lib/action_mosaico/attachments/minification.rb +17 -0
  39. data/lib/action_mosaico/attachments/mosaico_conversion.rb +37 -0
  40. data/lib/action_mosaico/attribute.rb +66 -0
  41. data/lib/action_mosaico/content.rb +132 -0
  42. data/lib/action_mosaico/encryption.rb +39 -0
  43. data/lib/action_mosaico/engine.rb +86 -0
  44. data/lib/action_mosaico/fixture_set.rb +61 -0
  45. data/lib/action_mosaico/fragment.rb +57 -0
  46. data/lib/action_mosaico/html_conversion.rb +25 -0
  47. data/lib/action_mosaico/mosaico_attachment.rb +95 -0
  48. data/lib/action_mosaico/plain_text_conversion.rb +84 -0
  49. data/lib/action_mosaico/rendering.rb +30 -0
  50. data/lib/action_mosaico/serialization.rb +36 -0
  51. data/lib/action_mosaico/system_test_helper.rb +55 -0
  52. data/lib/action_mosaico/version.rb +5 -0
  53. data/lib/action_mosaico.rb +40 -0
  54. data/lib/generators/action_mosaico/install/install_generator.rb +60 -0
  55. data/lib/generators/action_mosaico/install/templates/actiontext.css +35 -0
  56. data/lib/tasks/action_mosaico.rake +6 -0
  57. data/package.json +32 -0
  58. metadata +172 -0
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMosaico
4
+ class Content
5
+ include Serialization
6
+ include Rendering
7
+
8
+ attr_reader :fragment
9
+
10
+ delegate :blank?, :empty?, :html_safe, :present?, to: :to_html # Delegating to to_html to avoid including the layout
11
+
12
+ class << self
13
+ def fragment_by_canonicalizing_content(content)
14
+ fragment = ActionMosaico::Attachment.fragment_by_canonicalizing_attachments(content)
15
+ ActionMosaico::AttachmentGallery.fragment_by_canonicalizing_attachment_galleries(fragment)
16
+ end
17
+ end
18
+
19
+ def initialize(content = nil, options = {})
20
+ options.with_defaults! canonicalize: true
21
+
22
+ @fragment = if options[:canonicalize]
23
+ self.class.fragment_by_canonicalizing_content(content)
24
+ else
25
+ ActionMosaico::Fragment.wrap(content)
26
+ end
27
+ end
28
+
29
+ def links
30
+ @links ||= fragment.find_all('a[href]').map { |a| a['href'] }.uniq
31
+ end
32
+
33
+ def attachments
34
+ @attachments ||= attachment_nodes.map do |node|
35
+ attachment_for_node(node)
36
+ end
37
+ end
38
+
39
+ def attachment_galleries
40
+ @attachment_galleries ||= attachment_gallery_nodes.map do |node|
41
+ attachment_gallery_for_node(node)
42
+ end
43
+ end
44
+
45
+ def gallery_attachments
46
+ @gallery_attachments ||= attachment_galleries.flat_map(&:attachments)
47
+ end
48
+
49
+ def attachables
50
+ @attachables ||= attachment_nodes.map do |node|
51
+ ActionMosaico::Attachable.from_node(node)
52
+ end
53
+ end
54
+
55
+ def append_attachables(attachables)
56
+ attachments = ActionMosaico::Attachment.from_attachables(attachables)
57
+ self.class.new([to_s.presence, *attachments].compact.join("\n"))
58
+ end
59
+
60
+ def render_attachments(**options, &block)
61
+ content = fragment.replace(ActionMosaico::Attachment.tag_name) do |node|
62
+ block.call(attachment_for_node(node, **options))
63
+ end
64
+ self.class.new(content, canonicalize: false)
65
+ end
66
+
67
+ def render_attachment_galleries(&block)
68
+ content = ActionMosaico::AttachmentGallery.fragment_by_replacing_attachment_gallery_nodes(fragment) do |node|
69
+ block.call(attachment_gallery_for_node(node))
70
+ end
71
+ self.class.new(content, canonicalize: false)
72
+ end
73
+
74
+ def to_plain_text
75
+ render_attachments(with_full_attributes: false, &:to_plain_text).fragment.to_plain_text
76
+ end
77
+
78
+ def to_mosaico_html
79
+ render_attachments(&:to_mosaico_attachment).to_html
80
+ end
81
+
82
+ def to_html
83
+ fragment.to_html
84
+ end
85
+
86
+ def to_rendered_html_with_layout
87
+ render layout: 'action_mosaico/contents/content', partial: to_partial_path, formats: :html,
88
+ locals: { content: self }
89
+ end
90
+
91
+ def to_partial_path
92
+ 'action_mosaico/contents/content'
93
+ end
94
+
95
+ def to_s
96
+ to_rendered_html_with_layout
97
+ end
98
+
99
+ def as_json(*)
100
+ to_html
101
+ end
102
+
103
+ def inspect
104
+ "#<#{self.class.name} #{to_s.truncate(25).inspect}>"
105
+ end
106
+
107
+ def ==(other)
108
+ to_s == other.to_s if other.is_a?(self.class)
109
+ end
110
+
111
+ private
112
+
113
+ def attachment_nodes
114
+ @attachment_nodes ||= fragment.find_all(ActionMosaico::Attachment.tag_name)
115
+ end
116
+
117
+ def attachment_gallery_nodes
118
+ @attachment_gallery_nodes ||= ActionMosaico::AttachmentGallery.find_attachment_gallery_nodes(fragment)
119
+ end
120
+
121
+ def attachment_for_node(node, with_full_attributes: true)
122
+ attachment = ActionMosaico::Attachment.from_node(node)
123
+ with_full_attributes ? attachment.with_full_attributes : attachment
124
+ end
125
+
126
+ def attachment_gallery_for_node(node)
127
+ ActionMosaico::AttachmentGallery.from_node(node)
128
+ end
129
+ end
130
+ end
131
+
132
+ ActiveSupport.run_load_hooks :action_mosaico_content, ActionMosaico::Content
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMosaico
4
+ module Encryption
5
+ def encrypt
6
+ transaction do
7
+ super
8
+ encrypt_rich_texts if has_encrypted_rich_texts?
9
+ end
10
+ end
11
+
12
+ def decrypt
13
+ transaction do
14
+ super
15
+ decrypt_rich_texts if has_encrypted_rich_texts?
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def encrypt_rich_texts
22
+ encryptable_rich_texts.each(&:encrypt)
23
+ end
24
+
25
+ def decrypt_rich_texts
26
+ encryptable_rich_texts.each(&:decrypt)
27
+ end
28
+
29
+ def has_encrypted_rich_texts?
30
+ encryptable_rich_texts.present?
31
+ end
32
+
33
+ def encryptable_rich_texts
34
+ @encryptable_rich_texts ||= self.class.rich_text_association_names
35
+ .filter_map { |attribute_name| send(attribute_name) }
36
+ .find_all { |record| record.is_a?(ActionMosaico::EncryptedRichText) }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails'
4
+ require 'action_controller/railtie'
5
+ require 'active_record/railtie'
6
+ require 'active_storage/engine'
7
+
8
+ require 'action_mosaico'
9
+
10
+ module ActionMosaico
11
+ class Engine < Rails::Engine
12
+ isolate_namespace ActionMosaico
13
+ config.eager_load_namespaces << ActionMosaico
14
+
15
+ config.action_mosaico = ActiveSupport::OrderedOptions.new
16
+ config.action_mosaico.attachment_tag_name = 'action-text-attachment'
17
+ config.autoload_once_paths = %W[
18
+ #{root}/app/helpers
19
+ #{root}/app/models
20
+ ]
21
+
22
+ initializer 'action_mosaico.attribute' do
23
+ ActiveSupport.on_load(:active_record) do
24
+ include ActionMosaico::Attribute
25
+ prepend ActionMosaico::Encryption
26
+ end
27
+ end
28
+
29
+ initializer 'action_mosaico.asset' do
30
+ if Rails.application.config.respond_to?(:assets)
31
+ Rails.application.config.assets.precompile += %w[action_mosaico.js mosaico.js mosaico.css]
32
+ end
33
+ end
34
+
35
+ initializer 'action_mosaico.attachable' do
36
+ ActiveSupport.on_load(:active_storage_blob) do
37
+ include ActionMosaico::Attachable
38
+
39
+ def previewable_attachable?
40
+ representable?
41
+ end
42
+
43
+ def attachable_plain_text_representation(caption = nil)
44
+ "[#{caption || filename}]"
45
+ end
46
+
47
+ def to_mosaico_content_attachment_partial_path
48
+ nil
49
+ end
50
+ end
51
+ end
52
+
53
+ initializer 'action_mosaico.helper' do
54
+ %i[action_controller_base action_mailer].each do |abstract_controller|
55
+ ActiveSupport.on_load(abstract_controller) do
56
+ helper ActionMosaico::Engine.helpers
57
+ end
58
+ end
59
+ end
60
+
61
+ initializer 'action_mosaico.renderer' do
62
+ ActiveSupport.on_load(:action_mosaico_content) do
63
+ self.default_renderer = Class.new(ActionController::Base).renderer
64
+ end
65
+
66
+ %i[action_controller_base action_mailer].each do |abstract_controller|
67
+ ActiveSupport.on_load(abstract_controller) do
68
+ around_action do |controller, action|
69
+ ActionMosaico::Content.with_renderer(controller, &action)
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ initializer 'action_mosaico.system_test_helper' do
76
+ ActiveSupport.on_load(:action_dispatch_system_test_case) do
77
+ require 'action_mosaico/system_test_helper'
78
+ include ActionMosaico::SystemTestHelper
79
+ end
80
+ end
81
+
82
+ initializer 'action_mosaico.configure' do |app|
83
+ ActionMosaico::Attachment.tag_name = app.config.action_mosaico.attachment_tag_name
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMosaico
4
+ # Fixtures are a way of organizing data that you want to test against; in
5
+ # short, sample data.
6
+ #
7
+ # To learn more about fixtures, read the
8
+ # {ActiveRecord::FixtureSet}[rdoc-ref:ActiveRecord::FixtureSet] documentation.
9
+ #
10
+ # === YAML
11
+ #
12
+ # Like other Active Record-backed models, ActionMosaico::RichText records inherit
13
+ # from ActiveRecord::Base instances and therefore can be populated by
14
+ # fixtures.
15
+ #
16
+ # Consider a hypothetical <tt>Article</tt> model class, its related fixture
17
+ # data, as well as fixture data for related ActionMosaico::RichText records:
18
+ #
19
+ # # app/models/article.rb
20
+ # class Article < ApplicationRecord
21
+ # has_rich_text :content
22
+ # end
23
+ #
24
+ # # tests/fixtures/articles.yml
25
+ # first:
26
+ # title: An Article
27
+ #
28
+ # # tests/fixtures/action_mosaico/rich_texts.yml
29
+ # first_content:
30
+ # record: first (Article)
31
+ # name: content
32
+ # body: <div>Hello, world.</div>
33
+ #
34
+ # When processed, Active Record will insert database records for each fixture
35
+ # entry and will ensure the Action Mosaico relationship is intact.
36
+ class FixtureSet
37
+ # Fixtures support Action Mosaico attachments as part of their <tt>body</tt>
38
+ # HTML.
39
+ #
40
+ # === Examples
41
+ #
42
+ # For example, consider a second <tt>Article</tt> record that mentions the
43
+ # first as part of its <tt>content</tt> HTML:
44
+ #
45
+ # # tests/fixtures/articles.yml
46
+ # second:
47
+ # title: Another Article
48
+ #
49
+ # # tests/fixtures/action_mosaico/rich_texts.yml
50
+ # second_content:
51
+ # record: second (Article)
52
+ # name: content
53
+ # body: <div>Hello, <%= ActionMosaico::FixtureSet.attachment("articles", :first) %></div>
54
+ def self.attachment(fixture_set_name, label, column_type: :integer)
55
+ signed_global_id = ActiveRecord::FixtureSet.signed_global_id fixture_set_name, label,
56
+ column_type: column_type, for: ActionMosaico::Attachable::LOCATOR_NAME
57
+
58
+ %(<action-text-attachment sgid="#{signed_global_id}"></action-text-attachment>)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMosaico
4
+ class Fragment
5
+ class << self
6
+ def wrap(fragment_or_html)
7
+ case fragment_or_html
8
+ when self
9
+ fragment_or_html
10
+ when Nokogiri::HTML::DocumentFragment
11
+ new(fragment_or_html)
12
+ else
13
+ from_html(fragment_or_html)
14
+ end
15
+ end
16
+
17
+ def from_html(html)
18
+ new(ActionMosaico::HtmlConversion.fragment_for_html(html.to_s.strip))
19
+ end
20
+ end
21
+
22
+ attr_reader :source
23
+
24
+ def initialize(source)
25
+ @source = source
26
+ end
27
+
28
+ def find_all(selector)
29
+ source.css(selector)
30
+ end
31
+
32
+ def update
33
+ yield source = self.source.clone
34
+ self.class.new(source)
35
+ end
36
+
37
+ def replace(selector)
38
+ update do |source|
39
+ source.css(selector).each do |node|
40
+ node.replace(yield(node).to_s)
41
+ end
42
+ end
43
+ end
44
+
45
+ def to_plain_text
46
+ @plain_text ||= PlainTextConversion.node_to_plain_text(source)
47
+ end
48
+
49
+ def to_html
50
+ @html ||= HtmlConversion.node_to_html(source)
51
+ end
52
+
53
+ def to_s
54
+ to_html
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMosaico
4
+ module HtmlConversion
5
+ extend self
6
+
7
+ def node_to_html(node)
8
+ node.to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_HTML)
9
+ end
10
+
11
+ def fragment_for_html(html)
12
+ document.fragment(html)
13
+ end
14
+
15
+ def create_element(tag_name, attributes = {})
16
+ document.create_element(tag_name, attributes)
17
+ end
18
+
19
+ private
20
+
21
+ def document
22
+ Nokogiri::HTML::Document.new.tap { |doc| doc.encoding = 'UTF-8' }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMosaico
4
+ class TrixAttachment
5
+ TAG_NAME = 'figure'
6
+ SELECTOR = '[data-mosaico-attachment]'
7
+
8
+ COMPOSED_ATTRIBUTES = %w[caption presentation].freeze
9
+ ATTRIBUTES = %w[sgid contentType url href filename filesize width height previewable
10
+ content] + COMPOSED_ATTRIBUTES
11
+ ATTRIBUTE_TYPES = {
12
+ 'previewable' => ->(value) { value.to_s == 'true' },
13
+ 'filesize' => ->(value) { Integer(value.to_s, exception: false) || value },
14
+ 'width' => ->(value) { Integer(value.to_s, exception: false) },
15
+ 'height' => ->(value) { Integer(value.to_s, exception: false) },
16
+ :default => ->(value) { value.to_s }
17
+ }.freeze
18
+
19
+ class << self
20
+ def from_attributes(attributes)
21
+ attributes = process_attributes(attributes)
22
+
23
+ mosaico_attachment_attributes = attributes.except(*COMPOSED_ATTRIBUTES)
24
+ mosaico_attributes = attributes.slice(*COMPOSED_ATTRIBUTES)
25
+
26
+ node = ActionMosaico::HtmlConversion.create_element(TAG_NAME)
27
+ node['data-mosaico-attachment'] = JSON.generate(mosaico_attachment_attributes)
28
+ node['data-mosaico-attributes'] = JSON.generate(mosaico_attributes) if mosaico_attributes.any?
29
+
30
+ new(node)
31
+ end
32
+
33
+ private
34
+
35
+ def process_attributes(attributes)
36
+ typecast_attribute_values(transform_attribute_keys(attributes))
37
+ end
38
+
39
+ def transform_attribute_keys(attributes)
40
+ attributes.transform_keys { |key| key.to_s.underscore.camelize(:lower) }
41
+ end
42
+
43
+ def typecast_attribute_values(attributes)
44
+ attributes.map do |key, value|
45
+ typecast = ATTRIBUTE_TYPES[key] || ATTRIBUTE_TYPES[:default]
46
+ [key, typecast.call(value)]
47
+ end.to_h
48
+ end
49
+ end
50
+
51
+ attr_reader :node
52
+
53
+ def initialize(node)
54
+ @node = node
55
+ end
56
+
57
+ def attributes
58
+ @attributes ||= attachment_attributes.merge(composed_attributes).slice(*ATTRIBUTES)
59
+ end
60
+
61
+ def to_html
62
+ ActionMosaico::HtmlConversion.node_to_html(node)
63
+ end
64
+
65
+ def to_s
66
+ to_html
67
+ end
68
+
69
+ private
70
+
71
+ def attachment_attributes
72
+ read_json_object_attribute('data-mosaico-attachment')
73
+ end
74
+
75
+ def composed_attributes
76
+ read_json_object_attribute('data-mosaico-attributes')
77
+ end
78
+
79
+ def read_json_object_attribute(name)
80
+ read_json_attribute(name) || {}
81
+ end
82
+
83
+ def read_json_attribute(name)
84
+ if value = node[name]
85
+ begin
86
+ JSON.parse(value)
87
+ rescue StandardError => e
88
+ Rails.logger.error "[#{self.class.name}] Couldn't parse JSON #{value} from NODE #{node.inspect}"
89
+ Rails.logger.error "[#{self.class.name}] Failed with #{e.class}: #{e.backtrace}"
90
+ nil
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMosaico
4
+ module PlainTextConversion
5
+ extend self
6
+
7
+ def node_to_plain_text(node)
8
+ remove_trailing_newlines(plain_text_for_node(node))
9
+ end
10
+
11
+ private
12
+
13
+ def plain_text_for_node(node, index = 0)
14
+ if respond_to?(plain_text_method_for_node(node), true)
15
+ send(plain_text_method_for_node(node), node, index)
16
+ else
17
+ plain_text_for_node_children(node)
18
+ end
19
+ end
20
+
21
+ def plain_text_for_node_children(node)
22
+ texts = []
23
+ node.children.each_with_index do |child, index|
24
+ texts << plain_text_for_node(child, index)
25
+ end
26
+ texts.join
27
+ end
28
+
29
+ def plain_text_method_for_node(node)
30
+ :"plain_text_for_#{node.name}_node"
31
+ end
32
+
33
+ def plain_text_for_block(node, _index = 0)
34
+ "#{remove_trailing_newlines(plain_text_for_node_children(node))}\n\n"
35
+ end
36
+
37
+ %i[h1 p ul ol].each do |element|
38
+ alias_method :"plain_text_for_#{element}_node", :plain_text_for_block
39
+ end
40
+
41
+ def plain_text_for_br_node(_node, _index)
42
+ "\n"
43
+ end
44
+
45
+ def plain_text_for_text_node(node, _index)
46
+ remove_trailing_newlines(node.text)
47
+ end
48
+
49
+ def plain_text_for_div_node(node, _index)
50
+ "#{remove_trailing_newlines(plain_text_for_node_children(node))}\n"
51
+ end
52
+
53
+ def plain_text_for_figcaption_node(node, _index)
54
+ "[#{remove_trailing_newlines(plain_text_for_node_children(node))}]"
55
+ end
56
+
57
+ def plain_text_for_blockquote_node(node, _index)
58
+ text = plain_text_for_block(node)
59
+ text.sub(/\A(\s*)(.+?)(\s*)\Z/m, '\1“\2”\3')
60
+ end
61
+
62
+ def plain_text_for_li_node(node, index)
63
+ bullet = bullet_for_li_node(node, index)
64
+ text = remove_trailing_newlines(plain_text_for_node_children(node))
65
+ "#{bullet} #{text}\n"
66
+ end
67
+
68
+ def remove_trailing_newlines(text)
69
+ text.chomp('')
70
+ end
71
+
72
+ def bullet_for_li_node(node, index)
73
+ if list_node_name_for_li_node(node) == 'ol'
74
+ "#{index + 1}."
75
+ else
76
+ '•'
77
+ end
78
+ end
79
+
80
+ def list_node_name_for_li_node(node)
81
+ node.ancestors.lazy.map(&:name).grep(/^[uo]l$/).first
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+ require 'active_support/core_ext/module/attribute_accessors_per_thread'
5
+
6
+ module ActionMosaico
7
+ module Rendering # :nodoc:
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ cattr_accessor :default_renderer, instance_accessor: false
12
+ thread_cattr_accessor :renderer, instance_accessor: false
13
+ delegate :render, to: :class
14
+ end
15
+
16
+ class_methods do
17
+ def with_renderer(renderer)
18
+ previous_renderer = self.renderer
19
+ self.renderer = renderer
20
+ yield
21
+ ensure
22
+ self.renderer = previous_renderer
23
+ end
24
+
25
+ def render(...)
26
+ (renderer || default_renderer).render_to_string(...)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMosaico
4
+ module Serialization
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ def load(content)
9
+ new(content) if content
10
+ end
11
+
12
+ def dump(content)
13
+ case content
14
+ when nil
15
+ nil
16
+ when self
17
+ content.to_html
18
+ when ActionMosaico::RichText
19
+ content.body.to_html
20
+ else
21
+ new(content).to_html
22
+ end
23
+ end
24
+ end
25
+
26
+ # Marshal compatibility
27
+
28
+ class_methods do
29
+ alias_method :_load, :load
30
+ end
31
+
32
+ def _dump(*)
33
+ self.class.dump(self)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMosaico
4
+ module SystemTestHelper
5
+ # Locates a Trix editor and fills it in with the given HTML.
6
+ #
7
+ # The editor can be found by:
8
+ # * its +id+
9
+ # * its +placeholder+
10
+ # * the text from its +label+ element
11
+ # * its +aria-label+
12
+ # * the +name+ of its input
13
+ #
14
+ # Examples:
15
+ #
16
+ # # <mosaico-editor id="message_content" ...></mosaico-editor>
17
+ # fill_in_rich_text_area "message_content", with: "Hello <em>world!</em>"
18
+ #
19
+ # # <mosaico-editor placeholder="Your message here" ...></mosaico-editor>
20
+ # fill_in_rich_text_area "Your message here", with: "Hello <em>world!</em>"
21
+ #
22
+ # # <label for="message_content">Message content</label>
23
+ # # <mosaico-editor id="message_content" ...></mosaico-editor>
24
+ # fill_in_rich_text_area "Message content", with: "Hello <em>world!</em>"
25
+ #
26
+ # # <mosaico-editor aria-label="Message content" ...></mosaico-editor>
27
+ # fill_in_rich_text_area "Message content", with: "Hello <em>world!</em>"
28
+ #
29
+ # # <input id="mosaico_input_1" name="message[content]" type="hidden">
30
+ # # <mosaico-editor input="mosaico_input_1"></mosaico-editor>
31
+ # fill_in_rich_text_area "message[content]", with: "Hello <em>world!</em>"
32
+ def fill_in_rich_text_area(locator = nil, with:)
33
+ find(:rich_text_area, locator).execute_script('this.editor.loadHTML(arguments[0])', with.to_s)
34
+ end
35
+ end
36
+ end
37
+
38
+ Capybara.add_selector :rich_text_area do
39
+ label 'rich-text area'
40
+ xpath do |locator|
41
+ if locator.nil?
42
+ XPath.descendant(:'mosaico-editor')
43
+ else
44
+ input_located_by_name = XPath.anywhere(:input).where(XPath.attr(:name) == locator).attr(:id)
45
+ input_located_by_label = XPath.anywhere(:label).where(XPath.string.n.is(locator)).attr(:for)
46
+
47
+ XPath.descendant(:'mosaico-editor').where \
48
+ XPath.attr(:id).equals(locator) |
49
+ XPath.attr(:placeholder).equals(locator) |
50
+ XPath.attr(:'aria-label').equals(locator) |
51
+ XPath.attr(:input).equals(input_located_by_name) |
52
+ XPath.attr(:id).equals(input_located_by_label)
53
+ end
54
+ end
55
+ end