Introduction
Uptil this point, in this unit, we've covered a great deal of stuff. We started out with the basics of drag-and-drop, then got into the very details of draggable elements while considering the three events dragstart
, drag
, and dragend
and many other related concepts; and then finally how to work with the drag data store using dataTransfer
.
Not to mention, your foundations on drag-and-drop have become exceptionally strong by this point. But the story doesn't end here. There is still a lot to cover in this DnD API.
In this chapter, we shall get a quick recap of 'dropzones'; work with the familiar events dragover
and drop
; get introduced to two new events: dragenter
and dragleave
; and consider many minute concepts related to dropzones, in general.
Without further ado, let's begin.
A quick recap
Back in the Basics chapter, we got ourselves introduced to the concept of dropzones. In this section, we'll get a quick recap of this idea and a couple more related ones.
When we drag an item anywhere on our operating system, it eventually has to end with a drop. The 'drop' action occurs when the mouse button is released amid a drag operation.
If the place where the draggable item is dropped accepts drops, we call it a dropzone.
In simple terms,
Some other terms used to refer to a dropzone are drop target, droppable element, droppable target, droppable, destination element.
By default, nothing on a webpage is a droppable, except for the following:
- Text input boxes (given by
<input type="text">
) - Textarea boxes (given by
<textarea>
) - Any element with
contenteditable
set to"true"
.
Basically, wherever we have the ability to input stuff using the keyboard, it is a dropzone by default.
However, using the DnD API we could turn almost any element into a dropzone by listening to a couple of events.
<img>
element can't be made a dropzone.The event that sits between a droppable and non-droppable target is dragover
. It is what makes a droppable target droppable.
But how?
Let's see it in detail...
The dragover
event
The dragover
event fires repeatedly on an element as the mouse pointer is brought over it amid an ongoing drag operation.
It stops the moment the mouse pointer exits the element.
By default, the dragover
handler for all elements is configured to cancel any subsequent events from firing on the element.
This means that, by default, the drop
event, which fires after dragover
, never gets dispatched due to the native mechanism of the browser for handling dragover
.
Fortunately, we could prevent this from happening by setting up a custom handler and stopping the default action from getting executed.
An example follows:
<p>The element below is a dropzone.</p>
<section></section>
section {
min-height: 200px;
background-color: #eee;
max-width: 500px;
}
We have a <section>
element which we would be turning into a dropzone. Here's how it looks initially:
The element below is a dropzone.
To make the element a dropzone, we'll simply handle its dragover
event and call e.preventDefault()
in there. There's no need to execute any other statement in here (though, we could, as we wish), so why worry about giving one?
var sectionEle = document.querySelector('section');
sectionEle.ondragover = function(e) {
e.preventDefault();
}
With this set, now our <section>
element has become a dropzone.
The element below is a dropzone.
You might say: How do I know whether the <section>
element is a dropzone?
Well, try dragging a text selection into the <section>
element and see the pointer shown. Is it any different from the one shown previously?
This is the first hint for the fact that the element is a dropzone.
But the real interest comes after we start handling the drop
event.
Handling dragover
is a MUST for drop
to fire!
Handling drop
ain't possible without setting up the minimal dragover
handler shown above.
But why?
Without cancelling dragover
, as we did in the last example above, a subsequent drop
event won't ever fire. Hence, the respective element won't ever be able to accept drops.
But with the dragover
handler set and its default action prevented, a subsequent drop
event would be able to take place on the element, and hence turn it into a dropzone.
Usually, nothing is required on the occurence of dragover
, except for simply cancelling the event's default action.
Moving on, since the dragover
event fires very quickly at regular intervals, it's recommended NOT to put complicated code in its handler.
Doing so could wreak havoc on low-end devices, sometimes even high-end devices. If you really need to perform a complicated task in here (which we think no developer would ever need to) then you should consider throttling or debouncing the event's handler.
Logically, the next event to unravel after dragover
is drop
.
Let's move on to it...
The drop
event
By far, the most awaited event of the whole DnD API is drop
.
The drop
event occurs on an element right after dragover
when a dragged item is brought over it and the mouse button is released in the meanwhile.
Most of the bulk of work of a custom DnD functionality is performed in the drop
's handler. This includes showing files, images, text, whatever is dropped, or throwing errors if the dragged element is not supported to be displayed in the dropzone.
As always, let's set up an example to demonstrate the drop
event.
For the first one, we'll configure the handler for drop
such that it removes the dragged element from its original location and adds it inside the dropzone.
You know how to accomplish this!
<div draggable="true">Drag and Drop</div>
<section></section>
var draggedEle;
var divEle = document.querySelector('div');
var sectionEle = document.querySelector('section');
divEle.ondragstart = function(e) {
draggedEle = this;
}
sectionEle.ondragover = function(e) {
e.preventDefault();
}
sectionEle.ondrop = function(e) {
this.appendChild(draggedEle);
}
This was basic!
For the second example, keeping the HTML (and the CSS) the same, we'll configure the ondrop
handler such that it copies the dragged element inside it:
var draggedEle;
var divEle = document.querySelector('div');
var sectionEle = document.querySelector('section');
divEle.ondragstart = function(e) {
draggedEle = this;
}
sectionEle.ondragover = function(e) {
e.preventDefault();
}
sectionEle.ondrop = function(e) {
this.innerHTML += draggedEle.outerHTML;
}
Yet, another simple functionality.
For the third example, we'll do a little bit more work.
We'll create a <section>
dropzone which would only accept stuff that has some drag data of type text/uri-list
set on it.
As soon as such an item is dropped over this dropzone, a new <a>
element is created, its href
and content gets set to the URL stored in the drag data store, and finally appended to the dropzone.
First thing's first, let's set up the desired HTML and CSS:
<p>Put links here:</p>
<section></section>
section {
min-height: 200px;
background-color: #eee;
max-width: 500px;
}
section a {
display: block;
margin: 5px
}
The declarations for section a
are given to make each link appear on a newline with enough whitespace to breathe in.
Put links here:
With this done, it's time to set up the logic:
var sectionEle = document.querySelector('section');
sectionEle.ondragover = function(e) {
e.preventDefault();
}
sectionEle.ondrop = function(e) {
var url = e.dataTransfer.getData('text/uri-list');
if (url) {
var a = document.createElement('a');
a.href = url;
a.innerHTML = url;
this.appendChild(a);
}
}
First inside the drop
's handler, we check whether the drag's data store has some data of the format text/uri-list
. If it has one, we create a new <a>
element and set its href
and innerHTML
to the value of this text/uri-list
data.
Here, we could say that the dropzone <section>
is acting as a storage area for links.
Let's try this live.
A bit more involved, but still simple!
Moving on, akin to all drag events we've seen so far, the drop
event could be handled on the global window
object as well. And before that, a global handler for dragover
is required in order to cancel the default action for the event.
Consider the code below:
window.ondragover = function(e) {
e.preventDefault();
}
window.ondrop = function(e) {
// drop handling code
}
This makes the whole HTML document a dropzone.
Usually, it is not recommended to do this but if you think you really need to listen to drop
on the whole document, then go for handling it (and dragover
as well) on window
.
dragover
on the window
object.dragenter
and dragleave
events
Both the events dragover
and drop
were already familiar to you from the Drag and Drop Basics chapter, and so hopefully you might not have had a hard time understanding them.
The DnD API provides two other method which fire on a dropzone as stuff is dragged into and out of it.
They are dragenter
and dragleave
, respectively.
dragenter
fires once when the item is dragged into the dropzone.dragleave
fires once when the item is dragged out of the dropzone.The names of both these events are pretty much self-explanatory as to when exactly do the they fire. Let's understand the naming...
As soon as an item, being dragged, enters an element, dragenter
gets dispatched on the element, and as soon as the item leaves the element, dragleave
gets dispatched on the element.
Now the thing is that do these events have any practical importance or not?
A huge yes!
dragenter
and dragleave
could be handled to toggle the styles of a dropzone to visually indicate to the user that the element could accept the item being dragged.
For intance, on dragenter
we could reduce the opacity
of the dropzone to a low value, and then restore it back to 1
on dragleave
. This would make the dropzone respond interactively on hover.
There is no other way to accomplish this without dragenter
and dragleave
in the whole DnD API.
Alright, time for a quick example.
Below we have a draggable element and a dropzone:
<div draggable="true">Drag and Drop</div>
<section></section>
var draggedEle;
var divEle = document.querySelector('div');
var sectionEle = document.querySelector('section');
divEle.ondragstart = function(e) { draggedEle = this; }
divEle.ondragend = function(e) {
sectionEle.style.opacity = '1';
}
sectionEle.ondragenter = function(e) {
this.style.opacity = '0.3';
}
sectionEle.ondragleave = function(e) {
this.style.opacity = '1';
}
sectionEle.ondragover = function(e) { e.preventDefault(); }
sectionEle.ondrop = function(e) { this.innerHTML += draggedEle.outerHTML; }
Almost everything here is pretty much standard as to what we've been doing so far. Only three things are of particular interest to us: lines 7-9, 11-13 and 15-17.
In lines 11-13 and 15-17, the dragenter
and dragleave
events are being handled on the <section>
element, respectively. Both these handlers toggle between opacity
values for the element, making it seem interactive when stuff is dragged over it.
In lines 7-9, we handle dragend
on the <div>
element. This is necessary because if the dragged <div>
is dropped onto the dropzone, dragleave
would never fire and hence the dropzone would become set to opacity: 0.3
. In order to reset it back to its original value in this case, we have to listen to dragend
and accomplish the opacity
reset there.
Let's consider another example.
This time we toggle between a class on the dropzone element upon the dragenter
and dragleave
events. The class when added, sets up a keyframe animation on the element, which in turn, zooms it in and out continuously.
.zoom {
animation: zoomInOut 0.4s ease-in-out infinite alternate
}
@keyframes zoomInOut {
to {
transform: scale(1.1);
}
}
var draggedEle;
var divEle = document.querySelector('div');
var sectionEle = document.querySelector('section');
divEle.ondragstart = function(e) { draggedEle = this; }
divEle.ondragend = function(e) {
sectionEle.classList.remove('zoom');
}
sectionEle.ondragenter = function(e) {
this.classList.add('zoom');
}
sectionEle.ondragleave = function(e) {
this.classList.remove('zoom');
}
sectionEle.ondragover = function(e) { e.preventDefault(); }
sectionEle.ondrop = function(e) { this.innerHTML += draggedEle.outerHTML; }
Simple, isn't it?
Usually, the events dragenter
and dragleave
are both handled together; it's rarely ever needed to just handle one event.
And one thing more — be wary of the dispatch of dragenter
and dragleave
events. Sometimes, they could fire in unexpected ways. The following section explains this.
Unexpected dragenter
and dragleave
dispatch
One strange behavior in regards to dragleave
is that the event fires on a dropzone as soon as the pointer goes over one of its child elements within it.
Ideally, the event shouldn't fire unless the pointer goes out of the whole dropzone element.
However, as in the current implementation, it fires when the pointer goes over a child element, even if that child element is within the dropzone.
And before this, dragenter
fires over that child element.
Does this flow of event dispatch resemble any other set of events in JavaScript?
Well yes, it sure does!
dragenter
and dragleave
work pretty much like mouseover
and mouseout
.