git.iko.soy/typeable/elm-ui
on 7e08166 help sign in
1900 lines 53 kB history raw
1module Element.Input exposing
2 ( button
3 , checkbox, defaultCheckbox
4 , text, Placeholder, placeholder
5 , username, newPassword, currentPassword, email, search, spellChecked
6 , multiline
7 , slider, Thumb, thumb, defaultThumb
8 , radio, radioRow, Option, option, optionWith, OptionState(..)
9 , Label, labelAbove, labelBelow, labelLeft, labelRight, labelHidden
10 , focusedOnLoad
11 )
13{-|
15@docs button
17@docs checkbox, defaultCheckbox
20## Text Input
22@docs text, Placeholder, placeholder
24We can also give a hint about what type of content our text field contains. This will allow things like autofill to work correctly.
26@docs username, newPassword, currentPassword, email, search, spellChecked
29## Multiline Text
31@docs multiline
34## Slider
36@docs slider, Thumb, thumb, defaultThumb
39## Radio Buttons
41@docs radio, radioRow, Option, option, optionWith, OptionState
44## Labels
46@docs Label, labelAbove, labelBelow, labelLeft, labelRight, labelHidden
48@docs focusedOnLoad
50-}
52import Element exposing (Attribute, Color, Element)
53import Element.Background as Background
54import Element.Border as Border
55import Element.Events as Events
56import Element.Font as Font
57import Element.Region as Region
58import Html
59import Html.Attributes
60import Html.Events
61import Internal.Flag as Flag
62import Internal.Model as Internal
63import Internal.Style exposing (classes)
64import Json.Decode as Json
67{-| -}
68type Placeholder msg
69 = Placeholder (List (Attribute msg)) (Element msg)
72white =
73 Element.rgb 1 1 1
76darkGrey =
77 Element.rgb (186 / 255) (189 / 255) (182 / 255)
80charcoal =
81 Element.rgb
82 (136 / 255)
83 (138 / 255)
84 (133 / 255)
87{-| -}
88placeholder : List (Attribute msg) -> Element msg -> Placeholder msg
89placeholder =
90 Placeholder
93type LabelLocation
94 = OnRight
95 | OnLeft
96 | Above
97 | Below
100{-| Every input has a required `label`.
101-}
102type Label msg
103 = Label LabelLocation (List (Attribute msg)) (Element msg)
104 | HiddenLabel String
107{-| -}
108labelRight : List (Attribute msg) -> Element msg -> Label msg
109labelRight =
110 Label OnRight
113{-| -}
114labelLeft : List (Attribute msg) -> Element msg -> Label msg
115labelLeft =
116 Label OnLeft
119{-| -}
120labelAbove : List (Attribute msg) -> Element msg -> Label msg
121labelAbove =
122 Label Above
125{-| -}
126labelBelow : List (Attribute msg) -> Element msg -> Label msg
127labelBelow =
128 Label Below
131{-| Sometimes you may need to have a label which is not visible, but is still accessible to screen readers.
133Seriously consider a visible label before using this.
135The situations where a hidden label makes sense:
137 - A searchbar with a `search` button right next to it.
138 - A `table` of inputs where the header gives the label.
140Basically, a hidden label works when there are other contextual clues that sighted people can pick up on.
142-}
143labelHidden : String -> Label msg
144labelHidden =
145 HiddenLabel
148hiddenLabelAttribute label =
149 case label of
150 HiddenLabel textLabel ->
151 Internal.Describe (Internal.Label textLabel)
153 Label _ _ _ ->
154 Internal.NoAttribute
157{-| A standard button.
159The `onPress` handler will be fired either `onClick` or when the element is focused and the enter key has been pressed.
161 import Element.Input as Input
163 Input.button []
164 { onPress = ClickMsg
165 , label = text "My Button"
166 }
168`onPress` takes a `Maybe msg`. If you provide the value `Nothing`, then the button will be disabled.
170-}
171button :
172 List (Attribute msg)
173 ->
174 { onPress : Maybe msg
175 , label : Element msg
176 }
177 -> Element msg
178button attrs { onPress, label } =
179 Internal.element
180 Internal.asEl
181 -- We don't explicitly label this node as a button,
182 -- because buttons fire a bunch of times when you hold down the enter key.
183 -- We'd like to fire just once on the enter key, which means using keyup instead of keydown.
184 -- Because we have no way to disable keydown, though our messages get doubled.
185 Internal.div
186 (Element.width Element.shrink
187 :: Element.height Element.shrink
188 :: Internal.htmlClass
189 (classes.contentCenterX
190 ++ " "
191 ++ classes.contentCenterY
192 ++ " "
193 ++ classes.seButton
194 ++ " "
195 ++ classes.noTextSelection
196 )
197 :: Element.pointer
198 :: focusDefault attrs
199 :: Internal.Describe Internal.Button
200 :: Internal.Attr (Html.Attributes.tabindex 0)
201 :: (case onPress of
202 Nothing ->
203 Internal.Attr (Html.Attributes.disabled True) :: attrs
205 Just msg ->
206 Events.onClick msg
207 :: onEnter msg
208 :: attrs
209 )
210 )
211 (Internal.Unkeyed [ label ])
214focusDefault attrs =
215 if List.any hasFocusStyle attrs then
216 Internal.NoAttribute
218 else
219 Internal.htmlClass "focusable"
222hasFocusStyle attr =
223 case attr of
224 Internal.StyleClass _ (Internal.PseudoSelector Internal.Focus _) ->
225 True
227 _ ->
228 False
231{-| -}
232type alias Checkbox msg =
233 { onChange : Maybe (Bool -> msg)
234 , icon : Maybe (Element msg)
235 , checked : Bool
236 , label : Label msg
237 }
240{-| -}
241checkbox :
242 List (Attribute msg)
243 ->
244 { onChange : Bool -> msg
245 , icon : Bool -> Element msg
246 , checked : Bool
247 , label : Label msg
248 }
249 -> Element msg
250checkbox attrs { label, icon, checked, onChange } =
251 let
252 attributes =
253 [ if isHiddenLabel label then
254 Internal.NoAttribute
256 else
257 Element.spacing
258 6
259 , Internal.Attr (Html.Events.onClick (onChange (not checked)))
260 , Region.announce
261 , onKeyLookup <|
262 \code ->
263 if code == enter then
264 Just <| onChange (not checked)
266 else if code == space then
267 Just <| onChange (not checked)
269 else
270 Nothing
271 , tabindex 0
272 , Element.pointer
273 , Element.alignLeft
274 , Element.width Element.fill
275 ]
276 ++ attrs
277 in
278 applyLabel attributes
279 label
280 (Internal.element
281 Internal.asEl
282 Internal.div
283 [ Internal.Attr <|
284 Html.Attributes.attribute "role" "checkbox"
285 , Internal.Attr <|
286 Html.Attributes.attribute "aria-checked" <|
287 if checked then
288 "true"
290 else
291 "false"
292 , hiddenLabelAttribute label
293 , Element.centerY
294 , Element.height Element.fill
295 , Element.width Element.shrink
296 ]
297 (Internal.Unkeyed
298 [ icon checked
299 ]
300 )
301 )
304{-| -}
305type Thumb
306 = Thumb (List (Attribute Never))
309{-| -}
310thumb : List (Attribute Never) -> Thumb
311thumb =
312 Thumb
315{-| -}
316defaultThumb : Thumb
317defaultThumb =
318 Thumb
319 [ Element.width (Element.px 16)
320 , Element.height (Element.px 16)
321 , Border.rounded 8
322 , Border.width 1
323 , Border.color (Element.rgb 0.5 0.5 0.5)
324 , Background.color (Element.rgb 1 1 1)
325 ]
328{-| A slider input, good for capturing float values.
330 Input.slider
331 [ Element.height (Element.px 30)
333 -- Here is where we're creating/styling the "track"
334 , Element.behindContent
335 (Element.el
336 [ Element.width Element.fill
337 , Element.height (Element.px 2)
338 , Element.centerY
339 , Background.color grey
340 , Border.rounded 2
341 ]
342 Element.none
343 )
344 ]
345 { onChange = AdjustValue
346 , label = Input.labelAbove [] (text "My Slider Value")
347 , min = 0
348 , max = 75
349 , step = Nothing
350 , value = model.sliderValue
351 , thumb =
352 Input.defaultThumb
353 }
355The `thumb` is the icon that you can move around.
357The slider can be vertical or horizontal depending on the width/height of the slider.
359 - `height fill` and `width (px someWidth)` will cause the slider to be vertical.
360 - `height (px someHeight)` and `width (px someWidth)` where `someHeight` > `someWidth` will also do it.
361 - otherwise, the slider will be horizontal.
363**Note:** If you want a slider for an `Int` value:
365 - set `step` to be `Just 1`, or some other whole value
366 - `value = toFloat model.myInt`
367 - And finally, round the value before making a message `onChange = round >> AdjustValue`
369-}
370slider :
371 List (Attribute msg)
372 ->
373 { onChange : Float -> msg
374 , label : Label msg
375 , min : Float
376 , max : Float
377 , value : Float
378 , thumb : Thumb
379 , step : Maybe Float
380 }
381 -> Element msg
382slider attributes input =
383 let
384 (Thumb thumbAttributes) =
385 input.thumb
387 width =
388 Internal.getWidth thumbAttributes
390 height =
391 Internal.getHeight thumbAttributes
393 vertical =
394 case ( trackWidth, trackHeight ) of
395 ( Nothing, Nothing ) ->
396 False
398 ( Just (Internal.Px w), Just (Internal.Px h) ) ->
399 h > w
401 ( Just (Internal.Px _), Just (Internal.Fill _) ) ->
402 True
404 _ ->
405 False
407 trackHeight =
408 Internal.getHeight attributes
410 trackWidth =
411 Internal.getWidth attributes
413 ( spacingX, spacingY ) =
414 Internal.getSpacing attributes ( 5, 5 )
416 factor =
417 (input.value - input.min)
418 / (input.max - input.min)
420 {- Needed attributes
422 Thumb Attributes
423 - Width/Height of thumb so that the input can shadow it.
426 Attributes
428 OnParent ->
429 Spacing
432 On track ->
433 Everything else
438 The `<input>`
441 -}
442 className =
443 "thmb-" ++ thumbWidthString ++ "-" ++ thumbHeightString
445 thumbWidthString =
446 case width of
447 Nothing ->
448 "20px"
450 Just (Internal.Px px) ->
451 String.fromInt px ++ "px"
453 _ ->
454 "100%"
456 thumbHeightString =
457 case height of
458 Nothing ->
459 "20px"
461 Just (Internal.Px px) ->
462 String.fromInt px ++ "px"
464 _ ->
465 "100%"
467 thumbShadowStyle =
468 [ Internal.Property "width"
469 thumbWidthString
470 , Internal.Property "height"
471 thumbHeightString
472 ]
473 in
474 applyLabel
475 [ if isHiddenLabel input.label then
476 Internal.NoAttribute
478 else
479 Element.spacingXY spacingX spacingY
480 , Region.announce
481 , Element.width
482 (case trackWidth of
483 Nothing ->
484 Element.fill
486 Just (Internal.Px _) ->
487 Element.shrink
489 Just x ->
490 x
491 )
492 , Element.height
493 (case trackHeight of
494 Nothing ->
495 Element.shrink
497 Just (Internal.Px _) ->
498 Element.shrink
500 Just x ->
501 x
502 )
503 ]
504 input.label
505 (Element.row
506 [ Element.width
507 (Maybe.withDefault Element.fill trackWidth)
508 , Element.height
509 (Maybe.withDefault (Element.px 20) trackHeight)
510 ]
511 [ Internal.element
512 Internal.asEl
513 (Internal.NodeName "input")
514 [ hiddenLabelAttribute input.label
515 , Internal.StyleClass Flag.active
516 (Internal.Style
517 ("input[type=\"range\"]." ++ className ++ "::-moz-range-thumb")
518 thumbShadowStyle
519 )
520 , Internal.StyleClass Flag.hover
521 (Internal.Style
522 ("input[type=\"range\"]." ++ className ++ "::-webkit-slider-thumb")
523 thumbShadowStyle
524 )
525 , Internal.StyleClass Flag.focus
526 (Internal.Style
527 ("input[type=\"range\"]." ++ className ++ "::-ms-thumb")
528 thumbShadowStyle
529 )
530 , Internal.Attr (Html.Attributes.class (className ++ " focusable-parent"))
531 , Internal.Attr
532 (Html.Events.onInput
533 (\str ->
534 case String.toFloat str of
535 Nothing ->
536 -- This should never happen because the browser
537 -- should always provide a Float.
538 input.onChange 0
540 Just val ->
541 input.onChange val
542 )
543 )
544 , Internal.Attr <|
545 Html.Attributes.type_ "range"
546 , Internal.Attr <|
547 Html.Attributes.step
548 (case input.step of
549 Nothing ->
550 -- Note: If we set `any` here,
551 -- Firefox makes a single press of the arrows keys equal to 1
552 -- We could set the step manually to the effective range / 100
553 -- String.fromFloat ((input.max - input.min) / 100)
554 -- Which matches Chrome's default behavior
555 -- HOWEVER, that means manually moving a slider with the mouse will snap to that interval.
556 "any"
558 Just step ->
559 String.fromFloat step
560 )
561 , Internal.Attr <|
562 Html.Attributes.min (String.fromFloat input.min)
563 , Internal.Attr <|
564 Html.Attributes.max (String.fromFloat input.max)
565 , Internal.Attr <|
566 Html.Attributes.value (String.fromFloat input.value)
567 , if vertical then
568 Internal.Attr <|
569 Html.Attributes.attribute "orient" "vertical"
571 else
572 Internal.NoAttribute
573 , Element.width <|
574 if vertical then
575 Maybe.withDefault (Element.px 20) trackHeight
577 else
578 Maybe.withDefault Element.fill trackWidth
579 , Element.height <|
580 if vertical then
581 Maybe.withDefault Element.fill trackWidth
583 else
584 Maybe.withDefault (Element.px 20) trackHeight
585 ]
586 (Internal.Unkeyed [])
587 , Element.el
588 (Element.width
589 (Maybe.withDefault Element.fill trackWidth)
590 :: Element.height
591 (Maybe.withDefault (Element.px 20) trackHeight)
592 :: attributes
593 -- This is after `attributes` because the thumb should be in front of everything.
594 ++ [ Element.behindContent <|
595 if vertical then
596 viewVerticalThumb factor thumbAttributes trackWidth
598 else
599 viewHorizontalThumb factor thumbAttributes trackHeight
600 ]
601 )
602 Element.none
603 ]
604 )
607viewHorizontalThumb factor thumbAttributes trackHeight =
608 Element.row
609 [ Element.width Element.fill
610 , Element.height (Maybe.withDefault Element.fill trackHeight)
611 , Element.centerY
612 ]
613 [ Element.el
614 [ Element.width (Element.fillPortion (round <| factor * 10000))
615 ]
616 Element.none
617 , Element.el
618 (Element.centerY
619 :: List.map (Internal.mapAttr Basics.never) thumbAttributes
620 )
621 Element.none
622 , Element.el
623 [ Element.width (Element.fillPortion (round <| (abs <| 1 - factor) * 10000))
624 ]
625 Element.none
626 ]
629viewVerticalThumb factor thumbAttributes trackWidth =
630 Element.column
631 [ Element.height Element.fill
632 , Element.width (Maybe.withDefault Element.fill trackWidth)
633 , Element.centerX
634 ]
635 [ Element.el
636 [ Element.height (Element.fillPortion (round <| (abs <| 1 - factor) * 10000))
637 ]
638 Element.none
639 , Element.el
640 (Element.centerX
641 :: List.map (Internal.mapAttr Basics.never) thumbAttributes
642 )
643 Element.none
644 , Element.el
645 [ Element.height (Element.fillPortion (round <| factor * 10000))
646 ]
647 Element.none
648 ]
651type alias TextInput =
652 { type_ : TextKind
653 , spellchecked : Bool
654 , autofill : Maybe String
655 }
658type TextKind
659 = TextInputNode String
660 | TextArea
663{-| -}
664type alias Text msg =
665 { onChange : String -> msg
666 , text : String
667 , placeholder : Maybe (Placeholder msg)
668 , label : Label msg
669 }
672type Padding
673 = Padding Int Int Int Int
676inheritablePlaceholderAttributes attr =
677 case attr of
678 Internal.StyleClass _ (Internal.PaddingStyle _ _ _ _ _) ->
679 True
681 Internal.StyleClass _ (Internal.BorderWidth _ _ _ _ _) ->
682 True
684 Internal.StyleClass _ (Internal.Transform _) ->
685 True
687 _ ->
688 False
691{-|
693 attributes
695 <parent>
696 attribute::width/height fill
697 attribtue::alignment
698 attribute::spacing
699 attribute::fontsize/family/lineheight
700 <el-wrapper>
701 attribute::nearby(placeholder)
702 attribute::width/height fill
703 inFront ->
704 placeholder
705 attribute::padding
708 <input>
709 textarea ->
710 special height for height-content
711 attribtue::padding
712 attribute::lineHeight
713 attributes
714 <label>
716-}
717textHelper : TextInput -> List (Attribute msg) -> Text msg -> Element msg
718textHelper textInput attrs textOptions =
719 let
720 attributes =
721 defaultTextBoxStyle ++ attrs
723 behavior =
724 [ Internal.Attr (Html.Events.onInput textOptions.onChange) ]
726 forPlaceholder =
727 List.filter inheritablePlaceholderAttributes attributes
729 noNearbys =
730 List.filter (not << forNearby) attributes
732 forNearby attr =
733 case attr of
734 Internal.Nearby _ _ ->
735 True
737 _ ->
738 False
740 ( inputNode, inputAttrs, inputChildren ) =
741 case textInput.type_ of
742 TextInputNode inputType ->
743 ( "input"
744 , [ value textOptions.text
745 , Internal.Attr (Html.Attributes.type_ inputType)
746 , spellcheck textInput.spellchecked
747 , Internal.htmlClass classes.inputText
748 , case textInput.autofill of
749 Nothing ->
750 Internal.NoAttribute
752 Just fill ->
753 autofill fill
754 ]
755 ++ noNearbys
756 , []
757 )
759 TextArea ->
760 let
761 { maybePadding, heightContent, maybeSpacing, adjustedAttributes, maybeBorder } =
762 (value textOptions.text :: attributes)
763 |> List.foldr
764 (\attr found ->
765 case attr of
766 Internal.Describe _ ->
767 found
769 Internal.Height len ->
770 case found.heightContent of
771 Nothing ->
772 { found
773 | heightContent = Just (Internal.isContent len)
774 , adjustedAttributes = attr :: found.adjustedAttributes
775 }
777 _ ->
778 found
780 Internal.StyleClass _ (Internal.BorderWidth _ t r b l) ->
781 case found.maybeBorder of
782 Nothing ->
783 { found
784 | maybeBorder = Just (Padding t r b l)
785 , adjustedAttributes = attr :: found.adjustedAttributes
786 }
788 _ ->
789 found
791 Internal.StyleClass _ (Internal.PaddingStyle _ t r b l) ->
792 case found.maybePadding of
793 Nothing ->
794 { found
795 | maybePadding = Just (Padding t r b l)
797 -- Skip storing padding because it's being customized later
798 , adjustedAttributes = found.adjustedAttributes
799 }
801 _ ->
802 found
804 Internal.StyleClass _ (Internal.SpacingStyle _ x y) ->
805 case found.maybeSpacing of
806 Nothing ->
807 { found
808 | maybeSpacing = Just y
809 , adjustedAttributes = attr :: found.adjustedAttributes
810 }
812 _ ->
813 found
815 _ ->
816 { found | adjustedAttributes = attr :: found.adjustedAttributes }
817 )
818 { maybePadding = Nothing
819 , heightContent = Nothing
820 , maybeBorder = Nothing
821 , maybeSpacing = Nothing
822 , adjustedAttributes = []
823 }
825 -- NOTE: This is where default text spacing is set
826 spacing =
827 Maybe.withDefault 5 maybeSpacing
828 in
829 ( "textarea"
830 , [ spellcheck textInput.spellchecked
831 , Internal.htmlClass classes.inputMultiline
832 , Maybe.map autofill textInput.autofill
833 |> Maybe.withDefault Internal.NoAttribute
834 , case maybePadding of
835 Nothing ->
836 Internal.NoAttribute
838 Just (Padding t r b l) ->
839 Element.paddingEach
840 { top = max 0 (t - (spacing // 2))
841 , bottom = max 0 (b - (spacing // 2))
842 , left = l
843 , right = r
844 }
845 , case heightContent of
846 Nothing ->
847 Internal.NoAttribute
849 Just True ->
850 textHeightContent textOptions.text spacing maybePadding maybeBorder
852 Just False ->
853 Internal.NoAttribute
854 ]
855 ++ adjustedAttributes
856 , [ Internal.unstyled (Html.text textOptions.text) ]
857 )
859 heightFillFromChild =
860 Internal.get attributes <|
861 \attr ->
862 case attr of
863 Internal.Height (Internal.Fill _) ->
864 True
866 _ ->
867 False
869 attributesFromChild =
870 Internal.get attributes <|
871 \attr ->
872 case attr of
873 Internal.Width (Internal.Fill _) ->
874 True
876 Internal.Height (Internal.Fill _) ->
877 True
879 Internal.AlignX _ ->
880 True
882 Internal.AlignY _ ->
883 True
885 Internal.StyleClass _ (Internal.SpacingStyle _ _ _) ->
886 True
888 Internal.StyleClass _ (Internal.FontSize _) ->
889 True
891 Internal.StyleClass _ (Internal.FontFamily _ _) ->
892 True
894 _ ->
895 False
897 nearbys =
898 Internal.get attributes <|
899 \attr ->
900 case attr of
901 Internal.Nearby _ _ ->
902 True
904 _ ->
905 False
907 inputPadding =
908 Internal.get attributes <|
909 \attr ->
910 case attr of
911 Internal.StyleClass _ (Internal.PaddingStyle _ _ _ _ _) ->
912 True
914 _ ->
915 False
917 inputElement =
918 Internal.element
919 Internal.asEl
920 Internal.div
921 (Element.width Element.fill
922 :: List.concat
923 [ nearbys
924 , heightFillFromChild
925 , case textOptions.placeholder of
926 Nothing ->
927 []
929 Just (Placeholder placeholderAttrs placeholderEl) ->
930 [ Element.inFront
931 (Element.el
932 (defaultTextPadding
933 :: forPlaceholder
934 ++ [ Font.color charcoal
935 , Internal.htmlClass (classes.noTextSelection ++ " " ++ classes.passPointerEvents)
936 , Border.color (Element.rgba 0 0 0 0)
937 , Background.color (Element.rgba 0 0 0 0)
938 , Element.height Element.fill
939 , Element.width Element.fill
940 , Element.alpha
941 (if textOptions.text == "" then
942 1
944 else
945 0
946 )
947 ]
948 ++ placeholderAttrs
949 )
950 placeholderEl
951 )
952 ]
953 ]
954 )
955 (Internal.Unkeyed
956 [ Internal.element
957 Internal.asEl
958 (Internal.NodeName inputNode)
959 (List.concat
960 [ [ focusDefault attrs
961 , hiddenLabelAttribute textOptions.label
962 ]
963 , inputAttrs
964 , behavior
965 ]
966 )
967 (Internal.Unkeyed inputChildren)
968 ]
969 )
970 in
971 applyLabel
972 (Internal.Class Flag.cursor classes.cursorText
973 :: (if isHiddenLabel textOptions.label then
974 Internal.NoAttribute
976 else
977 Element.spacing
978 5
979 )
980 :: Region.announce
981 :: attributesFromChild
982 )
983 textOptions.label
984 inputElement
987textHeightContent textValue spacing maybePadding maybeBorder =
988 let
989 newlineCount =
990 String.lines textValue
991 |> List.length
992 |> (\x ->
993 if x < 1 then
994 1
996 else
997 x
998 )
1000 topBottom (Padding t _ b _) =
1001 t + b
1003 additionalSpacing =
1004 ((newlineCount - 1) * spacing)
1005 + Maybe.withDefault 0 (Maybe.map topBottom maybePadding)
1006 + Maybe.withDefault 0 (Maybe.map topBottom maybeBorder)
1008 heightValue count =
1009 "calc("
1010 ++ String.fromInt count
1011 ++ "em + "
1012 ++ String.fromInt additionalSpacing
1013 ++ "px) !important"
1014 in
1015 Internal.StyleClass Flag.heightTextAreaContent
1016 (Internal.Single ("textarea-height-" ++ String.fromInt newlineCount)
1017 "height"
1018 (heightValue newlineCount)
1022{-| -}
1023text :
1024 List (Attribute msg)
1025 ->
1026 { onChange : String -> msg
1027 , text : String
1028 , placeholder : Maybe (Placeholder msg)
1029 , label : Label msg
1031 -> Element msg
1032text =
1033 textHelper
1034 { type_ = TextInputNode "text"
1035 , spellchecked = False
1036 , autofill = Nothing
1040{-| If spell checking is available, this input will be spellchecked.
1042spellChecked :
1043 List (Attribute msg)
1044 ->
1045 { onChange : String -> msg
1046 , text : String
1047 , placeholder : Maybe (Placeholder msg)
1048 , label : Label msg
1050 -> Element msg
1051spellChecked =
1052 textHelper
1053 { type_ = TextInputNode "text"
1054 , spellchecked = True
1055 , autofill = Nothing
1059{-| -}
1060search :
1061 List (Attribute msg)
1062 ->
1063 { onChange : String -> msg
1064 , text : String
1065 , placeholder : Maybe (Placeholder msg)
1066 , label : Label msg
1068 -> Element msg
1069search =
1070 textHelper
1071 { type_ = TextInputNode "search"
1072 , spellchecked = False
1073 , autofill = Nothing
1077{-| A password input that allows the browser to autofill.
1079It's `newPassword` instead of just `password` because it gives the browser a hint on what type of password input it is.
1081A password takes all the arguments a normal `Input.text` would, and also `show`, which will remove the password mask (e.g. `****` vs `pass1234`)
1084newPassword :
1085 List (Attribute msg)
1086 ->
1087 { onChange : String -> msg
1088 , text : String
1089 , placeholder : Maybe (Placeholder msg)
1090 , label : Label msg
1091 , show : Bool
1093 -> Element msg
1094newPassword attrs pass =
1095 textHelper
1096 { type_ =
1097 TextInputNode <|
1098 if pass.show then
1099 "text"
1101 else
1102 "password"
1103 , spellchecked = False
1104 , autofill = Just "new-password"
1106 attrs
1107 { onChange = pass.onChange
1108 , text = pass.text
1109 , placeholder = pass.placeholder
1110 , label = pass.label
1114{-| -}
1115currentPassword :
1116 List (Attribute msg)
1117 ->
1118 { onChange : String -> msg
1119 , text : String
1120 , placeholder : Maybe (Placeholder msg)
1121 , label : Label msg
1122 , show : Bool
1124 -> Element msg
1125currentPassword attrs pass =
1126 textHelper
1127 { type_ =
1128 TextInputNode <|
1129 if pass.show then
1130 "text"
1132 else
1133 "password"
1134 , spellchecked = False
1135 , autofill = Just "current-password"
1137 attrs
1138 { onChange = pass.onChange
1139 , text = pass.text
1140 , placeholder = pass.placeholder
1141 , label = pass.label
1145{-| -}
1146username :
1147 List (Attribute msg)
1148 ->
1149 { onChange : String -> msg
1150 , text : String
1151 , placeholder : Maybe (Placeholder msg)
1152 , label : Label msg
1154 -> Element msg
1155username =
1156 textHelper
1157 { type_ = TextInputNode "text"
1158 , spellchecked = False
1159 , autofill = Just "username"
1163{-| -}
1164email :
1165 List (Attribute msg)
1166 ->
1167 { onChange : String -> msg
1168 , text : String
1169 , placeholder : Maybe (Placeholder msg)
1170 , label : Label msg
1172 -> Element msg
1173email =
1174 textHelper
1175 { type_ = TextInputNode "email"
1176 , spellchecked = False
1177 , autofill = Just "email"
1181{-| -}
1182multiline :
1183 List (Attribute msg)
1184 ->
1185 { onChange : String -> msg
1186 , text : String
1187 , placeholder : Maybe (Placeholder msg)
1188 , label : Label msg
1189 , spellcheck : Bool
1191 -> Element msg
1192multiline attrs multi =
1193 textHelper
1194 { type_ =
1195 TextArea
1196 , spellchecked = multi.spellcheck
1197 , autofill = Nothing
1199 attrs
1200 { onChange = multi.onChange
1201 , text = multi.text
1202 , placeholder = multi.placeholder
1203 , label = multi.label
1207isHiddenLabel label =
1208 case label of
1209 HiddenLabel _ ->
1210 True
1212 _ ->
1213 False
1216applyLabel : List (Attribute msg) -> Label msg -> Element msg -> Element msg
1217applyLabel attrs label input =
1218 case label of
1219 HiddenLabel labelText ->
1220 -- NOTE: This means that the label is applied outside of this function!
1221 -- It would be nice to unify this logic, but it's a little tricky
1222 Internal.element
1223 Internal.asColumn
1224 (Internal.NodeName "label")
1225 attrs
1226 (Internal.Unkeyed [ input ])
1228 Label position labelAttrs labelChild ->
1229 let
1230 labelElement =
1231 Internal.element
1232 Internal.asEl
1233 Internal.div
1234 labelAttrs
1235 (Internal.Unkeyed [ labelChild ])
1236 in
1237 case position of
1238 Above ->
1239 Internal.element
1240 Internal.asColumn
1241 (Internal.NodeName "label")
1242 attrs
1243 (Internal.Unkeyed [ labelElement, input ])
1245 Below ->
1246 Internal.element
1247 Internal.asColumn
1248 (Internal.NodeName "label")
1249 attrs
1250 (Internal.Unkeyed [ input, labelElement ])
1252 OnRight ->
1253 Internal.element
1254 Internal.asRow
1255 (Internal.NodeName "label")
1256 attrs
1257 (Internal.Unkeyed [ input, labelElement ])
1259 OnLeft ->
1260 Internal.element
1261 Internal.asRow
1262 (Internal.NodeName "label")
1263 attrs
1264 (Internal.Unkeyed [ labelElement, input ])
1267{-| Add choices to your radio and select menus.
1269type Option value msg
1270 = Option value (OptionState -> Element msg)
1273{-| -}
1274type OptionState
1275 = Idle
1276 | Focused
1277 | Selected
1280{-| -}
1281option : value -> Element msg -> Option value msg
1282option val txt =
1283 Option val (defaultRadioOption txt)
1286{-| -}
1287optionWith : value -> (OptionState -> Element msg) -> Option value msg
1288optionWith val view =
1289 Option val view
1292{-|
1294 Input.radio
1295 [ padding 10
1296 , spacing 20
1298 { onChange = ChooseLunch
1299 , selected = Just model.lunch
1300 , label = Input.labelAbove (text "Lunch")
1301 , options =
1302 [ Input.styledChoice Burrito <|
1303 \selected ->
1304 Element.row
1305 [ spacing 5 ]
1306 [ el None [] <|
1307 if selected then
1308 text ":D"
1310 else
1311 text ":("
1312 , text "burrito"
1314 , Input.option Taco (text "Taco!")
1315 , Input.option Gyro (text "Gyro")
1320radio :
1321 List (Attribute msg)
1322 ->
1323 { onChange : option -> msg
1324 , options : List (Option option msg)
1325 , selected : Maybe option
1326 , label : Label msg
1328 -> Element msg
1329radio =
1330 radioHelper Column
1333{-| Same as radio, but displayed as a row
1335radioRow :
1336 List (Attribute msg)
1337 ->
1338 { onChange : option -> msg
1339 , options : List (Option option msg)
1340 , selected : Maybe option
1341 , label : Label msg
1343 -> Element msg
1344radioRow =
1345 radioHelper Row
1348defaultRadioOption : Element msg -> OptionState -> Element msg
1349defaultRadioOption optionLabel status =
1350 Element.row
1351 [ Element.spacing 10
1352 , Element.alignLeft
1353 , Element.width Element.shrink
1355 [ Element.el
1356 [ Element.width (Element.px 14)
1357 , Element.height (Element.px 14)
1358 , Background.color white
1359 , Border.rounded 7
1360 , case status of
1361 Selected ->
1362 Internal.htmlClass "focusable"
1364 _ ->
1365 Internal.NoAttribute
1367 -- , Border.shadow <|
1368 -- -- case status of
1369 -- -- Idle ->
1370 -- -- { offset = ( 0, 0 )
1371 -- -- , blur =
1372 -- -- 1
1373 -- -- , color = Color.rgb 235 235 235
1374 -- -- }
1375 -- -- Focused ->
1376 -- -- { offset = ( 0, 0 )
1377 -- -- , blur =
1378 -- -- 0
1379 -- -- , color = Color.rgba 235 235 235 0
1380 -- -- }
1381 -- -- Selected ->
1382 -- { offset = ( 0, 0 )
1383 -- , blur =
1384 -- 1
1385 -- , color = Color.rgba 235 235 235 0
1386 -- }
1387 , Border.width <|
1388 case status of
1389 Idle ->
1392 Focused ->
1395 Selected ->
1397 , Border.color <|
1398 case status of
1399 Idle ->
1400 Element.rgb (208 / 255) (208 / 255) (208 / 255)
1402 Focused ->
1403 Element.rgb (208 / 255) (208 / 255) (208 / 255)
1405 Selected ->
1406 Element.rgb (59 / 255) (153 / 255) (252 / 255)
1408 Element.none
1409 , Element.el [ Element.width Element.fill, Internal.htmlClass "unfocusable" ] optionLabel
1413radioHelper :
1414 Orientation
1415 -> List (Attribute msg)
1416 ->
1417 { onChange : option -> msg
1418 , options : List (Option option msg)
1419 , selected : Maybe option
1420 , label : Label msg
1422 -> Element msg
1423radioHelper orientation attrs input =
1424 let
1425 renderOption (Option val view) =
1426 let
1427 status =
1428 if Just val == input.selected then
1429 Selected
1431 else
1432 Idle
1433 in
1434 Element.el
1435 [ Element.pointer
1436 , case orientation of
1437 Row ->
1438 Element.width Element.shrink
1440 Column ->
1441 Element.width Element.fill
1442 , Events.onClick (input.onChange val)
1443 , case status of
1444 Selected ->
1445 Internal.Attr <|
1446 Html.Attributes.attribute "aria-checked"
1447 "true"
1449 _ ->
1450 Internal.Attr <|
1451 Html.Attributes.attribute "aria-checked"
1452 "false"
1453 , Internal.Attr <|
1454 Html.Attributes.attribute "role" "radio"
1456 (view status)
1458 optionArea =
1459 case orientation of
1460 Row ->
1461 row (hiddenLabelAttribute input.label :: attrs)
1462 (List.map renderOption input.options)
1464 Column ->
1465 column (hiddenLabelAttribute input.label :: attrs)
1466 (List.map renderOption input.options)
1468 prevNext =
1469 case input.options of
1470 [] ->
1471 Nothing
1473 (Option val _) :: _ ->
1474 List.foldl track ( NotFound, val, val ) input.options
1475 |> (\( found, b, a ) ->
1476 case found of
1477 NotFound ->
1478 Just ( b, val )
1480 BeforeFound ->
1481 Just ( b, val )
1483 _ ->
1484 Just ( b, a )
1487 track opt ( found, prev, nxt ) =
1488 case opt of
1489 Option val _ ->
1490 case found of
1491 NotFound ->
1492 if Just val == input.selected then
1493 ( BeforeFound, prev, nxt )
1495 else
1496 ( found, val, nxt )
1498 BeforeFound ->
1499 ( AfterFound, prev, val )
1501 AfterFound ->
1502 ( found, prev, nxt )
1504 events =
1505 Internal.get
1506 attrs
1507 <|
1508 \attr ->
1509 case attr of
1510 Internal.Width (Internal.Fill _) ->
1511 True
1513 Internal.Height (Internal.Fill _) ->
1514 True
1516 Internal.Attr _ ->
1517 True
1519 _ ->
1520 False
1521 in
1522 applyLabel
1523 (List.filterMap identity
1524 [ Just Element.alignLeft
1525 , Just (tabindex 0)
1526 , Just (Internal.htmlClass "focus")
1527 , Just Region.announce
1528 , Just <|
1529 Internal.Attr <|
1530 Html.Attributes.attribute "role" "radiogroup"
1531 , case prevNext of
1532 Nothing ->
1533 Nothing
1535 Just ( prev, next ) ->
1536 Just
1537 (onKeyLookup <|
1538 \code ->
1539 if code == leftArrow then
1540 Just (input.onChange prev)
1542 else if code == upArrow then
1543 Just (input.onChange prev)
1545 else if code == rightArrow then
1546 Just (input.onChange next)
1548 else if code == downArrow then
1549 Just (input.onChange next)
1551 else if code == space then
1552 case input.selected of
1553 Nothing ->
1554 Just (input.onChange prev)
1556 _ ->
1557 Nothing
1559 else
1560 Nothing
1563 ++ events
1564 -- ++ hideIfEverythingisInvisible
1566 input.label
1567 optionArea
1570type Found
1571 = NotFound
1572 | BeforeFound
1573 | AfterFound
1576type Orientation
1577 = Row
1578 | Column
1581column : List (Attribute msg) -> List (Internal.Element msg) -> Internal.Element msg
1582column attributes children =
1583 Internal.element
1584 Internal.asColumn
1585 Internal.div
1586 (Element.height Element.shrink
1587 :: Element.width Element.fill
1588 :: attributes
1590 (Internal.Unkeyed children)
1593row : List (Attribute msg) -> List (Internal.Element msg) -> Internal.Element msg
1594row attributes children =
1595 Internal.element
1596 Internal.asRow
1597 Internal.div
1598 (Element.width Element.fill
1599 :: attributes
1601 (Internal.Unkeyed children)
1605{- Event Handlers -}
1608{-| -}
1609onEnter : msg -> Attribute msg
1610onEnter msg =
1611 onKey enter msg
1614{-| -}
1615onSpace : msg -> Attribute msg
1616onSpace msg =
1617 onKey space msg
1620{-| -}
1621onUpArrow : msg -> Attribute msg
1622onUpArrow msg =
1623 onKey upArrow msg
1626{-| -}
1627onRightArrow : msg -> Attribute msg
1628onRightArrow msg =
1629 onKey rightArrow msg
1632{-| -}
1633onLeftArrow : msg -> Attribute msg
1634onLeftArrow msg =
1635 onKey leftArrow msg
1638{-| -}
1639onDownArrow : msg -> Attribute msg
1640onDownArrow msg =
1641 onKey downArrow msg
1644enter : String
1645enter =
1646 "Enter"
1649tab : String
1650tab =
1651 "Tab"
1654delete : String
1655delete =
1656 "Delete"
1659backspace : String
1660backspace =
1661 "Backspace"
1664upArrow : String
1665upArrow =
1666 "ArrowUp"
1669leftArrow : String
1670leftArrow =
1671 "ArrowLeft"
1674rightArrow : String
1675rightArrow =
1676 "ArrowRight"
1679downArrow : String
1680downArrow =
1681 "ArrowDown"
1684space : String
1685space =
1686 " "
1689{-| -}
1690onKey : String -> msg -> Attribute msg
1691onKey desiredCode msg =
1692 let
1693 decode code =
1694 if code == desiredCode then
1695 Json.succeed msg
1697 else
1698 Json.fail "Not the enter key"
1700 isKey =
1701 Json.field "key" Json.string
1702 |> Json.andThen decode
1703 in
1704 Internal.Attr <|
1705 Html.Events.preventDefaultOn "keyup"
1706 (Json.map (\fired -> ( fired, True )) isKey)
1710-- preventKeydown : String -> a -> Attribute a
1711-- preventKeydown desiredCode msg =
1712-- let
1713-- decode code =
1714-- if code == desiredCode then
1715-- Json.succeed msg
1716-- else
1717-- Json.fail "Not the enter key"
1718-- isKey =
1719-- Json.field "key" Json.string
1720-- |> Json.andThen decode
1721-- in
1722-- Events.onWithOptions "keydown"
1723-- { stopPropagation = False
1724-- , preventDefault = True
1725-- }
1726-- isKey
1729{-| -}
1730onKeyLookup : (String -> Maybe msg) -> Attribute msg
1731onKeyLookup lookup =
1732 let
1733 decode code =
1734 case lookup code of
1735 Nothing ->
1736 Json.fail "No key matched"
1738 Just msg ->
1739 Json.succeed msg
1741 isKey =
1742 Json.field "key" Json.string
1743 |> Json.andThen decode
1744 in
1745 Internal.Attr <| Html.Events.on "keyup" isKey
1748{-| -}
1749onFocusOut : msg -> Attribute msg
1750onFocusOut msg =
1751 Internal.Attr <| Html.Events.on "focusout" (Json.succeed msg)
1754{-| -}
1755onFocusIn : msg -> Attribute msg
1756onFocusIn msg =
1757 Internal.Attr <| Html.Events.on "focusin" (Json.succeed msg)
1760type_ : String -> Attribute msg
1761type_ =
1762 Internal.Attr << Html.Attributes.type_
1766-- checked : Bool -> Attribute msg
1767-- checked =
1768-- Internal.Attr << Html.Attributes.checked
1771selected : Bool -> Attribute msg
1772selected =
1773 Internal.Attr << Html.Attributes.selected
1776name : String -> Attribute msg
1777name =
1778 Internal.Attr << Html.Attributes.name
1781value : String -> Attribute msg
1782value =
1783 Internal.Attr << Html.Attributes.value
1786tabindex : Int -> Attribute msg
1787tabindex =
1788 Internal.Attr << Html.Attributes.tabindex
1791disabled : Bool -> Attribute msg
1792disabled =
1793 Internal.Attr << Html.Attributes.disabled
1796spellcheck : Bool -> Attribute msg
1797spellcheck =
1798 Internal.Attr << Html.Attributes.spellcheck
1801readonly : Bool -> Attribute msg
1802readonly =
1803 Internal.Attr << Html.Attributes.readonly
1806autofill : String -> Attribute msg
1807autofill =
1808 Internal.Attr << Html.Attributes.attribute "autocomplete"
1811{-| -}
1812focusedOnLoad : Attribute msg
1813focusedOnLoad =
1814 Internal.Attr <| Html.Attributes.autofocus True
1818{- Style Defaults -}
1821defaultTextBoxStyle : List (Attribute msg)
1822defaultTextBoxStyle =
1823 [ defaultTextPadding
1824 , Border.rounded 3
1825 , Border.color darkGrey
1826 , Background.color white
1827 , Border.width 1
1828 , Element.spacing 3
1829 , Element.width Element.fill
1833defaultTextPadding : Attribute msg
1834defaultTextPadding =
1835 Element.paddingXY 12 12
1838{-| -}
1839defaultCheckbox : Bool -> Element msg
1840defaultCheckbox checked =
1841 Element.el
1842 [ Internal.htmlClass "focusable"
1843 , Element.width (Element.px 14)
1844 , Element.height (Element.px 14)
1845 , Font.color white
1846 , Element.centerY
1847 , Font.size 9
1848 , Font.center
1849 , Border.rounded 3
1850 , Border.color <|
1851 if checked then
1852 Element.rgb (59 / 255) (153 / 255) (252 / 255)
1854 else
1855 Element.rgb (211 / 255) (211 / 255) (211 / 255)
1856 , Border.shadow <|
1857 { offset = ( 0, 0 )
1858 , blur = 1
1859 , size = 1
1860 , color =
1861 if checked then
1862 Element.rgba (238 / 255) (238 / 255) (238 / 255) 0
1864 else
1865 Element.rgb (238 / 255) (238 / 255) (238 / 255)
1867 , Background.color <|
1868 if checked then
1869 Element.rgb (59 / 255) (153 / 255) (252 / 255)
1871 else
1872 white
1873 , Border.width <|
1874 if checked then
1877 else
1880 (if checked then
1881 Element.el
1882 [ Border.color white
1883 , Element.height (Element.px 6)
1884 , Element.width (Element.px 9)
1885 , Element.rotate (degrees -45)
1886 , Element.centerX
1887 , Element.centerY
1888 , Element.moveUp 1
1889 , Border.widthEach
1890 { top = 0
1891 , left = 2
1892 , bottom = 2
1893 , right = 0
1896 Element.none
1898 else
1899 Element.none