The most interesting part is about how the dragging state is represented. It’s simply whether something is being dragged, or nothing.
type State
= Dragging Element (Float, Float) -- x and y position
| DoingNothing
Compare to this other possible representation:
type MouseState
= Up
| Down
type alias State =
{ mouseState : MouseState
, mousePos : (Float, Float)
, draggingElement : Maybe Element
, receivingElement : Maybe Element
}
With the first union type, the representation is a lot simpler and impossible states are impossible.
import Browser
import Browser.Events as Events
import Browser.Dom exposing (Element, getElement)
import Json.Decode as Decode exposing (Decoder)
import Html exposing (Html, div, ul, li, span, text)
import Html.Attributes exposing (style)
-- mpizenberg/elm-pointer-events
import Html.Events.Extra.Mouse as Mouse
import Task
type alias Dropbox =
{ id : String
, content : Maybe String
}
type State
= Dragging String (Float, Float) -- x and y position
| DoingNothing
{-| Set the value of a dropbox
and reset the value of the other dropboxes
-}
setValue : String -> String -> List Dropbox -> List Dropbox
setValue id value ds =
List.map (\d ->
if d.id == id then
{ d | content = Just value }
else
{ d | content = Nothing }
) ds
displayDropbox : Dropbox -> Html Msg
displayDropbox dropbox =
let
css =
[ style "display" "inline"
, style "border-style" "solid"
, style "border-width" "1px"
, style "padding" "4px"
, style "margin-right" "10px"
, style "vertical-align" "middle"
]
in
case dropbox.content of
Just string ->
div
([ Mouse.onDown (\{clientPos} -> Drag string clientPos)
, style "cursor" "grab"
] ++ css)
[ text string ]
Nothing ->
div
([ Mouse.onUp (\_ -> ReleaseOn (Just dropbox))
] ++ css)
[ text "Drop it!" ]
displayDrag : Model -> Html Msg
displayDrag model =
case model.state of
Dragging string (x, y) ->
let
(width, height) = model.dragging
newX = String.fromFloat (x - width / 2)
newY = String.fromFloat (y - height / 2)
in
span
[ Html.Attributes.id "dragging"
, style "position" "fixed"
-- make mouse events go through this dragging element
, style "pointer-events" "none"
-- this property will not work because
-- pointer-events is set to none
, style "cursor" "grabbing"
, style "top" (newY ++ "px")
, style "left" (newX ++ "px")
]
[ text string ]
DoingNothing ->
span [] []
-- MAIN
type Msg
= Drag String (Float, Float)
| ReleaseOn (Maybe Dropbox)
| SetDraggingElement (Result Browser.Dom.Error Element)
type alias Model =
{ state : State
, dropboxes : List Dropbox
-- width and height of the dragging element
, dragging : (Float, Float)
}
init : () -> (Model, Cmd Msg)
init _ =
let
dropboxes =
[ { id = "d1"
, content = Just "Drag me"
}
, { id = "d2"
, content = Nothing
}
, { id = "d3"
, content = Nothing
}
]
model =
{ state = DoingNothing
, dropboxes = dropboxes
, dragging = (0, 0)
}
in
(model, Cmd.none)
main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Drag string pos ->
(
{ model | state = Dragging string pos }
, Task.attempt SetDraggingElement (getElement "dragging")
)
ReleaseOn something ->
case something of
Just dst ->
case model.state of
Dragging string pos ->
({ model
| dropboxes = setValue dst.id string model.dropboxes
, state = DoingNothing
}, Cmd.none)
DoingNothing ->
({ model | state = DoingNothing }, Cmd.none)
Nothing ->
({ model | state = DoingNothing }, Cmd.none)
SetDraggingElement res ->
case res of
Ok {element} ->
({ model | dragging = (element.width, element.height)}, Cmd.none)
Err e ->
(model, Cmd.none)
{-| Only listen to the mouse move event
when there is a dragging element
-}
subscriptions : Model -> Sub Msg
subscriptions model =
case model.state of
Dragging string _ ->
Sub.batch
[ Events.onMouseMove (decoder string)
, Events.onMouseUp (Decode.succeed (ReleaseOn Nothing))
]
DoingNothing ->
Sub.none
{-| Set the position of the dragging element
while the mouse is moving
-}
decoder : String -> Decoder Msg
decoder string =
Decode.map2 Tuple.pair
(Decode.field "clientX" Decode.float)
(Decode.field "clientY" Decode.float)
|> Decode.andThen (\pos ->
Decode.succeed (Drag string pos)
)
view : Model -> Html Msg
view model =
div
[]
[ displayDrag model
, div [] (List.map displayDropbox model.dropboxes)
]