InputWithAutocomplete.purs 3.57 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
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

16
here = R2.here "Gargantext.Components.InputWithAutocomplete"
17

18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

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
33
inputWithAutocompleteCpt = here.component "inputWithAutocomplete" cpt
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
  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
68
          let val = R.unsafeEventValue e
69 70 71 72 73 74
          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
75
            let val = R.unsafeEventValue e
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
            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
92
            className = "completions " <> (if completions == [] then "d-none" else "")
93 94 95 96 97 98 99 100

            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