Drag and drop is one of the most fundamental interactions afforded by graphical user interfaces. In one gesture, it allows users to pair the selection of an object with the execution of an action, often including a second object in the operation. It's a simple yet powerful UI concept used to support copying, list reordering, deletion (ala the Trash / Recycle Bin), and even the creation of link relationships.
Since it's so fundamental, offering drag and drop in web
applications has been a no-brainer ever since browsers first
offered mouse events in DHTML. But, although
mousedown
, mousemove
, and
mouseup
made it possible, the implementation has been
limited to the bounds of the browser window. Additionally,
since these events refer only to the object being dragged,
there's a challenge to find the subject of the drop when
the interaction is completed.
Of course, that doesn't prevent most modern JavaScript frameworks from abstracting away most of the problems and throwing in some flourishes while they're at it. But, wouldn't it be nice if browsers offered first-class support for drag and drop, and maybe even extended it beyond the window sandbox?
As it turns out, this very wish is answered by the HTML 5 specification section on new drag-and-drop events, and Firefox 3.5 includes an implementation of those events.
If you want to jump straight to the code, I've put together some simple demos of the new events.
I've even scratched an itch of my own and built the beginnings of an outline editor, where every draggable element is also a drop target—of which there could be dozens to hundreds in a complex document, something that gave me some minor hair-tearing moments in the past while trying to make do with plain old mouse events.
And, all the above can be downloaded or cloned from a GitHub repository I've created especially for this article.
So, with no further ado, here are the new drag and drop events, in roughly the order you might expect to see them fired:
dragstart
drag
dragenter
dragover
false
or calling preventDefault()
in
the event handler indicates that a drop is allowed here.
dragleave
drop
dragend
Like the mouse events of yore, listeners can be attached to
elements using addEventListener()
directly or by way of your favorite JS library.
Consider the following example using jQuery, also available as a live demo:
<div id="newschool">
<div class="dragme">Drag me!</div>
<div class="drophere">Drop here!</div>
</div>
<script type="text/javascript">
$(document).ready(function() {
$('#newschool .dragme')
.attr('draggable', 'true')
.bind('dragstart', function(ev) {
var dt = ev.originalEvent.dataTransfer;
dt.setData("Text", "Dropped in zone!");
return true;
})
.bind('dragend', function(ev) {
return false;
});
$('#newschool .drophere')
.bind('dragenter', function(ev) {
$(ev.target).addClass('dragover');
return false;
})
.bind('dragleave', function(ev) {
$(ev.target).removeClass('dragover');
return false;
})
.bind('dragover', function(ev) {
return false;
})
.bind('drop', function(ev) {
var dt = ev.originalEvent.dataTransfer;
alert(dt.getData('Text'));
return false;
});
});
</script>
Thanks to the new events and jQuery, this example is both short and simple—but it packs in a lot of functionality, as the rest of this article will explain.
Before moving on, there are at least three things about the above code that are worth mentioning:
Drop targets are enabled by virtue of having
listeners for drop events. But,
per the HTML 5 spec,
draggable elements need an
attribute of draggable="true"
, set either in
markup or in JavaScript.
Thus, $('#newschool .dragme').attr('draggable', 'true')
.
The original DOM event (as opposed to jQuery's event
wrapper) offers a property called dataTransfer
.
Beyond just manipulating elements, the new drag and drop
events accomodate the transmission of user-definable data
during the course of the interaction.
Since these are first-class events, you can apply the technique of Event Delegation.
What's that? Well, imagine you have a list of 1000
list items—as part of a deeply-nested outline document,
for instance. Rather than needing to attach listeners
or otherwise fiddle with all 1000 items, simply attach
a listener to the parent node (eg. the
<ul>
element) and all events from
the children will propagate up to the single parent listener.
As a bonus, all new child elements added after page
load will enjoy the same benefits.
Check out this demo, and the associated JS code to see more about these events and Event Delegation.
As mentioned in the last section, the new drag and drop events let you send data along with a dragged element. But, it's even better than that: Your drop targets can receive data transferred by content objects dragged into the window from other browser windows, and even other applications.
Since the example is a bit longer,
check out the live demo
and
associated code
to get an idea of what's possible with dataTransfer
.
In a nutshell, the stars of this show are the
setData()
and getData()
methods of
the dataTransfer
property exposed by the Event object.
The setData()
method is typically called in the
dragstart
listener, loading dataTransfer
up with one or more strings of content with associated
recommended content types.
For illustration, here's a quick snippet from the example code:
var dt = ev.originalEvent.dataTransfer;
dt.setData('text/plain', $('#logo').parent().text());
dt.setData('text/html', $('#logo').parent().html());
dt.setData('text/uri-list', $('#logo')[0].src);
On the other end, getData()
allows you to query
for content by type (eg. text/html
followed by
text/plain
). This, in turn, allows you to decide
on acceptable content types at the time of the
drop
event or even during dragover
to offer feedback for unacceptable types during the drag.
Here's another example from the receiving end of the example code:
var dt = ev.originalEvent.dataTransfer;
$('.content_url .content').text(dt.getData('text/uri-list'));
$('.content_text .content').text(dt.getData('text/plain'));
$('.content_html .content').html(dt.getData('text/html'));
Where dataTransfer
really shines, though, is that
it allows your drop targets to receive content from
sources outside your defined draggable elements and even from
outside the browser altogether. Firefox accepts such drags,
and attempts to populate dataTransfer
with
appropriate content types extracted from the external object.
Thus, you could select some text in a word processor window and
drop it into one of your elements, and at least expect to find
it available as text/plain
content.
You can also select content in
another browser window, and expect to see text/html
appear in your events. Check out the
outline editing demo
and see what happens when you try dragging various elements
(eg. images, tables, and lists) and highlighted content from
other windows onto the items there.
An important aspect of the drag and drop interaction is a
representation of the thing being dragged. By default in
Firefox, this is a "ghost" image of the dragged element itself. But,
the dataTransfer
property of the original Event
object exposes the method setDragImage()
for use
in customizing this representation.
There's a live demo of this feature, as well as associated JS code available. The gist, however, is sketched out in these code snippets:
var dt = ev.originalEvent.dataTransfer;
dt.setDragImage( $('#feedback_image h2')[0], 0, 0);
dt.setDragImage( $('#logo')[0], 32, 32);
var canvas = document.createElement("canvas");
canvas.width = canvas.height = 50;
var ctx = canvas.getContext("2d");
ctx.lineWidth = 8;
ctx.moveTo(25,0);
ctx.lineTo(50, 50);
ctx.lineTo(0, 50);
ctx.lineTo(25, 0);
ctx.stroke();
dt.setDragImage(canvas, 25, 25);
You can supply a DOM node as the first parameter to
setDragImage()
, which includes everything from
text to images to <canvas>
elements. The
second two parameters indicate at what left and top offset
the mouse should appear in the image while dragging.
For example, since the #logo
image is 64x64,
the parameters in the second setDragImage()
method places the mouse right in the center of the image.
On the other hand, the first call positions the feedback
image such that the mouse rests in the upper left corner.
As mentioned at the start of this article, the drag and drop
interaction has been used to support actions such as copying,
moving, and linking. Accordingly, the HTML 5 specification
accomodates these operations in the form of the
effectAllowed
and dropEffect
properties exposed by the Event object.
For a quick fix, check out the a live demo of this feature, as well as the associated JS code.
The basic idea is that the dragstart
event
listener can set a value for effectAllowed
like so:
var dt = ev.originalEvent.dataTransfer;
switch (ev.target.id) {
case 'effectdrag0': dt.effectAllowed = 'copy'; break;
case 'effectdrag1': dt.effectAllowed = 'move'; break;
case 'effectdrag2': dt.effectAllowed = 'link'; break;
case 'effectdrag3': dt.effectAllowed = 'all'; break;
case 'effectdrag4': dt.effectAllowed = 'none'; break;
}
The choices available for this property include the following:
none
copy
move
link
copyMove
copyLink
linkMove
all
On the other end, the dragover
event listener
can set the value of the
dropEffect
property to indicate the expected effect
invoked on a successful drop. If the value does
not match up with effectAllowed
, the drop will
be considered cancelled on completion.
In the a live demo, you should be able to see that only elements with matching effects can be dropped into the appropriate drop zones. This is accomplished with code like the following:
var dt = ev.originalEvent.dataTransfer;
switch (ev.target.id) {
case 'effectdrop0': dt.dropEffect = 'copy'; break;
case 'effectdrop1': dt.dropEffect = 'move'; break;
case 'effectdrop2': dt.dropEffect = 'link'; break;
case 'effectdrop3': dt.dropEffect = 'all'; break;
case 'effectdrop4': dt.dropEffect = 'none'; break;
}
Although the OS itself can provide some feedback, you can also use these properties to update your own visible feedback, both on the dragged element and on the drop zone itself.
The new first-class drag and drop events in HTML5 and Firefox make supporting this form of UI interaction simple, concise, and powerful in the browser. But beyond the new simplicity of these events, the ability to transfer content between applications opens brand new avenues for web-based applications and collaboration with desktop software in general.