mirror of
https://github.com/nunntom/elm-ui-select.git
synced 2024-11-22 11:54:11 +03:00
Remove elm-css parts
This commit is contained in:
parent
8fb28f2c01
commit
0150f7096a
10
elm.json
10
elm.json
@ -4,20 +4,14 @@
|
||||
"summary": "A select widget for elm-ui with keyboard input, filter, scrolling and requests!",
|
||||
"license": "BSD-3-Clause",
|
||||
"version": "3.1.3",
|
||||
"exposed-modules": [
|
||||
"Select",
|
||||
"Select.Filter",
|
||||
"Select.Effect",
|
||||
"Select.ElmCss"
|
||||
],
|
||||
"exposed-modules": ["Select", "Select.Filter", "Select.Effect"],
|
||||
"elm-version": "0.19.0 <= v < 0.20.0",
|
||||
"dependencies": {
|
||||
"elm/browser": "1.0.0 <= v < 2.0.0",
|
||||
"elm/core": "1.0.0 <= v < 2.0.0",
|
||||
"elm/html": "1.0.0 <= v < 2.0.0",
|
||||
"elm/json": "1.0.0 <= v < 2.0.0",
|
||||
"mdgriffith/elm-ui": "1.0.0 <= v < 2.0.0",
|
||||
"rtfeldman/elm-css": "18.0.0 <= v < 19.0.0"
|
||||
"mdgriffith/elm-ui": "1.0.0 <= v < 2.0.0"
|
||||
},
|
||||
"test-dependencies": {}
|
||||
}
|
||||
|
@ -10,7 +10,6 @@
|
||||
"elm/http": "2.0.0",
|
||||
"elm/json": "1.1.3",
|
||||
"mdgriffith/elm-ui": "1.1.8",
|
||||
"rtfeldman/elm-css": "18.0.0",
|
||||
"supermario/elm-countries": "1.1.1"
|
||||
},
|
||||
"indirect": {
|
||||
|
@ -1,147 +0,0 @@
|
||||
module ElmCssEffectExample exposing (Model, Msg(..), MyEffect(..), init, main, update, view)
|
||||
|
||||
import Browser
|
||||
import Countries exposing (Country)
|
||||
import Css
|
||||
import Html.Styled as Html exposing (Html)
|
||||
import Html.Styled.Attributes exposing (css)
|
||||
import Select.Effect
|
||||
import Select.ElmCss as Select exposing (Select)
|
||||
|
||||
|
||||
main : Program () Model Msg
|
||||
main =
|
||||
Browser.element
|
||||
{ init = \_ -> init () |> Tuple.mapSecond performEffect
|
||||
, view = view >> Html.toUnstyled
|
||||
, update = \msg model -> update msg model |> Tuple.mapSecond performEffect
|
||||
, subscriptions = \_ -> Sub.none
|
||||
}
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ countrySelect : Select Country
|
||||
|
||||
-- Note it is not necessary to store the following in the model, because you can just use `Select.toValue` etc
|
||||
-- These are just here for the tests
|
||||
, selectedCountry : Maybe Country
|
||||
, inputIsFocused : Maybe Bool
|
||||
, inputValue : String
|
||||
}
|
||||
|
||||
|
||||
init : () -> ( Model, MyEffect )
|
||||
init _ =
|
||||
( { countrySelect =
|
||||
Select.init "country-select"
|
||||
|> Select.setItems Countries.all
|
||||
, selectedCountry = Nothing
|
||||
, inputIsFocused = Nothing
|
||||
, inputValue = ""
|
||||
}
|
||||
, NoEffect
|
||||
)
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
Html.div
|
||||
[ css
|
||||
[ Css.displayFlex
|
||||
, Css.alignItems Css.center
|
||||
, Css.flexDirection Css.column
|
||||
, Css.marginTop (Css.px 200)
|
||||
, Css.property "gap" "2em"
|
||||
, Css.fontFamilies [ "Arial" ]
|
||||
]
|
||||
]
|
||||
[ Html.label
|
||||
[ css
|
||||
[ Css.fontSize (Css.rem 1.2)
|
||||
, Css.lineHeight (Css.rem 1.5)
|
||||
]
|
||||
]
|
||||
[ Html.text "Choose a country"
|
||||
, Select.view
|
||||
|> Select.withClearButton
|
||||
(Just <|
|
||||
Select.clearButton
|
||||
[ Css.height (Css.pct 100)
|
||||
, Css.displayFlex
|
||||
, Css.alignItems Css.center
|
||||
, Css.marginRight (Css.em 1)
|
||||
, Css.fontSize (Css.rem 0.6)
|
||||
, Css.cursor Css.pointer
|
||||
]
|
||||
(Html.text "❌")
|
||||
)
|
||||
|> Select.toStyled
|
||||
[ Css.padding (Css.em 0.5)
|
||||
, Css.paddingRight (Css.em 1.5)
|
||||
, Css.fontSize (Css.rem 1.2)
|
||||
, Css.borderRadius (Css.px 4)
|
||||
, Css.borderWidth (Css.px 1)
|
||||
, Css.borderColor (Css.rgba 0 0 0 0.5)
|
||||
]
|
||||
{ select = model.countrySelect
|
||||
, onChange = CountrySelectMsg
|
||||
, itemToString = \c -> c.flag ++ " " ++ c.name
|
||||
}
|
||||
]
|
||||
, Html.div []
|
||||
[ Maybe.map (\{ name } -> Html.text ("You chose " ++ name)) (Select.toValue model.countrySelect)
|
||||
|> Maybe.withDefault (Html.text "")
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
type Msg
|
||||
= CountrySelectMsg (Select.Msg Country)
|
||||
| SelectionChanged (Maybe Country)
|
||||
| InputFocused
|
||||
| InputLostFocus
|
||||
| InputChanged String
|
||||
|
||||
|
||||
type MyEffect
|
||||
= SelectEffect (Select.Effect Never Msg)
|
||||
| NoEffect
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, MyEffect )
|
||||
update msg model =
|
||||
case msg of
|
||||
CountrySelectMsg subMsg ->
|
||||
Select.Effect.updateWith
|
||||
[ Select.Effect.onSelectedChange SelectionChanged
|
||||
, Select.Effect.onFocus InputFocused
|
||||
, Select.Effect.onLoseFocus InputLostFocus
|
||||
, Select.Effect.onInput InputChanged
|
||||
]
|
||||
CountrySelectMsg
|
||||
subMsg
|
||||
model.countrySelect
|
||||
|> Tuple.mapFirst (\select -> { model | countrySelect = select })
|
||||
|> Tuple.mapSecond SelectEffect
|
||||
|
||||
SelectionChanged selected ->
|
||||
( { model | selectedCountry = selected }, NoEffect )
|
||||
|
||||
InputFocused ->
|
||||
( { model | inputIsFocused = Just True }, NoEffect )
|
||||
|
||||
InputLostFocus ->
|
||||
( { model | inputIsFocused = Just False }, NoEffect )
|
||||
|
||||
InputChanged val ->
|
||||
( { model | inputValue = val }, NoEffect )
|
||||
|
||||
|
||||
performEffect : MyEffect -> Cmd Msg
|
||||
performEffect effect =
|
||||
case effect of
|
||||
NoEffect ->
|
||||
Cmd.none
|
||||
|
||||
SelectEffect selectEffect ->
|
||||
Select.Effect.perform selectEffect
|
@ -1,97 +0,0 @@
|
||||
module ElmCssExample exposing (main)
|
||||
|
||||
import Browser
|
||||
import Countries exposing (Country)
|
||||
import Css
|
||||
import Html.Styled as Html exposing (Html)
|
||||
import Html.Styled.Attributes exposing (css)
|
||||
import Select.ElmCss as Select exposing (Select)
|
||||
|
||||
|
||||
main : Program () Model Msg
|
||||
main =
|
||||
Browser.element
|
||||
{ init = init
|
||||
, view = view >> Html.toUnstyled
|
||||
, update = update
|
||||
, subscriptions = \_ -> Sub.none
|
||||
}
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ countrySelect : Select Country
|
||||
}
|
||||
|
||||
|
||||
init : () -> ( Model, Cmd Msg )
|
||||
init _ =
|
||||
( { countrySelect =
|
||||
Select.init "country-select"
|
||||
|> Select.setItems Countries.all
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
Html.div
|
||||
[ css
|
||||
[ Css.displayFlex
|
||||
, Css.alignItems Css.center
|
||||
, Css.flexDirection Css.column
|
||||
, Css.marginTop (Css.px 200)
|
||||
, Css.property "gap" "2em"
|
||||
, Css.fontFamilies [ "Arial" ]
|
||||
]
|
||||
]
|
||||
[ Html.label
|
||||
[ css
|
||||
[ Css.fontSize (Css.rem 1.2)
|
||||
, Css.lineHeight (Css.rem 1.5)
|
||||
]
|
||||
]
|
||||
[ Html.text "Choose a country"
|
||||
, Select.view
|
||||
|> Select.withClearButton
|
||||
(Just <|
|
||||
Select.clearButton
|
||||
[ Css.height (Css.pct 100)
|
||||
, Css.displayFlex
|
||||
, Css.alignItems Css.center
|
||||
, Css.marginRight (Css.em 1)
|
||||
, Css.fontSize (Css.rem 0.6)
|
||||
, Css.cursor Css.pointer
|
||||
]
|
||||
(Html.text "❌")
|
||||
)
|
||||
|> Select.toStyled
|
||||
[ Css.padding (Css.em 0.5)
|
||||
, Css.paddingRight (Css.em 1.5)
|
||||
, Css.fontSize (Css.rem 1.2)
|
||||
, Css.borderRadius (Css.px 4)
|
||||
, Css.borderWidth (Css.px 1)
|
||||
, Css.borderColor (Css.rgba 0 0 0 0.5)
|
||||
]
|
||||
{ select = model.countrySelect
|
||||
, onChange = CountrySelectMsg
|
||||
, itemToString = \c -> c.flag ++ " " ++ c.name
|
||||
}
|
||||
]
|
||||
, Html.div []
|
||||
[ Maybe.map (\{ name } -> Html.text ("You chose " ++ name)) (Select.toValue model.countrySelect)
|
||||
|> Maybe.withDefault (Html.text "")
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
type Msg
|
||||
= CountrySelectMsg (Select.Msg Country)
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
CountrySelectMsg subMsg ->
|
||||
Select.update CountrySelectMsg subMsg model.countrySelect
|
||||
|> Tuple.mapFirst (\select -> { model | countrySelect = select })
|
@ -1,182 +0,0 @@
|
||||
module ElmCssExampleTest exposing (exampleProgramTest)
|
||||
|
||||
import Countries exposing (Country)
|
||||
import ElmCssEffectExample as App
|
||||
import Expect
|
||||
import Html
|
||||
import Html.Styled
|
||||
import ProgramTest exposing (ProgramTest, SimulatedEffect)
|
||||
import Select.Effect
|
||||
import Select.ElmCss as Select exposing (Select)
|
||||
import SimulateInput
|
||||
import SimulatedEffect.Cmd as SimulatedCmd
|
||||
import SimulatedEffect.Process as SimulatedProcess
|
||||
import SimulatedEffect.Task as SimulatedTask
|
||||
import Test exposing (Test)
|
||||
import Test.Html.Event
|
||||
import Test.Html.Query as Query exposing (Single)
|
||||
import Test.Html.Selector as Selector exposing (Selector)
|
||||
|
||||
|
||||
exampleProgramTest : Test
|
||||
exampleProgramTest =
|
||||
Test.describe "Select Tests"
|
||||
[ Test.test "Filter for United Kingdom produces one result" <|
|
||||
\() ->
|
||||
programTest
|
||||
|> focusInput
|
||||
|> ProgramTest.fillIn "" "Choose a country" "United Kingdom"
|
||||
|> ProgramTest.ensureView
|
||||
(Query.find [ Selector.id (Select.toMenuElementId countrySelect) ]
|
||||
>> Query.contains [ Html.text "🇬🇧 United Kingdom of Great Britain and Northern Ireland" ]
|
||||
)
|
||||
|> ProgramTest.expectView
|
||||
(Query.find [ Selector.id (Select.toMenuElementId countrySelect) ]
|
||||
>> Query.children []
|
||||
>> Query.count (Expect.equal 1)
|
||||
)
|
||||
, Test.test "Click United Kingdom selects it" <|
|
||||
\() ->
|
||||
programTest
|
||||
|> focusInput
|
||||
|> ProgramTest.fillIn "" "Choose a country" "United"
|
||||
|> Select.Effect.simulateClickOption simulateInputConfig "country-select" "🇬🇧 United Kingdom of Great Britain and Northern Ireland"
|
||||
|> ProgramTest.expectViewHas [ Selector.text "You chose United Kingdom of Great Britain and Northern Ireland" ]
|
||||
, Test.test "Keyboard select United Kingdom" <|
|
||||
\() ->
|
||||
programTest
|
||||
|> focusInput
|
||||
|> ProgramTest.fillIn "" "Choose a country" "United"
|
||||
|> SimulateInput.arrowDown "country-select"
|
||||
|> SimulateInput.enter "country-select"
|
||||
|> ProgramTest.expectViewHas [ Selector.text "You chose United Kingdom of Great Britain and Northern Ireland" ]
|
||||
, Test.test "Focusing on the input triggers the onFocus msg" <|
|
||||
\() ->
|
||||
programTest
|
||||
|> focusInput
|
||||
|> ProgramTest.expectModel (.inputIsFocused >> Expect.equal (Just True))
|
||||
, Test.test "Input losing focus triggers the onLoseFocus msg" <|
|
||||
\() ->
|
||||
programTest
|
||||
|> focusInput
|
||||
|> ProgramTest.simulateDomEvent (Query.find [ Selector.id (Select.toInputElementId countrySelect) ]) Test.Html.Event.blur
|
||||
|> ProgramTest.expectModel (.inputIsFocused >> Expect.equal (Just False))
|
||||
, Test.test "Filling in the input triggers the onInput msg" <|
|
||||
\() ->
|
||||
programTest
|
||||
|> ProgramTest.fillIn "" "Choose a country" "Testing the input"
|
||||
|> ProgramTest.expectModel (.inputValue >> Expect.equal "Testing the input")
|
||||
, Test.test "Typing 2 chars with withMinInputLength (Just 3) does not show any items" <|
|
||||
\() ->
|
||||
programTestWith (Select.withMinInputLength (Just 3))
|
||||
|> focusInput
|
||||
|> ProgramTest.fillIn "" "Choose a country" "un"
|
||||
|> ProgramTest.expectViewHasNot [ Selector.text "🇬🇧 United Kingdom of Great Britain and Northern Ireland" ]
|
||||
, Test.test "Typing 3 chars with withMinInputLength (Just 3) does show items" <|
|
||||
\() ->
|
||||
programTestWith (Select.withMinInputLength (Just 3))
|
||||
|> focusInput
|
||||
|> ProgramTest.fillIn "" "Choose a country" "uni"
|
||||
|> ProgramTest.expectViewHas [ Selector.text "🇬🇧 United Kingdom of Great Britain and Northern Ireland" ]
|
||||
, Test.test "Typing less than minInputLength does not show no matches even if nothing matched" <|
|
||||
\() ->
|
||||
programTestWith (Select.withMinInputLength (Just 5))
|
||||
|> focusInput
|
||||
|> ProgramTest.fillIn "" "Choose a country" "zzzz"
|
||||
|> ProgramTest.expectViewHasNot [ Selector.text "No matches" ]
|
||||
, Test.test "Typing up to the minInputLength shows no matches if nothing matched" <|
|
||||
\() ->
|
||||
programTestWith (Select.withMinInputLength (Just 3))
|
||||
|> focusInput
|
||||
|> ProgramTest.fillIn "" "Choose a country" "zzzz"
|
||||
|> ProgramTest.expectViewHas [ Selector.text "No matches" ]
|
||||
, Test.test "Choosing an option and then focusing back on the input shows all the options again" <|
|
||||
\() ->
|
||||
programTest
|
||||
|> focusInput
|
||||
|> ProgramTest.fillIn "" "Choose a country" "United"
|
||||
|> Select.Effect.simulateClickOption simulateInputConfig "country-select" "🇬🇧 United Kingdom of Great Britain and Northern Ireland"
|
||||
|> ProgramTest.simulateDomEvent (Query.find [ Selector.id (Select.toInputElementId countrySelect) ]) Test.Html.Event.focus
|
||||
|> ProgramTest.expectViewHas [ Selector.text "🇦🇩 Andorra" ]
|
||||
, Test.test "Setting open on focus to false does not open the menu when the input is focused" <|
|
||||
\() ->
|
||||
programTestWith (Select.withOpenMenuOnFocus False)
|
||||
|> focusInput
|
||||
|> ProgramTest.expectModel (.countrySelect >> Select.isMenuOpen >> Expect.equal False)
|
||||
, Test.test "Setting open on focus to true does open the menu when the input is focused" <|
|
||||
\() ->
|
||||
programTestWith (Select.withOpenMenuOnFocus True)
|
||||
|> focusInput
|
||||
|> ProgramTest.expectModel (.countrySelect >> Select.isMenuOpen >> Expect.equal True)
|
||||
]
|
||||
|
||||
|
||||
programTest : ProgramTest App.Model App.Msg App.MyEffect
|
||||
programTest =
|
||||
ProgramTest.createElement
|
||||
{ init = App.init
|
||||
, update = App.update
|
||||
, view = App.view >> Html.Styled.toUnstyled
|
||||
}
|
||||
|> ProgramTest.withSimulatedEffects simulateEffect
|
||||
|> ProgramTest.start ()
|
||||
|
||||
|
||||
programTestWith : (Select.ViewConfig Country App.Msg -> Select.ViewConfig Country App.Msg) -> ProgramTest App.Model App.Msg App.MyEffect
|
||||
programTestWith f =
|
||||
ProgramTest.createElement
|
||||
{ init = App.init
|
||||
, update = App.update
|
||||
, view =
|
||||
\m ->
|
||||
Html.div []
|
||||
[ Html.label []
|
||||
[ Html.text "Choose a country"
|
||||
, Select.view
|
||||
|> f
|
||||
|> Select.toStyled []
|
||||
{ select = m.countrySelect
|
||||
, onChange = App.CountrySelectMsg
|
||||
, itemToString = \c -> c.flag ++ " " ++ c.name
|
||||
}
|
||||
|> Html.Styled.toUnstyled
|
||||
]
|
||||
]
|
||||
}
|
||||
|> ProgramTest.withSimulatedEffects simulateEffect
|
||||
|> ProgramTest.start ()
|
||||
|
||||
|
||||
countrySelect : Select Country
|
||||
countrySelect =
|
||||
App.init ()
|
||||
|> Tuple.first
|
||||
|> .countrySelect
|
||||
|
||||
|
||||
simulateInputConfig : Select.Effect.SimulateInputConfig (Single msg) Selector (ProgramTest model msg effect)
|
||||
simulateInputConfig =
|
||||
{ simulateDomEvent = ProgramTest.simulateDomEvent
|
||||
, find = Query.find
|
||||
, attribute = Selector.attribute
|
||||
}
|
||||
|
||||
|
||||
simulateEffect : App.MyEffect -> SimulatedEffect App.Msg
|
||||
simulateEffect effect =
|
||||
case effect of
|
||||
App.NoEffect ->
|
||||
SimulatedCmd.none
|
||||
|
||||
App.SelectEffect selectEffect ->
|
||||
Select.Effect.simulate
|
||||
{ perform = SimulatedTask.perform
|
||||
, batch = SimulatedCmd.batch
|
||||
, sleep = SimulatedProcess.sleep
|
||||
}
|
||||
selectEffect
|
||||
|
||||
|
||||
focusInput : ProgramTest model msg effect -> ProgramTest model msg effect
|
||||
focusInput =
|
||||
ProgramTest.simulateDomEvent (Query.find [ Selector.id (Select.toInputElementId countrySelect) ]) Test.Html.Event.focus
|
@ -9,7 +9,9 @@
|
||||
"ci": "npm run review && npm run review-examples && npm run test",
|
||||
"examples": "cd examples/src && elm reactor"
|
||||
},
|
||||
"pre-commit": [],
|
||||
"pre-commit": [
|
||||
"ci"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/nunntom/elm-ui-select.git"
|
||||
|
@ -1,342 +0,0 @@
|
||||
module Internal.View.ElmCss exposing
|
||||
( Config
|
||||
, ViewConfig
|
||||
, defaultOptionElement
|
||||
, init
|
||||
, toStyled
|
||||
)
|
||||
|
||||
import Browser.Dom as Dom
|
||||
import Css exposing (Style)
|
||||
import Element exposing (Attribute)
|
||||
import Html.Styled as Html exposing (Attribute, Html)
|
||||
import Html.Styled.Attributes as Attributes
|
||||
import Html.Styled.Events as Events
|
||||
import Internal.Model as Model exposing (Model)
|
||||
import Internal.Msg exposing (Msg(..))
|
||||
import Internal.Option as Option exposing (Option)
|
||||
import Internal.OptionState exposing (OptionState(..))
|
||||
import Internal.Placement as Placement exposing (Placement)
|
||||
import Internal.View.Common as View
|
||||
import Internal.View.Events as ViewEvents
|
||||
import Internal.ViewConfig as ViewConfig exposing (ViewConfigInternal)
|
||||
import Json.Decode as Decode
|
||||
|
||||
|
||||
type alias Config a msg =
|
||||
{ select : Model a
|
||||
, onChange : Msg a -> msg
|
||||
, itemToString : a -> String
|
||||
}
|
||||
|
||||
|
||||
type alias ViewConfig a msg =
|
||||
ViewConfigInternal a Style (Html msg)
|
||||
|
||||
|
||||
init : ViewConfig a msg
|
||||
init =
|
||||
ViewConfig.init
|
||||
|
||||
|
||||
toStyled : List Style -> Config a msg -> ViewConfig a msg -> Html msg
|
||||
toStyled attrs ({ select } as config) viewConfig =
|
||||
toStyled_ attrs
|
||||
(ViewConfig.toPlacement select viewConfig)
|
||||
(ViewConfig.toFilteredOptions select config.itemToString viewConfig)
|
||||
config
|
||||
viewConfig
|
||||
|
||||
|
||||
toStyled_ : List Style -> Placement -> List (Option a) -> Config a msg -> ViewConfig a msg -> Html msg
|
||||
toStyled_ attrs placement filteredOptions ({ select } as config) viewConfig =
|
||||
Html.div
|
||||
(List.concat
|
||||
[ [ Attributes.id <| Model.toContainerElementId select
|
||||
, Attributes.class "elm-select-container"
|
||||
, Attributes.css
|
||||
[ Css.position Css.relative
|
||||
, Css.boxSizing Css.borderBox
|
||||
, if Model.isOpen select then
|
||||
Css.zIndex (Css.int 21)
|
||||
|
||||
else
|
||||
Css.batch []
|
||||
]
|
||||
]
|
||||
, ViewEvents.updateFilteredOptions config.onChange config.itemToString select viewConfig filteredOptions
|
||||
|> List.map Attributes.fromUnstyled
|
||||
]
|
||||
)
|
||||
[ View.relativeContainerMarker select
|
||||
|> Html.fromUnstyled
|
||||
, inputView attrs filteredOptions config viewConfig
|
||||
, if Model.toValue select /= Nothing || Model.toInputValue select /= "" then
|
||||
viewConfig.clearButton
|
||||
|> Maybe.map
|
||||
(\( attrs_, el ) ->
|
||||
clearButtonElement config.onChange attrs_ el
|
||||
)
|
||||
|> Maybe.withDefault (Html.text "")
|
||||
|
||||
else
|
||||
Html.text ""
|
||||
, if ViewConfig.shouldShowNoMatchElement filteredOptions select viewConfig then
|
||||
Html.div
|
||||
[ Attributes.css
|
||||
[ Css.position Css.absolute
|
||||
, Css.width (Css.pct 100)
|
||||
]
|
||||
]
|
||||
[ Maybe.withDefault defaultNoMatchElement viewConfig.noMatchElement ]
|
||||
|
||||
else
|
||||
Html.text ""
|
||||
, if viewConfig.positionFixed then
|
||||
positionFixedEl placement
|
||||
(Model.toContainerElement select)
|
||||
(menuView
|
||||
(defaultMenuAttrs placement
|
||||
(List.concatMap (\toAttrs -> toAttrs placement) viewConfig.menuAttributes)
|
||||
{ menuWidth = Model.toMenuMinWidth select
|
||||
, maxWidth = viewConfig.menuMaxWidth
|
||||
, menuHeight = Model.toMenuMaxHeight viewConfig.menuMaxHeight viewConfig.menuPlacement select
|
||||
}
|
||||
)
|
||||
{ menuId = Model.toMenuElementId select
|
||||
, toOptionId = Model.toOptionElementId select
|
||||
, toOptionState = Model.toOptionState select
|
||||
, onChange = config.onChange
|
||||
, menuOpen = Model.isOpen select
|
||||
, options = filteredOptions
|
||||
, optionElement = Maybe.withDefault (defaultOptionElement config.itemToString) viewConfig.optionElement
|
||||
}
|
||||
)
|
||||
|
||||
else
|
||||
menuView
|
||||
(defaultMenuAttrs placement
|
||||
(List.concatMap (\toAttrs -> toAttrs placement) viewConfig.menuAttributes)
|
||||
{ menuWidth = Model.toMenuMinWidth select
|
||||
, maxWidth = viewConfig.menuMaxWidth
|
||||
, menuHeight = Model.toMenuMaxHeight viewConfig.menuMaxHeight viewConfig.menuPlacement select
|
||||
}
|
||||
)
|
||||
{ menuId = Model.toMenuElementId select
|
||||
, toOptionId = Model.toOptionElementId select
|
||||
, toOptionState = Model.toOptionState select
|
||||
, onChange = config.onChange
|
||||
, menuOpen = Model.isOpen select
|
||||
, options = filteredOptions
|
||||
, optionElement = Maybe.withDefault (defaultOptionElement config.itemToString) viewConfig.optionElement
|
||||
}
|
||||
, if Model.isOpen select then
|
||||
View.ariaLive (List.length filteredOptions)
|
||||
|> Html.fromUnstyled
|
||||
|
||||
else
|
||||
Html.text ""
|
||||
]
|
||||
|
||||
|
||||
inputView : List Style -> List (Option a) -> Config a msg -> ViewConfig a msg -> Html msg
|
||||
inputView attrs filteredOptions ({ select } as config) viewConfig =
|
||||
Html.input
|
||||
([ ViewEvents.onFocus config.onChange config.itemToString select viewConfig filteredOptions
|
||||
|> Attributes.fromUnstyled
|
||||
, Events.onClick (InputClicked |> config.onChange)
|
||||
, Events.onBlur
|
||||
(config.onChange
|
||||
(InputLostFocus
|
||||
{ clearInputValue = viewConfig.clearInputValueOnBlur
|
||||
, selectExactMatch = viewConfig.selectExactMatchOnBlur
|
||||
}
|
||||
filteredOptions
|
||||
)
|
||||
)
|
||||
, Attributes.fromUnstyled <|
|
||||
ViewEvents.onKeyDown (Model.isOpen select) (KeyDown viewConfig.selectOnTab filteredOptions >> config.onChange)
|
||||
, Attributes.id <| Model.toInputElementId select
|
||||
, Events.onInput (ViewEvents.onInput config.onChange config.itemToString select viewConfig)
|
||||
, Attributes.value <| Model.toInputText config.itemToString select
|
||||
, Attributes.attribute "autocomplete" "dont-fill-in-this-box"
|
||||
, Attributes.css
|
||||
[ Css.width (Css.pct 100)
|
||||
, Css.boxSizing Css.borderBox
|
||||
, Css.batch attrs
|
||||
]
|
||||
]
|
||||
++ List.map Attributes.fromUnstyled (View.inputAccessibilityAttributes select)
|
||||
)
|
||||
[]
|
||||
|
||||
|
||||
menuView :
|
||||
List (Attribute msg)
|
||||
->
|
||||
{ menuId : String
|
||||
, toOptionId : Int -> String
|
||||
, toOptionState : ( Int, a ) -> OptionState
|
||||
, onChange : Msg a -> msg
|
||||
, menuOpen : Bool
|
||||
, options : List (Option a)
|
||||
, optionElement : OptionState -> a -> Html msg
|
||||
}
|
||||
-> Html msg
|
||||
menuView attribs v =
|
||||
List.indexedMap (optionElement v) v.options
|
||||
|> Html.div
|
||||
(attribs
|
||||
++ (Attributes.id v.menuId
|
||||
:: (if v.menuOpen && List.length v.options > 0 then
|
||||
[]
|
||||
|
||||
else
|
||||
[ Attributes.style "visibility" "hidden"
|
||||
, Attributes.attribute "aria-visible"
|
||||
(if v.menuOpen then
|
||||
"false"
|
||||
|
||||
else
|
||||
"true"
|
||||
)
|
||||
, Attributes.style "height" "0"
|
||||
, Attributes.style "overflow-y" "hidden"
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
optionElement :
|
||||
{ b
|
||||
| toOptionState : ( Int, a ) -> OptionState
|
||||
, toOptionId : Int -> String
|
||||
, onChange : Msg a -> msg
|
||||
, optionElement : OptionState -> a -> Html msg
|
||||
}
|
||||
-> Int
|
||||
-> Option a
|
||||
-> Html msg
|
||||
optionElement v i opt =
|
||||
Html.div
|
||||
[ Attributes.id (v.toOptionId i)
|
||||
, Attributes.attribute "role" "option"
|
||||
, Attributes.attribute "value" (Option.toString opt)
|
||||
, Events.preventDefaultOn "mousedown" (Decode.succeed ( v.onChange NoOp, True ))
|
||||
, Events.preventDefaultOn "click" (Decode.succeed ( v.onChange <| OptionClicked opt, True ))
|
||||
, Events.onMouseEnter (v.onChange <| MouseEnteredOption i)
|
||||
]
|
||||
[ v.optionElement (v.toOptionState ( i, Option.toItem opt )) (Option.toItem opt) ]
|
||||
|
||||
|
||||
clearButtonElement : (Msg a -> msg) -> List Style -> Html msg -> Html msg
|
||||
clearButtonElement onChange attribs element =
|
||||
Html.button
|
||||
[ Attributes.css
|
||||
[ Css.position Css.absolute
|
||||
, Css.right Css.zero
|
||||
, Css.top Css.zero
|
||||
, Css.backgroundColor Css.transparent
|
||||
, Css.borderWidth Css.zero
|
||||
, Css.padding Css.zero
|
||||
, Css.margin Css.zero
|
||||
, Css.batch attribs
|
||||
]
|
||||
, Attributes.tabindex -1
|
||||
, Attributes.type_ "button"
|
||||
, Events.onClick (onChange ClearButtonPressed)
|
||||
]
|
||||
[ element ]
|
||||
|
||||
|
||||
defaultMenuAttrs :
|
||||
Placement
|
||||
-> List Style
|
||||
->
|
||||
{ menuWidth : Maybe Int
|
||||
, maxWidth : Maybe Int
|
||||
, menuHeight : Maybe Int
|
||||
}
|
||||
-> List (Attribute msg)
|
||||
defaultMenuAttrs placement css { menuWidth, maxWidth, menuHeight } =
|
||||
[ Attributes.attribute "role" "listbox"
|
||||
, Attributes.css
|
||||
[ Css.position Css.absolute
|
||||
, case placement of
|
||||
Placement.Above ->
|
||||
Css.bottom (Css.pct 100)
|
||||
|
||||
Placement.Below ->
|
||||
Css.batch []
|
||||
, Maybe.map (toFloat >> Css.px >> Css.maxHeight) menuHeight
|
||||
|> Maybe.withDefault (Css.batch [])
|
||||
, Maybe.map (toFloat >> Css.px >> Css.maxWidth) maxWidth
|
||||
|> Maybe.withDefault (Css.batch [])
|
||||
, Maybe.map (toFloat >> Css.px >> Css.minWidth) menuWidth
|
||||
|> Maybe.withDefault (Css.batch [])
|
||||
, Css.overflowY Css.scroll
|
||||
, Css.border3 (Css.px 1) Css.solid (Css.rgb 204 204 204)
|
||||
, Css.borderRadius (Css.px 5)
|
||||
, Css.backgroundColor (Css.rgb 255 255 255)
|
||||
, Css.padding2 (Css.px 5) (Css.px 0)
|
||||
, Css.width (Css.pct 100)
|
||||
, Css.boxSizing Css.borderBox
|
||||
, Css.batch css
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
positionFixedEl : Placement -> Maybe Dom.Element -> Html msg -> Html msg
|
||||
positionFixedEl placement container content =
|
||||
Html.div
|
||||
(Attributes.style "position" "fixed"
|
||||
:: (if placement == Placement.Above then
|
||||
[ Attributes.style "transform"
|
||||
("translateY(calc(-100% - 5px - "
|
||||
++ (Maybe.map (.element >> .height >> String.fromFloat) container |> Maybe.withDefault "0")
|
||||
++ "px))"
|
||||
)
|
||||
]
|
||||
|
||||
else
|
||||
[]
|
||||
)
|
||||
)
|
||||
[ content ]
|
||||
|
||||
|
||||
defaultOptionElement : (a -> String) -> OptionState -> a -> Html msg
|
||||
defaultOptionElement toString optionState a =
|
||||
Html.div
|
||||
[ Attributes.style "cursor" "pointer"
|
||||
, Attributes.style "padding" "10px 14px"
|
||||
, Attributes.style "background-color" <|
|
||||
case optionState of
|
||||
Highlighted ->
|
||||
"rgb(89%, 89%, 89%)"
|
||||
|
||||
Selected ->
|
||||
"rgba(64%, 83%, 97%, 0.8)"
|
||||
|
||||
SelectedAndHighlighted ->
|
||||
"rgba(64%, 83%, 97%, 1)"
|
||||
|
||||
Idle ->
|
||||
"rgb(255, 255, 255)"
|
||||
]
|
||||
[ Html.text (toString a) ]
|
||||
|
||||
|
||||
defaultNoMatchElement : Html msg
|
||||
defaultNoMatchElement =
|
||||
Html.div
|
||||
[ Attributes.css
|
||||
[ Css.padding (Css.px 5)
|
||||
, Css.border3 (Css.px 1) Css.solid (Css.rgba 0 0 0 0.5)
|
||||
, Css.borderRadius (Css.px 5)
|
||||
, Css.backgroundColor (Css.rgb 255 255 255)
|
||||
, Css.width (Css.pct 100)
|
||||
]
|
||||
]
|
||||
[ Html.text "No matches" ]
|
@ -1,723 +0,0 @@
|
||||
module Select.ElmCss exposing
|
||||
( Select
|
||||
, init
|
||||
, setItems, setSelected, setInputValue, closeMenu
|
||||
, toValue, toInputValue, toInputElementId, toMenuElementId
|
||||
, isMenuOpen, isLoading, isRequestFailed, isFocused
|
||||
, Msg, update, updateWith, sendRequest
|
||||
, UpdateOption, request, requestMinInputLength, requestDebounceDelay, onSelectedChange, onInput, onFocus, onLoseFocus, onKeyDown
|
||||
, ViewConfig, view, withMenuAttributes, MenuPlacement(..), withMenuMaxHeight, withMenuMaxWidth, withNoMatchElement, withOptionElement, defaultOptionElement, OptionState(..), withClearButton, ClearButton, clearButton, withFilter, withMenuAlwaysAbove, withMenuAlwaysBelow, withMenuPlacementAuto, withMenuPositionFixed, withClearInputValueOnBlur, withSelectExactMatchOnBlur, withSelectOnTab, withMinInputLength, withOpenMenuOnFocus
|
||||
, toStyled
|
||||
, Effect
|
||||
)
|
||||
|
||||
{-| A select widget for elm-ui.
|
||||
|
||||
|
||||
# Type
|
||||
|
||||
@docs Select
|
||||
|
||||
|
||||
# Create
|
||||
|
||||
@docs init
|
||||
|
||||
|
||||
# Set
|
||||
|
||||
@docs setItems, setSelected, setInputValue, closeMenu
|
||||
|
||||
|
||||
# Get
|
||||
|
||||
@docs toValue, toInputValue, toInputElementId, toMenuElementId
|
||||
|
||||
|
||||
# Check
|
||||
|
||||
@docs isMenuOpen, isLoading, isRequestFailed, isFocused
|
||||
|
||||
|
||||
# Update the Select
|
||||
|
||||
@docs Msg, update, updateWith, sendRequest
|
||||
|
||||
|
||||
# Update Options
|
||||
|
||||
@docs UpdateOption, request, requestMinInputLength, requestDebounceDelay, onSelectedChange, onInput, onFocus, onLoseFocus, onKeyDown
|
||||
|
||||
|
||||
# Configure View
|
||||
|
||||
@docs ViewConfig, view, withMenuAttributes, MenuPlacement, withMenuMaxHeight, withMenuMaxWidth, withNoMatchElement, withOptionElement, defaultOptionElement, OptionState, withClearButton, ClearButton, clearButton, withFilter, withMenuAlwaysAbove, withMenuAlwaysBelow, withMenuPlacementAuto, withMenuPositionFixed, withClearInputValueOnBlur, withSelectExactMatchOnBlur, withSelectOnTab, withMinInputLength, withOpenMenuOnFocus
|
||||
|
||||
|
||||
# Element
|
||||
|
||||
@docs toStyled
|
||||
|
||||
|
||||
# Effect
|
||||
|
||||
@docs Effect
|
||||
|
||||
-}
|
||||
|
||||
import Css exposing (Style)
|
||||
import Html.Styled exposing (Html)
|
||||
import Internal.Effect as Effect
|
||||
import Internal.Model as Model exposing (Model)
|
||||
import Internal.Msg as Msg
|
||||
import Internal.OptionState as OptionState
|
||||
import Internal.Placement as Placement
|
||||
import Internal.Update as Update
|
||||
import Internal.UpdateOptions as UpdateOptions exposing (UpdateOption)
|
||||
import Internal.View.ElmCss as View
|
||||
import Select.Filter exposing (Filter)
|
||||
|
||||
|
||||
|
||||
-- MODEL
|
||||
|
||||
|
||||
{-| The main Select type
|
||||
-}
|
||||
type alias Select a =
|
||||
Model a
|
||||
|
||||
|
||||
{-| Initialise the Select. You must provide a unique id. The id will be used for getting DOM elements etc.
|
||||
-}
|
||||
init : String -> Select a
|
||||
init =
|
||||
Model.init
|
||||
|
||||
|
||||
{-| Set the list of items
|
||||
|
||||
You can do this on init:
|
||||
|
||||
type alias Model =
|
||||
{ select : Select String
|
||||
}
|
||||
|
||||
init : List String -> ( Model, Cmd Msg )
|
||||
init things =
|
||||
( { select =
|
||||
Select.init "thing-select"
|
||||
|> Select.setItems things
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
Or you can do it in your view if you need to keep your items in your own model.
|
||||
You'd probably only do this if you want to select from things that are owned/managed by other parts of the app.
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
Select.view []
|
||||
{ onChange = SelectMsg
|
||||
, label = Input.labelAbove [] (Element.text "Choose a thing")
|
||||
, placeholder = Just (Input.placeholder [] (Element.text "Type to search"))
|
||||
, itemToString = identity
|
||||
}
|
||||
|> Select.toElement (Select.setItems model.things model.select)
|
||||
|
||||
-}
|
||||
setItems : List a -> Select a -> Select a
|
||||
setItems =
|
||||
Model.setItems
|
||||
|
||||
|
||||
{-| Set the selected item
|
||||
-}
|
||||
setSelected : Maybe a -> Select a -> Select a
|
||||
setSelected =
|
||||
Model.setSelected
|
||||
|
||||
|
||||
{-| Set the input value
|
||||
-}
|
||||
setInputValue : String -> Select a -> Select a
|
||||
setInputValue =
|
||||
Model.onInputChange
|
||||
|
||||
|
||||
{-| Close the menu
|
||||
-}
|
||||
closeMenu : Select a -> Select a
|
||||
closeMenu =
|
||||
Model.closeMenu
|
||||
|
||||
|
||||
|
||||
-- GET
|
||||
|
||||
|
||||
{-| Get the selected item
|
||||
-}
|
||||
toValue : Select a -> Maybe a
|
||||
toValue =
|
||||
Model.toValue
|
||||
|
||||
|
||||
{-| Get the value of the input
|
||||
-}
|
||||
toInputValue : Select a -> String
|
||||
toInputValue =
|
||||
Model.toInputValue
|
||||
|
||||
|
||||
{-| Get the id of the DOM input element. Useful in tests or to associate the provided label with the input
|
||||
-}
|
||||
toInputElementId : Select a -> String
|
||||
toInputElementId =
|
||||
Model.toInputElementId
|
||||
|
||||
|
||||
{-| Get the id of the DOM menu container. Useful for testing
|
||||
-}
|
||||
toMenuElementId : Select a -> String
|
||||
toMenuElementId =
|
||||
Model.toMenuElementId
|
||||
|
||||
|
||||
|
||||
-- CHECK
|
||||
|
||||
|
||||
{-| Is the menu open?
|
||||
-}
|
||||
isMenuOpen : Select a -> Bool
|
||||
isMenuOpen =
|
||||
Model.isOpen
|
||||
|
||||
|
||||
{-| Is there a request currently loading? Could be used to add loading styling.
|
||||
-}
|
||||
isLoading : Select a -> Bool
|
||||
isLoading =
|
||||
Model.isLoading
|
||||
|
||||
|
||||
{-| Did a request fail?
|
||||
-}
|
||||
isRequestFailed : Select a -> Bool
|
||||
isRequestFailed =
|
||||
Model.isRequestFailed
|
||||
|
||||
|
||||
{-| Is the input focused?
|
||||
-}
|
||||
isFocused : Select a -> Bool
|
||||
isFocused =
|
||||
Model.isFocused
|
||||
|
||||
|
||||
|
||||
-- UPDATE
|
||||
|
||||
|
||||
{-| The Msg type
|
||||
-}
|
||||
type alias Msg a =
|
||||
Msg.Msg a
|
||||
|
||||
|
||||
{-| Update the Select
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
SelectMsg subMsg ->
|
||||
Select.update SelectMsg subMsg model.select
|
||||
|> Tuple.mapFirst (\select -> { model | select = select })
|
||||
|
||||
-}
|
||||
update : (Msg a -> msg) -> Msg a -> Select a -> ( Select a, Cmd msg )
|
||||
update tagger msg select =
|
||||
Update.update (UpdateOptions.fromList []) tagger msg select
|
||||
|> Tuple.mapSecond (Effect.perform (\_ -> Cmd.none))
|
||||
|
||||
|
||||
{-| Update with options.
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
SelectMsg subMsg ->
|
||||
Select.updateWith [ Select.onSelectedChanged ThingSelected ] SelectMsg subMsg model.select
|
||||
|> Tuple.mapFirst (\select -> { model | select = select })
|
||||
|
||||
ThingSelected maybeThing ->
|
||||
Debug.todo "Do something when the thing is selected/deselected"
|
||||
|
||||
-}
|
||||
updateWith : List (UpdateOption err a msg) -> (Msg a -> msg) -> Msg a -> Select a -> ( Select a, Cmd msg )
|
||||
updateWith options tagger msg select =
|
||||
Update.update (UpdateOptions.fromList options) tagger msg select
|
||||
|> Tuple.mapSecond (Effect.perform identity)
|
||||
|
||||
|
||||
{-| Options for use with updateWith.
|
||||
-}
|
||||
type alias UpdateOption err a msg =
|
||||
UpdateOptions.UpdateOption err (Cmd msg) a msg
|
||||
|
||||
|
||||
{-| Use an HTTP request to retrieve matching remote results. Note that in order to avoid an elm/http dependency in this package,
|
||||
you will need to provide the request Cmd yourself. Provide a function that takes the input value and a msg tagger and returns a Cmd.
|
||||
Update will return this Cmd when the user types in the input.
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
SelectMsg subMsg ->
|
||||
Select.updateWith [ Select.request fetchThings ] SelectMsg subMsg model.select
|
||||
|> Tuple.mapFirst (\select -> { model | select = select })
|
||||
|
||||
fetchThings : String -> (Result Http.Error (List Thing) -> Msg) -> Cmd Msg
|
||||
fetchThings query tagger =
|
||||
Http.get
|
||||
{ url = "https://awesome-thing.api/things?search=" ++ query
|
||||
, expect =
|
||||
Http.expectJson tagger
|
||||
(Decode.list thingDecoder)
|
||||
}
|
||||
|
||||
-}
|
||||
request : (String -> (Result err (List a) -> msg) -> Cmd msg) -> UpdateOption err a msg
|
||||
request effect =
|
||||
UpdateOptions.Request effect
|
||||
|
||||
|
||||
{-| Configure debouncing for the request. How long should we wait in milliseconds after the user stops typing to send the request? Default is 300.
|
||||
|
||||
Select.updateWith [ Select.request fetchThings, Select.requestDebounceDelay 500 ] SelectMsg subMsg model.select
|
||||
|
||||
-}
|
||||
requestDebounceDelay : Float -> UpdateOption err a msg
|
||||
requestDebounceDelay delay =
|
||||
UpdateOptions.DebounceRequest delay
|
||||
|
||||
|
||||
{-| How many characters does a user need to type before a request is sent?
|
||||
If this is too low you may get an unmanagable number of results! Default is 3 characters.
|
||||
|
||||
Select.updateWith [ Select.request fetchThings, Select.requestMinInputLength 4 ] SelectMsg subMsg model.select
|
||||
|
||||
-}
|
||||
requestMinInputLength : Int -> UpdateOption err a msg
|
||||
requestMinInputLength len =
|
||||
UpdateOptions.RequestMinInputLength len
|
||||
|
||||
|
||||
{-| If provided this msg will be sent whenever the selected item changes.
|
||||
|
||||
Select.updateWith [ Select.onSelectedChange SelectionChanged ] SelectMsg subMsg model.select
|
||||
|
||||
-}
|
||||
onSelectedChange : (Maybe a -> msg) -> UpdateOption err a msg
|
||||
onSelectedChange msg =
|
||||
UpdateOptions.OnSelect msg
|
||||
|
||||
|
||||
{-| If provided this msg will be sent whenever the input value changes.
|
||||
-}
|
||||
onInput : (String -> msg) -> UpdateOption err a msg
|
||||
onInput msg =
|
||||
UpdateOptions.OnInput msg
|
||||
|
||||
|
||||
{-| If provided this msg will be sent whenever the input is focused.
|
||||
-}
|
||||
onFocus : msg -> UpdateOption err a msg
|
||||
onFocus msg =
|
||||
UpdateOptions.OnFocus msg
|
||||
|
||||
|
||||
{-| If provided this msg will be sent whenever the input loses focus.
|
||||
-}
|
||||
onLoseFocus : msg -> UpdateOption err a msg
|
||||
onLoseFocus msg =
|
||||
UpdateOptions.OnLoseFocus msg
|
||||
|
||||
|
||||
{-| If provided this will be sent whenever there is a keydown event in the input.
|
||||
-}
|
||||
onKeyDown : (String -> msg) -> UpdateOption err a msg
|
||||
onKeyDown msg =
|
||||
UpdateOptions.OnKeyDown msg
|
||||
|
||||
|
||||
{-| Send a request to populate the menu items. This is useful for initialising the select with items from an api.
|
||||
Provide a function that takes the current input value and a msg tagger and returns a Cmd which can be used to perform an HTTP request.
|
||||
|
||||
init : ( Model, Cmd Msg )
|
||||
init =
|
||||
let
|
||||
( select, cmd ) =
|
||||
Select.init "thing-select"
|
||||
|> Select.sendRequest SelectMsg fetchThings Nothing
|
||||
in
|
||||
( { select = select }
|
||||
, cmd
|
||||
)
|
||||
|
||||
fetchThings : String -> (Result Http.Error (List Thing) -> Msg) -> Cmd Msg
|
||||
fetchThings query tagger =
|
||||
Http.get
|
||||
{ url = "https://awesome-thing.api/things?search=" ++ query
|
||||
, expect =
|
||||
Http.expectJson tagger
|
||||
(Decode.list thingDecoder)
|
||||
}
|
||||
|
||||
Optionally provide a function to select one the items when the response returns:
|
||||
|
||||
init : ThingId -> ( Model, Cmd Msg )
|
||||
init thingId =
|
||||
let
|
||||
( select, cmd ) =
|
||||
Select.init "thing-select"
|
||||
|> Select.sendRequest SelectMsg fetchThings (Just (\{ id } -> id == thingId))
|
||||
in
|
||||
( { select = select }
|
||||
, cmd
|
||||
)
|
||||
|
||||
-}
|
||||
sendRequest : (Msg a -> msg) -> (String -> (Result err (List a) -> msg) -> Cmd msg) -> Maybe (a -> Bool) -> Select a -> ( Select a, Cmd msg )
|
||||
sendRequest tagger req andSelect select =
|
||||
Update.sendRequest tagger andSelect select req
|
||||
|> Tuple.mapSecond (Effect.perform (\_ -> Cmd.none))
|
||||
|
||||
|
||||
|
||||
-- VIEW
|
||||
|
||||
|
||||
{-| The View Configuration
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
Select.view []
|
||||
{ onChange = SelectMsg
|
||||
, label = Input.labelAbove [] (text "Choose a thing")
|
||||
, placeholder = Just (Input.placeholder [] (text "Type to search"))
|
||||
, itemToString = .name
|
||||
}
|
||||
|> Select.toElement model.thingsSelect
|
||||
|
||||
-}
|
||||
type ViewConfig a msg
|
||||
= ViewConfig (View.ViewConfig a msg)
|
||||
|
||||
|
||||
{-| Initialise the default ViewConfig
|
||||
-}
|
||||
view : ViewConfig a msg
|
||||
view =
|
||||
ViewConfig View.init
|
||||
|
||||
|
||||
{-| Customise the filtering of the menu based on input value. See [Select.Filter](Select-Filter). Default is startsWithThenContains.
|
||||
-}
|
||||
withFilter : Maybe (Filter a) -> ViewConfig a msg -> ViewConfig a msg
|
||||
withFilter filter (ViewConfig config) =
|
||||
ViewConfig { config | filter = filter }
|
||||
|
||||
|
||||
{-| Force the menu to always appear below the input. You may use this for example if you have issues with an input inside a scrollable transformed container.
|
||||
By default the menu will try to detect whether there is more space above or below and appear there, preferring below.
|
||||
-}
|
||||
withMenuAlwaysBelow : ViewConfig a msg -> ViewConfig a msg
|
||||
withMenuAlwaysBelow (ViewConfig config) =
|
||||
ViewConfig { config | menuPlacement = Just Placement.Below }
|
||||
|
||||
|
||||
{-| Force the menu to always appear above the input. You may use this for example if you have issues with an input inside a scrollable transformed container.
|
||||
-}
|
||||
withMenuAlwaysAbove : ViewConfig a msg -> ViewConfig a msg
|
||||
withMenuAlwaysAbove (ViewConfig config) =
|
||||
ViewConfig { config | menuPlacement = Just Placement.Above }
|
||||
|
||||
|
||||
{-| Menu decides whether to appear above or below based on how much space is available. This is the default.
|
||||
You'd only use this function if you're passing around a config and need to reset this option.
|
||||
-}
|
||||
withMenuPlacementAuto : ViewConfig a msg -> ViewConfig a msg
|
||||
withMenuPlacementAuto (ViewConfig config) =
|
||||
ViewConfig { config | menuPlacement = Nothing }
|
||||
|
||||
|
||||
{-| Set a maximum height for the menu
|
||||
-}
|
||||
withMenuMaxHeight : Maybe Int -> ViewConfig a msg -> ViewConfig a msg
|
||||
withMenuMaxHeight height (ViewConfig config) =
|
||||
ViewConfig { config | menuMaxHeight = height }
|
||||
|
||||
|
||||
{-| Set a maximum width for the menu
|
||||
-}
|
||||
withMenuMaxWidth : Maybe Int -> ViewConfig a msg -> ViewConfig a msg
|
||||
withMenuMaxWidth width (ViewConfig config) =
|
||||
ViewConfig { config | menuMaxWidth = width }
|
||||
|
||||
|
||||
{-| Set arbitrary attributes for the menu element. You can call this multiple times and it will accumulate attributes.
|
||||
You can define different attributes based on whether the menu appears above or below the input.
|
||||
|
||||
Select.view []
|
||||
{ onChange = SelectMsg
|
||||
, label = Input.labelAbove [] (Element.text "Choose a thing")
|
||||
, placeholder = Just (Input.placeholder [] (Element.text "Type to search"))
|
||||
, itemToString = .name
|
||||
}
|
||||
|> Select.withMenuAttributes
|
||||
(\placement ->
|
||||
[ Element.Font.size 16
|
||||
, Element.Border.width 2
|
||||
]
|
||||
++ (case placement of
|
||||
Select.MenuAbove ->
|
||||
[ Element.moveUp 10 ]
|
||||
|
||||
Select.MenuBelow ->
|
||||
[ Element.moveDown 10 ]
|
||||
)
|
||||
)
|
||||
|> Select.toElement model.select
|
||||
|
||||
-}
|
||||
withMenuAttributes : (MenuPlacement -> List Style) -> ViewConfig a msg -> ViewConfig a msg
|
||||
withMenuAttributes attribs (ViewConfig config) =
|
||||
ViewConfig { config | menuAttributes = config.menuAttributes ++ [ mapPlacement >> attribs ] }
|
||||
|
||||
|
||||
{-| Will the menu appear above or below the input?
|
||||
-}
|
||||
type MenuPlacement
|
||||
= MenuAbove
|
||||
| MenuBelow
|
||||
|
||||
|
||||
{-| Provide your own element for the options in the menu, based on the current [state](#OptionState) of the option.
|
||||
|
||||
Select.view []
|
||||
{ onChange = SelectMsg
|
||||
, label = Input.labelAbove [] (Element.text "Choose a thing")
|
||||
, placeholder = Just (Input.placeholder [] (Element.text "Type to search"))
|
||||
, itemToString = .name
|
||||
}
|
||||
|> Select.withOptionElement
|
||||
(\state item ->
|
||||
Element.el
|
||||
[ Element.width Element.fill
|
||||
, Element.paddingXY 14 10
|
||||
, Background.color <|
|
||||
case optionState of
|
||||
Idle ->
|
||||
Element.rgb 1 1 1
|
||||
|
||||
Highlighted ->
|
||||
Element.rgb 0.95 0.95 0.95
|
||||
|
||||
Selected ->
|
||||
Element.rgba 0.64 0.83 0.97 0.8
|
||||
|
||||
SelectedAndHighlighted ->
|
||||
Element.rgba 0.64 0.83 0.97 1
|
||||
]
|
||||
(Element.text item.name)
|
||||
)
|
||||
|> Select.toElement model.select
|
||||
|
||||
-}
|
||||
withOptionElement : (OptionState -> a -> Html msg) -> ViewConfig a msg -> ViewConfig a msg
|
||||
withOptionElement toEl (ViewConfig config) =
|
||||
ViewConfig { config | optionElement = Just (\state -> toEl (mapOptionState state)) }
|
||||
|
||||
|
||||
{-| The default option element. Use this with withOptionElement only if you want the
|
||||
item text on the options to be different from that used in the input and search filtering.
|
||||
-}
|
||||
defaultOptionElement : (a -> String) -> (OptionState -> a -> Html msg)
|
||||
defaultOptionElement itemToString =
|
||||
\state -> View.defaultOptionElement itemToString (reverseMapOptionState state)
|
||||
|
||||
|
||||
{-| Option state for use with custom option element
|
||||
-}
|
||||
type OptionState
|
||||
= Idle
|
||||
| Highlighted
|
||||
| Selected
|
||||
| SelectedAndHighlighted
|
||||
|
||||
|
||||
{-| Provide your own element to show when there are no matches based on the filter and input value. This appears below the input.
|
||||
-}
|
||||
withNoMatchElement : Html msg -> ViewConfig a msg -> ViewConfig a msg
|
||||
withNoMatchElement element (ViewConfig config) =
|
||||
ViewConfig { config | noMatchElement = Just element }
|
||||
|
||||
|
||||
{-| Add a button to clear the input. This element is positioned as Element.inFront.
|
||||
|
||||
Select.view []
|
||||
{ onChange = SelectMsg
|
||||
, label = Input.labelAbove [] (Element.text "Choose a thing")
|
||||
, placeholder = Just (Input.placeholder [] (Element.text "Type to search"))
|
||||
, itemToString = .name
|
||||
}
|
||||
|> Select.withClearButton
|
||||
(Just
|
||||
(Select.clearButton
|
||||
[ Element.alignRight
|
||||
, Element.centerY
|
||||
, Element.moveLeft 12
|
||||
]
|
||||
(Element.el [ Element.Region.description "clear selection" ] (Element.text "❌"))
|
||||
)
|
||||
)
|
||||
|> Select.toElement model.select
|
||||
|
||||
-}
|
||||
withClearButton : Maybe (ClearButton msg) -> ViewConfig a msg -> ViewConfig a msg
|
||||
withClearButton cb (ViewConfig config) =
|
||||
ViewConfig { config | clearButton = Maybe.map (\(ClearButton attrs el) -> ( attrs, el )) cb }
|
||||
|
||||
|
||||
{-| A button to clear the input
|
||||
-}
|
||||
type ClearButton msg
|
||||
= ClearButton (List Style) (Html msg)
|
||||
|
||||
|
||||
{-| Create a clear button
|
||||
-}
|
||||
clearButton : List Style -> Html msg -> ClearButton msg
|
||||
clearButton attribs label =
|
||||
ClearButton attribs label
|
||||
|
||||
|
||||
{-| Use style: position fixed for the menu. This can be used if the select is inside a scrollable container to allow the menu to overflow the parent.
|
||||
Note that if any transforms (e.g. Element.moveUp/Element.moveLeft) are applied to the parent, this no longer works and the menu will be clipped.
|
||||
This is due to [a feature of the current CSS spec](https://bugs.chromium.org/p/chromium/issues/detail?id=20574).
|
||||
Also if the container or window is scrolled or resized without the input losing focus, the menu will appear detached from the input!
|
||||
To overcome this you may want to listen to scroll and resize events on the parent and window and use [closeMenu](#closeMenu) to hide the menu.
|
||||
-}
|
||||
withMenuPositionFixed : Bool -> ViewConfig a msg -> ViewConfig a msg
|
||||
withMenuPositionFixed v (ViewConfig config) =
|
||||
ViewConfig { config | positionFixed = v }
|
||||
|
||||
|
||||
{-| Should the input value be cleared when the input loses focus if nothing is selected?
|
||||
-}
|
||||
withClearInputValueOnBlur : Bool -> ViewConfig a msg -> ViewConfig a msg
|
||||
withClearInputValueOnBlur v (ViewConfig config) =
|
||||
ViewConfig { config | clearInputValueOnBlur = v }
|
||||
|
||||
|
||||
{-| If nothing is selected, but the input value matches exactly one of the options (case insensitive),
|
||||
should we select it automatically when the input loses focus?
|
||||
-}
|
||||
withSelectExactMatchOnBlur : Bool -> ViewConfig a msg -> ViewConfig a msg
|
||||
withSelectExactMatchOnBlur v (ViewConfig config) =
|
||||
ViewConfig { config | selectExactMatchOnBlur = v }
|
||||
|
||||
|
||||
{-| Should we select the highlighted option when the TAB key is pressed?
|
||||
-}
|
||||
withSelectOnTab : Bool -> ViewConfig a msg -> ViewConfig a msg
|
||||
withSelectOnTab v (ViewConfig config) =
|
||||
ViewConfig { config | selectOnTab = v }
|
||||
|
||||
|
||||
{-| If set, no options will show until the specified number of characters have been typed into the input
|
||||
-}
|
||||
withMinInputLength : Maybe Int -> ViewConfig a msg -> ViewConfig a msg
|
||||
withMinInputLength v (ViewConfig config) =
|
||||
ViewConfig { config | minInputLength = v }
|
||||
|
||||
|
||||
{-| Should the menu be opened when the input gets focus?
|
||||
-}
|
||||
withOpenMenuOnFocus : Bool -> ViewConfig a msg -> ViewConfig a msg
|
||||
withOpenMenuOnFocus v (ViewConfig config) =
|
||||
ViewConfig { config | openOnFocus = v }
|
||||
|
||||
|
||||
{-| Turn the ViewConfig into an Element.
|
||||
-}
|
||||
toStyled :
|
||||
List Style
|
||||
->
|
||||
{ select : Model a
|
||||
, onChange : Msg a -> msg
|
||||
, itemToString : a -> String
|
||||
}
|
||||
-> ViewConfig a msg
|
||||
-> Html msg
|
||||
toStyled attrs config (ViewConfig vc) =
|
||||
View.toStyled attrs config vc
|
||||
|
||||
|
||||
|
||||
-- EFFECT
|
||||
|
||||
|
||||
{-| For use with the [Effect pattern](https://sporto.github.io/elm-patterns/architecture/effects.html) and [elm-program-test](https://package.elm-lang.org/packages/avh4/elm-program-test/3.6.3/),
|
||||
see [Select.Effect](Select-Effect).
|
||||
-}
|
||||
type alias Effect effect msg =
|
||||
Effect.Effect effect msg
|
||||
|
||||
|
||||
|
||||
-- INTERNAL
|
||||
|
||||
|
||||
mapOptionState : OptionState.OptionState -> OptionState
|
||||
mapOptionState state =
|
||||
case state of
|
||||
OptionState.Idle ->
|
||||
Idle
|
||||
|
||||
OptionState.Highlighted ->
|
||||
Highlighted
|
||||
|
||||
OptionState.Selected ->
|
||||
Selected
|
||||
|
||||
OptionState.SelectedAndHighlighted ->
|
||||
SelectedAndHighlighted
|
||||
|
||||
|
||||
reverseMapOptionState : OptionState -> OptionState.OptionState
|
||||
reverseMapOptionState state =
|
||||
case state of
|
||||
Idle ->
|
||||
OptionState.Idle
|
||||
|
||||
Highlighted ->
|
||||
OptionState.Highlighted
|
||||
|
||||
Selected ->
|
||||
OptionState.Selected
|
||||
|
||||
SelectedAndHighlighted ->
|
||||
OptionState.SelectedAndHighlighted
|
||||
|
||||
|
||||
mapPlacement : Placement.Placement -> MenuPlacement
|
||||
mapPlacement placement =
|
||||
case placement of
|
||||
Placement.Above ->
|
||||
MenuAbove
|
||||
|
||||
Placement.Below ->
|
||||
MenuBelow
|
Loading…
Reference in New Issue
Block a user