How can I make a draggable split panel with Elm (preferably Elm-UI)? - elm

I looked for a split-pane for Elm and found things that don't seem straightforward to use.
doodledood/elm-split-pane is not compatible with Elm 0.19.1, so I guess I would either have to switch to Elm 0.18, or convert this to Elm 0.19.1 to use it. Also it is defined in terms of Elm HTML, not Elm UI.
I also found https://github.com/ellie-app/ellie/blob/master/assets/src/Ellie/Ui/SplitPane.elm. That involves a bunch of custom JavaScript, and it's not obvious to me how tightly coupled it is with the rest of the app.

I'm using a split pane in my elm project with elm-ui. I took this SplitPane elm-html-based package https://github.com/doodledood/elm-split-pane, copied it's source into my project and converted its internals to return Element msg from its view function by wrapping the content and the attributes.
Here is the source code of the converted functions:
view : ViewConfig msg -> Element msg -> Element msg -> State -> Element msg
view (ViewConfig viewConfig) firstView secondView (State state) =
let
splitter =
getConcreteSplitter viewConfig state.orientation state.dragState
in
case state.orientation of
Horizontal ->
row
(paneContainerStyle state.orientation
++ [ width fill ]
)
[ el (firstChildViewStyle (State state)) firstView
, splitter
, el (secondChildViewStyle (State state)) secondView
]
Vertical ->
column (paneContainerStyle state.orientation)
[ el (firstChildViewStyle (State state)) firstView
, splitter
, el (secondChildViewStyle (State state)) secondView
]
viewReversed : ViewConfig msg -> Element msg -> Element msg -> State -> Element msg
viewReversed (ViewConfig viewConfig) firstView secondView (State state) =
let
splitter =
getConcreteSplitter viewConfig state.orientation state.dragState
in
case state.orientation of
Horizontal ->
row
(paneContainerStyle state.orientation
++ [ width fill ]
)
[ el (secondChildViewStyle (State state)) secondView
, splitter
, el (firstChildViewStyle (State state)) firstView
]
Vertical ->
column (paneContainerStyle state.orientation)
[ el (secondChildViewStyle (State state)) firstView
, splitter
, el (firstChildViewStyle (State state)) firstView
]
getConcreteSplitter :
{ toMsg : Msg -> msg
, splitter : Maybe (CustomSplitter msg)
}
-> Orientation
-> DragState
-> Element msg
getConcreteSplitter viewConfig orientation4 dragState =
case viewConfig.splitter of
Just (CustomSplitter splitter) ->
splitter
Nothing ->
case createCustomSplitter viewConfig.toMsg <| createDefaultSplitterDetails orientation4 dragState of
CustomSplitter defaultSplitter ->
defaultSplitter
Here are the styles functions:
paneContainerStyle : Orientation -> List (Attribute msg)
paneContainerStyle orientation5 =
[ style "overflow" "hidden"
, style "display" "flex"
, style "flexDirection"
(case orientation5 of
Horizontal ->
"row"
Vertical ->
"column"
)
, style "justifyContent" "center"
, style "alignItems" "center"
, style "width" "100%"
, style "height" "100%"
, style "boxSizing" "border-box"
]
|> List.map Element.htmlAttribute
firstChildViewStyle : State -> List (Attribute msg)
firstChildViewStyle (State state) =
case state.splitterPosition of
Px px2 ->
let
v =
(String.fromFloat <| toFloat (getValue px2)) ++ "px"
in
case state.orientation of
Horizontal ->
[ style "display" "flex"
, style "width" v
, style "height" "100%"
, style "overflow" "hidden"
, style "boxSizing" "border-box"
, style "position" "relative"
]
|> List.map Element.htmlAttribute
Vertical ->
[ style "display" "flex"
, style "width" "100%"
, style "height" v
, style "overflow" "hidden"
, style "boxSizing" "border-box"
, style "position" "relative"
]
|> List.map Element.htmlAttribute
Percentage p ->
let
v =
String.fromFloat <| getValue p
in
[ style "display" "flex"
, style "flex" v
, style "width" "100%"
, style "height" "100%" -- pz edit
, style "overflow" "hidden"
, style "boxSizing" "border-box"
, style "position" "relative"
]
|> List.map Element.htmlAttribute
secondChildViewStyle : State -> List (Attribute msg)
secondChildViewStyle (State state) =
case state.splitterPosition of
Px _ ->
[ style "display" "flex"
, style "flex" "1"
, style "width" "100%"
, style "height" "100%"
, style "overflow" "hidden"
, style "boxSizing" "border-box"
, style "position" "relative"
]
|> List.map Element.htmlAttribute
Percentage p ->
let
v =
String.fromFloat <| 1 - getValue p
in
[ style "display" "flex"
, style "flex" v
, style "width" "100%"
, style "height" "100%"
, style "overflow" "hidden"
, style "boxSizing" "border-box"
, style "position" "relative"
]
|> List.map Element.htmlAttribute
Uri

