Course: JavaScript

Progress (0%)

DnD API - Drag Data

Chapter 71 39 mins

Learning outcomes:

  1. What is drag data
  2. The DataTransfer interface and the dataTransfer property
  3. Setting drag data using setData()
  4. The types property
  5. Getting drag data using getData()
  6. Clearing drag data using clearData()

Introduction

In the last chapter, JavaScript Drag and Drop — Draggable Elements, we explored all the details of how to configure a draggable element. We worked with the events dragstart, drag and dragend, and saw an example for each one.

Now, what we'll be covering in this chapter is very interesting. We'll see how to configure data for a given drag operation, natively using the DnD API, as opposed to using some global variables, and see the benefit of doing so.

We'll also see how to set data of a specific format; how to retrieve that data back; how to retrieve all the formats of set data; some very poorly-supported formats; and much more.

What is drag data?

By default, every drag operation on a webpage has some piece of data associated with it.

Formally, we refer to this as the drag's data.

The data associated with a drag operation is known as the drag data.

As we already know, there are only three things that can accept drag operations by default.

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

Only these three things are draggable out-of-the-box. Likewise, only they can be the target of any drag operation, to begin with.

Now for each of these, the browser sets some data automatically as soon as the drag operation commences on them:

  1. For a link, its drag data is its fully-qualified href.
  2. For an image, its drag data is its fully-qualified src.
  3. For a text selection, its drag data depends on the selection. It could be just a plain piece of text, or include the HTML tags of the selection made.
The fully-qualified href or src means that it consists of the complete URL of the webpage. For example, if we are on localhost:90/home and an image has src="image.png" set, the fully-qualified src would be localhost:90/image.png.

Apart from these three, if we want to store some data for any other element, explicitly made draggable by us, we have to do so manually.

This requires us to work with the dataTransfer property of the fired drag event.

Let's explore it.

The dataTransfer property

As soon as a drag operation begins on a draggable element, i.e. the dragstart event gets fired on it, we could initialize its data using the dataTransfer property of the event.

The dataTransfer property doesn't directly do any of this magic — we have to call its setData() method in order to set given data.

But before dissecting the syntax of this method, let's first see what is the dataTransfer property.

The dataTransfer property of a drag event serves to hold data regarding the ongoing drag operation.

In particular, the dataTransfer property holds a DataTransfer instance, representing the drag data store. The drag data store is simply a place where data related to the drag is stored.

The DataTransfer interface provides methods to set, get and clear data from this store.

Morover, it also defines some properties to further configure the nature of the drag, such as specify the allowed set of drag actions, or customize the feedback shown to the user, and so on and so forth.

Below shown are the properties and methods exposed by the DataTransfer interface.

Property/methodPurpose
dropEffectSpecifies which drag operation is accepted by the dropzone.
effectAllowedSpecifies which operation can be performed on the dragged item.
typesReturns an array holding all formats of data stored in the drag's data store.
clearData()Clears all data from the data store.
getData()Returns data for a given format.
setData()Sets data for a given format.
setDragImage()Provides an image to move along with a drag operation.

In the sections below, we go over the following methods in detail: setData(), getData() and clearData().

Let's get into the discussion.

Setting drag data

