emjay 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 (59) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +158 -0
  4. data/lib/emjay/body_component.rb +142 -0
  5. data/lib/emjay/component.rb +61 -0
  6. data/lib/emjay/components/body/mj_accordion.rb +99 -0
  7. data/lib/emjay/components/body/mj_accordion_element.rb +127 -0
  8. data/lib/emjay/components/body/mj_accordion_text.rb +123 -0
  9. data/lib/emjay/components/body/mj_accordion_title.rb +171 -0
  10. data/lib/emjay/components/body/mj_body.rb +70 -0
  11. data/lib/emjay/components/body/mj_button.rb +198 -0
  12. data/lib/emjay/components/body/mj_carousel.rb +410 -0
  13. data/lib/emjay/components/body/mj_carousel_image.rb +188 -0
  14. data/lib/emjay/components/body/mj_column.rb +287 -0
  15. data/lib/emjay/components/body/mj_divider.rb +120 -0
  16. data/lib/emjay/components/body/mj_group.rb +196 -0
  17. data/lib/emjay/components/body/mj_hero.rb +382 -0
  18. data/lib/emjay/components/body/mj_image.rb +188 -0
  19. data/lib/emjay/components/body/mj_navbar.rb +187 -0
  20. data/lib/emjay/components/body/mj_navbar_link.rb +129 -0
  21. data/lib/emjay/components/body/mj_raw.rb +34 -0
  22. data/lib/emjay/components/body/mj_section.rb +442 -0
  23. data/lib/emjay/components/body/mj_social.rb +174 -0
  24. data/lib/emjay/components/body/mj_social_element.rb +272 -0
  25. data/lib/emjay/components/body/mj_spacer.rb +57 -0
  26. data/lib/emjay/components/body/mj_table.rb +113 -0
  27. data/lib/emjay/components/body/mj_text.rb +100 -0
  28. data/lib/emjay/components/body/mj_wrapper.rb +56 -0
  29. data/lib/emjay/components/head/mj_attributes.rb +38 -0
  30. data/lib/emjay/components/head/mj_breakpoint.rb +28 -0
  31. data/lib/emjay/components/head/mj_font.rb +24 -0
  32. data/lib/emjay/components/head/mj_head.rb +20 -0
  33. data/lib/emjay/components/head/mj_html_attributes.rb +33 -0
  34. data/lib/emjay/components/head/mj_preview.rb +24 -0
  35. data/lib/emjay/components/head/mj_style.rb +34 -0
  36. data/lib/emjay/components/head/mj_title.rb +24 -0
  37. data/lib/emjay/global_data.rb +64 -0
  38. data/lib/emjay/head_component.rb +37 -0
  39. data/lib/emjay/helpers/conditional_tag.rb +24 -0
  40. data/lib/emjay/helpers/fonts.rb +34 -0
  41. data/lib/emjay/helpers/gen_random_hex_string.rb +9 -0
  42. data/lib/emjay/helpers/make_lower_breakpoint.rb +17 -0
  43. data/lib/emjay/helpers/media_queries.rb +47 -0
  44. data/lib/emjay/helpers/merge_outlook_conditionals.rb +11 -0
  45. data/lib/emjay/helpers/minify_outlook_conditionals.rb +18 -0
  46. data/lib/emjay/helpers/shorthand_parser.rb +33 -0
  47. data/lib/emjay/helpers/styles.rb +34 -0
  48. data/lib/emjay/helpers/suffix_css_classes.rb +12 -0
  49. data/lib/emjay/helpers/width_parser.rb +26 -0
  50. data/lib/emjay/rails/mail_interceptor.rb +37 -0
  51. data/lib/emjay/rails/template_handler.rb +16 -0
  52. data/lib/emjay/railtie.rb +21 -0
  53. data/lib/emjay/registry.rb +19 -0
  54. data/lib/emjay/renderer.rb +302 -0
  55. data/lib/emjay/skeleton.rb +80 -0
  56. data/lib/emjay/version.rb +5 -0
  57. data/lib/emjay.rb +66 -0
  58. data/llms.txt +130 -0
  59. metadata +129 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1eadb92be994c929ee0f5a480b38ea56de397a2cf6591c3c178e36960c47dbab
