diff --git a/docs/unstable/index.html b/docs/unstable/index.html new file mode 100644 index 0000000..7346cb7 --- /dev/null +++ b/docs/unstable/index.html @@ -0,0 +1,17994 @@ + + + + + Example + + + + + +

+
+
+
+
+
\ No newline at end of file
diff --git a/example/src/Component.elm b/example/src/Component.elm
index ea6dab8..f1a527f 100644
--- a/example/src/Component.elm
+++ b/example/src/Component.elm
@@ -4,6 +4,8 @@ import Browser
 import Element exposing (Color, Element)
 import Element.Background as Background
 import Element.Input as Input
+import Element.Border as Border
+import Element.Font as Font
 import Framework
 import Framework.Button as Button
 import Framework.Card as Card
@@ -19,22 +21,32 @@ import Html.Attributes as Attributes
 import Set exposing (Set)
 import Time
 import Widget
+import Widget.Button as Button exposing (ButtonStyle)
 import Widget.FilterSelect as FilterSelect
+import Widget.FilterMultiSelect as FilterMultiSelect
 import Widget.ScrollingNav as ScrollingNav
 import Widget.Snackbar as Snackbar
 import Widget.ValidatedInput as ValidatedInput
 
-
 type alias Model =
     { filterSelect : FilterSelect.Model
+    , filterMultiSelect : FilterMultiSelect.Model
     , validatedInput : ValidatedInput.Model () ( String, String )
     }
 
 
 type Msg
     = FilterSelectSpecific FilterSelect.Msg
+    | FilterMultiSelectSpecific FilterMultiSelect.Msg
     | ValidatedInputSpecific ValidatedInput.Msg
 
+chipButton : ButtonStyle msg
+chipButton =
+    { container = Tag.simple
+    , disabled = []
+    , label = Grid.simple
+    , active = Color.primary
+    }
 
 init : Model
 init =
@@ -56,6 +68,24 @@ init =
         ]
             |> Set.fromList
             |> FilterSelect.init
+    , filterMultiSelect =
+        [ "Apple"
+        , "Kiwi"
+        , "Strawberry"
+        , "Pineapple"
+        , "Mango"
+        , "Grapes"
+        , "Watermelon"
+        , "Orange"
+        , "Lemon"
+        , "Blueberry"
+        , "Grapefruit"
+        , "Coconut"
+        , "Cherry"
+        , "Banana"
+        ]
+            |> Set.fromList
+            |> FilterMultiSelect.init
     , validatedInput =
         ValidatedInput.init
             { value = ( "John", "Doe" )
@@ -83,6 +113,13 @@ update msg model =
             , Cmd.none
             )
 
+        FilterMultiSelectSpecific m ->
+            ( { model
+                | filterMultiSelect = model.filterMultiSelect |> FilterMultiSelect.update m
+              }
+            , Cmd.none
+            )
+
         ValidatedInputSpecific m ->
             ( { model
                 | validatedInput = model.validatedInput |> ValidatedInput.update m
@@ -129,6 +166,55 @@ filterSelect model =
                     ]
     )
 
+filterMultiSelect : FilterMultiSelect.Model -> (String,Element Msg)
+filterMultiSelect model =
+    ( "Filter Multi Select"
+    ,   [ FilterMultiSelect.viewInput model
+            { msgMapper = FilterMultiSelectSpecific
+            , placeholder =
+                Just <|
+                    Input.placeholder [] <|
+                        Element.text <|
+                            "Fruit"
+            , label = "Fruit"
+            , toChip = \string ->
+                { text = string
+                , onPress = Just <| FilterMultiSelectSpecific <| FilterMultiSelect.ToggleSelected <| string
+                , icon = Element.none
+                }
+            }
+            |> Widget.textInput
+                { chip = chipButton
+                , chipsRow = 
+                    [ Element.width <| Element.shrink
+                    , Element.spacing <| 4 ]
+                , containerRow = 
+                    Button.simple
+                    ++ Color.light
+                    ++ [ Border.color <| Element.rgb255 186 189 182
+                        , Font.alignLeft
+                        , Element.padding 8
+                        , Element.height <| Element.px <|42
+                        ]
+                    ++ Grid.simple
+                , input =
+                    Color.light
+                    ++ [Element.padding 0]
+                }
+
+        , model
+            |> FilterMultiSelect.viewOptions
+            |> List.map
+                (\string ->
+                    Input.button (Button.simple ++ Tag.simple)
+                        { onPress = Just <| FilterMultiSelectSpecific <| FilterMultiSelect.ToggleSelected <| string
+                        , label = Element.text string
+                        }
+                )
+            |> Element.wrappedRow [ Element.spacing 10 ]
+        ]
+            |> Element.column Grid.simple
+    )
 
 validatedInput : ValidatedInput.Model () ( String, String ) -> (String,Element Msg)
 validatedInput model =
