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.
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