DnD API - Draggable Elements
Learning outcomes:
- A quick recap of what is 'draggable'
- The
dragstart
event - The
dragend
event - The
drag
event - Pitfalls of
draggable
Introduction
In the previous JavaScript Drag and Drop Basics chapter, we learnt how to set up an elementary drag-and-drop feature on a webpage.
There we got introduced to the term 'draggable element'. It simply refers to an element which is capable of being dragged and ultimately dropped over a 'dropzone' — another term which refers to an element where stuff could be dropped.
In this chapter, we shall get a quick recap of working with draggable elements and then see how to configure them using multiple event handlers such as those for dragstart
, dragend
and drag
.
Finally, we'll see how to set a custom drag image and some issues following from making an element draggable.
So without wasting any more time, let's get to the real stuff!
A quick recap
We'll start off by giving a quick recap of what is meant when we say that something is draggable.
The dragging action is done when the user presses the mouse button while the pointer is over the respective element and then moves the mouse while the button is still held down.
By default, only three things on a webpage are draggable:
- Links (given by
<a>
) - Images
- Text selections
That's it!
All other stuff, such as <div>
, <p>
, <input>
, and so on, are non-draggable.
Performing the drag action on a non-draggable element has no meaningful consequence. If the element has text in it, for instance, then the text usually gets selected by the user, instead of the element being dragged as a whole unit.
Anyways, the most important question in this regards is how do we make any given element draggable?
Can we even do so?
A big yes! We already know how to do so from the previous chapter.
To make any given element draggable, we simply set the draggable
HTML attribute on it to the value "true"
.
An example follows:
We have a simple <div>
to start with which we customise just like we did in the previous chapter — giving it a large colored clickable region.
<div>Drag and Drop</div>
div {
backgrond-color: #fff493; /* light tint of yellow */
padding: 50px;
display: inline-block
}
Try dragging this <div>
. You'll most probably end up making a text selection inside it — this is because we can't drag it as of yet.
Now we'll add the draggable
attribute to it and see the effect:
<div draggable="true">Drag and Drop</div>
Here's the new output:
Now try dragging the <div>
. Did you notice any change?
A vital point to note over here is that draggable
is not a Boolean attribute unlike some other HTML attributes such as required
. That is, we couldn't just write draggable
on an HTML element and expect is to become draggable.
Not all all!
Rather, we have to give the attribute a value: "true"
means that the element is draggable, while "false"
means that the element in not draggable.
Simple.
Following from all this knowledge, the following snippets show redundant pieces of code:
<!--redundant-->
<div draggable="false">Some text</div>
<!--redundant-->
<img src="some-image.png" draggable="true">
In the first snippet, we set draggable="false"
on a <div>
element. As stated earlier, <div>
elements are non-draggable by default, so it would surely be redundant to again specify this manually.
In the second snippet, we set draggable="true"
on an <img>
element. Once again, based on what we learn just right now, <img>
elements are draggable by default; hence they already have their draggable
attribute set to "true"
. Setting this once again while defining the element is redundant.
Alright, so this was it for the recap of what 'draggable' means and that how could we make any given element draggable.
It's time to see how to configure a draggable element now that we have one...
The draggable
DOM property
The HTMLElement
interface defines a draggable
property that specifies whether or not a given element is draggable.
When retrieved, it returns back a Boolean value representing the draggability of the element.
Note that the draggable
property does NOT necessarily return back the value of the draggable
HTML attribute. The attribute doesn't even need to be present in order for the property to have a non-null
value.
For example, in the following code, the <img>
element doesn't have a draggable
attribute on it:
<img src="image.png">
yet if we head over to the console and inspect the draggable
DOM property of the corresponding element node, we get true
returned:
var imageElement = document.querySelector('div')
true
However, if we inspect the attribute's value directly via the getAttribute()
method, we get null
returned since the element doesn't have such an attribute on it:
imageElement.getAttribute('draggable')
null
What this means is that the DOM draggable
property actually tells whether the element is draggable or not; it does NOT return the value of the draggable
HTML attribute of the element.
The draggable
property can be set as well. In this case, it leads to the draggable
HTML attribute being set on the element , with the provided value.
For example, given the following HTML:
<div>A div</div>
as soon as we execute the code below:
var divElement = document.querySelector('div');
divElement.draggable = true;
the <div>
element gets a draggable
attribute on it. This can be confirmed by a simple getAttribute()
inspection:
divElement.getAttribute('draggable')
true
The value provided to the draggable
property is first coerced into a Boolean and then the resulting value is used to set the draggable
attribute on the underlying element.
In the snippet below, we set the draggable
property of the <div>
to an empty string (''
) and, thus, get the corresponding attribute on it to have the value "false"
:
divElement.draggable = ''
''
divElement.getAttribute('draggable')
'false'
Although, such an approach works, it's NOT recommended to provided a non-Boolean value to the draggable
property, as it can lead to unexpected results.
For example, in the following snippet, we manually set the draggable
property to the string 'false'
, but the corresponding attribute on the element turns out to get a completely different value:
divElement.draggable = 'false'
''
divElement.draggable
true
divElement.getAttribute('draggable')
'true'
This is simply because of the value provided to draggable
being coerced to a Boolean. When we set the property to 'false'
, 'false'
becomes true
, and that's what gets assigned to the property. This ultimately creates a draggable
attribute on the element with the value "true"
.
Likewise, whenever working with the draggable
DOM property, always use Booleans.
The dragstart
event
Throughout the lifecycle of a drag-and-drop operation, three events fire on a draggable element (given that it's part of a webpage) as its dragged, and ultimately dropped at some location.
They are as follows:
dragstart
— indicates that the drag has begun.drag
— indicates that the element is being dragged.dragend
— indicates that the dragging operation has ended.
First things first, in this section, we'll go over the dragstart
event and see its significance in creating a drag-and-drop effect from scratch.
Starting with the definition:
dragstart
event fires on a draggable element as soon as we begin to drag it.As the name implies, the dragstart
event denotes the 'start' of the drag action.
In almost all cases, we need to listen to this event in order to make our custom drag-and-drop functionality meaningful.
But why? Well, let's see.
Initializing some data upon dragstart
When we begin to drag an element, it's important that we initialize some data related to the drag at this stage, which we could then retrieve later on when we drop the respective element in a dropzone.
For instance, if we drag a <div>
from a list of draggable <div>
s, we may want to save a reference to it upon dragstart
, which could be used when we drop the <div>
over a dropzone. In this way, we would know which element was dragged-and-dropped onto the dropzone, and consequently be able to perform any desired set of actions on that element.
Without saving any sort of data while dragging an element, which was not draggable by default, would make the whole DnD functionality totally meaningless. We would have no clue as to what was dragged, as we'd have absolutely no data to work with!
For elements draggable by default — such as images, links — it won't be absurd if we left out handling dragstart
for them, since the browser automatically saves some sort of data for them (in its default dragstart
action).
But regardless, this data isn't very useful — either way, a developer has to tap into the dragstart
event when making a custom DnD functionality and initialize some kind of data upon its occurrence.
Coming back to the dragstart
event, let's consider a quick example to see when dragstart
fires:
<div draggable="true">Drag and Drop</div>
We have the same <div>
as before, and the same CSS as well.
For the scripting part, we register a dragstart
handler on the <div>
, making a simple log in there:
var divElement = document.querySelector('div');
divElement.ondragstart = function(e) {
console.log('Drag has started!');
}
Let's try out this code in order to see when exactly does dragstart
fire.
As soon as we press the mouse down while the pointer is over the element and then move the pointer, even a pixel away from its initial location, the dragstart
event occurs.
Time to consider another example.
We'll use the same HTML markup as before. But this time, instead of making a generic log, we'll save the reference of the dragged element inside a global variable draggedElement
:
<div draggable="true">Drag and Drop</div>
var draggedElement;
var divElement = document.querySelector('div');
divElement.ondragstart = function(e) {
draggedElement = e.target;
}
With this in place, as soon as we start to drag the <div>
, the global variable draggedElement
would store its reference. This would be used later on when we drop this element over a dropzone.
Let's now create the dropzone where we could drop this <div>
.
As was the case in the previous JavaScript Drag and Drop — Basics chapter, we'll create a <section>
element to act as the dropzone:
<div draggable="true">Drag and Drop</div>
<section></section>
The CSS below customizes the size of the <section>
and gives it a background color so that we are able to easily drag and drop items over it:
section {
height: 200px;
width: 500px;
background-color: #eee;
margin-top: 20px
}
The following code turns this <section>
element into a dropzone:
/* ... previous code ... */
var sectionElement = document.querySelector('section');
sectionElement.ondragover = function(e) {
e.preventDefault();
}
sectionElement.ondrop = function(e) {
this.appendChild(draggedElement);
}
Inside the dragover
handler, we call e.preventDefault()
in order to cancel the browser's default action associated for dragover
. This is necessary to be able to receive a subsequent drop
event, as we learnt in the previous JavaScript Drag and Drop — Basics chapter.
Moving on, inside the drop
handler, we retrieve the global variable draggedElement
and append it inside the <section>
by means of calling appendChild()
.
This, altogether, creates a simplistic and fully-functional drag-and-drop feature; one that we've already seen in the previous chapter.
Let's take a look at it again.
The dragend
event
Let's now talk about the dragend
event.
If we just focus on the naming of the dragend
event, it seems to be the opposite of dragstart
. Guess what — it actually is!
dragend
event fires as soon as a drag operation ends.When does a drag operation end? Well, it ends as soon as we release the mouse button after having dragged a draggable item, or when we press the Esc key during the drag.
If the drag operation ends by dropping the item over a dropzone, then the dragend
event gets fired after drop
. In other cases (i.e. when the drag operation doesn't end by dropping the item over a dropzone), drop
doesn't fire, so this distinction as to which event fires first obviously fades away.
Anyways, coming back to the event, what do you think can we possibly do upon the dragend
event?
Do you think it is a useful event to handle?
Hmm... Let's see.
If you make some visual changes to an element as soon as it's dragged, by handling its dragstart
event, then it's important that you revert those when the drag operation finishes.
Note that it doesn't always have to be the case that the drag operation finishes inside a dropzone — it could well finish outside one as well. In this case, it won't be possible for us to revert the styles of the source element if we made them inside the drop
's handler, since it would only fire if the drag finished inside a dropzone.
So it turns out that reverting style changes made to an element on dragstart
can't be done reliably inside the drop
event's handler. This is because there is no guarantee that the drag would always end with a drop
.
The best choice is to use dragend
for this purpose.
As stated earlier, dragend
fires on a draggable element as soon as its dragging finishes. It doesn't impose any sort of restrictions to finish the drag inside a dropzone — you could finish the drag anywhere on or outside the webpage, and dragend
would regardless fire.
Let's consider this example for real.
Below we have the same code as before, with the same <div>
element and the same <section>
dropzone. The script is also the same. Didn't we say 'same' quite many times?
<div draggable="true">Drag and Drop</div>
<section></section>
var draggedElement;
var divElement = document.querySelector('div');
var sectionElement = document.querySelector('section');
divElement.ondragstart = function(e) { /* ... */ }
sectionElement.ondragover = function(e) { /* ... */ }
sectionElement.ondrop = function(e) { /* ... */ }
The only addition we'll make over here is that we'll reduce the opacity of the <div>
as soon as it's dragged and then restore it back to its original opacity as soon as it's dropped anywhere on the page (even inside a non-droppable target).
Here are the additions:
var draggedElement;
var divElement = document.querySelector('div');
var sectionElement = document.querySelector('section');
divElement.ondragstart = function(e) {
this.style.opacity = '0.3';
draggedElement = e.target;
}
divElement.ondragend = function(e) {
this.style.opacity = '1';
}
sectionElement.ondragover = function(e) { /* ... */ }
sectionElement.ondrop = function(e) { /* ... */ }
Inside ondragstart
, we set <divs>
opacity to 0.3
and then inside ondragend
, reset it back to 1
.
This was really simple. Don't you think so?
The drag
event
Of all the three events fired on a draggable element as it's dragged and dropped, the least used event by far is drag
.
Nonetheless, it is an event after all, so we ought to give some respect to it. Let's see how it works.
drag
event gets fired repeatedly on a draggable element as soon as it is begun to be dragged.Even if the pointer is kept in its position as is, the event still gets fired.
Let's see a clear example.
Below we have an element that is made draggable, and whose dragstart
and drag
events are being handled. In the handler for drag
, we increment the number of times the event has fired since the previous dragstart
, and display the value out on in the element <code>
.
<p>Number of times drag fired: <code>0</code></p>
<div draggable="true"></div>
var divElement = document.querySelector('div');
var codeEle = document.querySelector('code');
var timesDragFired;
divElement.ondragstart = function(e) {
timesDragFired = 0;
}
divElement.ondrag = function(e) {
timesDragFired += 1;
codeEle.innerHTML = timesDragFired;
}
The drag
event is rarely used in applications where custom DnD functionality is set up. Yes, dragstart
and dragend
have some practical applications, but this couldn't be said for drag
as well.
As you may have seen in the live example above, drag
fires at a fairly quick speed, which means that we couldn't put stressful routine inside its handler.
Doing so could cause performance issues on low-end devices, sometimes even high-end devices. So be careful while handling drag
!
Pitfalls of draggable
It all sounds interesting to take an element and convert it into a draggable source, with the help of the draggable
attribute. However, doing so may cause some problems later on when we'd want to interact with the element in a different way.
Let's see one such problem.
Suppose you have an <input>
element inside a <div>
as shown below:
<div>
<input type="text" value="Some text">
</div>
Let's set draggable="true"
on this <div>
. This, as we know, would make the element draggable:
<div draggable="true">
<input type="text" value="Some text">
</div>
From this point onwards, if we wish to select some text in the <input>
element by means of dragging from a given point inside the element, we would NOT be able to do so; the drag would move the whole <div>
element.
Try selecting the text inside the <input>
below using the mouse.
Were you able to select the text? No, right? That's because as we initiate the action to select the text inside the <input>
, since the input is draggable, we get a drag operation commenced.
In simple words, selecting text inside the <input>
becomes impossible, at least by using mouse gestures, as soon as we make the <div>
draggable.
This is a problem of using draggable
.
<input>
element can be used, though, in this case. For instance, we could click on the input field, place the cursor at a given position, and then use the keyboard to make the desired selection.Selection of text inside an element, using the mouse pointer, becomes impossible as soon as it's made draggable. This could compromise the usability of the site, and thus make it difficult for the user to use and interact with.
Therefore, whenever you make any element draggable, make sure to check that it does not have any input fields. You wouldn't want to prevent them from accepting any mouse-based text selections.
Spread the word
Think that the content was awesome? Share it with your friends!
Join the community
Can't understand something related to the content? Get help from the community.