InputWithAutocomplete.purs 4.9 KB
Newer Older
1 2 3
module Gargantext.Components.InputWithAutocomplete where

import Prelude
4

5
import DOM.Simple (contains)
6 7
import DOM.Simple as DOM
import DOM.Simple.Event as DE
8
import Data.Maybe (Maybe(..), maybe)
9
import Data.Nullable (Nullable, null, toMaybe)
10
import Effect (Effect)
11
import FFI.Simple ((..))
12
import Gargantext.Utils.Reactix as R2
13 14
import Reactix as R
import Reactix.DOM.HTML as H
15
import Toestand as T
16

17
here :: R2.Here
18
here = R2.here "Gargantext.Components.InputWithAutocomplete"
19

20 21 22 23 24

type Completions = Array String

type Props =
  (
25 26
    autocompleteSearch  :: String -> Completions
  , classes             :: String
27
  , onAutocompleteClick :: String -> Effect Unit
28 29
  , onEnterPress        :: String -> Effect Unit
  , state               :: T.Box String
30 31
  )

arturo's avatar
arturo committed
32 33
inputWithAutocomplete :: R2.Leaf Props
inputWithAutocomplete = R2.leaf inputWithAutocompleteCpt
34
inputWithAutocompleteCpt :: R.Component Props
35
inputWithAutocompleteCpt = here.component "inputWithAutocomplete" cpt
36
  where
37 38 39 40 41 42 43 44 45 46 47 48
    cpt { autocompleteSearch
        , classes
        , onAutocompleteClick
        , onEnterPress
        , state } _ = do
      -- States
      state'        <- T.useLive T.unequal state
      containerRef  <- R.useRef null
      inputRef      <- R.useRef null
      completions   <- T.useBox $ autocompleteSearch state'

      -- Render
49
      pure $
50 51 52 53 54

        H.div
        { className: "input-with-autocomplete " <> classes
        , ref: containerRef
        }
55
        [
56
          completionsCpt { completions, onAutocompleteClick, state } []
57 58 59 60
        , H.input { type: "text"
                  , ref: inputRef
                  , className: "form-control"
                  , value: state'
61
                  , on: { focus: onFocus completions state'
62 63
                        , input: onInput completions
                        , change: onInput completions
64 65 66 67
                        , keyUp: onInputKeyUp inputRef
                        , blur: onBlur completions containerRef
                        }
                  }
68 69
        ]

70
      -- Helpers
71
      where
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
        -- (!) `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
89

90 91 92 93 94
          where
            mContains = do
              a <- toMaybe $ R.readRef containerRef
              b <- toMaybe (event .. "relatedTarget")
              Just (contains a b)
95

96 97 98 99 100 101 102
            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
103
        onInput completions e = do
104
          let val = R.unsafeEventValue e
105 106
          T.write_ val state
          T.write_ (autocompleteSearch val) completions
107

108
        onInputKeyUp :: R.Ref (Nullable DOM.Element) -> DE.KeyboardEvent -> Effect Boolean
109
        onInputKeyUp inputRef e = do
110
          if DE.key e == "Enter" then do
111 112
            R2.preventDefault e
            R2.stopPropagation e
113
            let val = R.unsafeEventValue e
114
            let mInput = toMaybe $ R.readRef inputRef
115
            T.write_ val state
116 117
            onEnterPress val
            case mInput of
118 119 120 121
              Nothing -> pure false
              Just input -> do
                R2.blur input
                pure false
122
          else
123
            pure $ false
124

125 126 127 128


---------------------------------------------------------

129
type CompletionsProps =
130
  ( completions         :: T.Box Completions
131
  , onAutocompleteClick :: String -> Effect Unit
132
  , state               :: T.Box String
133 134 135 136 137 138 139 140 141
  )

completionsCpt :: R2.Component CompletionsProps
completionsCpt = R.createElement completionsCptCpt

completionsCptCpt :: R.Component CompletionsProps
completionsCptCpt = here.component "completionsCpt" cpt
  where
    cpt { completions, onAutocompleteClick, state } _ = do
142
      -- State
143 144 145 146
      completions' <- T.useLive T.unequal completions

      let className = "completions " <> (if completions' == [] then "d-none" else "")

147 148 149 150 151
      -- Render
      pure $

        H.div
        { className }
152 153 154
        [
          H.div { className: "list-group" } (cCpt <$> completions')
        ]
155 156

      -- Helpers
157
      where
158

159 160 161 162
        cCpt c =
          H.button { type: "button"
                    , className: "list-group-item"
                    , on: { click: onClick c } } [ H.text c ]
163

164 165
        onClick c _ = do
          T.write_ c state
166
          T.write_ [] completions
167
          onAutocompleteClick c