mjml-rb 0.2.12 → 0.2.13
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 +4 -4
- data/README.md +2 -2
- data/lib/mjml-rb/components/carousel.rb +449 -0
- data/lib/mjml-rb/components/carousel_image.rb +162 -0
- data/lib/mjml-rb/renderer.rb +4 -0
- data/lib/mjml-rb/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9c54cb0ddf9071b81422853dfcca7e3748ed404b7ea1afba2a99ce9a42229d01
|
|
4
|
+
data.tar.gz: 7d2469230fa62b2dd52df43e3e8858f1a38aeb5964469af5ec8669182664e9f8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8e349fdbec211923111e8ce2f7a593f46f18308aef3461c531dc3fa6ccfdf97d024c8b181e0289c0b8819db272d49fd3c45c2130298373a8867f6f8f19f14916
|
|
7
|
+
data.tar.gz: 9603224d7e49fc263441767a0d3a289bd5bbba4cdf93afebc07b1dd7dd0416f8cd5e0161843d50d31e2f21b6390be8a946569027c52f19ab4d6605442544507b
|
data/README.md
CHANGED
|
@@ -69,8 +69,8 @@ The table below tracks current JS-to-Ruby migration status for MJML components i
|
|
|
69
69
|
| `mj-preview` | migrated | Implemented in `head.rb`. |
|
|
70
70
|
| `mj-style` | migrated | Implemented in `head.rb`, including inline-style registration. |
|
|
71
71
|
| `mj-font` | migrated | Implemented in `head.rb`. |
|
|
72
|
-
| `mj-carousel` |
|
|
73
|
-
| `mj-carousel-image` |
|
|
72
|
+
| `mj-carousel` | migrated | Implemented in `carousel.rb`, including per-instance radio/thumbnail CSS, Outlook fallback rendering, and thumbnail/control output. |
|
|
73
|
+
| `mj-carousel-image` | migrated | Implemented in `carousel_image.rb`, including radio, thumbnail, and main image rendering helpers used by `mj-carousel`. |
|
|
74
74
|
| `mj-breakpoint` | migrated | Supported in `mj-head` and used to control desktop column media-query widths. |
|
|
75
75
|
| `mj-html-attributes` | migrated | Supported in `mj-head` and applied to the rendered HTML via CSS selectors. |
|
|
76
76
|
| `mj-selector` | migrated | Supported as the selector container for `mj-html-attribute` rules. |
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module MjmlRb
|
|
6
|
+
module Components
|
|
7
|
+
class Carousel < Base
|
|
8
|
+
TAGS = ["mj-carousel"].freeze
|
|
9
|
+
|
|
10
|
+
ALLOWED_ATTRIBUTES = {
|
|
11
|
+
"align" => "enum(left,center,right)",
|
|
12
|
+
"border-radius" => "unit(px,%){1,4}",
|
|
13
|
+
"container-background-color" => "color",
|
|
14
|
+
"icon-width" => "unit(px,%)",
|
|
15
|
+
"left-icon" => "string",
|
|
16
|
+
"padding" => "unit(px,%){1,4}",
|
|
17
|
+
"padding-top" => "unit(px,%)",
|
|
18
|
+
"padding-bottom" => "unit(px,%)",
|
|
19
|
+
"padding-left" => "unit(px,%)",
|
|
20
|
+
"padding-right" => "unit(px,%)",
|
|
21
|
+
"right-icon" => "string",
|
|
22
|
+
"thumbnails" => "enum(visible,hidden,supported)",
|
|
23
|
+
"tb-border" => "string",
|
|
24
|
+
"tb-border-radius" => "unit(px,%)",
|
|
25
|
+
"tb-hover-border-color" => "color",
|
|
26
|
+
"tb-selected-border-color" => "color",
|
|
27
|
+
"tb-width" => "unit(px,%)"
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
DEFAULT_ATTRIBUTES = {
|
|
31
|
+
"align" => "center",
|
|
32
|
+
"border-radius" => "6px",
|
|
33
|
+
"icon-width" => "44px",
|
|
34
|
+
"left-icon" => "https://i.imgur.com/xTh3hln.png",
|
|
35
|
+
"right-icon" => "https://i.imgur.com/os7o9kz.png",
|
|
36
|
+
"thumbnails" => "visible",
|
|
37
|
+
"tb-border" => "2px solid transparent",
|
|
38
|
+
"tb-border-radius" => "6px",
|
|
39
|
+
"tb-hover-border-color" => "#fead0d",
|
|
40
|
+
"tb-selected-border-color" => "#ccc"
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
def tags
|
|
44
|
+
TAGS
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def render(tag_name:, node:, context:, attrs:, parent:)
|
|
48
|
+
a = DEFAULT_ATTRIBUTES.merge(attrs)
|
|
49
|
+
children = carousel_images(node)
|
|
50
|
+
return "" if children.empty?
|
|
51
|
+
|
|
52
|
+
carousel_id = SecureRandom.hex(8)
|
|
53
|
+
context[:head_styles] << component_head_style(carousel_id, children.length, a)
|
|
54
|
+
|
|
55
|
+
outer_td_attrs = {
|
|
56
|
+
"align" => a["align"],
|
|
57
|
+
"class" => a["css-class"],
|
|
58
|
+
"style" => style_join(
|
|
59
|
+
"background" => a["container-background-color"],
|
|
60
|
+
"font-size" => "0px",
|
|
61
|
+
"padding" => a["padding"],
|
|
62
|
+
"padding-top" => a["padding-top"],
|
|
63
|
+
"padding-right" => a["padding-right"],
|
|
64
|
+
"padding-bottom" => a["padding-bottom"],
|
|
65
|
+
"padding-left" => a["padding-left"],
|
|
66
|
+
"word-break" => "break-word"
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
inner_container_width = content_width(context[:container_width], a)
|
|
71
|
+
|
|
72
|
+
content = <<~HTML
|
|
73
|
+
#{mso_conditional_tag(interactive_markup(node, children, context, a, carousel_id, inner_container_width), true)}
|
|
74
|
+
#{render_fallback(node, children.first, context, a, inner_container_width)}
|
|
75
|
+
HTML
|
|
76
|
+
|
|
77
|
+
%(<tr><td#{html_attrs(outer_td_attrs)}>#{content}</td></tr>)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def interactive_markup(node, children, context, attrs, carousel_id, inner_container_width)
|
|
83
|
+
carousel_classes = ["mj-carousel", attrs["css-class"]].compact.reject(&:empty?).join(" ")
|
|
84
|
+
|
|
85
|
+
<<~HTML
|
|
86
|
+
<div#{html_attrs("class" => carousel_classes)}>
|
|
87
|
+
#{generate_radios(children, carousel_id)}
|
|
88
|
+
<div#{html_attrs(
|
|
89
|
+
"class" => "mj-carousel-content mj-carousel-#{carousel_id}-content",
|
|
90
|
+
"style" => style_join(
|
|
91
|
+
"display" => "table",
|
|
92
|
+
"width" => "100%",
|
|
93
|
+
"table-layout" => "fixed",
|
|
94
|
+
"text-align" => "center",
|
|
95
|
+
"font-size" => "0px"
|
|
96
|
+
)
|
|
97
|
+
)}>
|
|
98
|
+
#{generate_thumbnails(node, children, context, attrs, carousel_id, inner_container_width)}
|
|
99
|
+
#{generate_carousel(node, children, context, attrs, carousel_id, inner_container_width)}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
HTML
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def generate_radios(children, carousel_id)
|
|
106
|
+
image_component = carousel_image_component
|
|
107
|
+
children.each_with_index.map do |child, index|
|
|
108
|
+
image_component.render_radio(index: index, carousel_id: carousel_id)
|
|
109
|
+
end.join
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def generate_thumbnails(node, children, context, attrs, carousel_id, inner_container_width)
|
|
113
|
+
thumbnails = attrs["thumbnails"]
|
|
114
|
+
return "" unless %w[visible supported].include?(thumbnails)
|
|
115
|
+
|
|
116
|
+
image_component = carousel_image_component
|
|
117
|
+
tb_width = thumbnail_width(attrs, children.length, inner_container_width)
|
|
118
|
+
|
|
119
|
+
with_inherited_mj_class(context, node) do
|
|
120
|
+
children.each_with_index.map do |child, index|
|
|
121
|
+
child_attrs = child_pass_through_attributes(child, context, attrs, tb_width)
|
|
122
|
+
image_component.render_thumbnail(
|
|
123
|
+
child,
|
|
124
|
+
attrs: child_attrs,
|
|
125
|
+
index: index,
|
|
126
|
+
carousel_id: carousel_id,
|
|
127
|
+
thumbnails: thumbnails,
|
|
128
|
+
tb_width: tb_width
|
|
129
|
+
)
|
|
130
|
+
end.join
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def generate_carousel(node, children, context, attrs, carousel_id, inner_container_width)
|
|
135
|
+
<<~HTML
|
|
136
|
+
<table#{html_attrs(
|
|
137
|
+
"style" => style_join(
|
|
138
|
+
"caption-side" => "top",
|
|
139
|
+
"display" => "table-caption",
|
|
140
|
+
"table-layout" => "fixed",
|
|
141
|
+
"width" => "100%"
|
|
142
|
+
),
|
|
143
|
+
"border" => "0",
|
|
144
|
+
"cellpadding" => "0",
|
|
145
|
+
"cellspacing" => "0",
|
|
146
|
+
"width" => "100%",
|
|
147
|
+
"role" => "presentation",
|
|
148
|
+
"class" => "mj-carousel-main"
|
|
149
|
+
)}>
|
|
150
|
+
<tbody>
|
|
151
|
+
<tr>
|
|
152
|
+
#{generate_controls("previous", attrs["left-icon"], carousel_id, children.length, attrs["icon-width"])}
|
|
153
|
+
#{generate_images(node, children, context, attrs, inner_container_width)}
|
|
154
|
+
#{generate_controls("next", attrs["right-icon"], carousel_id, children.length, attrs["icon-width"])}
|
|
155
|
+
</tr>
|
|
156
|
+
</tbody>
|
|
157
|
+
</table>
|
|
158
|
+
HTML
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def generate_controls(direction, icon, carousel_id, child_count, icon_width)
|
|
162
|
+
parsed_icon_width = parse_pixel_value(icon_width).to_i
|
|
163
|
+
|
|
164
|
+
labels = (1..child_count).map do |index|
|
|
165
|
+
<<~HTML
|
|
166
|
+
<label#{html_attrs(
|
|
167
|
+
"for" => "mj-carousel-#{carousel_id}-radio-#{index}",
|
|
168
|
+
"class" => "mj-carousel-#{direction} mj-carousel-#{direction}-#{index}"
|
|
169
|
+
)}>
|
|
170
|
+
<img#{html_attrs(
|
|
171
|
+
"src" => icon,
|
|
172
|
+
"alt" => direction,
|
|
173
|
+
"style" => style_join(
|
|
174
|
+
"display" => "block",
|
|
175
|
+
"width" => icon_width,
|
|
176
|
+
"height" => "auto"
|
|
177
|
+
),
|
|
178
|
+
"width" => parsed_icon_width.to_s
|
|
179
|
+
)} />
|
|
180
|
+
</label>
|
|
181
|
+
HTML
|
|
182
|
+
end.join
|
|
183
|
+
|
|
184
|
+
<<~HTML
|
|
185
|
+
<td#{html_attrs(
|
|
186
|
+
"class" => "mj-carousel-#{carousel_id}-icons-cell",
|
|
187
|
+
"style" => style_join(
|
|
188
|
+
"font-size" => "0px",
|
|
189
|
+
"display" => "none",
|
|
190
|
+
"mso-hide" => "all",
|
|
191
|
+
"padding" => "0px"
|
|
192
|
+
)
|
|
193
|
+
)}>
|
|
194
|
+
<div#{html_attrs(
|
|
195
|
+
"class" => "mj-carousel-#{direction}-icons",
|
|
196
|
+
"style" => style_join(
|
|
197
|
+
"display" => "none",
|
|
198
|
+
"mso-hide" => "all"
|
|
199
|
+
)
|
|
200
|
+
)}>
|
|
201
|
+
#{labels}
|
|
202
|
+
</div>
|
|
203
|
+
</td>
|
|
204
|
+
HTML
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def generate_images(node, children, context, attrs, inner_container_width)
|
|
208
|
+
image_component = carousel_image_component
|
|
209
|
+
images = with_inherited_mj_class(context, node) do
|
|
210
|
+
children.each_with_index.map do |child, index|
|
|
211
|
+
child_attrs = child_pass_through_attributes(child, context, attrs, nil)
|
|
212
|
+
image_component.render_item(
|
|
213
|
+
child,
|
|
214
|
+
attrs: child_attrs,
|
|
215
|
+
index: index,
|
|
216
|
+
container_width: inner_container_width,
|
|
217
|
+
visible: index.zero?
|
|
218
|
+
)
|
|
219
|
+
end.join
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
<<~HTML
|
|
223
|
+
<td#{html_attrs("style" => "padding:0px;")}>
|
|
224
|
+
<div#{html_attrs("class" => "mj-carousel-images")}>
|
|
225
|
+
#{images}
|
|
226
|
+
</div>
|
|
227
|
+
</td>
|
|
228
|
+
HTML
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def render_fallback(node, first_child, context, attrs, inner_container_width)
|
|
232
|
+
return "" unless first_child
|
|
233
|
+
|
|
234
|
+
child_attrs = with_inherited_mj_class(context, node) do
|
|
235
|
+
child_pass_through_attributes(first_child, context, attrs, nil)
|
|
236
|
+
end
|
|
237
|
+
fallback = carousel_image_component.render_item(
|
|
238
|
+
first_child,
|
|
239
|
+
attrs: child_attrs,
|
|
240
|
+
index: 0,
|
|
241
|
+
container_width: inner_container_width,
|
|
242
|
+
visible: true
|
|
243
|
+
)
|
|
244
|
+
mso_conditional_tag(fallback)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def child_pass_through_attributes(child, context, parent_attrs, tb_width)
|
|
248
|
+
child_attrs = resolved_attributes(child, context)
|
|
249
|
+
pass_through = {
|
|
250
|
+
"border-radius" => parent_attrs["border-radius"],
|
|
251
|
+
"tb-border" => parent_attrs["tb-border"],
|
|
252
|
+
"tb-border-radius" => parent_attrs["tb-border-radius"]
|
|
253
|
+
}
|
|
254
|
+
pass_through["tb-width"] = tb_width if tb_width
|
|
255
|
+
pass_through.merge(child_attrs)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def carousel_images(node)
|
|
259
|
+
node.element_children.select { |child| child.tag_name == "mj-carousel-image" }
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def carousel_image_component
|
|
263
|
+
renderer.send(:component_for, "mj-carousel-image")
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def thumbnail_width(attrs, child_count, inner_container_width)
|
|
267
|
+
return attrs["tb-width"] if attrs["tb-width"] && !attrs["tb-width"].empty?
|
|
268
|
+
return "0px" if child_count.zero?
|
|
269
|
+
|
|
270
|
+
"#{[inner_container_width.to_f / child_count, 110].min}px"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def content_width(container_width, attrs)
|
|
274
|
+
total = parse_pixel_value(container_width || "600px")
|
|
275
|
+
total -= padding_side(attrs, "left")
|
|
276
|
+
total -= padding_side(attrs, "right")
|
|
277
|
+
[total, 0].max
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def padding_side(attrs, side)
|
|
281
|
+
specific = attrs["padding-#{side}"]
|
|
282
|
+
return parse_pixel_value(specific) unless blank?(specific)
|
|
283
|
+
|
|
284
|
+
shorthand_padding_value(attrs["padding"], side)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def shorthand_padding_value(value, side)
|
|
288
|
+
return 0 if blank?(value)
|
|
289
|
+
|
|
290
|
+
parts = value.to_s.strip.split(/\s+/)
|
|
291
|
+
case parts.length
|
|
292
|
+
when 1
|
|
293
|
+
parse_pixel_value(parts[0])
|
|
294
|
+
when 2
|
|
295
|
+
%w[left right].include?(side) ? parse_pixel_value(parts[1]) : parse_pixel_value(parts[0])
|
|
296
|
+
when 3
|
|
297
|
+
%w[left right].include?(side) ? parse_pixel_value(parts[1]) : parse_pixel_value(side == "top" ? parts[0] : parts[2])
|
|
298
|
+
when 4
|
|
299
|
+
parse_pixel_value(parts[side == "left" ? 3 : 1])
|
|
300
|
+
else
|
|
301
|
+
0
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def component_head_style(carousel_id, length, attrs)
|
|
306
|
+
return "" if length.zero?
|
|
307
|
+
|
|
308
|
+
hide_non_selected = (0...length).map do |index|
|
|
309
|
+
".mj-carousel-#{carousel_id}-radio:checked #{adjacent_siblings(index)}+ .mj-carousel-content .mj-carousel-image"
|
|
310
|
+
end.join(",\n")
|
|
311
|
+
|
|
312
|
+
show_selected = (0...length).map do |index|
|
|
313
|
+
".mj-carousel-#{carousel_id}-radio-#{index + 1}:checked #{adjacent_siblings(length - index - 1)}+ .mj-carousel-content .mj-carousel-image-#{index + 1}"
|
|
314
|
+
end.join(",\n")
|
|
315
|
+
|
|
316
|
+
next_icons = (0...length).map do |index|
|
|
317
|
+
target = ((index + 1) % length) + 1
|
|
318
|
+
".mj-carousel-#{carousel_id}-radio-#{index + 1}:checked #{adjacent_siblings(length - index - 1)}+ .mj-carousel-content .mj-carousel-next-#{target}"
|
|
319
|
+
end.join(",\n")
|
|
320
|
+
|
|
321
|
+
previous_icons = (0...length).map do |index|
|
|
322
|
+
target = ((index - 1) % length) + 1
|
|
323
|
+
".mj-carousel-#{carousel_id}-radio-#{index + 1}:checked #{adjacent_siblings(length - index - 1)}+ .mj-carousel-content .mj-carousel-previous-#{target}"
|
|
324
|
+
end.join(",\n")
|
|
325
|
+
|
|
326
|
+
selected_thumbnail = (0...length).map do |index|
|
|
327
|
+
".mj-carousel-#{carousel_id}-radio-#{index + 1}:checked #{adjacent_siblings(length - index - 1)}+ .mj-carousel-content .mj-carousel-#{carousel_id}-thumbnail-#{index + 1}"
|
|
328
|
+
end.join(",\n")
|
|
329
|
+
|
|
330
|
+
show_thumbnails = (0...length).map do |index|
|
|
331
|
+
".mj-carousel-#{carousel_id}-radio-#{index + 1}:checked #{adjacent_siblings(length - index - 1)}+ .mj-carousel-content .mj-carousel-#{carousel_id}-thumbnail"
|
|
332
|
+
end.join(",\n")
|
|
333
|
+
|
|
334
|
+
hide_on_hover = (0...length).map do |index|
|
|
335
|
+
".mj-carousel-#{carousel_id}-thumbnail:hover #{adjacent_siblings(length - index - 1)}+ .mj-carousel-main .mj-carousel-image"
|
|
336
|
+
end.join(",\n")
|
|
337
|
+
|
|
338
|
+
show_on_hover = (0...length).map do |index|
|
|
339
|
+
".mj-carousel-#{carousel_id}-thumbnail-#{index + 1}:hover #{adjacent_siblings(length - index - 1)}+ .mj-carousel-main .mj-carousel-image-#{index + 1}"
|
|
340
|
+
end.join(",\n")
|
|
341
|
+
|
|
342
|
+
<<~CSS
|
|
343
|
+
.mj-carousel {
|
|
344
|
+
-webkit-user-select: none;
|
|
345
|
+
-moz-user-select: none;
|
|
346
|
+
user-select: none;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.mj-carousel-#{carousel_id}-icons-cell {
|
|
350
|
+
display: table-cell !important;
|
|
351
|
+
width: #{attrs["icon-width"]} !important;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.mj-carousel-radio,
|
|
355
|
+
.mj-carousel-next,
|
|
356
|
+
.mj-carousel-previous {
|
|
357
|
+
display: none !important;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.mj-carousel-thumbnail,
|
|
361
|
+
.mj-carousel-next,
|
|
362
|
+
.mj-carousel-previous {
|
|
363
|
+
touch-action: manipulation;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
#{hide_non_selected} {
|
|
367
|
+
display: none !important;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
#{show_selected} {
|
|
371
|
+
display: block !important;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.mj-carousel-previous-icons,
|
|
375
|
+
.mj-carousel-next-icons,
|
|
376
|
+
#{next_icons},
|
|
377
|
+
#{previous_icons} {
|
|
378
|
+
display: block !important;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
#{selected_thumbnail} {
|
|
382
|
+
border-color: #{attrs["tb-selected-border-color"]} !important;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
#{show_thumbnails} {
|
|
386
|
+
display: inline-block !important;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.mj-carousel-image img + div,
|
|
390
|
+
.mj-carousel-thumbnail img + div {
|
|
391
|
+
display: none !important;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
#{hide_on_hover} {
|
|
395
|
+
display: none !important;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.mj-carousel-thumbnail:hover {
|
|
399
|
+
border-color: #{attrs["tb-hover-border-color"]} !important;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
#{show_on_hover} {
|
|
403
|
+
display: block !important;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.mj-carousel noinput { display:block !important; }
|
|
407
|
+
.mj-carousel noinput .mj-carousel-image-1 { display: block !important; }
|
|
408
|
+
.mj-carousel noinput .mj-carousel-arrows,
|
|
409
|
+
.mj-carousel noinput .mj-carousel-thumbnails { display: none !important; }
|
|
410
|
+
|
|
411
|
+
[owa] .mj-carousel-thumbnail { display: none !important; }
|
|
412
|
+
|
|
413
|
+
@media screen yahoo {
|
|
414
|
+
.mj-carousel-#{carousel_id}-icons-cell,
|
|
415
|
+
.mj-carousel-previous-icons,
|
|
416
|
+
.mj-carousel-next-icons {
|
|
417
|
+
display: none !important;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.mj-carousel-#{carousel_id}-radio-1:checked #{adjacent_siblings(length - 1)}+ .mj-carousel-content .mj-carousel-#{carousel_id}-thumbnail-1 {
|
|
421
|
+
border-color: transparent;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
CSS
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def adjacent_siblings(count)
|
|
428
|
+
"+ * " * count
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def mso_conditional_tag(content, negation = false)
|
|
432
|
+
if negation
|
|
433
|
+
"<!--[if !mso]><!-->#{content}<!--<![endif]-->"
|
|
434
|
+
else
|
|
435
|
+
"<!--[if mso]>#{content}<![endif]-->"
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def parse_pixel_value(value)
|
|
440
|
+
matched = value.to_s.match(/(-?\d+(?:\.\d+)?)/)
|
|
441
|
+
matched ? matched[1].to_f : 0.0
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def blank?(value)
|
|
445
|
+
value.nil? || value.to_s.strip.empty?
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
require_relative "base"
|
|
2
|
+
|
|
3
|
+
module MjmlRb
|
|
4
|
+
module Components
|
|
5
|
+
class CarouselImage < Base
|
|
6
|
+
TAGS = ["mj-carousel-image"].freeze
|
|
7
|
+
|
|
8
|
+
ALLOWED_ATTRIBUTES = {
|
|
9
|
+
"alt" => "string",
|
|
10
|
+
"href" => "string",
|
|
11
|
+
"rel" => "string",
|
|
12
|
+
"target" => "string",
|
|
13
|
+
"title" => "string",
|
|
14
|
+
"src" => "string",
|
|
15
|
+
"thumbnails-src" => "string",
|
|
16
|
+
"border-radius" => "unit(px,%){1,4}",
|
|
17
|
+
"tb-border" => "string",
|
|
18
|
+
"tb-border-radius" => "unit(px,%){1,4}"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
DEFAULT_ATTRIBUTES = {
|
|
22
|
+
"alt" => "",
|
|
23
|
+
"target" => "_blank"
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
def tags
|
|
27
|
+
TAGS
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def render(tag_name:, node:, context:, attrs:, parent:)
|
|
31
|
+
render_item(
|
|
32
|
+
node,
|
|
33
|
+
attrs: DEFAULT_ATTRIBUTES.merge(attrs),
|
|
34
|
+
index: 0,
|
|
35
|
+
container_width: parse_pixel_value(context[:container_width] || "600px"),
|
|
36
|
+
visible: true
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def render_radio(index:, carousel_id:)
|
|
41
|
+
input_attrs = {
|
|
42
|
+
"class" => "mj-carousel-radio mj-carousel-#{carousel_id}-radio mj-carousel-#{carousel_id}-radio-#{index + 1}",
|
|
43
|
+
"checked" => (index.zero? ? "checked" : nil),
|
|
44
|
+
"type" => "radio",
|
|
45
|
+
"name" => "mj-carousel-radio-#{carousel_id}",
|
|
46
|
+
"id" => "mj-carousel-#{carousel_id}-radio-#{index + 1}",
|
|
47
|
+
"style" => style_join(
|
|
48
|
+
"display" => "none",
|
|
49
|
+
"mso-hide" => "all"
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
%(<input#{html_attrs(input_attrs)} />)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def render_thumbnail(node, attrs:, index:, carousel_id:, thumbnails:, tb_width:)
|
|
57
|
+
a = DEFAULT_ATTRIBUTES.merge(attrs)
|
|
58
|
+
css_class = suffix_css_classes(a["css-class"], "thumbnail")
|
|
59
|
+
link_classes = [
|
|
60
|
+
"mj-carousel-thumbnail",
|
|
61
|
+
"mj-carousel-#{carousel_id}-thumbnail",
|
|
62
|
+
"mj-carousel-#{carousel_id}-thumbnail-#{index + 1}",
|
|
63
|
+
css_class
|
|
64
|
+
].compact.reject(&:empty?).join(" ")
|
|
65
|
+
|
|
66
|
+
link_attrs = {
|
|
67
|
+
"style" => style_join(
|
|
68
|
+
"border" => a["tb-border"],
|
|
69
|
+
"border-radius" => a["tb-border-radius"],
|
|
70
|
+
"display" => (thumbnails == "supported" ? "none" : "inline-block"),
|
|
71
|
+
"overflow" => "hidden",
|
|
72
|
+
"width" => tb_width
|
|
73
|
+
),
|
|
74
|
+
"href" => "##{index + 1}",
|
|
75
|
+
"target" => a["target"],
|
|
76
|
+
"class" => link_classes
|
|
77
|
+
}
|
|
78
|
+
label_attrs = {
|
|
79
|
+
"for" => "mj-carousel-#{carousel_id}-radio-#{index + 1}"
|
|
80
|
+
}
|
|
81
|
+
image_attrs = {
|
|
82
|
+
"style" => style_join(
|
|
83
|
+
"display" => "block",
|
|
84
|
+
"width" => "100%",
|
|
85
|
+
"height" => "auto"
|
|
86
|
+
),
|
|
87
|
+
"src" => a["thumbnails-src"] || a["src"],
|
|
88
|
+
"alt" => a["alt"],
|
|
89
|
+
"width" => parse_pixel_value(tb_width).to_i.to_s
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
<<~HTML.chomp
|
|
93
|
+
<a#{html_attrs(link_attrs)}>
|
|
94
|
+
<label#{html_attrs(label_attrs)}>
|
|
95
|
+
#{build_img_tag(image_attrs)}
|
|
96
|
+
</label>
|
|
97
|
+
</a>
|
|
98
|
+
HTML
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def render_item(node, attrs:, index:, container_width:, visible:)
|
|
102
|
+
a = DEFAULT_ATTRIBUTES.merge(attrs)
|
|
103
|
+
css_class = a["css-class"].to_s
|
|
104
|
+
classes = ["mj-carousel-image", "mj-carousel-image-#{index + 1}", css_class].reject(&:empty?).join(" ")
|
|
105
|
+
div_style = visible ? nil : style_join("display" => "none", "mso-hide" => "all")
|
|
106
|
+
img_attrs = {
|
|
107
|
+
"title" => a["title"],
|
|
108
|
+
"src" => a["src"],
|
|
109
|
+
"alt" => a["alt"],
|
|
110
|
+
"style" => style_join(
|
|
111
|
+
"border-radius" => a["border-radius"],
|
|
112
|
+
"display" => "block",
|
|
113
|
+
"width" => "#{container_width.to_i}px",
|
|
114
|
+
"max-width" => "100%",
|
|
115
|
+
"height" => "auto"
|
|
116
|
+
),
|
|
117
|
+
"width" => container_width.to_i.to_s,
|
|
118
|
+
"border" => "0"
|
|
119
|
+
}
|
|
120
|
+
image_tag = build_img_tag(img_attrs)
|
|
121
|
+
|
|
122
|
+
content = if a["href"]
|
|
123
|
+
link_attrs = {
|
|
124
|
+
"href" => a["href"],
|
|
125
|
+
"rel" => a["rel"],
|
|
126
|
+
"target" => a["target"] || "_blank"
|
|
127
|
+
}
|
|
128
|
+
%(<a#{html_attrs(link_attrs)}>#{image_tag}</a>)
|
|
129
|
+
else
|
|
130
|
+
image_tag
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
%(<div#{html_attrs("class" => classes, "style" => div_style)}>#{content}</div>)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def parse_pixel_value(value)
|
|
139
|
+
matched = value.to_s.match(/(-?\d+(?:\.\d+)?)/)
|
|
140
|
+
matched ? matched[1].to_f : 0.0
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def suffix_css_classes(classes, suffix)
|
|
144
|
+
return nil if classes.nil? || classes.empty?
|
|
145
|
+
|
|
146
|
+
classes.split(/\s+/).map { |klass| "#{klass}-#{suffix}" }.join(" ")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def build_img_tag(attrs)
|
|
150
|
+
ordered_keys = %w[title src alt style width border]
|
|
151
|
+
rendered = ordered_keys.filter_map do |key|
|
|
152
|
+
next unless attrs.key?(key)
|
|
153
|
+
next if key != "alt" && (attrs[key].nil? || attrs[key].to_s.empty?)
|
|
154
|
+
|
|
155
|
+
%(#{key}="#{escape_attr(attrs[key].to_s)}")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
"<img #{rendered.join(' ')} />"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
data/lib/mjml-rb/renderer.rb
CHANGED
|
@@ -5,6 +5,8 @@ require_relative "components/attributes"
|
|
|
5
5
|
require_relative "components/body"
|
|
6
6
|
require_relative "components/breakpoint"
|
|
7
7
|
require_relative "components/button"
|
|
8
|
+
require_relative "components/carousel"
|
|
9
|
+
require_relative "components/carousel_image"
|
|
8
10
|
require_relative "components/head"
|
|
9
11
|
require_relative "components/hero"
|
|
10
12
|
require_relative "components/image"
|
|
@@ -440,6 +442,8 @@ module MjmlRb
|
|
|
440
442
|
register_component(registry, Components::Breakpoint.new(self))
|
|
441
443
|
register_component(registry, Components::Accordion.new(self))
|
|
442
444
|
register_component(registry, Components::Button.new(self))
|
|
445
|
+
register_component(registry, Components::Carousel.new(self))
|
|
446
|
+
register_component(registry, Components::CarouselImage.new(self))
|
|
443
447
|
register_component(registry, Components::Hero.new(self))
|
|
444
448
|
register_component(registry, Components::Image.new(self))
|
|
445
449
|
register_component(registry, Components::Navbar.new(self))
|
data/lib/mjml-rb/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mjml-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.13
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Andriichuk
|
|
@@ -59,6 +59,8 @@ files:
|
|
|
59
59
|
- lib/mjml-rb/components/body.rb
|
|
60
60
|
- lib/mjml-rb/components/breakpoint.rb
|
|
61
61
|
- lib/mjml-rb/components/button.rb
|
|
62
|
+
- lib/mjml-rb/components/carousel.rb
|
|
63
|
+
- lib/mjml-rb/components/carousel_image.rb
|
|
62
64
|
- lib/mjml-rb/components/column.rb
|
|
63
65
|
- lib/mjml-rb/components/divider.rb
|
|
64
66
|
- lib/mjml-rb/components/head.rb
|