Things to try with a mouse:
- Drag outside the diagram, and come back {should still be dragging}
- Drag outside the diagram, and let go of the mouse button, then come back into the diagram {should stop dragging}
- Drag outside the window, then let go of the mouse button {should stop dragging}
- Right click on the circle {should not drag, but bring up the context menu}
- Right button drag the circle {should not drag, but bring up the context menu on Mac or Linux, or do nothing on Windows}
- Drag with the middle button {should not drag, but enable autoscroll}
- Drag with the left button, then hold down right button, continue drag, then release left button, then move mouse, then release right button {should stop dragging when releasing left button; I want a different behavior from this test[1]}
- Chords¹: drag with the left button, then hold down Ctrl, then release the left button {Mac/Firefox behaves differently, giving
pointerdown.left
and thenpointerup.right
whereas other browsers givepointerdown.left
andpointerup.left
} - While dragging, Alt+Tab (Cmd+Tab) to switch windows, then let go of the mouse button {should stop dragging}
- While dragging, use keyboard to scroll {should still be dragging² ⁵}
- While dragging, use scrollwheel to scroll {should still be dragging²}
- While dragging, use middle button to scroll {should still be dragging²}
- While dragging, resize the window {should still be dragging²}
- Select all text on page, then drag the circle {should drag the circle, not the text}
- Plug in two mice, start dragging on one, click on the other {want this to continue dragging, but it won’t³}
- On Mac, use an iPad as a second screen, drag with stylus on a mac browser {should drag}
- While dragging, put the computer to sleep and then wake it up and continue the drag {??}
- While dragging, unplug the mouse and plug it back in {??}
Things to try with touch:
- Drag the circle up or down {should not scroll the page}
- Drag the diagram up or down {should scroll the page}
- Long press on the circle {should not select the text or bring up a menu}
- Use two separate fingers to drag two separate objects {should both drag independently}
- Use two separate fingers to drag one object {should only drag with the first finger⁴}
- Drag with one finger then try to pinch zoom with a second finger {should not zoom}
- Drag with one finger then try to pinch zoom with two other fingers {does not zoom, not sure what it should do}
- Drag with one finger then rotate the phone {will rotate and continue dragging on iOS; will not rotate on Android}
- On iPad, use Mac “continuity” to drag with the mouse on an ipad browser {should drag}
Notes:
- ¹solution based on pointermove getting called for button changes[2]
- ²it won’t update until you nudge the mouse pointer
- ³this is a limitation of the OS level mouse handling
- ⁴I have a solution but rarely use it
- ⁵Firefox will fire
mouseover
,mouseenter
whereas Chrome will firepointerover
,pointerenter
,mouseover
,mouseenter
. There are a lot of differences in over/enter events but I haven’t investigated them yet.
1 SVG single element#
- Basic tests work
- On desktop, scrolling while dragging doesn’t drag until you move your mouse a tiny amount. This could be solved either with a timer or by catching the scroll event. Low priority for me.
I use this helper function quite a bit when working with SVG:
/** Convert from event coordinate space (on the page) to SVG coordinate * space (within the svg, honoring responsive resizing, width/height, * and viewBox) */ function convertPixelToSvgCoord(event) { const svg = event.currentTarget.ownerSVGElement; let p = svg.createSVGPoint(); p.x = event.clientX; p.y = event.clientY; return p.matrixTransform(svg.getScreenCTM().inverse()); }
This function gives coordinates for the <svg>
doesn’t handle other SVG transforms; see the parent transform section for a version that does.
2 SVG nested element with text#
- I use pointer capture on
currentTarget
nottarget
. - While dragging, I set CSS user-select: none. This prevents selecting the text by dragging or double clicking (desktop) or long press (mobile). But I don’t want this all the time. When not dragging, I want the text to be selectable.
- On desktop, if you select the text of the page and then try to drag it, it will let you drag that text outside the browser. But if you also try to drag this circle the two drag handlers will intefere. Use preventDefault() on the
dragstart
event to fix this.
3 Canvas painting#
The same drag event handlers can be used for painting instead of moving an object:
4 Canvas dragging#
- ⁴if I put a second finger on the diagram it had been jumping to the second finger’s position. This is because the same is getting the
pointermove
event for the second finger, whereas in the other demos a different element gets the events for the second finger. To fix this, I checked the.pointerId
field in thepointermove
event to make sure it matches the.pointerId
from thepointerdown
event.
5 HTML div absolute positioned#
- It is possible to place the box outside the container by placing it on the right, then shrinking the browser size. I’m going to leave this up to the application and not try to solve it generically.
6 HTML div css transform#
7 HTML div with a link#
- The dragging code intercepts the left button so you can’t click on the link.
- You can still middle click or right click on the link.
However in this demo you can click the link or drag with it:
This was a little tricky and I don’t know if it handles all cases but:
- on
pointerdown
, don’t capture; instead, set a flag moved=false - on
pointermove
, ifmoved
is stillfalse
, capture - on
pointermove
, set moved=true - on
click
, ifmoved
istrue
,event.preventDefault()
In general though I don’t try to do this type of thing. I don’t put links inside my draggable objects.
8 Draggable with a nested draggable#
I don’t have a test here but I wrote one on another site[3], where both the yellow and red boxes are draggable. To handle this case:
- use
stopPropagation()
to prevent the pointer event from going up to the parent element
9 Click events#
This stackoverflow question[4] made me wonder what happens when we have both click and drag event handlers on the same object. The idea here is to listen to both types of events. If we detect a pointermove
, then don’t process the click
. Maybe it needs to have some threshold of how many pixels we moved before we suppress the click. I also wrote up a full demo on the stackoverflow page.
10 Coordinate transform on parent#
The draggable element might be inside another element that has a transform
applied to it.
The problem is that the convertPixelToSvgCoord
function I use calculates coordinates relative to the <svg>
element. This is fine if your draggable object is positioned relative to the <svg>
’s coordinate system, but in this test there’s <svg>
then <g>
with a rotate()
on it. The pointer coordinates are in screen coordinates, and we need to unrotate them if we want the draggable to be placed within the rotated <g>
.
To handle this, extend the convertPixelToSvgCoord
function to take an element the coordinate should be relative to, instead of assuming the coordinates should be in the <svg>
’s coordinate space.
/** Convert from event coordinate space (on the page) to SVG coordinate * space (within the svg, honoring responsive resizing, width/height, * and viewBox) */ function convertPixelToSvgCoord(event, relativeTo=event.currentTarget.ownerSVGElement) { // if relativeTo is the <svg> then its ownerSVGElement is null, so // we want to point back to the <svg> but otherwise we assume it's // a child of <svg> and we want to find the <svg> let p = (relativeTo.ownerSVGElement ?? relativeTo).createSVGPoint(); p.x = event.clientX; p.y = event.clientY; return p.matrixTransform(relativeTo.getScreenCTM().inverse()); }
However, this function only handles transforms on the SVG elements. For transforms on non-SVG elements, I don’t know what to do. This is an unsolved issue, not only for me, but also other libraries like d3.js. See d3-selection issue #67[5] and firefox bug #1610093[6] . There’s probably something we can do using window.getComputedStyle(element)
but I haven’t tried.
11 Scrubbable number#
On some of my pages, like Probability of Damage Rolls, I have some numbers in the text of the page, and those numbers are “scrubbable”. The reader can drag them left/right to change the value. I want to be able to handle that with the same dragging code that I use for dragging objects.
It’s a constrained drag. I ignore the y
value from the drag. But the object doesn’t actually move around on the screen. The x
value is displayed as text instead of being used to set the position. This example, and those on my constrained dragging page, show how the same basic recipe can be used for a wide variety of situations.
12 Painting state#
I don’t have a demo here but look at Painting on a map, where the initialpointerdown
event remembers what color we’re painting, and then all subsequent pointermove
events paint in that color. If you first painted in blue, then you continue painting in blue until the pointerup
.