git.iko.soy/typeable/elm-ui
on 2acff4b help sign in
2218 lines 63 kB history raw
1module Element.Input exposing
2 ( focusedOnLoad
3 , button
4 , checkbox, defaultCheckbox
5 , text, multiline
6 , Placeholder, placeholder
7 , username, newPassword, currentPassword, email, search, spellChecked
8 , slider, Thumb, thumb, defaultThumb
9 , radio, radioRow, Option, option, optionWith, OptionState(..)
10 , Label, labelAbove, labelBelow, labelLeft, labelRight, labelHidden
11 )
13{-| Input elements have a lot of constraints!
15We want all of our input elements to:
17 - _Always be accessible_
18 - _Behave intuitively_
19 - _Be completely restyleable_
21While these three goals may seem pretty obvious, Html and CSS have made it surprisingly difficult to achieve!
23And incredibly difficult for developers to remember all the tricks necessary to make things work. If you've every tried to make a `<textarea>` be the height of it's content or restyle a radio button while maintaining accessibility, you may be familiar.
25This module is intended to be accessible by default. You shouldn't have to wade through docs, articles, and books to find out [exactly how accessible your html actually is](https://www.powermapper.com/tests/screen-readers/aria/index.html).
28# Focus Styling
30All Elements can be styled on focus by using [`Element.focusStyle`](Element#focusStyle) to set a global focus style or [`Element.focused`](Element#focused) to set a focus style individually for an element.
32@docs focusedOnLoad
35# Buttons
37@docs button
40# Checkboxes
42A checkbox requires you to store a `Bool` in your model.
44This is also the first input element that has a [`required label`](#Label).
46 import Element exposing (text)
47 import Element.Input as Input
49 type Msg
50 = GuacamoleChecked Bool
52 view model =
53 Input.checkbox []
54 { onChange = GuacamoleChecked
55 , icon = Input.defaultCheckbox
56 , checked = model.guacamole
57 , label =
58 Input.labelRight []
59 (text "Do you want Guacamole?")
60 }
62@docs checkbox, defaultCheckbox
65# Text
67@docs text, multiline
69@docs Placeholder, placeholder
72## Text with autofill
74If we want to play nicely with a browser's ability to autofill a form, we need to be able to give it a hint about what we're expecting.
76The following inputs are very similar to `Input.text`, but they give the browser a hint to allow autofill to work correctly.
78@docs username, newPassword, currentPassword, email, search, spellChecked
81# Sliders
83A slider is great for choosing between a range of numerical values.
85 - **thumb** - The icon that you click and drag to change the value.
86 - **track** - The line behind the thumb denoting where you can slide to.
88@docs slider, Thumb, thumb, defaultThumb
91# Radio Selection
93The fact that we still call this a radio selection is fascinating. I can't remember the last time I actually used an honest-to-goodness button on a radio. Chalk it up along with the floppy disk save icon or the word [Dashboard](https://en.wikipedia.org/wiki/Dashboard).
95Perhaps a better name would be `Input.chooseOne`, because this allows you to select one of a set of options!
97Nevertheless, here we are. Here's how you put one together
99 Input.radio
100 [ padding 10
101 , spacing 20
102 ]
103 { onChange = ChooseLunch
104 , selected = Just model.lunch
105 , label = Input.labelAbove [] (text "Lunch")
106 , options =
107 [ Input.option Burrito (text "Burrito")
108 , Input.option Taco (text "Taco!")
109 , Input.option Gyro (text "Gyro")
110 ]
111 }
113**Note** we're using `Input.option`, which will render the default radio icon you're probably used to. If you want compeltely custom styling, use `Input.optionWith`!
115@docs radio, radioRow, Option, option, optionWith, OptionState
118# Labels
120Every input has a required `Label`.
122@docs Label, labelAbove, labelBelow, labelLeft, labelRight, labelHidden
125# Form Elements
127You might be wondering where something like `<form>` is.
129What I've found is that most people who want `<form>` usually want it for the [implicit submission behavior](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#implicit-submission) or to be clearer, they want to do something when the `Enter` key is pressed.
131Instead of implicit submission behavior, [try making an `onEnter` event handler like in this Ellie Example](https://ellie-app.com/5X6jBKtxzdpa1). Then everything is explicit!
133And no one has to look up obtuse html documentation to understand the behavior of their code :).
136# Disabling Inputs
138You also might be wondering how to disable an input.
140Disabled inputs can be a little problematic for user experience, and doubly so for accessibility. This is because it's now your priority to inform the user _why_ some field is disabled.
142If an input is truly disabled, meaning it's not focusable or doesn't send off a `Msg`, you actually lose your ability to help the user out! For those wary about accessibility [this is a big problem.](https://ux.stackexchange.com/questions/103239/should-disabled-elements-be-focusable-for-accessibility-purposes)
144Here are some alternatives to think about that don't involve explicitly disabling an input.
146**Disabled Buttons** - Change the `Msg` it fires, the text that is rendered, and optionally set a `Region.description` which will be available to screen readers.
148 import Element.Input as Input
149 import Element.Region as Region
151 myButton ready =
152 if ready then
153 Input.button
154 [ Background.color blue
155 ]
156 { onPress =
157 Just SaveButtonPressed
158 , label =
159 text "Save blog post"
160 }
162 else
163 Input.button
164 [ Background.color grey
165 , Region.description
166 "A publish date is required before saving a blogpost."
167 ]
168 { onPress =
169 Just DisabledSaveButtonPressed
170 , label =
171 text "Save Blog "
172 }
174Consider showing a hint if `DisabledSaveButtonPressed` is sent.
176For other inputs such as `Input.text`, consider simply rendering it in a normal `paragraph` or `el` if it's not editable.
178Alternatively, see if it's reasonable to _not_ display an input if you'd normally disable it. Is there an option where it's only visible when it's editable?
180-}
182import Element exposing (Attribute, Color, Element)
183import Element.Background as Background
184import Element.Border as Border
185import Element.Events as Events
186import Element.Font as Font
187import Element.Region as Region
188import Html
189import Html.Attributes
190import Html.Events
191import Internal.Flag as Flag
192import Internal.Model as Internal
193import Internal.Style exposing (classes)
194import Json.Decode as Json
197{-| -}
198type Placeholder msg
199 = Placeholder (List (Attribute msg)) (Element msg)
202white =
203 Element.rgb 1 1 1
206darkGrey =
207 Element.rgb (186 / 255) (189 / 255) (182 / 255)
210charcoal =
211 Element.rgb
212 (136 / 255)
213 (138 / 255)
214 (133 / 255)
217{-| -}
218placeholder : List (Attribute msg) -> Element msg -> Placeholder msg
219placeholder =
220 Placeholder
223type LabelLocation
224 = OnRight
225 | OnLeft
226 | Above
227 | Below
230{-| -}
231type Label msg
232 = Label LabelLocation (List (Attribute msg)) (Element msg)
233 | HiddenLabel String
236isStacked : Label msg -> Bool
237isStacked label =
238 case label of
239 Label loc _ _ ->
240 case loc of
241 OnRight ->
242 False
244 OnLeft ->
245 False
247 Above ->
248 True
250 Below ->
251 True
253 HiddenLabel _ ->
254 True
257{-| -}
258labelRight : List (Attribute msg) -> Element msg -> Label msg
259labelRight =
260 Label OnRight
263{-| -}
264labelLeft : List (Attribute msg) -> Element msg -> Label msg
265labelLeft =
266 Label OnLeft
269{-| -}
270labelAbove : List (Attribute msg) -> Element msg -> Label msg
271labelAbove =
272 Label Above
275{-| -}
276labelBelow : List (Attribute msg) -> Element msg -> Label msg
277labelBelow =
278 Label Below
281{-| Sometimes you may need to have a label which is not visible, but is still accessible to screen readers.
283Seriously consider a visible label before using this.
285The situations where a hidden label makes sense:
287 - A searchbar with a `search` button right next to it.
288 - A `table` of inputs where the header gives the label.
290Basically, a hidden label works when there are other contextual clues that sighted people can pick up on.
292-}
293labelHidden : String -> Label msg
294labelHidden =
295 HiddenLabel
298hiddenLabelAttribute label =
299 case label of
300 HiddenLabel textLabel ->
301 Internal.Describe (Internal.Label textLabel)
303 Label _ _ _ ->
304 Internal.NoAttribute
307{-| A standard button.
309The `onPress` handler will be fired either `onClick` or when the element is focused and the `Enter` key has been pressed.
311 import Element exposing (rgb255, text)
312 import Element.Background as Background
313 import Element.Input as Input
315 blue =
316 Element.rgb255 238 238 238
318 myButton =
319 Input.button
320 [ Background.color blue
321 , Element.focused
322 [ Background.color purple ]
323 ]
324 { onPress = Just ClickMsg
325 , label = text "My Button"
326 }
328-}
329button :
330 List (Attribute msg)
331 ->
332 { onPress : Maybe msg
333 , label : Element msg
334 }
335 -> Element msg
336button attrs { onPress, label } =
337 Internal.element
338 Internal.asEl
339 -- We don't explicitly label this node as a button,
340 -- because buttons fire a bunch of times when you hold down the enter key.
341 -- We'd like to fire just once on the enter key, which means using keyup instead of keydown.
342 -- Because we have no way to disable keydown, though our messages get doubled.
343 Internal.div
344 (Element.width Element.shrink
345 :: Element.height Element.shrink
346 :: Internal.htmlClass
347 (classes.contentCenterX
348 ++ " "
349 ++ classes.contentCenterY
350 ++ " "
351 ++ classes.seButton
352 ++ " "
353 ++ classes.noTextSelection
354 )
355 :: Element.pointer
356 :: focusDefault attrs
357 :: Internal.Describe Internal.Button
358 :: Internal.Attr (Html.Attributes.tabindex 0)
359 :: (case onPress of
360 Nothing ->
361 Internal.Attr (Html.Attributes.disabled True) :: attrs
363 Just msg ->
364 Events.onClick msg
365 :: onEnter msg
366 :: attrs
367 )
368 )
369 (Internal.Unkeyed [ label ])
372focusDefault attrs =
373 if List.any hasFocusStyle attrs then
374 Internal.NoAttribute
376 else
377 Internal.htmlClass "focusable"
380hasFocusStyle attr =
381 case attr of
382 Internal.StyleClass _ (Internal.PseudoSelector Internal.Focus _) ->
383 True
385 _ ->
386 False
389{-| -}
390type alias Checkbox msg =
391 { onChange : Maybe (Bool -> msg)
392 , icon : Maybe (Element msg)
393 , checked : Bool
394 , label : Label msg
395 }
398{-|
400 - **onChange** - The `Msg` to send.
401 - **icon** - The checkbox icon to show. This can be whatever you'd like, but `Input.defaultCheckbox` is included to get you started.
402 - **checked** - The current checked state.
403 - **label** - The [`Label`](#Label) for this checkbox
405-}
406checkbox :
407 List (Attribute msg)
408 ->
409 { onChange : Bool -> msg
410 , icon : Bool -> Element msg
411 , checked : Bool
412 , label : Label msg
413 }
414 -> Element msg
415checkbox attrs { label, icon, checked, onChange } =
416 let
417 attributes =
418 [ if isHiddenLabel label then
419 Internal.NoAttribute
421 else
422 Element.spacing
423 6
424 , Internal.Attr (Html.Events.onClick (onChange (not checked)))
425 , Region.announce
426 , onKeyLookup <|
427 \code ->
428 if code == enter then
429 Just <| onChange (not checked)
431 else if code == space then
432 Just <| onChange (not checked)
434 else
435 Nothing
436 , tabindex 0
437 , Element.pointer
438 , Element.alignLeft
439 , Element.width Element.fill
440 ]
441 ++ attrs
442 in
443 applyLabel attributes
444 label
445 (Internal.element
446 Internal.asEl
447 Internal.div
448 [ Internal.Attr <|
449 Html.Attributes.attribute "role" "checkbox"
450 , Internal.Attr <|
451 Html.Attributes.attribute "aria-checked" <|
452 if checked then
453 "true"
455 else
456 "false"
457 , hiddenLabelAttribute label
458 , Element.centerY
459 , Element.height Element.fill
460 , Element.width Element.shrink
461 ]
462 (Internal.Unkeyed
463 [ icon checked
464 ]
465 )
466 )
469{-| -}
470type Thumb
471 = Thumb (List (Attribute Never))
474{-| -}
475thumb : List (Attribute Never) -> Thumb
476thumb =
477 Thumb
480{-| -}
481defaultThumb : Thumb
482defaultThumb =
483 Thumb
484 [ Element.width (Element.px 16)
485 , Element.height (Element.px 16)
486 , Border.rounded 8
487 , Border.width 1
488 , Border.color (Element.rgb 0.5 0.5 0.5)
489 , Background.color (Element.rgb 1 1 1)
490 ]
493{-| A slider input, good for capturing float values.
495 Input.slider
496 [ Element.height (Element.px 30)
498 -- Here is where we're creating/styling the "track"
499 , Element.behindContent
500 (Element.el
501 [ Element.width Element.fill
502 , Element.height (Element.px 2)
503 , Element.centerY
504 , Background.color grey
505 , Border.rounded 2
506 ]
507 Element.none
508 )
509 ]
510 { onChange = AdjustValue
511 , label =
512 Input.labelAbove []
513 (text "My Slider Value")
514 , min = 0
515 , max = 75
516 , step = Nothing
517 , value = model.sliderValue
518 , thumb =
519 Input.defaultThumb
520 }
522`Element.behindContent` is used to render the track of the slider. Without it, no track would be rendered. The `thumb` is the icon that you can move around.
524The slider can be vertical or horizontal depending on the width/height of the slider.
526 - `height fill` and `width (px someWidth)` will cause the slider to be vertical.
527 - `height (px someHeight)` and `width (px someWidth)` where `someHeight` > `someWidth` will also do it.
528 - otherwise, the slider will be horizontal.
530**Note** If you want a slider for an `Int` value:
532 - set `step` to be `Just 1`, or some other whole value
533 - `value = toFloat model.myInt`
534 - And finally, round the value before making a message `onChange = round >> AdjustValue`
536-}
537slider :
538 List (Attribute msg)
539 ->
540 { onChange : Float -> msg
541 , label : Label msg
542 , min : Float
543 , max : Float
544 , value : Float
545 , thumb : Thumb
546 , step : Maybe Float
547 }
548 -> Element msg
549slider attributes input =
550 let
551 (Thumb thumbAttributes) =
552 input.thumb
554 width =
555 Internal.getWidth thumbAttributes
557 height =
558 Internal.getHeight thumbAttributes
560 vertical =
561 case ( trackWidth, trackHeight ) of
562 ( Nothing, Nothing ) ->
563 False
565 ( Just (Internal.Px w), Just (Internal.Px h) ) ->
566 h > w
568 ( Just (Internal.Px _), Just (Internal.Fill _) ) ->
569 True
571 _ ->
572 False
574 trackHeight =
575 Internal.getHeight attributes
577 trackWidth =
578 Internal.getWidth attributes
580 ( spacingX, spacingY ) =
581 Internal.getSpacing attributes ( 5, 5 )
583 factor =
584 (input.value - input.min)
585 / (input.max - input.min)
587 {- Needed attributes
589 Thumb Attributes
590 - Width/Height of thumb so that the input can shadow it.
593 Attributes
595 OnParent ->
596 Spacing
599 On track ->
600 Everything else
605 The `<input>`
608 -}
609 className =
610 "thmb-" ++ thumbWidthString ++ "-" ++ thumbHeightString
612 thumbWidthString =
613 case width of
614 Nothing ->
615 "20px"
617 Just (Internal.Px px) ->
618 String.fromInt px ++ "px"
620 _ ->
621 "100%"
623 thumbHeightString =
624 case height of
625 Nothing ->
626 "20px"
628 Just (Internal.Px px) ->
629 String.fromInt px ++ "px"
631 _ ->
632 "100%"
634 thumbShadowStyle =
635 [ Internal.Property "width"
636 thumbWidthString
637 , Internal.Property "height"
638 thumbHeightString
639 ]
640 in
641 applyLabel
642 [ if isHiddenLabel input.label then
643 Internal.NoAttribute
645 else
646 Element.spacingXY spacingX spacingY
647 , Region.announce
648 , Element.width
649 (case trackWidth of
650 Nothing ->
651 Element.fill
653 Just (Internal.Px _) ->
654 Element.shrink
656 Just x ->
657 x
658 )
659 , Element.height
660 (case trackHeight of
661 Nothing ->
662 Element.shrink
664 Just (Internal.Px _) ->
665 Element.shrink
667 Just x ->
668 x
669 )
670 ]
671 input.label
672 (Element.row
673 [ Element.width
674 (Maybe.withDefault Element.fill trackWidth)
675 , Element.height
676 (Maybe.withDefault (Element.px 20) trackHeight)
677 ]
678 [ Internal.element
679 Internal.asEl
680 (Internal.NodeName "input")
681 [ hiddenLabelAttribute input.label
682 , Internal.StyleClass Flag.active
683 (Internal.Style
684 ("input[type=\"range\"]." ++ className ++ "::-moz-range-thumb")
685 thumbShadowStyle
686 )
687 , Internal.StyleClass Flag.hover
688 (Internal.Style
689 ("input[type=\"range\"]." ++ className ++ "::-webkit-slider-thumb")
690 thumbShadowStyle
691 )
692 , Internal.StyleClass Flag.focus
693 (Internal.Style
694 ("input[type=\"range\"]." ++ className ++ "::-ms-thumb")
695 thumbShadowStyle
696 )
697 , Internal.Attr (Html.Attributes.class (className ++ " focusable-parent"))
698 , Internal.Attr
699 (Html.Events.onInput
700 (\str ->
701 case String.toFloat str of
702 Nothing ->
703 -- This should never happen because the browser
704 -- should always provide a Float.
705 input.onChange 0
707 Just val ->
708 input.onChange val
709 )
710 )
711 , Internal.Attr <|
712 Html.Attributes.type_ "range"
713 , Internal.Attr <|
714 Html.Attributes.step
715 (case input.step of
716 Nothing ->
717 -- Note: If we set `any` here,
718 -- Firefox makes a single press of the arrows keys equal to 1
719 -- We could set the step manually to the effective range / 100
720 -- String.fromFloat ((input.max - input.min) / 100)
721 -- Which matches Chrome's default behavior
722 -- HOWEVER, that means manually moving a slider with the mouse will snap to that interval.
723 "any"
725 Just step ->
726 String.fromFloat step
727 )
728 , Internal.Attr <|
729 Html.Attributes.min (String.fromFloat input.min)
730 , Internal.Attr <|
731 Html.Attributes.max (String.fromFloat input.max)
732 , Internal.Attr <|
733 Html.Attributes.value (String.fromFloat input.value)
734 , if vertical then
735 Internal.Attr <|
736 Html.Attributes.attribute "orient" "vertical"
738 else
739 Internal.NoAttribute
740 , Element.width <|
741 if vertical then
742 Maybe.withDefault (Element.px 20) trackHeight
744 else
745 Maybe.withDefault Element.fill trackWidth
746 , Element.height <|
747 if vertical then
748 Maybe.withDefault Element.fill trackWidth
750 else
751 Maybe.withDefault (Element.px 20) trackHeight
752 ]
753 (Internal.Unkeyed [])
754 , Element.el
755 (Element.width
756 (Maybe.withDefault Element.fill trackWidth)
757 :: Element.height
758 (Maybe.withDefault (Element.px 20) trackHeight)
759 :: attributes
760 -- This is after `attributes` because the thumb should be in front of everything.
761 ++ [ Element.behindContent <|
762 if vertical then
763 viewVerticalThumb factor thumbAttributes trackWidth
765 else
766 viewHorizontalThumb factor thumbAttributes trackHeight
767 ]
768 )
769 Element.none
770 ]
771 )
774viewHorizontalThumb factor thumbAttributes trackHeight =
775 Element.row
776 [ Element.width Element.fill
777 , Element.height (Maybe.withDefault Element.fill trackHeight)
778 , Element.centerY
779 ]
780 [ Element.el
781 [ Element.width (Element.fillPortion (round <| factor * 10000))
782 ]
783 Element.none
784 , Element.el
785 (Element.centerY
786 :: List.map (Internal.mapAttr Basics.never) thumbAttributes
787 )
788 Element.none
789 , Element.el
790 [ Element.width (Element.fillPortion (round <| (abs <| 1 - factor) * 10000))
791 ]
792 Element.none
793 ]
796viewVerticalThumb factor thumbAttributes trackWidth =
797 Element.column
798 [ Element.height Element.fill
799 , Element.width (Maybe.withDefault Element.fill trackWidth)
800 , Element.centerX
801 ]
802 [ Element.el
803 [ Element.height (Element.fillPortion (round <| (abs <| 1 - factor) * 10000))
804 ]
805 Element.none
806 , Element.el
807 (Element.centerX
808 :: List.map (Internal.mapAttr Basics.never) thumbAttributes
809 )
810 Element.none
811 , Element.el
812 [ Element.height (Element.fillPortion (round <| factor * 10000))
813 ]
814 Element.none
815 ]
818type alias TextInput =
819 { type_ : TextKind
820 , spellchecked : Bool
821 , autofill : Maybe String
822 }
825type TextKind
826 = TextInputNode String
827 | TextArea
830{-| -}
831type alias Text msg =
832 { onChange : String -> msg
833 , text : String
834 , placeholder : Maybe (Placeholder msg)
835 , label : Label msg
836 }
839{-| -}
840textHelper : TextInput -> List (Attribute msg) -> Text msg -> Element msg
841textHelper textInput attrs textOptions =
842 let
843 withDefaults =
844 defaultTextBoxStyle ++ attrs
846 redistributed =
847 redistribute (textInput.type_ == TextArea)
848 (isStacked textOptions.label)
849 withDefaults
851 onlySpacing attr =
852 case attr of
853 Internal.StyleClass _ (Internal.SpacingStyle _ _ _) ->
854 True
856 _ ->
857 False
859 getPadding attr =
860 case attr of
861 Internal.StyleClass cls (Internal.PaddingStyle pad t r b l) ->
862 -- The - 3 is here to prevent accidental triggering of scrollbars
863 -- when things are off by a pixel or two.
864 -- (or at least when the browser *thinks* it's off by a pixel or two)
865 Just
866 { top = max 0 (floor (toFloat t - 3))
867 , right = max 0 (floor (toFloat r - 3))
868 , bottom = max 0 (floor (toFloat b - 3))
869 , left = max 0 (floor (toFloat l - 3))
870 }
872 _ ->
873 Nothing
875 heightConstrained =
876 case textInput.type_ of
877 TextInputNode inputType ->
878 False
880 TextArea ->
881 withDefaults
882 |> List.filterMap getHeight
883 |> List.reverse
884 |> List.head
885 |> Maybe.map isConstrained
886 |> Maybe.withDefault False
888 parentPadding =
889 withDefaults
890 |> List.filterMap getPadding
891 |> List.reverse
892 |> List.head
893 |> Maybe.withDefault
894 { top = 0
895 , right = 0
896 , bottom = 0
897 , left = 0
898 }
900 inputElement =
901 Internal.element
902 Internal.asEl
903 (case textInput.type_ of
904 TextInputNode inputType ->
905 Internal.NodeName "input"
907 TextArea ->
908 Internal.NodeName "textarea"
909 )
910 ((case textInput.type_ of
911 TextInputNode inputType ->
912 -- Note: Due to a weird edgecase in...Edge...
913 -- `type` needs to come _before_ `value`
914 -- More reading: https://github.com/mdgriffith/elm-ui/pull/94/commits/4f493a27001ccc3cf1f2baa82e092c35d3811876
915 [ Internal.Attr (Html.Attributes.type_ inputType)
916 , Internal.htmlClass classes.inputText
917 ]
919 TextArea ->
920 [ Element.clip
921 , Element.height Element.fill
922 , Internal.htmlClass classes.inputMultiline
923 , calcMoveToCompensateForPadding withDefaults
925 -- The only reason we do this padding trick is so that when the user clicks in the padding,
926 -- that the cursor will reset correctly.
927 -- This could probably be combined with the above `calcMoveToCompensateForPadding`
928 , Element.paddingEach parentPadding
929 , Internal.Attr (Html.Attributes.style "margin" (renderBox (negateBox parentPadding)))
930 , Internal.Attr (Html.Attributes.style "box-sizing" "content-box")
931 ]
932 )
933 ++ [ value textOptions.text
934 , Internal.Attr (Html.Events.onInput textOptions.onChange)
935 , hiddenLabelAttribute textOptions.label
936 , spellcheck textInput.spellchecked
937 , Maybe.map autofill textInput.autofill
938 |> Maybe.withDefault Internal.NoAttribute
939 ]
940 ++ redistributed.input
941 )
942 (Internal.Unkeyed [])
944 wrappedInput =
945 case textInput.type_ of
946 TextArea ->
947 -- textarea with height-content means that
948 -- the input element is rendered `inFront` with a transparent background
949 -- Then the input text is rendered as the space filling element.
950 Internal.element
951 Internal.asEl
952 Internal.div
953 ((if heightConstrained then
954 (::) Element.scrollbarY
956 else
957 identity
958 )
959 [ Element.width Element.fill
960 , Internal.htmlClass classes.focusedWithin
961 , Internal.htmlClass classes.inputMultilineWrapper
962 ]
963 ++ redistributed.parent
964 )
965 (Internal.Unkeyed
966 [ Internal.element
967 Internal.asParagraph
968 Internal.div
969 (Element.width Element.fill
970 :: Element.height Element.fill
971 :: Element.inFront inputElement
972 :: Internal.htmlClass classes.inputMultilineParent
973 :: redistributed.wrapper
974 )
975 (Internal.Unkeyed
976 (if textOptions.text == "" then
977 case textOptions.placeholder of
978 Nothing ->
979 -- Without this, firefox will make the text area lose focus
980 -- if the input is empty and you mash the keyboard
981 [ Element.text "\u{00A0}"
982 ]
984 Just place ->
985 [ renderPlaceholder place
986 []
987 (textOptions.text == "")
988 ]
990 else
991 [ Internal.unstyled
992 (Html.span [ Html.Attributes.class classes.inputMultilineFiller ]
993 -- We append a non-breaking space to the end of the content so that newlines don't get chomped.
994 [ Html.text (textOptions.text ++ "\u{00A0}")
995 ]
996 )
997 ]
998 )
999 )
1003 TextInputNode inputType ->
1004 Internal.element
1005 Internal.asEl
1006 Internal.div
1007 (Element.width Element.fill
1008 :: Internal.htmlClass classes.focusedWithin
1009 :: List.concat
1010 [ redistributed.parent
1011 , case textOptions.placeholder of
1012 Nothing ->
1013 []
1015 Just place ->
1016 [ Element.behindContent
1017 (renderPlaceholder place redistributed.cover (textOptions.text == ""))
1021 (Internal.Unkeyed [ inputElement ])
1022 in
1023 applyLabel
1024 (Internal.Class Flag.cursor classes.cursorText
1025 :: (if isHiddenLabel textOptions.label then
1026 Internal.NoAttribute
1028 else
1029 Element.spacing
1032 :: Region.announce
1033 :: redistributed.fullParent
1035 textOptions.label
1036 wrappedInput
1039getHeight attr =
1040 case attr of
1041 Internal.Height h ->
1042 Just h
1044 _ ->
1045 Nothing
1048negateBox box =
1049 { top = negate box.top
1050 , right = negate box.right
1051 , bottom = negate box.bottom
1052 , left = negate box.left
1056renderBox { top, right, bottom, left } =
1057 String.fromInt top
1058 ++ "px "
1059 ++ String.fromInt right
1060 ++ "px "
1061 ++ String.fromInt bottom
1062 ++ "px "
1063 ++ String.fromInt left
1064 ++ "px"
1067renderPlaceholder (Placeholder placeholderAttrs placeholderEl) forPlaceholder on =
1068 Element.el
1069 (forPlaceholder
1070 ++ [ Font.color charcoal
1071 , Internal.htmlClass (classes.noTextSelection ++ " " ++ classes.passPointerEvents)
1072 , Element.clip
1073 , Border.color (Element.rgba 0 0 0 0)
1074 , Background.color (Element.rgba 0 0 0 0)
1075 , Element.height Element.fill
1076 , Element.width Element.fill
1077 , Element.alpha
1078 (if on then
1081 else
1085 ++ placeholderAttrs
1087 placeholderEl
1090{-| Because textareas are now shadowed, where they're rendered twice,
1091we to move the literal text area up because spacing is based on line height.
1093calcMoveToCompensateForPadding : List (Attribute msg) -> Attribute msg
1094calcMoveToCompensateForPadding attrs =
1095 let
1096 gatherSpacing attr found =
1097 case attr of
1098 Internal.StyleClass _ (Internal.SpacingStyle _ x y) ->
1099 case found of
1100 Nothing ->
1101 Just y
1103 _ ->
1104 found
1106 _ ->
1107 found
1108 in
1109 case List.foldr gatherSpacing Nothing attrs of
1110 Nothing ->
1111 Internal.NoAttribute
1113 Just vSpace ->
1114 Element.moveUp (toFloat vSpace / 2)
1117{-| Given the list of attributes provided to `Input.multiline` or `Input.text`,
1119redistribute them to the parent, the input, or the cover.
1121 - fullParent -> Wrapper around label and input
1122 - parent -> parent of wrapper
1123 - wrapper -> the element that is here to take up space.
1124 - cover -> things like placeholders or text areas which are layered on top of input.
1125 - input -> actual input element
1128redistribute :
1129 Bool
1130 -> Bool
1131 -> List (Attribute msg)
1132 ->
1133 { fullParent : List (Attribute msg)
1134 , parent : List (Attribute msg)
1135 , wrapper : List (Attribute msg)
1136 , input : List (Attribute msg)
1137 , cover : List (Attribute msg)
1139redistribute isMultiline stacked attrs =
1140 List.foldl (redistributeOver isMultiline stacked)
1141 { fullParent = []
1142 , parent = []
1143 , input = []
1144 , cover = []
1145 , wrapper = []
1147 attrs
1148 |> (\redist ->
1149 { parent = List.reverse redist.parent
1150 , fullParent = List.reverse redist.fullParent
1151 , wrapper = List.reverse redist.wrapper
1152 , input = List.reverse redist.input
1153 , cover = List.reverse redist.cover
1158isFill len =
1159 case len of
1160 Internal.Fill _ ->
1161 True
1163 Internal.Content ->
1164 False
1166 Internal.Px _ ->
1167 False
1169 Internal.Min _ l ->
1170 isFill l
1172 Internal.Max _ l ->
1173 isFill l
1176isShrink len =
1177 case len of
1178 Internal.Content ->
1179 True
1181 Internal.Px _ ->
1182 False
1184 Internal.Fill _ ->
1185 False
1187 Internal.Min _ l ->
1188 isShrink l
1190 Internal.Max _ l ->
1191 isShrink l
1194isConstrained len =
1195 case len of
1196 Internal.Content ->
1197 False
1199 Internal.Px _ ->
1200 True
1202 Internal.Fill _ ->
1203 True
1205 Internal.Min _ l ->
1206 isConstrained l
1208 Internal.Max _ l ->
1209 True
1212isPixel len =
1213 case len of
1214 Internal.Content ->
1215 False
1217 Internal.Px _ ->
1218 True
1220 Internal.Fill _ ->
1221 False
1223 Internal.Min _ l ->
1224 isPixel l
1226 Internal.Max _ l ->
1227 isPixel l
1230{-| isStacked means that the label is above or below
1232redistributeOver isMultiline stacked attr els =
1233 case attr of
1234 Internal.Nearby _ _ ->
1235 { els | parent = attr :: els.parent }
1237 Internal.Width width ->
1238 if stacked then
1239 { els | fullParent = attr :: els.fullParent }
1241 else if isFill width then
1242 { els
1243 | fullParent = attr :: els.fullParent
1244 , parent = attr :: els.parent
1247 else if isPixel width then
1248 { els | parent = attr :: els.parent }
1250 else
1251 { els
1252 | parent = attr :: els.parent
1255 Internal.Height height ->
1256 if not stacked then
1257 { els
1258 | fullParent = attr :: els.fullParent
1259 , parent = attr :: els.parent
1262 else if isFill height then
1263 { els
1264 | fullParent = attr :: els.fullParent
1265 , parent = attr :: els.parent
1268 else if isPixel height then
1269 { els | parent = attr :: els.parent }
1271 else
1272 { els
1273 | parent = attr :: els.parent
1276 Internal.AlignX _ ->
1277 { els | fullParent = attr :: els.fullParent }
1279 Internal.AlignY _ ->
1280 { els | fullParent = attr :: els.fullParent }
1282 Internal.StyleClass _ (Internal.SpacingStyle _ _ _) ->
1283 { els
1284 | fullParent = attr :: els.fullParent
1285 , parent = attr :: els.parent
1286 , input = attr :: els.input
1287 , wrapper = attr :: els.wrapper
1290 Internal.StyleClass cls (Internal.PaddingStyle pad t r b l) ->
1291 if isMultiline then
1292 { els
1293 | parent = attr :: els.parent
1294 , cover = attr :: els.cover
1297 else
1298 let
1299 newHeight =
1300 Element.htmlAttribute
1301 (Html.Attributes.style
1302 "height"
1303 ("calc(1.0em + " ++ String.fromInt (2 * min t b) ++ "px)")
1306 newLineHeight =
1307 Element.htmlAttribute
1308 (Html.Attributes.style
1309 "line-height"
1310 ("calc(1.0em + " ++ String.fromInt (2 * min t b) ++ "px)")
1313 reducedVerticalPadding =
1314 Element.paddingEach
1315 { top = t - min t b
1316 , right = r
1317 , bottom = b - min t b
1318 , left = l
1320 in
1321 { els
1322 | parent = reducedVerticalPadding :: els.parent
1323 , input = newHeight :: newLineHeight :: els.input
1324 , cover = attr :: els.cover
1327 Internal.StyleClass _ (Internal.BorderWidth _ _ _ _ _) ->
1328 { els
1329 | parent = attr :: els.parent
1330 , cover = attr :: els.cover
1333 Internal.StyleClass _ (Internal.Transform _) ->
1334 { els
1335 | parent = attr :: els.parent
1336 , cover = attr :: els.cover
1339 Internal.StyleClass _ (Internal.FontSize _) ->
1340 { els | fullParent = attr :: els.fullParent }
1342 Internal.StyleClass _ (Internal.FontFamily _ _) ->
1343 { els | fullParent = attr :: els.fullParent }
1345 Internal.StyleClass flag cls ->
1346 { els | parent = attr :: els.parent }
1348 Internal.NoAttribute ->
1349 els
1351 Internal.Attr a ->
1352 { els | input = attr :: els.input }
1354 Internal.Describe _ ->
1355 { els | input = attr :: els.input }
1357 Internal.Class _ _ ->
1358 { els | parent = attr :: els.parent }
1360 Internal.TransformComponent _ _ ->
1361 { els | input = attr :: els.input }
1364{-| -}
1365text :
1366 List (Attribute msg)
1367 ->
1368 { onChange : String -> msg
1369 , text : String
1370 , placeholder : Maybe (Placeholder msg)
1371 , label : Label msg
1373 -> Element msg
1374text =
1375 textHelper
1376 { type_ = TextInputNode "text"
1377 , spellchecked = False
1378 , autofill = Nothing
1382{-| If spell checking is available, this input will be spellchecked.
1384spellChecked :
1385 List (Attribute msg)
1386 ->
1387 { onChange : String -> msg
1388 , text : String
1389 , placeholder : Maybe (Placeholder msg)
1390 , label : Label msg
1392 -> Element msg
1393spellChecked =
1394 textHelper
1395 { type_ = TextInputNode "text"
1396 , spellchecked = True
1397 , autofill = Nothing
1401{-| -}
1402search :
1403 List (Attribute msg)
1404 ->
1405 { onChange : String -> msg
1406 , text : String
1407 , placeholder : Maybe (Placeholder msg)
1408 , label : Label msg
1410 -> Element msg
1411search =
1412 textHelper
1413 { type_ = TextInputNode "search"
1414 , spellchecked = False
1415 , autofill = Nothing
1419{-| A password input that allows the browser to autofill.
1421It's `newPassword` instead of just `password` because it gives the browser a hint on what type of password input it is.
1423A password takes all the arguments a normal `Input.text` would, and also **show**, which will remove the password mask (e.g. `****` vs `pass1234`)
1426newPassword :
1427 List (Attribute msg)
1428 ->
1429 { onChange : String -> msg
1430 , text : String
1431 , placeholder : Maybe (Placeholder msg)
1432 , label : Label msg
1433 , show : Bool
1435 -> Element msg
1436newPassword attrs pass =
1437 textHelper
1438 { type_ =
1439 TextInputNode <|
1440 if pass.show then
1441 "text"
1443 else
1444 "password"
1445 , spellchecked = False
1446 , autofill = Just "new-password"
1448 attrs
1449 { onChange = pass.onChange
1450 , text = pass.text
1451 , placeholder = pass.placeholder
1452 , label = pass.label
1456{-| -}
1457currentPassword :
1458 List (Attribute msg)
1459 ->
1460 { onChange : String -> msg
1461 , text : String
1462 , placeholder : Maybe (Placeholder msg)
1463 , label : Label msg
1464 , show : Bool
1466 -> Element msg
1467currentPassword attrs pass =
1468 textHelper
1469 { type_ =
1470 TextInputNode <|
1471 if pass.show then
1472 "text"
1474 else
1475 "password"
1476 , spellchecked = False
1477 , autofill = Just "current-password"
1479 attrs
1480 { onChange = pass.onChange
1481 , text = pass.text
1482 , placeholder = pass.placeholder
1483 , label = pass.label
1487{-| -}
1488username :
1489 List (Attribute msg)
1490 ->
1491 { onChange : String -> msg
1492 , text : String
1493 , placeholder : Maybe (Placeholder msg)
1494 , label : Label msg
1496 -> Element msg
1497username =
1498 textHelper
1499 { type_ = TextInputNode "text"
1500 , spellchecked = False
1501 , autofill = Just "username"
1505{-| -}
1506email :
1507 List (Attribute msg)
1508 ->
1509 { onChange : String -> msg
1510 , text : String
1511 , placeholder : Maybe (Placeholder msg)
1512 , label : Label msg
1514 -> Element msg
1515email =
1516 textHelper
1517 { type_ = TextInputNode "email"
1518 , spellchecked = False
1519 , autofill = Just "email"
1523{-| A multiline text input.
1525By default it will have a minimum height of one line and resize based on it's contents.
1528multiline :
1529 List (Attribute msg)
1530 ->
1531 { onChange : String -> msg
1532 , text : String
1533 , placeholder : Maybe (Placeholder msg)
1534 , label : Label msg
1535 , spellcheck : Bool
1537 -> Element msg
1538multiline attrs multi =
1539 textHelper
1540 { type_ =
1541 TextArea
1542 , spellchecked = multi.spellcheck
1543 , autofill = Nothing
1545 attrs
1546 { onChange = multi.onChange
1547 , text = multi.text
1548 , placeholder = multi.placeholder
1549 , label = multi.label
1553isHiddenLabel label =
1554 case label of
1555 HiddenLabel _ ->
1556 True
1558 _ ->
1559 False
1562applyLabel : List (Attribute msg) -> Label msg -> Element msg -> Element msg
1563applyLabel attrs label input =
1564 case label of
1565 HiddenLabel labelText ->
1566 -- NOTE: This means that the label is applied outside of this function!
1567 -- It would be nice to unify this logic, but it's a little tricky
1568 Internal.element
1569 Internal.asColumn
1570 (Internal.NodeName "label")
1571 attrs
1572 (Internal.Unkeyed [ input ])
1574 Label position labelAttrs labelChild ->
1575 let
1576 labelElement =
1577 Internal.element
1578 Internal.asEl
1579 Internal.div
1580 labelAttrs
1581 (Internal.Unkeyed [ labelChild ])
1582 in
1583 case position of
1584 Above ->
1585 Internal.element
1586 Internal.asColumn
1587 (Internal.NodeName "label")
1588 attrs
1589 (Internal.Unkeyed [ labelElement, input ])
1591 Below ->
1592 Internal.element
1593 Internal.asColumn
1594 (Internal.NodeName "label")
1595 attrs
1596 (Internal.Unkeyed [ input, labelElement ])
1598 OnRight ->
1599 Internal.element
1600 Internal.asRow
1601 (Internal.NodeName "label")
1602 attrs
1603 (Internal.Unkeyed [ input, labelElement ])
1605 OnLeft ->
1606 Internal.element
1607 Internal.asRow
1608 (Internal.NodeName "label")
1609 attrs
1610 (Internal.Unkeyed [ labelElement, input ])
1613{-| -}
1614type Option value msg
1615 = Option value (OptionState -> Element msg)
1618{-| -}
1619type OptionState
1620 = Idle
1621 | Focused
1622 | Selected
1625{-| Add a choice to your radio element. This will be rendered with the default radio icon.
1627option : value -> Element msg -> Option value msg
1628option val txt =
1629 Option val (defaultRadioOption txt)
1632{-| Customize exactly what your radio option should look like in different states.
1634optionWith : value -> (OptionState -> Element msg) -> Option value msg
1635optionWith val view =
1636 Option val view
1639{-| -}
1640radio :
1641 List (Attribute msg)
1642 ->
1643 { onChange : option -> msg
1644 , options : List (Option option msg)
1645 , selected : Maybe option
1646 , label : Label msg
1648 -> Element msg
1649radio =
1650 radioHelper Column
1653{-| Same as radio, but displayed as a row
1655radioRow :
1656 List (Attribute msg)
1657 ->
1658 { onChange : option -> msg
1659 , options : List (Option option msg)
1660 , selected : Maybe option
1661 , label : Label msg
1663 -> Element msg
1664radioRow =
1665 radioHelper Row
1668defaultRadioOption : Element msg -> OptionState -> Element msg
1669defaultRadioOption optionLabel status =
1670 Element.row
1671 [ Element.spacing 10
1672 , Element.alignLeft
1673 , Element.width Element.shrink
1675 [ Element.el
1676 [ Element.width (Element.px 14)
1677 , Element.height (Element.px 14)
1678 , Background.color white
1679 , Border.rounded 7
1680 , case status of
1681 Selected ->
1682 Internal.htmlClass "focusable"
1684 _ ->
1685 Internal.NoAttribute
1687 -- , Border.shadow <|
1688 -- -- case status of
1689 -- -- Idle ->
1690 -- -- { offset = ( 0, 0 )
1691 -- -- , blur =
1692 -- -- 1
1693 -- -- , color = Color.rgb 235 235 235
1694 -- -- }
1695 -- -- Focused ->
1696 -- -- { offset = ( 0, 0 )
1697 -- -- , blur =
1698 -- -- 0
1699 -- -- , color = Color.rgba 235 235 235 0
1700 -- -- }
1701 -- -- Selected ->
1702 -- { offset = ( 0, 0 )
1703 -- , blur =
1704 -- 1
1705 -- , color = Color.rgba 235 235 235 0
1706 -- }
1707 , Border.width <|
1708 case status of
1709 Idle ->
1712 Focused ->
1715 Selected ->
1717 , Border.color <|
1718 case status of
1719 Idle ->
1720 Element.rgb (208 / 255) (208 / 255) (208 / 255)
1722 Focused ->
1723 Element.rgb (208 / 255) (208 / 255) (208 / 255)
1725 Selected ->
1726 Element.rgb (59 / 255) (153 / 255) (252 / 255)
1728 Element.none
1729 , Element.el [ Element.width Element.fill, Internal.htmlClass "unfocusable" ] optionLabel
1733radioHelper :
1734 Orientation
1735 -> List (Attribute msg)
1736 ->
1737 { onChange : option -> msg
1738 , options : List (Option option msg)
1739 , selected : Maybe option
1740 , label : Label msg
1742 -> Element msg
1743radioHelper orientation attrs input =
1744 let
1745 renderOption (Option val view) =
1746 let
1747 status =
1748 if Just val == input.selected then
1749 Selected
1751 else
1752 Idle
1753 in
1754 Element.el
1755 [ Element.pointer
1756 , case orientation of
1757 Row ->
1758 Element.width Element.shrink
1760 Column ->
1761 Element.width Element.fill
1762 , Events.onClick (input.onChange val)
1763 , case status of
1764 Selected ->
1765 Internal.Attr <|
1766 Html.Attributes.attribute "aria-checked"
1767 "true"
1769 _ ->
1770 Internal.Attr <|
1771 Html.Attributes.attribute "aria-checked"
1772 "false"
1773 , Internal.Attr <|
1774 Html.Attributes.attribute "role" "radio"
1776 (view status)
1778 optionArea =
1779 case orientation of
1780 Row ->
1781 row (hiddenLabelAttribute input.label :: attrs)
1782 (List.map renderOption input.options)
1784 Column ->
1785 column (hiddenLabelAttribute input.label :: attrs)
1786 (List.map renderOption input.options)
1788 prevNext =
1789 case input.options of
1790 [] ->
1791 Nothing
1793 (Option val _) :: _ ->
1794 List.foldl track ( NotFound, val, val ) input.options
1795 |> (\( found, b, a ) ->
1796 case found of
1797 NotFound ->
1798 Just ( b, val )
1800 BeforeFound ->
1801 Just ( b, val )
1803 _ ->
1804 Just ( b, a )
1807 track opt ( found, prev, nxt ) =
1808 case opt of
1809 Option val _ ->
1810 case found of
1811 NotFound ->
1812 if Just val == input.selected then
1813 ( BeforeFound, prev, nxt )
1815 else
1816 ( found, val, nxt )
1818 BeforeFound ->
1819 ( AfterFound, prev, val )
1821 AfterFound ->
1822 ( found, prev, nxt )
1824 events =
1825 Internal.get
1826 attrs
1827 <|
1828 \attr ->
1829 case attr of
1830 Internal.Width (Internal.Fill _) ->
1831 True
1833 Internal.Height (Internal.Fill _) ->
1834 True
1836 Internal.Attr _ ->
1837 True
1839 _ ->
1840 False
1841 in
1842 applyLabel
1843 (List.filterMap identity
1844 [ Just Element.alignLeft
1845 , Just (tabindex 0)
1846 , Just (Internal.htmlClass "focus")
1847 , Just Region.announce
1848 , Just <|
1849 Internal.Attr <|
1850 Html.Attributes.attribute "role" "radiogroup"
1851 , case prevNext of
1852 Nothing ->
1853 Nothing
1855 Just ( prev, next ) ->
1856 Just
1857 (onKeyLookup <|
1858 \code ->
1859 if code == leftArrow then
1860 Just (input.onChange prev)
1862 else if code == upArrow then
1863 Just (input.onChange prev)
1865 else if code == rightArrow then
1866 Just (input.onChange next)
1868 else if code == downArrow then
1869 Just (input.onChange next)
1871 else if code == space then
1872 case input.selected of
1873 Nothing ->
1874 Just (input.onChange prev)
1876 _ ->
1877 Nothing
1879 else
1880 Nothing
1883 ++ events
1884 -- ++ hideIfEverythingisInvisible
1886 input.label
1887 optionArea
1890type Found
1891 = NotFound
1892 | BeforeFound
1893 | AfterFound
1896type Orientation
1897 = Row
1898 | Column
1901column : List (Attribute msg) -> List (Internal.Element msg) -> Internal.Element msg
1902column attributes children =
1903 Internal.element
1904 Internal.asColumn
1905 Internal.div
1906 (Element.height Element.shrink
1907 :: Element.width Element.fill
1908 :: attributes
1910 (Internal.Unkeyed children)
1913row : List (Attribute msg) -> List (Internal.Element msg) -> Internal.Element msg
1914row attributes children =
1915 Internal.element
1916 Internal.asRow
1917 Internal.div
1918 (Element.width Element.fill
1919 :: attributes
1921 (Internal.Unkeyed children)
1925{- Event Handlers -}
1928{-| -}
1929onEnter : msg -> Attribute msg
1930onEnter msg =
1931 onKey enter msg
1934{-| -}
1935onSpace : msg -> Attribute msg
1936onSpace msg =
1937 onKey space msg
1940{-| -}
1941onUpArrow : msg -> Attribute msg
1942onUpArrow msg =
1943 onKey upArrow msg
1946{-| -}
1947onRightArrow : msg -> Attribute msg
1948onRightArrow msg =
1949 onKey rightArrow msg
1952{-| -}
1953onLeftArrow : msg -> Attribute msg
1954onLeftArrow msg =
1955 onKey leftArrow msg
1958{-| -}
1959onDownArrow : msg -> Attribute msg
1960onDownArrow msg =
1961 onKey downArrow msg
1964enter : String
1965enter =
1966 "Enter"
1969tab : String
1970tab =
1971 "Tab"
1974delete : String
1975delete =
1976 "Delete"
1979backspace : String
1980backspace =
1981 "Backspace"
1984upArrow : String
1985upArrow =
1986 "ArrowUp"
1989leftArrow : String
1990leftArrow =
1991 "ArrowLeft"
1994rightArrow : String
1995rightArrow =
1996 "ArrowRight"
1999downArrow : String
2000downArrow =
2001 "ArrowDown"
2004space : String
2005space =
2006 " "
2009{-| -}
2010onKey : String -> msg -> Attribute msg
2011onKey desiredCode msg =
2012 let
2013 decode code =
2014 if code == desiredCode then
2015 Json.succeed msg
2017 else
2018 Json.fail "Not the enter key"
2020 isKey =
2021 Json.field "key" Json.string
2022 |> Json.andThen decode
2023 in
2024 Internal.Attr <|
2025 Html.Events.preventDefaultOn "keyup"
2026 (Json.map (\fired -> ( fired, True )) isKey)
2030-- preventKeydown : String -> a -> Attribute a
2031-- preventKeydown desiredCode msg =
2032-- let
2033-- decode code =
2034-- if code == desiredCode then
2035-- Json.succeed msg
2036-- else
2037-- Json.fail "Not the enter key"
2038-- isKey =
2039-- Json.field "key" Json.string
2040-- |> Json.andThen decode
2041-- in
2042-- Events.onWithOptions "keydown"
2043-- { stopPropagation = False
2044-- , preventDefault = True
2045-- }
2046-- isKey
2049{-| -}
2050onKeyLookup : (String -> Maybe msg) -> Attribute msg
2051onKeyLookup lookup =
2052 let
2053 decode code =
2054 case lookup code of
2055 Nothing ->
2056 Json.fail "No key matched"
2058 Just msg ->
2059 Json.succeed msg
2061 isKey =
2062 Json.field "key" Json.string
2063 |> Json.andThen decode
2064 in
2065 Internal.Attr <| Html.Events.on "keyup" isKey
2068{-| -}
2069onFocusOut : msg -> Attribute msg
2070onFocusOut msg =
2071 Internal.Attr <| Html.Events.on "focusout" (Json.succeed msg)
2074{-| -}
2075onFocusIn : msg -> Attribute msg
2076onFocusIn msg =
2077 Internal.Attr <| Html.Events.on "focusin" (Json.succeed msg)
2080selected : Bool -> Attribute msg
2081selected =
2082 Internal.Attr << Html.Attributes.selected
2085name : String -> Attribute msg
2086name =
2087 Internal.Attr << Html.Attributes.name
2090value : String -> Attribute msg
2091value =
2092 Internal.Attr << Html.Attributes.value
2095tabindex : Int -> Attribute msg
2096tabindex =
2097 Internal.Attr << Html.Attributes.tabindex
2100disabled : Bool -> Attribute msg
2101disabled =
2102 Internal.Attr << Html.Attributes.disabled
2105spellcheck : Bool -> Attribute msg
2106spellcheck =
2107 Internal.Attr << Html.Attributes.spellcheck
2110readonly : Bool -> Attribute msg
2111readonly =
2112 Internal.Attr << Html.Attributes.readonly
2115autofill : String -> Attribute msg
2116autofill =
2117 Internal.Attr << Html.Attributes.attribute "autocomplete"
2120{-| Attach this attribute to any `Input` that you would like to be automatically focused when the page loads.
2122You should only have a maximum of one per page.
2125focusedOnLoad : Attribute msg
2126focusedOnLoad =
2127 Internal.Attr <| Html.Attributes.autofocus True
2131{- Style Defaults -}
2134defaultTextBoxStyle : List (Attribute msg)
2135defaultTextBoxStyle =
2136 [ defaultTextPadding
2137 , Border.rounded 3
2138 , Border.color darkGrey
2139 , Background.color white
2140 , Border.width 1
2141 , Element.spacing 5
2142 , Element.width Element.fill
2143 , Element.height Element.shrink
2147defaultTextPadding : Attribute msg
2148defaultTextPadding =
2149 Element.paddingXY 12 12
2152{-| The blue default checked box icon.
2154You'll likely want to make your own checkbox at some point that fits your design.
2157defaultCheckbox : Bool -> Element msg
2158defaultCheckbox checked =
2159 Element.el
2160 [ Internal.htmlClass "focusable"
2161 , Element.width (Element.px 14)
2162 , Element.height (Element.px 14)
2163 , Font.color white
2164 , Element.centerY
2165 , Font.size 9
2166 , Font.center
2167 , Border.rounded 3
2168 , Border.color <|
2169 if checked then
2170 Element.rgb (59 / 255) (153 / 255) (252 / 255)
2172 else
2173 Element.rgb (211 / 255) (211 / 255) (211 / 255)
2174 , Border.shadow
2175 { offset = ( 0, 0 )
2176 , blur = 1
2177 , size = 1
2178 , color =
2179 if checked then
2180 Element.rgba (238 / 255) (238 / 255) (238 / 255) 0
2182 else
2183 Element.rgb (238 / 255) (238 / 255) (238 / 255)
2185 , Background.color <|
2186 if checked then
2187 Element.rgb (59 / 255) (153 / 255) (252 / 255)
2189 else
2190 white
2191 , Border.width <|
2192 if checked then
2195 else
2198 (if checked then
2199 Element.el
2200 [ Border.color white
2201 , Element.height (Element.px 6)
2202 , Element.width (Element.px 9)
2203 , Element.rotate (degrees -45)
2204 , Element.centerX
2205 , Element.centerY
2206 , Element.moveUp 1
2207 , Border.widthEach
2208 { top = 0
2209 , left = 2
2210 , bottom = 2
2211 , right = 0
2214 Element.none
2216 else
2217 Element.none