Course: JavaScript

Progress (0%)

  1. Foundation

  2. Numbers

  3. Strings

  4. Conditions

  5. Loops

  6. Arrays

  7. Functions

  8. Objects

  9. Exceptions

  10. HTML DOM

  11. CSSOM

  12. Events

  13. Drag and Drop

  14. opt Touch Events

  15. Misc

  16. Project: Analog Clock

JavaScript Drag-and-Drop Basics

Chapter 68 27 mins

Learning outcomes:

  1. What are draggable items
  2. What are dropzones
  3. Default drag-and-drop behavior in browsers
  4. The draggable HTML attribute
  5. The dragstart, dragover and drop events
  6. A complex drag-and-drop example

The DnD API

Back in the day, when Internet Explorer 5 was about to be shipped and HTML5 was already a recent technology, IE launched a new API to enable native drag-and-drop functionality. The developer could use this API to customize things as they were dragged and dropped.

With newer and newer versions of the browser, Microsoft kept on adding more and more bulk to this API ultimately leading to the API finding itself all the way in the HTML5 specification and hence other browsers implementing it.

Strictly speaking, the implementation of IE for its drag-and-drop functionality was reverse-engineered by Ian Hickson while writing its spec for the HTML5 specification. This specification was then followed by other web browsers, such as Google Chrome, Firefox, Safari, Opera, etc. (although not very strictly).

In these early days, the implementation of the DnD (drag-and-drop) API was extremely inconsistent and buggy across various browsers. Some features worked in some browsers; some didn't. However, today, at the time of this writing, all major web browsers have excellent support for basic DnD functionality.

Even though this is some, arguably irrelevant, history, it does tell us about one of the many contributions of IE to web technologies — well, IE has quite many of them!

With this brief history out of the way, let's now begin the real discussion.

Draggable items

We'll start off by understanding what exactly is meant by being 'draggable'.

Anything that could be dragged on a webpage is called draggable.

By default, only the following items are draggable on a webpage:

  1. Links (given by <a> with an href attribute)
  2. Images (given by <img>)
  3. Text selections

Nothing else is draggable.

So uptil this point, we know that not everything is capable of being dragged in web browsers, to begin with. Only three things are: links, images and text selections.

However, as we shall learn later on in the next section, we can explicitly define any HTML element to be draggable as well, with the help of the draggable attribute.

Now once something is selected and thereby dragged, it eventually has to be put somewhere, or better to say, dropped somewhere.

This gives birth to another concept:

Any element where we can drop stuff is called a dropzone.

A dropzone is also referred to as a drop region, droppable region, droppable element, destination element, or simply a droppable.

Once again, not everything on a webpage is a dropzone by default.

Only the following are considered dropzones by default:

  1. Text input boxes (given by <input type="text">)
  2. Textarea boxes (given by <textarea>)
  3. Any element with contenteditable set to "true".

Nothing else!

So uptil this point, it's clear that a dropzone is an element that accepts drops, and that not everything is a dropzone by default — only the ones listed above enjoy this facility.

This means that we could drag an <img> element (which is draggable by default) and drop it into a <textarea> element (which is droppable by default).

The consequence of doing so is interesting...

When we drop an <img> inside a text box, <textarea> in this case, the fully-qualified src of the image is put inside the text box.

You can check this yourself below:



Try dragging and then dropping the image into the text box. As soon as you do so, you'll notice the box filled up with the src of the image.

Drag-and-drop functionality can only be tested on desktop devices. If you're on a tablet or mobile device, you won't be able to drag the image above.

Now this was about images; what about other draggable stuff?

Let's define the consequence for all of them:

  1. When an <a> element is dropped onto any droppable element, its href attribute is copied onto the textbox.
  2. When a text selection is dropped over a textbox, the selected text is put inside the box. If the selected text was from a textbox itself, then that text is removed from the original textbox, by default. However, by pressing the Ctrl key, we could change this and prevent the text from being removed.

All these can be seen in the live previews below:

To boil it all down, web browsers provide quite a lot of drag-and-drop functionality by default. But generally, all this is not very frequently used.

How many times do you remember dragging text from an input field into another field and dropping it there with the Ctrl key pressed? You, more than often, copy the text and paste it into the desired field, isn't that so?

How many times do you drag links into textboxes?

Or for that matter, how many times do you drag images into textboxes? Once in a month, once in a year? Or maybe never?

One thing known for sure is that these out-of-the-box DnD facilities provided by web browsers aren't that handy. OK, let's be fair — they aren't handy at all!

Tapping into this default mechanism is utterly necessary and the whole point of this unit, if our goal is to create useful DnD experiences for the end user.

