Hello World

This commit is contained in:
Unknown 2020-03-15 19:13:12 +01:00
parent 837ee316d8
commit d4a7221f15
18 changed files with 15276 additions and 2 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# elm-package generated files
elm-stuff
# elm-repl generated files
repl-temp-*

29
LICENSE Normal file
View 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.

View File

@ -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

1
docs.json Normal file

File diff suppressed because one or more lines are too long

13358
docs/index.html Normal file

File diff suppressed because it is too large Load Diff

BIN
docs/select.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 B

29
elm.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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
View 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
View 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
View 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
}
)
}

View 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
}