InputWithAutocomplete.purs 3.51 KB
module Gargantext.Components.InputWithAutocomplete where

import Prelude
import Data.Maybe (Maybe(..))
import Data.Nullable (Nullable, null, toMaybe)
import Data.Tuple.Nested ((/\))
import DOM.Simple as DOM
import DOM.Simple.Event as DE
import Effect (Effect)
import Effect.Timer (setTimeout)
import Reactix as R
import Reactix.DOM.HTML as H

import Gargantext.Utils.Reactix as R2


type Completions = Array String

type Props =
  (
    autocompleteSearch :: String -> Completions
  , onAutocompleteClick :: String -> Effect Unit
  , onEnterPress :: String -> Effect Unit
  , state :: R.State String
  )

inputWithAutocomplete :: Record Props -> R.Element
inputWithAutocomplete props = R.createElement inputWithAutocompleteCpt props []

inputWithAutocompleteCpt :: R.Component Props
inputWithAutocompleteCpt = R.hooksComponent "InputWithAutocomplete" cpt
  where
    cpt props@{autocompleteSearch, onAutocompleteClick, onEnterPress, state: state@(state' /\ setState)} _ = do
      inputRef <- R.useRef null
      completions <- R.useState' $ autocompleteSearch state'

      pure $
        H.span { className: "input-with-autocomplete" }
        [
          completionsCpt completions
        , H.input { type: "text"
                  , ref: inputRef
                  , className: "form-control"
                  , value: state'
                  , on: { blur: onBlur completions
                        , focus: onFocus completions
                        , input: onInput completions
                        , change: onInput completions
                        , keyUp: onInputKeyUp inputRef completions } }
        ]

      where

        -- setTimeout is a bit of a hack here -- clicking on autocomplete
        -- element will clear out the blur first, so the autocomplete click
        -- won't fire without a timeout here.  However, blur is very handy and
        -- handles automatic autocomplete search, otherwise I'd have to hide it
        -- in various different places (i.e. carefully handle all possible
        -- events where blur happens and autocomplete should hide).
        onBlur (_ /\ setCompletions) e = setTimeout 100 $ do
          setCompletions $ const []

        onFocus (_ /\ setCompletions) e = setCompletions $ const $ autocompleteSearch state'

        onInput (_ /\ setCompletions) e = do
          let val = R2.unsafeEventValue e
          setState $ const val
          setCompletions $ const $ autocompleteSearch val

        onInputKeyUp :: R.Ref (Nullable DOM.Element) -> R.State Completions -> DE.KeyboardEvent -> Effect Unit
        onInputKeyUp inputRef (_ /\ setCompletions) e = do
          if DE.key e == "Enter" then do
            let val = R2.unsafeEventValue e
            let mInput = toMaybe $ R.readRef inputRef
            setState $ const val
            onEnterPress val
            case mInput of
              Nothing -> pure unit
              Just input -> R2.blur input
          else
            pure $ unit

        completionsCpt :: R.State Completions -> R.Element
        completionsCpt (completions /\ setCompletions) =
          H.div { className }
          [
            H.div { className: "list-group" } (cCpt <$> completions)
          ]
          where
            className = "completions " <> (if completions == [] then "hidden" else "")

            cCpt c =
              H.button { type: "button"
                       , className: "list-group-item"
                       , on: { click: onClick c } } [ H.text c ]
            onClick c _ = do
              setState $ const c
              onAutocompleteClick c