mirror of
https://github.com/Orasund/elm-ui-widgets.git
synced 2024-11-21 18:05:00 +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
|
||||
Collection of reusable views for elm-ui.
|
||||
# Elm-Ui-Widgets
|
||||
|
||||
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