DragânâDrop is a great interface solution. Taking something and dragging and dropping it is a clear and simple way to do many things, from copying and moving documents (as in file managers) to ordering (dropping items into a cart).
In the modern HTML standard thereâs a section about Drag and Drop with special events such as dragstart, dragend, and so on.
These events allow us to support special kinds of dragânâdrop, such as handling dragging a file from OS file-manager and dropping it into the browser window. Then JavaScript can access the contents of such files.
But native Drag Events also have limitations. For instance, we canât prevent dragging from a certain area. Also we canât make the dragging âhorizontalâ or âverticalâ only. And there are many other dragânâdrop tasks that canât be done using them. Also, mobile device support for such events is very weak.
So here weâll see how to implement DragânâDrop using mouse events.
DragânâDrop algorithm
The basic DragânâDrop algorithm looks like this:
- On
mousedownâ prepare the element for moving, if needed (maybe create a clone of it, add a class to it or whatever). - Then on
mousemovemove it by changingleft/topwithposition:absolute. - On
mouseupâ perform all actions related to finishing the dragânâdrop.
These are the basics. Later weâll see how to add other features, such as highlighting current underlying elements while we drag over them.
Hereâs the implementation of dragging a ball:
ball.onmousedown = function(event) {
// (1) prepare to moving: make absolute and on top by z-index
ball.style.position = 'absolute';
ball.style.zIndex = 1000;
// move it out of any current parents directly into body
// to make it positioned relative to the body
document.body.append(ball);
// centers the ball at (pageX, pageY) coordinates
function moveAt(pageX, pageY) {
ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
ball.style.top = pageY - ball.offsetHeight / 2 + 'px';
}
// move our absolutely positioned ball under the pointer
moveAt(event.pageX, event.pageY);
function onMouseMove(event) {
moveAt(event.pageX, event.pageY);
}
// (2) move the ball on mousemove
document.addEventListener('mousemove', onMouseMove);
// (3) drop the ball, remove unneeded handlers
ball.onmouseup = function() {
document.removeEventListener('mousemove', onMouseMove);
ball.onmouseup = null;
};
};
If we run the code, we can notice something strange. On the beginning of the dragânâdrop, the ball âforksâ: we start dragging its âcloneâ.
Hereâs an example in action:
Try to dragânâdrop with the mouse and youâll see such behavior.
Thatâs because the browser has its own dragânâdrop support for images and some other elements. It runs automatically and conflicts with ours.
To disable it:
ball.ondragstart = function() {
return false;
};
Now everything will be all right.
In action:
Another important aspect â we track mousemove on document, not on ball. From the first sight it may seem that the mouse is always over the ball, and we can put mousemove on it.
But as we remember, mousemove triggers often, but not for every pixel. So after swift move the pointer can jump from the ball somewhere in the middle of document (or even outside of the window).
So we should listen on document to catch it.
Correct positioning
In the examples above the ball is always moved so that its center is under the pointer:
ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
ball.style.top = pageY - ball.offsetHeight / 2 + 'px';
Not bad, but thereâs a side effect. To initiate the dragânâdrop, we can mousedown anywhere on the ball. But if âtakeâ it from its edge, then the ball suddenly âjumpsâ to become centered under the mouse pointer.
It would be better if we keep the initial shift of the element relative to the pointer.
For instance, if we start dragging by the edge of the ball, then the pointer should remain over the edge while dragging.
Letâs update our algorithm:
-
When a visitor presses the button (
mousedown) â remember the distance from the pointer to the left-upper corner of the ball in variablesshiftX/shiftY. Weâll keep that distance while dragging.To get these shifts we can substract the coordinates:
// onmousedown let shiftX = event.clientX - ball.getBoundingClientRect().left; let shiftY = event.clientY - ball.getBoundingClientRect().top; -
Then while dragging we position the ball on the same shift relative to the pointer, like this:
// onmousemove // ball has position:absolute ball.style.left = event.pageX - shiftX + 'px'; ball.style.top = event.pageY - shiftY + 'px';
The final code with better positioning:
ball.onmousedown = function(event) {
let shiftX = event.clientX - ball.getBoundingClientRect().left;
let shiftY = event.clientY - ball.getBoundingClientRect().top;
ball.style.position = 'absolute';
ball.style.zIndex = 1000;
document.body.append(ball);
moveAt(event.pageX, event.pageY);
// moves the ball at (pageX, pageY) coordinates
// taking initial shifts into account
function moveAt(pageX, pageY) {
ball.style.left = pageX - shiftX + 'px';
ball.style.top = pageY - shiftY + 'px';
}
function onMouseMove(event) {
moveAt(event.pageX, event.pageY);
}
// move the ball on mousemove
document.addEventListener('mousemove', onMouseMove);
// drop the ball, remove unneeded handlers
ball.onmouseup = function() {
document.removeEventListener('mousemove', onMouseMove);
ball.onmouseup = null;
};
};
ball.ondragstart = function() {
return false;
};
In action (inside <iframe>):
The difference is especially noticeable if we drag the ball by its right-bottom corner. In the previous example the ball âjumpsâ under the pointer. Now it fluently follows the pointer from the current position.
Potential drop targets (droppables)
In previous examples the ball could be dropped just âanywhereâ to stay. In real-life we usually take one element and drop it onto another. For instance, a âfileâ into a âfolderâ or something else.
Speaking abstract, we take a âdraggableâ element and drop it onto âdroppableâ element.
We need to know:
- where the element was dropped at the end of DragânâDrop â to do the corresponding action,
- and, preferably, know the droppable weâre dragging over, to highlight it.
The solution is kind-of interesting and just a little bit tricky, so letâs cover it here.
What may be the first idea? Probably to set mouseover/mouseup handlers on potential droppables?
But that doesnât work.
The problem is that, while weâre dragging, the draggable element is always above other elements. And mouse events only happen on the top element, not on those below it.
For instance, below are two <div> elements, red one on top of the blue one (fully covers). Thereâs no way to catch an event on the blue one, because the red is on top:
<style>
div {
width: 50px;
height: 50px;
position: absolute;
top: 0;
}
</style>
<div style="background:blue" onmouseover="alert('never works')"></div>
<div style="background:red" onmouseover="alert('over red!')"></div>
The same with a draggable element. The ball is always on top over other elements, so events happen on it. Whatever handlers we set on lower elements, they wonât work.
Thatâs why the initial idea to put handlers on potential droppables doesnât work in practice. They wonât run.
So, what to do?
Thereâs a method called document.elementFromPoint(clientX, clientY). It returns the most nested element on given window-relative coordinates (or null if given coordinates are out of the window). If there are multiple overlapping elements on the same coordinates, then the topmost one is returned.
We can use it in any of our mouse event handlers to detect the potential droppable under the pointer, like this:
// in a mouse event handler
ball.hidden = true; // (*) hide the element that we drag
let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
// elemBelow is the element below the ball, may be droppable
ball.hidden = false;
Please note: we need to hide the ball before the call (*). Otherwise weâll usually have a ball on these coordinates, as itâs the top element under the pointer: elemBelow=ball. So we hide it and immediately show again.
We can use that code to check what element weâre âflying overâ at any time. And handle the drop when it happens.
An extended code of onMouseMove to find âdroppableâ elements:
// potential droppable that we're flying over right now
let currentDroppable = null;
function onMouseMove(event) {
moveAt(event.pageX, event.pageY);
ball.hidden = true;
let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
ball.hidden = false;
// mousemove events may trigger out of the window (when the ball is dragged off-screen)
// if clientX/clientY are out of the window, then elementFromPoint returns null
if (!elemBelow) return;
// potential droppables are labeled with the class "droppable" (can be other logic)
let droppableBelow = elemBelow.closest('.droppable');
if (currentDroppable != droppableBelow) {
// we're flying in or out...
// note: both values can be null
// currentDroppable=null if we were not over a droppable before this event (e.g over an empty space)
// droppableBelow=null if we're not over a droppable now, during this event
if (currentDroppable) {
// the logic to process "flying out" of the droppable (remove highlight)
leaveDroppable(currentDroppable);
}
currentDroppable = droppableBelow;
if (currentDroppable) {
// the logic to process "flying in" of the droppable
enterDroppable(currentDroppable);
}
}
}
In the example below when the ball is dragged over the soccer goal, the goal is highlighted.
#gate {
cursor: pointer;
margin-bottom: 100px;
width: 83px;
height: 46px;
}
#ball {
cursor: pointer;
width: 40px;
height: 40px;
}<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<p>Drag the ball.</p>
<img src="https://en.js.cx/clipart/soccer-gate.svg" id="gate" class="droppable">
<img src="https://en.js.cx/clipart/ball.svg" id="ball">
<script>
let currentDroppable = null;
ball.onmousedown = function(event) {
let shiftX = event.clientX - ball.getBoundingClientRect().left;
let shiftY = event.clientY - ball.getBoundingClientRect().top;
ball.style.position = 'absolute';
ball.style.zIndex = 1000;
document.body.append(ball);
moveAt(event.pageX, event.pageY);
function moveAt(pageX, pageY) {
ball.style.left = pageX - shiftX + 'px';
ball.style.top = pageY - shiftY + 'px';
}
function onMouseMove(event) {
moveAt(event.pageX, event.pageY);
ball.hidden = true;
let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
ball.hidden = false;
if (!elemBelow) return;
let droppableBelow = elemBelow.closest('.droppable');
if (currentDroppable != droppableBelow) {
if (currentDroppable) { // null when we were not over a droppable before this event
leaveDroppable(currentDroppable);
}
currentDroppable = droppableBelow;
if (currentDroppable) { // null if we're not coming over a droppable now
// (maybe just left the droppable)
enterDroppable(currentDroppable);
}
}
}
document.addEventListener('mousemove', onMouseMove);
ball.onmouseup = function() {
document.removeEventListener('mousemove', onMouseMove);
ball.onmouseup = null;
};
};
function enterDroppable(elem) {
elem.style.background = 'pink';
}
function leaveDroppable(elem) {
elem.style.background = '';
}
ball.ondragstart = function() {
return false;
};
</script>
</body>
</html>Now we have the current âdrop targetâ, that weâre flying over, in the variable currentDroppable during the whole process and can use it to highlight or any other stuff.
Summary
We considered a basic DragânâDrop algorithm.
The key components:
- Events flow:
ball.mousedownâdocument.mousemoveâball.mouseup(donât forget to cancel nativeondragstart). - At the drag start â remember the initial shift of the pointer relative to the element:
shiftX/shiftYand keep it during the dragging. - Detect droppable elements under the pointer using
document.elementFromPoint.
We can lay a lot on this foundation.
- On
mouseupwe can intellectually finalize the drop: change data, move elements around. - We can highlight the elements weâre flying over.
- We can limit dragging by a certain area or direction.
- We can use event delegation for
mousedown/up. A large-area event handler that checksevent.targetcan manage DragânâDrop for hundreds of elements. - And so on.
There are frameworks that build architecture over it: DragZone, Droppable, Draggable and other classes. Most of them do the similar stuff to whatâs described above, so it should be easy to understand them now. Or roll your own, as you can see that thatâs easy enough to do, sometimes easier than adapting a third-party solution.
Comments
<code>tag, for several lines â wrap them in<pre>tag, for more than 10 lines â use a sandbox (plnkr, jsbin, codepenâ¦)