PageRenderTime 60ms CodeModel.GetById 11ms RepoModel.GetById 1ms app.codeStats 0ms

/html/Shpadoinkle/Html/Event.hs

https://gitlab.com/platonic/shpadoinkle
Haskell | 207 lines | 123 code | 57 blank | 27 comment | 0 complexity | 6cb5a31b8d4d48fd5f93153657528c7c MD5 | raw file
  1. {-# LANGUAGE AllowAmbiguousTypes #-}
  2. {-# LANGUAGE CPP #-}
  3. {-# LANGUAGE FlexibleContexts #-}
  4. {-# LANGUAGE LambdaCase #-}
  5. {-# LANGUAGE OverloadedStrings #-}
  6. {-# LANGUAGE ScopedTypeVariables #-}
  7. {-# LANGUAGE TemplateHaskell #-}
  8. {-# LANGUAGE TypeApplications #-}
  9. -- | This module provides a DSL of Events found on HTML elements.
  10. -- This DSL is entirely optional. You may use the 'Prop's 'PListener' constructor
  11. -- provided by Shpadoinkle core and completely ignore this module.
  12. -- You can use the 'listener', 'listen', 'listenRaw', 'listenC', and 'listenM' convenience
  13. -- functions as well without using this module. For those who like a typed
  14. -- DSL with named functions and overloading, this is for you.
  15. --
  16. -- All listeners come in 4 flavors. Unctuous flavors. Plain ('onInput'), continuous ('onInputC'), monadic ('onInputM'), and forgetful ('onInputM_').
  17. --
  18. -- A flavor providing access to the 'RawNode' and the 'RawEvent' are not provided
  19. -- here. If you want access to these, try the 'listenRaw' constructor. The intent
  20. -- of this DSL is to provide simple named functions.
  21. --
  22. -- Right now this module features limited specialization, but ideally we specialize
  23. -- all of these listeners. For example, the 'onInput' listener takes a function
  24. -- @(Text -> a -> a)@ where 'Text' is the current value of the input and 'onKeyup' takes
  25. -- a function of type @(KeyCode -> a -> a)@ from 'Shpadoinkle.Keyboard'. Mouse move
  26. -- listeners, for example, should take a function of @((Float, Float) -> a -> a)@, but
  27. -- this work is not yet done.
  28. module Shpadoinkle.Html.Event
  29. ( module Shpadoinkle.Html.Event
  30. , module Shpadoinkle.Html.Event.Basic
  31. ) where
  32. import Control.Concurrent.STM (retry)
  33. import Control.Lens ((^.))
  34. import Control.Monad (unless, void)
  35. import Control.Monad.IO.Class (liftIO)
  36. import Data.Text
  37. import GHCJS.DOM.Types hiding (Text)
  38. import Language.Javascript.JSaddle hiding (JSM, liftJSM, toJSString)
  39. import UnliftIO.Concurrent (forkIO)
  40. import UnliftIO.STM
  41. import Shpadoinkle
  42. import Shpadoinkle.Html.Event.Basic
  43. import Shpadoinkle.Html.TH
  44. import Shpadoinkle.Keyboard
  45. mkWithFormVal :: (JSVal -> JSM v) -> Text -> JSString -> (v -> Continuation m a) -> (Text, Prop m a)
  46. mkWithFormVal valTo evt from f = listenRaw evt $ \(RawNode n) _ ->
  47. f <$> liftJSM (valTo =<< unsafeGetProp from =<< valToObject n)
  48. onInputC :: (Text -> Continuation m a) -> (Text, Prop m a)
  49. onInputC = mkWithFormVal valToText "input" "value"
  50. $(mkEventVariantsAfforded "input" ''Text)
  51. onBeforeinputC :: (Text -> Continuation m a) -> (Text, Prop m a)
  52. onBeforeinputC = mkWithFormVal valToText "beforeinput" "value"
  53. $(mkEventVariantsAfforded "beforeinput" ''Text)
  54. onOptionC :: (Text -> Continuation m a) -> (Text, Prop m a)
  55. onOptionC = mkWithFormVal valToText "change" "value"
  56. $(mkEventVariantsAfforded "option" ''Text)
  57. mkOnKey :: Text -> (KeyCode -> Continuation m a) -> (Text, Prop m a)
  58. mkOnKey t f = listenRaw t $ \_ (RawEvent e) ->
  59. f <$> liftJSM (fmap round $ valToNumber =<< unsafeGetProp "keyCode" =<< valToObject e)
  60. onKeyupC, onKeydownC, onKeypressC :: (KeyCode -> Continuation m a) -> (Text, Prop m a)
  61. onKeyupC = mkOnKey "keyup"
  62. onKeydownC = mkOnKey "keydown"
  63. onKeypressC = mkOnKey "keypress"
  64. $(mkEventVariantsAfforded "keyup" ''KeyCode)
  65. $(mkEventVariantsAfforded "keydown" ''KeyCode)
  66. $(mkEventVariantsAfforded "keypress" ''KeyCode)
  67. onCheckC :: (Bool -> Continuation m a) -> (Text, Prop m a)
  68. onCheckC = mkWithFormVal valToBool "change" "checked"
  69. $(mkEventVariantsAfforded "check" ''Bool)
  70. preventDefault :: RawEvent -> JSM ()
  71. preventDefault e = void $ valToObject e # ("preventDefault" :: String) $ ([] :: [()])
  72. stopPropagation :: RawEvent -> JSM ()
  73. stopPropagation e = void $ valToObject e # ("stopPropagation" :: String) $ ([] :: [()])
  74. onSubmitC :: Continuation m a -> (Text, Prop m a)
  75. onSubmitC m = listenRaw "submit" $ \_ e -> preventDefault e >> return m
  76. $(mkEventVariants "submit")
  77. mkGlobalMailbox :: Continuation m a -> JSM (JSM (), STM (Continuation m a))
  78. mkGlobalMailbox c = do
  79. (notify, stream) <- mkGlobalMailboxAfforded (const c)
  80. return (notify (), stream)
  81. mkGlobalMailboxAfforded :: (b -> Continuation m a) -> JSM (b -> JSM (), STM (Continuation m a))
  82. mkGlobalMailboxAfforded bc = do
  83. (notify, twas) <- liftIO $ (,) <$> newTVarIO (0, Nothing) <*> newTVarIO (0 :: Int)
  84. return (\b -> atomically $ modifyTVar notify (\(i, _) -> (i + 1, Just b)), do
  85. (new', b) <- readTVar notify
  86. old <- readTVar twas
  87. case b of
  88. Just b' | new' /= old -> bc b' <$ writeTVar twas new'
  89. _ -> retry)
  90. onClickAwayC :: Continuation m a -> (Text, Prop m a)
  91. onClickAwayC c =
  92. ( "onclickaway"
  93. , PPotato $ \(RawNode elm) -> liftJSM $ do
  94. (notify, stream) <- mkGlobalMailbox c
  95. void $ jsg ("document" :: Text) ^. js2 ("addEventListener" :: Text) ("click" :: Text)
  96. (fun $ \_ _ -> \case
  97. evt:_ -> void . forkIO $ do
  98. target <- evt ^. js ("target" :: Text)
  99. onTarget <- fromJSVal =<< elm ^. js1 ("contains" :: Text) target
  100. case onTarget of
  101. Just False -> notify
  102. _ -> return ()
  103. [] -> pure ())
  104. return stream
  105. )
  106. $(mkEventVariants "clickAway")
  107. mkGlobalKey :: Text -> (KeyCode -> Continuation m a) -> (Text, Prop m a)
  108. mkGlobalKey evtName c =
  109. ( "global" <> evtName
  110. , PPotato $ \_ -> liftJSM $ do
  111. (notify, stream) <- mkGlobalMailboxAfforded c
  112. void $ jsg ("window" :: Text) ^. js2 ("addEventListener" :: Text) evtName
  113. (fun $ \_ _ -> \case
  114. e:_ -> notify . round =<< valToNumber =<< unsafeGetProp "keyCode" =<< valToObject e
  115. [] -> return ())
  116. return stream
  117. )
  118. mkGlobalKeyNoRepeat :: Text -> (KeyCode -> Continuation m a) -> (Text, Prop m a)
  119. mkGlobalKeyNoRepeat evtName c =
  120. ( "global" <> evtName
  121. , PPotato $ \_ -> liftJSM $ do
  122. (notify, stream) <- mkGlobalMailboxAfforded c
  123. void $ jsg ("window" :: Text) ^. js2 ("addEventListener" :: Text) evtName
  124. (fun $ \_ _ -> \case
  125. e:_ -> do
  126. eObj <- valToObject e
  127. isRepeat <- valToBool =<< unsafeGetProp "repeat" eObj
  128. unless isRepeat $
  129. notify . round =<< valToNumber =<< unsafeGetProp "keyCode" eObj
  130. [] -> return ())
  131. return stream
  132. )
  133. onGlobalKeyPressC, onGlobalKeyDownC, onGlobalKeyUpC :: (KeyCode -> Continuation m a) -> (Text, Prop m a)
  134. onGlobalKeyPressC = mkGlobalKey "keypress"
  135. onGlobalKeyDownC = mkGlobalKey "keydown"
  136. onGlobalKeyUpC = mkGlobalKey "keyup"
  137. $(mkEventVariantsAfforded "globalKeyPress" ''KeyCode)
  138. $(mkEventVariantsAfforded "globalKeyDown" ''KeyCode)
  139. $(mkEventVariantsAfforded "globalKeyUp" ''KeyCode)
  140. onGlobalKeyDownNoRepeatC :: (KeyCode -> Continuation m a) -> (Text, Prop m a)
  141. onGlobalKeyDownNoRepeatC = mkGlobalKeyNoRepeat "keydown"
  142. $(mkEventVariantsAfforded "globalKeyDownNoRepeat" ''KeyCode)
  143. onEscapeC :: Continuation m a -> (Text, Prop m a)
  144. onEscapeC c = onKeyupC $ \case 27 -> c; _ -> done
  145. $(mkEventVariants "escape")
  146. onEnterC :: (Text -> Continuation m a) -> (Text, Prop m a)
  147. onEnterC f = listenRaw "keyup" $ \(RawNode n) _ -> liftJSM $
  148. f <$> (valToText =<< unsafeGetProp "value"
  149. =<< valToObject n)
  150. $(mkEventVariantsAfforded "enter" ''Text)