mirror of
https://github.com/Orasund/elm-ui-widgets.git
synced 2024-11-22 04:58:49 +03:00
Hello World
This commit is contained in:
parent
837ee316d8
commit
d4a7221f15
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# elm-package generated files
|
||||||
|
elm-stuff
|
||||||
|
# elm-repl generated files
|
||||||
|
repl-temp-*
|
29
LICENSE
Normal file
29
LICENSE
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2019, Orasund
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
17
README.md
17
README.md
@ -1,2 +1,15 @@
|
|||||||
# elm-widgets
|
# Elm-Ui-Widgets
|
||||||
Collection of reusable views for elm-ui.
|
|
||||||
|
Usefull Widgets written in for Elm-ui.
|
||||||
|
These include:
|
||||||
|
|
||||||
|
* Select
|
||||||
|
* Multi Select
|
||||||
|
* Collapsable
|
||||||
|
* Dialog
|
||||||
|
* Carousel
|
||||||
|
* Snackbar
|
||||||
|
* Sort Table
|
||||||
|
* Filter Select
|
||||||
|
* Validated Input
|
||||||
|
* Scrolling Nav
|
13358
docs/index.html
Normal file
13358
docs/index.html
Normal file
File diff suppressed because it is too large
Load Diff
BIN
docs/select.png
Normal file
BIN
docs/select.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 933 B |
29
elm.json
Normal file
29
elm.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"type": "package",
|
||||||
|
"name": "Orasund/elm-ui-widgets",
|
||||||
|
"summary": "Collection of reusable views for elm-ui.",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"exposed-modules": [
|
||||||
|
"View",
|
||||||
|
"View.FilterSelect",
|
||||||
|
"View.ValidatedInput",
|
||||||
|
"View.WrappedColumn"
|
||||||
|
],
|
||||||
|
"elm-version": "0.19.0 <= v < 0.20.0",
|
||||||
|
"dependencies": {
|
||||||
|
"Orasund/elm-ui-framework": "1.6.1 <= v < 2.0.0",
|
||||||
|
"elm/browser": "1.0.2 <= v < 2.0.0",
|
||||||
|
"elm/core": "1.0.0 <= v < 2.0.0",
|
||||||
|
"elm/html": "1.0.0 <= v < 2.0.0",
|
||||||
|
"elm/time": "1.0.0 <= v < 2.0.0",
|
||||||
|
"elm-community/intdict": "3.0.0 <= v < 4.0.0",
|
||||||
|
"jasonliang512/elm-heroicons": "1.0.2 <= v < 2.0.0",
|
||||||
|
"mdgriffith/elm-ui": "1.1.5 <= v < 2.0.0",
|
||||||
|
"turboMaCk/queue": "1.0.2 <= v < 2.0.0",
|
||||||
|
"wernerdegroot/listzipper": "4.0.0 <= v < 5.0.0"
|
||||||
|
},
|
||||||
|
"test-dependencies": {
|
||||||
|
"elm-explorations/test": "1.2.1 <= v < 2.0.0"
|
||||||
|
}
|
||||||
|
}
|
32
example/elm.json
Normal file
32
example/elm.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"type": "application",
|
||||||
|
"source-directories": [
|
||||||
|
"src",
|
||||||
|
"../src"
|
||||||
|
],
|
||||||
|
"elm-version": "0.19.1",
|
||||||
|
"dependencies": {
|
||||||
|
"direct": {
|
||||||
|
"Orasund/elm-ui-framework": "1.6.1",
|
||||||
|
"elm/browser": "1.0.2",
|
||||||
|
"elm/core": "1.0.5",
|
||||||
|
"elm/html": "1.0.0",
|
||||||
|
"elm/time": "1.0.0",
|
||||||
|
"elm-community/intdict": "3.0.0",
|
||||||
|
"jasonliang512/elm-heroicons": "1.0.2",
|
||||||
|
"mdgriffith/elm-ui": "1.1.5",
|
||||||
|
"turboMaCk/queue": "1.0.2",
|
||||||
|
"wernerdegroot/listzipper": "4.0.0"
|
||||||
|
},
|
||||||
|
"indirect": {
|
||||||
|
"elm/json": "1.1.3",
|
||||||
|
"elm/svg": "1.0.1",
|
||||||
|
"elm/url": "1.0.0",
|
||||||
|
"elm/virtual-dom": "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test-dependencies": {
|
||||||
|
"direct": {},
|
||||||
|
"indirect": {}
|
||||||
|
}
|
||||||
|
}
|
179
example/src/Component.elm
Normal file
179
example/src/Component.elm
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
module Component exposing (Model,Msg(..),init,update,view)
|
||||||
|
|
||||||
|
import Browser
|
||||||
|
import Element exposing (Element,Color)
|
||||||
|
import Element.Input as Input
|
||||||
|
import Element.Background as Background
|
||||||
|
import Framework
|
||||||
|
import Framework.Button as Button
|
||||||
|
import Framework.Card as Card
|
||||||
|
import Framework.Color as Color
|
||||||
|
import Framework.Grid as Grid
|
||||||
|
import Framework.Group as Group
|
||||||
|
import Framework.Heading as Heading
|
||||||
|
import Framework.Input as Input
|
||||||
|
import Framework.Tag as Tag
|
||||||
|
import Html exposing (Html)
|
||||||
|
import Html.Attributes as Attributes
|
||||||
|
import Set exposing (Set)
|
||||||
|
import Widget
|
||||||
|
import Widget.FilterSelect as FilterSelect
|
||||||
|
import Widget.ScrollingNav as ScrollingNav
|
||||||
|
import Widget.ValidatedInput as ValidatedInput
|
||||||
|
import Widget.Snackbar as Snackbar
|
||||||
|
import Time
|
||||||
|
import Heroicons.Solid as Heroicons
|
||||||
|
|
||||||
|
type alias Model =
|
||||||
|
{ filterSelect : FilterSelect.Model
|
||||||
|
, validatedInput : ValidatedInput.Model () (( String, String ))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Msg
|
||||||
|
= FilterSelectSpecific FilterSelect.Msg
|
||||||
|
| ValidatedInputSpecific ValidatedInput.Msg
|
||||||
|
|
||||||
|
|
||||||
|
init : Model
|
||||||
|
init =
|
||||||
|
|
||||||
|
{ filterSelect =
|
||||||
|
[ "Apple"
|
||||||
|
, "Kiwi"
|
||||||
|
, "Strawberry"
|
||||||
|
, "Pineapple"
|
||||||
|
, "Mango"
|
||||||
|
, "Grapes"
|
||||||
|
, "Watermelon"
|
||||||
|
, "Orange"
|
||||||
|
, "Lemon"
|
||||||
|
, "Blueberry"
|
||||||
|
, "Grapefruit"
|
||||||
|
, "Coconut"
|
||||||
|
, "Cherry"
|
||||||
|
, "Banana"
|
||||||
|
]
|
||||||
|
|> Set.fromList
|
||||||
|
|> FilterSelect.init
|
||||||
|
, validatedInput =
|
||||||
|
ValidatedInput.init
|
||||||
|
{ value = ("John","Doe")
|
||||||
|
, validator =
|
||||||
|
\string ->
|
||||||
|
case string |> String.split " " of
|
||||||
|
[ first, second ] ->
|
||||||
|
Ok (( first, second ))
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
Err ()
|
||||||
|
, toString =
|
||||||
|
(\( first, second ) -> first ++ " " ++ second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||||
|
update msg model =
|
||||||
|
case msg of
|
||||||
|
FilterSelectSpecific m ->
|
||||||
|
( { model
|
||||||
|
| filterSelect = model.filterSelect |> FilterSelect.update m
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
ValidatedInputSpecific m ->
|
||||||
|
( { model
|
||||||
|
| validatedInput = model.validatedInput |> ValidatedInput.update m
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
filterSelect : FilterSelect.Model -> Element Msg
|
||||||
|
filterSelect model =
|
||||||
|
Element.column (Grid.simple ++ Card.small) <|
|
||||||
|
[ Element.el Heading.h3 <| Element.text "Filter Select"
|
||||||
|
, case model.selected of
|
||||||
|
Just selected ->
|
||||||
|
Element.row Grid.compact
|
||||||
|
[ Element.el (Tag.simple ++ Group.left) <| Element.text <| selected
|
||||||
|
, Input.button (Tag.simple ++ Group.right ++ Color.danger)
|
||||||
|
{ onPress = Just <| FilterSelectSpecific <| FilterSelect.Selected Nothing
|
||||||
|
, label = Element.html <| Heroicons.x [Attributes.width 16]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
Element.column Grid.simple
|
||||||
|
[ FilterSelect.viewInput Input.simple
|
||||||
|
model
|
||||||
|
{ msgMapper = FilterSelectSpecific
|
||||||
|
, placeholder = Just <| Input.placeholder [] <| Element.text <|
|
||||||
|
"Fruit"
|
||||||
|
, label = "Fruit"
|
||||||
|
}
|
||||||
|
, model
|
||||||
|
|> FilterSelect.viewOptions
|
||||||
|
|> List.map
|
||||||
|
(\string ->
|
||||||
|
Input.button (Button.simple ++ Tag.simple)
|
||||||
|
{ onPress = Just <| FilterSelectSpecific <| FilterSelect.Selected <| Just <| string
|
||||||
|
, label = Element.text string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|> Element.wrappedRow [ Element.spacing 10 ]
|
||||||
|
]
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
validatedInput : ValidatedInput.Model () ( ( String, String )) -> Element Msg
|
||||||
|
validatedInput model =
|
||||||
|
Element.column (Grid.simple ++ Card.small) <|
|
||||||
|
[ Element.el Heading.h3 <| Element.text "Validated Input"
|
||||||
|
, ValidatedInput.view Input.simple
|
||||||
|
model
|
||||||
|
{ label = "First Name, Sir Name"
|
||||||
|
, msgMapper = ValidatedInputSpecific
|
||||||
|
, placeholder = Nothing
|
||||||
|
, readOnly = \maybeTuple ->
|
||||||
|
Element.row Grid.compact
|
||||||
|
[ maybeTuple
|
||||||
|
|> (\(a,b) -> a ++ " " ++ b)
|
||||||
|
|> Element.text
|
||||||
|
|
||||||
|
|> Element.el (Tag.simple ++ Group.left)
|
||||||
|
, Heroicons.pencil [Attributes.width 16]
|
||||||
|
|> Element.html
|
||||||
|
|>Element.el (Tag.simple ++ Group.right ++ Color.primary)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
scrollingNavCard : Element msg
|
||||||
|
scrollingNavCard =
|
||||||
|
[Element.el Heading.h3 <| Element.text "Scrolling Nav"
|
||||||
|
, Element.text "Resize the screen and use the scrollbar to see the scrolling navigation in action."
|
||||||
|
|> List.singleton
|
||||||
|
|> Element.paragraph []
|
||||||
|
]
|
||||||
|
|> Element.column (Grid.simple ++ Card.small)
|
||||||
|
|
||||||
|
view : Model -> Element Msg
|
||||||
|
view model =
|
||||||
|
Element.column (Grid.section ++ [Element.centerX])
|
||||||
|
[ Element.el Heading.h2 <| Element.text "Components"
|
||||||
|
, "Components have a Model, an Update- and sometimes even a Subscription-function. It takes some time to set them up correctly."
|
||||||
|
|> Element.text
|
||||||
|
|> List.singleton
|
||||||
|
|> Element.paragraph []
|
||||||
|
, Element.wrappedRow (Grid.simple ++ [Element.centerX])
|
||||||
|
<|
|
||||||
|
[ filterSelect model.filterSelect
|
||||||
|
, validatedInput model.validatedInput
|
||||||
|
, scrollingNavCard
|
||||||
|
]
|
||||||
|
]
|
274
example/src/Example.elm
Normal file
274
example/src/Example.elm
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
module Example exposing (main)
|
||||||
|
|
||||||
|
import Browser
|
||||||
|
import Element exposing (Element)
|
||||||
|
import Element.Input as Input
|
||||||
|
import Framework
|
||||||
|
import Framework.Button as Button
|
||||||
|
import Framework.Card as Card
|
||||||
|
import Framework.Color as Color
|
||||||
|
import Framework.Grid as Grid
|
||||||
|
import Framework.Group as Group
|
||||||
|
import Framework.Heading as Heading
|
||||||
|
import Framework.Input as Input
|
||||||
|
import Framework.Tag as Tag
|
||||||
|
import Html exposing (Html)
|
||||||
|
import Html.Attributes as Attributes
|
||||||
|
import Set exposing (Set)
|
||||||
|
import Widget
|
||||||
|
import Widget.FilterSelect as FilterSelect
|
||||||
|
import Widget.ScrollingNav as ScrollingNav
|
||||||
|
import Widget.ValidatedInput as ValidatedInput
|
||||||
|
import Widget.Snackbar as Snackbar
|
||||||
|
import Stateless
|
||||||
|
import Reusable
|
||||||
|
import Component
|
||||||
|
import Time
|
||||||
|
|
||||||
|
type Section
|
||||||
|
= ComponentViews
|
||||||
|
| ReusableViews
|
||||||
|
| StatelessViews
|
||||||
|
|
||||||
|
type alias Model =
|
||||||
|
{ component : Component.Model
|
||||||
|
, stateless : Stateless.Model
|
||||||
|
, reusable : Reusable.Model
|
||||||
|
, scrollingNav : ScrollingNav.Model Section
|
||||||
|
, snackbar : Snackbar.Model String
|
||||||
|
, displayDialog : Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Msg
|
||||||
|
= StatelessSpecific Stateless.Msg
|
||||||
|
| ReusableSpecific Reusable.Msg
|
||||||
|
| ComponentSpecific Component.Msg
|
||||||
|
| ScrollingNavSpecific (ScrollingNav.Msg Section)
|
||||||
|
| TimePassed Int
|
||||||
|
| AddSnackbar String
|
||||||
|
| ToggleDialog Bool
|
||||||
|
|
||||||
|
|
||||||
|
init : () -> ( Model, Cmd Msg )
|
||||||
|
init () =
|
||||||
|
let
|
||||||
|
( scrollingNav, cmd ) =
|
||||||
|
ScrollingNav.init
|
||||||
|
{ labels =
|
||||||
|
\section ->
|
||||||
|
case section of
|
||||||
|
ComponentViews ->
|
||||||
|
"Component Views"
|
||||||
|
ReusableViews ->
|
||||||
|
"Reusable Views"
|
||||||
|
|
||||||
|
StatelessViews ->
|
||||||
|
"Stateless Views"
|
||||||
|
, arrangement = [ StatelessViews, ReusableViews, ComponentViews ]
|
||||||
|
}
|
||||||
|
in
|
||||||
|
({ component = Component.init
|
||||||
|
, stateless = Stateless.init
|
||||||
|
, reusable = Reusable.init
|
||||||
|
, scrollingNav = scrollingNav
|
||||||
|
, snackbar = Snackbar.init
|
||||||
|
, displayDialog = False
|
||||||
|
}
|
||||||
|
|
||||||
|
, cmd |> Cmd.map ScrollingNavSpecific
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
view : Model -> Html Msg
|
||||||
|
view model =
|
||||||
|
[ Element.el [ Element.height <| Element.px <| 42 ] <| Element.none
|
||||||
|
, [ Element.el Heading.h1 <| Element.text "Elm-Ui-Widgets"
|
||||||
|
, model.scrollingNav
|
||||||
|
|> ScrollingNav.view
|
||||||
|
(\section ->
|
||||||
|
case section of
|
||||||
|
ComponentViews ->
|
||||||
|
model.component
|
||||||
|
|> Component.view
|
||||||
|
|> Element.map ComponentSpecific
|
||||||
|
ReusableViews ->
|
||||||
|
Reusable.view
|
||||||
|
{addSnackbar = AddSnackbar
|
||||||
|
,model = model.reusable
|
||||||
|
,msgMapper = ReusableSpecific
|
||||||
|
}
|
||||||
|
|
||||||
|
StatelessViews ->
|
||||||
|
Stateless.view
|
||||||
|
{ msgMapper = StatelessSpecific
|
||||||
|
, showDialog = ToggleDialog True
|
||||||
|
}
|
||||||
|
model.stateless
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|> Element.column Framework.container
|
||||||
|
]
|
||||||
|
|> Element.column Grid.compact
|
||||||
|
|> Framework.responsiveLayout
|
||||||
|
[ Element.inFront <|
|
||||||
|
(model.scrollingNav
|
||||||
|
|> ScrollingNav.viewSections
|
||||||
|
{ fromString =
|
||||||
|
\string ->
|
||||||
|
case string of
|
||||||
|
"Component Views" ->
|
||||||
|
Just ComponentViews
|
||||||
|
"Reusable Views" ->
|
||||||
|
Just ReusableViews
|
||||||
|
|
||||||
|
"Stateless Views" ->
|
||||||
|
Just StatelessViews
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
Nothing
|
||||||
|
, label = Element.text
|
||||||
|
, msgMapper = ScrollingNavSpecific
|
||||||
|
}
|
||||||
|
|> Widget.select
|
||||||
|
|> List.map (\(config,selected) ->
|
||||||
|
Input.button (Button.simple
|
||||||
|
++ Group.center
|
||||||
|
++ (if selected then
|
||||||
|
Color.primary
|
||||||
|
|
||||||
|
else
|
||||||
|
Color.dark
|
||||||
|
))
|
||||||
|
config
|
||||||
|
)
|
||||||
|
|> Element.row
|
||||||
|
(Color.dark ++ Grid.compact)
|
||||||
|
|> Element.el
|
||||||
|
(Framework.container
|
||||||
|
++ [ Element.padding <| 0
|
||||||
|
, Element.centerX
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|> Element.el
|
||||||
|
(Color.dark
|
||||||
|
++ [ Element.alignTop
|
||||||
|
, Element.height <| Element.px <| 42
|
||||||
|
, Element.width <| Element.fill
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
, Element.inFront <|
|
||||||
|
( model.snackbar
|
||||||
|
|> Snackbar.current
|
||||||
|
|> Maybe.map
|
||||||
|
(Element.text >>
|
||||||
|
List.singleton >>
|
||||||
|
Element.paragraph (Card.simple ++ Color.dark)
|
||||||
|
>> Element.el [Element.padding 8,Element.alignBottom
|
||||||
|
, Element.alignRight]
|
||||||
|
|
||||||
|
|
||||||
|
)
|
||||||
|
|> Maybe.withDefault Element.none
|
||||||
|
)
|
||||||
|
, Element.inFront <|
|
||||||
|
if model.displayDialog then
|
||||||
|
Widget.dialog {
|
||||||
|
onDismiss = Just <| ToggleDialog False
|
||||||
|
,content =
|
||||||
|
[ Element.el Heading.h3 <| Element.text "Dialog"
|
||||||
|
, "This is a dialog window"
|
||||||
|
|> Element.text
|
||||||
|
|> List.singleton
|
||||||
|
|> Element.paragraph []
|
||||||
|
, Input.button (Button.simple ++ [Element.alignRight])
|
||||||
|
{onPress = Just <| ToggleDialog False
|
||||||
|
, label = Element.text "Ok"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|>Element.column (Grid.simple ++ Card.large)
|
||||||
|
} else Element.none
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||||
|
update msg model =
|
||||||
|
case msg of
|
||||||
|
ComponentSpecific m ->
|
||||||
|
model.component
|
||||||
|
|> Component.update m
|
||||||
|
|> Tuple.mapBoth
|
||||||
|
(\component ->
|
||||||
|
{ model
|
||||||
|
| component = component
|
||||||
|
}
|
||||||
|
)
|
||||||
|
(Cmd.map ComponentSpecific)
|
||||||
|
|
||||||
|
ReusableSpecific m ->
|
||||||
|
(model.reusable
|
||||||
|
|> Reusable.update m
|
||||||
|
|> (\reusable ->
|
||||||
|
{ model
|
||||||
|
| reusable = reusable
|
||||||
|
}
|
||||||
|
),Cmd.none)
|
||||||
|
|
||||||
|
StatelessSpecific m ->
|
||||||
|
model.stateless
|
||||||
|
|> Stateless.update m
|
||||||
|
|> Tuple.mapBoth
|
||||||
|
(\stateless ->
|
||||||
|
{ model
|
||||||
|
| stateless = stateless
|
||||||
|
}
|
||||||
|
)
|
||||||
|
(Cmd.map StatelessSpecific)
|
||||||
|
|
||||||
|
|
||||||
|
ScrollingNavSpecific m ->
|
||||||
|
model.scrollingNav
|
||||||
|
|> ScrollingNav.update m
|
||||||
|
|> Tuple.mapBoth
|
||||||
|
(\scrollingNav ->
|
||||||
|
{ model
|
||||||
|
| scrollingNav = scrollingNav
|
||||||
|
}
|
||||||
|
)
|
||||||
|
(Cmd.map ScrollingNavSpecific)
|
||||||
|
|
||||||
|
TimePassed int ->
|
||||||
|
({ model
|
||||||
|
| snackbar = model.snackbar |> Snackbar.timePassed int
|
||||||
|
},Cmd.none)
|
||||||
|
|
||||||
|
AddSnackbar string ->
|
||||||
|
( { model | snackbar = model.snackbar |> Snackbar.insert string}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
ToggleDialog bool ->
|
||||||
|
( { model | displayDialog = bool }
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
subscriptions : Model -> Sub Msg
|
||||||
|
subscriptions model=
|
||||||
|
Sub.batch
|
||||||
|
[ScrollingNav.subscriptions
|
||||||
|
|> Sub.map ScrollingNavSpecific
|
||||||
|
, Time.every 500 (always ( TimePassed 500))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
main : Program () Model Msg
|
||||||
|
main =
|
||||||
|
Browser.element
|
||||||
|
{ init = init
|
||||||
|
, view = view
|
||||||
|
, update = update
|
||||||
|
, subscriptions = subscriptions
|
||||||
|
}
|
144
example/src/Reusable.elm
Normal file
144
example/src/Reusable.elm
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
module Reusable exposing (Model,Msg,init,view,update)
|
||||||
|
|
||||||
|
import Browser
|
||||||
|
import Element exposing (Element,Color)
|
||||||
|
import Element.Input as Input
|
||||||
|
import Element.Background as Background
|
||||||
|
import Framework
|
||||||
|
import Framework.Button as Button
|
||||||
|
import Framework.Card as Card
|
||||||
|
import Framework.Color as Color
|
||||||
|
import Framework.Grid as Grid
|
||||||
|
import Framework.Group as Group
|
||||||
|
import Framework.Heading as Heading
|
||||||
|
import Framework.Input as Input
|
||||||
|
import Framework.Tag as Tag
|
||||||
|
import Html exposing (Html)
|
||||||
|
import Html.Attributes as Attributes
|
||||||
|
import Set exposing (Set)
|
||||||
|
import Widget
|
||||||
|
import Widget.FilterSelect as FilterSelect
|
||||||
|
import Widget.ScrollingNav as ScrollingNav
|
||||||
|
import Widget.ValidatedInput as ValidatedInput
|
||||||
|
import Widget.Snackbar as Snackbar
|
||||||
|
import Widget.SortTable as SortTable
|
||||||
|
import Time
|
||||||
|
import Heroicons.Solid as Heroicons
|
||||||
|
import Element.Font as Font
|
||||||
|
|
||||||
|
type alias Model =
|
||||||
|
SortTable.Model
|
||||||
|
|
||||||
|
type Msg =
|
||||||
|
SortBy {title : String, asc : Bool }
|
||||||
|
|
||||||
|
type alias Item =
|
||||||
|
{name : String
|
||||||
|
,amount : Int
|
||||||
|
,price : Float
|
||||||
|
}
|
||||||
|
|
||||||
|
update : Msg -> Model -> Model
|
||||||
|
update msg model =
|
||||||
|
case msg of
|
||||||
|
SortBy m ->
|
||||||
|
m
|
||||||
|
|
||||||
|
init : Model
|
||||||
|
init =
|
||||||
|
SortTable.sortBy {title="Name",asc=True}
|
||||||
|
|
||||||
|
snackbar : (String -> msg) -> Element msg
|
||||||
|
snackbar addSnackbar =
|
||||||
|
[ Element.el Heading.h3 <| Element.text "Snackbar"
|
||||||
|
, Input.button Button.simple
|
||||||
|
{ onPress = Just <| addSnackbar "This is a notification. It will disappear after 10 seconds."
|
||||||
|
, label = "Add Notification"
|
||||||
|
|> Element.text
|
||||||
|
|> List.singleton
|
||||||
|
|> Element.paragraph []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|> Element.column (Grid.simple ++ Card.small)
|
||||||
|
|
||||||
|
sortTable : SortTable.Model -> Element Msg
|
||||||
|
sortTable model =
|
||||||
|
[ Element.el Heading.h3 <| Element.text "Sort Table"
|
||||||
|
, SortTable.view
|
||||||
|
{ content =
|
||||||
|
[ {id = 1, name = "Antonio", rating = 2.456}
|
||||||
|
, {id = 2, name = "Ana", rating = 1.34}
|
||||||
|
, {id = 3, name = "Alfred", rating = 4.22}
|
||||||
|
, {id = 4, name = "Thomas", rating = 3 }
|
||||||
|
]
|
||||||
|
, columns =
|
||||||
|
[ SortTable.intColumn
|
||||||
|
{ title = "Id"
|
||||||
|
, value = .id
|
||||||
|
, toString = \int -> "#" ++ String.fromInt int
|
||||||
|
}
|
||||||
|
, SortTable.stringColumn
|
||||||
|
{ title = "Name"
|
||||||
|
, value = .name
|
||||||
|
, toString = identity
|
||||||
|
}
|
||||||
|
, SortTable.floatColumn
|
||||||
|
{ title = "rating"
|
||||||
|
, value = .rating
|
||||||
|
, toString = String.fromFloat
|
||||||
|
}
|
||||||
|
]
|
||||||
|
, model = model
|
||||||
|
}
|
||||||
|
|> (\{data,columns} ->
|
||||||
|
{data = data
|
||||||
|
,columns = columns
|
||||||
|
|> List.map (\config->
|
||||||
|
{ header =
|
||||||
|
Input.button [Font.bold]
|
||||||
|
{ onPress = {title = config.header
|
||||||
|
,asc = if config.header == model.title then
|
||||||
|
not model.asc
|
||||||
|
else
|
||||||
|
True
|
||||||
|
}|> SortBy |> Just
|
||||||
|
, label =
|
||||||
|
if config.header == model.title then
|
||||||
|
[config.header |> Element.text
|
||||||
|
, Element.html <|if model.asc then
|
||||||
|
Heroicons.cheveronUp [Attributes.width 16]
|
||||||
|
|
||||||
|
else
|
||||||
|
Heroicons.cheveronDown [Attributes.width 16]
|
||||||
|
]
|
||||||
|
|> Element.row (Grid.simple ++ [Font.bold])
|
||||||
|
else
|
||||||
|
|
||||||
|
config.header |> Element.text
|
||||||
|
}
|
||||||
|
|
||||||
|
, view = config.view >> Element.text
|
||||||
|
, width = Element.fill
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|> Element.table Grid.simple
|
||||||
|
]
|
||||||
|
|> Element.column (Grid.simple ++ Card.small)
|
||||||
|
|
||||||
|
view : { addSnackbar : String -> msg
|
||||||
|
, msgMapper : Msg -> msg
|
||||||
|
, model : Model } -> Element msg
|
||||||
|
view {addSnackbar,msgMapper,model} =
|
||||||
|
Element.column (Grid.section ++ [Element.centerX])
|
||||||
|
[ Element.el Heading.h2 <| Element.text "Reusable Views"
|
||||||
|
, "Reusable views have an internal state but no update function. You will need to do some wiring, but nothing complicated."
|
||||||
|
|> Element.text
|
||||||
|
|> List.singleton
|
||||||
|
|> Element.paragraph []
|
||||||
|
, Element.wrappedRow (Grid.simple ++ [Element.centerX])
|
||||||
|
<|
|
||||||
|
[ snackbar addSnackbar
|
||||||
|
, sortTable model |> Element.map msgMapper
|
||||||
|
]
|
||||||
|
]
|
219
example/src/Stateless.elm
Normal file
219
example/src/Stateless.elm
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
module Stateless exposing (Model,Msg,init,update,view)
|
||||||
|
|
||||||
|
import Element exposing (Element)
|
||||||
|
import Element.Input as Input
|
||||||
|
import Element.Background as Background
|
||||||
|
import Set exposing (Set)
|
||||||
|
import Framework.Grid as Grid
|
||||||
|
import Framework.Button as Button
|
||||||
|
import Framework.Card as Card
|
||||||
|
import Framework.Color as Color
|
||||||
|
import Framework.Group as Group
|
||||||
|
import Framework.Heading as Heading
|
||||||
|
import Framework.Input as Input
|
||||||
|
import Framework.Tag as Tag
|
||||||
|
import Heroicons.Solid as Heroicons
|
||||||
|
import Widget
|
||||||
|
import Html exposing (Html)
|
||||||
|
import Html.Attributes as Attributes
|
||||||
|
import Array exposing (Array)
|
||||||
|
|
||||||
|
type alias Model =
|
||||||
|
{ selected : Maybe Int
|
||||||
|
, multiSelected : Set Int
|
||||||
|
, isCollapsed : Bool
|
||||||
|
, carousel : Int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Msg =
|
||||||
|
ChangedSelected Int
|
||||||
|
| ChangedMultiSelected Int
|
||||||
|
| ToggleCollapsable Bool
|
||||||
|
| SetCarousel Int
|
||||||
|
|
||||||
|
init : Model
|
||||||
|
init =
|
||||||
|
{ selected = Nothing
|
||||||
|
, multiSelected = Set.empty
|
||||||
|
, isCollapsed = False
|
||||||
|
, carousel = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
update : Msg -> Model -> (Model,Cmd Msg)
|
||||||
|
update msg model =
|
||||||
|
case msg of
|
||||||
|
ChangedSelected int ->
|
||||||
|
( { model
|
||||||
|
| selected = Just int
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
ChangedMultiSelected int ->
|
||||||
|
( { model
|
||||||
|
| multiSelected =
|
||||||
|
model.multiSelected |>
|
||||||
|
if model.multiSelected |> Set.member int then
|
||||||
|
Set.remove int
|
||||||
|
else
|
||||||
|
Set.insert int
|
||||||
|
}
|
||||||
|
, Cmd.none )
|
||||||
|
|
||||||
|
ToggleCollapsable bool ->
|
||||||
|
( { model
|
||||||
|
| isCollapsed = bool
|
||||||
|
}
|
||||||
|
, Cmd.none)
|
||||||
|
|
||||||
|
SetCarousel int ->
|
||||||
|
( if (int < 0) || (int > 3) then
|
||||||
|
model
|
||||||
|
else
|
||||||
|
{model
|
||||||
|
| carousel = int
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
select : Model -> Element Msg
|
||||||
|
select model =
|
||||||
|
[ Element.el Heading.h3 <| Element.text "Select"
|
||||||
|
, Widget.select
|
||||||
|
{ selected = model.selected
|
||||||
|
, options = [ 1, 2, 42 ]
|
||||||
|
, label = String.fromInt >> Element.text
|
||||||
|
, onChange = ChangedSelected
|
||||||
|
}
|
||||||
|
|> List.indexedMap (\i (config,selected)->
|
||||||
|
Input.button
|
||||||
|
(Button.simple
|
||||||
|
++ (if i == 0 then
|
||||||
|
Group.left
|
||||||
|
else if i == 2 then
|
||||||
|
Group.right
|
||||||
|
else
|
||||||
|
Group.center)
|
||||||
|
++ (if selected then
|
||||||
|
Color.primary
|
||||||
|
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
config
|
||||||
|
)
|
||||||
|
|> Element.row Grid.compact
|
||||||
|
]
|
||||||
|
|> Element.column (Grid.simple ++ Card.small)
|
||||||
|
|
||||||
|
multiSelect : Model -> Element Msg
|
||||||
|
multiSelect model =
|
||||||
|
[ Element.el Heading.h3 <| Element.text "Multi Select"
|
||||||
|
, Widget.multiSelect
|
||||||
|
{ selected = model.multiSelected
|
||||||
|
, options = [ 1, 2, 42 ]
|
||||||
|
, label = String.fromInt >> Element.text
|
||||||
|
, onChange = ChangedMultiSelected
|
||||||
|
}
|
||||||
|
|> List.indexedMap (\i (config,selected)->
|
||||||
|
Input.button
|
||||||
|
(Button.simple
|
||||||
|
++ (if i == 0 then
|
||||||
|
Group.left
|
||||||
|
else if i == 2 then
|
||||||
|
Group.right
|
||||||
|
else
|
||||||
|
Group.center)
|
||||||
|
++ (if selected then
|
||||||
|
Color.primary
|
||||||
|
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
config
|
||||||
|
)
|
||||||
|
|> Element.row Grid.compact
|
||||||
|
]
|
||||||
|
|> Element.column (Grid.simple ++ Card.small)
|
||||||
|
|
||||||
|
|
||||||
|
collapsable : Model -> Element Msg
|
||||||
|
collapsable model =
|
||||||
|
[Element.el Heading.h3 <| Element.text "Collapsable"
|
||||||
|
,Widget.collapsable
|
||||||
|
{onToggle = ToggleCollapsable
|
||||||
|
,isCollapsed = model.isCollapsed
|
||||||
|
,label = Element.row Grid.compact
|
||||||
|
[ Element.html <|
|
||||||
|
if model.isCollapsed then
|
||||||
|
Heroicons.cheveronRight [ Attributes.width 20]
|
||||||
|
else
|
||||||
|
Heroicons.cheveronDown [ Attributes.width 20]
|
||||||
|
, Element.el Heading.h4 <|Element.text <| "Title"
|
||||||
|
]
|
||||||
|
,content = Element.text <| "Hello World"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|> Element.column (Grid.simple ++ Card.small)
|
||||||
|
|
||||||
|
dialog : msg -> Model -> Element msg
|
||||||
|
dialog showDialog model =
|
||||||
|
[ Element.el Heading.h3 <| Element.text "Dialog"
|
||||||
|
, Input.button Button.simple
|
||||||
|
{ onPress = Just showDialog
|
||||||
|
, label = Element.text <| "Show Dialog"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|> Element.column (Grid.simple ++ Card.small)
|
||||||
|
|
||||||
|
carousel : Model -> Element Msg
|
||||||
|
carousel model =
|
||||||
|
[ Element.el Heading.h3 <| Element.text "Carousel"
|
||||||
|
, Widget.carousel
|
||||||
|
{content = (Color.cyan,[Color.yellow, Color.green , Color.red ]|> Array.fromList)
|
||||||
|
,current = model.carousel
|
||||||
|
, label = \c ->
|
||||||
|
[ Input.button [Element.centerY]
|
||||||
|
{ onPress = Just <| SetCarousel <| model.carousel - 1
|
||||||
|
, label = Heroicons.cheveronLeft [Attributes.width 20]
|
||||||
|
|> Element.html
|
||||||
|
}
|
||||||
|
, Element.el
|
||||||
|
(Card.simple
|
||||||
|
++ [ Background.color <| c
|
||||||
|
, Element.height <| Element.px <| 100
|
||||||
|
, Element.width <| Element.px <| 100
|
||||||
|
]
|
||||||
|
) <| Element.none
|
||||||
|
, Input.button [Element.centerY]
|
||||||
|
{ onPress = Just <| SetCarousel <| model.carousel + 1
|
||||||
|
, label = Heroicons.cheveronRight [Attributes.width 20]
|
||||||
|
|> Element.html
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|> Element.row (Grid.simple ++ [Element.centerX, Element.width<| Element.shrink])
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|> Element.column (Grid.simple ++ Card.small)
|
||||||
|
|
||||||
|
|
||||||
|
view : { msgMapper : Msg -> msg, showDialog : msg} -> Model -> Element msg
|
||||||
|
view {msgMapper,showDialog} model =
|
||||||
|
Element.column (Grid.section)
|
||||||
|
[ Element.el Heading.h2 <| Element.text "Stateless Views"
|
||||||
|
, "Stateless views are simple functions that view some content. No wiring required."
|
||||||
|
|> Element.text
|
||||||
|
|> List.singleton
|
||||||
|
|> Element.paragraph []
|
||||||
|
, Element.wrappedRow
|
||||||
|
Grid.simple
|
||||||
|
<|
|
||||||
|
[ select model |> Element.map msgMapper
|
||||||
|
, multiSelect model |> Element.map msgMapper
|
||||||
|
, collapsable model |> Element.map msgMapper
|
||||||
|
, dialog showDialog model
|
||||||
|
, carousel model |> Element.map msgMapper
|
||||||
|
]
|
||||||
|
]
|
247
src/Widget.elm
Normal file
247
src/Widget.elm
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
module Widget exposing (select, multiSelect, collapsable, carousel, dialog)
|
||||||
|
|
||||||
|
{-| This module contains functions for displaying data.
|
||||||
|
|
||||||
|
@docs select, multiSelect, collapsable, carousel, dialog
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Array exposing (Array)
|
||||||
|
import Element exposing (Element)
|
||||||
|
import Element.Background as Background
|
||||||
|
import Element.Events as Events
|
||||||
|
import Element.Input as Input
|
||||||
|
import Set exposing (Set)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Selects one out of multiple options. This can be used for radio buttons or Menus.
|
||||||
|
|
||||||
|
```
|
||||||
|
Widget.select
|
||||||
|
{ selected = model.selected
|
||||||
|
, options = [ 1, 2, 42 ]
|
||||||
|
, label = String.fromInt >> Element.text
|
||||||
|
, onChange = ChangedSelected
|
||||||
|
}
|
||||||
|
|> List.map (\(config,selected)->
|
||||||
|
Input.button (if selected then [Font.bold] else []) config
|
||||||
|
)
|
||||||
|
|> Element.row []
|
||||||
|
```
|
||||||
|
|
||||||
|
-}
|
||||||
|
select :
|
||||||
|
{ selected : Maybe a
|
||||||
|
, options : List a
|
||||||
|
, label : a -> Element msg
|
||||||
|
, onChange : a -> msg
|
||||||
|
}
|
||||||
|
->
|
||||||
|
List
|
||||||
|
( { label : Element msg
|
||||||
|
, onPress : Maybe msg
|
||||||
|
}
|
||||||
|
, Bool
|
||||||
|
)
|
||||||
|
select { selected, options, label, onChange } =
|
||||||
|
options
|
||||||
|
|> List.map
|
||||||
|
(\a ->
|
||||||
|
( { onPress = a |> onChange |> Just
|
||||||
|
, label = label a
|
||||||
|
}
|
||||||
|
, selected == Just a
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Selects multible options. This can be used for checkboxes.
|
||||||
|
|
||||||
|
```
|
||||||
|
Widget.multiSelect
|
||||||
|
{ selected = model.multiSelected
|
||||||
|
, options = [ 1, 2, 42 ]
|
||||||
|
, label = String.fromInt >> Element.text
|
||||||
|
, onChange = ChangedMultiSelected
|
||||||
|
}
|
||||||
|
|> List.map (\(config,selected)->
|
||||||
|
Input.button
|
||||||
|
(if selected then
|
||||||
|
[Font.bold]
|
||||||
|
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
config
|
||||||
|
)
|
||||||
|
|> Element.row []
|
||||||
|
```
|
||||||
|
|
||||||
|
-}
|
||||||
|
multiSelect :
|
||||||
|
{ selected : Set comparable
|
||||||
|
, options : List comparable
|
||||||
|
, label : comparable -> Element msg
|
||||||
|
, onChange : comparable -> msg
|
||||||
|
}
|
||||||
|
->
|
||||||
|
List
|
||||||
|
( { label : Element msg
|
||||||
|
, onPress : Maybe msg
|
||||||
|
}
|
||||||
|
, Bool
|
||||||
|
)
|
||||||
|
multiSelect { selected, options, label, onChange } =
|
||||||
|
options
|
||||||
|
|> List.map
|
||||||
|
(\a ->
|
||||||
|
( { onPress = a |> onChange |> Just
|
||||||
|
, label =
|
||||||
|
label a
|
||||||
|
}
|
||||||
|
, selected |> Set.member a
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Some collapsable content.
|
||||||
|
|
||||||
|
```
|
||||||
|
Widget.collapsable
|
||||||
|
{onToggle = ToggleCollapsable
|
||||||
|
,isCollapsed = model.isCollapsed
|
||||||
|
,label = Element.row Grid.compact
|
||||||
|
[ Element.html <|
|
||||||
|
if model.isCollapsed then
|
||||||
|
Heroicons.cheveronRight [ Attributes.width 20]
|
||||||
|
else
|
||||||
|
Heroicons.cheveronDown [ Attributes.width 20]
|
||||||
|
, Element.el Heading.h4 <|Element.text <| "Title"
|
||||||
|
]
|
||||||
|
,content = Element.text <| "Hello World"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
-}
|
||||||
|
collapsable :
|
||||||
|
{ onToggle : Bool -> msg
|
||||||
|
, isCollapsed : Bool
|
||||||
|
, label : Element msg
|
||||||
|
, content : Element msg
|
||||||
|
}
|
||||||
|
-> Element msg
|
||||||
|
collapsable { onToggle, isCollapsed, label, content } =
|
||||||
|
Element.column [] <|
|
||||||
|
[ Input.button []
|
||||||
|
{ onPress = Just <| onToggle <| not isCollapsed
|
||||||
|
, label = label
|
||||||
|
}
|
||||||
|
]
|
||||||
|
++ (if isCollapsed then
|
||||||
|
[]
|
||||||
|
|
||||||
|
else
|
||||||
|
[ content ]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
{-| A dialog element displaying important information.
|
||||||
|
|
||||||
|
```
|
||||||
|
Framework.Layout
|
||||||
|
[ Element.inFront <|
|
||||||
|
if model.displayDialog then
|
||||||
|
Widget.dialog
|
||||||
|
{ onDismiss = Just <| ToggleDialog False
|
||||||
|
, content =
|
||||||
|
[ "This is a dialog window"
|
||||||
|
|> Element.text
|
||||||
|
, Input.button []
|
||||||
|
{onPress = Just <| ToggleDialog False
|
||||||
|
, label = Element.text "Ok"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|> Element.column []
|
||||||
|
}
|
||||||
|
else Element.none
|
||||||
|
] <|
|
||||||
|
Element.text "some Content"
|
||||||
|
```
|
||||||
|
|
||||||
|
-}
|
||||||
|
dialog :
|
||||||
|
{ onDismiss : Maybe msg
|
||||||
|
, content : Element msg
|
||||||
|
}
|
||||||
|
-> Element msg
|
||||||
|
dialog { onDismiss, content } =
|
||||||
|
content
|
||||||
|
|> Element.el
|
||||||
|
[ Element.centerX
|
||||||
|
, Element.centerY
|
||||||
|
]
|
||||||
|
|> Element.el
|
||||||
|
([ Element.width <| Element.fill
|
||||||
|
, Element.height <| Element.fill
|
||||||
|
, Background.color <| Element.rgba255 0 0 0 0.5
|
||||||
|
]
|
||||||
|
++ (onDismiss
|
||||||
|
|> Maybe.map (Events.onClick >> List.singleton)
|
||||||
|
|> Maybe.withDefault []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
{-| A Carousel circles through a non empty list of contents.
|
||||||
|
|
||||||
|
```
|
||||||
|
Widget.carousel
|
||||||
|
{content = ("Blue",["Yellow", "Green" , "Red" ]|> Array.fromList)
|
||||||
|
,current = model.carousel
|
||||||
|
, label = \c ->
|
||||||
|
[ Input.button [Element.centerY]
|
||||||
|
{ onPress = Just <|
|
||||||
|
SetCarousel <|
|
||||||
|
(\x -> if x < 0 then 0 else x) <|
|
||||||
|
model.carousel - 1
|
||||||
|
, label = "<" |> Element.text
|
||||||
|
}
|
||||||
|
, c |> Element.text
|
||||||
|
, Input.button [Element.centerY]
|
||||||
|
{ onPress = Just <|
|
||||||
|
SetCarousel <|
|
||||||
|
(\x -> if x > 3 then 3 else x) <|
|
||||||
|
model.carousel + 1
|
||||||
|
, label = ">" |> Element.text
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|> Element.row [Element.centerX, Element.width<| Element.shrink]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
-}
|
||||||
|
carousel :
|
||||||
|
{ content : ( a, Array a )
|
||||||
|
, current : Int
|
||||||
|
, label : a -> Element msg
|
||||||
|
}
|
||||||
|
-> Element msg
|
||||||
|
carousel { content, current, label } =
|
||||||
|
let
|
||||||
|
( head, tail ) =
|
||||||
|
content
|
||||||
|
in
|
||||||
|
(if current <= 0 then
|
||||||
|
head
|
||||||
|
|
||||||
|
else if current > Array.length tail then
|
||||||
|
tail
|
||||||
|
|> Array.get (Array.length tail - 1)
|
||||||
|
|> Maybe.withDefault head
|
||||||
|
|
||||||
|
else
|
||||||
|
tail
|
||||||
|
|> Array.get (current - 1)
|
||||||
|
|> Maybe.withDefault head
|
||||||
|
)
|
||||||
|
|> label
|
93
src/Widget/FilterSelect.elm
Normal file
93
src/Widget/FilterSelect.elm
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
module Widget.FilterSelect exposing (Model, Msg(..), init, update, viewInput, viewOptions)
|
||||||
|
|
||||||
|
{-|
|
||||||
|
|
||||||
|
@docs Model, Msg, init, update, viewInput, viewOptions
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Element exposing (Attribute, Element)
|
||||||
|
import Element.Input as Input exposing (Placeholder)
|
||||||
|
import Set exposing (Set)
|
||||||
|
|
||||||
|
|
||||||
|
{-| The Model
|
||||||
|
-}
|
||||||
|
type alias Model =
|
||||||
|
{ raw : String
|
||||||
|
, selected : Maybe String
|
||||||
|
, options : Set String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| The Msg is exposed by design. You can unselect by sending `Selected Nothing`.
|
||||||
|
-}
|
||||||
|
type Msg
|
||||||
|
= ChangedRaw String
|
||||||
|
| Selected (Maybe String)
|
||||||
|
|
||||||
|
|
||||||
|
{-| The initial state contains the set of possible options.
|
||||||
|
-}
|
||||||
|
init : Set String -> Model
|
||||||
|
init options =
|
||||||
|
{ raw = ""
|
||||||
|
, selected = Nothing
|
||||||
|
, options = options
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Updates the Model
|
||||||
|
-}
|
||||||
|
update : Msg -> Model -> Model
|
||||||
|
update msg model =
|
||||||
|
case msg of
|
||||||
|
ChangedRaw string ->
|
||||||
|
{ model
|
||||||
|
| raw = string
|
||||||
|
}
|
||||||
|
|
||||||
|
Selected maybe ->
|
||||||
|
{ model
|
||||||
|
| selected = maybe
|
||||||
|
}
|
||||||
|
|> (case maybe of
|
||||||
|
Just string ->
|
||||||
|
\m -> { m | raw = string }
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
identity
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
{-| A wrapper around Input.text.
|
||||||
|
-}
|
||||||
|
viewInput :
|
||||||
|
List (Attribute msg)
|
||||||
|
-> Model
|
||||||
|
->
|
||||||
|
{ msgMapper : Msg -> msg
|
||||||
|
, placeholder : Maybe (Placeholder msg)
|
||||||
|
, label : String
|
||||||
|
}
|
||||||
|
-> Element msg
|
||||||
|
viewInput attributes model { msgMapper, placeholder, label } =
|
||||||
|
Input.text attributes
|
||||||
|
{ onChange = ChangedRaw >> msgMapper
|
||||||
|
, text = model.raw
|
||||||
|
, placeholder = placeholder
|
||||||
|
, label = Input.labelHidden label
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Returns a List of all options that matches the filter.
|
||||||
|
-}
|
||||||
|
viewOptions : Model -> List String
|
||||||
|
viewOptions { raw, options } =
|
||||||
|
if raw == "" then
|
||||||
|
[]
|
||||||
|
|
||||||
|
else
|
||||||
|
options
|
||||||
|
|> Set.filter (String.toUpper >> String.contains (raw |> String.toUpper))
|
||||||
|
|> Set.toList
|
195
src/Widget/ScrollingNav.elm
Normal file
195
src/Widget/ScrollingNav.elm
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
module Widget.ScrollingNav exposing
|
||||||
|
( Model, Msg, init, update, subscriptions, view, viewSections
|
||||||
|
, jumpTo, syncPositions
|
||||||
|
)
|
||||||
|
|
||||||
|
{-| The Scrolling Nav is a navigation bar thats updates while you scroll through
|
||||||
|
the page. Clicking on a navigation button will scroll directly to that section.
|
||||||
|
|
||||||
|
|
||||||
|
# Basics
|
||||||
|
|
||||||
|
@docs Model, Msg, init, update, subscriptions, view, viewSections
|
||||||
|
|
||||||
|
|
||||||
|
# Operations
|
||||||
|
|
||||||
|
@docs jumpTo, syncPositions
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Browser.Dom as Dom
|
||||||
|
import Element exposing (Element)
|
||||||
|
import Framework.Grid as Grid
|
||||||
|
import Html.Attributes as Attributes
|
||||||
|
import IntDict exposing (IntDict)
|
||||||
|
import Task
|
||||||
|
import Time
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
type alias Model elem =
|
||||||
|
{ labels : elem -> String
|
||||||
|
, positions : IntDict String
|
||||||
|
, arrangement : List elem
|
||||||
|
, scrollPos : Int
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
type Msg elem
|
||||||
|
= GotHeaderPos elem (Result Dom.Error Int)
|
||||||
|
| ChangedViewport (Result Dom.Error ())
|
||||||
|
| SyncPosition Int
|
||||||
|
| JumpTo elem
|
||||||
|
| TimePassed
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
init :
|
||||||
|
{ labels : elem -> String
|
||||||
|
, arrangement : List elem
|
||||||
|
}
|
||||||
|
-> ( Model elem, Cmd (Msg elem) )
|
||||||
|
init { labels, arrangement } =
|
||||||
|
{ labels = labels
|
||||||
|
, positions = IntDict.empty
|
||||||
|
, arrangement = arrangement
|
||||||
|
, scrollPos = 0
|
||||||
|
}
|
||||||
|
|> (\a ->
|
||||||
|
( a
|
||||||
|
, syncPositions a
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
update : Msg elem -> Model elem -> ( Model elem, Cmd (Msg elem) )
|
||||||
|
update msg model =
|
||||||
|
case msg of
|
||||||
|
GotHeaderPos label result ->
|
||||||
|
( case result of
|
||||||
|
Ok pos ->
|
||||||
|
{ model
|
||||||
|
| positions =
|
||||||
|
model.positions
|
||||||
|
|> IntDict.insert pos
|
||||||
|
(label |> model.labels)
|
||||||
|
}
|
||||||
|
|
||||||
|
Err _ ->
|
||||||
|
model
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
ChangedViewport _ ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
SyncPosition pos ->
|
||||||
|
( { model
|
||||||
|
| scrollPos = pos
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
TimePassed ->
|
||||||
|
( model
|
||||||
|
, Dom.getViewport
|
||||||
|
|> Task.map (.viewport >> .y >> round)
|
||||||
|
|> Task.perform SyncPosition
|
||||||
|
)
|
||||||
|
|
||||||
|
JumpTo elem ->
|
||||||
|
( model
|
||||||
|
, model
|
||||||
|
|> jumpTo elem
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
subscriptions : Sub (Msg msg)
|
||||||
|
subscriptions =
|
||||||
|
Time.every 1000 (always TimePassed)
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
jumpTo : elem -> Model elem -> Cmd (Msg msg)
|
||||||
|
jumpTo section { labels } =
|
||||||
|
Dom.getElement (section |> labels)
|
||||||
|
|> Task.andThen
|
||||||
|
(\{ element } ->
|
||||||
|
Dom.setViewport 0 element.y
|
||||||
|
)
|
||||||
|
|> Task.attempt ChangedViewport
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
syncPositions : Model elem -> Cmd (Msg elem)
|
||||||
|
syncPositions { labels, arrangement } =
|
||||||
|
arrangement
|
||||||
|
|> List.map
|
||||||
|
(\label ->
|
||||||
|
Dom.getElement (labels label)
|
||||||
|
|> Task.map
|
||||||
|
(.element
|
||||||
|
>> .y
|
||||||
|
>> round
|
||||||
|
)
|
||||||
|
|> Task.attempt
|
||||||
|
(GotHeaderPos label)
|
||||||
|
)
|
||||||
|
|> Cmd.batch
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
viewSections :
|
||||||
|
{ label : String -> Element msg
|
||||||
|
, fromString : String -> Maybe elem
|
||||||
|
, msgMapper : Msg elem -> msg
|
||||||
|
}
|
||||||
|
-> Model elem
|
||||||
|
->
|
||||||
|
{ selected : Maybe elem
|
||||||
|
, options : List elem
|
||||||
|
, label : elem -> Element msg
|
||||||
|
, onChange : elem -> msg
|
||||||
|
}
|
||||||
|
viewSections { label, fromString, msgMapper } { arrangement, scrollPos, labels, positions } =
|
||||||
|
let
|
||||||
|
current =
|
||||||
|
positions
|
||||||
|
|> IntDict.before (scrollPos + 1)
|
||||||
|
|> Maybe.map Just
|
||||||
|
|> Maybe.withDefault (positions |> IntDict.after (scrollPos + 1))
|
||||||
|
|> Maybe.map Tuple.second
|
||||||
|
|> Maybe.andThen fromString
|
||||||
|
in
|
||||||
|
{ selected = current
|
||||||
|
, options = arrangement
|
||||||
|
, label = \elem -> label (elem |> labels)
|
||||||
|
, onChange = JumpTo >> msgMapper
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
view :
|
||||||
|
(elem -> Element msg)
|
||||||
|
-> Model elem
|
||||||
|
-> Element msg
|
||||||
|
view asElement { labels, arrangement } =
|
||||||
|
arrangement
|
||||||
|
|> List.map
|
||||||
|
(\header ->
|
||||||
|
Element.el
|
||||||
|
[ header
|
||||||
|
|> labels
|
||||||
|
|> Attributes.id
|
||||||
|
|> Element.htmlAttribute
|
||||||
|
, Element.width <| Element.fill
|
||||||
|
]
|
||||||
|
<|
|
||||||
|
asElement <|
|
||||||
|
header
|
||||||
|
)
|
||||||
|
|> Element.column Grid.simple
|
94
src/Widget/Snackbar.elm
Normal file
94
src/Widget/Snackbar.elm
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
module Widget.Snackbar exposing
|
||||||
|
( Model, init, current, timePassed
|
||||||
|
, insert, insertFor, dismiss
|
||||||
|
)
|
||||||
|
|
||||||
|
{-| A [snackbar](https://material.io/components/snackbars/) shows notification, one at a time.
|
||||||
|
|
||||||
|
|
||||||
|
# Basics
|
||||||
|
|
||||||
|
@docs Model, init, current, timePassed
|
||||||
|
|
||||||
|
|
||||||
|
# Operations
|
||||||
|
|
||||||
|
@docs insert, insertFor, dismiss
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Queue exposing (Queue)
|
||||||
|
|
||||||
|
|
||||||
|
{-| A snackbar has a queue of Notifications, each with the amount of ms the message should be displayed
|
||||||
|
-}
|
||||||
|
type alias Model a =
|
||||||
|
{ queue : Queue ( a, Int )
|
||||||
|
, current : Maybe ( a, Int )
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Inital state
|
||||||
|
-}
|
||||||
|
init : Model a
|
||||||
|
init =
|
||||||
|
{ queue = Queue.empty
|
||||||
|
, current = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Insert a message that will last for 10 seconds.
|
||||||
|
-}
|
||||||
|
insert : a -> Model a -> Model a
|
||||||
|
insert =
|
||||||
|
insertFor 10000
|
||||||
|
|
||||||
|
|
||||||
|
{-| Insert a message for a specific amount of milli seconds.
|
||||||
|
-}
|
||||||
|
insertFor : Int -> a -> Model a -> Model a
|
||||||
|
insertFor removeIn a model =
|
||||||
|
case model.current of
|
||||||
|
Nothing ->
|
||||||
|
{ model | current = Just ( a, removeIn ) }
|
||||||
|
|
||||||
|
Just _ ->
|
||||||
|
{ model | queue = model.queue |> Queue.enqueue ( a, removeIn ) }
|
||||||
|
|
||||||
|
|
||||||
|
{-| Dismiss the current message.
|
||||||
|
-}
|
||||||
|
dismiss : Model a -> Model a
|
||||||
|
dismiss model =
|
||||||
|
{ model | current = Nothing }
|
||||||
|
|
||||||
|
|
||||||
|
{-| Updates the model. This functions should be called regularly.
|
||||||
|
The first argument is the milli seconds that passed since the last time the function was called.
|
||||||
|
-}
|
||||||
|
timePassed : Int -> Model a -> Model a
|
||||||
|
timePassed ms model =
|
||||||
|
case model.current of
|
||||||
|
Nothing ->
|
||||||
|
let
|
||||||
|
( c, queue ) =
|
||||||
|
model.queue |> Queue.dequeue
|
||||||
|
in
|
||||||
|
{ model
|
||||||
|
| current = c
|
||||||
|
, queue = queue
|
||||||
|
}
|
||||||
|
|
||||||
|
Just ( _, removeIn ) ->
|
||||||
|
if removeIn <= ms then
|
||||||
|
model |> dismiss
|
||||||
|
|
||||||
|
else
|
||||||
|
{ model | current = model.current |> Maybe.map (Tuple.mapSecond ((+) -ms)) }
|
||||||
|
|
||||||
|
|
||||||
|
{-| Returns the current element.
|
||||||
|
-}
|
||||||
|
current : Model a -> Maybe a
|
||||||
|
current model =
|
||||||
|
model.current |> Maybe.map Tuple.first
|
219
src/Widget/SortTable.elm
Normal file
219
src/Widget/SortTable.elm
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
module Widget.SortTable exposing
|
||||||
|
( Model, init, view, sortBy
|
||||||
|
, intColumn, floatColumn, stringColumn
|
||||||
|
)
|
||||||
|
|
||||||
|
{-| A Sortable list allows you to sort coulmn.
|
||||||
|
|
||||||
|
```
|
||||||
|
SortTable.view
|
||||||
|
{ content =
|
||||||
|
[ {id = 1, name = "Antonio", rating = 2.456}
|
||||||
|
, {id = 2, name = "Ana", rating = 1.34}
|
||||||
|
, {id = 3, name = "Alfred", rating = 4.22}
|
||||||
|
, {id = 4, name = "Thomas", rating = 3 }
|
||||||
|
]
|
||||||
|
, columns =
|
||||||
|
[ SortTable.intColumn
|
||||||
|
{ title = "Id"
|
||||||
|
, value = .id
|
||||||
|
, toString = \int -> "#" ++ String.fromInt int
|
||||||
|
}
|
||||||
|
, SortTable.stringColumn
|
||||||
|
{ title = "Name"
|
||||||
|
, value = .name
|
||||||
|
, toString = identity
|
||||||
|
}
|
||||||
|
, SortTable.floatColumn
|
||||||
|
{ title = "rating"
|
||||||
|
, value = .rating
|
||||||
|
, toString = String.fromFloat
|
||||||
|
}
|
||||||
|
]
|
||||||
|
, model = model
|
||||||
|
}
|
||||||
|
|> (\{data,columns} ->
|
||||||
|
{data = data
|
||||||
|
,columns = columns
|
||||||
|
|> List.map (\config->
|
||||||
|
{ header =
|
||||||
|
Input.button [Font.bold]
|
||||||
|
{ onPress =
|
||||||
|
{ title = config.header
|
||||||
|
, asc =
|
||||||
|
if config.header == model.title then
|
||||||
|
not model.asc
|
||||||
|
else
|
||||||
|
True
|
||||||
|
}
|
||||||
|
|> SortBy
|
||||||
|
|> Just
|
||||||
|
, label =
|
||||||
|
if config.header == model.title then
|
||||||
|
[ config.header |> Element.text
|
||||||
|
, Element.text <|
|
||||||
|
if model.asc then
|
||||||
|
"/\"
|
||||||
|
else
|
||||||
|
"\/"
|
||||||
|
]
|
||||||
|
|> Element.row [Font.bold]
|
||||||
|
else
|
||||||
|
config.header |> Element.text
|
||||||
|
}
|
||||||
|
, view = config.view >> Element.text
|
||||||
|
, width = Element.fill
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|> Element.table []
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# Basics
|
||||||
|
|
||||||
|
@docs Model, init, view, sortBy
|
||||||
|
|
||||||
|
|
||||||
|
# Columns
|
||||||
|
|
||||||
|
@docs intColumn, floatColumn, stringColumn
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
|
||||||
|
type ColumnType a
|
||||||
|
= StringColumn { value : a -> String, toString : String -> String }
|
||||||
|
| IntColumn { value : a -> Int, toString : Int -> String }
|
||||||
|
| FloatColumn { value : a -> Float, toString : Float -> String }
|
||||||
|
|
||||||
|
|
||||||
|
{-| The Model contains the sorting column name and if ascending or descending.
|
||||||
|
-}
|
||||||
|
type alias Model =
|
||||||
|
{ title : String
|
||||||
|
, asc : Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias Column a =
|
||||||
|
{ title : String
|
||||||
|
, content : ColumnType a
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| The initial State setting the sorting column name to the empty string.
|
||||||
|
-}
|
||||||
|
init : Model
|
||||||
|
init =
|
||||||
|
{ title = "", asc = True }
|
||||||
|
|
||||||
|
|
||||||
|
{-| A Column containing a Int
|
||||||
|
-}
|
||||||
|
intColumn : { title : String, value : a -> Int, toString : Int -> String } -> Column a
|
||||||
|
intColumn { title, value, toString } =
|
||||||
|
{ title = title
|
||||||
|
, content = IntColumn { value = value, toString = toString }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| A Column containing a Float
|
||||||
|
-}
|
||||||
|
floatColumn : { title : String, value : a -> Float, toString : Float -> String } -> Column a
|
||||||
|
floatColumn { title, value, toString } =
|
||||||
|
{ title = title
|
||||||
|
, content = FloatColumn { value = value, toString = toString }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| A Column containing a String
|
||||||
|
-}
|
||||||
|
stringColumn : { title : String, value : a -> String, toString : String -> String } -> Column a
|
||||||
|
stringColumn { title, value, toString } =
|
||||||
|
{ title = title
|
||||||
|
, content = StringColumn { value = value, toString = toString }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Change the sorting criteras.
|
||||||
|
|
||||||
|
```
|
||||||
|
sortBy =
|
||||||
|
identity
|
||||||
|
```
|
||||||
|
|
||||||
|
-}
|
||||||
|
sortBy : { title : String, asc : Bool } -> Model
|
||||||
|
sortBy =
|
||||||
|
identity
|
||||||
|
|
||||||
|
|
||||||
|
{-| The View
|
||||||
|
-}
|
||||||
|
view :
|
||||||
|
{ content : List a
|
||||||
|
, columns : List (Column a)
|
||||||
|
, model : Model
|
||||||
|
}
|
||||||
|
->
|
||||||
|
{ data : List a
|
||||||
|
, columns : List { header : String, view : a -> String }
|
||||||
|
}
|
||||||
|
view { content, columns, model } =
|
||||||
|
let
|
||||||
|
findTitle : List (Column a) -> Maybe (ColumnType a)
|
||||||
|
findTitle list =
|
||||||
|
case list of
|
||||||
|
[] ->
|
||||||
|
Nothing
|
||||||
|
|
||||||
|
head :: tail ->
|
||||||
|
if head.title == model.title then
|
||||||
|
Just head.content
|
||||||
|
|
||||||
|
else
|
||||||
|
findTitle tail
|
||||||
|
in
|
||||||
|
{ data =
|
||||||
|
content
|
||||||
|
|> (columns
|
||||||
|
|> findTitle
|
||||||
|
|> Maybe.map
|
||||||
|
(\c ->
|
||||||
|
case c of
|
||||||
|
StringColumn { value } ->
|
||||||
|
List.sortBy value
|
||||||
|
|
||||||
|
IntColumn { value } ->
|
||||||
|
List.sortBy value
|
||||||
|
|
||||||
|
FloatColumn { value } ->
|
||||||
|
List.sortBy value
|
||||||
|
)
|
||||||
|
|> Maybe.withDefault identity
|
||||||
|
)
|
||||||
|
|> (if model.asc then
|
||||||
|
identity
|
||||||
|
|
||||||
|
else
|
||||||
|
List.reverse
|
||||||
|
)
|
||||||
|
, columns =
|
||||||
|
columns
|
||||||
|
|> List.map
|
||||||
|
(\column ->
|
||||||
|
{ header = column.title
|
||||||
|
, view =
|
||||||
|
case column.content of
|
||||||
|
IntColumn { value, toString } ->
|
||||||
|
value >> toString
|
||||||
|
|
||||||
|
FloatColumn { value, toString } ->
|
||||||
|
value >> toString
|
||||||
|
|
||||||
|
StringColumn { value, toString } ->
|
||||||
|
value >> toString
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
144
src/Widget/ValidatedInput.elm
Normal file
144
src/Widget/ValidatedInput.elm
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
module Widget.ValidatedInput exposing
|
||||||
|
( Model, Msg, init, update, view
|
||||||
|
, getError, getRaw, getValue
|
||||||
|
)
|
||||||
|
|
||||||
|
{-| The validated Input is a wrapper around `Input.text`.
|
||||||
|
They can validate the input and return an error if nessarry.
|
||||||
|
|
||||||
|
|
||||||
|
# Basics
|
||||||
|
|
||||||
|
@docs Model, Msg, init, update, view
|
||||||
|
|
||||||
|
|
||||||
|
# Access the Model
|
||||||
|
|
||||||
|
@docs getError, getRaw, getValue
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Element exposing (Attribute, Element)
|
||||||
|
import Element.Events as Events
|
||||||
|
import Element.Input as Input exposing (Placeholder)
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
type Model err a
|
||||||
|
= Model
|
||||||
|
{ raw : Maybe String
|
||||||
|
, value : a
|
||||||
|
, err : Maybe err
|
||||||
|
, validator : String -> Result err a
|
||||||
|
, toString : a -> String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
getRaw : Model err a -> String
|
||||||
|
getRaw (Model { raw, value, toString }) =
|
||||||
|
case raw of
|
||||||
|
Just string ->
|
||||||
|
string
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
value |> toString
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
getValue : Model err a -> a
|
||||||
|
getValue (Model { value }) =
|
||||||
|
value
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
getError : Model err a -> Maybe err
|
||||||
|
getError (Model { err }) =
|
||||||
|
err
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
type Msg
|
||||||
|
= ChangedRaw String
|
||||||
|
| LostFocus
|
||||||
|
| StartEditing
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
init : { value : a, validator : String -> Result err a, toString : a -> String } -> Model err a
|
||||||
|
init { validator, toString, value } =
|
||||||
|
Model
|
||||||
|
{ raw = Nothing
|
||||||
|
, value = value
|
||||||
|
, err = Nothing
|
||||||
|
, validator = validator
|
||||||
|
, toString = toString
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
update : Msg -> Model err a -> Model err a
|
||||||
|
update msg (Model model) =
|
||||||
|
case msg of
|
||||||
|
StartEditing ->
|
||||||
|
Model
|
||||||
|
{ model
|
||||||
|
| raw = model.value |> model.toString |> Just
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangedRaw string ->
|
||||||
|
Model
|
||||||
|
{ model
|
||||||
|
| raw = Just string
|
||||||
|
, err = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
LostFocus ->
|
||||||
|
case model.raw of
|
||||||
|
Just string ->
|
||||||
|
case model.validator string of
|
||||||
|
Ok value ->
|
||||||
|
Model
|
||||||
|
{ model
|
||||||
|
| value = value
|
||||||
|
, raw = Nothing
|
||||||
|
, err = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
Model
|
||||||
|
{ model
|
||||||
|
| raw = Nothing
|
||||||
|
, err = Just err
|
||||||
|
}
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
Model model
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
view :
|
||||||
|
List (Attribute msg)
|
||||||
|
-> Model err a
|
||||||
|
->
|
||||||
|
{ msgMapper : Msg -> msg
|
||||||
|
, placeholder : Maybe (Placeholder msg)
|
||||||
|
, label : String
|
||||||
|
, readOnly : a -> Element msg
|
||||||
|
}
|
||||||
|
-> Element msg
|
||||||
|
view attributes (Model model) { msgMapper, placeholder, label, readOnly } =
|
||||||
|
case model.raw of
|
||||||
|
Just string ->
|
||||||
|
Input.text (attributes ++ [ Events.onLoseFocus <| msgMapper <| LostFocus ])
|
||||||
|
{ onChange = ChangedRaw >> msgMapper
|
||||||
|
, text = string
|
||||||
|
, placeholder = placeholder
|
||||||
|
, label = Input.labelHidden label
|
||||||
|
}
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
Input.button []
|
||||||
|
{ onPress = Just (StartEditing |> msgMapper)
|
||||||
|
, label = model.value |> readOnly
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user