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.

A draggable item is simply one that can be dragged by the user.

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:

  1. Links (given by <a>)
  2. Images
  3. 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
}
Drag and Drop

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:

Drag and Drop

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 dragstart event

In the DnD API available in web browsers, at the time of this writing, three events fire on a draggable element as its dragged, and ultimately dropped at some location.

They are:

  1. dragstart — indicates that the drag has begun.
  2. drag — indicates that the element is being dragged.
  3. dragend — indicates that the dragging operation has ended.

First, in this section, let's go over the dragstart event and see its significance in creating a drag-and-drop effect from scratch.

Starting with the definition:

The dragstart event fires on a draggable element, as soon as we begin to drag it.

As the name implies, the 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?

When we begin to drag an element, it's important that we set some data related to the drag 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 which we could then retrieve later on when we drop it. In this way, we would know which element was dragged-and-dropped onto the dropzone, and then be able to perform any desired set of acton on that element.

And this reference-saving thing could only be sensibly done inside dragstart's handler.

Without saving any sort of data while dragging an element which was non-draggable by default, would make the whole drag-and-drop functionality totally meaningless. We would have no clue as to what was dragged — just no data to work with!

For elements draggable by default, such as images, links, it won't be totally awkward if we left out handling dragstart for them, since the browser automatically saves some data for these elements in their default dragstart handling mechanisms.

But regardless, these default mechanisms aren't very useful in terms of the data they store — either way, a developer has to tap into the handler when making a custom drag-and-drop functionality.

Anyways, coming back to the dragstart event, we could handle it just like we could handle most DOM events:

  1. Use the ondragstart property and assign it a function
  2. Use the addEventListener() method and pass 'dragstart' as its first arg.

In all the examples that follow, we'll use the former since it is relatively simpler to write and digest visually. However, it's generally recommended to use addEventListener() to listen to DOM events.

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 handle the dragstart event on the <div> and make a simple console log in there:

var divEle = document.querySelector('div');

divEle.ondragstart = function(e) {
   console.log('Drag has started!');
}

Inside the ondragstart handler, we make a console log.

Now, it's up to trying out this example live in order to see when does dragstart really fire.

Live Example

Perfect!

Time to consider another example.

We'll use the same HTML markup as before. But this time, instead of making a generic console log, we'll save the reference of the dragged element inside a global variable draggedEle:

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

divEle.ondragstart = function(e) {
   draggedEle = e.target;
}

With this in place, as soon as we start to drag the <div>, the global variable draggedEle would point to it. This would be used later on when we drop this element into a dropzone.

Let's also create a dropzone where we could drop this <div>:

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

<section></section>

The HTML is almost the same as before. Just one new element is added which is <section>. This will act as the dropzone.

The CSS below makes each element seem more what it is meant to:

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

The following code turns this <section> element into a dropzone and handles any drops made over it:

/* ... previous code ... */

var sectionEle = document.querySelector('section');

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

sectionEle.ondrop = function(e) {
   this.appendChild(draggedEle);
}
The this keyword inside the ondrop handler refers to sectionEle since the handler is attached directly to sectionEle.

Inside the ondragover handler, we call e.preventDefault() in order to cancel the browser's default action for the event. This is necessary to be able to catch the subsequent drop event.

As we know from the previous chapter, the brower's default action prevents any subsequent events from firing after dragover, hence we need to override this behavior in order to receive the subsequent, and most important, drop event.

Moving on, inside the ondrop handler, we retrieve the global variable draggedEle and append it inside the dropzone element by means of calling appendChild().

This altogether creates a simplistic, functional, drag-and-drop feature.

Wanna try it?

Here's a live example.

Live Example

The dragend event

What's the opposite of the word 'start' in English? 'End'? Well, yes. The word 'end' is indeed the opposite of 'start'.

If we just focus on the naming of the dragend event, it seems to be the opposite of dragstart.

Guess what — it actually is!

The dragend event fires when a drag operation ends.

A drag operation ends as soon as we lift the mouse button after having dragged a draggable item.

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 doesn't obviously matter then.

Anyways, coming back to the point, what do you think can we possibly do inside the dragend event's handler?

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 draggedEle;
var divEle = document.querySelector('div');
var sectionEle = document.querySelector('section');

divEle.ondragstart = function(e) { /* ... */ }

sectionEle.ondragover = function(e) { /* ... */ }
sectionEle.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 draggedEle;
var divEle = document.querySelector('div');
var sectionEle = document.querySelector('section');

divEle.ondragstart = function(e) {
this.style.opacity = '0.3'; draggedEle = e.target; }
divEle.ondragend = function(e) {
this.style.opacity = '1';
} sectionEle.ondragover = function(e) { /* ... */ } sectionEle.ondrop = function(e) { /* ... */ }

Inside ondragstart, we set <divs> opacity to 0.3 and then inside ondragend, reset it back to 1.

Live Example

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.

The 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 divEle = document.querySelector('div');
var codeEle = document.querySelector('code');
var timesDragFired;

divEle.ondragstart = function(e) {
   timesDragFired = 0;
}

divEle.ondrag = function(e) {
   timesDragFired += 1;
   codeEle.innerHTML = timesDragFired;
}

Live Example

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. However, doing so may cause some problems later on when you would 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 on, if we wish to select some text in the <input> element by means of dragging from a given point in the input box, we won't be able to do so since the drag would move the whole element.

Try selecting the value inside the <input> box below using the mouse.

Were you able to select the text?

In simple words, selecting text inside the <input> becomes impossible, at least by using mouse gestures, as soon as we make the <div> draggable.

Other ways to select text in the <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.

This is a problem of using draggable.

Selection inside an element, using the mouse pointer, become 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 out whether it has any input boxes. You wouldn't want to prevent them from accepting any mouse-based text selections.