I tried this approach but personally found the combination of html and elm-ui very confusing. (This is me, YMMV.) My simpler approach is just to use an elm-ui scrollbar with a custom thumb.
contentArea : Model -> Element Msg
contentArea model =
let
leftPane =
column
[ width fill, alignTop ]
[ ... ]
rightPane =
column [ spacing 5, padding 5, alignTop, centerX ]
[ ... ]
splitter =
Input.slider
[ height <| px 2
, width <| px 1000
, centerY
, behindContent <|
-- Slider track
el
[ width fill
, height <| px 1
, centerY
, centerX
]
none
]
{ onChange = ResizeViews << round
, label =
Input.labelHidden "Splitter"
, min = 0.0
, max = toFloat 1000
, step = Just 1
, value = toFloat model.splitInPixels
, thumb = customThumb
}
customThumb =
Input.thumb
[ width (px 3)
, height (px 600)
, moveDown 300
, Border.rounded 1
, Border.width 1
, Border.color (rgb 0.5 0.5 0.5)
, Background.color FlatColors.BritishPalette.seabrook
]
in
column [ width fill, spacing 5, padding 5 ]
[ splitter
, row [ width fill, spacing 5, padding 5 ]
[ el [ width <| fillPortion model.splitInPixels, alignTop ] leftPane
, el [ width <| fillPortion (1000 - model.splitInPixels), alignTop ] rightPane
]
]

Related

Modal in Elm without framework