In the discussion that follows, we'll see how to create customized DnD functionality.

The draggable attribute

The first step to configure DnD functionality is the draggable HTML attribute.

Let's see what it does...

As the name suggests,

The draggable HTML attribute makes an element draggable.

This is the simplest of all definitions.

By default, as we stated earlier, not all elements on a webpage are draggable. However, by setting the draggable attribute to "true", we can make any element draggable.

Shown below is an example:

<div>Drag and Drop</div>
div {
   backgrond-color: #fff493; /* light tint of yellow */
   padding: 50px;
   display: inline-block
}

We have a <div> element with some CSS styles applied to give it a large clickable area.

Drag and Drop

Try dragging this <div>, by pressing the mouse button down while the pointer is over it and then moving the mouse while the button is held down. You'll most probably end up making a text selection — this is because we can't drag this element.

Let's go forward and add the draggable attribute:

<div draggable="true">Drag and Drop</div>

Here's the new output:

Drag and Drop

Visually, the output is the same as before. But this time, as you drag the <div>, you'll see the magic of the draggable attribute.

Upon dragging the <div>, we see a translucent, replica image of it moving along with the pointer. This confirms that the <div> is draggable.

draggable ain't a Boolean attribute!

The draggable attribute is not a Boolean attribute in HTML, unlike many other attributes. That is, we couldn't set it as follows:

<div draggable>This is wrong</div>

It must have a value, as shown below:

<div draggable="true">This is draggable</div>
<div draggable="false">This is not draggable</div>

"true" means that the element is draggable, while "false" means that the element is not draggable.

So now if you're asked to reason about the purpose of the draggable attribute, you'd have an answer right away: draggable is required to make an element draggable.

Without draggable, we couldn't manually configure the drag-and-drop behavior of an HTML element — we would never be able to drag it, likewise any sort of drag-and-drop configurations would go ignored (unless that element is draggable by default).

Setting draggable="true" on links or images is redundant since these elements are draggable by default. However, we could use the attribute to turn off this feature for these elements, i.e. by setting draggable="false".

The dragstart event

After setting the draggable attribute on an element, the next step is to handle the dragstart event on the element.

Let's see what dragstart is.

The moment we begin to drag a draggable element (by pressing the mouse button while the pointer is over it and then moving the mouse), it receives a dragstart event.

As the name suggests, dragstart implies the 'start' of the drag action.

Subsequent code can obviously be then executed by handling this dragstart event.

But what action could we possibly want to do upon dragstart? Well, we could set some data for the respective drag operation.

For instance, if we drag a <div>, we could save its reference in a global variable and then retrieve the value of this variable later on when we drop the <div> in a dropzone. We'll see such an example later on in this chapter.

The question is: Why do we even need consider dragstart? Can't we implement DnD functionality without it?

We could, but only for elements that are draggable by default. For other elements, we couldn't do anything useful without listening to the dragstart event.

But why? Let's see...

Suppose we drag a <div> element (which was obviously made draggable using the draggable attribute), and then drop it to a dropzone. What would happen at this point? Yes, we haven't shown you how to work with dropzones, but let's imagine for the moment that the dropzone is fully configured. What would happen?

Well, if we did not listen to the dragstart event, we wouldn't have specified the data associated with the current drag. This would've meant that the moment we drop the <div> into the dropzone, our application would have no clue regarding what has been dropped — surprisingly, there's no way to retrieve the element that has been dropped over a dropzone.

In short, there would be absolutely NO data at all to work with!

All this simply means one thing: to set the data associated with a drag operation, we need to set it manually as soon as the drag operation begins, which is marked by the dragstart event.

Keep in mind, though, that it's not just dragstart where we could set data for the ongoing drag operation. We could do this inside the drag event's handler as well, which we shall explore in the JavaScript Drag and Drop — Draggable Elements chapter.

But sensibly speaking, dragstart represents the start of a drag event, and it makes much more sense to set the data at this point, rather than when the drag has already begun. Moreover, drag tends to fire continuously, which would make the data be set again and again — something clearly unnecessary and inefficient!

Let's now handle the dragstart event for our draggable <div> element created above:

<div draggable="true">Drag and Drop</div>
var draggedElement;
var divElement = document.querySelector('div');

divElement.ondragstart = function(e) {
   draggedElement = e.target;
}

First we create a global variable draggedElement to hold a reference to the dragged element later on. Next, we select the <div> element in the variable divElement and then set up a dragstart handler on it. Inside the handler, we save the event's target inside draggedElement.

At least for now, you won't find this code snippet useful at all.

After all, we could've directly assigned divElement to draggedElement inside the handler, instead of going with e.target, or simply just removed the dragstart handler since we already have a reference to the <div> element.

