git.iko.soy/typeable/elm-ui
on f4cd4da help sign in
2227 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 , if List.any hasFocusStyle withDefaults then
961 Internal.NoAttribute
963 else
964 Internal.htmlClass classes.focusedWithin
965 , Internal.htmlClass classes.inputMultilineWrapper
966 ]
967 ++ redistributed.parent
968 )
969 (Internal.Unkeyed
970 [ Internal.element
971 Internal.asParagraph
972 Internal.div
973 (Element.width Element.fill
974 :: Element.height Element.fill
975 :: Element.inFront inputElement
976 :: Internal.htmlClass classes.inputMultilineParent
977 :: redistributed.wrapper
978 )
979 (Internal.Unkeyed
980 (if textOptions.text == "" then
981 case textOptions.placeholder of
982 Nothing ->
983 -- Without this, firefox will make the text area lose focus
984 -- if the input is empty and you mash the keyboard
985 [ Element.text "\u{00A0}"
986 ]
988 Just place ->
989 [ renderPlaceholder place
990 []
991 (textOptions.text == "")
992 ]
994 else
995 [ Internal.unstyled
996 (Html.span [ Html.Attributes.class classes.inputMultilineFiller ]
997 -- We append a non-breaking space to the end of the content so that newlines don't get chomped.
998 [ Html.text (textOptions.text ++ "\u{00A0}")
999 ]
1007 TextInputNode inputType ->
1008 Internal.element
1009 Internal.asEl
1010 Internal.div
1011 (Element.width Element.fill
1012 :: (if List.any hasFocusStyle withDefaults then
1013 Internal.NoAttribute
1015 else
1016 Internal.htmlClass classes.focusedWithin
1018 :: List.concat
1019 [ redistributed.parent
1020 , case textOptions.placeholder of
1021 Nothing ->
1022 []
1024 Just place ->
1025 [ Element.behindContent
1026 (renderPlaceholder place redistributed.cover (textOptions.text == ""))
1030 (Internal.Unkeyed [ inputElement ])
1031 in
1032 applyLabel
1033 (Internal.Class Flag.cursor classes.cursorText
1034 :: (if isHiddenLabel textOptions.label then
1035 Internal.NoAttribute
1037 else
1038 Element.spacing
1041 :: Region.announce
1042 :: redistributed.fullParent
1044 textOptions.label
1045 wrappedInput
1048getHeight attr =
1049 case attr of
1050 Internal.Height h ->
1051 Just h
1053 _ ->
1054 Nothing
1057negateBox box =
1058 { top = negate box.top
1059 , right = negate box.right
1060 , bottom = negate box.bottom
1061 , left = negate box.left
1065renderBox { top, right, bottom, left } =
1066 String.fromInt top
1067 ++ "px "
1068 ++ String.fromInt right
1069 ++ "px "
1070 ++ String.fromInt bottom
1071 ++ "px "
1072 ++ String.fromInt left
1073 ++ "px"
1076renderPlaceholder (Placeholder placeholderAttrs placeholderEl) forPlaceholder on =
1077 Element.el
1078 (forPlaceholder
1079 ++ [ Font.color charcoal
1080 , Internal.htmlClass (classes.noTextSelection ++ " " ++ classes.passPointerEvents)
1081 , Element.clip
1082 , Border.color (Element.rgba 0 0 0 0)
1083 , Background.color (Element.rgba 0 0 0 0)
1084 , Element.height Element.fill
1085 , Element.width Element.fill
1086 , Element.alpha
1087 (if on then
1090 else
1094 ++ placeholderAttrs
1096 placeholderEl
1099{-| Because textareas are now shadowed, where they're rendered twice,
1100we to move the literal text area up because spacing is based on line height.
1102calcMoveToCompensateForPadding : List (Attribute msg) -> Attribute msg
1103calcMoveToCompensateForPadding attrs =
1104 let
1105 gatherSpacing attr found =
1106 case attr of
1107 Internal.StyleClass _ (Internal.SpacingStyle _ x y) ->
1108 case found of
1109 Nothing ->
1110 Just y
1112 _ ->
1113 found
1115 _ ->
1116 found
1117 in
1118 case List.foldr gatherSpacing Nothing attrs of
1119 Nothing ->
1120 Internal.NoAttribute
1122 Just vSpace ->
1123 Element.moveUp (toFloat (floor (toFloat vSpace / 2)))
1126{-| Given the list of attributes provided to `Input.multiline` or `Input.text`,
1128redistribute them to the parent, the input, or the cover.
1130 - fullParent -> Wrapper around label and input
1131 - parent -> parent of wrapper
1132 - wrapper -> the element that is here to take up space.
1133 - cover -> things like placeholders or text areas which are layered on top of input.
1134 - input -> actual input element
1137redistribute :
1138 Bool
1139 -> Bool
1140 -> List (Attribute msg)
1141 ->
1142 { fullParent : List (Attribute msg)
1143 , parent : List (Attribute msg)
1144 , wrapper : List (Attribute msg)
1145 , input : List (Attribute msg)
1146 , cover : List (Attribute msg)
1148redistribute isMultiline stacked attrs =
1149 List.foldl (redistributeOver isMultiline stacked)
1150 { fullParent = []
1151 , parent = []
1152 , input = []
1153 , cover = []
1154 , wrapper = []
1156 attrs
1157 |> (\redist ->
1158 { parent = List.reverse redist.parent
1159 , fullParent = List.reverse redist.fullParent
1160 , wrapper = List.reverse redist.wrapper
1161 , input = List.reverse redist.input
1162 , cover = List.reverse redist.cover
1167isFill len =
1168 case len of
1169 Internal.Fill _ ->
1170 True
1172 Internal.Content ->
1173 False
1175 Internal.Px _ ->
1176 False
1178 Internal.Min _ l ->
1179 isFill l
1181 Internal.Max _ l ->
1182 isFill l
1185isShrink len =
1186 case len of
1187 Internal.Content ->
1188 True
1190 Internal.Px _ ->
1191 False
1193 Internal.Fill _ ->
1194 False
1196 Internal.Min _ l ->
1197 isShrink l
1199 Internal.Max _ l ->
1200 isShrink l
1203isConstrained len =
1204 case len of
1205 Internal.Content ->
1206 False
1208 Internal.Px _ ->
1209 True
1211 Internal.Fill _ ->
1212 True
1214 Internal.Min _ l ->
1215 isConstrained l
1217 Internal.Max _ l ->
1218 True
1221isPixel len =
1222 case len of
1223 Internal.Content ->
1224 False
1226 Internal.Px _ ->
1227 True
1229 Internal.Fill _ ->
1230 False
1232 Internal.Min _ l ->
1233 isPixel l
1235 Internal.Max _ l ->
1236 isPixel l
1239{-| isStacked means that the label is above or below
1241redistributeOver isMultiline stacked attr els =
1242 case attr of
1243 Internal.Nearby _ _ ->
1244 { els | parent = attr :: els.parent }
1246 Internal.Width width ->
1247 if isFill width then
1248 { els
1249 | fullParent = attr :: els.fullParent
1250 , parent = attr :: els.parent
1251 , input = attr :: els.input
1254 else if stacked then
1255 { els
1256 | fullParent = attr :: els.fullParent
1259 else
1260 { els
1261 | parent = attr :: els.parent
1264 Internal.Height height ->
1265 if not stacked then
1266 { els
1267 | fullParent = attr :: els.fullParent
1268 , parent = attr :: els.parent
1271 else if isFill height then
1272 { els
1273 | fullParent = attr :: els.fullParent
1274 , parent = attr :: els.parent
1277 else if isPixel height then
1278 { els | parent = attr :: els.parent }
1280 else
1281 { els
1282 | parent = attr :: els.parent
1285 Internal.AlignX _ ->
1286 { els | fullParent = attr :: els.fullParent }
1288 Internal.AlignY _ ->
1289 { els | fullParent = attr :: els.fullParent }
1291 Internal.StyleClass _ (Internal.SpacingStyle _ _ _) ->
1292 { els
1293 | fullParent = attr :: els.fullParent
1294 , parent = attr :: els.parent
1295 , input = attr :: els.input
1296 , wrapper = attr :: els.wrapper
1299 Internal.StyleClass cls (Internal.PaddingStyle pad t r b l) ->
1300 if isMultiline then
1301 { els
1302 | parent = attr :: els.parent
1303 , cover = attr :: els.cover
1306 else
1307 let
1308 newHeight =
1309 Element.htmlAttribute
1310 (Html.Attributes.style
1311 "height"
1312 ("calc(1.0em + " ++ String.fromInt (2 * min t b) ++ "px)")
1315 newLineHeight =
1316 Element.htmlAttribute
1317 (Html.Attributes.style
1318 "line-height"
1319 ("calc(1.0em + " ++ String.fromInt (2 * min t b) ++ "px)")
1322 reducedVerticalPadding =
1323 Element.paddingEach
1324 { top = t - min t b
1325 , right = r
1326 , bottom = b - min t b
1327 , left = l
1329 in
1330 { els
1331 | parent = reducedVerticalPadding :: els.parent
1332 , input = newHeight :: newLineHeight :: els.input
1333 , cover = attr :: els.cover
1336 Internal.StyleClass _ (Internal.BorderWidth _ _ _ _ _) ->
1337 { els
1338 | parent = attr :: els.parent
1339 , cover = attr :: els.cover
1342 Internal.StyleClass _ (Internal.Transform _) ->
1343 { els
1344 | parent = attr :: els.parent
1345 , cover = attr :: els.cover
1348 Internal.StyleClass _ (Internal.FontSize _) ->
1349 { els | fullParent = attr :: els.fullParent }
1351 Internal.StyleClass _ (Internal.FontFamily _ _) ->
1352 { els | fullParent = attr :: els.fullParent }
1354 Internal.StyleClass flag cls ->
1355 { els | parent = attr :: els.parent }
1357 Internal.NoAttribute ->
1358 els
1360 Internal.Attr a ->
1361 { els | input = attr :: els.input }
1363 Internal.Describe _ ->
1364 { els | input = attr :: els.input }
1366 Internal.Class _ _ ->
1367 { els | parent = attr :: els.parent }
1369 Internal.TransformComponent _ _ ->
1370 { els | input = attr :: els.input }
1373{-| -}
1374text :
1375 List (Attribute msg)
1376 ->
1377 { onChange : String -> msg
1378 , text : String
1379 , placeholder : Maybe (Placeholder msg)
1380 , label : Label msg
1382 -> Element msg
1383text =
1384 textHelper
1385 { type_ = TextInputNode "text"
1386 , spellchecked = False
1387 , autofill = Nothing
1391{-| If spell checking is available, this input will be spellchecked.
1393spellChecked :
1394 List (Attribute msg)
1395 ->
1396 { onChange : String -> msg
1397 , text : String
1398 , placeholder : Maybe (Placeholder msg)
1399 , label : Label msg
1401 -> Element msg
1402spellChecked =
1403 textHelper
1404 { type_ = TextInputNode "text"
1405 , spellchecked = True
1406 , autofill = Nothing
1410{-| -}
1411search :
1412 List (Attribute msg)
1413 ->
1414 { onChange : String -> msg
1415 , text : String
1416 , placeholder : Maybe (Placeholder msg)
1417 , label : Label msg
1419 -> Element msg
1420search =
1421 textHelper
1422 { type_ = TextInputNode "search"
1423 , spellchecked = False
1424 , autofill = Nothing
1428{-| A password input that allows the browser to autofill.
1430It's `newPassword` instead of just `password` because it gives the browser a hint on what type of password input it is.
1432A password takes all the arguments a normal `Input.text` would, and also **show**, which will remove the password mask (e.g. `****` vs `pass1234`)
1435newPassword :
1436 List (Attribute msg)
1437 ->
1438 { onChange : String -> msg
1439 , text : String
1440 , placeholder : Maybe (Placeholder msg)
1441 , label : Label msg
1442 , show : Bool
1444 -> Element msg
1445newPassword attrs pass =
1446 textHelper
1447 { type_ =
1448 TextInputNode <|
1449 if pass.show then
1450 "text"
1452 else
1453 "password"
1454 , spellchecked = False
1455 , autofill = Just "new-password"
1457 attrs
1458 { onChange = pass.onChange
1459 , text = pass.text
1460 , placeholder = pass.placeholder
1461 , label = pass.label
1465{-| -}
1466currentPassword :
1467 List (Attribute msg)
1468 ->
1469 { onChange : String -> msg
1470 , text : String
1471 , placeholder : Maybe (Placeholder msg)
1472 , label : Label msg
1473 , show : Bool
1475 -> Element msg
1476currentPassword attrs pass =
1477 textHelper
1478 { type_ =
1479 TextInputNode <|
1480 if pass.show then
1481 "text"
1483 else
1484 "password"
1485 , spellchecked = False
1486 , autofill = Just "current-password"
1488 attrs
1489 { onChange = pass.onChange
1490 , text = pass.text
1491 , placeholder = pass.placeholder
1492 , label = pass.label
1496{-| -}
1497username :
1498 List (Attribute msg)
1499 ->
1500 { onChange : String -> msg
1501 , text : String
1502 , placeholder : Maybe (Placeholder msg)
1503 , label : Label msg
1505 -> Element msg
1506username =
1507 textHelper
1508 { type_ = TextInputNode "text"
1509 , spellchecked = False
1510 , autofill = Just "username"
1514{-| -}
1515email :
1516 List (Attribute msg)
1517 ->
1518 { onChange : String -> msg
1519 , text : String
1520 , placeholder : Maybe (Placeholder msg)
1521 , label : Label msg
1523 -> Element msg
1524email =
1525 textHelper
1526 { type_ = TextInputNode "email"
1527 , spellchecked = False
1528 , autofill = Just "email"
1532{-| A multiline text input.
1534By default it will have a minimum height of one line and resize based on it's contents.
1537multiline :
1538 List (Attribute msg)
1539 ->
1540 { onChange : String -> msg
1541 , text : String
1542 , placeholder : Maybe (Placeholder msg)
1543 , label : Label msg
1544 , spellcheck : Bool
1546 -> Element msg
1547multiline attrs multi =
1548 textHelper
1549 { type_ =
1550 TextArea
1551 , spellchecked = multi.spellcheck
1552 , autofill = Nothing
1554 attrs
1555 { onChange = multi.onChange
1556 , text = multi.text
1557 , placeholder = multi.placeholder
1558 , label = multi.label
1562isHiddenLabel label =
1563 case label of
1564 HiddenLabel _ ->
1565 True
1567 _ ->
1568 False
1571applyLabel : List (Attribute msg) -> Label msg -> Element msg -> Element msg
1572applyLabel attrs label input =
1573 case label of
1574 HiddenLabel labelText ->
1575 -- NOTE: This means that the label is applied outside of this function!
1576 -- It would be nice to unify this logic, but it's a little tricky
1577 Internal.element
1578 Internal.asColumn
1579 (Internal.NodeName "label")
1580 attrs
1581 (Internal.Unkeyed [ input ])
1583 Label position labelAttrs labelChild ->
1584 let
1585 labelElement =
1586 Internal.element
1587 Internal.asEl
1588 Internal.div
1589 labelAttrs
1590 (Internal.Unkeyed [ labelChild ])
1591 in
1592 case position of
1593 Above ->
1594 Internal.element
1595 Internal.asColumn
1596 (Internal.NodeName "label")
1597 attrs
1598 (Internal.Unkeyed [ labelElement, input ])
1600 Below ->
1601 Internal.element
1602 Internal.asColumn
1603 (Internal.NodeName "label")
1604 attrs
1605 (Internal.Unkeyed [ input, labelElement ])
1607 OnRight ->
1608 Internal.element
1609 Internal.asRow
1610 (Internal.NodeName "label")
1611 attrs
1612 (Internal.Unkeyed [ input, labelElement ])
1614 OnLeft ->
1615 Internal.element
1616 Internal.asRow
1617 (Internal.NodeName "label")
1618 attrs
1619 (Internal.Unkeyed [ labelElement, input ])
1622{-| -}
1623type Option value msg
1624 = Option value (OptionState -> Element msg)
1627{-| -}
1628type OptionState
1629 = Idle
1630 | Focused
1631 | Selected
1634{-| Add a choice to your radio element. This will be rendered with the default radio icon.
1636option : value -> Element msg -> Option value msg
1637option val txt =
1638 Option val (defaultRadioOption txt)
1641{-| Customize exactly what your radio option should look like in different states.
1643optionWith : value -> (OptionState -> Element msg) -> Option value msg
1644optionWith val view =
1645 Option val view
1648{-| -}
1649radio :
1650 List (Attribute msg)
1651 ->
1652 { onChange : option -> msg
1653 , options : List (Option option msg)
1654 , selected : Maybe option
1655 , label : Label msg
1657 -> Element msg
1658radio =
1659 radioHelper Column
1662{-| Same as radio, but displayed as a row
1664radioRow :
1665 List (Attribute msg)
1666 ->
1667 { onChange : option -> msg
1668 , options : List (Option option msg)
1669 , selected : Maybe option
1670 , label : Label msg
1672 -> Element msg
1673radioRow =
1674 radioHelper Row
1677defaultRadioOption : Element msg -> OptionState -> Element msg
1678defaultRadioOption optionLabel status =
1679 Element.row
1680 [ Element.spacing 10
1681 , Element.alignLeft
1682 , Element.width Element.shrink
1684 [ Element.el
1685 [ Element.width (Element.px 14)
1686 , Element.height (Element.px 14)
1687 , Background.color white
1688 , Border.rounded 7
1689 , case status of
1690 Selected ->
1691 Internal.htmlClass "focusable"
1693 _ ->
1694 Internal.NoAttribute
1696 -- , Border.shadow <|
1697 -- -- case status of
1698 -- -- Idle ->
1699 -- -- { offset = ( 0, 0 )
1700 -- -- , blur =
1701 -- -- 1
1702 -- -- , color = Color.rgb 235 235 235
1703 -- -- }
1704 -- -- Focused ->
1705 -- -- { offset = ( 0, 0 )
1706 -- -- , blur =
1707 -- -- 0
1708 -- -- , color = Color.rgba 235 235 235 0
1709 -- -- }
1710 -- -- Selected ->
1711 -- { offset = ( 0, 0 )
1712 -- , blur =
1713 -- 1
1714 -- , color = Color.rgba 235 235 235 0
1715 -- }
1716 , Border.width <|
1717 case status of
1718 Idle ->
1721 Focused ->
1724 Selected ->
1726 , Border.color <|
1727 case status of
1728 Idle ->
1729 Element.rgb (208 / 255) (208 / 255) (208 / 255)
1731 Focused ->
1732 Element.rgb (208 / 255) (208 / 255) (208 / 255)
1734 Selected ->
1735 Element.rgb (59 / 255) (153 / 255) (252 / 255)
1737 Element.none
1738 , Element.el [ Element.width Element.fill, Internal.htmlClass "unfocusable" ] optionLabel
1742radioHelper :
1743 Orientation
1744 -> List (Attribute msg)
1745 ->
1746 { onChange : option -> msg
1747 , options : List (Option option msg)
1748 , selected : Maybe option
1749 , label : Label msg
1751 -> Element msg
1752radioHelper orientation attrs input =
1753 let
1754 renderOption (Option val view) =
1755 let
1756 status =
1757 if Just val == input.selected then
1758 Selected
1760 else
1761 Idle
1762 in
1763 Element.el
1764 [ Element.pointer
1765 , case orientation of
1766 Row ->
1767 Element.width Element.shrink
1769 Column ->
1770 Element.width Element.fill
1771 , Events.onClick (input.onChange val)
1772 , case status of
1773 Selected ->
1774 Internal.Attr <|
1775 Html.Attributes.attribute "aria-checked"
1776 "true"
1778 _ ->
1779 Internal.Attr <|
1780 Html.Attributes.attribute "aria-checked"
1781 "false"
1782 , Internal.Attr <|
1783 Html.Attributes.attribute "role" "radio"
1785 (view status)
1787 optionArea =
1788 case orientation of
1789 Row ->
1790 row (hiddenLabelAttribute input.label :: attrs)
1791 (List.map renderOption input.options)
1793 Column ->
1794 column (hiddenLabelAttribute input.label :: attrs)
1795 (List.map renderOption input.options)
1797 prevNext =
1798 case input.options of
1799 [] ->
1800 Nothing
1802 (Option val _) :: _ ->
1803 List.foldl track ( NotFound, val, val ) input.options
1804 |> (\( found, b, a ) ->
1805 case found of
1806 NotFound ->
1807 Just ( b, val )
1809 BeforeFound ->
1810 Just ( b, val )
1812 _ ->
1813 Just ( b, a )
1816 track opt ( found, prev, nxt ) =
1817 case opt of
1818 Option val _ ->
1819 case found of
1820 NotFound ->
1821 if Just val == input.selected then
1822 ( BeforeFound, prev, nxt )
1824 else
1825 ( found, val, nxt )
1827 BeforeFound ->
1828 ( AfterFound, prev, val )
1830 AfterFound ->
1831 ( found, prev, nxt )
1833 events =
1834 Internal.get
1835 attrs
1836 <|
1837 \attr ->
1838 case attr of
1839 Internal.Width (Internal.Fill _) ->
1840 True
1842 Internal.Height (Internal.Fill _) ->
1843 True
1845 Internal.Attr _ ->
1846 True
1848 _ ->
1849 False
1850 in
1851 applyLabel
1852 (List.filterMap identity
1853 [ Just Element.alignLeft
1854 , Just (tabindex 0)
1855 , Just (Internal.htmlClass "focus")
1856 , Just Region.announce
1857 , Just <|
1858 Internal.Attr <|
1859 Html.Attributes.attribute "role" "radiogroup"
1860 , case prevNext of
1861 Nothing ->
1862 Nothing
1864 Just ( prev, next ) ->
1865 Just
1866 (onKeyLookup <|
1867 \code ->
1868 if code == leftArrow then
1869 Just (input.onChange prev)
1871 else if code == upArrow then
1872 Just (input.onChange prev)
1874 else if code == rightArrow then
1875 Just (input.onChange next)
1877 else if code == downArrow then
1878 Just (input.onChange next)
1880 else if code == space then
1881 case input.selected of
1882 Nothing ->
1883 Just (input.onChange prev)
1885 _ ->
1886 Nothing
1888 else
1889 Nothing
1892 ++ events
1893 -- ++ hideIfEverythingisInvisible
1895 input.label
1896 optionArea
1899type Found
1900 = NotFound
1901 | BeforeFound
1902 | AfterFound
1905type Orientation
1906 = Row
1907 | Column
1910column : List (Attribute msg) -> List (Internal.Element msg) -> Internal.Element msg
1911column attributes children =
1912 Internal.element
1913 Internal.asColumn
1914 Internal.div
1915 (Element.height Element.shrink
1916 :: Element.width Element.fill
1917 :: attributes
1919 (Internal.Unkeyed children)
1922row : List (Attribute msg) -> List (Internal.Element msg) -> Internal.Element msg
1923row attributes children =
1924 Internal.element
1925 Internal.asRow
1926 Internal.div
1927 (Element.width Element.fill
1928 :: attributes
1930 (Internal.Unkeyed children)
1934{- Event Handlers -}
1937{-| -}
1938onEnter : msg -> Attribute msg
1939onEnter msg =
1940 onKey enter msg
1943{-| -}
1944onSpace : msg -> Attribute msg
1945onSpace msg =
1946 onKey space msg
1949{-| -}
1950onUpArrow : msg -> Attribute msg
1951onUpArrow msg =
1952 onKey upArrow msg
1955{-| -}
1956onRightArrow : msg -> Attribute msg
1957onRightArrow msg =
1958 onKey rightArrow msg
1961{-| -}
1962onLeftArrow : msg -> Attribute msg
1963onLeftArrow msg =
1964 onKey leftArrow msg
1967{-| -}
1968onDownArrow : msg -> Attribute msg
1969onDownArrow msg =
1970 onKey downArrow msg
1973enter : String
1974enter =
1975 "Enter"
1978tab : String
1979tab =
1980 "Tab"
1983delete : String
1984delete =
1985 "Delete"
1988backspace : String
1989backspace =
1990 "Backspace"
1993upArrow : String
1994upArrow =
1995 "ArrowUp"
1998leftArrow : String
1999leftArrow =
2000 "ArrowLeft"
2003rightArrow : String
2004rightArrow =
2005 "ArrowRight"
2008downArrow : String
2009downArrow =
2010 "ArrowDown"
2013space : String
2014space =
2015 " "
2018{-| -}
2019onKey : String -> msg -> Attribute msg
2020onKey desiredCode msg =
2021 let
2022 decode code =
2023 if code == desiredCode then
2024 Json.succeed msg
2026 else
2027 Json.fail "Not the enter key"
2029 isKey =
2030 Json.field "key" Json.string
2031 |> Json.andThen decode
2032 in
2033 Internal.Attr <|
2034 Html.Events.preventDefaultOn "keyup"
2035 (Json.map (\fired -> ( fired, True )) isKey)
2039-- preventKeydown : String -> a -> Attribute a
2040-- preventKeydown desiredCode msg =
2041-- let
2042-- decode code =
2043-- if code == desiredCode then
2044-- Json.succeed msg
2045-- else
2046-- Json.fail "Not the enter key"
2047-- isKey =
2048-- Json.field "key" Json.string
2049-- |> Json.andThen decode
2050-- in
2051-- Events.onWithOptions "keydown"
2052-- { stopPropagation = False
2053-- , preventDefault = True
2054-- }
2055-- isKey
2058{-| -}
2059onKeyLookup : (String -> Maybe msg) -> Attribute msg
2060onKeyLookup lookup =
2061 let
2062 decode code =
2063 case lookup code of
2064 Nothing ->
2065 Json.fail "No key matched"
2067 Just msg ->
2068 Json.succeed msg
2070 isKey =
2071 Json.field "key" Json.string
2072 |> Json.andThen decode
2073 in
2074 Internal.Attr <| Html.Events.on "keyup" isKey
2077{-| -}
2078onFocusOut : msg -> Attribute msg
2079onFocusOut msg =
2080 Internal.Attr <| Html.Events.on "focusout" (Json.succeed msg)
2083{-| -}
2084onFocusIn : msg -> Attribute msg
2085onFocusIn msg =
2086 Internal.Attr <| Html.Events.on "focusin" (Json.succeed msg)
2089selected : Bool -> Attribute msg
2090selected =
2091 Internal.Attr << Html.Attributes.selected
2094name : String -> Attribute msg
2095name =
2096 Internal.Attr << Html.Attributes.name
2099value : String -> Attribute msg
2100value =
2101 Internal.Attr << Html.Attributes.value
2104tabindex : Int -> Attribute msg
2105tabindex =
2106 Internal.Attr << Html.Attributes.tabindex
2109disabled : Bool -> Attribute msg
2110disabled =
2111 Internal.Attr << Html.Attributes.disabled
2114spellcheck : Bool -> Attribute msg
2115spellcheck =
2116 Internal.Attr << Html.Attributes.spellcheck
2119readonly : Bool -> Attribute msg
2120readonly =
2121 Internal.Attr << Html.Attributes.readonly
2124autofill : String -> Attribute msg
2125autofill =
2126 Internal.Attr << Html.Attributes.attribute "autocomplete"
2129{-| Attach this attribute to any `Input` that you would like to be automatically focused when the page loads.
2131You should only have a maximum of one per page.
2134focusedOnLoad : Attribute msg
2135focusedOnLoad =
2136 Internal.Attr <| Html.Attributes.autofocus True
2140{- Style Defaults -}
2143defaultTextBoxStyle : List (Attribute msg)
2144defaultTextBoxStyle =
2145 [ defaultTextPadding
2146 , Border.rounded 3
2147 , Border.color darkGrey
2148 , Background.color white
2149 , Border.width 1
2150 , Element.spacing 5
2151 , Element.width Element.fill
2152 , Element.height Element.shrink
2156defaultTextPadding : Attribute msg
2157defaultTextPadding =
2158 Element.paddingXY 12 12
2161{-| The blue default checked box icon.
2163You'll likely want to make your own checkbox at some point that fits your design.
2166defaultCheckbox : Bool -> Element msg
2167defaultCheckbox checked =
2168 Element.el
2169 [ Internal.htmlClass "focusable"
2170 , Element.width (Element.px 14)
2171 , Element.height (Element.px 14)
2172 , Font.color white
2173 , Element.centerY
2174 , Font.size 9
2175 , Font.center
2176 , Border.rounded 3
2177 , Border.color <|
2178 if checked then
2179 Element.rgb (59 / 255) (153 / 255) (252 / 255)
2181 else
2182 Element.rgb (211 / 255) (211 / 255) (211 / 255)
2183 , Border.shadow
2184 { offset = ( 0, 0 )
2185 , blur = 1
2186 , size = 1
2187 , color =
2188 if checked then
2189 Element.rgba (238 / 255) (238 / 255) (238 / 255) 0
2191 else
2192 Element.rgb (238 / 255) (238 / 255) (238 / 255)
2194 , Background.color <|
2195 if checked then
2196 Element.rgb (59 / 255) (153 / 255) (252 / 255)
2198 else
2199 white
2200 , Border.width <|
2201 if checked then
2204 else
2207 (if checked then
2208 Element.el
2209 [ Border.color white
2210 , Element.height (Element.px 6)
2211 , Element.width (Element.px 9)
2212 , Element.rotate (degrees -45)
2213 , Element.centerX
2214 , Element.centerY
2215 , Element.moveUp 1
2216 , Border.widthEach
2217 { top = 0
2218 , left = 2
2219 , bottom = 2
2220 , right = 0
2223 Element.none
2225 else
2226 Element.none