{- |
Module      : Hypermedia.Datastar.ExecuteScript
Description : Execute JavaScript in the browser via SSE

Sometimes you need a one-shot browser-side effect that doesn't fit neatly
into DOM patching or signal updates — redirecting to another page, focusing
an input, triggering a download, or calling a browser API.
'executeScript' lets the server push arbitrary JavaScript to the browser.

Under the hood, Datastar appends a @\<script\>@ tag to @\<body\>@. By default
the script tag removes itself from the DOM after executing (see 'esAutoRemove').

Note: per the Datastar protocol, script execution uses the
@datastar-patch-elements@ event type — there is no separate event type for
scripts.

@
sendExecuteScript gen (executeScript \"window.location = \\\"/dashboard\\\"\")
@

To add attributes to the generated @\<script\>@ tag:

@
sendExecuteScript gen
  (executeScript \"import(\\\"/modules\/chart.js\\\").then(m => m.render())\")
    { esAttributes = [\"type=\\\"module\\\"\"] }
@
-}
module Hypermedia.Datastar.ExecuteScript where

import Data.Text (Text)
import Data.Text qualified as T
import Hypermedia.Datastar.Types

{- | Configuration for executing a script in the browser.

Construct values with 'executeScript', then customise with record updates.
-}
data ExecuteScript = ExecuteScript
  { ExecuteScript -> Text
esScript :: Text
  -- ^ The JavaScript code to execute.
  , ExecuteScript -> Bool
esAutoRemove :: Bool
  {- ^ Whether the @\<script\>@ tag should remove itself from the DOM after
  executing. Default: 'True'. Set to 'False' if the script defines
  functions or variables that need to persist in the page.
  -}
  , ExecuteScript -> [Text]
esAttributes :: [Text]
  {- ^ Extra attributes to add to the @\<script\>@ tag. For example,
  @[\"type=\\\"module\\\"\"]@ to use ES module imports, or
  @[\"nonce=\\\"abc123\\\"\"]@ for CSP compliance.
  -}
  , ExecuteScript -> Maybe Text
esEventId :: Maybe Text
  -- ^ Optional SSE event ID for reconnection.
  , ExecuteScript -> Int
esRetryDuration :: Int
  -- ^ SSE retry interval in milliseconds. Default: @1000@.
  }
  deriving (ExecuteScript -> ExecuteScript -> Bool
(ExecuteScript -> ExecuteScript -> Bool)
-> (ExecuteScript -> ExecuteScript -> Bool) -> Eq ExecuteScript
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: ExecuteScript -> ExecuteScript -> Bool
== :: ExecuteScript -> ExecuteScript -> Bool
$c/= :: ExecuteScript -> ExecuteScript -> Bool
/= :: ExecuteScript -> ExecuteScript -> Bool
Eq, Int -> ExecuteScript -> ShowS
[ExecuteScript] -> ShowS
ExecuteScript -> String
(Int -> ExecuteScript -> ShowS)
-> (ExecuteScript -> String)
-> ([ExecuteScript] -> ShowS)
-> Show ExecuteScript
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> ExecuteScript -> ShowS
showsPrec :: Int -> ExecuteScript -> ShowS
$cshow :: ExecuteScript -> String
show :: ExecuteScript -> String
$cshowList :: [ExecuteScript] -> ShowS
showList :: [ExecuteScript] -> ShowS
Show)

{- | Build an 'ExecuteScript' event with sensible defaults.

The argument is the JavaScript source code to run in the browser.

@
executeScript \"document.getElementById(\\\"name\\\").focus()\"
@
-}
executeScript :: Text -> ExecuteScript
executeScript :: Text -> ExecuteScript
executeScript Text
js =
  ExecuteScript
    { esScript :: Text
esScript = Text
js
    , esAutoRemove :: Bool
esAutoRemove = Bool
defaultAutoRemove
    , esAttributes :: [Text]
esAttributes = []
    , esEventId :: Maybe Text
esEventId = Maybe Text
forall a. Maybe a
Nothing
    , esRetryDuration :: Int
esRetryDuration = Int
defaultRetryDuration
    }

toDatastarEvent :: ExecuteScript -> DatastarEvent
toDatastarEvent :: ExecuteScript -> DatastarEvent
toDatastarEvent ExecuteScript
es =
  DatastarEvent
    { eventType :: EventType
eventType = EventType
EventPatchElements -- Correct, there is no EventExecuteScript, see the ADR
    , eventId :: Maybe Text
eventId = ExecuteScript -> Maybe Text
esEventId ExecuteScript
es
    , retry :: Int
retry = ExecuteScript -> Int
esRetryDuration ExecuteScript
es
    , dataLines :: [Text]
dataLines =
        [ Text
"selector body"
        , Text
"mode append"
        ]
          [Text] -> [Text] -> [Text]
forall a. Semigroup a => a -> a -> a
<> ExecuteScript -> [Text]
buildScriptLines ExecuteScript
es
    }

buildScriptLines :: ExecuteScript -> [Text]
buildScriptLines :: ExecuteScript -> [Text]
buildScriptLines ExecuteScript
es =
  case Text -> [Text]
T.lines (ExecuteScript -> Text
esScript ExecuteScript
es) of
    [] -> [Text
"elements " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
openTag Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
closeTag]
    [Text
single] -> [Text
"elements " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
openTag Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
single Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
closeTag]
    [Text]
multiple ->
      [Text
"elements " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
openTag]
        [Text] -> [Text] -> [Text]
forall a. Semigroup a => a -> a -> a
<> (Text -> Text) -> [Text] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map (Text
"elements " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<>) [Text]
multiple
        [Text] -> [Text] -> [Text]
forall a. Semigroup a => a -> a -> a
<> [Text
"elements " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
closeTag]
 where
  openTag :: Text
  openTag :: Text
openTag =
    Text
"<script"
      Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> (if ExecuteScript -> Bool
esAutoRemove ExecuteScript
es then Text
" data-effect=\"el.remove()\"" else Text
"")
      Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> (Text -> Text) -> [Text] -> Text
forall m a. Monoid m => (a -> m) -> [a] -> m
forall (t :: * -> *) m a.
(Foldable t, Monoid m) =>
(a -> m) -> t a -> m
foldMap (Text
" " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<>) (ExecuteScript -> [Text]
esAttributes ExecuteScript
es)
      Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
">"

  closeTag :: Text
  closeTag :: Text
closeTag = Text
"</script>"