diff --git a/.gitignore b/.gitignore index 167b6cb..8ff99ff 100644 --- a/.gitignore +++ b/.gitignore @@ -2,10 +2,12 @@ .psci_modules bower_components node_modules +.spago # Generated files .psci .psc-ide-port +.purs-repl output package-lock.json generated-docs diff --git a/bower.json b/bower.json index 7758170..7690d05 100644 --- a/bower.json +++ b/bower.json @@ -31,12 +31,13 @@ "generated-docs" ], "dependencies": { - "purescript-halogen": "^5.0.0-rc.4", - "purescript-record": "^2.0.0" + "purescript-halogen": "^5.0.0-rc.9", + "purescript-halogen-hooks": "https://github.com/thomashoneyman/purescript-halogen-hooks.git#v0.4.0", + "purescript-halogen-hooks-extra": "https://github.com/JordanMartinez/purescript-halogen-hooks-extra.git#v0.6.0" }, "devDependencies": { - "purescript-debug": "^4.0.0", - "purescript-affjax": "^9.0.0", - "purescript-argonaut": "^6.0.0" + "purescript-affjax": "^10.0.0", + "purescript-argonaut": "^6.0.0", + "purescript-psci-support": "v4.0.0" } } diff --git a/examples.dhall b/examples.dhall new file mode 100644 index 0000000..28213d4 --- /dev/null +++ b/examples.dhall @@ -0,0 +1,6 @@ +let + config = ./spago.dhall +in config // { + sources = config.sources # [ "examples/**/*.purs" ], + dependencies = config.dependencies # [ "affjax", "argonaut", "psci-support" ] +} diff --git a/examples/Components/Dropdown.purs b/examples/Components/Dropdown.purs index 635aba8..8dc32bc 100644 --- a/examples/Components/Dropdown.purs +++ b/examples/Components/Dropdown.purs @@ -2,24 +2,22 @@ module Components.Dropdown where import Prelude -import Effect.Aff (Aff) import Data.Array ((!!), mapWithIndex, length) +import Data.Const (Const) import Data.Maybe (Maybe(..), fromMaybe) import Data.Monoid (guard) +import Data.Tuple.Nested ((/\)) +import Effect.Aff (Aff) import Halogen as H import Halogen.HTML as HH +import Halogen.Hooks (useLifecycleEffect, useState) +import Halogen.Hooks as Hooks +import Halogen.Hooks.Extra.Hooks (useEvent) import Internal.CSS (class_, classes_, whenElem) +import Select (SelectReturn(..), selectInput, useSelect) import Select as S -import Select.Setters as SS - -type Slot = - H.Slot S.Query' Message -type State = - ( items :: Array String - , selection :: Maybe String - , buttonLabel :: String - ) +type Slot = H.Slot (Const Void) Message data Message = SelectionChanged (Maybe String) (Maybe String) @@ -31,55 +29,51 @@ type Input = , buttonLabel :: String } -component :: H.Component HH.HTML S.Query' Input Message Aff -component = S.component input $ S.defaultSpec - { render = render - , handleEvent = handleEvent - } - where - input :: Input -> S.Input State - input { items, buttonLabel } = - { inputType: S.Toggle - , search: Nothing - , debounceTime: Nothing - , getItemCount: length <<< _.items - , items - , buttonLabel - , selection: Nothing +component :: H.Component HH.HTML (Const Void) Input Message Aff +component = Hooks.component \tokens { items, buttonLabel } -> Hooks.do + selection /\ selectionId <- useState Nothing + selectedIndexChanges <- useEvent + SelectReturn select <- useSelect $ selectInput + { getItemCount = pure (length items) + , pushSelectedIdxChanged = selectedIndexChanges.push } - handleEvent :: S.Event -> H.HalogenM (S.State State) S.Action' () Message Aff Unit - handleEvent = case _ of - S.Selected ix -> do - st <- H.get - let selection = st.items !! ix - H.modify_ _ { selection = selection, visibility = S.Off } - H.raise $ SelectionChanged st.selection selection - _ -> pure unit + useLifecycleEffect do + void $ selectedIndexChanges.setCallback $ Just \_ ix -> do + oldSelection <- Hooks.get selectionId + let newSelection = items !! ix + select.setVisibility S.Off + Hooks.put selectionId newSelection + Hooks.raise tokens.outputToken $ SelectionChanged oldSelection newSelection - render :: S.State State -> H.ComponentHTML S.Action' () Aff - render st = + pure Nothing + + Hooks.pure $ HH.div [ class_ "Dropdown" ] - [ renderToggle, renderContainer ] - where - renderToggle = + [ renderToggle select buttonLabel selection + , renderContainer select items + ] + where + renderToggle select buttonLabel selection = HH.button - ( SS.setToggleProps [ class_ "Dropdown__toggle" ] ) - [ HH.text (fromMaybe st.buttonLabel st.selection) ] + ( select.setToggleProps [ class_ "Dropdown__toggle" ] ) + [ HH.text (fromMaybe buttonLabel selection) ] - renderContainer = whenElem (st.visibility == S.On) \_ -> - HH.div - ( SS.setContainerProps [ class_ "Dropdown__container" ] ) - ( renderItem `mapWithIndex` st.items ) - where - renderItem index item = + renderContainer select items = + whenElem (select.visibility == S.On) \_ -> HH.div - ( SS.setItemProps index - [ classes_ - [ "Dropdown__item" - , "Dropdown__item--highlighted" # guard (st.highlightedIndex == Just index) - ] - ] - ) - [ HH.text item ] + ( select.setContainerProps [ class_ "Dropdown__container" ] ) + ( mapWithIndex (renderItem select) items ) + + renderItem select index item = + HH.div + ( select.setItemProps index + [ classes_ + [ "Dropdown__item" + , "Dropdown__item--highlighted" + # guard (select.highlightedIndex == Just index) + ] + ] + ) + [ HH.text item ] diff --git a/examples/Components/Typeahead.purs b/examples/Components/Typeahead.purs index 21190f0..4863933 100644 --- a/examples/Components/Typeahead.purs +++ b/examples/Components/Typeahead.purs @@ -7,34 +7,27 @@ import Affjax.ResponseFormat as AR import Components.Dropdown as D import Data.Argonaut.Decode ((.:), decodeJson) import Data.Array (mapWithIndex, filter, (:), (!!), length, null, difference) +import Data.Bifunctor (bimap) import Data.Foldable (for_) -import Data.Bifunctor (lmap) import Data.Maybe (Maybe(..), maybe) import Data.Monoid (guard) import Data.Symbol (SProxy(..)) import Data.Time.Duration (Milliseconds(..)) import Data.Traversable (traverse) +import Data.Tuple.Nested ((/\)) import Effect.Aff (Aff) +import Halogen (liftAff) import Halogen as H import Halogen.HTML as HH import Halogen.HTML.Events as HE import Halogen.HTML.Properties as HP +import Halogen.Hooks (useLifecycleEffect, useState) +import Halogen.Hooks as Hooks +import Halogen.Hooks.Extra.Hooks (useEvent) import Internal.CSS (class_, classes_, whenElem) import Internal.RemoteData as RD +import Select (SelectEvent(..), SelectReturn(..), selectInput, useSelect) import Select as S -import Select.Setters as SS - -type Slot = - S.Slot Query ChildSlots Message - -type State = - ( selections :: Array Location - , available :: RD.RemoteData String (Array Location) - ) - -data Action - = Remove Location - | HandleDropdown D.Message data Query a = GetSelections (Array Location -> a) @@ -46,95 +39,87 @@ data Message type ChildSlots = ( dropdown :: D.Slot Unit ) -component :: H.Component HH.HTML (S.Query Query ChildSlots) Unit Message Aff -component = S.component (const input) $ S.defaultSpec - { render = render - , handleAction = handleAction - , handleQuery = handleQuery - , handleEvent = handleEvent - } - where - -- this typeahead will be opaque; users can just use this pre-built - -- input instead of the usual select one. - input :: S.Input State - input = - { inputType: S.Text - , debounceTime: Just (Milliseconds 300.0) - , search: Nothing - , getItemCount: maybe 0 length <<< RD.toMaybe <<< _.available - , selections: [] - , available: RD.NotAsked +component :: H.Component HH.HTML Query Unit Message Aff +component = Hooks.component \tokens _ -> Hooks.do + selections /\ selectionsId <- useState [] + + available /\ availableId <- useState RD.NotAsked + + selectEvents <- useEvent + + SelectReturn select <- useSelect $ selectInput + { inputType = S.Text + , debounceTime = Just (Milliseconds 300.0) + , getItemCount = pure $ maybe 0 length $ RD.toMaybe available + , pushSelectedIdxChanged = selectEvents.push <<< SelectedIndex + , pushNewSearch = selectEvents.push <<< NewSearch } - handleEvent - :: S.Event - -> H.HalogenM (S.State State) (S.Action Action) ChildSlots Message Aff Unit - handleEvent = case _ of - S.Selected ix -> do - st <- H.get - for_ st.available \arr -> - for_ (arr !! ix) \item -> do - let newSelections = item : st.selections - H.modify_ _ - { selections = item : st.selections - , available = RD.Success (filter (_ /= item) arr) - , search = "" - } - H.raise $ SelectionsChanged newSelections - S.Searched str -> do - st <- H.get - -- we'll use an external api to search locations - H.modify_ _ { available = RD.Loading } - items <- H.liftAff $ searchLocations str - H.modify_ _ { available = items <#> \xs -> difference xs st.selections } - _ -> pure unit - - -- You can remove all type signatures except for this one; we need to tell the - -- compiler about the `a` type variable. The minimal necessary signature is below. - handleQuery :: forall a. Query a -> H.HalogenM _ _ _ _ _ (Maybe a) - handleQuery = case _ of + useLifecycleEffect do + void $ selectEvents.setCallback $ Just \_ val -> case val of + SelectedIndex ix -> do + available' <- Hooks.get availableId + for_ available' \arr -> + for_ (arr !! ix) \item -> do + selections' <- Hooks.get selectionsId + let newSelections = item : selections' + Hooks.modify_ availableId $ const (RD.Success (filter (_ /= item) arr)) + Hooks.put selectionsId newSelections + select.clearSearch + Hooks.raise tokens.outputToken $ SelectionsChanged newSelections + + NewSearch str -> do + selections' <- Hooks.get selectionsId + -- we'll use an external api to search locations + Hooks.put availableId RD.Loading + items <- liftAff $ searchLocations str + Hooks.put availableId $ items <#> \xs -> difference xs selections' + + _ -> pure unit + + pure Nothing + + Hooks.useQuery tokens.queryToken case _ of GetSelections reply -> do - st <- H.get - pure $ Just $ reply st.selections - - handleAction - :: Action - -> H.HalogenM (S.State State) (S.Action Action) ChildSlots Message Aff Unit - handleAction = case _ of - Remove item -> do - st <- H.get - let newSelections = filter (_ /= item) st.selections - H.modify_ _ { selections = newSelections } - H.raise $ ItemRemoved item - HandleDropdown msg -> case msg of - D.SelectionChanged oldSelection newSelection -> do - st <- H.get - let - mkLocation str = { name: "User Added: " <> str, population: "1" } - newSelections = case oldSelection, newSelection of - Nothing, Nothing -> - Nothing - Nothing, Just str -> - Just (mkLocation str : st.selections) - Just str, Nothing -> - Just (filter (_ /= mkLocation str) st.selections) - Just old, Just new -> - Just (mkLocation new : (filter (_ /= mkLocation old) st.selections)) - for_ newSelections \selections -> - H.modify_ _ { selections = selections } - - render :: S.State State -> H.ComponentHTML (S.Action Action) ChildSlots Aff - render st = + pure $ Just $ reply selections + + Hooks.pure $ HH.div [ class_ "Typeahead" ] - [ renderSelections, renderInput, renderDropdown, renderContainer ] - where - hasSelections = length st.selections > 0 - - renderSelections = whenElem hasSelections \_ -> + [ renderSelections selections tokens.outputToken selectionsId + , renderInput select selections + , renderDropdown select selectionsId + , renderContainer select selections available + ] + where + remove tOutput selectionsId item = do + selections <- Hooks.get selectionsId + let newSelections = filter (_ /= item) selections + Hooks.put selectionsId newSelections + Hooks.raise tOutput $ ItemRemoved item + + handleDropdown selectionsId msg = case msg of + D.SelectionChanged oldSelection newSelection -> do + selections <- Hooks.get selectionsId + let + mkLocation str = { name: "User Added: " <> str, population: "1" } + newSelections = case oldSelection, newSelection of + Nothing, Nothing -> + Nothing + Nothing, Just str -> + Just (mkLocation str : selections) + Just str, Nothing -> + Just (filter (_ /= mkLocation str) selections) + Just old, Just new -> + Just (mkLocation new : (filter (_ /= mkLocation old) selections)) + for_ newSelections \selections' -> + Hooks.put selectionsId selections' + + renderSelections selections tOutput selectionsId = + whenElem (length selections > 0) \_ -> HH.div [ class_ "Typeahead__selections" ] - (renderSelectedItem <$> st.selections) + (renderSelectedItem <$> selections) where renderSelectedItem item = HH.div @@ -148,29 +133,34 @@ component = S.component (const input) $ S.defaultSpec closeButton item = HH.span [ class_ "Location__closeButton" - , HE.onClick \_ -> Just $ S.Action $ Remove item + , HE.onClick \_ -> Just $ remove tOutput selectionsId item ] [ HH.text "×" ] - renderInput = HH.input $ SS.setInputProps - [ classes_ - [ "Typeahead__input" - , "Typeahead__input--selections" # guard hasSelections - , "Typeahead__input--active" # guard (st.visibility == S.On) - ] - , HP.placeholder "Type to search..." - ] - - renderDropdown = whenElem (st.visibility == S.On) \_ -> + renderInput select selections = + HH.input + (select.setInputProps + [ classes_ + [ "Typeahead__input" + , "Typeahead__input--selections" # guard (length selections > 0) + , "Typeahead__input--active" + # guard (select.visibility == S.On) + ] + , HP.placeholder "Type to search..." + ]) + + renderDropdown select selectionsId = + whenElem (select.visibility == S.On) \_ -> HH.slot _dropdown unit D.component dropdownInput handler - where - _dropdown = SProxy :: SProxy "dropdown" - handler msg = Just $ S.Action $ HandleDropdown msg - dropdownInput = { items: [ "Earth", "Mars" ], buttonLabel: "Human Planets" } + where + _dropdown = SProxy :: SProxy "dropdown" + handler msg = Just $ handleDropdown selectionsId msg + dropdownInput = { items: [ "Earth", "Mars" ], buttonLabel: "Human Planets" } - renderContainer = whenElem (st.visibility == S.On) \_ -> + renderContainer select selections available = + whenElem (select.visibility == S.On) \_ -> HH.div - (SS.setContainerProps + (select.setContainerProps [ classes_ [ "Typeahead__container" , "Typeahead__container--hasItems" # guard hasItems @@ -179,20 +169,21 @@ component = S.component (const input) $ S.defaultSpec ) renderItems where - hasItems = maybe false (not <<< null) (RD.toMaybe st.available) + hasItems = maybe false (not <<< null) (RD.toMaybe available) renderItems = do let renderMsg msg = [ HH.span_ [ HH.text msg ] ] - case st.available of + case available of RD.NotAsked -> renderMsg "No search performed..." RD.Loading -> renderMsg "Loading..." RD.Failure e -> renderMsg e - RD.Success available - | hasItems -> renderItem `mapWithIndex` available + RD.Success available' + | length available' > 0 -> renderItem `mapWithIndex` available' | otherwise -> renderMsg "No results found" renderItem index { name, population } = HH.div - (SS.setItemProps index [ classes_ [ base, highlight, "Location" ] ]) + (select.setItemProps index + [ classes_ [ base, highlight, "Location" ] ]) [ HH.span [ class_ "Location__name" ] [ HH.text name ] @@ -202,7 +193,8 @@ component = S.component (const input) $ S.defaultSpec ] where base = "Typeahead__item" - highlight = "Typeahead__item--highlighted" # guard (st.highlightedIndex == Just index) + highlight = "Typeahead__item--highlighted" + # guard (select.highlightedIndex == Just index) -- Let's make this typeahead async. @@ -214,6 +206,6 @@ type Location = searchLocations :: String -> Aff (RD.RemoteData String (Array Location)) searchLocations search = do - res <- AX.get AR.json ("https://swapi.co/api/planets/?search=" <> search) - let body = lmap AR.printResponseFormatError res.body + eitherRes <- AX.get AR.json ("https://swapi.co/api/planets/?search=" <> search) + let body = bimap AX.printError (_.body) eitherRes pure $ RD.fromEither $ traverse decodeJson =<< (_ .: "results") =<< decodeJson =<< body diff --git a/packages.dhall b/packages.dhall new file mode 100644 index 0000000..726f531 --- /dev/null +++ b/packages.dhall @@ -0,0 +1,131 @@ +{- +Welcome to your new Dhall package-set! + +Below are instructions for how to edit this file for most use +cases, so that you don't need to know Dhall to use it. + +## Warning: Don't Move This Top-Level Comment! + +Due to how `dhall format` currently works, this comment's +instructions cannot appear near corresponding sections below +because `dhall format` will delete the comment. However, +it will not delete a top-level comment like this one. + +## Use Cases + +Most will want to do one or both of these options: +1. Override/Patch a package's dependency +2. Add a package not already in the default package set + +This file will continue to work whether you use one or both options. +Instructions for each option are explained below. + +### Overriding/Patching a package + +Purpose: +- Change a package's dependency to a newer/older release than the + default package set's release +- Use your own modified version of some dependency that may + include new API, changed API, removed API by + using your custom git repo of the library rather than + the package set's repo + +Syntax: +Replace the overrides' "{=}" (an empty record) with the following idea +The "//" or "⫽" means "merge these two records and + when they have the same value, use the one on the right:" +------------------------------- +let overrides = + { packageName = + upstream.packageName // { updateEntity1 = "new value", updateEntity2 = "new value" } + , packageName = + upstream.packageName // { version = "v4.0.0" } + , packageName = + upstream.packageName // { repo = "https://www.example.com/path/to/new/repo.git" } + } +------------------------------- + +Example: +------------------------------- +let overrides = + { halogen = + upstream.halogen // { version = "master" } + , halogen-vdom = + upstream.halogen-vdom // { version = "v4.0.0" } + } +------------------------------- + +### Additions + +Purpose: +- Add packages that aren't already included in the default package set + +Syntax: +Replace the additions' "{=}" (an empty record) with the following idea: +------------------------------- +let additions = + { package-name = + { dependencies = + [ "dependency1" + , "dependency2" + ] + , repo = + "https://example.com/path/to/git/repo.git" + , version = + "tag ('v4.0.0') or branch ('master')" + } + , package-name = + { dependencies = + [ "dependency1" + , "dependency2" + ] + , repo = + "https://example.com/path/to/git/repo.git" + , version = + "tag ('v4.0.0') or branch ('master')" + } + , etc. + } +------------------------------- + +Example: +------------------------------- +let additions = + { benchotron = + { dependencies = + [ "arrays" + , "exists" + , "profunctor" + , "strings" + , "quickcheck" + , "lcg" + , "transformers" + , "foldable-traversable" + , "exceptions" + , "node-fs" + , "node-buffer" + , "node-readline" + , "datetime" + , "now" + ] + , repo = + "https://github.com/hdgarrood/purescript-benchotron.git" + , version = + "v7.0.0" + } + } +------------------------------- +-} + + +let upstream = + https://github.com/purescript/package-sets/releases/download/psc-0.13.6-20200507/packages.dhall sha256:9c1e8951e721b79de1de551f31ecb5a339e82bbd43300eb5ccfb1bf8cf7bbd62 + +let overrides = + { halogen-hooks = upstream.halogen-hooks // { version = "v0.4.0" } + , halogen-hooks-extra = upstream.halogen-hooks-extra // { version = "v0.6.0" } + } + +let additions = {=} + +in upstream // overrides // additions diff --git a/readme.md b/readme.md index 1e7a033..14ae538 100644 --- a/readme.md +++ b/readme.md @@ -1,13 +1,13 @@ # Select [![CircleCI](https://circleci.com/gh/citizennet/purescript-halogen-select/tree/master.svg?style=shield)](https://circleci.com/gh/citizennet/purescript-halogen-select/tree/master) -[![Maintainer: thomashoneyman](https://img.shields.io/badge/maintainer-thomashoneyman-lightgrey.svg)](http://github.com/thomashoneyman) -`Select` provides flexible building blocks for selection interfaces in Halogen. If you need a dropdown menu, typeahead, autocomplete, multi-select, calendar, image picker, or other selection interface, and you want it to be accessible, and you also want complete visual control over the component, then you're in the right place. +`Select` is a Hook which provides flexible building blocks for selection interfaces in Halogen. If you need a dropdown menu, typeahead, autocomplete, multi-select, calendar, image picker, or other selection interface, and you want it to be accessible, and you also want complete visual control over the component, then you're in the right place. -- [Official Documentation / Tutorials / Getting Started](https://citizennet.github.io/purescript-halogen-select) - [Module Documentation on Pursuit](https://pursuit.purescript.org/packages/purescript-halogen-select) -- [Learn About & Build Your Own Renderless Components](https://github.com/thomashoneyman/purescript-halogen-renderless) +- [Learn About Halogen Hooks](https://github.com/thomashoneyman/purescript-halogen-hooks) + +:warning: You are looking at the pre-release branch for the Hooks version of Select. If you want the latest published version, please [browse the repository at the `v5.0.0` release](https://github.com/citizennet/purescript-halogen-select/tree/v5.0.0). # Get Started / Learn More @@ -15,14 +15,10 @@ There are a few ways to get started with the `Select` library. **Installation** -`Select` is available on Bower and Pursuit: +Install `Select` with Spago: ```sh -# Using psc-package -psc-package install halogen-select - -# Using bower -bower i --save purescript-halogen-select +spago install halogen-select ``` For more information, try the [official documentation](https://citizennet.github.io/purescript-halogen-select). @@ -61,7 +57,6 @@ For example, you can make your container compatible with the component with the ] ``` - > Warning: If you provide any of the same events that we use for our behaviors, only yours will trigger, preventing that behavior from being applied. E.g., if you provide your own `HE.onValueInput` event on the element you're applying `setInputProps` to, you will end up overriding our search functionality for that input. diff --git a/spago.dhall b/spago.dhall new file mode 100644 index 0000000..4793939 --- /dev/null +++ b/spago.dhall @@ -0,0 +1,15 @@ +{- +Welcome to a Spago project! +You can edit this file as you like. +-} +{ name = "halogen-select" +, license = "Apache-2.0" +, repository = "https://github.com/citizennet/purescript-halogen-select" +, dependencies = + [ "halogen" + , "halogen-hooks" + , "halogen-hooks-extra" + ] +, packages = ./packages.dhall +, sources = [ "src/**/*.purs", "test/**/*.purs" ] +} diff --git a/src/Select.purs b/src/Select.purs index aa595ca..2e987d8 100644 --- a/src/Select.purs +++ b/src/Select.purs @@ -1,74 +1,95 @@ --- | This module exposes a component that can be used to build accessible selection --- | user interfaces. You are responsible for providing all rendering, with the help --- | of the `Select.Setters` module, but this component provides the relevant --- | behaviors for dropdowns, autocompletes, typeaheads, keyboard-navigable calendars, --- | and other selection UIs. module Select where import Prelude -import Control.Monad.Free (liftF) -import Data.Const (Const) import Data.Maybe (Maybe(..), fromMaybe) -import Data.Symbol (SProxy(..)) -import Data.Time.Duration (Milliseconds) -import Data.Traversable (for_, traverse, traverse_) -import Effect.Aff (Fiber, delay, error, forkAff, killFiber) -import Effect.Aff.AVar (AVar) -import Effect.Aff.AVar as AVar +import Data.Newtype (class Newtype) +import Data.Traversable (for_) +import Data.Tuple.Nested ((/\)) +import Effect.Aff (Milliseconds) import Effect.Aff.Class (class MonadAff) -import Effect.Ref (Ref) -import Effect.Ref as Ref import Halogen as H -import Halogen.HTML as HH -import Halogen.Query.ChildQuery (ChildQueryBox) -import Prim.Row as Row -import Record.Builder as Builder -import Unsafe.Coerce (unsafeCoerce) -import Web.Event.Event (preventDefault) +import Halogen.HTML.Events as HE +import Halogen.HTML.Properties as HP +import Halogen.Hooks (Hook, HookM, StateId, UseState, useState) +import Halogen.Hooks as Hooks +import Halogen.Hooks.Extra.Actions.Events (preventKeyEvent, preventMouseEvent) +import Halogen.Hooks.Extra.Hooks (UseDebouncer, useDebouncer) +import Web.Event.Event as E import Web.HTML.HTMLElement as HTMLElement +import Web.UIEvent.FocusEvent as FE +import Web.UIEvent.KeyboardEvent (KeyboardEvent) import Web.UIEvent.KeyboardEvent as KE +import Web.UIEvent.MouseEvent (MouseEvent) import Web.UIEvent.MouseEvent as ME -data Action action - = Search String - | Highlight Target - | Select Target (Maybe ME.MouseEvent) - | ToggleClick ME.MouseEvent - | Focus Boolean - | Key KE.KeyboardEvent - | PreventClick ME.MouseEvent - | SetVisibility Visibility - | Initialize (Maybe action) - | Action action - -type Action' = Action Void - ------ --- QUERIES - -data Query query slots a - = Send (ChildQueryBox slots (Maybe a)) - | Query (query a) - -type Query' = Query (Const Void) () - ------ --- Event - -data Event - = Searched String - | Selected Int - | VisibilityChanged Visibility - ------ --- HELPER TYPES - --- | The component slot type for easy use in a parent component -type Slot query slots msg = H.Slot (Query query slots) msg - --- | The component slot type when there is no extension -type Slot' = Slot (Const Void) () Void +-- | The properties that must be supported by the HTML element that serves +-- | as a menu toggle. This should be used with toggle-driven `Select` components. +-- | +-- | It allows the toggle element to register key events for navigation or +-- | highlighting, record open and close events based on focus and blur, +-- | and to be focused with the tab key. +-- | +-- | ```purescript +-- | renderToggle = div (setToggleProps [ class "btn-class" ]) [ ...html ] +-- | ``` +type ToggleProps props = + ( onFocus :: FE.FocusEvent + , onKeyDown :: KE.KeyboardEvent + , onMouseDown :: ME.MouseEvent + , onClick :: ME.MouseEvent + , onBlur :: FE.FocusEvent + , tabIndex :: Int + | props + ) + +-- | The properties that must be supported by the HTML element that serves +-- | as a text input. This should be used with input-driven `Select` components. +-- | +-- | It allows the input element to capture string values, register key events +-- | for navigation, record open and close events based on focus and blur, +-- | and to be focused with the tab key. +-- | +-- | ```purescript +-- | renderInput = input_ (setInputProps [ class "my-class" ]) +-- | ``` +type InputProps props = + ( onFocus :: FE.FocusEvent + , onKeyDown :: KE.KeyboardEvent + , onInput :: E.Event + , value :: String + , onMouseDown :: ME.MouseEvent + , onBlur :: FE.FocusEvent + , tabIndex :: Int + | props + ) + +-- | The properties that must be supported by the HTML element that acts as a +-- | selectable "item" in your UI. This should be attached to every item that +-- | can be selected. It allows items to be highlighted and selected. +-- | +-- | This expects an index for use in highlighting. It's useful in combination +-- | with `mapWithIndex`: +-- | +-- | ```purescript +-- | renderItem index itemHTML = +-- | HH.li (setItemProps index [ props ]) [ itemHTML ] +-- | +-- | render = renderItem `mapWithIndex` itemsArray +-- | ``` +type ItemProps props = + ( onMouseDown :: ME.MouseEvent + , onMouseOver :: ME.MouseEvent + | props + ) + +-- | This should be used on the parent element that contains your items. +-- | It prevents clicking on an item within an enclosing HTML element +-- | from bubbling up a blur event to the DOM. +type ContainerProps props = + ( onMouseDown :: ME.MouseEvent + | props + ) -- | Represents a way to navigate on `Highlight` events: to the previous -- | item, next item, or the item at a particular index. @@ -90,266 +111,275 @@ derive instance ordVisibility :: Ord Visibility -- | about searches when time has expired). data InputType = Text | Toggle --- | The component state -type State st = +type SelectInput m = { inputType :: InputType - , search :: String - , debounceTime :: Milliseconds - , debounceRef :: Maybe (Ref (Maybe Debouncer)) + , search :: Maybe String + , debounceTime :: Maybe Milliseconds + , getItemCount :: HookM m Int + , pushNewSearch :: String -> HookM m Unit + , pushVisibilityChanged :: Visibility -> HookM m Unit + , pushSelectedIdxChanged :: Int -> HookM m Unit + } + +type SelectState = + { search :: String , visibility :: Visibility , highlightedIndex :: Maybe Int - , getItemCount :: {| st } -> Int - | st } -type Debouncer = - { var :: AVar Unit - , fiber :: Fiber Unit - } +newtype SelectReturn m = SelectReturn + { search :: String + , visibility :: Visibility + , highlightedIndex :: Maybe Int -type Input st = - { inputType :: InputType - , search :: Maybe String - , debounceTime :: Maybe Milliseconds - , getItemCount :: {| st } -> Int - | st + , setFocus :: Boolean -> HookM m Unit + , setVisibility :: Visibility -> HookM m Unit + , clearSearch :: HookM m Unit + + , setToggleProps + :: forall toggleProps + . Array (HP.IProp (ToggleProps toggleProps) (HookM m Unit)) + -> Array (HP.IProp (ToggleProps toggleProps) (HookM m Unit)) + , setItemProps + :: forall itemProps + . Int + -> Array (HP.IProp (ItemProps itemProps) (HookM m Unit)) + -> Array (HP.IProp (ItemProps itemProps) (HookM m Unit)) + , setContainerProps + :: forall containerProps + . Array (HP.IProp (ContainerProps containerProps) (HookM m Unit)) + -> Array (HP.IProp (ContainerProps containerProps) (HookM m Unit)) + , setInputProps + :: forall inputProps + . Array (HP.IProp (InputProps inputProps) (HookM m Unit)) + -> Array (HP.IProp (InputProps inputProps) (HookM m Unit)) } -type Component query slots input msg m = - H.Component HH.HTML (Query query slots) input msg m - -type ComponentHTML action slots m = - H.ComponentHTML (Action action) slots m - -type HalogenM st action slots msg m a = - H.HalogenM (State st) (Action action) slots msg m a - -type Spec st query action slots input msg m = - { -- usual Halogen component spec - render - :: State st - -> ComponentHTML action slots m - - -- handle additional actions provided to the component - , handleAction - :: action - -> HalogenM st action slots msg m Unit - - -- handle additional queries provided to the component - , handleQuery - :: forall a - . query a - -> HalogenM st action slots msg m (Maybe a) - - -- handle messages emitted by the component; provide H.raise to simply - -- raise the Select messages to the parent. - , handleEvent - :: Event - -> HalogenM st action slots msg m Unit - - -- optionally handle input on parent re-renders - , receive - :: input - -> Maybe action - - -- perform some action when the component initializes. - , initialize - :: Maybe action - - -- optionally perform some action on initialization. disabled by default. - , finalize - :: Maybe action - } +-- | When pushing all Select events into the same handler, this data type +-- | distinguishes one event type from another. +data SelectEvent + = NewSearch String + | VisibilityChangedTo Visibility + | SelectedIndex Int -type Spec' st input m = Spec st (Const Void) Void () input Void m - -defaultSpec - :: forall st query action slots input msg m - . Spec st query action slots input msg m -defaultSpec = - { render: const (HH.text mempty) - , handleAction: const (pure unit) - , handleQuery: const (pure Nothing) - , handleEvent: const (pure unit) - , receive: const Nothing - , initialize: Nothing - , finalize: Nothing +newtype UseSelect hooks = UseSelect + (UseDebouncer String + (UseState SelectState hooks)) + +derive instance newtypeUseSelect :: Newtype (UseSelect hooks) _ + +-- | A `SelectInput` value whose defaults can be overrided. **Note**: +-- | `getItemCount` must be overrided: +-- | +-- | Default values are: +-- | ``` +-- | { inputType: Toggle +-- | , search: Nothing +-- | , debounceTime: Nothing +-- | , getItemCount: pure 0 -- this must be overrided! +-- | , pushNewSearch: \_ -> pure unit +-- | , pushVisibilityChanged: \_ -> pure unit +-- | , pushSelectedIdxChanged: \_ -> pure unit +-- | } +-- | ``` +-- | +-- | Example: +-- | ``` +-- | events <- useEvent +-- | SelectReturn select <- useSelect $ selectInput +-- | { getItemCount = pure (length items) +-- | , pushNewSearch = events.push +-- | } +-- | ``` +selectInput :: forall m. SelectInput m +selectInput = + { inputType: Toggle + , search: Nothing + , debounceTime: Nothing + , getItemCount: pure 0 + , pushNewSearch: mempty + , pushVisibilityChanged: mempty + , pushSelectedIdxChanged: mempty } -component - :: forall st query action slots input msg m +useSelect + :: forall m . MonadAff m - => Row.Lacks "debounceRef" st - => Row.Lacks "visibility" st - => Row.Lacks "highlightedIndex" st - => (input -> Input st) - -> Spec st query action slots input msg m - -> H.Component HH.HTML (Query query slots) input msg m -component mkInput spec = H.mkComponent - { initialState: initialState <<< mkInput - , render: spec.render - , eval: H.mkEval - { handleQuery: handleQuery spec.handleQuery - , handleAction: handleAction spec.handleAction spec.handleEvent - , initialize: Just (Initialize spec.initialize) - , receive: map Action <<< spec.receive - , finalize: map Action spec.finalize + => SelectInput m + -> Hook m UseSelect (SelectReturn m) +useSelect inputRec = + let + initialSearchValue = fromMaybe "" inputRec.search + debounceTime = fromMaybe mempty inputRec.debounceTime + in Hooks.wrap Hooks.do + state /\ stateId <- useState + { search: initialSearchValue + , visibility: Off + , highlightedIndex: Nothing + } + + searchDebouncer <- useDebouncer debounceTime \lastSearchState -> Hooks.do + case inputRec.inputType of + Text -> do + Hooks.modify_ stateId (_ { highlightedIndex = (Just 0) }) + inputRec.pushNewSearch lastSearchState + + -- Key stream is not yet implemented. However, this should capture user + -- key events and expire their search after a set number of milliseconds. + _ -> pure unit + + Hooks.pure $ SelectReturn + -- state + { search: state.search + , visibility: state.visibility + , highlightedIndex: state.highlightedIndex + + -- actions + , setFocus + , setVisibility: setVisibility stateId + , clearSearch: Hooks.modify_ stateId (_ { search = "" }) + + -- props + , setToggleProps: append (toggleProps stateId) + , setItemProps: \i -> append (itemProps stateId i) + , setContainerProps: append containerProps + , setInputProps: append (inputProps stateId searchDebouncer) } - } - where - initialState :: Input st -> State st - initialState = Builder.build pipeline - where - pipeline = - Builder.modify (SProxy :: _ "search") (fromMaybe "") - >>> Builder.modify (SProxy :: _ "debounceTime") (fromMaybe mempty) - >>> Builder.insert (SProxy :: _ "debounceRef") Nothing - >>> Builder.insert (SProxy :: _ "visibility") Off - >>> Builder.insert (SProxy :: _ "highlightedIndex") Nothing - -handleQuery - :: forall st query action slots msg m a - . MonadAff m - => (query a -> HalogenM st action slots msg m (Maybe a)) - -> Query query slots a - -> HalogenM st action slots msg m (Maybe a) -handleQuery handleQuery' = case _ of - Send box -> - H.HalogenM $ liftF $ H.ChildQuery box - - Query query -> - handleQuery' query - -handleAction - :: forall st action slots msg m - . MonadAff m - => Row.Lacks "debounceRef" st - => Row.Lacks "visibility" st - => Row.Lacks "highlightedIndex" st - => (action -> HalogenM st action slots msg m Unit) - -> (Event -> HalogenM st action slots msg m Unit) - -> Action action - -> HalogenM st action slots msg m Unit -handleAction handleAction' handleEvent = case _ of - Initialize mbAction -> do - ref <- H.liftEffect $ Ref.new Nothing - H.modify_ _ { debounceRef = Just ref } - for_ mbAction handleAction' - - Search str -> do - st <- H.get - ref <- H.liftEffect $ map join $ traverse Ref.read st.debounceRef - H.modify_ _ { search = str } - void $ H.fork $ handle $ SetVisibility On - - case st.inputType, ref of - Text, Nothing -> unit <$ do - var <- H.liftAff AVar.empty - fiber <- H.liftAff $ forkAff do - delay st.debounceTime - AVar.put unit var - - -- This compututation will fork and run in the background. When the - -- var is finally filled, the action will run - void $ H.fork do - void $ H.liftAff $ AVar.take var - void $ H.liftEffect $ traverse_ (Ref.write Nothing) st.debounceRef - H.modify_ _ { highlightedIndex = Just 0 } - newState <- H.get - handleEvent $ Searched newState.search - - void $ H.liftEffect $ traverse_ (Ref.write $ Just { var, fiber }) st.debounceRef - - Text, Just debouncer -> do - let var = debouncer.var - void $ H.liftAff $ killFiber (error "Time's up!") debouncer.fiber - fiber <- H.liftAff $ forkAff do - delay st.debounceTime - AVar.put unit var - void $ H.liftEffect $ traverse_ (Ref.write $ Just { var, fiber }) st.debounceRef - - -- Key stream is not yet implemented. However, this should capture user - -- key events and expire their search after a set number of milliseconds. - _, _ -> pure unit - - Highlight target -> do - st <- H.get - when (st.visibility == On) do - H.modify_ _ { highlightedIndex = Just $ getTargetIndex st target } - - Select target mbEv -> do - for_ mbEv (H.liftEffect <<< preventDefault <<< ME.toEvent) - st <- H.get - when (st.visibility == On) case target of - Index ix -> handleEvent $ Selected ix - Next -> handleEvent $ Selected $ getTargetIndex st target - Prev -> handleEvent $ Selected $ getTargetIndex st target - - ToggleClick ev -> do - H.liftEffect $ preventDefault $ ME.toEvent ev - st <- H.get - case st.visibility of - On -> do - handle $ Focus false - handle $ SetVisibility Off - Off -> do - handle $ Focus true - handle $ SetVisibility On - - Focus shouldFocus -> do - inputElement <- H.getHTMLElementRef $ H.RefLabel "select-input" - for_ inputElement \el -> H.liftEffect case shouldFocus of - true -> HTMLElement.focus el - _ -> HTMLElement.blur el - - Key ev -> do - void $ H.fork $ handle $ SetVisibility On - let preventIt = H.liftEffect $ preventDefault $ KE.toEvent ev - case KE.key ev of - x | x == "ArrowUp" || x == "Up" -> - preventIt *> handle (Highlight Prev) - x | x == "ArrowDown" || x == "Down" -> - preventIt *> handle (Highlight Next) - x | x == "Escape" || x == "Esc" -> do - inputElement <- H.getHTMLElementRef $ H.RefLabel "select-input" - preventIt - for_ inputElement (H.liftEffect <<< HTMLElement.blur) - "Enter" -> do - st <- H.get - preventIt - for_ st.highlightedIndex \ix -> - handle $ Select (Index ix) Nothing - otherKey -> pure unit - - PreventClick ev -> - H.liftEffect $ preventDefault $ ME.toEvent ev - - SetVisibility v -> do - st <- H.get - when (st.visibility /= v) do - H.modify_ _ { visibility = v, highlightedIndex = Just 0 } - handleEvent $ VisibilityChanged v - - Action act -> handleAction' act - - where - -- eta-expansion is necessary to avoid infinite recursion - handle act = handleAction handleAction' handleEvent act - - getTargetIndex st = case _ of - Index i -> i - Prev -> case st.highlightedIndex of - Just i | i /= 0 -> i - 1 - _ -> lastIndex st - Next -> case st.highlightedIndex of - Just i | i /= lastIndex st -> i + 1 - _ -> 0 where - -- we know that the getItemCount function will only touch user fields, - -- and that the state record contains *at least* the user fields, so - -- this saves us from a set of unnecessary record deletions / modifications - userState :: State st -> {| st } - userState = unsafeCoerce - - lastIndex :: State st -> Int - lastIndex = (_ - 1) <<< st.getItemCount <<< userState + -- | See `ToggleProps` for docs. + toggleProps :: forall toggleProps. _ -> Array (HP.IProp (ToggleProps toggleProps) (HookM m Unit)) + toggleProps stateId = + [ HE.onFocus \_ -> Just (setVisibility stateId On) + , HE.onMouseDown \ev -> Just (toggleClick stateId ev) + , HE.onKeyDown \ev -> Just (key stateId ev) + , HE.onBlur \ev -> Just (setVisibility stateId Off) + , HP.tabIndex 0 + , HP.ref (H.RefLabel "select-input") + ] + + -- | See `ItemProps` for docs. + itemProps :: forall itemProps. _ -> Int -> Array (HP.IProp (ItemProps itemProps) (HookM m Unit)) + itemProps stateId index = + [ HE.onMouseDown \ev -> Just (select stateId (Index index) (Just ev)) + , HE.onMouseOver \_ -> Just (highlight stateId (Index index)) + ] + + -- | See `ContainerProps` for docs. + containerProps :: forall containerProps. Array (HP.IProp (ContainerProps containerProps) (HookM m Unit)) + containerProps = + [ HE.onMouseDown \ev -> Just (preventMouseEvent ev) ] + + -- | See `InputProps` for docs. + inputProps :: forall inputProps. _ -> _ -> Array (HP.IProp (InputProps inputProps) (HookM m Unit)) + inputProps stateId searchDebouncer = + [ HE.onFocus \_ -> Just (setVisibility stateId On) + , HE.onKeyDown \ev -> Just (key stateId ev) + , HE.onValueInput \str -> Just (handleSearch stateId searchDebouncer str) + , HE.onMouseDown \_ -> Just (setVisibility stateId On) + , HE.onBlur \_ -> Just (setVisibility stateId Off) + , HP.tabIndex 0 + , HP.ref (H.RefLabel "select-input") + ] + + getTargetIndex :: Maybe Int -> Int -> Target -> Int + getTargetIndex highlightedIndex itemCount = case _ of + Index i -> i + Prev -> case highlightedIndex of + Just i | i /= 0 -> i - 1 + _ -> itemCount - 1 + Next -> case highlightedIndex of + Just i | i /= (itemCount - 1) -> i + 1 + _ -> 0 + + setFocus :: Boolean -> HookM m Unit + setFocus shouldFocus = do + inputElement <- Hooks.getHTMLElementRef $ H.RefLabel "select-input" + for_ inputElement \el -> H.liftEffect case shouldFocus of + true -> HTMLElement.focus el + _ -> HTMLElement.blur el + + setVisibility + :: StateId SelectState + -> Visibility + -> HookM m Unit + setVisibility stateId v = do + st <- Hooks.get stateId + when (st.visibility /= v) do + Hooks.modify_ stateId (_ { visibility = v, highlightedIndex = Just 0 }) + inputRec.pushVisibilityChanged v + + handleSearch + :: StateId SelectState + -> (String -> HookM m Unit) + -> String + -> HookM m Unit + handleSearch stateId searchDebouncer str = do + Hooks.modify_ stateId (_ { search = str }) + void $ Hooks.fork $ setVisibility stateId On + searchDebouncer str + + highlight + :: StateId SelectState + -> Target + -> HookM m Unit + highlight stateId target = do + st <- Hooks.get stateId + when (st.visibility == On) do + itemCount <- inputRec.getItemCount + let newIndex = Just (getTargetIndex st.highlightedIndex itemCount target) + Hooks.modify_ stateId (_ { highlightedIndex = newIndex }) + + select + :: StateId SelectState + -> Target + -> Maybe MouseEvent + -> HookM m Unit + select stateId target mbEv = do + for_ mbEv preventMouseEvent + st <- Hooks.get stateId + when (st.visibility == On) case target of + Index ix -> inputRec.pushSelectedIdxChanged ix + Next -> do + itemCount <- inputRec.getItemCount + inputRec.pushSelectedIdxChanged $ getTargetIndex st.highlightedIndex itemCount target + Prev -> do + itemCount <- inputRec.getItemCount + inputRec.pushSelectedIdxChanged $ getTargetIndex st.highlightedIndex itemCount target + + toggleClick + :: StateId SelectState + -> MouseEvent + -> HookM m Unit + toggleClick stateId ev = do + preventMouseEvent ev + st <- Hooks.get stateId + case st.visibility of + On -> do + setFocus false + setVisibility stateId Off + Off -> do + setFocus true + setVisibility stateId On + + key + :: StateId SelectState + -> KeyboardEvent + -> HookM m Unit + key stateId ev = do + void $ Hooks.fork $ setVisibility stateId On + let preventIt = preventKeyEvent ev + case KE.key ev of + x | x == "ArrowUp" || x == "Up" -> + preventIt *> highlight stateId Prev + x | x == "ArrowDown" || x == "Down" -> + preventIt *> highlight stateId Next + x | x == "Escape" || x == "Esc" -> do + inputElement <- Hooks.getHTMLElementRef $ H.RefLabel "select-input" + preventIt + for_ inputElement (H.liftEffect <<< HTMLElement.blur) + "Enter" -> do + st <- Hooks.get stateId + preventIt + for_ st.highlightedIndex \ix -> + select stateId (Index ix) Nothing + otherKey -> pure unit diff --git a/src/Select/Setters.purs b/src/Select/Setters.purs deleted file mode 100644 index 1238c66..0000000 --- a/src/Select/Setters.purs +++ /dev/null @@ -1,127 +0,0 @@ --- | This module exposes helper functions necessary for the library to attach behaviors --- | to your render functions. These allow you to write a render function for your --- | `Select` UI and then augment it at relevant points with the properties defined --- | below. -module Select.Setters where - -import Prelude (append, ($), (<<<)) - -import Data.Maybe (Maybe(..)) -import Halogen as H -import Halogen.HTML.Events as HE -import Halogen.HTML.Properties as HP -import Select -import Web.Event.Event as E -import Web.UIEvent.FocusEvent as FE -import Web.UIEvent.KeyboardEvent as KE -import Web.UIEvent.MouseEvent as ME - --- | The properties that must be supported by the HTML element that serves --- | as a menu toggle. This should be used with toggle-driven `Select` components. -type ToggleProps props = - ( onFocus :: FE.FocusEvent - , onKeyDown :: KE.KeyboardEvent - , onMouseDown :: ME.MouseEvent - , onClick :: ME.MouseEvent - , onBlur :: FE.FocusEvent - , tabIndex :: Int - | props - ) - --- | A helper function that augments an array of `IProps` with `ToggleProps`. It --- | allows the toggle element to register key events for navigation or highlighting, --- | record open and close events based on focus and blur, and to be focused with --- | the tab key. --- | --- | ```purescript --- | renderToggle = div (setToggleProps [ class "btn-class" ]) [ ...html ] --- | ``` -setToggleProps - :: forall props act - . Array (HP.IProp (ToggleProps props) (Action act)) - -> Array (HP.IProp (ToggleProps props) (Action act)) -setToggleProps = append - [ HE.onFocus \_ -> Just $ SetVisibility On - , HE.onMouseDown $ Just <<< ToggleClick - , HE.onKeyDown $ Just <<< Key - , HE.onBlur \_ -> Just $ SetVisibility Off - , HP.tabIndex 0 - , HP.ref (H.RefLabel "select-input") - ] - --- | The properties that must be supported by the HTML element that serves --- | as a text input. This should be used with input-driven `Select` components. -type InputProps props = - ( onFocus :: FE.FocusEvent - , onKeyDown :: KE.KeyboardEvent - , onInput :: E.Event - , value :: String - , onMouseDown :: ME.MouseEvent - , onBlur :: FE.FocusEvent - , tabIndex :: Int - | props - ) - --- | A helper function that augments an array of `IProps` with `InputProps`. It --- | allows the input element to capture string values, register key events for --- | navigation, record open and close events based on focus and blur, and to be --- | focused with the tab key. --- | --- | ```purescript --- | renderInput = input_ (setInputProps [ class "my-class" ]) --- | ``` -setInputProps - :: forall props act - . Array (HP.IProp (InputProps props) (Action act)) - -> Array (HP.IProp (InputProps props) (Action act)) -setInputProps = append - [ HE.onFocus \_ -> Just $ SetVisibility On - , HE.onKeyDown $ Just <<< Key - , HE.onValueInput $ Just <<< Search - , HE.onMouseDown \_ -> Just $ SetVisibility On - , HE.onBlur \_ -> Just $ SetVisibility Off - , HP.tabIndex 0 - , HP.ref (H.RefLabel "select-input") - ] - --- | The properties that must be supported by the HTML element that acts as a --- | selectable "item" in your UI. This should be attached to every item that --- | can be selected. -type ItemProps props = - ( onMouseDown :: ME.MouseEvent - , onMouseOver :: ME.MouseEvent - | props - ) - --- | A helper function that augments an array of `IProps` with `ItemProps`. It --- | allows items to be highlighted and selected. --- | --- | This expects an index for use in highlighting. It's useful in combination --- | with `mapWithIndex`: --- | --- | ```purescript --- | renderItem index itemHTML = --- | HH.li (setItemProps index [ props ]) [ itemHTML ] --- | --- | render = renderItem `mapWithIndex` itemsArray --- | ``` -setItemProps - :: forall props act - . Int - -> Array (HP.IProp (ItemProps props) (Action act)) - -> Array (HP.IProp (ItemProps props) (Action act)) -setItemProps index = append - [ HE.onMouseDown \ev -> Just (Select (Index index) (Just ev)) - , HE.onMouseOver \_ -> Just $ Highlight (Index index) - ] - --- | A helper function that augments an array of `IProps` with a `MouseDown` --- | handler. It prevents clicking on an item within an enclosing HTML element --- | from bubbling up a blur event to the DOM. This should be used on the parent --- | element that contains your items. -setContainerProps - :: forall props act - . Array (HP.IProp (onMouseDown :: ME.MouseEvent | props) (Action act)) - -> Array (HP.IProp (onMouseDown :: ME.MouseEvent | props) (Action act)) -setContainerProps = append - [ HE.onMouseDown $ Just <<< PreventClick ]