@@ -162,6 +248,7 @@ view msgMapper model =
     , description = "Components have a Model, an Update- and sometimes even a Subscription-function. It takes some time to set them up correctly."
     , items =
         [ filterSelect model.filterSelect
+        , filterMultiSelect model.filterMultiSelect
         , validatedInput model.validatedInput 
         ]
             |> List.map (Tuple.mapSecond (Element.map msgMapper) )
diff --git a/example/src/Example.elm b/example/src/Example.elm
index 332b723..0a0b58a 100644
--- a/example/src/Example.elm
+++ b/example/src/Example.elm
@@ -127,7 +127,8 @@ style =
             Card.simple 
             ++ Color.dark
             ++ Grid.simple
-            ++ [ Element.paddingXY 8 6]
+            ++ [ Element.paddingXY 8 6
+                , Element.height <| Element.px <|54]
         , button = 
             { label = Grid.simple
             , container = Button.simple ++ Color.dark
@@ -137,6 +138,23 @@ style =
         , text = [Element.paddingXY 8 0]
         }
     , layout = Framework.responsiveLayout
+    {--\a w ->
+        Html.div []
+        [ Html.node "meta"
+            [ Attributes.attribute "name" "viewport"
+            , Attributes.attribute "content" "width=device-width, initial-scale=1.0"
+            ]
+            []
+        , Element.layoutWith
+            {options = (Element.focusStyle
+                { borderColor = Nothing
+                , backgroundColor = Nothing
+                , shadow = Nothing
+                }
+                |> List.singleton)
+            }
+         (Framework.layoutAttributes ++ a) <| w
+        ]--}
     , header =
         Framework.container
             ++ Color.dark
diff --git a/example/src/Stateless.elm b/example/src/Stateless.elm
index d946a8c..3dd9347 100644
--- a/example/src/Stateless.elm
+++ b/example/src/Stateless.elm
@@ -21,6 +21,7 @@ import Widget.Button as Button exposing (ButtonStyle)
 import Layout exposing (Part(..))
 import Icons
 import Widget
+import Element.Font as Font
 
 buttonStyle : ButtonStyle msg
 buttonStyle =
@@ -38,13 +39,23 @@ tabButtonStyle=
     , active = Color.primary
     }
 
+chipButton : ButtonStyle msg
+chipButton =
+    { container = Tag.simple
+    , disabled = []
+    , label = Grid.simple
+    , active = Color.primary
+    }
+
 type alias Model =
     { selected : Maybe Int
     , multiSelected : Set Int
+    , chipTextInput : Set String
     , isCollapsed : Bool
     , carousel : Int
     , tab : Maybe Int
     , button : Bool
+    , textInput : String
     }
 
 
@@ -52,19 +63,24 @@ type Msg
     = ChangedSelected Int
     | ChangedMultiSelected Int
     | ToggleCollapsable Bool
+    | ToggleTextInputChip String
     | ChangedTab Int
     | SetCarousel Int
     | ToggleButton Bool
+    | SetTextInput String
+    | Idle
 
 
 init : Model
 init =
     { selected = Nothing
     , multiSelected = Set.empty
+    , chipTextInput = Set.empty
     , isCollapsed = False
     , carousel = 0
     , tab = Just 1
     , button = True
+    , textInput = ""
     }
 
 
