git.iko.soy/typeable/elm-ui
on dee2fda help sign in
1968 lines 56 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 makes sense 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
676{-|
678 attributes
680 <parent>
681 attribute::width/height fill
682 attribtue::alignment
683 attribute::spacing
684 attribute::fontsize/family/lineheight
685 <el-wrapper>
686 attribute::nearby(placeholder)
687 attribute::width/height fill
688 inFront ->
689 placeholder
690 attribute::padding
693 <input>
694 textarea ->
695 special height for height-content
696 attribtue::padding
697 attribute::lineHeight
698 attributes
699 <label>
701-}
702textHelper : TextInput -> List (Attribute msg) -> Text msg -> Element msg
703textHelper textInput attrs textOptions =
704 let
705 attributes =
706 defaultTextBoxStyle ++ attrs
708 behavior =
709 [ Internal.Attr (Html.Events.onInput textOptions.onChange) ]
711 noNearbys =
712 List.filter (not << forNearby) attributes
714 forNearby attr =
715 case attr of
716 Internal.Nearby _ _ ->
717 True
719 _ ->
720 False
722 ( inputNode, inputAttrs, inputChildren ) =
723 case textInput.type_ of
724 TextInputNode inputType ->
725 ( "input"
726 , [ value textOptions.text
727 , Internal.Attr (Html.Attributes.type_ inputType)
728 , spellcheck textInput.spellchecked
729 , Internal.htmlClass classes.inputText
730 , case textInput.autofill of
731 Nothing ->
732 Internal.NoAttribute
734 Just fill ->
735 autofill fill
736 ]
737 ++ noNearbys
738 , []
739 )
741 TextArea ->
742 let
743 { maybePadding, heightContent, maybeSpacing, adjustedAttributes, maybeBorder } =
744 attributes
745 |> List.foldr
746 (\attr found ->
747 case attr of
748 Internal.Describe _ ->
749 found
751 Internal.Height len ->
752 case found.heightContent of
753 Nothing ->
754 { found
755 | heightContent = Just (Internal.isContent len)
756 , adjustedAttributes = attr :: found.adjustedAttributes
757 }
759 _ ->
760 found
762 Internal.StyleClass _ (Internal.BorderWidth _ t r b l) ->
763 case found.maybeBorder of
764 Nothing ->
765 { found
766 | maybeBorder = Just (Padding t r b l)
767 , adjustedAttributes = attr :: found.adjustedAttributes
768 }
770 _ ->
771 found
773 Internal.StyleClass _ (Internal.PaddingStyle _ t r b l) ->
774 case found.maybePadding of
775 Nothing ->
776 { found
777 | maybePadding = Just (Padding t r b l)
779 -- Skip storing padding because it's being customized later
780 , adjustedAttributes = found.adjustedAttributes
781 }
783 _ ->
784 found
786 Internal.StyleClass _ (Internal.SpacingStyle _ x y) ->
787 case found.maybeSpacing of
788 Nothing ->
789 { found
790 | maybeSpacing = Just y
791 , adjustedAttributes = attr :: found.adjustedAttributes
792 }
794 _ ->
795 found
797 _ ->
798 { found | adjustedAttributes = attr :: found.adjustedAttributes }
799 )
800 { maybePadding = Nothing
801 , heightContent = Nothing
802 , maybeBorder = Nothing
803 , maybeSpacing = Nothing
804 , adjustedAttributes = []
805 }
807 -- NOTE: This is where default text spacing is set
808 spacing =
809 Maybe.withDefault 5 maybeSpacing
810 in
811 ( "textarea"
812 , [ spellcheck textInput.spellchecked
813 , Internal.htmlClass classes.inputMultiline
814 , Maybe.map autofill textInput.autofill
815 |> Maybe.withDefault Internal.NoAttribute
816 , case maybePadding of
817 Nothing ->
818 Internal.NoAttribute
820 Just (Padding t r b l) ->
821 Element.paddingEach
822 { top = max 0 (t - (spacing // 2))
823 , bottom = max 0 (b - (spacing // 2))
824 , left = l
825 , right = r
826 }
827 , case heightContent of
828 Nothing ->
829 Internal.NoAttribute
831 Just True ->
832 textHeightContent textOptions.text spacing maybePadding maybeBorder
834 Just False ->
835 Internal.NoAttribute
836 ]
837 ++ adjustedAttributes
838 , [ Internal.unstyled (Html.text textOptions.text) ]
839 )
841 heightFillFromChild =
842 Internal.get attributes <|
843 \attr ->
844 case attr of
845 Internal.Height (Internal.Fill _) ->
846 True
848 _ ->
849 False
851 attributesFromChild =
852 Internal.get attributes <|
853 \attr ->
854 case attr of
855 Internal.Width (Internal.Fill _) ->
856 True
858 Internal.Height (Internal.Fill _) ->
859 True
861 Internal.AlignX _ ->
862 True
864 Internal.AlignY _ ->
865 True
867 Internal.StyleClass _ (Internal.SpacingStyle _ _ _) ->
868 True
870 Internal.StyleClass _ (Internal.FontSize _) ->
871 True
873 Internal.StyleClass _ (Internal.FontFamily _ _) ->
874 True
876 _ ->
877 False
879 nearbys =
880 Internal.get attributes <|
881 \attr ->
882 case attr of
883 Internal.Nearby _ _ ->
884 True
886 _ ->
887 False
889 inputPadding =
890 Internal.get attributes <|
891 \attr ->
892 case attr of
893 Internal.StyleClass _ (Internal.PaddingStyle _ _ _ _ _) ->
894 True
896 _ ->
897 False
899 inputElement =
900 Internal.element
901 Internal.asEl
902 Internal.div
903 (Element.width Element.fill
904 :: List.concat
905 [ nearbys
906 , heightFillFromChild
907 , case textOptions.placeholder of
908 Nothing ->
909 []
911 Just (Placeholder placeholderAttrs placeholderEl) ->
912 [ Element.inFront
913 (Element.el
914 (defaultTextPadding
915 :: noNearbys
916 ++ [ Font.color charcoal
917 , Internal.htmlClass (classes.noTextSelection ++ " " ++ classes.passPointerEvents)
918 , Border.color (Element.rgba 0 0 0 0)
919 , Background.color (Element.rgba 0 0 0 0)
920 , Element.height Element.fill
921 , Element.width Element.fill
922 , Element.alpha
923 (if textOptions.text == "" then
924 1
926 else
927 0
928 )
929 ]
930 ++ placeholderAttrs
931 )
932 placeholderEl
933 )
934 ]
935 ]
936 )
937 (Internal.Unkeyed
938 [ Internal.element
939 Internal.asEl
940 (Internal.NodeName inputNode)
941 (List.concat
942 [ [ focusDefault attrs
943 , hiddenLabelAttribute textOptions.label
944 ]
945 , inputAttrs
946 , behavior
947 ]
948 )
949 (Internal.Unkeyed inputChildren)
950 ]
951 )
952 in
953 applyLabel
954 (Internal.Class Flag.cursor classes.cursorText
955 :: (if isHiddenLabel textOptions.label then
956 Internal.NoAttribute
958 else
959 Element.spacing
960 5
961 )
962 :: Region.announce
963 :: attributesFromChild
964 )
965 textOptions.label
966 inputElement
969textHeightContent textValue spacing maybePadding maybeBorder =
970 let
971 newlineCount =
972 String.lines textValue
973 |> List.length
974 |> (\x ->
975 if x < 1 then
976 1
978 else
979 x
980 )
982 topBottom (Padding t _ b _) =
983 t + b
985 additionalSpacing =
986 ((newlineCount - 1) * spacing)
987 + Maybe.withDefault 0 (Maybe.map topBottom maybePadding)
988 + Maybe.withDefault 0 (Maybe.map topBottom maybeBorder)
990 heightValue count =
991 "calc("
992 ++ String.fromInt count
993 ++ "em + "
994 ++ String.fromInt additionalSpacing
995 ++ "px) !important"
996 in
997 Internal.StyleClass Flag.heightTextAreaContent
998 (Internal.Single ("textarea-height-" ++ String.fromInt newlineCount)
999 "height"
1000 (heightValue newlineCount)
1004{-| -}
1005text :
1006 List (Attribute msg)
1007 ->
1008 { onChange : String -> msg
1009 , text : String
1010 , placeholder : Maybe (Placeholder msg)
1011 , label : Label msg
1013 -> Element msg
1014text =
1015 textHelper
1016 { type_ = TextInputNode "text"
1017 , spellchecked = False
1018 , autofill = Nothing
1022{-| If spell checking is available, this input will be spellchecked.
1024spellChecked :
1025 List (Attribute msg)
1026 ->
1027 { onChange : String -> msg
1028 , text : String
1029 , placeholder : Maybe (Placeholder msg)
1030 , label : Label msg
1032 -> Element msg
1033spellChecked =
1034 textHelper
1035 { type_ = TextInputNode "text"
1036 , spellchecked = True
1037 , autofill = Nothing
1041{-| -}
1042search :
1043 List (Attribute msg)
1044 ->
1045 { onChange : String -> msg
1046 , text : String
1047 , placeholder : Maybe (Placeholder msg)
1048 , label : Label msg
1050 -> Element msg
1051search =
1052 textHelper
1053 { type_ = TextInputNode "search"
1054 , spellchecked = False
1055 , autofill = Nothing
1059{-| A password input that allows the browser to autofill.
1061It's `newPassword` instead of just `password` because it gives the browser a hint on what type of password input it is.
1063A password takes all the arguments a normal `Input.text` would, and also `show`, which will remove the password mask (e.g. `****` vs `pass1234`)
1066newPassword :
1067 List (Attribute msg)
1068 ->
1069 { onChange : String -> msg
1070 , text : String
1071 , placeholder : Maybe (Placeholder msg)
1072 , label : Label msg
1073 , show : Bool
1075 -> Element msg
1076newPassword attrs pass =
1077 textHelper
1078 { type_ =
1079 TextInputNode <|
1080 if pass.show then
1081 "text"
1083 else
1084 "password"
1085 , spellchecked = False
1086 , autofill = Just "new-password"
1088 attrs
1089 { onChange = pass.onChange
1090 , text = pass.text
1091 , placeholder = pass.placeholder
1092 , label = pass.label
1096{-| -}
1097currentPassword :
1098 List (Attribute msg)
1099 ->
1100 { onChange : String -> msg
1101 , text : String
1102 , placeholder : Maybe (Placeholder msg)
1103 , label : Label msg
1104 , show : Bool
1106 -> Element msg
1107currentPassword attrs pass =
1108 textHelper
1109 { type_ =
1110 TextInputNode <|
1111 if pass.show then
1112 "text"
1114 else
1115 "password"
1116 , spellchecked = False
1117 , autofill = Just "current-password"
1119 attrs
1120 { onChange = pass.onChange
1121 , text = pass.text
1122 , placeholder = pass.placeholder
1123 , label = pass.label
1127{-| -}
1128username :
1129 List (Attribute msg)
1130 ->
1131 { onChange : String -> msg
1132 , text : String
1133 , placeholder : Maybe (Placeholder msg)
1134 , label : Label msg
1136 -> Element msg
1137username =
1138 textHelper
1139 { type_ = TextInputNode "text"
1140 , spellchecked = False
1141 , autofill = Just "username"
1145{-| -}
1146email :
1147 List (Attribute msg)
1148 ->
1149 { onChange : String -> msg
1150 , text : String
1151 , placeholder : Maybe (Placeholder msg)
1152 , label : Label msg
1154 -> Element msg
1155email =
1156 textHelper
1157 { type_ = TextInputNode "email"
1158 , spellchecked = False
1159 , autofill = Just "email"
1163{-| -}
1164multiline :
1165 List (Attribute msg)
1166 ->
1167 { onChange : String -> msg
1168 , text : String
1169 , placeholder : Maybe (Placeholder msg)
1170 , label : Label msg
1171 , spellcheck : Bool
1173 -> Element msg
1174multiline attrs multi =
1175 textHelper
1176 { type_ =
1177 TextArea
1178 , spellchecked = multi.spellcheck
1179 , autofill = Nothing
1181 attrs
1182 { onChange = multi.onChange
1183 , text = multi.text
1184 , placeholder = multi.placeholder
1185 , label = multi.label
1189isHiddenLabel label =
1190 case label of
1191 HiddenLabel _ ->
1192 True
1194 _ ->
1195 False
1198applyLabel : List (Attribute msg) -> Label msg -> Element msg -> Element msg
1199applyLabel attrs label input =
1200 case label of
1201 HiddenLabel labelText ->
1202 -- NOTE: This means that the label is applied outside of this function!
1203 -- It would be nice to unify this logic, but it's a little tricky
1204 Internal.element
1205 Internal.asColumn
1206 (Internal.NodeName "label")
1207 attrs
1208 (Internal.Unkeyed [ input ])
1210 Label position labelAttrs labelChild ->
1211 let
1212 labelElement =
1213 Internal.element
1214 Internal.asEl
1215 Internal.div
1216 labelAttrs
1217 (Internal.Unkeyed [ labelChild ])
1218 in
1219 case position of
1220 Above ->
1221 Internal.element
1222 Internal.asColumn
1223 (Internal.NodeName "label")
1224 attrs
1225 (Internal.Unkeyed [ labelElement, input ])
1227 Below ->
1228 Internal.element
1229 Internal.asColumn
1230 (Internal.NodeName "label")
1231 attrs
1232 (Internal.Unkeyed [ input, labelElement ])
1234 OnRight ->
1235 Internal.element
1236 Internal.asRow
1237 (Internal.NodeName "label")
1238 attrs
1239 (Internal.Unkeyed [ input, labelElement ])
1241 OnLeft ->
1242 Internal.element
1243 Internal.asRow
1244 (Internal.NodeName "label")
1245 attrs
1246 (Internal.Unkeyed [ labelElement, input ])
1249{-| Add choices to your radio and select menus.
1251type Option value msg
1252 = Option value (OptionState -> Element msg)
1255{-| -}
1256type OptionState
1257 = Idle
1258 | Focused
1259 | Selected
1262{-| -}
1263option : value -> Element msg -> Option value msg
1264option val txt =
1265 Option val (defaultRadioOption txt)
1268{-| -}
1269optionWith : value -> (OptionState -> Element msg) -> Option value msg
1270optionWith val view =
1271 Option val view
1274{-|
1276 Input.radio
1277 [ padding 10
1278 , spacing 20
1280 { onChange = ChooseLunch
1281 , selected = Just model.lunch
1282 , label = Input.labelAbove (text "Lunch")
1283 , options =
1284 [ Input.styledChoice Burrito <|
1285 \selected ->
1286 Element.row
1287 [ spacing 5 ]
1288 [ el None [] <|
1289 if selected then
1290 text ":D"
1292 else
1293 text ":("
1294 , text "burrito"
1296 , Input.option Taco (text "Taco!")
1297 , Input.option Gyro (text "Gyro")
1302radio :
1303 List (Attribute msg)
1304 ->
1305 { onChange : option -> msg
1306 , options : List (Option option msg)
1307 , selected : Maybe option
1308 , label : Label msg
1310 -> Element msg
1311radio =
1312 radioHelper Column
1315{-| Same as radio, but displayed as a row
1317radioRow :
1318 List (Attribute msg)
1319 ->
1320 { onChange : option -> msg
1321 , options : List (Option option msg)
1322 , selected : Maybe option
1323 , label : Label msg
1325 -> Element msg
1326radioRow =
1327 radioHelper Row
1330defaultRadioOption : Element msg -> OptionState -> Element msg
1331defaultRadioOption optionLabel status =
1332 Element.row
1333 [ Element.spacing 10
1334 , Element.alignLeft
1335 , Element.width Element.shrink
1337 [ Element.el
1338 [ Element.width (Element.px 14)
1339 , Element.height (Element.px 14)
1340 , Background.color white
1341 , Border.rounded 7
1342 , case status of
1343 Selected ->
1344 Internal.htmlClass "focusable"
1346 _ ->
1347 Internal.NoAttribute
1349 -- , Border.shadow <|
1350 -- -- case status of
1351 -- -- Idle ->
1352 -- -- { offset = ( 0, 0 )
1353 -- -- , blur =
1354 -- -- 1
1355 -- -- , color = Color.rgb 235 235 235
1356 -- -- }
1357 -- -- Focused ->
1358 -- -- { offset = ( 0, 0 )
1359 -- -- , blur =
1360 -- -- 0
1361 -- -- , color = Color.rgba 235 235 235 0
1362 -- -- }
1363 -- -- Selected ->
1364 -- { offset = ( 0, 0 )
1365 -- , blur =
1366 -- 1
1367 -- , color = Color.rgba 235 235 235 0
1368 -- }
1369 , Border.width <|
1370 case status of
1371 Idle ->
1374 Focused ->
1377 Selected ->
1379 , Border.color <|
1380 case status of
1381 Idle ->
1382 Element.rgb (208 / 255) (208 / 255) (208 / 255)
1384 Focused ->
1385 Element.rgb (208 / 255) (208 / 255) (208 / 255)
1387 Selected ->
1388 Element.rgb (59 / 255) (153 / 255) (252 / 255)
1390 Element.none
1391 , Element.el [ Element.width Element.fill, Internal.htmlClass "unfocusable" ] optionLabel
1395radioHelper :
1396 Orientation
1397 -> List (Attribute msg)
1398 ->
1399 { onChange : option -> msg
1400 , options : List (Option option msg)
1401 , selected : Maybe option
1402 , label : Label msg
1404 -> Element msg
1405radioHelper orientation attrs input =
1406 let
1407 renderOption (Option val view) =
1408 let
1409 status =
1410 if Just val == input.selected then
1411 Selected
1413 else
1414 Idle
1415 in
1416 Element.el
1417 [ Element.pointer
1418 , case orientation of
1419 Row ->
1420 Element.width Element.shrink
1422 Column ->
1423 Element.width Element.fill
1424 , Events.onClick (input.onChange val)
1425 , case status of
1426 Selected ->
1427 Internal.Attr <|
1428 Html.Attributes.attribute "aria-checked"
1429 "true"
1431 _ ->
1432 Internal.Attr <|
1433 Html.Attributes.attribute "aria-checked"
1434 "false"
1435 , Internal.Attr <|
1436 Html.Attributes.attribute "role" "radio"
1438 (view status)
1440 optionArea =
1441 case orientation of
1442 Row ->
1443 row (hiddenLabelAttribute input.label :: attrs)
1444 (List.map renderOption input.options)
1446 Column ->
1447 column (hiddenLabelAttribute input.label :: attrs)
1448 (List.map renderOption input.options)
1450 prevNext =
1451 case input.options of
1452 [] ->
1453 Nothing
1455 (Option val _) :: _ ->
1456 List.foldl track ( NotFound, val, val ) input.options
1457 |> (\( found, b, a ) ->
1458 case found of
1459 NotFound ->
1460 Just ( b, val )
1462 BeforeFound ->
1463 Just ( b, val )
1465 _ ->
1466 Just ( b, a )
1469 track opt ( found, prev, nxt ) =
1470 case opt of
1471 Option val _ ->
1472 case found of
1473 NotFound ->
1474 if Just val == input.selected then
1475 ( BeforeFound, prev, nxt )
1477 else
1478 ( found, val, nxt )
1480 BeforeFound ->
1481 ( AfterFound, prev, val )
1483 AfterFound ->
1484 ( found, prev, nxt )
1486 inputVisible =
1487 List.isEmpty <|
1488 Internal.get attrs <|
1489 \attr ->
1490 case attr of
1491 Internal.StyleClass _ (Internal.Transparency _ _) ->
1492 True
1494 Internal.Class _ "hidden" ->
1495 True
1497 _ ->
1498 False
1500 labelVisible =
1501 case input.label of
1502 Label _ labelAttrs _ ->
1503 List.isEmpty <|
1504 Internal.get labelAttrs <|
1505 \attr ->
1506 case attr of
1507 Internal.StyleClass _ (Internal.Transparency _ _) ->
1508 True
1510 Internal.Class _ "hidden" ->
1511 True
1513 _ ->
1514 False
1516 HiddenLabel _ ->
1517 True
1519 hideIfEverythingisInvisible =
1520 if not labelVisible && not inputVisible then
1521 let
1522 pseudos =
1523 List.filterMap
1524 (\attr ->
1525 case attr of
1526 Internal.StyleClass _ style ->
1527 case style of
1528 Internal.PseudoSelector pseudo styles ->
1529 let
1530 transparent =
1531 List.filter forTransparency styles
1533 forTransparency psuedoStyle =
1534 case psuedoStyle of
1535 Internal.Transparency _ _ ->
1536 True
1538 _ ->
1539 False
1541 flag =
1542 case pseudo of
1543 Internal.Hover ->
1544 Flag.hover
1546 Internal.Focus ->
1547 Flag.focus
1549 Internal.Active ->
1550 Flag.active
1551 in
1552 case transparent of
1553 [] ->
1554 Nothing
1556 _ ->
1557 Just <| Internal.StyleClass flag <| Internal.PseudoSelector pseudo transparent
1559 _ ->
1560 Nothing
1562 _ ->
1563 Nothing
1565 attrs
1566 in
1567 Internal.StyleClass Flag.transparency (Internal.Transparency "transparent" 1.0) :: pseudos
1569 else
1570 []
1572 events =
1573 Internal.get
1574 attrs
1575 <|
1576 \attr ->
1577 case attr of
1578 Internal.Width (Internal.Fill _) ->
1579 True
1581 Internal.Height (Internal.Fill _) ->
1582 True
1584 Internal.Attr _ ->
1585 True
1587 _ ->
1588 False
1589 in
1590 applyLabel
1591 (List.filterMap identity
1592 [ Just Element.alignLeft
1593 , Just (tabindex 0)
1594 , Just (Internal.htmlClass "focus")
1595 , Just Region.announce
1596 , Just <|
1597 Internal.Attr <|
1598 Html.Attributes.attribute "role" "radiogroup"
1599 , case prevNext of
1600 Nothing ->
1601 Nothing
1603 Just ( prev, next ) ->
1604 Just
1605 (onKeyLookup <|
1606 \code ->
1607 if code == leftArrow then
1608 Just (input.onChange prev)
1610 else if code == upArrow then
1611 Just (input.onChange prev)
1613 else if code == rightArrow then
1614 Just (input.onChange next)
1616 else if code == downArrow then
1617 Just (input.onChange next)
1619 else if code == space then
1620 case input.selected of
1621 Nothing ->
1622 Just (input.onChange prev)
1624 _ ->
1625 Nothing
1627 else
1628 Nothing
1631 ++ events
1632 ++ hideIfEverythingisInvisible
1634 input.label
1635 optionArea
1638type Found
1639 = NotFound
1640 | BeforeFound
1641 | AfterFound
1644type Orientation
1645 = Row
1646 | Column
1649column : List (Attribute msg) -> List (Internal.Element msg) -> Internal.Element msg
1650column attributes children =
1651 Internal.element
1652 Internal.asColumn
1653 Internal.div
1654 (Element.height Element.shrink
1655 :: Element.width Element.fill
1656 :: attributes
1658 (Internal.Unkeyed children)
1661row : List (Attribute msg) -> List (Internal.Element msg) -> Internal.Element msg
1662row attributes children =
1663 Internal.element
1664 Internal.asRow
1665 Internal.div
1666 (Element.width Element.fill
1667 :: attributes
1669 (Internal.Unkeyed children)
1673{- Event Handlers -}
1676{-| -}
1677onEnter : msg -> Attribute msg
1678onEnter msg =
1679 onKey enter msg
1682{-| -}
1683onSpace : msg -> Attribute msg
1684onSpace msg =
1685 onKey space msg
1688{-| -}
1689onUpArrow : msg -> Attribute msg
1690onUpArrow msg =
1691 onKey upArrow msg
1694{-| -}
1695onRightArrow : msg -> Attribute msg
1696onRightArrow msg =
1697 onKey rightArrow msg
1700{-| -}
1701onLeftArrow : msg -> Attribute msg
1702onLeftArrow msg =
1703 onKey leftArrow msg
1706{-| -}
1707onDownArrow : msg -> Attribute msg
1708onDownArrow msg =
1709 onKey downArrow msg
1712enter : String
1713enter =
1714 "Enter"
1717tab : String
1718tab =
1719 "Tab"
1722delete : String
1723delete =
1724 "Delete"
1727backspace : String
1728backspace =
1729 "Backspace"
1732upArrow : String
1733upArrow =
1734 "ArrowUp"
1737leftArrow : String
1738leftArrow =
1739 "ArrowLeft"
1742rightArrow : String
1743rightArrow =
1744 "ArrowRight"
1747downArrow : String
1748downArrow =
1749 "ArrowDown"
1752space : String
1753space =
1754 " "
1757{-| -}
1758onKey : String -> msg -> Attribute msg
1759onKey desiredCode msg =
1760 let
1761 decode code =
1762 if code == desiredCode then
1763 Json.succeed msg
1765 else
1766 Json.fail "Not the enter key"
1768 isKey =
1769 Json.field "key" Json.string
1770 |> Json.andThen decode
1771 in
1772 Internal.Attr <|
1773 Html.Events.preventDefaultOn "keyup"
1774 (Json.map (\fired -> ( fired, True )) isKey)
1778-- preventKeydown : String -> a -> Attribute a
1779-- preventKeydown desiredCode msg =
1780-- let
1781-- decode code =
1782-- if code == desiredCode then
1783-- Json.succeed msg
1784-- else
1785-- Json.fail "Not the enter key"
1786-- isKey =
1787-- Json.field "key" Json.string
1788-- |> Json.andThen decode
1789-- in
1790-- Events.onWithOptions "keydown"
1791-- { stopPropagation = False
1792-- , preventDefault = True
1793-- }
1794-- isKey
1797{-| -}
1798onKeyLookup : (String -> Maybe msg) -> Attribute msg
1799onKeyLookup lookup =
1800 let
1801 decode code =
1802 case lookup code of
1803 Nothing ->
1804 Json.fail "No key matched"
1806 Just msg ->
1807 Json.succeed msg
1809 isKey =
1810 Json.field "key" Json.string
1811 |> Json.andThen decode
1812 in
1813 Internal.Attr <| Html.Events.on "keyup" isKey
1816{-| -}
1817onFocusOut : msg -> Attribute msg
1818onFocusOut msg =
1819 Internal.Attr <| Html.Events.on "focusout" (Json.succeed msg)
1822{-| -}
1823onFocusIn : msg -> Attribute msg
1824onFocusIn msg =
1825 Internal.Attr <| Html.Events.on "focusin" (Json.succeed msg)
1828type_ : String -> Attribute msg
1829type_ =
1830 Internal.Attr << Html.Attributes.type_
1834-- checked : Bool -> Attribute msg
1835-- checked =
1836-- Internal.Attr << Html.Attributes.checked
1839selected : Bool -> Attribute msg
1840selected =
1841 Internal.Attr << Html.Attributes.selected
1844name : String -> Attribute msg
1845name =
1846 Internal.Attr << Html.Attributes.name
1849value : String -> Attribute msg
1850value =
1851 Internal.Attr << Html.Attributes.value
1854tabindex : Int -> Attribute msg
1855tabindex =
1856 Internal.Attr << Html.Attributes.tabindex
1859disabled : Bool -> Attribute msg
1860disabled =
1861 Internal.Attr << Html.Attributes.disabled
1864spellcheck : Bool -> Attribute msg
1865spellcheck =
1866 Internal.Attr << Html.Attributes.spellcheck
1869readonly : Bool -> Attribute msg
1870readonly =
1871 Internal.Attr << Html.Attributes.readonly
1874autofill : String -> Attribute msg
1875autofill =
1876 Internal.Attr << Html.Attributes.attribute "autocomplete"
1879{-| -}
1880focusedOnLoad : Attribute msg
1881focusedOnLoad =
1882 Internal.Attr <| Html.Attributes.autofocus True
1886{- Style Defaults -}
1889defaultTextBoxStyle : List (Attribute msg)
1890defaultTextBoxStyle =
1891 [ defaultTextPadding
1892 , Border.rounded 3
1893 , Border.color darkGrey
1894 , Background.color white
1895 , Border.width 1
1896 , Element.spacing 3
1897 , Element.width Element.fill
1901defaultTextPadding : Attribute msg
1902defaultTextPadding =
1903 Element.paddingXY 12 12
1906{-| -}
1907defaultCheckbox : Bool -> Element msg
1908defaultCheckbox checked =
1909 Element.el
1910 [ Internal.htmlClass "focusable"
1911 , Element.width (Element.px 14)
1912 , Element.height (Element.px 14)
1913 , Font.color white
1914 , Element.centerY
1915 , Font.size 9
1916 , Font.center
1917 , Border.rounded 3
1918 , Border.color <|
1919 if checked then
1920 Element.rgb (59 / 255) (153 / 255) (252 / 255)
1922 else
1923 Element.rgb (211 / 255) (211 / 255) (211 / 255)
1924 , Border.shadow <|
1925 { offset = ( 0, 0 )
1926 , blur = 1
1927 , size = 1
1928 , color =
1929 if checked then
1930 Element.rgba (238 / 255) (238 / 255) (238 / 255) 0
1932 else
1933 Element.rgb (238 / 255) (238 / 255) (238 / 255)
1935 , Background.color <|
1936 if checked then
1937 Element.rgb (59 / 255) (153 / 255) (252 / 255)
1939 else
1940 white
1941 , Border.width <|
1942 if checked then
1945 else
1948 (if checked then
1949 Element.el
1950 [ Border.color white
1951 , Element.height (Element.px 6)
1952 , Element.width (Element.px 9)
1953 , Element.rotate (degrees -45)
1954 , Element.centerX
1955 , Element.centerY
1956 , Element.moveUp 1
1957 , Border.widthEach
1958 { top = 0
1959 , left = 2
1960 , bottom = 2
1961 , right = 0
1964 Element.none
1966 else
1967 Element.none