An easy way to make DOM elements draggable
Have elm installed.
elm install zaboco/elm-draggable
- Basic / Code
- Custom events / Code
- Constraints - restrict dragging to one axis at a time / Code
- Pan & Zoom - drag to pan & scroll to zoom / Code
- Multiple Targets / Code
This library is meant to be easy to use, by keeping its internal details hidden and only communicating to the parent application by emitting Event
messages. So, each time the internals change and something relevant happens (such as "started dragging", "dragged at", etc.), a new message is sent as a Cmd
and handled in the main update
function. To better understand how this works, see the snippets below and also the working examples.
In order to make a DOM element draggable, you'll need to:
import Draggable
Include:
- The element's position.
- The internal
Drag
state. Note that, for simplicity, the model entry holding this state must be calleddrag
, since the update function below follows this naming convention. A future update could allow using custom field names. Please note that for the sake of example, we are specifyingString
as the type to tag draggable elements with. If you have only one such element,()
might be a better type.
type alias Model =
{ position : ( Int, Int )
, drag : Draggable.State String
}
initModel : Model
initModel =
{ position = ( 0, 0 )
, drag = Draggable.init
}
OnDragBy
is for actually updating the position, taking aDraggable.Delta
as an argument.Delta
is just an alias for a tuple of(Float, Float)
and it represents the distance between two consecutive drag points.DragMsg
is for handling internalDrag
state updates.
type Msg
= OnDragBy Draggable.Delta
| DragMsg (Draggable.Msg String)
For the simplest case, you only have to provide a handler for onDragBy
:
dragConfig : Draggable.Config String Msg
dragConfig =
Draggable.basicConfig OnDragBy
- For
OnDragBy
, which will be emitted when the user drags the element, the new position will be computed using theDelta
(dx, dy)
DragMsg
will be forwarded toDraggable.update
which takes care of both updating theDrag
state and sending the appropriate event commands. In order to do that, it receives thedragConfig
. As mentioned above, this function assumes that the model has adrag
field holding the internalDrag
state.
update : Msg -> Model -> ( Model, Cmd Msg )
update msg ({ position } as model) =
case msg of
OnDragBy ( dx, dy ) ->
let
( x, y ) =
position
in
( { model | position = ( round (toFloat x + dx), round (toFloat y + dy) ) }, Cmd.none )
DragMsg dragMsg ->
Draggable.update dragConfig dragMsg model
subscriptions : Model -> Sub Msg
subscriptions { drag } =
Draggable.subscriptions DragMsg drag
Inside your view
function, you must somehow make the element draggable. You do that by adding a trigger for the mousedown
event. You must also specify a key
for that element. This can be useful when there are multiple drag targets in the same view.
Of course, you'll also have to style your DOM element such that it reflects its moving position (with top: x; left: y
or transform: translate
)
view : Model -> Html Msg
view { position } =
Html.div
[ Draggable.mouseTrigger "my-element" DragMsg
-- , Html.Attributes.style (someStyleThatSetsPosition position)
]
[ Html.text "Drag me" ]
For working demos, see the basic example or the examples with multiple targets
If you want to trigger drags on touch events (i.e. on mobile platforms) as well
as mouse events, you need to add touchTriggers
to your elements. Building on
the previous example, it looks like this.
view : Model -> Html Msg
view { position } =
Html.div
[ Draggable.mouseTrigger "my-element" DragMsg
-- , Html.Attributes.style (someStyleThatSetsPosition position)
] ++ (Draggable.touchTriggers "my-element" DragMsg)
[ Html.text "Drag me" ]
The basic example demonstrates this as well.
Besides tracking the mouse moves, this library can also track all the other associated events related to dragging. But, before enumerating these events, it's import to note that an element is not considered to be dragging if the mouse was simply clicked (without moving). That allows tracking both click
and drag
events:
- "mouse down" + "mouse up" = "click"
- "mouse down" + "mouse moves" + "mouse up" = "drag"
So, the mouse events are:
onMouseDown
- on mouse press.onDragStart
- on the first mouse move after pressing.onDragBy
- on every mouse move.onDragEnd
- on releasing the mouse after dragging.onClick
- on releasing the mouse without dragging.
All of these events are optional, and can be provided to Draggable.customConfig
using an API similar to the one used by VirtualDom.node
to specify the Attribute
s. For example, if we want to handle all the events, we define the config
like:
import Draggable
import Draggable.Events exposing (onClick, onDragBy, onDragEnd, onDragStart, onMouseDown)
dragConfig : Draggable.Config String Msg
dragConfig =
Draggable.customConfig
[ onDragStart OnDragStart
, onDragEnd OnDragEnd
, onDragBy OnDragBy
, onClick CountClick
, onMouseDown (SetClicked True)
]
Note: If we need to handle mouseup
after either a drag
or a click
, we can use the DOM
event handler onMouseUp
from Html.Events
or Svg.Events
.
See the full example
By default, OnDragBy
message will have a Draggable.Delta
parameter, which, as we saw, is just an alias for (Float, Float)
. However, there are situations when we would like some other data type for representing our delta
.
Luckily, that's pretty easy using function composition. For example, we can use a Vec2 type from the linear-algebra
library, which provides handy function like translate
, scale
and negate
.
import Math.Vector2 as Vector2 exposing (Vec2)
type Msg
= OnDragBy Vec2
-- | ...
dragConfig : Draggable.Config Msg
dragConfig =
Draggable.basicConfig (OnDragBy << (\( dx, dy ) -> Vector2.vec2 dx dy))
There is actually an example right for this use-case
There are cases when we need some additional information (e.g. mouse offset) about the mousedown
event which triggers the drag. For these cases, there is an advanced customMouseTrigger
which also takes a JSON Decoder
for the MouseEvent
.
import Json.Decode as Decode exposing (Decoder)
type Msg
= CustomMouseDown (Draggable.Msg ()) (Float, Float)
-- | ...
update msg model =
case msg of
CustomMouseDown dragMsg startPoint ->
{ model | startPoint = startPoint }
|> Draggable.update dragConfig dragMsg
view { scene } =
Svg.svg
[ Draggable.customMouseTrigger () mouseOffsetDecoder CustomMouseDown
-- , ...
]
[]
mouseOffsetDecoder : Decoder (Float, Float)
mouseOffsetDecoder =
Decode.map2 (,)
(Decode.field "offsetX" Decode.float)
(Decode.field "offsetY" Decode.float)