@@ -98,6 +114,18 @@ update msg model =
               }
             , Cmd.none
             )
+        
+        ToggleTextInputChip string ->
+            ( { model
+                | chipTextInput =
+                    model.chipTextInput |>
+                        if model.chipTextInput |> Set.member string then
+                            Set.remove string
+                        else
+                            Set.insert string
+                }
+            , Cmd.none
+            )
 
         SetCarousel int ->
             ( if (int < 0) || (int > 3) then
@@ -115,6 +143,12 @@ update msg model =
         
         ToggleButton bool ->
             ( { model | button = bool }, Cmd.none )
+        
+        SetTextInput string ->
+            ( {model | textInput = string },Cmd.none)
+
+        Idle ->
+            ( model, Cmd.none)
 
 
 select : Model -> (String,Element Msg)
@@ -187,29 +221,35 @@ multiSelect model =
 collapsable : Model -> (String,Element Msg)
 collapsable model =
     ( "Collapsable"
-    , Widget.collapsable
-        { onToggle = ToggleCollapsable
+    ,   { onToggle = ToggleCollapsable
         , isCollapsed = model.isCollapsed
         , label =
-            Element.row Grid.compact
+            Element.row (Grid.simple ++ [Element.width<| Element.fill])
                 [ Element.html <|
                     if model.isCollapsed then
                         Heroicons.cheveronRight [ Attributes.width 20 ]
 
                     else
                         Heroicons.cheveronDown [ Attributes.width 20 ]
-                , Element.el Heading.h4 <| Element.text <| "Title"
+                , Element.text <| "Title"
                 ]
         , content = Element.text <| "Hello World"
         }
+        |>Widget.collapsable
+        { containerColumn = Card.simple ++ Grid.simple
+            ++ [ Element.padding 8 ]
+        , button = []
+        }
+        
     )
 
 tab : Model -> (String,Element Msg)
 tab model =
     ( "Tab"
     , Widget.tab 
-            { tabButton = tabButtonStyle
-            , tabRow = Grid.simple
+            { button = tabButtonStyle
+            , optionRow = Grid.simple
+            , containerColumn = Grid.compact
             } 
             { selected = model.tab
             , options = [ 1, 2, 3 ]
@@ -315,6 +355,64 @@ iconButton model =
     ] |> Element.column Grid.simple
     )
 
+textInput : Model -> (String,Element Msg)
+textInput model =
+    ( "Chip Text Input"
+    ,   [ { chips =
+            model.chipTextInput
+            |> Set.toList
+            |> List.map (\string ->
+                    { icon = Element.none
+                    , text = string
+                    , onPress =
+                        string
+                            |> ToggleTextInputChip
+                            |> Just
+                    }
+                )
+        , text = model.textInput
+        , placeholder = Nothing
+        , label = "Chips"
+        , onChange = SetTextInput
+        }
+            |> Widget.textInput
+            { chip = chipButton
+            , chipsRow = 
+                [ Element.width <| Element.shrink
+                , Element.spacing <| 4 ]
+            , containerRow = 
+                Button.simple
+                ++ Color.light
+                ++ [ Border.color <| Element.rgb255 186 189 182
+                    , Font.alignLeft
+                    , Element.padding 8
+                    , Element.height <| Element.px <|42
+                    ]
+                ++ Grid.simple
+            , input =
+                Color.light
+                ++ [Element.padding 0]
+            }
+        , model.chipTextInput
+            |> Set.diff
+                (["A","B","C"]
+                    |> Set.fromList
+                )
+            |> Set.toList
+            |> List.map
+                (\string ->
+                    Input.button (Button.simple ++ Tag.simple)
+                        { onPress =
+                            string
+                            |> ToggleTextInputChip
+                            |> Just
+                        , label = Element.text string
+                        }
+                )
+            |> Element.wrappedRow [ Element.spacing 10 ]
+        ] |> Element.column Grid.simple
+    )
+
 view : 
     { msgMapper : Msg -> msg
     , showDialog : msg
@@ -336,5 +434,6 @@ view { msgMapper, showDialog, changedSheet } model =
         , carousel model |> Tuple.mapSecond (Element.map msgMapper)
         , tab model |> Tuple.mapSecond (Element.map msgMapper)
         , dialog showDialog model
+        , textInput model |> Tuple.mapSecond (Element.map msgMapper)
         ]
     }
diff --git a/src/Widget.elm b/src/Widget.elm
index 05227c6..53e52f6 100644
--- a/src/Widget.elm
+++ b/src/Widget.elm
@@ -1,24 +1,19 @@
 module Widget exposing
     ( select, multiSelect, collapsable, carousel, modal, tab, dialog
-    , Dialog, MultiSelect, Select, selectButton
+    , Dialog, Select, selectButton, textInput
     )
 
 {-| This module contains functions for displaying data.
 
 @docs select, multiSelect, collapsable, carousel, modal, tab, dialog
 
-
-# DEPRECATED
-
-@docs dialog
-
 -}
 
 import Array exposing (Array)
 import Element exposing (Attribute, Element)
 import Element.Background as Background
 import Element.Events as Events
-import Element.Input as Input
+import Element.Input as Input exposing (Placeholder)
 import Set exposing (Set)
 import Widget.Button as Button exposing (Button, ButtonStyle, TextButton)
 
@@ -34,17 +29,6 @@ type alias Select msg =
     }
 
 
-type alias MultiSelect msg =
-    { selected : Set Int
-    , options :
-        List
-            { text : String
-            , icon : Element Never
-            }
-    , onSelect : Int -> Maybe msg
-    }
-
-
 type alias Dialog msg =
     { title : Maybe String
     , body : Element msg
@@ -95,7 +79,14 @@ select { selected, options, onSelect } =
 {-| Selects multible options. This can be used for checkboxes.
 -}
 multiSelect :
-    MultiSelect msg
+    { selected : Set Int
+    , options :
+        List
+            { text : String
+            , icon : Element Never
+            }
+    , onSelect : Int -> Maybe msg
+    }
     -> List ( Bool, Button msg )
 multiSelect { selected, options, onSelect } =
     options
@@ -110,18 +101,51 @@ multiSelect { selected, options, onSelect } =
             )
 
 
+{-| -}
+textInput :
+    { chip : ButtonStyle msg
+    , containerRow : List (Attribute msg)
+    , chipsRow : List (Attribute msg)
+    , input : List (Attribute msg)
+    }
+    ->
+        { chips : List (Button msg)
+        , text : String
+        , placeholder : Maybe (Placeholder msg)
+        , label : String
+        , onChange : String -> msg
+        }
+    -> Element msg
+textInput style { chips, placeholder, label, text, onChange } =
+    Element.row style.containerRow
+        [ chips
+            |> List.map (Button.view style.chip)
+            |> Element.row style.chipsRow
+        , Input.text style.input
+            { onChange = onChange
+            , text = text
+            , placeholder = placeholder
+            , label = Input.labelHidden label
+            }
+        ]
+
+
 {-| Some collapsable content.
 -}
 collapsable :
-    { onToggle : Bool -> msg
-    , isCollapsed : Bool
-    , label : Element msg
-    , content : Element msg
+    { containerColumn : List (Attribute msg)
+    , button : List (Attribute msg)
     }
+    ->
+        { onToggle : Bool -> msg
+        , isCollapsed : Bool
+        , label : Element msg
+        , content : Element msg
+        }
     -> Element msg
-collapsable { onToggle, isCollapsed, label, content } =
-    Element.column [] <|
-        [ Input.button []
+collapsable style { onToggle, isCollapsed, label, content } =
+    Element.column style.containerColumn <|
+        [ Input.button style.button
             { onPress = Just <| onToggle <| not isCollapsed
             , label = label
             }
@@ -137,9 +161,9 @@ collapsable { onToggle, isCollapsed, label, content } =
 {-| Displayes a list of contents in a tab
 -}
 tab :
-    { style
-        | tabButton : ButtonStyle msg
-        , tabRow : List (Attribute msg)
+    { button : ButtonStyle msg
+    , optionRow : List (Attribute msg)
+    , containerColumn : List (Attribute msg)
     }
     -> Select msg
     -> (Maybe Int -> Element msg)
@@ -147,12 +171,12 @@ tab :
 tab style options content =
     [ options
         |> select
-        |> List.map (selectButton style.tabButton)
-        |> Element.row style.tabRow
+        |> List.map (selectButton style.button)
+        |> Element.row style.optionRow
     , options.selected
         |> content
     ]
-        |> Element.column []
+        |> Element.column style.containerColumn
 
 
 dialog :
diff --git a/src/Widget/FilterMultiSelect.elm b/src/Widget/FilterMultiSelect.elm
new file mode 100644
index 0000000..da4f13e
--- /dev/null
+++ b/src/Widget/FilterMultiSelect.elm
@@ -0,0 +1,108 @@
+module Widget.FilterMultiSelect exposing (Model, Msg(..), init, update, viewInput, viewOptions)
+
+{-|
+
+@docs Model, Msg, init, update, viewInput, viewOptions
+
+-}
+
+import Element.Input exposing (Placeholder)
+import Set exposing (Set)
+import Widget.Button exposing (Button)
+
+
+{-| The Model containing the raw value, the selected value and all the possible options.
+-}
+type alias Model =
+    { raw : String
+    , selected : Set String
+    , options : Set String
+    }
+
+
+{-| The Msg is exposed by design. You can unselect by sending `Selected Nothing`.
+-}
+type Msg
+    = ChangedRaw String
+    | ToggleSelected String
+
+
+{-| The initial state contains the set of possible options.
+-}
+init : Set String -> Model
+init options =
+    { raw = ""
+    , selected = Set.empty
+    , options = options
+    }
+
+
+{-| Updates the Model
+-}
+update : Msg -> Model -> Model
+update msg model =
+    case msg of
+        ChangedRaw string ->
+            { model
+                | raw = string
+            }
+
+        ToggleSelected string ->
+            if model.selected |> Set.member string then
+                { model
+                    | selected = model.selected |> Set.remove string
+                }
+
+            else
+                { model
+                    | selected = model.selected |> Set.insert string
+                    , raw = ""
+                }
+
+
+{-| A wrapper around Input.text.
+-}
+viewInput :
+    Model
+    ->
+        { msgMapper : Msg -> msg
+        , placeholder : Maybe (Placeholder msg)
+        , label : String
+        , toChip : String -> Button msg
+        }
+    ->
+        { chips : List (Button msg)
+        , text : String
+        , placeholder : Maybe (Placeholder msg)
+        , label : String
+        , onChange : String -> msg
+        }
+viewInput model { msgMapper, placeholder, label, toChip } =
+    { chips =
+        model.selected
+            |> Set.toList
+            |> List.map toChip
+    , text = model.raw
+    , placeholder = placeholder
+    , label = label
+    , onChange = ChangedRaw >> msgMapper
+    }
+
+
+{-| Returns a List of all options that matches the filter.
+-}
+viewOptions : Model -> List String
+viewOptions { raw, options, selected } =
+    if raw == "" then
+        []
+
+    else
+        options
+            |> Set.filter (String.toUpper >> String.contains (raw |> String.toUpper))
+            |> Set.filter
+                (\string ->
+                    selected
+                        |> Set.member string
+                        |> not
+                )
+            |> Set.toList
diff --git a/src/Widget/Snackbar.elm b/src/Widget/Snackbar.elm
index 464f3fd..a4d90d0 100644
--- a/src/Widget/Snackbar.elm
+++ b/src/Widget/Snackbar.elm
@@ -20,7 +20,6 @@ module Widget.Snackbar exposing
 
 import Element exposing (Attribute, Element)
 import Queue exposing (Queue)
-import Widget
 import Widget.Button as Button exposing (ButtonStyle, TextButton)