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.
As we already know, there are only three things that can accept drag operations by default.
- Links (given by
<a>
with anhref
) - Images
- 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:
- For a link, its drag data is its fully-qualified
href
. - For an image, its drag data is its fully-qualified
src
. - 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.
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.
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/method | Purpose |
---|---|
dropEffect | Specifies which drag operation is accepted by the dropzone. |
effectAllowed | Specifies which operation can be performed on the dragged item. |
types | Returns 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.
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:
'text/plain'
'text/uri-list'
'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.
'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.
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:
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>
.
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.
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:
- Under the format
text/html
, we store the element'sinnerHTML
. - Under the format
text/plain
, we store the element'sinnerText
.
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.
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'
.
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:
- When they are dragged and dropped on any location that supports URLs, they are shown as a URL.
- 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)
, wheretext
is the text with the<a>
tag (excluding the markup) andurl
is its fully-qualifiedhref
.
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 thetext/uri-list
format. - The
innerText
, after being concatenated with other strings, gets stored in thetext/plain
format.
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
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.
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.
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?
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.
<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') === '');
}
As soon as we drop the <div>
into the <section>
dropzone, the following log is made:
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));
});
}
As we initiate to drag the link, we get the following logs in the console:
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));
});
}
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.