DEV Community

Michael Jones for Zaptic

Posted on

Right clicks & Elm

At Zaptic, we allow our customers to create a flow chart describing the business processes that they would like to complete. We draw the flow chart in SVG using a pan & zoom interface to make it easy to look around. We track mouse down, mouse drag & mouse up events to manage this UI.

Unfortunately, in development, the experience was a little annoying as every time we loaded the browser's inspector on an element the mousedown of the right-click would be tracked by the UI but the mouseup would be over the context menu and so would not register. The result? The UI gets left in "pan" mode and attempts to follow your mouse everywhere even though you don't have any mouse buttons pressed any more.

To overcome this, we needed to look into tracking which mouse button was being used so that we could ignore the right-clicks. We are using the elm-lang/mouse package which includes a position decoder that can be used for mousedown events. That decoder only provides the mouse location as pageX and pageY, it doesn't provide any information about which button was pressed so we need to go a little deeper.

If we take a look at the source code for the module we can see that the position decoder is doing the following:

position : Json.Decoder Position
position =
  Json.map2 Position
    (Json.field "pageX" Json.int)
    (Json.field "pageY" Json.int)
Enter fullscreen mode Exit fullscreen mode

It is looking for the pageX and pageY properties, as we suspected, and it expects them to be integers. If we look at the lovely MDN documentation for a mousedown event we can see that it has lots of other properties including a integer button property which contains some indication of which button is being used. We can set up our own type & decoder as:

type alias MouseClick =
    { pageX : Int
    , pageY : Int
    , button : Int
    }


mouseClickDecoder : Json.Decode.Decoder PFM.MouseClick
mouseClickDecoder =
    Json.Decode.map3 PFM.MouseClick
        (Json.Decode.field "pageX" Json.Decode.int)
        (Json.Decode.field "pageY" Json.Decode.int)
        (Json.Decode.field "button" Json.Decode.int)

Enter fullscreen mode Exit fullscreen mode

And adapt our event handler to use it:

type Msg
    = DragStart MouseClick
    | ...

onMouseDown : Attribute Msg
onMouseDown =
    on "mousedown" (Json.Decode.map DragStart mouseClickDecoder)
Enter fullscreen mode Exit fullscreen mode

Then in our update function that is going to handle the DragStart message we can check the value of button and ignore right-clicks. There are some inconsistencies between browsers on how different buttons are represented as integers which you can read about on quirksmode. Fortunately they all align for the 'right-click' which is represented as 2. So we can ignore that:

DragStart click ->
    -- Ignore right-clicks which are given value '2' by browsers
    if click.button /= 2 then
        let
            pos =
                { x = click.pageX, y = click.pageY }
        in
        ({ model | = drag = Just { start = pos, current = pos } }, Cmd.none)
    else
        (model, Cmd.none)
Enter fullscreen mode Exit fullscreen mode

And there we have it! We're free of the right-clicks and can happily jump into the browser dev-tools whenever we like.

If you have any thoughts or advice, please let me know! Always happy to learn.

Update: Checkout Ilias' comment below. It really cleans up the message handling in the update:

-        DragStart click ->
-            -- Ignore right-clicks which are given value '2' by browsers
-            if click.button /= 2 then
-                let
-                    pos =
-                        { x = click.pageX, y = click.pageY }
-                in
-                { model | drag = Just { start = pos, current = pos } } ! []
-            else
-                model ! []
+        DragStart pos ->
+            { model | drag = Just { start = pos, current = pos } } ! []
Enter fullscreen mode Exit fullscreen mode

Top comments (2)

Collapse
 
zwilias profile image
Ilias Van Peer

One possibility to keep ignored events out of your update and - incidentally - encourage some more reuse, is to move the decision into the decoder:

mouseLeftClick : Json.Decoder Position
mouseLeftClick =
    Json.field "button" Json.int
        |> Json.andThen (\button ->
            if button == 2 then
                fail "I'm ignored!"
            else
                Mouse.position
        )

The gist is that if an event-decoder fails, the event just happens but no message is sent to Elm.

Collapse
 
michaeljones profile image
Michael Jones

Thanks Ilias! That is much cleaner. A better expression of the intention as well. I'll update our code :)