reflex-dom-0.1: src/Reflex/Dom/Widget/Input.hs
{-# LANGUAGE ConstraintKinds, TypeFamilies, FlexibleContexts, DataKinds, GADTs, ScopedTypeVariables, FlexibleInstances, RecursiveDo, TemplateHaskell #-}
module Reflex.Dom.Widget.Input (module Reflex.Dom.Widget.Input, def, (&), (.~)) where
import Prelude
import Reflex.Dom.Class
import Reflex.Dom.Widget.Basic
import Reflex
import Reflex.Host.Class
import GHCJS.DOM.HTMLInputElement
import GHCJS.DOM.HTMLTextAreaElement
import GHCJS.DOM.Element
import GHCJS.DOM.HTMLSelectElement
import GHCJS.DOM.EventM
import GHCJS.DOM.UIEvent
import Data.Monoid
import Data.Map as Map
import Control.Lens
import Control.Monad hiding (forM_)
import Control.Monad.IO.Class
import Data.Default
import Data.Maybe
import Safe
import Data.Dependent.Sum (DSum (..))
data TextInput t
= TextInput { _textInput_value :: Dynamic t String
, _textInput_keypress :: Event t Int
, _textInput_keydown :: Event t Int
, _textInput_keyup :: Event t Int
, _textInput_hasFocus :: Dynamic t Bool
, _textInput_element :: HTMLInputElement
}
data TextInputConfig t
= TextInputConfig { _textInputConfig_inputType :: String
, _textInputConfig_initialValue :: String
, _textInputConfig_setValue :: Event t String
, _textInputConfig_attributes :: Dynamic t (Map String String)
}
instance Reflex t => Default (TextInputConfig t) where
def = TextInputConfig { _textInputConfig_inputType = "text"
, _textInputConfig_initialValue = ""
, _textInputConfig_setValue = never
, _textInputConfig_attributes = constDyn mempty
}
textInput :: MonadWidget t m => TextInputConfig t -> m (TextInput t)
textInput (TextInputConfig inputType initial eSetValue dAttrs) = do
e <- liftM castToHTMLInputElement $ buildEmptyElement "input" =<< mapDyn (Map.insert "type" inputType) dAttrs
liftIO $ htmlInputElementSetValue e initial
performEvent_ $ fmap (liftIO . htmlInputElementSetValue e) eSetValue
eChange <- wrapDomEvent e elementOninput $ liftIO $ htmlInputElementGetValue e
postGui <- askPostGui
runWithActions <- askRunWithActions
eChangeFocus <- newEventWithTrigger $ \eChangeFocusTrigger -> do
unsubscribeOnblur <- liftIO $ elementOnblur e $ liftIO $ do
postGui $ runWithActions [eChangeFocusTrigger :=> False]
unsubscribeOnfocus <- liftIO $ elementOnfocus e $ liftIO $ do
postGui $ runWithActions [eChangeFocusTrigger :=> True]
return $ liftIO $ unsubscribeOnblur >> unsubscribeOnfocus
dFocus <- holdDyn False eChangeFocus
eKeypress <- wrapDomEvent e elementOnkeypress getKeyEvent
eKeydown <- wrapDomEvent e elementOnkeydown getKeyEvent
eKeyup <- wrapDomEvent e elementOnkeyup getKeyEvent
dValue <- holdDyn initial $ leftmost [eSetValue, eChange]
return $ TextInput dValue eKeypress eKeydown eKeyup dFocus e
textInputGetEnter :: Reflex t => TextInput t -> Event t ()
textInputGetEnter i = fmapMaybe (\n -> if n == keycodeEnter then Just () else Nothing) $ _textInput_keypress i
data TextAreaConfig t
= TextAreaConfig { _textAreaConfig_initialValue :: String
, _textAreaConfig_setValue :: Event t String
, _textAreaConfig_attributes :: Dynamic t (Map String String)
}
instance Reflex t => Default (TextAreaConfig t) where
def = TextAreaConfig { _textAreaConfig_initialValue = ""
, _textAreaConfig_setValue = never
, _textAreaConfig_attributes = constDyn mempty
}
data TextArea t
= TextArea { _textArea_value :: Dynamic t String
, _textArea_element :: HTMLTextAreaElement
, _textArea_hasFocus :: Dynamic t Bool
, _textArea_keypress :: Event t Int
}
textArea :: MonadWidget t m => TextAreaConfig t -> m (TextArea t)
textArea (TextAreaConfig initial eSet attrs) = do
e <- liftM castToHTMLTextAreaElement $ buildEmptyElement "textarea" attrs
liftIO $ htmlTextAreaElementSetValue e initial
postGui <- askPostGui
runWithActions <- askRunWithActions
eChangeFocus <- newEventWithTrigger $ \eChangeFocusTrigger -> do
unsubscribeOnblur <- liftIO $ elementOnblur e $ liftIO $ do
postGui $ runWithActions [eChangeFocusTrigger :=> False]
unsubscribeOnfocus <- liftIO $ elementOnfocus e $ liftIO $ do
postGui $ runWithActions [eChangeFocusTrigger :=> True]
return $ liftIO $ unsubscribeOnblur >> unsubscribeOnfocus
performEvent_ $ fmap (liftIO . htmlTextAreaElementSetValue e) eSet
f <- holdDyn False eChangeFocus
ev <- wrapDomEvent e elementOninput $ liftIO $ htmlTextAreaElementGetValue e
v <- holdDyn initial $ leftmost [eSet, ev]
eKeypress <- wrapDomEvent e elementOnkeypress getKeyEvent
return $ TextArea v e f eKeypress
data CheckboxConfig t
= CheckboxConfig { _checkboxConfig_setValue :: Event t Bool
, _checkboxConfig_attributes :: Dynamic t (Map String String)
}
instance Reflex t => Default (CheckboxConfig t) where
def = CheckboxConfig { _checkboxConfig_setValue = never
, _checkboxConfig_attributes = constDyn mempty
}
data Checkbox t
= Checkbox { _checkbox_value :: Dynamic t Bool
}
--TODO: Make attributes possibly dynamic
-- | Create an editable checkbox
-- Note: if the "type" or "checked" attributes are provided as attributes, they will be ignored
checkbox :: MonadWidget t m => Bool -> CheckboxConfig t -> m (Checkbox t)
checkbox checked config = do
attrs <- mapDyn (\c -> Map.insert "type" "checkbox" $ (if checked then Map.insert "checked" "checked" else Map.delete "checked") c) (_checkboxConfig_attributes config)
e <- liftM castToHTMLInputElement $ buildEmptyElement "input" attrs
eClick <- wrapDomEvent e elementOnclick $ liftIO $ htmlInputElementGetChecked e
performEvent_ $ fmap (\v -> liftIO $ htmlInputElementSetChecked e $! v) $ _checkboxConfig_setValue config
dValue <- holdDyn checked $ leftmost [_checkboxConfig_setValue config, eClick]
return $ Checkbox dValue
checkboxView :: MonadWidget t m => Dynamic t (Map String String) -> Dynamic t Bool -> m (Event t Bool)
checkboxView dAttrs dValue = do
e <- liftM castToHTMLInputElement $ buildEmptyElement "input" =<< mapDyn (Map.insert "type" "checkbox") dAttrs
eClicked <- wrapDomEvent e elementOnclick $ do
preventDefault
liftIO $ htmlInputElementGetChecked e
schedulePostBuild $ do
v <- sample $ current dValue
when v $ liftIO $ htmlInputElementSetChecked e True
performEvent_ $ fmap (\v -> liftIO $ htmlInputElementSetChecked e $! v) $ updated dValue
return eClicked
data Dropdown t k
= Dropdown { _dropdown_value :: Dynamic t k
}
data DropdownConfig t k
= DropdownConfig { _dropdownConfig_setValue :: Event t k
, _dropdownConfig_attributes :: Dynamic t (Map String String)
}
instance (Reflex t, Ord k, Show k, Read k) => Default (DropdownConfig t k) where
def = DropdownConfig { _dropdownConfig_setValue = never
, _dropdownConfig_attributes = constDyn mempty
}
--TODO: We should allow the user to specify an ordering instead of relying on the ordering of the Map
--TODO: Get rid of Show k and Read k by indexing the possible values ourselves
-- | Create a dropdown box
-- The first argument gives the initial value of the dropdown; if it is not present in the map of options provided, it will be added with an empty string as its text
dropdown :: forall k t m. (MonadWidget t m, Ord k, Show k, Read k) => k -> Dynamic t (Map k String) -> DropdownConfig t k -> m (Dropdown t k)
dropdown k0 options (DropdownConfig setK attrs) = do
(eRaw, _) <- elDynAttr' "select" attrs $ do
optionsWithDefault <- mapDyn (`Map.union` (k0 =: "")) options
listWithKey optionsWithDefault $ \k v -> do
elAttr "option" ("value" =: show k <> if k == k0 then "selected" =: "selected" else mempty) $ dynText v
let e = castToHTMLSelectElement $ _el_element eRaw
performEvent_ $ fmap (liftIO . htmlSelectElementSetValue e . show) setK
eChange <- wrapDomEvent e elementOnchange $ do
kStr <- liftIO $ htmlSelectElementGetValue e
return $ readMay kStr
let readKey opts mk = fromMaybe k0 $ do
k <- mk
guard $ Map.member k opts
return k
dValue <- combineDyn readKey options =<< holdDyn (Just k0) (leftmost [eChange, fmap Just setK])
return $ Dropdown dValue
liftM concat $ mapM makeLenses
[ ''TextAreaConfig
, ''TextArea
, ''TextInputConfig
, ''TextInput
, ''DropdownConfig
, ''Dropdown
, ''CheckboxConfig
, ''Checkbox
]
class HasAttributes a where
type Attrs a :: *
attributes :: Lens' a (Attrs a)
instance HasAttributes (TextAreaConfig t) where
type Attrs (TextAreaConfig t) = Dynamic t (Map String String)
attributes = textAreaConfig_attributes
instance HasAttributes (TextInputConfig t) where
type Attrs (TextInputConfig t) = Dynamic t (Map String String)
attributes = textInputConfig_attributes
instance HasAttributes (DropdownConfig t k) where
type Attrs (DropdownConfig t k) = Dynamic t (Map String String)
attributes = dropdownConfig_attributes
instance HasAttributes (CheckboxConfig t) where
type Attrs (CheckboxConfig t) = Dynamic t (Map String String)
attributes = checkboxConfig_attributes
class HasSetValue a where
type SetValue a :: *
setValue :: Lens' a (SetValue a)
instance HasSetValue (TextAreaConfig t) where
type SetValue (TextAreaConfig t) = Event t String
setValue = textAreaConfig_setValue
instance HasSetValue (TextInputConfig t) where
type SetValue (TextInputConfig t) = Event t String
setValue = textInputConfig_setValue
instance HasSetValue (DropdownConfig t k) where
type SetValue (DropdownConfig t k) = Event t k
setValue = dropdownConfig_setValue
instance HasSetValue (CheckboxConfig t) where
type SetValue (CheckboxConfig t) = Event t Bool
setValue = checkboxConfig_setValue
class HasValue a where
type Value a :: *
value :: a -> Value a
instance HasValue (TextArea t) where
type Value (TextArea t) = Dynamic t String
value = _textArea_value
instance HasValue (TextInput t) where
type Value (TextInput t) = Dynamic t String
value = _textInput_value
instance HasValue (Dropdown t k) where
type Value (Dropdown t k) = Dynamic t k
value = _dropdown_value
instance HasValue (Checkbox t) where
type Value (Checkbox t) = Dynamic t Bool
value = _checkbox_value
{-
type family Controller sm t a where
Controller Edit t a = (a, Event t a) -- Initial value and setter
Controller View t a = Dynamic t a -- Value (always)
type family Output sm t a where
Output Edit t a = Dynamic t a -- Value (always)
Output View t a = Event t a -- Requested changes
data CheckboxConfig sm t
= CheckboxConfig { _checkbox_input :: Controller sm t Bool
, _checkbox_attributes :: Attributes
}
instance Reflex t => Default (CheckboxConfig Edit t) where
def = CheckboxConfig (False, never) mempty
data Checkbox sm t
= Checkbox { _checkbox_output :: Output sm t Bool
}
data StateMode = Edit | View
--TODO: There must be a more generic way to get this witness and allow us to case on the type-level StateMode
data StateModeWitness (sm :: StateMode) where
EditWitness :: StateModeWitness Edit
ViewWitness :: StateModeWitness View
class HasStateModeWitness (sm :: StateMode) where
stateModeWitness :: StateModeWitness sm
instance HasStateModeWitness Edit where
stateModeWitness = EditWitness
instance HasStateModeWitness View where
stateModeWitness = ViewWitness
-}