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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +158 -0
- data/lib/emjay/body_component.rb +142 -0
- data/lib/emjay/component.rb +61 -0
- data/lib/emjay/components/body/mj_accordion.rb +99 -0
- data/lib/emjay/components/body/mj_accordion_element.rb +127 -0
- data/lib/emjay/components/body/mj_accordion_text.rb +123 -0
- data/lib/emjay/components/body/mj_accordion_title.rb +171 -0
- data/lib/emjay/components/body/mj_body.rb +70 -0
- data/lib/emjay/components/body/mj_button.rb +198 -0
- data/lib/emjay/components/body/mj_carousel.rb +410 -0
- data/lib/emjay/components/body/mj_carousel_image.rb +188 -0
- data/lib/emjay/components/body/mj_column.rb +287 -0
- data/lib/emjay/components/body/mj_divider.rb +120 -0
- data/lib/emjay/components/body/mj_group.rb +196 -0
- data/lib/emjay/components/body/mj_hero.rb +382 -0
- data/lib/emjay/components/body/mj_image.rb +188 -0
- data/lib/emjay/components/body/mj_navbar.rb +187 -0
- data/lib/emjay/components/body/mj_navbar_link.rb +129 -0
- data/lib/emjay/components/body/mj_raw.rb +34 -0
- data/lib/emjay/components/body/mj_section.rb +442 -0
- data/lib/emjay/components/body/mj_social.rb +174 -0
- data/lib/emjay/components/body/mj_social_element.rb +272 -0
- data/lib/emjay/components/body/mj_spacer.rb +57 -0
- data/lib/emjay/components/body/mj_table.rb +113 -0
- data/lib/emjay/components/body/mj_text.rb +100 -0
- data/lib/emjay/components/body/mj_wrapper.rb +56 -0
- data/lib/emjay/components/head/mj_attributes.rb +38 -0
- data/lib/emjay/components/head/mj_breakpoint.rb +28 -0
- data/lib/emjay/components/head/mj_font.rb +24 -0
- data/lib/emjay/components/head/mj_head.rb +20 -0
- data/lib/emjay/components/head/mj_html_attributes.rb +33 -0
- data/lib/emjay/components/head/mj_preview.rb +24 -0
- data/lib/emjay/components/head/mj_style.rb +34 -0
- data/lib/emjay/components/head/mj_title.rb +24 -0
- data/lib/emjay/global_data.rb +64 -0
- data/lib/emjay/head_component.rb +37 -0
- data/lib/emjay/helpers/conditional_tag.rb +24 -0
- data/lib/emjay/helpers/fonts.rb +34 -0
- data/lib/emjay/helpers/gen_random_hex_string.rb +9 -0
- data/lib/emjay/helpers/make_lower_breakpoint.rb +17 -0
- data/lib/emjay/helpers/media_queries.rb +47 -0
- data/lib/emjay/helpers/merge_outlook_conditionals.rb +11 -0
- data/lib/emjay/helpers/minify_outlook_conditionals.rb +18 -0
- data/lib/emjay/helpers/shorthand_parser.rb +33 -0
- data/lib/emjay/helpers/styles.rb +34 -0
- data/lib/emjay/helpers/suffix_css_classes.rb +12 -0
- data/lib/emjay/helpers/width_parser.rb +26 -0
- data/lib/emjay/rails/mail_interceptor.rb +37 -0
- data/lib/emjay/rails/template_handler.rb +16 -0
- data/lib/emjay/railtie.rb +21 -0
- data/lib/emjay/registry.rb +19 -0
- data/lib/emjay/renderer.rb +302 -0
- data/lib/emjay/skeleton.rb +80 -0
- data/lib/emjay/version.rb +5 -0
- data/lib/emjay.rb +66 -0
- data/llms.txt +130 -0
- 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
|