I am new to ELM and I want to create a modal without the use of any libraries such as Bootstrap or ELM-UI. I found this simple example online which is also using JSON Decode. Is there a possibility to have the modal work simply without any framework/library and JSON Decode? How can I modify the code to simply get a working modal?
module Main exposing (main)
import Browser
import Html exposing (Html, Attribute, button, div, span, text)
import Html.Events exposing (onClick, on)
import Html.Attributes exposing (class, style)
import Json.Decode as Decode
type alias Model =
{ isVisible : Bool, count : Int }
initialModel : Model
initialModel =
{ isVisible = False, count = 0 }
type Msg
= Show
| Hide
| Increment
| Decrement
update : Msg -> Model -> Model
update msg model =
case msg of
Show ->
{ model | isVisible = True }
Hide ->
{ model | isVisible = False }
Increment ->
{ model | count = model.count + 1 }
Decrement ->
{ model | count = model.count - 1 }
view : Model -> Html Msg
view model =
div []
[ button [ onClick Show ] [ text "Show!" ]
, if model.isVisible then
div
([ class dialogContainerClass
, on "click" (containerClickDecoder Hide)
]
++ dialogContainerStyle
)
[ div dialogContentStyle
[ span [] [ text "Click anywhere outside this dialog to close it!" ]
, span [] [ text "Clicking on anything inside of this dialog works as normal." ]
, div []
[ button [ onClick Decrement ] [ text "-" ]
, text (String.fromInt model.count)
, button [ onClick Increment ] [ text "+" ]
]
]
]
else
div [] []
]
dialogContainerClass : String
dialogContainerClass = "dialog-container-class"
containerClickDecoder : msg -> Decode.Decoder msg
containerClickDecoder closeMsg =
Decode.at [ "target", "className" ] Decode.string
|> Decode.andThen
(\c ->
if String.contains dialogContainerClass c then
Decode.succeed closeMsg
else
Decode.fail "ignoring"
)
dialogContainerStyle : List (Attribute msg)
dialogContainerStyle =
[ style "position" "absolute"
, style "top" "0"
, style "bottom" "0"
, style "right" "0"
, style "left" "0"
, style "display" "flex"
, style "align-items" "center"
, style "justify-content" "center"
, style "background-color" "rgba(33, 43, 54, 0.4)"
]
dialogContentStyle : List (Attribute msg)
dialogContentStyle =
[ style "border-style" "solid"
, style "border-radius" "3px"
, style "border-color" "white"
, style "background-color" "white"
, style "height" "120px"
, style "width" "440px"
, style "display" "flex"
, style "flex-direction" "column"
, style "align-items" "center"
, style "justify-content" "center"
]
main : Program () Model Msg
main =
Browser.sandbox
{ init = initialModel
, view = view
, update = update
}
If I understand your question correctly, the problem you're trying to solve is clicking outside the modal to close it. Decoding the event object to get information about the DOM is a bit of a hack in Elm – I think you're right to try to avoid it, unless necessary. One way to achieve the same thing is to add a click event handler with stop propagation to your modal contents – this stops the click event from firing on the container when it originates from within the modal.
I've put your example code in an Ellie and made some small changes: https://ellie-app.com/b9gDPHgtz2ca1
This solution uses Html.Events.stopPropagationOn, which is like on but does a call to event.stopPropagation(). This function does require you to supply a decoder, so I'm afraid you can't get away from importing Json.Decode, but we are using the simplest possible decoder – Decode.succeed – and only to satisfy the parameters of the function.
I've added a NoOp variant to Msg, as there is nothing to do when the modal is clicked; simply attaching this event handler stops the Hide event from firing when we don't want it to.
Code
module Main exposing (main)
import Browser
import Html exposing (Attribute, Html, button, div, span, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (on, onClick)
import Json.Decode as Decode
type alias Model =
{ isVisible : Bool, count : Int }
initialModel : Model
initialModel =
{ isVisible = False, count = 0 }
type Msg
= Show
| Hide
| Increment
| Decrement
| NoOp
update : Msg -> Model -> Model
update msg model =
case msg of
Show ->
{ model | isVisible = True }
Hide ->
{ model | isVisible = False }
Increment ->
{ model | count = model.count + 1 }
Decrement ->
{ model | count = model.count - 1 }
NoOp ->
model
view : Model -> Html Msg
view model =
div []
[ button [ onClick Show ] [ text "Show!" ]
, if model.isVisible then
div
(onClick Hide
:: dialogContainerStyle
)
[ div
(onClickStopPropagation NoOp
:: dialogContentStyle
)
[ span [] [ text "Click anywhere outside this dialog to close it!" ]
, span [] [ text "Clicking on anything inside of this dialog works as normal." ]
, div []
[ button [ onClick Decrement ] [ text "-" ]
, text (String.fromInt model.count)
, button [ onClick Increment ] [ text "+" ]
]
]
]
else
div [] []
]
onClickStopPropagation : msg -> Html.Attribute msg
onClickStopPropagation msg =
Html.Events.stopPropagationOn "click" <| Decode.succeed ( msg, True )
dialogContainerStyle : List (Attribute msg)
dialogContainerStyle =
[ style "position" "absolute"
, style "top" "0"
, style "bottom" "0"
, style "right" "0"
, style "left" "0"
, style "display" "flex"
, style "align-items" "center"
, style "justify-content" "center"
, style "background-color" "rgba(33, 43, 54, 0.4)"
]
dialogContentStyle : List (Attribute msg)
dialogContentStyle =
[ style "border-style" "solid"
, style "border-radius" "3px"
, style "border-color" "white"
, style "background-color" "white"
, style "height" "120px"
, style "width" "440px"
, style "display" "flex"
, style "flex-direction" "column"
, style "align-items" "center"
, style "justify-content" "center"
]
main : Program () Model Msg
main =
Browser.sandbox
{ init = initialModel
, view = view
, update = update
}

elm-ui center elements in wrapped row

I'm using Elm with mdgriffiths/elm-ui, and I've really been enjoying it. Right now, I'm trying to create a centered, wrapped row of elements:
I can get it to this point:
using this code:
button : String -> String -> Element Msg
button label url =
link
[ height (px 150)
, width (px 150)
, Border.width 1
, Background.color (rgb255 255 255 255)
]
{ url = url
, label =
Element.paragraph
[ Font.center ]
[ textEl [] label ]
}
row : Element Msg
row =
Element.wrappedRow
[ Element.spacing 25
, Element.centerX
, Element.centerY
, width (fill |> Element.maximum 600)
, Font.center
]
[ button "A" "/a"
, button "B" "/b"
, button "C" "/c"
, button "D" "/d"
]
But when I try adding Element.centerX to my buttons like this
link
[ Element.centerX
, ...
]
I get this instead:
I've also tried Font.center without success, and I don't know what else I can try.
I'm not sure if I'm missing something I should be using, or if the whole thing needs re-arranging, or if I just need to use the built-in CSS stuff.
Update:
Link to an Ellie with the issues I'm seeing.
https://ellie-app.com/7NpM6SPfhLHa1
This Github issue is useful for this problem. I'm afraid you will have to use some CSS (unless I'm missing something). I've found this before with elm-ui; every now and then it can't quite do what you want and you need a bit of CSS.
This works for me (taken from the post by AlienKevin in the link above). You need to set "marginLeft" and "marginRight" to "auto".
module Main exposing (main)
import Element as E
import Element.Border
import Html.Attributes
box : String -> E.Element msg
box label =
E.paragraph
[ E.width <| E.px 200
, Element.Border.width 1
, E.htmlAttribute (Html.Attributes.style "marginLeft" "auto")
, E.htmlAttribute (Html.Attributes.style "marginRight" "auto")
]
[ E.text label ]
row : E.Element msg
row =
E.wrappedRow []
[ box "A"
, box "B"
, box "C"
]
main =
E.layout [] row
(See here for an Ellie.)
You can also do the following:
Define the following elm-ui classes. I usually setup a UI.elm module for this
centerWrap : Attribute msg
centerWrap =
Html.Attributes.class "centerWrap"
|> htmlAttribute
dontCenterWrap : Attribute msg
dontCenterWrap =
Html.Attributes.class "dontCenterWrap"
|> htmlAttribute
Add the following to your css. Basically says center elements if has centerWrap class but not dontCenterWrap class.
:not(.dontCenterWrap).centerWrap>div.wrp {
justify-content: center !important;
}
Apply the attribute
wrappedRow [ width fill, UI.centerWrap, spaceEvenly ] [...]
Assuming you created a custom element that centerWraps and wanted to disable that you could use UI.dontCenterWrap
centerWrappedRow attr children =
wrappedRow (UI.centerWrap :: attr) children
-- somewhere else
...
centerWrappedRow [UI.dontCenterWrap] [..]
...