The setData() method of the dataTransfer object (which is a property of a drag event's object) serves to set data of a given format on the drag's data store.

Here's the syntax of the method:

dataTransferObj.setData(format, data)

format is a string specifying the type of data being stored; usually, it is given as MIME type. data is simply the actual data to store, also given as a string.

Besides setData(), there is another method on the dataTransfer object to set data. It is called mozSetDataAt(). However, note that it has very poor cross-browser support — as the name suggests, it's mainly available in Gecko-based browsers.

There are mainly three recognizable values for format as shown below:

  1. 'text/plain'
  2. 'text/uri-list'
  3. 'text/html'

Let's understand each one in detail...

'text/plain' is useful when we literally have text values to store, for example the id of an HTML element.

'text/uri-list' is useful when we have to store strings containing URLs. Although there's not a restriction to use it to store URLs, text/uri-list can be handy in some cases, like when we drag an element and then drop it into the address bar of the browser. Having some data of the format text/uri-list in the drag's data store would result in that value showing up in the address bar.

'text/html' is useful when we are dealing with strings that contain HTML. Note that text/html doesn't get any special treatment by the browser — the same data could also be stored using text/plain, but there are occassions when using both is a requirement, as we shall see later on in this chapter.

In older browsers, 'text/plain' was instead written as 'Text' (the value capitalized). This was to indicate that the format wasn't a true MIME type. However, in all modern browsers, 'text/plain' is the standard.

With these descriptions in mind, let's quickly consider an example of a custom DnD implementation whereby we set a piece of text on the drag's data store and then drag an element all the way into a document editor, and finally see the piece of text output in the editor.

Here's the code:

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

divElement.ondragstart = function(e) {
   e.dataTransfer.setData('text/plain', 'Working with dataTransfer!')
}

With the link below opened up, launch a text editor (such as Wordpad) on your OS and then try dragging the <div> into the editor window and then dropping it there. You'll notice the given text 'Working with dataTransfer!' displayed there.

Live Example

The following video demonstrates this. On the left-hand side, we have the webpage with our <div>, while on the right-hand side, we have the WordPad document editor:

See how the text set using setData() is copied onto the editor as soon as we drop the dragged <div> element in there.

This is the power of setData() and the native data store provided to us by the DnD API — we can set data for a given drag operation which could then end up anywhere on the OS and be processed respectively.

This example used the 'text/plain' data format. Now let's work with 'text/uri-list'.

As stated before, text/uri-list is used when we need to store a fully-qualified URL for a drag operation.

By default, when we drag a link (given by <a>) in a browser, it automatically sets a value of type text/uri-list on the drag's data store, pointing to the fully-qualified href of the link. Upon dropping the link onto the address bar of the browser, the browser puts the corresponding URL into the address bar.

Precisely speaking, if the drag data has the format text/uri-list, the browser puts that data, which simply represents a URL, into the address bar.

Moreover, on supported browsers, if we drag such an item onto the panel showing all tabs, it causes the browser to open a new tab pointing to the URL.

In short, text/uri-list gets special treatment by browsers.

Time for an example.

In the code below, we have a draggable <div> element with a data-href attribute set on it. This attribute holds a fully-qualified URL in it:

<div draggable="true" data-href="https://www.codeguage.com/">Div 1</div>

The dragstart handler of this <div> configures the drag data to the value of this data-href attribute, using the format 'text/uri-list':

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

divElement.ondragstart = function(e) {
   e.dataTransfer.setData('text/uri-list', this.getAttribute('data-href'));
}

In this way, if we drag-and-drop this <div> on the browser's address bar, we'd get the data-href URL input there. Or if we drag-and-drop it on the tab panel, we'd get a new tab loaded, pointing to the given URL.

Try performing these actions in the following link:

Live Example

So this was it for 'text/uri-list'.

For the last possible value of format, i.e. 'text/html', it also gets some special treatment by the browser and even certain software capable of processing HTML, such as Microsoft Word.

As we drag an item, with an associated text/html drag data, typically containing an HTML markup string, into an element with contenteditable="true" set (which makes it a dropzone), the HTML markup data is used to insert an element inside the dropzone.

For instance, consider the following example.

We have a draggable <div> and an editable <section>:

<div draggable="true">A div</div>

<section contenteditable="true"></section>

The idea is to configure a text/html-type drag data for the draggable <div> that holds the markup for an <h1> element. Because of this drag data, as soon as the <div> is dropped over the editable <section>, the <h1> element gets created in there.

Here's the script implementing this idea:

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

divElement.ondragstart = function(e) {
   e.dataTransfer.setData('text/html', '<h1>A heading</h1>');
}

In the link below, try dragging-and-dropping the <div> over the <section>. As you do so, you'll witness a new <h1> element inside the <section>.

Live Example

Notice how we don't have a dragover or drop handler for <section> in the code above. This is because when contenteditable="true" is set on an element, it automatically becomes a dropzone, with a default drop-handling mechanism.

In fact, if we were to handle these events on an element with contenteditable="true", we'd be essentially throwing away all the nice default drop-handling mechanism, and be left with implementing it on our own.

That would be quite tedious!

So far in this section, we've been considering examples calling setData() just once in order to store a given piece of data on the underlying drag data store. However, we could call setData() multiple times to set multiple kinds of data.

Let's see what this means...

Setting multiple pieces of data

Using the setData() method, it's possible to store more than one kind of data for a given drag operation.

To do so, we call the method multiple times, specifying each new value in each new call.

However, one thing to make sure is that, although more than one value could be stored in a drag data store, it has to be of a distinct format when compared to the values already stored.

If it is of the same format as an existing value, then that value is overridden with this new value.

You could think of this behavior much like CSS styles — when we put a new style inside a selector, if that style already has a value, that value is replaced with this new one.

Let's take a quick example.

Consider the following:

<div draggable="true">
   <h1>A heading</h1>
   <p>This is a paragraph</p>
</div>
var divElement = document.querySelector('div');

divElement.ondragstart = function(e) {
   e.dataTransfer.setData('text/html', this.innerHTML);
   e.dataTransfer.setData('text/plain', this.innerText);
}

We set up a dragstart handler on the <div> element. Inside the handler, we set two kinds of drag data:

  1. Under the format text/html, we store the element's innerHTML.
  2. Under the format text/plain, we store the element's innerText.

Now here's what this code does: if we drag-and-drop the <div> on a dropzone where text/html is understood, the respective drop handling mechanism of the dropzone will use the data associated with this text/html format, containing all the necessary tags and markup.

However, if text/html is not understood, then the data associated with text/plain will be read and used instead by the dropzone.

Live Example

If we call setData() with a format for which the method has been called previously as well, the new provided value is used to replace the previous one.

For instance, in the following extension to our previous code, we call setData() twice to store some text/plain data:

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

divElement.ondragstart = function(e) {
   e.dataTransfer.setData('text/html', this.innerHTML);
   e.dataTransfer.setData('text/plain', this.innerText);

   // Set another text/plain data.
e.dataTransfer.setData('text/plain', 'The second call'); }

When the <div> is dragged-and-dropped over a text editor (with no support for text/html drag data), with this code in place, the latter datum, i.e. 'The second call' is what we see in the editor.

This is simply because the second setData() call results in the replacement of this.innerText in the drag data store with 'The second call'.

Live Example

All good uptil now?

It's time for a real quick task.

Write a script to configure the dragstart of all <a> elements such that:

  1. When they are dragged and dropped on any location that supports URLs, they are shown as a URL.
  2. When they are dragged and dropped on any location that doesn't suppport URLs but does support plain text, they are shown as follows: text (url), where text is the text with the <a> tag (excluding the markup) and url is its fully-qualified href.

For instance, given the following <a> element,

<a href="https://www.codeguage.com">Codeguage</a>

if it is dragged onto the browser's address bar, it should get displayed as a URL — https://www.codeguage.com; otherwise if it's dragged, let's say, onto a text editor on the OS, then it should be displayed as the following plain text: 'Codeguage (https://www.codeguage.com)'.

The solution is pretty simple.

We just need to set two separate data values on the drag data store for any <a> element. The first one would be of type text/uri-list while the second one would be of type text/plain.

If a given <a> element is dropped over a region supporting URLs, such as the browser's address bar, it would extract out the text/uri-list data from this element. Otherwise, if it supports plain text, it would extract out the text/plain data form this element.

Here's the code:

function dragStartHandler(e) {
   var target = e.target;
   e.dataTransfer.setData('text/uri-list', target.href);
   e.dataTransfer.setData('text/plain', `${target.innerText} (${target.href})`);
}

var anchorElements = document.querySelectorAll('a');

anchorElements.forEach(function(anchorElement) {
   anchorElement.ondragstart = dragStartHandler;
});

First, we retrieve all <a> elements in anchorElements and then assign each of these elements a dragstart handler, i.e. the dragStartHandler() function.

Inside this function, we retrieve the href and innerText of the dragged <a> element node and then use these properties to set given data on the drag data store.

  • The href gets stored in the text/uri-list format.
  • The innerText, after being concatenated with other strings, gets stored in the text/plain format.
We retrieve the href of the dragged <a> using the href property of the element node. This is extremely important as the href property returns the fully-qualified href (supposing that the href attribute did not contain a complete URL).

Actually, we don't really need to set up the text/uri-list data in the handler, since the browser already does this on its own. So if we want to, we could remove line 3 from the code above.

The types property

The DataTransfer API is quite useful and extensive. In the previous section, we only covered a method of the interface, i.e. setData().

Now, let's see one very handy property — types.

The types property of the DataTransfer interface returns an array holding all the types of data set in the drag's data store (if there is any data, at all).

Each element of this array is a string — a better to say, a unique string — showcasing a given MIME type configured for the underlying drag operation

When setting data using setData(), because a given format could only be used once, and then these formats show up in types, it turns out that the types array contains unique items.

Consider the code below:

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

<section></section>

By now, you'd be much familiar with this basic HTML setup we use to demonstrate a custom DnD implementation — the <div> is a draggable item while the <section> is a dropzone.

Here's the script implementing the custom DnD behavior:

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

divElement.ondragstart = function(e) {
   e.dataTransfer.setData('text/html', this.innerHTML);
   e.dataTransfer.setData('text/plain', this.innerText);
}

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

sectionElement.ondrop = function(e) {
   console.log(e.dataTransfer.types);
}

In the dragstart handler of the <div>, we set up two different pieces of data on the underlying drag data store: one of the format text/html and the other of the format text/plain.

As we drag-and-drop this <div> inside the <section> dropzone, the drop handler of the <section> logs the types of data stored in the drag data store. Note that this time we don't move the <div> into the <section> upon its drop.

Live Example

Here's the log that gets made as soon as we drag-and-drop the <div> element over <section>:

['text/plain', 'text/html']

This log clearly shows that types contains the types of data stored in the underlying drag data store.

The order of items in types isn't necessarily the same as the order in which we set data in the underlying drag data store. In our case, in the dragstart handler above, we first set up some text/html data and then set up some text/plain data, yet types contains 'text/plain' first and then 'text/html'.

Talking about the practical applications of types, we could use it to check whether a dropzone can accept a given draggable item.

For instance, we may have a dropzone A that only accepts plain text, a dropzone B that only accepts URLs, and a dropzone C that only accepts some custom data.

Using types, we could configure each dropzone's drop handler to check the type of data associated with an item dragged-and-dropped over it, and process it only if the dropzone accepts that type.

Getting drag data

Getting data back from a given drag data store is possible, thanks to the getData() method of the DataTransfer interface.

The method takes just one argument, as shown below:

dataTransferObj.getData(format)

format is a string specifying the type of data to retrieve. It ought to be one of the strings passed to setData() when storing data for the underlying drag operation.

No method to access all drag data at once!

Keep in mind that it's NOT possible to get all the data at once from the drag data store. We could only retrieve data for a particular format at a time.

To retrieve all the data, we have to repeatedly call getData() with all the desired formats.

This makes sense since no one would ever want to retrieve all of the drag data at once. Each format has its own nature of data, and ideally needs to be processed separately.

Let's consider an example of using getData().

Below we have our same old custom DnD setup:

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

<section></section>
var divElement = document.querySelector('div');
var sectionElement = document.querySelector('section');

divElement.ondragstart = function(e) {
   e.dataTransfer.setData('text/html', this.outerHTML);
}

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

sectionElement.ondrop = function(e) {
   this.innerHTML += e.dataTransfer.getData('text/html');
}

However, this time we store the HTML markup of the <div> element in the drag data store as soon as it's dragged. Later on, we retrieve the markup back, when the <div> is dropped over <section>, to add it inside the <section> dropzone.

Sounds interesting, doesn't it?

Live Example

In this example, the dropzone acts as if it makes copies of the dragged <div> element.

In all the previous chapters, we used appendChild() to move the <div> into the dropzone when dropped over there. This had the consequence of removing the node from its original location. This time, though, we recreate it using its markup string.

What an amazing idea.

It's all about experimenting with the interfaces and utilities you already know to create different sorts of DnD functionalities.

This example could've been set up without using the native data store for drag operations, at all. We could've created a global variable to hold the HTML of the dragged element and then retrieve this variable back when the element is dropped over the <section> element. The whole point of using the native data store here is to show you how to use it and give some possible use cases for it.

Moving on, calling getData() with a non-existing format won't throw an error, as one might think. Rather, it would return an empty string ('').

The following code illustrates this with our same old <div> and <section> setup:

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

divElement.ondragstart = function(e) {
   e.dataTransfer.setData('text/html', this.outerHTML);
}

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

sectionElement.ondrop = function(e) {
console.log(e.dataTransfer.getData('text/random') === ''); }

Live Example

As soon as we drop the <div> into the <section> dropzone, the following log is made:

true

This confirms the fact that calling getData() with a non-existent data format indeed returns ''.

Clearing drag data

As per the name, the clearData() method of the DataTransfer interface serves to clear all the data from the underlying data store. This includes any default data stored by the browser itself.

Here's the syntax of the method:

dataTransferObj.clearData([format])

The optional format argument specifies the format of the data that we wish to clear from the underlying drag data store. If omitted, the data for all formats is deleted.

We might want to call clearData() if we know that there is some data already stored for a given drag operation that we don't want to work with.

Let's consider a real example.

First, we'll demonstrate the default behavior and then bring in clearData() to see the difference made.

In the following code, we have a draggable link (recall that <a> with href is draggable by default) whose dragstart handler logs all of the data stored (by the browser itself) as the link is begun to be dragged:

<a href="https://www.codeguage.com/">Visit codeguage.com</a>
var anchorElement = document.querySelector('#a1');

anchorElement.ondragstart = function(e) {
   // Go over each type and get its corresponding data.
   e.dataTransfer.types.forEach(function(type) {
      console.log(type, e.dataTransfer.getData(type));
   });
}

Live Example

As we initiate to drag the link, we get the following logs in the console:

text/uri-list https://www.codeguage.com/ text/plain https://www.codeguage.com/ text/html <a id="a1" href="https://www.codeguage.com/">Visit codeguage.com</a>

What this means is that the browser itself creates three different types of data for a dragged <a> element: text/uri-list and text/plain containing the fully-qualified href of the link, and finally text/html containing the outerHTML of the link.

So far, so good.

Now, let's call clearData() right at the start of the dragstart handler and then witness the difference it causes:

var anchorElement = document.querySelector('a');

anchorElement.ondragstart = function(e) {
e.dataTransfer.clearData(); // Go over each type and get its corresponding data. e.dataTransfer.types.forEach(function(type) { console.log(type, e.dataTransfer.getData(type)); }); }

Live Example

This time, as soon as we drag the link, nothing gets logged in the console. This is because types is empty, owing to the fact that there is absolutely no data in the underlying data store, and so the callback provided to forEach() never executes.

Simple.

As this example demonstrates, clearData() is a handy utility when we want to make sure that the browser doesn't interfere with its own data setup in a customized DnD functionality.

In such a case, we call clearData() right at the start of the dragstart handler, as we did above, and then proceed with any customized data setup that we want in our customized DnD functionality.

"I created Codeguage to save you from falling into the same learning conundrums that I fell into."

— Bilal Adnan, Founder of Codeguage