packages feed

datastar-hs-1.0.1.0: src/Hypermedia/Datastar/Attributes.hs

-- | Smart constructors and modifier combinators for Datastar HTML attributes.
--
-- Every Datastar attribute is a @(name, value)@ pair — the value of the
-- 'Attribute' type alias. Pair it with whatever HTML library you use:
--
-- > -- lucid2:
-- > button_ [ uncurry makeAttributes (onClick "$count++") ] "Inc"
--
-- Modifiers (the @__suffix@ parts of an attribute name) are plain
-- @Attribute -> Attribute@ functions you compose with '(.)' or '(&)':
--
-- > import Data.Function ((&))
-- > onInput "@get('/search')"
-- >   & debounce 200
-- >   & prevent
-- > -- => ("data-on:input__debounce.200ms__prevent", "@get('/search')")
--
-- For an event the library doesn't have a shortcut for, 'dataOn' takes any
-- 'Text':
--
-- > dataOn "my-custom-event" "..."
module Hypermedia.Datastar.Attributes
  ( -- * The attribute pair
    Attribute

    -- * Event handlers
  , dataOn
    -- ** Common event shortcuts
  , onClick
  , onSubmit
  , onInput
  , onChange
  , onFocus
  , onBlur
  , onKeyDown
  , onKeyUp
  , onKeyPress
  , onMouseDown
  , onMouseUp
  , onMouseMove
  , onMouseEnter
  , onMouseLeave
  , onMouseOver
  , onMouseOut
  , onWheel
  , onScroll
  , onResize
  , onContextMenu
  , onTouchStart
  , onTouchEnd
  , onTouchMove
  , onDragStart
  , onDragEnd
  , onDrop
  , onCopy
  , onCut
  , onPaste
  , onLoad
  , onAnimationStart
  , onAnimationEnd
  , onTransitionEnd

    -- * Reactive attributes
  , dataAttr
  , dataAttrs
  , dataClass
  , dataClasses
  , dataStyle
  , dataStyles
  , dataText
  , dataShow

    -- * Signals
  , dataSignals
  , dataSignalsJson
  , dataComputed
  , dataBind
  , dataBindValue
  , dataJsonSignals

    -- * Lifecycle / refs
  , dataInit
  , dataEffect
  , dataRef
  , dataRefValue
  , dataIndicator
  , dataIndicatorValue

    -- * Lifecycle events
  , dataOnIntersect
  , dataOnInterval
  , dataOnSignalPatch
  , dataOnSignalPatchFilter

    -- * Morph / ignore controls
  , dataIgnore
  , dataIgnoreMorph
  , dataPreserveAttr

    -- * Modifier combinators
    -- ** Event flags
  , prevent
  , stop
  , once
  , capture
  , passive
  , window
  , document_
  , outside
  , viewTransition
    -- ** Timing
  , debounce
  , debounceWith
  , throttle
  , throttleWith
  , delay
    -- ** Casing
  , camelCase
  , kebabCase
  , snakeCase
  , pascalCase
    -- ** on-intersect
  , full
  , half
  , threshold
  , exit
    -- ** on-interval
  , duration
  , durationWith
    -- ** bind
  , bindProp
  , bindEvents
    -- ** signals \/ json-signals \/ ignore
  , ifMissing
  , terse
  , self

    -- * Modifier tag constants
    -- $tags
  , leading
  , noTrailing
  , noLeading
  , trailing

    -- * Escape hatches
  , withModifier
  , withModifierTags
  , rawAttr
  ) where

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

-- | A @(name, value)@ pair ready to feed into any HTML library.
type Attribute = (Text, Text)

-- ---------------------------------------------------------------------------
-- Event handlers
-- ---------------------------------------------------------------------------