However, once we add more draggable <div>s, which we would very soon in this chapter, you'd see the benefit of using e.target inside the dragstart handler.

Anyways, so the code above gets the draggedElement variable filled up with the reference of <div> as soon as it's dragged. We could confirm this using a very simple log:

var draggedElement;
var divElement = document.querySelector('div');

divElement.ondragstart = function(e) {
   draggedElement = e.target;
console.log(draggedElement === divElement); // true }

Live Example

As soon as we drag the <div>, true is logged in the console. This means that yes, draggedElement is exactly the same element as divElement.

Perfect!

It's time to finally take this code snippet up a notch by creating and configuring a dropzone.

The dragover event

As stated before, NOT every element on a webpage is considered a dropzone, by default.

Only the following elements are considered as dropzones:

  1. Text input boxes (given by <input type="text">)
  2. Textarea boxes (given by <textarea>)
  3. Any element with contenteditable set to "true".

Fortunately though, we can make any element a dropzone using the DnD API.

The main step in doing so is to handle the dragover event on the respective element.

So what is dragover?

The dragover event fires on an element as soon as a draggable item is dragged over it.

The dragover event fires repeatedly, after short regular intervals, even if we don't move the mouse pointer at all. The event only stops firing when the pointer leaves the element.

Perhaps, the most important thing to note here is that the browser nullifies the drag operation in its default dragover handling mechanism (specifically, it sets dropEffect to 'none'; we'll see what this means). This prevents the subsequent drop event from firing.

Likewise, in order to be able to process a subsequent drop (of a draggable element), it's utmost necessary to prevent this default behavior. And that, obviously, means calling the preventDefault() method of the dragover event.

Other ways of cancelling an event's default action are to return a falsey value from its handler, or to set its returnValue property to false.

To restate it: handling the dragover event on an element, and then cancelling its default action, turns the element into a dropzone, i.e. it could accept drops of draggable items.

Going back with our last example, let's now add a new HTML element to act as the dropzone. We'll use a <section> element, but you could use any element you like (such as a <div>):

<div draggable="true">Drag and Drop</div>

<p>Dropzone</p>
<section></section>
div { /* same as above */ }

section {
   min-height: 200px;
   background-color: #eee;
   max-width: 500px;
}
Drag and Drop


Dropzone:

At this stage, if we drag the <div> above and take it into the <section> element, we'll notice a 🛇 icon displayed. This indicates that the <section> element doesn't accept any drops, i.e. it's not a dropzone.

In order to turn it into one, we need a dragover handler. Let's set that up now:

var draggedElement;
var divElement = document.querySelector('div');
var sectionElement = document.querySelector('section');

divElement.ondragstart = function(e) {
   draggedElement = e.target;
}

sectionElement.ondragover = function(e) {
e.preventDefault();
}

And now let's try performing the same drag action as we did above:

Live Example

This time, thanks to the dragover handler, as soon as the <div> is taken inside the <section>, we don't notice the 🛇 icon. This means that <section> can now accept a drop.

The last thing left now is to handle the drop event on the <section> element so that we are able to actually move the dragged <div> from its original position into the <section>.

The drop event

When a DnD operation successfully completes on a dropzone, as we drop an item over it, the dropzone element receives a drop event.

The drop event fires on a dropzone as soon as a draggable element is dropped over it.

This 'dropping' action refers to releasing the mouse button after a drag operation while the pointer is still over the dropzone. It's literally as if we 'drop' the dragged item over the dropzone.

Keep in mind that for drop to occur on any element, it's a MUST to cancel the default behavior of the browser for the dragover event on that element; omitting this step would simply keep drop from firing!

Upon the occurrence of drop, we could do numerous things. For example, if any file is dropped, we could maybe upload it to the server; if an image is dropped, we could showcase its preview; if any selected text is dropped, maybe we could process it; and so on

Going with our <div> and <section> example above, we'll remove the dragged <div> element from where it is in the DOM tree, and instead place it inside the <section> dropzone.

But how will we do this?

One way is to use removeChild() to remove this node from its original position and then use appendChild() to add it inside <section>.

But we could shorten this set of statements using a very simple trick. Calling appendChild() on an existing node automatically removes it from its original place — no need to do the removal manually.

Alright, so let's now handle the drop event on <section> and get this idea accomplished:

/* ... */

sectionElement.ondragover = function(e) {
   e.preventDefault();
}

sectionElement.ondrop = function(e) {
this.appendChild(draggedElement);
}
For simplicity, we've shortened the code here to just show the handlers for the dropzone. You'll, however, obviously need to include everything in your code.

As soon as the <div> is dropped over <section>, the drop event gets fired and, consequently, the <div> is moved from its original position to inside this dropzone.

Live Example

Simple, isn't it?

We have a draggable <div> element and a droppable <section> element. The <div> can be dragged-and-dropped into the <section>, at which point it's moved from its original location in the document to inside this <section> element.

This completes our basic and minimalistic DnD implementation.

Time to move over to a slightly more complex example.

A more complex example

In the example above, we had just one single draggable <div> element. What's more interesting, however, is to have many of them, each one capable of being dragged-and-dropped onto the <section> element.

Let's see how to implement this functionality.

First of all, note that no major changes would be required to be made in the previous code in order to implement this. We'll just need to create multiple <div>s and assign each one the same dragstart handler that we gave to the single <div> in our last example.

Doesn't seem complicated, right?

To start with, let's set up the HTML. Previously we had one <div>, now we'll have many of them — four, to be precise:

<div draggable="true">Div 1</div>
<div draggable="true">Div 2</div>
<div draggable="true">Div 3</div>
<div draggable="true">Div 4</div>

<p>Dropzone</p>
<section></section>

The CSS is the same as before. Just to put a little bit of spice, we'll give a different background color to each <div> element:

<div draggable="true">Div 1</div>
<div draggable="true" style="background-color: #f5deb3;">Div 2</div>
<div draggable="true" style="background-color: #ffb5b5;">Div 3</div>
<div draggable="true" style="background-color: #aaaaff;">Div 4</div>

<p>Dropzone</p>
<section></section>

For the scripting part, we'll first fetch the list of all these <div> elements inside the variable divElements, removing the old divElement variable:

var draggedElement;
var divElements = document.querySelectorAll('div');
var sectionElement = document.querySelector('section');

Then, we'll assign a dragstart handler to each element in divElements. This handler would be the same, so we'll create a named function for it:

var draggedElement;
var divElements = document.querySelectorAll('div');
var sectionElement = document.querySelector('section');

function dragStartHandler(e) {
   draggedElement = e.target;
}

divElements.forEach(function(divElement) {
   divElement.ondragstart = dragStartHandler;
})

Now before we proceed any further, notice one thing.

In the HTML above, we gave each <div> element the draggable attribute manually. Wouldn't it be a good idea if we could set this attribute from within JavaScript?

In this way, we won't have to manually set up the attribute on each element in the HTML.

So first, let's clean up our old HTML:

<div>Div 1</div>
<div style="background-color: #f5deb3;">Div 2</div>
<div style="background-color: #ffb5b5;">Div 3</div>
<div style="background-color: #aaaaff;">Div 4</div>

<p>Dropzone</p>
<section></section>

and then transfer the draggable-setting logic inside forEach():

var draggedElement;
var divElements = document.querySelectorAll('div');
var sectionElement = document.querySelector('section');

function dragStartHandler(e) {
   draggedElement = e.target;
}

divElements.forEach(function(e) {
e.draggable = true; e.ondragstart = dragStartHandler; })

With this done, let's resume from where we left.

So now that each <div> element has a dragstart handler registered, while keeping in mind that the <section> element has the same event handlers as we saw before, it's time to try out the example live.

Live Example

It works perfectly!

It's in this example that our old dragstart handler makes sense.

Previously, we just had one <div> element and so saving its reference inside the global variable draggedElement (inside the dragstart handler) was meaningless, since we already had a global variable divElement pointing to the <div>.

However, now the scenario is different. We have four <div> elements, each with the same dragstart handler. We can't possiblt know which one causes the event to fire and that's why we use the event's target to obtain this piece of information, and ultimately assign its value to draggedElement.

To summarize all this long discussion, using the DnD API provided by JavaScript, any given set of elements on a webpage can be given a custom drag-and-drop functionality.

Here's the minimal setup:

  1. Firstly, we set the draggable HTML attribute on the element to the value "true". This makes the element draggable, i.e. it could be dragged using the mouse pointer.
  2. Then we handle the dragstart event on this draggable element. Inside the handler, we usually configure some data related to the ongoing drag.
  3. Next, we configure an element into a dropzone by handling its dragover event and preventing its default action.
  4. Finally, we handle the drop event on this dropzone. In the handler, we process the dropped item.

This is a very basic implementation of a custom DnD functionality.

More complicated workflows can be made by listening to a larger number of events and by setting the data associated with the drag using specialized utilities made for the task. In the upcoming chapters, we'll cover all these details one-by-one.

For now, it's important that you get firm on the very basics of DnD that we learnt in this chapter.

Once these concepts are peaches and cream for you, consider moving over to the next chapter. Otherwise, you'll find yourself not able to fully comprehend the DnD API.