packages feed

mjml-0.1.0.0: src/Text/Mjml/Component/Carousel.hs

-- | @mj-carousel@ and @mj-carousel-image@.
module Text.Mjml.Component.Carousel (
    mjCarousel,
    mjCarouselImage,
) where

import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Text qualified as T

import Text.Mjml.Component.Section (setContext)
import Text.Mjml.Js (parseIntJs, showNumber)
import Text.Mjml.Node (Node (..))
import Text.Mjml.Render


-- | The identifier MJML draws at random in the carousel's constructor, kept
-- in the instance's props.
carouselIdKey :: Text
carouselIdKey = "mj-carousel-id"


-- | Which of the image's methods the carousel is calling: MJML renders the
-- same children three times, through @renderRadio@, @renderThumbnail@ and
-- @render@.
imageModeKey :: Text
imageModeKey = "mj-carousel-render"


tshow :: Int -> Text
tshow = T.pack . show


mjCarousel :: Component
mjCarousel =
    (defComponent "mj-carousel" build')
        { allowedAttributes =
            [ ("align", "enum(left,center,right)")
            , ("border-radius", "string")
            , ("container-background-color", "color")
            , ("icon-width", "unit(px,%)")
            , ("left-icon", "string")
            , ("padding", "unit(px,%){1,4}")
            , ("padding-top", "unit(px,%)")
            , ("padding-bottom", "unit(px,%)")
            , ("padding-left", "unit(px,%)")
            , ("padding-right", "unit(px,%)")
            , ("right-icon", "string")
            , ("thumbnails", "enum(visible,hidden,supported)")
            , ("tb-border", "string")
            , ("tb-border-radius", "string")
            , ("tb-hover-border-color", "color")
            , ("tb-selected-border-color", "color")
            , ("tb-width", "unit(px,%)")
            ]
        , defaults =
            [ ("align", "center")
            , ("border-radius", "6px")
            , ("icon-width", "44px")
            , ("left-icon", "https://i.imgur.com/xTh3hln.png")
            , ("right-icon", "https://i.imgur.com/os7o9kz.png")
            , ("thumbnails", "visible")
            , ("tb-border", "2px solid transparent")
            , ("tb-border-radius", "6px")
            , ("tb-hover-border-color", "#fead0d")
            , ("tb-selected-border-color", "#ccc")
            ]
        , prepare = \s -> do
            i <- nextId
            pure s {props = (props s) {extra = merge (extra (props s)) [(carouselIdKey, i)]}}
        }
  where
    build' s = inst
      where
        carouselId = fromMaybe "" (lookup carouselIdKey (extra (props s)))
        kids = children (node s)
        len = length kids
        a = attrOr s

        inst =
            (defInstance s render')
                { getStyles = \case
                    "carousel.div" ->
                        [ style "display" "table"
                        , style "width" "100%"
                        , style "table-layout" "fixed"
                        , style "text-align" "center"
                        , style "font-size" "0px"
                        ]
                    "carousel.table" ->
                        [ style "caption-side" "top"
                        , style "display" "table-caption"
                        , style "table-layout" "fixed"
                        , style "width" "100%"
                        ]
                    "images.td" -> [style "padding" "0px"]
                    "controls.div" -> [style "display" "none", style "mso-hide" "all"]
                    "controls.img" ->
                        [style "display" "block", ("width", attr s "icon-width"), style "height" "auto"]
                    "controls.td" ->
                        [ style "font-size" "0px"
                        , style "display" "none"
                        , style "mso-hide" "all"
                        , style "padding" "0px"
                        ]
                    _ -> []
                , childContext = setContext [("thumbnails", attr s "thumbnails")] (context s)
                , instanceHeadStyle = Just (const headStyle')
                }

        -- With no parent width in the context MJML's division is NaN, which
        -- lodash's min skips, so the default is always 110px.
        thumbnailsWidth
            | len == 0 = "0"
            | otherwise = case attr s "tb-width" of
                Just w | truthy (Just w) -> w
                _ -> "110px"

        modeOptions mode = defChildOptions {childProps = [(imageModeKey, mode)]}

        generateRadios =
            renderChildren
                inst
                (modeOptions "radio") {childAttributes = [("carouselId", carouselId)]}

        generateThumbnails
            | a "thumbnails" "" `notElem` ["visible", "supported"] = pure ""
            | otherwise =
                renderChildren
                    inst
                    (modeOptions "thumbnail")
                        { childAttributes =
                            [ ("tb-border", a "tb-border" "")
                            , ("tb-border-radius", a "tb-border-radius" "")
                            , ("tb-width", thumbnailsWidth)
                            , ("carouselId", carouselId)
                            ]
                        }

        generateControls direction icon =
            mconcat
                [ "<td"
                , htmlAttributes
                    inst
                    [ ("class", AV ("mj-carousel-" <> carouselId <> "-icons-cell"))
                    , ("style", AStyleOf "controls.td")
                    ]
                , "><div"
                , htmlAttributes
                    inst
                    [ ("class", AV ("mj-carousel-" <> direction <> "-icons"))
                    , ("style", AStyleOf "controls.div")
                    ]
                , ">"
                , mconcat
                    [ mconcat
                        [ "<label"
                        , htmlAttributes
                            inst
                            [ ("for", AV ("mj-carousel-" <> carouselId <> "-radio-" <> tshow i))
                            , ("class", AV ("mj-carousel-" <> direction <> " mj-carousel-" <> direction <> "-" <> tshow i))
                            ]
                        , "><img"
                        , htmlAttributes
                            inst
                            [ ("src", maybe ANone AV icon)
                            , ("alt", AV direction)
                            , ("style", AStyleOf "controls.img")
                            , ("width", AV (showNumber (parseIntJs (a "icon-width" ""))))
                            ]
                        , " /></label>"
                        ]
                    | i <- [1 .. len]
                    ]
                , "</div></td>"
                ]

        generateImages = do
            images <- renderChildren inst defChildOptions {childAttributes = borderRadius}
            pure $
                mconcat
                    [ "<td"
                    , htmlAttributes inst [("style", AStyleOf "images.td")]
                    , "><div"
                    , htmlAttributes inst [("class", AV "mj-carousel-images")]
                    , ">"
                    , images
                    , "</div></td>"
                    ]

        borderRadius = [("border-radius", a "border-radius" "")]

        generateCarousel = do
            images <- generateImages
            pure $
                mconcat
                    [ "<table"
                    , htmlAttributes
                        inst
                        [ ("style", AStyleOf "carousel.table")
                        , ("border", AV "0")
                        , ("cellpadding", AV "0")
                        , ("cellspacing", AV "0")
                        , ("width", AV "100%")
                        , ("role", AV "presentation")
                        , ("class", AV "mj-carousel-main")
                        ]
                    , "><tbody><tr>"
                    , generateControls "previous" (attr s "left-icon")
                    , images
                    , generateControls "next" (attr s "right-icon")
                    , "</tr></tbody></table>"
                    ]

        renderFallback = case kids of
            [] -> pure ""
            k : _ ->
                msoConditionalTag False
                    <$> renderChildren inst defChildOptions {childNodes = Just [k], childAttributes = borderRadius}

        render' = do
            radios <- generateRadios
            thumbnails <- generateThumbnails
            carousel <- generateCarousel
            fallback <- renderFallback
            pure $
                msoConditionalTag
                    True
                    ( mconcat
                        [ "<div"
                        , htmlAttributes inst [("class", AV "mj-carousel")]
                        , ">"
                        , radios
                        , "<div"
                        , htmlAttributes
                            inst
                            [ ("class", AV ("mj-carousel-content mj-carousel-" <> carouselId <> "-content"))
                            , ("style", AStyleOf "carousel.div")
                            ]
                        , ">"
                        , thumbnails
                        , carousel
                        , "</div></div>"
                        ]
                    )
                    <> fallback

        -- componentHeadStyle
        headStyle'
            | len == 0 = ""
            | otherwise = carouselCss <> "\n" <> fallbackCss
        cid = carouselId
        rep str n = T.replicate (max 0 n) str
        range = [0 .. len - 1]
        selectorList f = T.intercalate "," (map f range)
        carouselCss =
            T.unlines
                [ ".mj-carousel { -webkit-user-select: none; -moz-user-select: none; user-select: none; }"
                , ".mj-carousel-" <> cid <> "-icons-cell { display: table-cell !important; width: " <> a "icon-width" "" <> " !important; }"
                , ".mj-carousel-radio, .mj-carousel-next, .mj-carousel-previous { display: none !important; }"
                , ".mj-carousel-thumbnail, .mj-carousel-next, .mj-carousel-previous { touch-action: manipulation; }"
                , selectorList (\i -> ".mj-carousel-" <> cid <> "-radio:checked " <> rep "+ * " i <> "+ .mj-carousel-content .mj-carousel-image")
                    <> " { display: none !important; }"
                , selectorList (\i -> radioChecked i <> "+ .mj-carousel-content .mj-carousel-image-" <> tshow (i + 1))
                    <> " { display: block !important; }"
                , ".mj-carousel-previous-icons,\n.mj-carousel-next-icons,\n"
                    <> selectorList (\i -> radioChecked i <> "+ .mj-carousel-content .mj-carousel-next-" <> tshow ((i + (1 `mod` len) + len) `mod` len + 1))
                    <> ",\n"
                    <> selectorList (\i -> radioChecked i <> "+ .mj-carousel-content .mj-carousel-previous-" <> tshow ((i - (1 `mod` len) + len) `mod` len + 1))
                    <> " { display: block !important; }"
                , selectorList (\i -> radioChecked i <> "+ .mj-carousel-content .mj-carousel-" <> cid <> "-thumbnail-" <> tshow (i + 1))
                    <> " { border-color: "
                    <> a "tb-selected-border-color" ""
                    <> " !important; }"
                , selectorList (\i -> radioChecked i <> "+ .mj-carousel-content .mj-carousel-" <> cid <> "-thumbnail\n          ")
                    <> " { display: inline-block !important; }"
                , ".mj-carousel-image img + div, .mj-carousel-thumbnail img + div { display: none !important; }"
                , selectorList (\i -> ".mj-carousel-" <> cid <> "-thumbnail:hover " <> rep "+ * " (len - i - 1) <> "+ .mj-carousel-main .mj-carousel-image")
                    <> " { display: none !important; }"
                , ".mj-carousel-thumbnail:hover { border-color: " <> a "tb-hover-border-color" "" <> " !important; }"
                , selectorList (\i -> ".mj-carousel-" <> cid <> "-thumbnail-" <> tshow (i + 1) <> ":hover " <> rep "+ * " (len - i - 1) <> "+ .mj-carousel-main .mj-carousel-image-" <> tshow (i + 1))
                    <> " { display: block !important; }"
                ]
        radioChecked i = ".mj-carousel-" <> cid <> "-radio-" <> tshow (i + 1) <> ":checked " <> rep "+ * " (len - i - 1)
        fallbackCss =
            T.unlines
                [ ".mj-carousel noinput { display:block !important; }"
                , ".mj-carousel noinput .mj-carousel-image-1 { display: block !important;  }"
                , ".mj-carousel noinput .mj-carousel-arrows, .mj-carousel noinput .mj-carousel-thumbnails { display: none !important; }"
                , "[owa] .mj-carousel-thumbnail { display: none !important; }"
                , "@media screen yahoo {"
                , ".mj-carousel-" <> cid <> "-icons-cell, .mj-carousel-previous-icons, .mj-carousel-next-icons { display: none !important; }"
                , ".mj-carousel-" <> cid <> "-radio-1:checked " <> rep "+ *" (len - 1) <> "+ .mj-carousel-content .mj-carousel-" <> cid <> "-thumbnail-1 { border-color: transparent; }"
                , "}"
                ]


mjCarouselImage :: Component
mjCarouselImage =
    (defComponent "mj-carousel-image" build')
        { endingTag = True
        , allowedAttributes =
            [ ("alt", "string")
            , ("href", "string")
            , ("rel", "string")
            , ("target", "string")
            , ("title", "string")
            , ("src", "string")
            , ("thumbnails-src", "string")
            , ("border-radius", "string")
            , ("tb-border", "string")
            , ("tb-border-radius", "string")
            ]
        , defaults = [("alt", ""), ("target", "_blank")]
        }
  where
    build' s = inst
      where
        p = props s
        i = index p
        imgIndex = tshow (i + 1)
        containerWidth = fromMaybe "" (ctx s "containerWidth")
        carouselId = attrOr s "carouselId" ""
        opt k = maybe ANone AV (attr s k)

        hasThumbnailsSupported =
            let thumbnails = case attr s "thumbnails" of
                    Just t | truthy (Just t) -> Just t
                    _ -> ctx s "thumbnails"
             in thumbnails == Just "supported"

        inst =
            (defInstance s render')
                { getStyles = \case
                    "images.img" ->
                        [ ("border-radius", attr s "border-radius")
                        , style "display" "block"
                        , style "width" containerWidth
                        , style "max-width" "100%"
                        , style "height" "auto"
                        ]
                    "images.firstImageDiv" -> []
                    "images.otherImageDiv" -> [style "display" "none", style "mso-hide" "all"]
                    "radio.input" -> [style "display" "none", style "mso-hide" "all"]
                    "thumbnails.a" ->
                        [ ("border", attr s "tb-border")
                        , ("border-radius", attr s "tb-border-radius")
                        , style "display" (if hasThumbnailsSupported then "none" else "inline-block")
                        , style "overflow" "hidden"
                        , ("width", attr s "tb-width")
                        ]
                    "thumbnails.img" -> [style "display" "block", style "width" "100%", style "height" "auto"]
                    _ -> []
                }

        render' = pure $ case lookup imageModeKey (extra p) of
            Just "radio" -> renderRadio
            Just "thumbnail" -> renderThumbnail
            _ -> renderImage

        renderThumbnail =
            mconcat
                [ "<a"
                , htmlAttributes
                    inst
                    [ ("style", AStyleOf "thumbnails.a")
                    , ("href", AV ("#" <> imgIndex))
                    , ("target", opt "target")
                    , ( "class"
                      , AV
                            ( "mj-carousel-thumbnail mj-carousel-"
                                <> carouselId
                                <> "-thumbnail mj-carousel-"
                                <> carouselId
                                <> "-thumbnail-"
                                <> imgIndex
                                <> " "
                                <> suffixCssClasses (attr s "css-class") "thumbnail"
                            )
                      )
                    ]
                , "><label"
                , htmlAttributes inst [("for", AV ("mj-carousel-" <> carouselId <> "-radio-" <> imgIndex))]
                , "><img"
                , htmlAttributes
                    inst
                    [ ("style", AStyleOf "thumbnails.img")
                    , ( "src"
                      , case attr s "thumbnails-src" of
                            Just t | truthy (Just t) -> AV t
                            _ -> opt "src"
                      )
                    , ("alt", opt "alt")
                    , ("width", AV (showNumber (parseIntJs (attrOr s "tb-width" ""))))
                    ]
                , " /></label></a>"
                ]

        renderRadio =
            "<input"
                <> htmlAttributes
                    inst
                    [ ("class", AV ("mj-carousel-radio mj-carousel-" <> carouselId <> "-radio mj-carousel-" <> carouselId <> "-radio-" <> imgIndex))
                    , ("checked", if i == 0 then AV "checked" else ANone)
                    , ("type", AV "radio")
                    , ("name", AV ("mj-carousel-radio-" <> carouselId))
                    , ("id", AV ("mj-carousel-" <> carouselId <> "-radio-" <> imgIndex))
                    , ("style", AStyleOf "radio.input")
                    ]
                <> " />"

        renderImage =
            let image =
                    "<img"
                        <> htmlAttributes
                            inst
                            [ ("title", opt "title")
                            , ("src", opt "src")
                            , ("alt", opt "alt")
                            , ("style", AStyleOf "images.img")
                            , ("width", AV (showNumber (parseIntJs containerWidth)))
                            , ("border", AV "0")
                            ]
                        <> " />"
                cssClass = attrOr s "css-class" ""
             in mconcat
                    [ "<div"
                    , htmlAttributes
                        inst
                        [ ("class", AV ("mj-carousel-image mj-carousel-image-" <> imgIndex <> " " <> cssClass))
                        , ("style", AStyleOf (if i == 0 then "images.firstImageDiv" else "images.otherImageDiv"))
                        ]
                    , ">"
                    , case attr s "href" of
                        Just href
                            | truthy (Just href) ->
                                "<a"
                                    <> htmlAttributes inst [("href", AV href), ("rel", opt "rel"), ("target", AV "_blank")]
                                    <> ">"
                                    <> image
                                    <> "</a>"
                        _ -> image
                    , "</div>"
                    ]