InputWithAutocomplete.purs 9.34 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 Effect.Aff (Aff, launchAff_)
12
import FFI.Simple ((..))
13 14
import Gargantext.Components.Bootstrap as B
import Gargantext.Components.Bootstrap.Types (Elevation(..))
15
import Gargantext.Components.Forest.Tree.Node.Action.Types (Action)
16
import Gargantext.Utils.Reactix as R2
17 18
import Reactix as R
import Reactix.DOM.HTML as H
19
import Toestand as T
20

21
here :: R2.Here
22
here = R2.here "Gargantext.Components.InputWithAutocomplete"
23

24 25 26 27 28

type Completions = Array String

type Props =
  (
29
    autocompleteSearch  :: String -> Effect Completions
30
  , classes             :: String
31
  , onAutocompleteClick :: String -> Effect Unit
32
  , onEnterPress        :: String -> Effect Unit
33
  , placeholder         :: String
34
  , state               :: T.Box String
35 36
  )

arturo's avatar
arturo committed
37 38
inputWithAutocomplete :: R2.Leaf Props
inputWithAutocomplete = R2.leaf inputWithAutocompleteCpt
39
inputWithAutocompleteCpt :: R.Component Props
40
inputWithAutocompleteCpt = here.component "inputWithAutocomplete" cpt
41
  where
42 43 44 45
    cpt { autocompleteSearch
        , classes
        , onAutocompleteClick
        , onEnterPress
46 47
        , placeholder
        , state } _ = do
48 49 50 51
      -- States
      state'        <- T.useLive T.unequal state
      containerRef  <- R.useRef null
      inputRef      <- R.useRef null
52 53 54 55 56
      completions  <- T.useBox []

      R.useEffectOnce' $ do
        cs <- autocompleteSearch state'
        T.write_ cs completions
57 58

      -- Render
59
      pure $
60 61 62 63 64

        H.div
        { className: "input-with-autocomplete " <> classes
        , ref: containerRef
        }
65
        [
66
          completionsCpt { completions, onAutocompleteClick, state } []
67 68 69 70
        , H.input { type: "text"
                  , ref: inputRef
                  , className: "form-control"
                  , value: state'
71
                  , placeholder
72
                  , on: { focus: onFocus completions state'
73 74
                        , input: onInput completions
                        , change: onInput completions
75 76 77 78
                        , keyUp: onInputKeyUp inputRef
                        , blur: onBlur completions containerRef
                        }
                  }
79 80
        ]

81
      -- Helpers
82
      where
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
        -- (!) `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
100

101 102 103 104 105
          where
            mContains = do
              a <- toMaybe $ R.readRef containerRef
              b <- toMaybe (event .. "relatedTarget")
              Just (contains a b)
106

107 108 109 110
            isInnerEvent = maybe false identity mContains


        onFocus :: forall event. T.Box Completions -> String -> event -> Effect Unit
111 112 113
        onFocus completions st _ = do
          cs <- autocompleteSearch st
          T.write_ cs completions
114 115

        onInput :: forall event. T.Box Completions -> event -> Effect Unit
116
        onInput completions e = do
117
          let val = R.unsafeEventValue e
118
          T.write_ val state
119 120
          cs <- autocompleteSearch val
          T.write_ cs completions
121

122
        onInputKeyUp :: R.Ref (Nullable DOM.Element) -> DE.KeyboardEvent -> Effect Boolean
123
        onInputKeyUp inputRef e = do
124
          if DE.key e == "Enter" then do
125 126
            R2.preventDefault e
            R2.stopPropagation e
127
            let val = R.unsafeEventValue e
128
            let mInput = toMaybe $ R.readRef inputRef
129
            T.write_ val state
130 131
            onEnterPress val
            case mInput of
132 133 134 135
              Nothing -> pure false
              Just input -> do
                R2.blur input
                pure false
136
          else
137
            pure $ false
138

139 140
type Props' =
  (
141
    autocompleteSearch  :: String -> Effect Completions
142 143 144 145 146 147
  , classes             :: String
  , onAutocompleteClick :: String -> Effect Unit
  , dispatch            :: Action -> Aff Unit
  , boxAction           :: String -> Action
  , state               :: T.Box String
  , text                :: T.Box String
148
  , placeholder         :: String
149 150 151 152 153 154 155 156 157 158 159 160 161
  )

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
162
        , text
163
        , placeholder } _ = do
164 165 166 167
      -- States
      state'        <- T.useLive T.unequal state
      containerRef  <- R.useRef null
      inputRef      <- R.useRef null
168 169 170 171 172
      completions   <- T.useBox []

      R.useEffectOnce' $ do
        cs <- autocompleteSearch state'
        T.write_ cs completions
173 174 175 176 177 178 179 180 181 182 183 184 185 186

      -- 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'
187
                  , placeholder
188 189 190 191 192 193 194
                  , on: { focus: onFocus completions state'
                        , input: onInput completions
                        , change: onInput completions
                        , keyUp: onInputKeyUp inputRef
                        , blur: onBlur completions containerRef
                        }
                  }
195 196 197
        , B.iconButton
            { callback: submit state'
            , title: "Submit"
198
            , name: "send"
199 200
            , elevation: Level1
            }
201
        ]
202

203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
      -- 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
233 234 235
        onFocus completions st _ = do
          cs <- autocompleteSearch st
          T.write_ cs completions
236 237 238 239 240

        onInput :: forall event. T.Box Completions -> event -> Effect Unit
        onInput completions e = do
          let val = R.unsafeEventValue e
          T.write_ val state
241 242
          cs <- autocompleteSearch val
          T.write_ cs completions
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260

        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
261

262 263 264 265
        submit val _ = do
          T.write_ ("Invited " <> val <> " to the team") text
          launchAff_ $ dispatch (boxAction val)

266 267
---------------------------------------------------------

268
type CompletionsProps =
269
  ( completions         :: T.Box Completions
270
  , onAutocompleteClick :: String -> Effect Unit
271
  , state               :: T.Box String
272 273 274 275 276 277 278 279 280
  )

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

completionsCptCpt :: R.Component CompletionsProps
completionsCptCpt = here.component "completionsCpt" cpt
  where
    cpt { completions, onAutocompleteClick, state } _ = do
281
      -- State
282 283
      completions' <- T.useLive T.unequal completions

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

286 287 288 289 290
      -- Render
      pure $

        H.div
        { className }
291 292 293
        [
          H.div { className: "list-group" } (cCpt <$> completions')
        ]
294 295

      -- Helpers
296
      where
297

298 299 300 301
        cCpt c =
          H.button { type: "button"
                    , className: "list-group-item"
                    , on: { click: onClick c } } [ H.text c ]
302

303 304
        onClick c _ = do
          T.write_ c state
305
          T.write_ [] completions
306
          onAutocompleteClick c