module Gargantext.Components.InputWithAutocomplete where import Prelude import DOM.Simple (contains) import DOM.Simple as DOM import DOM.Simple.Event as DE import Data.Maybe (Maybe(..), maybe) import Data.Nullable (Nullable, null, toMaybe) import Effect (Effect) import Effect.Aff (Aff, launchAff_) import FFI.Simple ((..)) import Gargantext.Components.Bootstrap as B import Gargantext.Components.Bootstrap.Types (Elevation(..)) import Gargantext.Components.Forest.Tree.Node.Action.Types (Action) import Gargantext.Utils.Reactix as R2 import Reactix as R import Reactix.DOM.HTML as H import Toestand as T here :: R2.Here here = R2.here "Gargantext.Components.InputWithAutocomplete" type Completions = Array String type Props = ( autocompleteSearch :: String -> Completions , classes :: String , onAutocompleteClick :: String -> Effect Unit , onEnterPress :: String -> Effect Unit , state :: T.Box String , placeholder :: String ) inputWithAutocomplete :: R2.Leaf Props inputWithAutocomplete = R2.leaf inputWithAutocompleteCpt inputWithAutocompleteCpt :: R.Component Props inputWithAutocompleteCpt = here.component "inputWithAutocomplete" cpt where cpt { autocompleteSearch , classes , onAutocompleteClick , onEnterPress , state , placeholder } _ = do -- States state' <- T.useLive T.unequal state containerRef <- R.useRef null inputRef <- R.useRef null completions <- T.useBox $ autocompleteSearch state' -- Render pure $ H.div { className: "input-with-autocomplete " <> classes , ref: containerRef } [ completionsCpt { completions, onAutocompleteClick, state } [] , H.input { type: "text" , ref: inputRef , className: "form-control" , value: state' , placeholder: placeholder , on: { focus: onFocus completions state' , input: onInput completions , change: onInput completions , keyUp: onInputKeyUp inputRef , blur: onBlur completions containerRef } } ] -- Helpers where -- (!) `onBlur` DOM.Event is triggered before any `onClick` DOM.Event -- So when a completion is being clicked, the UX will be broken -- -- ↳ As a solution we chose to check if the click is made from -- the autocompletion list onBlur :: forall event. T.Box Completions -> R.Ref (Nullable DOM.Element) -> event -> Effect Unit onBlur completions containerRef event = if isInnerEvent then pure $ (event .. "preventDefault") else T.write_ [] completions where mContains = do a <- toMaybe $ R.readRef containerRef b <- toMaybe (event .. "relatedTarget") Just (contains a b) isInnerEvent = maybe false identity mContains onFocus :: forall event. T.Box Completions -> String -> event -> Effect Unit onFocus completions st _ = T.write_ (autocompleteSearch st) completions onInput :: forall event. T.Box Completions -> event -> Effect Unit onInput completions e = do let val = R.unsafeEventValue e T.write_ val state T.write_ (autocompleteSearch val) completions onInputKeyUp :: R.Ref (Nullable DOM.Element) -> DE.KeyboardEvent -> Effect Boolean onInputKeyUp inputRef e = do if DE.key e == "Enter" then do R2.preventDefault e R2.stopPropagation e let val = R.unsafeEventValue e let mInput = toMaybe $ R.readRef inputRef T.write_ val state onEnterPress val case mInput of Nothing -> pure false Just input -> do R2.blur input pure false else pure $ false type Props' = ( autocompleteSearch :: String -> Completions , classes :: String , onAutocompleteClick :: String -> Effect Unit , dispatch :: Action -> Aff Unit , boxAction :: String -> Action , state :: T.Box String , text :: T.Box String , placeholder :: String ) inputWithAutocomplete' :: R2.Leaf Props' inputWithAutocomplete' = R2.leaf inputWithAutocompleteCpt' inputWithAutocompleteCpt' :: R.Component Props' inputWithAutocompleteCpt' = here.component "inputWithAutocomplete" cpt where cpt { autocompleteSearch , classes , onAutocompleteClick , dispatch , boxAction , state , text , placeholder } _ = do -- States state' <- T.useLive T.unequal state containerRef <- R.useRef null inputRef <- R.useRef null completions <- T.useBox $ autocompleteSearch state' -- Render pure $ H.div { className: "input-with-autocomplete " <> classes , ref: containerRef } [ completionsCpt { completions, onAutocompleteClick, state } [] , H.input { type: "text" , ref: inputRef , className: "form-control" , value: state' , placeholder: placeholder , on: { focus: onFocus completions state' , input: onInput completions , change: onInput completions , keyUp: onInputKeyUp inputRef , blur: onBlur completions containerRef } } , B.iconButton { callback: submit state' , title: "Submit" , name: "send" , elevation: Level1 } ] -- Helpers where -- (!) `onBlur` DOM.Event is triggered before any `onClick` DOM.Event -- So when a completion is being clicked, the UX will be broken -- -- ↳ As a solution we chose to check if the click is made from -- the autocompletion list onBlur :: forall event. T.Box Completions -> R.Ref (Nullable DOM.Element) -> event -> Effect Unit onBlur completions containerRef event = if isInnerEvent then pure $ (event .. "preventDefault") else T.write_ [] completions where mContains = do a <- toMaybe $ R.readRef containerRef b <- toMaybe (event .. "relatedTarget") Just (contains a b) isInnerEvent = maybe false identity mContains onFocus :: forall event. T.Box Completions -> String -> event -> Effect Unit onFocus completions st _ = T.write_ (autocompleteSearch st) completions onInput :: forall event. T.Box Completions -> event -> Effect Unit onInput completions e = do let val = R.unsafeEventValue e T.write_ val state T.write_ (autocompleteSearch val) completions onInputKeyUp :: R.Ref (Nullable DOM.Element) -> DE.KeyboardEvent -> Effect Boolean onInputKeyUp inputRef e = do if DE.key e == "Enter" then do R2.preventDefault e R2.stopPropagation e let val = R.unsafeEventValue e let mInput = toMaybe $ R.readRef inputRef T.write_ val state launchAff_ $ dispatch (boxAction val) T.write_ ("Invited " <> val <> " to the team") text case mInput of Nothing -> pure false Just input -> do R2.blur input pure false else pure $ false submit val _ = do T.write_ ("Invited " <> val <> " to the team") text launchAff_ $ dispatch (boxAction val) --------------------------------------------------------- type CompletionsProps = ( completions :: T.Box Completions , onAutocompleteClick :: String -> Effect Unit , state :: T.Box String ) completionsCpt :: R2.Component CompletionsProps completionsCpt = R.createElement completionsCptCpt completionsCptCpt :: R.Component CompletionsProps completionsCptCpt = here.component "completionsCpt" cpt where cpt { completions, onAutocompleteClick, state } _ = do -- State completions' <- T.useLive T.unequal completions let className = "completions shadow" <> (if completions' == [] then "d-none" else "") -- Render pure $ H.div { className } [ H.div { className: "list-group" } (cCpt <$> completions') ] -- Helpers where cCpt c = H.button { type: "button" , className: "list-group-item" , on: { click: onClick c } } [ H.text c ] onClick c _ = do T.write_ c state T.write_ [] completions onAutocompleteClick c