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

DnD API - Draggable Elements

Chapter 70 28 mins

Learning outcomes:

  1. A quick recap of what is 'draggable'
  2. The dragstart event
  3. The dragend event
  4. The drag event
  5. 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.

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 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')
undefined
imageElement.draggable
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:

  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 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:

The 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.

Live Example

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
}
Drag and Drop

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.

Live Example

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!

The 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.

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 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;
}

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, 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.

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.

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.