{-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} module Glazier.React.Widgets.Input ( -- * Text input textInput -- * Checkbox input , checkboxInput , IndeterminateCheckboxInput(..) , indeterminateCheckboxInput ) where import Control.Lens import Control.Lens.Misc import qualified Data.Algorithm.Diff as D import qualified Data.JSString as J import Data.Tagged import qualified GHC.Generics as G import Glazier.React import Glazier.React.Effect.JavaScript import Glazier.React.Event.Synthetic import qualified JavaScript.Extras as JE ---------------------------------------- -- Tagged event. The convention is to fire "OnXXX" if the event is not handled -- or fire "XXX" to notify handled events. type InputChange = Tagged "InputChange" -- | Text inputs dosn't interact well as a React controlled component. -- Eg. cursor jumps if user types quickly. -- I think there a timing issue with lazy event handlers setting the value, -- So this prototype uses the React uncontrolled component -- (using defaultValue instead of value). -- -- For input, React uses controlled input if input.value is not null. -- -- This widget attempts to set the cursor position at the correct place -- by using a diffing algorithm on the old and new value. -- -- Warning: This widget listens to onChange and will update the model value with the DOM input value. -- potentially overridding any user changes. -- So when changing the model value, be sure that the onChange handler will not be called. textInput :: ( AsReactor cmd , AsJavascript cmd ) => ReactId -> Widget cmd p J.JSString (InputChange ()) textInput ri = let win = do s <- ask lf' ri "input" [ ("key", JE.toJSR ri) -- "value" cannot be used as React will take over as a controlled component. -- The defaultValue only sets the *initial* DOM value -- The user will need to modify reactKey if they want -- react to actually rerender, since React will not do anything -- even if defaultValue changes. -- But hopefully this is not necessary as the DOM inpt value -- is updated under the hood in onInitialized , ("defaultValue", JE.toJSR $ s ^. _model) ] gad = (finish hdlRendered) `also` hdlChange in (display win) `also` (lift gad) where -- | Modify the DOM input value after every render to match the model value hdlRendered :: ( AsReactor cmd , AsJavascript cmd ) => Gadget cmd p J.JSString () hdlRendered = onRendered $ do s <- getModel j <- getElementalRef ri (`evalMaybeT` ()) $ do start <- maybeGetProperty "selectionStart" j end <- maybeGetProperty "selectionEnd" j v <- maybeGetProperty "value" j let (a, b) = estimateSelectionRange (J.unpack v) (J.unpack s) start end exec' $ SetProperty ("value", JE.toJSR s) j exec' $ SetProperty ("selectionStart", JE.toJSR a) j exec' $ SetProperty ("selectionEnd", JE.toJSR b) j hdlChange :: ( AsReactor cmd , AsJavascript cmd ) => Gadget cmd p J.JSString (InputChange ()) hdlChange = do j <- trigger ri "onChange" (pure . target . toSyntheticEvent) maybeDelegate () $ runMaybeT $ do v <- maybeGetProperty "value" j tickModel $ id .= v pure $ Tagged @"InputChange" () -- This returns an greedy selection range for a new string based -- on the selection range on the original string, using a diffing algo. -- -- https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange -- selectionStart -- The 0-based index of the first selected character. -- selectionEnd -- The 0-based index of the character after the last selected character. -- -- So if there is no selection then selectionEnd == selectionStart estimateSelectionRange :: String -> String -> Int -> Int -> (Int, Int) estimateSelectionRange before after start end = let ds = D.getDiff before after in go ds start end 0 0 where go :: [D.Diff Char] -> Int -> Int -> Int -> Int -> (Int, Int) go [] _ _ a b = (a, b) go (d : ds) s e a b = if (s <= 0 && e <= -1) then (a, b) else let (s', a') = step d s a (e', b') = greedyStep d e b in go ds s' e' a' b' step :: D.Diff Char -> Int -> Int -> (Int, Int) step (D.First _) s s' = (if s > 0 then s - 1 else 0, s') step (D.Second _) s s' = (s, if s > 0 then s' + 1 else s') step (D.Both _ _) s s' = if s > 0 then (s - 1, s' + 1) else (0, s') greedyStep :: D.Diff Char -> Int -> Int -> (Int, Int) greedyStep (D.First _) s s' = (if s >= 0 then s - 1 else (-1), s') greedyStep (D.Second _) s s' = (s, if s >= 0 then s' + 1 else s') greedyStep (D.Both _ _) s s' = if s > 0 then (s - 1, s' + 1) else (-1, s') ---------------------------------------- -- | This is a 'React controlled' checkbox. -- For checkboxes, React uses controlled checkbox if input.checked is not null -- https://stackoverflow.com/questions/37427508/react-changing-an-uncontrolled-input checkboxInput :: (AsReactor cmd) => ReactId -> Widget cmd p Bool (InputChange ()) checkboxInput ri = let win = do s <- ask lf' ri "input" [ ("key", JE.toJSR ri) , ("type", "checkbox") , ("checked", JE.toJSR $ s ^. _model) ] gad = hdlChange in (display win) `also` (lift gad) where hdlChange :: (AsReactor cmd) => Gadget cmd p Bool (InputChange ()) hdlChange = do trigger_ ri "onChange" () tickModel $ id %= not pure $ Tagged @"InputChange" () data IndeterminateCheckboxInput = IndeterminateCheckboxInput { checked :: Bool , indeterminate :: Bool } deriving (G.Generic, Show, Eq, Ord) makeLenses_ ''IndeterminateCheckboxInput -- | Variation of 'checkboxInput' supporting indeterminate state. indeterminateCheckboxInput :: ( AsReactor cmd , AsJavascript cmd ) => ReactId -> Widget cmd p IndeterminateCheckboxInput (InputChange ()) indeterminateCheckboxInput ri = magnifyWidget _checked (checkboxInput ri) `also` finish (lift hdlRendered) where hdlRendered :: ( AsReactor cmd , AsJavascript cmd ) => Gadget cmd p IndeterminateCheckboxInput () hdlRendered = onRendered $ do j <- getElementalRef ri s <- getModel (`evalMaybeT` ()) $ do i <- MaybeT $ pure $ preview _indeterminate s exec' $ SetProperty ("indeterminate", JE.toJSR i) j