Layout questions with Style Elements and Elm

I am building my first app in Elm and decided to use Style Elements package instead of CSS.
This is the layout that I am attempting. Its a single page app that does not scroll.
here is some code
view : Model -> Html Msg
view model =
layout Styles.stylesheet <|
pageWrapper
pageWrapper : Element Styles variation Msg
column Page
[ width (percent 100), height (percent 100) ]
[ navMain
, contentArea
]
navMain : Element Styles variation Msg
navMain =
row Navigation
[ spread, padding 10 ]
[ el (Nav Logo) [ width (px 50)] (text "Logo")
, row (Nav Link)
[ padding 15, spacing 10]
[ el (Nav Link) [] (text "About")
, el (Nav Link) [] (text "Services")
, el (Nav Link) [] (text "Team")
, el (Nav Link) [] (text "Location")
, el (Nav Link) [] (text "Contact")
]
]
contentArea : Element Styles variation Msg
contentArea =
-- At this point I thought I would make a row with an el with the image
and a column containing the other two images. And other than creating heights with pixels I don't know how to extend the main content to the bottom of the page.
What are some good example apps I can look at to get a good idea of how to control the layout? I have looked through several, and I feel like I am just missing something very obvious because so far SE has been awesome to work with!
Here's a very basic example:
https://ellie-app.com/k25rby75Da1/1
I've left the heights of the elements undefined below but you should be able to figure that out to suit your needs. I updated the ellie link to a version that I think better approximates the height in your example.
module Main exposing (main)
import Html
import Html.Attributes
import Color
import Style
import Style.Font as Font
import Style.Color as Color
import Element exposing (..)
import Element.Attributes exposing (..)
main =
Html.beginnerProgram
{ model = model
, update = update
, view = view
}
type alias Model =
{ navbar : String
, leftcontent : String
, rightcontent : { top : String, bottom : String }
}
model : Model
model =
{ navbar = "This is the navbar"
, leftcontent = "This is the left column content"
, rightcontent =
{ top = "This is the right top content"
, bottom = "This is the right bottom content"
}
}
update model msg =
model
type MyStyles = Navbar | Left | RightTop | RightBottom | None
stylesheet =
Style.styleSheet
[ Style.style Navbar [ Color.background Color.red ]
, Style.style Left [ Color.background Color.blue ]
, Style.style RightTop [Color.background Color.purple ]
, Style.style RightBottom [Color.background Color.gray ]
]
view model =
Element.viewport stylesheet <|
(column None [] [
row Navbar [] [ text model.navbar ]
, wrappedRow None []
[ column Left [ width (percent 50)] [ text model.leftcontent ]
, wrappedColumn None [ width (percent 50)]
[ el RightTop [ width fill] (text model.rightcontent.top)
, el RightBottom [ width fill ] (text model.rightcontent.bottom)
]
]])