-- | @data-on:\<event\>="\<expr\>"@. Use the shortcuts below for common events,
-- or call directly for custom events (e.g. @dataOn \"my-custom-event\" ...@).
dataOn :: Text -> Text -> Attribute
dataOn ev expr = ("data-on:" <> ev, expr)

onClick, onSubmit, onInput, onChange, onFocus, onBlur :: Text -> Attribute
onClick  = dataOn "click"
onSubmit = dataOn "submit"
onInput  = dataOn "input"
onChange = dataOn "change"
onFocus  = dataOn "focus"
onBlur   = dataOn "blur"

onKeyDown, onKeyUp, onKeyPress :: Text -> Attribute
onKeyDown  = dataOn "keydown"
onKeyUp    = dataOn "keyup"
onKeyPress = dataOn "keypress"

onMouseDown, onMouseUp, onMouseMove,
  onMouseEnter, onMouseLeave, onMouseOver, onMouseOut :: Text -> Attribute
onMouseDown  = dataOn "mousedown"
onMouseUp    = dataOn "mouseup"
onMouseMove  = dataOn "mousemove"
onMouseEnter = dataOn "mouseenter"
onMouseLeave = dataOn "mouseleave"
onMouseOver  = dataOn "mouseover"
onMouseOut   = dataOn "mouseout"

onWheel, onScroll, onResize, onContextMenu :: Text -> Attribute
onWheel       = dataOn "wheel"
onScroll      = dataOn "scroll"
onResize      = dataOn "resize"
onContextMenu = dataOn "contextmenu"

onTouchStart, onTouchEnd, onTouchMove :: Text -> Attribute
onTouchStart = dataOn "touchstart"
onTouchEnd   = dataOn "touchend"
onTouchMove  = dataOn "touchmove"

onDragStart, onDragEnd, onDrop :: Text -> Attribute
onDragStart = dataOn "dragstart"
onDragEnd   = dataOn "dragend"
onDrop      = dataOn "drop"

onCopy, onCut, onPaste :: Text -> Attribute
onCopy  = dataOn "copy"
onCut   = dataOn "cut"
onPaste = dataOn "paste"

onLoad :: Text -> Attribute
onLoad = dataOn "load"

onAnimationStart, onAnimationEnd, onTransitionEnd :: Text -> Attribute
onAnimationStart = dataOn "animationstart"
onAnimationEnd   = dataOn "animationend"
onTransitionEnd  = dataOn "transitionend"

-- ---------------------------------------------------------------------------
-- Reactive attributes
-- ---------------------------------------------------------------------------

-- | @data-attr:\<name\>="\<expr\>"@
dataAttr :: Text -> Text -> Attribute
dataAttr name expr = ("data-attr:" <> name, expr)

-- | @data-attr="\<jsObject\>"@ — set multiple attributes at once.
dataAttrs :: Text -> Attribute
dataAttrs jsObj = ("data-attr", jsObj)

-- | @data-class:\<name\>="\<expr\>"@
dataClass :: Text -> Text -> Attribute
dataClass name expr = ("data-class:" <> name, expr)

-- | @data-class="\<jsObject\>"@ — toggle several classes from one expression.
dataClasses :: Text -> Attribute
dataClasses jsObj = ("data-class", jsObj)

-- | @data-style:\<property\>="\<expr\>"@
dataStyle :: Text -> Text -> Attribute
dataStyle prop expr = ("data-style:" <> prop, expr)

-- | @data-style="\<jsObject\>"@ — set several style properties at once.
dataStyles :: Text -> Attribute
dataStyles jsObj = ("data-style", jsObj)

-- | @data-text="\<expr\>"@
dataText :: Text -> Attribute
dataText expr = ("data-text", expr)

-- | @data-show="\<expr\>"@
dataShow :: Text -> Attribute
dataShow expr = ("data-show", expr)

-- ---------------------------------------------------------------------------
-- Signals
-- ---------------------------------------------------------------------------

-- | @data-signals:\<name\>="\<expr\>"@
dataSignals :: Text -> Text -> Attribute
dataSignals name expr = ("data-signals:" <> name, expr)

-- | @data-signals="\<jsObject\>"@
dataSignalsJson :: Text -> Attribute
dataSignalsJson jsObj = ("data-signals", jsObj)

-- | @data-computed:\<name\>="\<expr\>"@
dataComputed :: Text -> Text -> Attribute
dataComputed name expr = ("data-computed:" <> name, expr)

-- | @data-bind:\<signal\>@
dataBind :: Text -> Attribute
dataBind signal = ("data-bind:" <> signal, "")

-- | @data-bind="\<signal\>"@
dataBindValue :: Text -> Attribute
dataBindValue signal = ("data-bind", signal)

-- | @data-json-signals@. Wrap with 'terse' for compact output.
dataJsonSignals :: Attribute
dataJsonSignals = ("data-json-signals", "")

-- ---------------------------------------------------------------------------
-- Lifecycle / refs
-- ---------------------------------------------------------------------------

-- | @data-init="\<expr\>"@
dataInit :: Text -> Attribute
dataInit expr = ("data-init", expr)

-- | @data-effect="\<expr\>"@
dataEffect :: Text -> Attribute
dataEffect expr = ("data-effect", expr)

-- | @data-ref:\<name\>@
dataRef :: Text -> Attribute
dataRef name = ("data-ref:" <> name, "")

-- | @data-ref="\<name\>"@
dataRefValue :: Text -> Attribute
dataRefValue name = ("data-ref", name)

-- | @data-indicator:\<signal\>@
dataIndicator :: Text -> Attribute
dataIndicator signal = ("data-indicator:" <> signal, "")

-- | @data-indicator="\<signal\>"@
dataIndicatorValue :: Text -> Attribute
dataIndicatorValue signal = ("data-indicator", signal)

-- ---------------------------------------------------------------------------
-- Lifecycle events
-- ---------------------------------------------------------------------------

-- | @data-on-intersect="\<expr\>"@. Combine with 'full', 'half', 'threshold',
-- 'once', 'exit'.
dataOnIntersect :: Text -> Attribute
dataOnIntersect expr = ("data-on-intersect", expr)

-- | @data-on-interval="\<expr\>"@. Combine with 'duration'.
dataOnInterval :: Text -> Attribute
dataOnInterval expr = ("data-on-interval", expr)

-- | @data-on-signal-patch="\<expr\>"@. Pair with 'dataOnSignalPatchFilter'.
dataOnSignalPatch :: Text -> Attribute
dataOnSignalPatch expr = ("data-on-signal-patch", expr)

-- | @data-on-signal-patch-filter="\<jsObject\>"@
dataOnSignalPatchFilter :: Text -> Attribute
dataOnSignalPatchFilter v = ("data-on-signal-patch-filter", v)

-- ---------------------------------------------------------------------------
-- Morph / ignore controls
-- ---------------------------------------------------------------------------

-- | @data-ignore@. Wrap with 'self' for the self-only variant.
dataIgnore :: Attribute
dataIgnore = ("data-ignore", "")

-- | @data-ignore-morph@
dataIgnoreMorph :: Attribute
dataIgnoreMorph = ("data-ignore-morph", "")

-- | @data-preserve-attr="\<comma-list\>"@
dataPreserveAttr :: Text -> Attribute
dataPreserveAttr names = ("data-preserve-attr", names)

-- ---------------------------------------------------------------------------
-- Modifier combinators
-- ---------------------------------------------------------------------------

-- | Append @__\<label\>@ to the attribute name.
withModifier :: Text -> Attribute -> Attribute
withModifier label (k, v) = (k <> "__" <> label, v)

-- | Append @__\<label\>.\<tag\>.\<tag\>...@ to the attribute name.
withModifierTags :: Text -> [Text] -> Attribute -> Attribute
withModifierTags label tags (k, v) =
  (k <> "__" <> label <> T.concat (map ("." <>) tags), v)

-- | Build any @(name, value)@ pair directly. Escape hatch for attributes or
-- modifiers this module doesn't model.
rawAttr :: Text -> Text -> Attribute
rawAttr = (,)

-- Event flags --------------------------------------------------------------

prevent, stop, once, capture, passive :: Attribute -> Attribute
prevent = withModifier "prevent"
stop    = withModifier "stop"
once    = withModifier "once"
capture = withModifier "capture"
passive = withModifier "passive"

-- | Attach the listener to @window@ instead of the element.
window :: Attribute -> Attribute
window = withModifier "window"

-- | Attach the listener to @document@ instead of the element.
document_ :: Attribute -> Attribute
document_ = withModifier "document"

-- | Fire only when the event target is outside the element.
outside :: Attribute -> Attribute
outside = withModifier "outside"

-- | Wrap the handler in a View Transition.
viewTransition :: Attribute -> Attribute
viewTransition = withModifier "viewtransition"

-- Timing -------------------------------------------------------------------

-- | @__debounce.\<ms\>ms@
debounce :: Int -> Attribute -> Attribute
debounce ms = withModifierTags "debounce" [renderMs ms]

-- | @__debounce.\<ms\>ms.\<tag\>...@ — pass 'leading' and\/or 'noTrailing'.
debounceWith :: Int -> [Text] -> Attribute -> Attribute
debounceWith ms tags = withModifierTags "debounce" (renderMs ms : tags)

-- | @__throttle.\<ms\>ms@
throttle :: Int -> Attribute -> Attribute
throttle ms = withModifierTags "throttle" [renderMs ms]

-- | @__throttle.\<ms\>ms.\<tag\>...@ — pass 'noLeading' and\/or 'trailing'.
throttleWith :: Int -> [Text] -> Attribute -> Attribute
throttleWith ms tags = withModifierTags "throttle" (renderMs ms : tags)

-- | @__delay.\<ms\>ms@
delay :: Int -> Attribute -> Attribute
delay ms = withModifierTags "delay" [renderMs ms]

-- Casing -------------------------------------------------------------------

camelCase, kebabCase, snakeCase, pascalCase :: Attribute -> Attribute
camelCase  = withModifierTags "case" ["camel"]
kebabCase  = withModifierTags "case" ["kebab"]
snakeCase  = withModifierTags "case" ["snake"]
pascalCase = withModifierTags "case" ["pascal"]

-- on-intersect -------------------------------------------------------------

-- | Trigger only when 100% of the element is visible.
full :: Attribute -> Attribute
full = withModifier "full"

-- | Trigger when 50% of the element is visible.
half :: Attribute -> Attribute
half = withModifier "half"

-- | @__threshold.\<percent\>@ (0..100, clamped at runtime).
threshold :: Int -> Attribute -> Attribute
threshold pct = withModifierTags "threshold" [T.pack (show pct)]

-- | Trigger on exit rather than entry.
exit :: Attribute -> Attribute
exit = withModifier "exit"

-- on-interval --------------------------------------------------------------

-- | @__duration.\<ms\>ms@
duration :: Int -> Attribute -> Attribute
duration ms = withModifierTags "duration" [renderMs ms]

-- | @__duration.\<ms\>ms.\<tag\>...@ — pass 'leading' to fire immediately.
durationWith :: Int -> [Text] -> Attribute -> Attribute
durationWith ms tags = withModifierTags "duration" (renderMs ms : tags)

-- bind ---------------------------------------------------------------------

-- | @__prop.\<name\>@ — bind to an element property instead of an attribute.
bindProp :: Text -> Attribute -> Attribute
bindProp name = withModifierTags "prop" [name]

-- | @__event.\<ev\>.\<ev\>...@ — which DOM events trigger sync.
bindEvents :: [Text] -> Attribute -> Attribute
bindEvents events = withModifierTags "event" events

-- signals / json-signals / ignore -----------------------------------------

-- | @__ifmissing@ — only set the signal if it doesn't already exist.
ifMissing :: Attribute -> Attribute
ifMissing = withModifier "ifmissing"

-- | @__terse@ — emit JSON without indentation.
terse :: Attribute -> Attribute
terse = withModifier "terse"

-- | @__self@ — restrict @data-ignore@ to the element itself, not descendants.
self :: Attribute -> Attribute
self = withModifier "self"

-- ---------------------------------------------------------------------------
-- Tag constants
-- ---------------------------------------------------------------------------

-- $tags
-- Plain 'Text' values for use with the @*With@ combinators.

leading, noTrailing, noLeading, trailing :: Text
leading    = "leading"
noTrailing = "notrailing"
noLeading  = "noleading"
trailing   = "trailing"

-- ---------------------------------------------------------------------------
-- Internal
-- ---------------------------------------------------------------------------

renderMs :: Int -> Text
renderMs n = T.pack (show n) <> "ms"