mjml-0.1.0.0: src/Text/Mjml/Component/Accordion.hs
-- | @mj-accordion@ and its parts: @mj-accordion-element@,
-- @mj-accordion-title@ and @mj-accordion-text@.
module Text.Mjml.Component.Accordion (
mjAccordion,
mjAccordionElement,
mjAccordionTitle,
mjAccordionText,
) where
import Data.List (intersperse)
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as T
import Text.Mjml.Component.Section (setContext)
import Text.Mjml.Node (Node (..), element)
import Text.Mjml.Render
-- | The attributes an accordion and each element hand down to their children.
childrenAttrNames :: [Text]
childrenAttrNames =
[ "border"
, "icon-align"
, "icon-width"
, "icon-height"
, "icon-position"
, "icon-wrapped-url"
, "icon-wrapped-alt"
, "icon-unwrapped-url"
, "icon-unwrapped-alt"
]
childrenAttr :: Self -> Attrs
childrenAttr s = [(k, v) | k <- childrenAttrNames, Just v <- [attr s k]]
iconAttributes :: [(Text, Text)]
iconAttributes =
[ ("icon-align", "enum(top,middle,bottom)")
, ("icon-width", "unit(px,%)")
, ("icon-height", "unit(px,%)")
, ("icon-wrapped-url", "string")
, ("icon-wrapped-alt", "string")
, ("icon-unwrapped-url", "string")
, ("icon-unwrapped-alt", "string")
, ("icon-position", "enum(left,right)")
]
paddingAttributes :: [(Text, Text)]
paddingAttributes =
[ ("padding-bottom", "unit(px,%)")
, ("padding-left", "unit(px,%)")
, ("padding-right", "unit(px,%)")
, ("padding-top", "unit(px,%)")
, ("padding", "unit(px,%){1,4}")
]
mjAccordion :: Component
mjAccordion =
(defComponent "mj-accordion" build')
{ allowedAttributes =
[ ("container-background-color", "color")
, ("border", "string")
, ("font-family", "string")
]
<> iconAttributes
<> paddingAttributes
, defaults =
[ ("border", "2px solid black")
, ("font-family", "Ubuntu, Helvetica, Arial, sans-serif")
, ("icon-align", "middle")
, ("icon-wrapped-url", "https://i.imgur.com/bIXv1bk.png")
, ("icon-wrapped-alt", "+")
, ("icon-unwrapped-url", "https://i.imgur.com/w4uTygT.png")
, ("icon-unwrapped-alt", "-")
, ("icon-position", "right")
, ("icon-height", "32px")
, ("icon-width", "32px")
, ("padding", "10px 25px")
]
, componentHeadStyle = Just (const accordionHeadStyle)
}
where
build' s =
let inst =
(defInstance s (render' inst))
{ getStyles = \case
"table" ->
[ style "width" "100%"
, style "border-collapse" "collapse"
, ("border", attr s "border")
, style "border-bottom" "none"
, ("font-family", attr s "font-family")
]
_ -> []
, childContext = setContext [("accordionFontFamily", attr s "font-family")] (context s)
}
in inst
render' inst = do
kids <- renderChildren inst defChildOptions {childAttributes = childrenAttr (self inst)}
pure $
mconcat
[ "<table"
, htmlAttributes
inst
[ ("cellspacing", AV "0")
, ("cellpadding", AV "0")
, ("class", AV "mj-accordion")
, ("style", AStyleOf "table")
]
, "><tbody>"
, kids
, "</tbody></table>"
]
accordionHeadStyle :: Text
accordionHeadStyle =
T.unlines
[ "noinput.mj-accordion-checkbox { display:block!important; }"
, "@media yahoo, only screen and (min-width:0) {"
, " .mj-accordion-element { display:block; }"
, " .mj-accordion-checkbox[type=\"checkbox\"], .mj-accordion-less { display:none!important; }"
, " .mj-accordion-checkbox[type=\"checkbox\"] + * .mj-accordion-title { cursor:pointer; touch-action:manipulation; -webkit-user-select:none; -moz-user-select:none; user-select:none; }"
, " .mj-accordion-checkbox[type=\"checkbox\"] + * .mj-accordion-content { overflow:hidden; display:none; }"
, " .mj-accordion-checkbox[type=\"checkbox\"] + * .mj-accordion-more { display:block!important; }"
, " .mj-accordion-checkbox:checked + * .mj-accordion-content { display:block; }"
, " .mj-accordion-checkbox:checked + * .mj-accordion-more { display:none!important; }"
, " .mj-accordion-checkbox:checked + * .mj-accordion-less { display:block!important; }"
, "}"
, ".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; }"
, ".moz-text-html input.mj-accordion-checkbox + * .mj-accordion-content { overflow: hidden; display: block; }"
, ".moz-text-html input.mj-accordion-checkbox + * .mj-accordion-ico { display: none; }"
, "@goodbye { @gmail }"
]
mjAccordionElement :: Component
mjAccordionElement =
(defComponent "mj-accordion-element" build')
{ allowedAttributes =
[ ("background-color", "color")
, ("border", "string")
, ("font-family", "string")
]
<> iconAttributes
}
where
build' s =
let inst =
(defInstance s (render' inst))
{ getStyles = \case
"td" -> [style "padding" "0px", ("background-color", attr s "background-color")]
"label" -> [style "font-size" "13px", ("font-family", attr s "font-family")]
"input" -> [style "display" "none"]
_ -> []
, childContext = setContext [("elementFontFamily", attr s "font-family")] (context s)
}
in inst
-- handleMissingChildren: a title or text left out is rendered empty, built
-- outside the tree with only the inherited attributes.
handleMissingChildren inst = do
let s = self inst
kids = children (node s)
inherited = childrenAttr s
has t = any ((== t) . tagName) kids
standalone c =
render . build c $
Self
{ attrs = defaults c `merge` inherited
, attrIndex = Map.fromList (defaults c `merge` inherited)
, props = Props 0 True True 0 0 []
, context = childContext inst
, node = element (name c) [] []
, registry = registry s
, component = c
, mjClassScope = mjClassScope s
}
title <- if has "mj-accordion-title" then pure [] else (: []) <$> standalone mjAccordionTitle
body <- renderChildren inst defChildOptions {childAttributes = inherited}
text <- if has "mj-accordion-text" then pure [] else (: []) <$> standalone mjAccordionText
pure (mconcat (intersperse "\n" (title <> [body] <> text)))
render' inst = do
let s = self inst
inner <- handleMissingChildren inst
pure $
mconcat
[ "<tr"
, htmlAttributes inst [("class", maybe ANone AV (attr s "css-class"))]
, "><td"
, htmlAttributes inst [("style", AStyleOf "td")]
, "><label"
, htmlAttributes inst [("class", AV "mj-accordion-element"), ("style", AStyleOf "label")]
, ">"
, conditionalTag True $
"<input"
<> htmlAttributes
inst
[ ("class", AV "mj-accordion-checkbox")
, ("type", AV "checkbox")
, ("style", AStyleOf "input")
]
<> " />"
, "<div>"
, inner
, "</div></label></td></tr>"
]
-- | @resolveFontFamily@: the element's own @font-family@ when it set one,
-- otherwise the enclosing element's, otherwise the accordion's.
--
-- MJML checks the attributes written on the element itself, so an
-- @mj-attributes@ default does not count as the element's own choice.
resolveFontFamily :: Self -> Maybe Text
resolveFontFamily s
| any ((== "font-family") . fst) (attributes (node s)) = attr s "font-family"
| truthy (ctx s "elementFontFamily") = ctx s "elementFontFamily"
| truthy (ctx s "accordionFontFamily") = ctx s "accordionFontFamily"
| otherwise = Nothing
mjAccordionTitle :: Component
mjAccordionTitle =
(defComponent "mj-accordion-title" build')
{ endingTag = True
, allowedAttributes =
[ ("background-color", "color")
, ("color", "color")
, ("font-size", "unit(px)")
, ("font-family", "string")
, ("font-weight", "string")
]
<> paddingAttributes
, defaults = [("font-size", "13px"), ("padding", "16px")]
}
where
build' s =
let inst =
(defInstance s (pure rendered))
{ getStyles = \case
"td" ->
[ style "width" "100%"
, ("background-color", attr s "background-color")
, ("color", attr s "color")
, ("font-size", attr s "font-size")
, ("font-family", resolveFontFamily s)
, ("font-weight", attr s "font-weight")
, ("padding", attr s "padding")
, ("padding-bottom", attr s "padding-bottom")
, ("padding-left", attr s "padding-left")
, ("padding-right", attr s "padding-right")
, ("padding-top", attr s "padding-top")
]
"table" -> [style "width" "100%", ("border-bottom", attr s "border")]
"td2" ->
[ style "padding" "16px"
, ("background", attr s "background-color")
, ("vertical-align", attr s "icon-align")
]
"img" ->
[ style "display" "none"
, ("width", attr s "icon-width")
, ("height", attr s "icon-height")
]
_ -> []
}
renderTitle =
"<td"
<> htmlAttributes inst [("class", maybe ANone AV (attr s "css-class")), ("style", AStyleOf "td")]
<> ">"
<> fromText (getContent s)
<> "</td>"
renderIcons =
conditionalTag True $
mconcat
[ "<td"
, htmlAttributes inst [("class", AV "mj-accordion-ico"), ("style", AStyleOf "td2")]
, "><img"
, htmlAttributes
inst
[ ("src", maybe ANone AV (attr s "icon-wrapped-url"))
, ("alt", maybe ANone AV (attr s "icon-wrapped-alt"))
, ("class", AV "mj-accordion-more")
, ("style", AStyleOf "img")
]
, " /><img"
, htmlAttributes
inst
[ ("src", maybe ANone AV (attr s "icon-unwrapped-url"))
, ("alt", maybe ANone AV (attr s "icon-unwrapped-alt"))
, ("class", AV "mj-accordion-less")
, ("style", AStyleOf "img")
]
, " /></td>"
]
elements =
if attr s "icon-position" == Just "right"
then [renderTitle, renderIcons]
else [renderIcons, renderTitle]
rendered =
mconcat
[ "<div"
, htmlAttributes inst [("class", AV "mj-accordion-title")]
, "><table"
, htmlAttributes
inst
[("cellspacing", AV "0"), ("cellpadding", AV "0"), ("style", AStyleOf "table")]
, "><tbody><tr>"
, mconcat (intersperse "\n" elements)
, "</tr></tbody></table></div>"
]
in inst
mjAccordionText :: Component
mjAccordionText =
(defComponent "mj-accordion-text" build')
{ endingTag = True
, allowedAttributes =
[ ("background-color", "color")
, ("font-size", "unit(px)")
, ("font-family", "string")
, ("font-weight", "string")
, ("letter-spacing", "unitWithNegative(px,em)")
, ("line-height", "unit(px,%,)")
, ("color", "color")
]
<> paddingAttributes
, defaults = [("font-size", "13px"), ("line-height", "1"), ("padding", "16px")]
}
where
build' s =
let inst =
(defInstance s (pure rendered))
{ getStyles = \case
"td" ->
[ ("background", attr s "background-color")
, ("font-size", attr s "font-size")
, ("font-family", resolveFontFamily s)
, ("font-weight", attr s "font-weight")
, ("letter-spacing", attr s "letter-spacing")
, ("line-height", attr s "line-height")
, ("color", attr s "color")
, ("padding", attr s "padding")
, ("padding-bottom", attr s "padding-bottom")
, ("padding-left", attr s "padding-left")
, ("padding-right", attr s "padding-right")
, ("padding-top", attr s "padding-top")
]
"table" -> [style "width" "100%", ("border-bottom", attr s "border")]
_ -> []
}
rendered =
mconcat
[ "<div"
, htmlAttributes inst [("class", AV "mj-accordion-content")]
, "><table"
, htmlAttributes
inst
[("cellspacing", AV "0"), ("cellpadding", AV "0"), ("style", AStyleOf "table")]
, "><tbody><tr><td"
, htmlAttributes inst [("class", maybe ANone AV (attr s "css-class")), ("style", AStyleOf "td")]
, ">"
, fromText (getContent s)
, "</td></tr></tbody></table></div>"
]
in inst