Elm: Conditional preventDefault (with contentEditable)

I'm trying to make a content editable tag that uses enter to update the model.
My code is below, and here is a version that you can play around with on Ellie.
The on "blur" attribute works and updates the model when you click away. But I want to get the same 'update' functionality when an enter is pressed.
view : Model -> Html Msg
view model =
let
attrs =
[ contenteditable True
--, on "blur" (Json.map UpdateTitle targetTextContent)
, onInput2 UpdateTitle
, onEnter EnterPressed
, id "title"
, class "title"
]
in
div []
[ h1 attrs [ text model.existing ]
, text "Click above to start editing. Blur to save the value. The aim is to capture an <enter> and interpret that as a blur, i.e. to save the value and blur the field"
, p [] [ text <| "(" ++ model.existing ++ ")" ]
]
targetTextContent : Json.Decoder String
targetTextContent =
Json.at [ "target", "textContent" ] Json.string
onInput2 : (String -> msg) -> Attribute msg
onInput2 msgCreator =
on "input" (Json.map msgCreator targetTextContent)
onEnter : (Bool -> msg) -> Attribute msg
onEnter enterMsg =
onWithOptions "keydown"
{ stopPropagation = False
, preventDefault = False
}
(keyCode
|> Json.andThen
(\ch ->
let
_ =
Debug.log "on Enter" ch
in
Json.succeed (enterMsg <| ch == 13)
)
)
This code seems to be updating the model ok, but the DOM is getting messed up. For example if I enter enter after "blast" I see this
I tried switching to Html.Keyed and using "keydown" but it did not make any difference or just created different issues.
Solved! The key point is the filter function that uses Json.Decode.fail so that only <enter> is subject to preventDefault. See https://github.com/elm-lang/virtual-dom/issues/18#issuecomment-273403774 for the idea.
view : Model -> Html Msg
view model =
let
attrs =
[ contenteditable True
, on "blur" (Json.map UpdateTitle targetTextContent)
, onEnter EnterPressed
, id "title"
, class "title"
]
in
div []
[ h1 attrs [ text model.existing ]
, text "Click above to start editing. Blur to save the value. The aim is to capture an <enter> and interpret that as a blur, i.e. to save the value and blur the field"
, p [] [ text <| "(" ++ model.existing ++ ")" ]
]
targetTextContent : Json.Decoder String
targetTextContent =
Json.at [ "target", "textContent" ] Json.string
onEnter : msg -> Attribute msg
onEnter msg =
let
options =
{ defaultOptions | preventDefault = True }
filterKey code =
if code == 13 then
Json.succeed msg
else
Json.fail "ignored input"
decoder =
Html.Events.keyCode
|> Json.andThen filterKey
in
onWithOptions "keydown" options decoder

Custom event with two decoded values

I'm trying to write a custom event which sends two decoded values to update. on "mousedown", I'd like to inform update about the position of the mouse as well as the dimensions of the DOM element.
Here's what I've tried, but doesn't work:
dragMeListItem : Item -> Html Msg
dragMeListItem item =
div
[ on "mousedown" (Decode.map (\posit -> (Decode.map (\rect -> DragStart posit rect item) decodeRectangle)) Mouse.position)
, attribute "class" "drag-me"
, sharedStyles
, style
[ ( "background-color", item.color )
, ( "border", "1px solid #DD0848" )
]
]
[ text "Drag Me!"
, br [] []
, text (toString item.value)
]
-
decodeRectangle : Decode.Decoder Rectangle
decodeRectangle =
let
rectangle =
DOM.target
:> DOM.boundingClientRect
in
rectangle
-
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
DragStart xy rectangle item ->
let
xY =
Debug.log "xy:" xy
consoleRectangle =
Debug.log "rectangle:" rectangle
consoleItem =
Debug.log "item:" item
in
{ model
| draggingItem = Just ( item, rectangle )
, drag = Just (Drag xy xy)
}
! []
-
The compiler error is:
The type annotation for dragMeListItem does not match its
definition.
362| dragMeListItem : Item -> Html Msg
The type annotation is saying:
Item -> Html Msg
But I am inferring that the definition has this type:
Item -> Html (Decode.Decoder Msg)
The problem originates in your nested use of Decode.map in the "mousedown" line. Try using andThen instead:
(Mouse.position `Decode.andThen` \posit ->
decodeRectangle `Decode.andThen` \rect ->
Decode.succeed (DragStart posit rect item))