In this article, we are going to implement the draggable functionality (found in jQuery) using vanilla JavaScript with just a few lines of code.
While the JavaScript specification itself does not have any built-in
dragging mechanism just yet, that does not mean that we can't built one
ourselves.
Overview
The drag process essentially will involve careful use of the mouse event handlers, such as mousedown, mousemove and mouseup.
A drag in this case means (in JavaScript terms), a mousemove event, but only after a mousedown event has registered. And only before a mouseup has triggered, completing the cycle.
And lastly, it also involves tracking just where you clicked on that particular element. If you clicked 50px down and 100px left inside of an element, you'll want to shift the element based on that location and not based on the top-left corner of the element.
We'll get more into offset's further down below. For now:
Add a 'drag' class
Because we want to make any element on the page draggable, we can add a specific 'drag' class that will flag that particular element as draggable.
<div class='drag box'></div>
<div class='drag box box2'></div>
<div class='drag box box3'></div>
You can use the following CSS to style the boxes shown above, just for this example.
.box{
height: 200px;
width: 200px;
background: gold;
margin: 0px;
display:inline-block;
}
.box2{
background:dodgerblue;
}
.box3{
background:tomato;
}
Define variables
In order to make this implementation work, we will need to keep track of a few important variables.
Define the following variables at the top of your script.
var mousedown = false;
var current;
var draggables;
var currentx = 0;
var currenty = 0;
var offsetx = 0;
var offsety = 0;
var newx = 0;
var newy = 0;
var marginTop = 0;
var marginLeft = 0;
Here is a quick explanation of each of those variables:
mousedown - Will track if a mousedown event is currently occuring
current - A reference to the current element that is being dragged
draggables - The collection of all draggable elements on the page
currentx - Represents the current x coordinate (top-left) of an element being dragged
currenty - Represents the current y coordinate (top-left) of an element being dragged
offsetx - Represents the offset between the element's top-left coordinate and the mouse x coordinate
offsety - Represents the offset between the element's top-left coordinate and the mouse y coordinate
newx - Represents the new x coordinate that an element will occupy when being dragged
newy - Represents the new y coordinate that an element will occupy when being dragged
marginTop - Represents an elements margin-top
marginLeft - Represents an elements margin-left
The meaning of the variables will become more clear as we break down the event handlers below.
Add the event handlers
Next up, we need to bind each of the mentioned event handlers to all elements that include the drag class. And we can do that on page load.
window.addEventListener('load', function(){
draggables = document.querySelectorAll('.drag');
We're going to loop through each element found and to bind the mouse event handlers, starting with the mouseDown event as it represents the first step in the 3 step process.
MouseDown event
**Note - You can copy and paste the code on this page from top to bottom as it will be sequential. Here is the full mousedown event handler that we'll be using.
for (var i = 0; i < draggables.length; i++) {
var item = draggables[i];
item.onmousedown = (function() {
var index = i;
return function() {
mousedown = true;
current = draggables[index];
marginTop = parseInt(window.getComputedStyle(current, null).getPropertyValue('margin-top'));
marginLeft = parseInt(window.getComputedStyle(current, null).getPropertyValue('margin-left'));
let rect = current.getBoundingClientRect();
current.style.position = "fixed";
current.style.cursor = "pointer";
current.style.zIndex = "1000";
currentx = rect.left - marginLeft;
currenty = rect.top - marginTop;
}
})();
In the order of occurrence, we first set the mousedown variable to true and set the current variable to the element that is being dragged.
Next up, we are going to set the marginTop and marginLeft variables to the current element's margin properties, and we can grab those with the getComputedStyle() method.
Why do we need to store these margins you may wonder? Well, we need to take the margin into account so that the new drag location also takes into account that spacing. If there is a margin of 10px set for example around an element, the new location would be off by 10px, showing a weird looking instant shift of those10 pixels. We need to adjust for that.
In the mousedown handler, we are mainly just going to be setting the current element's position to fixed and calculating it's x and y coordinates (top-left location). Because this is only the mousedown step, we don't actually update the coordinates in any way. That happens in the next step.
MouseMove event
The mousemove event handler will only actually do anything if the mousedown variable is set to true, implying a drag is taking place.
If that is so, then its time to check the offset. If the offsetx and offsety are set to 0, then this is the first initial movement on that particular element. And if that's the case, we will need to set the offset to the mouse x and y coordinates minus the currentx and currenty values.
Here is a visual look at the offset logic. Note that the blue box represents the margin's in this case.
The code for the mousemove handler is the following:
item.addEventListener('mousemove', function (e) {
if (mousedown) {
if (offsetx == 0 && offsety == 0) {
offsetx = (e.clientX - currentx);
offsety = (e.clientY - currenty);
}
newx = (e.clientX - offsetx);
newy = (e.clientY - offsety);
current.style.left = newx + "px";
current.style.top = newy + "px";
current.style.bottom = 'auto';
current.style.right = 'auto';
}
});
The reason for all of this offset logic isn't obvious at first. But essentially, you want the elements top-left corner to remain where it is and not to jump to the mouse cursor's location.
We can't use the elements top, left, right or bottom coordinates to shift as that would cause the new top-left location of the element to appear where the mouse cursor location is.
The following diagram will explain further what I mean.
You want to shift the element based on that exact spot clicked, and not based on it's top-left coordinates. And that is what the offset represents. It may seem trivial, but if not accounted for correctly the drag animation will be off and will give a poorer user experience.
MouseUp event
And lastly, the mouseup event is bound to the element, which will simply just take us out of drag mode.
item.onmouseup = (function() {
return function() {
mousedown = false;
currentx = 0;
currenty = 0;
offsetx = 0;
offsety = 0;
marginTop = 0;
marginLeft = 0;
}
})();
This is the simplest function as all we are doing is basically resetting all of the drag variables back to 0 and the mousedown flag back to false.
Demo
Here is a running demo with 3 draggable components for your dragging pleasure.