4
+ data.tar.gz: 3dc92597d64b1166a9b5301b0eadf88d0078dc47affa308d57c3ac9ce18d2b10
5
+ SHA512:
6
+ metadata.gz: 820dd2348232379489964907f0e29c7ab679a3d3bf450bd9242c815efdc0d56102993894b2ecdf9ad08ec7f3a8d870ce1b9d916b73f8b363217c7ad4bcdd57cd
7
+ data.tar.gz: 6105020243889ce2517b3b786bfd50607cccda2a7fdecfb3515f58ca61560cae1608b35ff03c36ce74ba172142d3ea46791c59b56cce3ace716c324d0c5df12c
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Julik Tarkhanov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # emjay
2
+
3
+ A pure-Ruby implementation of [MJML](https://mjml.io), the email markup language. Converts MJML templates to responsive HTML emails — no Node.js, no native extensions, no shelling out.
4
+
5
+ ## Feature parity with MJML
6
+
7
+ emjay implements the full set of standard MJML components:
8
+
9
+ **Head components:** `mj-head`, `mj-attributes`, `mj-style` (including `inline`), `mj-font`, `mj-title`, `mj-preview`, `mj-breakpoint`, `mj-html-attributes`
10
+
11
+ **Body components:** `mj-body`, `mj-section`, `mj-wrapper`, `mj-column`, `mj-group`, `mj-text`, `mj-image`, `mj-button`, `mj-divider`, `mj-spacer`, `mj-table`, `mj-raw`, `mj-hero`, `mj-social` / `mj-social-element`, `mj-navbar` / `mj-navbar-link`, `mj-accordion` / `mj-accordion-element` / `mj-accordion-title` / `mj-accordion-text`, `mj-carousel` / `mj-carousel-image`
12
+
13
+ **Features:** CSS inlining via `<mj-style inline="inline">`, global default attributes, `mj-class`, `mj-html-attributes` with CSS selectors, custom fonts, responsive breakpoints, Outlook conditionals, `lang`/`dir` attributes.
14
+
15
+ All components and features from the [MJML documentation](https://documentation.mjml.io/) should work as described. The [MJML templates and examples](https://mjml.io/templates) are a good starting point for building your own emails. Output is tested against the upstream [MJML 4 JavaScript implementation](https://github.com/mjmlio/mjml) using fixture-based comparison and backported behavioral tests. If you find a case where emjay produces different output from the reference MJML implementation, please [open an issue](https://github.com/julik/emjay/issues).
16
+
17
+ ## Installation
18
+
19
+ Add to your Gemfile:
20
+
21
+ ```ruby
22
+ gem "emjay"
23
+ ```
24
+
25
+ Runtime dependencies: `nokogiri` for XML/HTML parsing, `premailer` for CSS inlining (`<mj-style inline="inline">`).
26
+
27
+ ## Usage
28
+
29
+ ### Standalone
30
+
31
+ ```ruby
32
+ require "emjay"
33
+
34
+ mjml = <<~MJML
35
+ <mjml>
36
+ <mj-body>
37
+ <mj-section>
38
+ <mj-column>
39
+ <mj-text>Hello World</mj-text>
40
+ </mj-column>
41
+ </mj-section>
42
+ </mj-body>
43
+ </mjml>
44
+ MJML
45
+
46
+ html = Emjay.to_html(mjml)
47
+ ```
48
+
49
+ ### Rails
50
+
51
+ emjay includes a Railtie that registers an ActionView template handler automatically. Create templates with the `.html.mjml` extension:
52
+
53
+ ```
54
+ app/views/user_mailer/welcome.html.mjml
55
+ ```
56
+
57
+ ERB tags work inside `.mjml` templates — the handler always chains through ERB before compiling MJML:
58
+
59
+ ```erb
60
+ <mjml>
61
+ <mj-body>
62
+ <mj-section>
63
+ <mj-column>
64
+ <mj-text>Welcome, <%= @user.name %>!</mj-text>
65
+ </mj-column>
66
+ </mj-section>
67
+ </mj-body>
68
+ </mjml>
69
+ ```
70
+
71
+ Your mailer needs no special setup:
72
+
73
+ ```ruby
74
+ class UserMailer < ApplicationMailer
75
+ def welcome(user)
76
+ @user = user
77
+ mail(to: @user.email)
78
+ end
79
+ end
80
+ ```
81
+
82
+ Rails resolves the template automatically: `.mjml` selects the handler, `.html` sets the MIME type. If you also provide `welcome.text.erb`, ActionMailer sends a multipart email with both HTML and plain text parts.
83
+
84
+ ### Layouts
85
+
86
+ MJML layouts work just like regular Rails mailer layouts. Create a layout that wraps `yield` with the MJML boilerplate:
87
+
88
+ ```erb
89
+ <%# app/views/layouts/mailer.html.mjml %>
90
+ <mjml>
91
+ <mj-head>
92
+ <mj-attributes>
93
+ <mj-all font-family="Helvetica, Arial, sans-serif" />
94
+ </mj-attributes>
95
+ </mj-head>
96
+ <mj-body>
97
+ <%= yield %>
98
+ </mj-body>
99
+ </mjml>
100
+ ```
101
+
102
+ Then your templates contain only the content sections:
103
+
104
+ ```erb
105
+ <%# app/views/user_mailer/welcome.html.mjml %>
106
+ <mj-section>
107
+ <mj-column>
108
+ <mj-text>Welcome, <%= @user.name %>!</mj-text>
109
+ </mj-column>
110
+ </mj-section>
111
+ ```
112
+
113
+ Set the layout in your mailer as usual:
114
+
115
+ ```ruby
116
+ class UserMailer < ApplicationMailer
117
+ layout "mailer"
118
+ end
119
+ ```
120
+
121
+ ### Partials
122
+
123
+ MJML partials work with the standard `render` helper. Use them to share common sections across emails:
124
+
125
+ ```erb
126
+ <%# app/views/shared/_header.html.mjml %>
127
+ <mj-section background-color="#f0f0f0">
128
+ <mj-column>
129
+ <mj-image src="<%= @logo_url %>" width="150px" />
130
+ </mj-column>
131
+ </mj-section>
132
+ ```
133
+
134
+ ```erb
135
+ <%# app/views/user_mailer/welcome.html.mjml %>
136
+ <%= render "shared/header" %>
137
+ <mj-section>
138
+ <mj-column>
139
+ <mj-text>Welcome!</mj-text>
140
+ </mj-column>
141
+ </mj-section>
142
+ ```
143
+
144
+ ### How it works
145
+
146
+ The `.mjml` template handler is an ERB passthrough — it does not compile MJML itself. Instead, an ActionMailer interceptor (`Emjay::Rails::MailInterceptor`) compiles the assembled MJML to HTML after Rails has applied layouts and partials. This means `yield` in a layout receives MJML (not HTML), and the full document is compiled in one pass.
147
+
148
+ ## Other Ruby MJML implementations
149
+
150
+ - [mjml-rails](https://github.com/sighmon/mjml-rails) — Rails integration that shells out to the MJML Node.js binary. Requires Node.js at runtime.
151
+ - [mjml-ruby](https://github.com/kolybasov/mjml-ruby) — Ruby wrapper around the MJML Node.js parser. Also requires Node.js.
152
+ - [mrml-ruby](https://github.com/jdrouet/mrml/tree/main/packages/mrml-ruby) — Ruby bindings to [MRML](https://github.com/jdrouet/mrml), a Rust reimplementation of MJML. Requires a compiled native extension.
153
+
154
+ emjay differs from all of the above by being pure Ruby — it has no dependency on Node.js or native extensions.
155
+
156
+ ## License
157
+
158
+ MIT
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "component"
4
+ require_relative "helpers/shorthand_parser"
5
+ require_relative "helpers/width_parser"
6
+
7
+ module Emjay
8
+ # Base class for body components. Port of JS BodyComponent from createComponent.js.
9
+ class BodyComponent < Component
10
+ def get_styles
11
+ {}
12
+ end
13
+
14
+ # Converts a style hash (or style name string) to an inline CSS string.
15
+ # Supports dot-notation for nested style lookups (e.g., "carousel.div").
16
+ def styles(name_or_hash)
17
+ styles_object = if name_or_hash.is_a?(String)
18
+ if name_or_hash.include?(".")
19
+ parts = name_or_hash.split(".")
20
+ result = get_styles
21
+ parts.each { |part| result = result&.dig(part.to_sym) || result&.dig(part) }
22
+ result
23
+ else
24
+ get_styles[name_or_hash.to_sym] || get_styles[name_or_hash]
25
+ end
26
+ elsif name_or_hash.is_a?(Symbol)
27
+ get_styles[name_or_hash]
28
+ else
29
+ name_or_hash
30
+ end
31
+
32
+ return "" unless styles_object
33
+
34
+ styles_object.each_with_object(+"") do |(name, value), output|
35
+ output << "#{name}:#{value};" unless value.nil?
36
+ end
37
+ end
38
+
39
+ # Builds an HTML attribute string. The `style:` key auto-resolves through #styles.
40
+ def html_attributes(attrs)
41
+ attrs.each_with_object(+"") do |(name, value), output|
42
+ next if value.nil?
43
+ resolved = if name.to_s == "style"
44
+ styles(value)
45
+ else
46
+ value
47
+ end
48
+ output << " #{name}=\"#{resolved}\""
49
+ end
50
+ end
51
+
52
+ def get_shorthand_attr_value(attribute, direction)
53
+ dir_attr = get_attribute("#{attribute}-#{direction}")
54
+ return dir_attr.to_i if dir_attr
55
+
56
+ base_attr = get_attribute(attribute)
57
+ return 0 unless base_attr
58
+
59
+ ShorthandParser.call(base_attr, direction)
60
+ end
61
+
62
+ def get_shorthand_border_value(direction, attribute = "border")
63
+ border_direction = direction && get_attribute("#{attribute}-#{direction}")
64
+ border = get_attribute(attribute)
65
+ BorderParser.call(border_direction || border || "0")
66
+ end
67
+
68
+ def get_box_widths
69
+ container_width = @context[:container_width]
70
+ parsed_width = container_width.to_i
71
+
72
+ paddings = get_shorthand_attr_value("padding", "right") +
73
+ get_shorthand_attr_value("padding", "left")
74
+
75
+ borders = get_shorthand_border_value("right") +
76
+ get_shorthand_border_value("left")
77
+
78
+ {
79
+ total_width: parsed_width,
80
+ borders: borders,
81
+ paddings: paddings,
82
+ box: parsed_width - paddings - borders
83
+ }
84
+ end
85
+
86
+ def get_child_context
87
+ @context
88
+ end
89
+
90
+ # Renders child components. Supports renderer: lambda, attributes: merge, raw_xml: mode.
91
+ def render_children(children = nil, opts = {})
92
+ renderer = opts[:renderer] || ->(component) { component.render }
93
+ extra_attributes = opts[:attributes] || {}
94
+ extra_props = opts[:props] || {}
95
+
96
+ children = children || @props[:children] || []
97
+
98
+ sibling = children.length
99
+ components = @context[:components] || {}
100
+
101
+ raw_components = components.values.select { |c| c.raw_element? }
102
+ non_raw_siblings = children.count { |child|
103
+ !raw_components.any? { |c| c.component_name == child[:tag_name] }
104
+ }
105
+
106
+ output = +""
107
+ children.each_with_index do |child, index|
108
+ component_class = components[child[:tag_name]]
109
+ next unless component_class
110
+
111
+ child_data = {
112
+ attributes: extra_attributes.merge(child[:attributes] || {}),
113
+ children: child[:children] || [],
114
+ content: child[:content] || "",
115
+ context: get_child_context,
116
+ global_attributes: child[:global_attributes] || {},
117
+ raw_attrs: child[:raw_attrs] || {},
118
+ props: extra_props.merge(
119
+ first: index == 0,
120
+ index: index,
121
+ last: index + 1 == sibling,
122
+ sibling: sibling,
123
+ non_raw_siblings: non_raw_siblings
124
+ )
125
+ }
126
+
127
+ component = component_class.new(child_data)
128
+
129
+ if component.respond_to?(:head_style)
130
+ @context[:add_head_style]&.call(child[:tag_name], component.method(:head_style))
131
+ end
132
+ if component.respond_to?(:component_head_style)
133
+ @context[:add_component_head_style]&.call(component.method(:component_head_style))
134
+ end
135
+
136
+ output << renderer.call(component)
137
+ end
138
+
139
+ output
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emjay
4
+ # Base class for all MJML components. Port of JS Component class from createComponent.js.
5
+ # Takes parsed node data (attributes, children, content) + context hash.
6
+ class Component
7
+ attr_reader :attributes, :props, :context
8
+
9
+ class << self
10
+ def component_name
11
+ raise NotImplementedError, "#{name} must define self.component_name"
12
+ end
13
+
14
+ def ending_tag?
15
+ false
16
+ end
17
+
18
+ def raw_element?
19
+ false
20
+ end
21
+
22
+ def default_attributes
23
+ {}
24
+ end
25
+
26
+ def allowed_attributes
27
+ {}
28
+ end
29
+ end
30
+
31
+ # initialDatas in JS: { attributes, children, content, context, props, globalAttributes, rawAttrs }
32
+ def initialize(initial_data = {})
33
+ attrs = initial_data[:attributes] || {}
34
+ @children = initial_data[:children] || []
35
+ @content = initial_data[:content] || ""
36
+ @context = initial_data[:context] || {}
37
+ @props = initial_data[:props] || {}
38
+ @props[:children] = @children
39
+ @props[:content] = @content
40
+ @props[:raw_attrs] = initial_data[:raw_attrs] || {}
41
+ global_attributes = initial_data[:global_attributes] || {}
42
+
43
+ # Attribute merging: defaults -> global -> element
44
+ @attributes = self.class.default_attributes
45
+ .merge(global_attributes)
46
+ .merge(attrs)
47
+ end
48
+
49
+ def get_child_context
50
+ @context
51
+ end
52
+
53
+ def get_attribute(name)
54
+ @attributes[name]
55
+ end
56
+
57
+ def get_content
58
+ (@content || "").strip
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../body_component"
4
+ require_relative "../../registry"
5
+
6
+ module Emjay
7
+ module Components
8
+ class MjAccordion < BodyComponent
9
+ def self.component_name
10
+ "mj-accordion"
11
+ end
12
+
13
+ def self.default_attributes
14
+ {
15
+ "border" => "2px solid black",
16
+ "font-family" => "Ubuntu, Helvetica, Arial, sans-serif",
17
+ "icon-align" => "middle",
18
+ "icon-wrapped-url" => "https://i.imgur.com/bIXv1bk.png",
19
+ "icon-wrapped-alt" => "+",
20
+ "icon-unwrapped-url" => "https://i.imgur.com/w4uTygT.png",
21
+ "icon-unwrapped-alt" => "-",
22
+ "icon-position" => "right",
23
+ "icon-height" => "32px",
24
+ "icon-width" => "32px",
25
+ "padding" => "10px 25px"
26
+ }
27
+ end
28
+
29
+ def self.allowed_attributes
30
+ {
31
+ "container-background-color" => "color",
32
+ "border" => "string",
33
+ "font-family" => "string",
34
+ "icon-align" => "enum(top,middle,bottom)",
35
+ "icon-width" => "unit(px,%)",
36
+ "icon-height" => "unit(px,%)",
37
+ "icon-wrapped-url" => "string",
38
+ "icon-wrapped-alt" => "string",
39
+ "icon-unwrapped-url" => "string",
40
+ "icon-unwrapped-alt" => "string",
41
+ "icon-position" => "enum(left,right)",
42
+ "padding-bottom" => "unit(px,%)",
43
+ "padding-left" => "unit(px,%)",
44
+ "padding-right" => "unit(px,%)",
45
+ "padding-top" => "unit(px,%)",
46
+ "padding" => "unit(px,%){1,4}"
47
+ }
48
+ end
49
+
50
+ def head_style(_breakpoint = nil)
51
+ "\n noinput.mj-accordion-checkbox { display:block!important; }\n\n @media yahoo, only screen and (min-width:0) {\n .mj-accordion-element { display:block; }\n .mj-accordion-checkbox[type=\"checkbox\"], .mj-accordion-less { display:none!important; }\n .mj-accordion-checkbox[type=\"checkbox\"] + * .mj-accordion-title { cursor:pointer; touch-action:manipulation; -webkit-user-select:none; -moz-user-select:none; user-select:none; }\n .mj-accordion-checkbox[type=\"checkbox\"] + * .mj-accordion-content { overflow:hidden; display:none; }\n .mj-accordion-checkbox[type=\"checkbox\"] + * .mj-accordion-more { display:block!important; }\n .mj-accordion-checkbox:checked + * .mj-accordion-content { display:block; }\n .mj-accordion-checkbox:checked + * .mj-accordion-more { display:none!important; }\n .mj-accordion-checkbox:checked + * .mj-accordion-less { display:block!important; }\n }\n\n .moz-text-html input.mj-accordion-checkbox + * .mj-accordion-title { cursor: auto; touch-action: auto; -webkit-user-select: auto; -moz-user-select: auto; user-select: auto; }\n .moz-text-html input.mj-accordion-checkbox + * .mj-accordion-content { overflow: hidden; display: block; }\n .moz-text-html input.mj-accordion-checkbox + * .mj-accordion-ico { display: none; }\n\n @goodbye { @gmail }\n "
52
+ end
53
+
54
+ def get_styles
55
+ {
56
+ table: {
57
+ "width" => "100%",
58
+ "border-collapse" => "collapse",
59
+ "border" => get_attribute("border"),
60
+ "border-bottom" => "none",
61
+ "font-family" => get_attribute("font-family")
62
+ }
63
+ }
64
+ end
65
+
66
+ def get_child_context
67
+ @context.merge(accordionFontFamily: get_attribute("font-family"))
68
+ end
69
+
70
+ def render
71
+ children = @props[:children] || []
72
+
73
+ children_attr = %w[border icon-align icon-width icon-height icon-position
74
+ icon-wrapped-url icon-wrapped-alt icon-unwrapped-url icon-unwrapped-alt].each_with_object({}) do |attr, hash|
75
+ hash[attr] = get_attribute(attr)
76
+ end
77
+
78
+ table_attrs = html_attributes(
79
+ cellspacing: "0",
80
+ cellpadding: "0",
81
+ class: "mj-accordion",
82
+ style: :table
83
+ )
84
+
85
+ <<~HTML
86
+ <table
87
+ #{table_attrs}
88
+ >
89
+ <tbody>
90
+ #{render_children(children, attributes: children_attr)}
91
+ </tbody>
92
+ </table>
93
+ HTML
94
+ end
95
+ end
96
+ end
97
+
98
+ Registry.register(Components::MjAccordion)
99
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../body_component"
4
+ require_relative "../../registry"
5
+ require_relative "../../helpers/conditional_tag"
6
+
7
+ module Emjay
8
+ module Components
9
+ class MjAccordionElement < BodyComponent
10
+ def self.component_name
11
+ "mj-accordion-element"
12
+ end
13
+
14
+ def self.default_attributes
15
+ {}
16
+ end
17
+
18
+ def self.allowed_attributes
19
+ {
20
+ "background-color" => "color",
21
+ "border" => "string",
22
+ "font-family" => "string",
23
+ "icon-align" => "enum(top,middle,bottom)",
24
+ "icon-width" => "unit(px,%)",
25
+ "icon-height" => "unit(px,%)",
26
+ "icon-wrapped-url" => "string",
27
+ "icon-wrapped-alt" => "string",
28
+ "icon-unwrapped-url" => "string",
29
+ "icon-unwrapped-alt" => "string",
30
+ "icon-position" => "enum(left,right)"
31
+ }
32
+ end
33
+
34
+ def get_styles
35
+ {
36
+ td: {
37
+ "padding" => "0px",
38
+ "background-color" => get_attribute("background-color")
39
+ },
40
+ label: {
41
+ "font-size" => "13px",
42
+ "font-family" => get_attribute("font-family")
43
+ },
44
+ input: {
45
+ "display" => "none"
46
+ }
47
+ }
48
+ end
49
+
50
+ def get_child_context
51
+ @context.merge(elementFontFamily: get_attribute("font-family"))
52
+ end
53
+
54
+ def render
55
+ tr_attrs = html_attributes(class: get_attribute("css-class"))
56
+ td_attrs = html_attributes(style: :td)
57
+ label_attrs = html_attributes(
58
+ class: "mj-accordion-element",
59
+ style: :label
60
+ )
61
+ input_attrs = html_attributes(
62
+ class: "mj-accordion-checkbox",
63
+ type: "checkbox",
64
+ style: :input
65
+ )
66
+
67
+ input_html = ConditionalTag.conditional_tag(
68
+ "<input#{input_attrs} />",
69
+ negation: true
70
+ )
71
+
72
+ <<~HTML
73
+ <tr
74
+ #{tr_attrs}
75
+ >
76
+ <td#{td_attrs}>
77
+ <label
78
+ #{label_attrs}
79
+ >
80
+ #{input_html}
81
+ <div>
82
+ #{handle_missing_children}
83
+ </div>
84
+ </label>
85
+ </td>
86
+ </tr>
87
+ HTML
88
+ end
89
+
90
+ private
91
+
92
+ def handle_missing_children
93
+ children = @props[:children] || []
94
+ children_attr = %w[border icon-align icon-width icon-height icon-position
95
+ icon-wrapped-url icon-wrapped-alt icon-unwrapped-url icon-unwrapped-alt].each_with_object({}) do |attr, hash|
96
+ hash[attr] = get_attribute(attr)
97
+ end
98
+
99
+ result = []
100
+
101
+ has_title = children.any? { |c| c[:tag_name] == "mj-accordion-title" }
102
+ unless has_title
103
+ title_component = Components::MjAccordionTitle.new(
104
+ attributes: children_attr,
105
+ context: get_child_context
106
+ )
107
+ result << title_component.render
108
+ end
109
+
110
+ result << render_children(children, attributes: children_attr)
111
+
112
+ has_text = children.any? { |c| c[:tag_name] == "mj-accordion-text" }
113
+ unless has_text
114
+ text_component = Components::MjAccordionText.new(
115
+ attributes: children_attr,
116
+ context: get_child_context
117
+ )
118
+ result << text_component.render
119
+ end
120
+
121
+ result.join("\n")
122
+ end
123
+ end
124
+ end
125
+
126
+ Registry.register(Components::MjAccordionElement)
127
+ end