Traditional drag-and-drop has been popular with users since the days of the original Apple Macintosh. But today's computers and mobile devices have much more sophisticated drag-and-drop behavior. Drag-and-drop is used in file management, transferring data, diagramming, and many other operations where moving an object is more naturally envisioned with a gesture than a key command. Ask developers on the street what drag-and-drop encompasses, and you are likely to get a myriad of different answers depending on their favorite programs and current work assignments. Ask non-technical users about drag-and-drop, and they may stare at you blankly; the feature is now so ingrained into computing that it does not often get called out by name anymore.
And yet, HTML has not had drag-and-drop as a core feature in its many years of existence. Although some developers have used the built-in ability to handle low-level mouse events as a way to hack up primitive drag-and-drop, those efforts paled in comparison to the type of drag-and-drop features that have been available in desktop applications for decades. With the arrival of a well-specified set of drag-and-drop functionality, HTML applications have advanced one step closer to matching the capabilities of their desktop counterparts.
You may have seen examples of drag-and-drop on the Web already and are wondering if these are uses of HTML5 drag-and-drop. The answer? Probably not.
The reason is that HTML and DOM have exposed low-level mouse events since the early days of DOM events, and that has been sufficient for creative developers to craft a rudimentary drag-and-drop capability. When coupled with CSS positioning, it is possible to approximate a drag-and-drop system through the creation of complex JavaScript libraries and a firm knowledge of DOM events.
For example, by handling the following DOM events, it is possible to move items around in a web page if you code a set of logical steps (and some caveats):
mousedown
: The user is starting some mouse operation. (Is it a drag or just a click?)mousemove
: If the mouse is not up yet, a move operation is starting. (Is it a drag or a select?)mouseover
: The mouse has moved over an element. (Is it one of the ones I want to drop on?)mouseout
: The mouse has left an element that will no longer be a possible place to drop. (Do I need to draw feedback?)mouseup
: The mouse has released, possibly triggering a drop operation. (Should the drop complete on this location based on where it started from?)Although modeling a crude drag-and-drop system using low-level events is possible, it suffers from some notable drawbacks. First, the logic necessary to handle the mouse events is more complex than you might imagine, as each of the listed events has many edge cases that must be accounted for. Although some were in the previous list, the reality is that there are enough of them to warrant their own chapter. During these events, CSS must be carefully updated to provide feedback to the user about the possibility of dragging or dropping at any particular location.
However, an even more serious drawback is that this type of ad hoc drag-and-drop implementation relies on total control of the system. If you try mixing your app content with other content in the same page, things quickly spiral out of control when different developers start using events for their own means. Similarly, if you try to drag-and-drop content from someone else's code, you may have trouble unless the two codebases are carefully coordinated beforehand. Also, ad hoc drag-and-drop does not interact with the user's desktop or work across windows.
The new HTML5 drag-and-drop API has been designed to address these limitations, borrowing from the way drag-and-drop has been provided in other user interface frameworks.
Note Even when properly implemented, beware of the limitations of drag-and-drop in any application. Mobile devices that use drag gestures to navigate might not function correctly if drag behavior is overridden. Also, drag-and-drop can interfere with drag selection. Take care to use it sparingly and appropriately.
If you have used the drag-and-drop APIs in programming technologies such as Java or Microsoft MFC, then you're in luck. The new HTML5 drag-and-drop API is closely modeled on the concepts of these environments. Getting started is easy, but mastering the new functionality means that you will need to become acquainted with a new set of DOM events, though this time at a higher level of abstraction.
The easiest way to learn the new API is to map it to the concepts with which you are already familiar. If you are reading a book on pro HTML5 programming, we'll make a bold assumption that you are experienced with using drag-and-drop in your day-to-day computing. Nonetheless, we can start by putting some standard terms on the major concepts.
As shown in Figure 9-1, when you (as a user) start a drag-and-drop operation, you start by clicking and dragging the pointer. The item or region where you began the drag is known as the drag source. When you release the pointer and complete the operation, the region or item you are targeting at the end is known as the drop target. As the mouse moves across the page, you may traverse a series of drop targets before you actually release the mouse.
So far, so good. But simply holding down the mouse and moving it to another part of an application is not what constitutes a drag-and-drop. Rather, it is the feedback during the operation that makes for a successful interaction. Consider your own uses of drag-and-drop in past experiences; the ones that are the most intuitive are those where the system is giving constant updates to let you know what will happen if you release at this point in time:
In order to give similar feedback to users over the course of an HTML drag-and-drop operation, the browsers will emit a whole slew of events over the course of a single drag. This proves quite handy, as during these events we will have full power to change the DOM and style of the page elements to give just the type of feedback that users will be expecting.
Beyond the drag source and drop target, there is one more key concept to learn in the new API: the data transfer. The specification describes the data transfer as the set of objects used to expose the drag data store that underlies a drag-and-drop operation. However, it may be easier just to think of the data transfer as being the central control of drag-and-drop. The operation type (e.g., move, copy, or link), the image to use as feedback during the drag, and the retrieval of the data itself are all managed here.
Regarding the data itself, the dataTransfer mechanism for completing the drop directly addresses one of the limitations of the old ad hoc drag-and-drop techniques described previously. Instead of forcing all drag sources and drop targets to be aware of each other, the data transfer mechanism works similar to a network protocol negotiation. In this case, the negotiation is performed via Multipurpose Internet Mail Exchange (MIME) types.
Note MIME types are the same types used to attach files to e-mail. They are an Internet standard that is used pervasively in all types of Web traffic, and they are very common in HTML5. In short, MIME types are standardized text strings used to classify the type of unknown content, such as “text/plain” for plain text and “image/png” for PNG images.
The purpose of using MIME types is to allow the source and target to negotiate on which format best suits the needs of the drop target. As shown in Figure 9-2, during a drag start, the dataTransfer object is loaded up with data representing all reasonable types, or “flavors,” by which the data can be transferred. Then, when the drop completes, the drop handler code can scan the available types of data and decide which MIME type format best suits its needs.
For example, imagine a list item in a web page representing a person. There are many different ways to represent the data for a person; some are standard, some are not. When a drag starts on a particular person's list item, the drag start handler can declare that the person's data is available in a few formats, as shown in Table 9-1.
When the drop completes, the drop handler can query for a list of available data types. From the provided list, the handler can choose which type is most appropriate. A text list drop target may choose to grab the text/plain “flavor” of data to retrieve the person's name, while a more advanced control might choose to retrieve and display the person's PNG image as a result of the drop. And, if the source and target have coordinated on non-standard types, the target could also retrieve the person's age at the time of the drop.
It is this negotiation process that allows for drag sources and drop targets to be decoupled. As long as the drag sources provide data in a choice of MIME types, the drop target can choose which format suits its operation the best, even if the two came from different developers. In later sections of this chapter, we'll explore how even more unusual MIME types, such as files, can be used.
Now that we've explored the key concepts of the drag-and-drop API, let's focus on the events that can be used throughout the process. As you'll see, the events operate at a higher level than the mouse events previously utilized to mock up a drag-and-drop system. However, drag-and-drop events extend the DOM mouse event. Therefore, you still have access to the low-level mouse information, such as coordinates, if you need it.s
But before we focus on drag and-drop-itself, let's refresh on two DOM event functions that have been around since the browsers standardized on DOM Level 3 events: the stopPropagation and preventDefault functions.
Consider the case where one element in a page is nested inside another element. We will refer to them as the child and parent elements, respectively. The child takes up some, but not all, of the visible space of the parent. Although we are only referring to two elements in our example, in practice a web page often has many levels of nesting.
When a user clicks a mouse on the child, which element should actually receive the event: the child, the parent, or both? And if both, in which order? The answer to this question was settled by the World Wide Web Consortium (W3C) in the DOM events specification. Events flow from a parent, through intermediaries, and down to the most specific child first in a process known as “event capture.” Once the child has had access to the event, the event flows back up the element hierarchy via a process known as “event bubbling.” Together, these two flows allow developers to catch and process the event in the way that is most suitable to their page architecture. Only elements with handlers actually registered will process the event, which keeps the system lightweight. The overall approach is a compromise among different behaviors from multiple browser vendors, and it is consistent with other native development frameworks, some of which capture and some of which bubble.
However, at any time a handler can call the stopPropagation function on the event, which will stop it from further traversing down the event capture chain or up through the bubbling phase.
Note Microsoft has provided a great interactive demo of event models at http://ie.microsoft.com/testdrive/HTML5/ComparingEventModels
Browsers also have default implementations for how some events will be handled. For example, when a user clicks on a page link, the default behavior is to navigate the browser to the destination specified by the link. Developers can prevent this by intercepting the event in a handler and calling preventDefault on it. This allows code to override the default behaviors of some built-in events. It is also how a developer can cancel a drag-and-drop operation in an event handler.
Both stopPropagation
and preventDefault
will be handy in our examples of the drag-and-drop API.
When a user initiates a drag-and-drop operation in an HTML5-ready browser, a series of events trigger at the start and continue throughout the course of the whole operation. We will examine them in turn here.
The dragstart event is fired on an element in the page when the user begins to drag on it. In other words, once the mouse is down and the user moves the mouse, the dragstart
is initiated. The dragstart event is of key importance, as it is the only event where the dataTransfer
can have data set on it using the setData
call. This means that in a dragStart
handler, the possible data types need to be set up so that they can be queried at the end of the drop, as described previously.
Interception!
The drag event can be thought of as the continuous event of a drag operation. As the user moves the mouse cursor around the page, the drag
event is called repeatedly on the drag source. The drag event will fire a few times each second during the operation. Although the visuals of the drag feedback can be modified during a drag
event, the data on the dataTransfer
is off-limits.
When the drag crosses into a new element on the page, a dragenter
event fires on that element. This event is a good time to set drop feedback on the element based on whether or not it can receive the drop.
Conversely, the browser will fire a dragleave
event whenever the user moves the drag out of the element where dragenter
was previously called. Drop feedback can be restored at this time, as the mouse is no longer over this target.
The dragover
event is called at frequent intervals as the mouse moves over an element during a drag operation. Unlike its counterpart drag event, which is called on the drag source, this event is called on the current target of the mouse.
The drop
event is called on the current mouse target when the user releases the mouse. Based on the result of the dataTransfer
object, this is where the code to handle the drop should be executed.
The final event in the chain, dragend
fires on the drag source, indicating that the drag completed. It is particularly suitable for cleaning up the state
used during the drag, as it is called regardless of whether or not the drop completes.
Altogether, there are plenty of ways for you to intercept the drag-and-drop operations and take action. The drag-and-drop event chain is summarized in Figure 9-3.
Now that you've seen the different events that can be triggered during a drag-and-drop operation, you might be wondering what it takes to mark elements in your web application as draggable. That's easy!
Aside from a few elements—such as text controls—elements in a page are not draggable by default. In order to mark a specific element as draggable, however, all you need to do is add one attribute: draggable.
<div id=”myDragSource” draggable=”true”>
Simply by adding that attribute, you cause the browser to fire the aforementioned events. Then, you only need to add the event handlers to manage them.
Before we move into our example, let's assess the dataTransfer
object in more detail. The dataTransfer is available from every drag-and-drop event, as shown in Listing 9-1.
Function handleDrag(evt) {
var transfer = evt.dataTransfer;
// …
}
As discussed in Listing 9-1, the dataTransfer
is used to get and set the actual drop data during the negotiation between source and target. This is done using the following functions and properties:
setData(format, data)
: Calling this function during dragStart allows you to register one transfer item under a MIME type format.getData(format)
: This function allows the registered data item for a given type to be retrieved.types
: This property returns an array of all currently registered formats.items
: This property returns a list of all items and their associated formats together.files
: This property returns any files associated with the drop. This is discussed in more detail in a later section.clearData()
: Calling this function with no argument clears out all registered data. Calling it with a format argument removes only that specific registration.Two more functions can be used to alter the feedback during a drag operation:
setDragImage(element, x, y)
: Tells the browser to use an existing image element as the drag image, which will display alongside the cursor to hint to the user about the drag operation effects. If x and y coordinates are provided, then those coordinates will be considered as the drop point for the mouse.addElement(element)
: By calling this function with a provided page element, you tell the browser to draw that element as a drag feedback image.A final set of properties allows the developer to set and/or query the types of drag operations that are allowed:
effectAllowed
: Setting this property to one of none, copy, copyLink, copyMove, link, linkMove, move, or all tells the browser that only the type(s) of operations listed here are to be allowed for the user. For example, if copy is set, only copy operations will be allowed, and move or link operations will be prevented.dropEffect
: This property can be used to determine which type of operation is currently underway or set to force a particular operation type. The types of operations are copy, link, and move. Or, the value none can be set to prevent any drop from happening at that point in time.Together, these operations give a fine level of control over drag-and-drop. Now, let's see them in action.
Using the concepts we've already learned, we'll build a simple drag-and-drop page in the theme of our Happy Trails Running Club. This page lets the club race organizers drag members of the club into one of two lists: racers and volunteers. In order to sort them into competitive groups, racers will be sorted by their age. Volunteers, on the other hand, are only sorted by their names, as their ages don't matter when they are not competing.
The sorting of the lists is done automatically. The application itself will show feedback indicating where proper drop areas are for members into the two lists as shown in Figure 9-4.
All of the code for this example is included with the book's samples in the code/draganddrop directory. We'll step through the page and explain how it works in practice.
First, let's look at the markup for the page. At the top, we've declared the data on our club members (see Listing 9-2).
<p>Drag members to either the Racers or Volunteers list.</p>
<ul id="members">
<li draggable="true" data-age="38">Brian Albers</li>
<li draggable="true" data-age="25">Frank Salim</li>
<li draggable="true" data-age="47">Jennifer Clark</li>
<li draggable="true" data-age="18">John Kemble</li>
<li draggable="true" data-age="20">Lorraine Gaunce</li>
<li draggable="true" data-age="30">Mark Wang</li>
<li draggable="true" data-age="41">Morgan Stephen</li>
<li draggable="true" data-age="39">Peter Lubbers</li>
<li draggable="true" data-age="33">Vanessa Combs</li>
<li draggable="true" data-age="54">Vivian Lopez</li>
</ul>
As you can see, each of the member list elements is marked as draggable
. This tells the browser to let drags start on each of them. The next thing you'll notice is that the age of a given member is encoded as a data attribute. The data
- notation is a standard way to store non-standard attributes on an HTML element.
Our next section contains the target lists (see Listing 9-3).
<div class="dropList">
<fieldset id="racersField">
<legend>Racers (by Age):</legend>
<ul id="racers"></ul>
</fieldset>
</div>
<div class="dropList">
<fieldset id="volunteersField">
<legend>Volunteers (by Name):</legend>
<ul id="volunteers"></ul>
</fieldset>
</div>
The unordered lists identified as racers
and volunteers
are the ultimate destinations where our members will be inserted. The fieldsets
surrounding them serve as functional equivalents of a moat around a castle. When the user drags into the fieldset
, we'll know that they have exited the contained list and we'll update our visual feedback accordingly.
Speaking of feedback, there are a few CSS styles in our page that are important to note (see Listing 9-4).
#members li {
cursor: move;
}
.highlighted {
background-color: yellow;
}
.validtarget {
background-color: lightblue;
}
First, we make sure that every member in our source list shows a move cursor. This gives a hint to the user that the items are draggable.
Next, we define two style classes: highlighted
and validtarget
. These are used to draw background colors on our lists as the drag-and-drop is in progress. The validtarget
background will be displayed on our destination lists during the entire drag to hint that they are valid drop targets. When the user actually moves a member over a target list it will change to the highlighted
style, indicating that the user is actually over a drop target.
To keep track of the state on our page, we'll declare a few variables (see Listing 9-5).
// these arrays hold the names of the members who are
// chosen to be racers and volunteers, respectively
var racers = [];
var volunteers = [];
// these variables store references to the visible
// elements for displaying who is a racer or volunteer
var racersList;
var volunteersList;
The first two variables will serve as internal arrays, which keep track of which members are in the racers and volunteers lists. The second two variables are only going to be used as handy references to the unordered lists containing the visual display of members in the respective lists.
Now, let's set all of our page items up to handle drag-and-drop (see Listing 9-6).
function loadDemo() {
racersList = document.getElementById("racers");
volunteersList = document.getElementById("volunteers");
// our target lists get handlers for drag enter, leave, and drop
var lists = [racersList, volunteersList];
[].forEach.call(lists, function(list) {
list.addEventListener("dragenter", handleDragEnter, false);
list.addEventListener("dragleave", handleDragLeave, false);
list.addEventListener("drop", handleDrop, false);
});
// each target list gets a particular dragover handler
racersList.addEventListener("dragover", handleDragOverRacers, false);
volunteersList.addEventListener("dragover", handleDragOverVolunteers, false);
// the fieldsets around our lists serve as buffers for resetting
// the style during drag over
var fieldsets = document.querySelectorAll("#racersField, #volunteersField");
[].forEach.call(fieldsets, function(fieldset) {
fieldset.addEventListener("dragover", handleDragOverOuter, false);
});
// each draggable member gets a handler for drag start and end
var members = document.querySelectorAll("#members li");
[].forEach.call(members, function(member) {
member.addEventListener("dragstart", handleDragStart, false);
member.addEventListener("dragend", handleDragEnd, false);
});
}
window.addEventListener("load", loadDemo, false);
When the window initially loads, we call a loadDemo
function to set up all of our drag-and-drop event handlers. Most of them don't need event capture, and we will set the capture argument accordingly. Both the racersList
and the volunteersList
will receive handlers for dragenter, dragleave
, and drop
events, as these are fired on drop targets. Each list will receive a separate dragover event listener, as that will allow us to easily update the drag feedback based on the target the user is currently dragging over.
As mentioned previously, we are also adding dragover
handlers on the fieldsets surrounding the target lists. Why do we do this? To make it easier to detect
when a drag has exited our target lists. Although it is easy for us to detect that a user has dragged an item over our list, it is not so easy to determine when the user has dragged an item out of our list. This is because the dragleave events fire both when an item is dragged out of our list and when the item is dragged over a child already in the destination list. Essentially, when you drag from a parent element over one of its contained children, the drag exits the parent and enters the child. Although this provides a lot of information, it actually makes it tricky to know when a drag is leaving the outer boundaries of a parent element. Therefore, we will use a notification that we are dragging over an element surrounding our list to inform us that we have exited the list. More information on this will be provided later.
This Way to the eGRESS
Our final set of handlers registers dragstart
and dragend
listeners on every draggable
club member in our initial list. We will use them to initialize and clean up any drag. You might notice that we don't add handlers for the drag
event, which fires periodically on the drag source. As we will not be updating the appearance of the dragged item, it will be unnecessary for our example.
Now, we'll go through the actual event handlers in turn, based on the order in which they generally fire (see Listing 9-7).
// called at the beginning of any drag
function handleDragStart(evt) {
// our drag only allows copy operations
evt.effectAllowed = "copy";
// the target of a drag start is one of our members
// the data for a member is either their name or age
evt.dataTransfer.setData("text/plain", evt.target.textContent);
evt.dataTransfer.setData("text/html", evt.target.dataset.age);
// highlight the potential drop targets
racersList.className = "validtarget";
volunteersList.className = "validtarget";
return true;
}
The handler for dragstart
is called on the draggable
item where the users begin the operation. It is a somewhat special handler, as it sets up the capabilities of the entire process. First, we set the effectAllowed
, which tells the browser that only copies are allowed when dragging from this element—no moves or links.
Next, we preload all of the possible flavors of data that might be requested at the end of a successful drop. Naturally, we want to support a text version of our element, so we set the MIME type text/plain
to return the text inside our draggable
node, (i.e., the club member's name).
For our second data flavor, we would like the drop operation to transfer another type of data about the drag source; in our case, it is the age of the club member. Unfortunately, due to bugs, not all browsers support user-defined MIME types such as application/x-age
yet, which would be the best fit for such an arbitrary flavor. Instead, we will reuse another commonly supported MIME format—text/html
—to stand in for an age flavor for now. Hopefully the WebKit browsers will address this limitation soon.
Don't forget that the dragstart
handler is the only handler where data transfer values can be set. Attempting to do so in other handlers will fail in order to prevent rogue code from changing the data mid-drag.
Our final action in the start handler is purely for demo purposes. We will change the background color of our potential drop target lists to give the user a hint about what is possible. Our next handlers will process events as the dragged item enters and leaves elements on the page (see Listing 9-8).
// stop propagation and prevent default drag behavior
// to show that our target lists are valid drop targets
function handleDragEnter(evt) {
evt.stopPropagation();
evt.preventDefault();
return false;
}
function handleDragLeave(evt) {
return false;
}
The dragleave
event is not used by our demo, and we handle it purely for illustrative purposes.
The dragenter
event, however, can be handled and canceled by calling preventDefault
on it when it is fired over a valid drop target. This informs the browser that the current target is a valid drop target, as the default behavior is to assume that any target is not a valid drop target.
Next, we will look at the dragover handlers (see Listing 9-9). Recall that these fire at regular intervals whenever the drag hovers over the elements in question.
// for better drop feedback, we use an event for dragging
// over the surrounding control as a flag to turn off
// drop highlighting
function handleDragOverOuter(evt) {
// due to Mozilla firing drag over events to
// parents from nested children, we check the id
// before handling
if (evt.target.id == "racersField")
racersList.className = "validtarget";
else if (evt.target.id == "volunteersField")
volunteersList.className = "validtarget";
evt.stopPropagation();
return false;
}
Our first of three dragover
handlers will be used only to adjust the drag feedback. Recall that it is difficult to detect when a drag has left a target, such as our intended racers and volunteers lists. Therefore, we use a drag movement over the fieldsets surrounding the lists to indicate that the drag has exited the vicinity of the lists. This allows us to turn off the drop highlighting on the lists accordingly.
Note that our simple code, as listed, will change the CSS className
repeatedly if the user hovers in the fieldset area. For optimization purposes, it is good practice to only change the className
once, as it may cause the browser to do more work than necessary.
Finally, we stop propagation of the event to any other handlers in the page. We don't want any other handlers to override our logic. In the next two dragover
handlers, we take a different approach (see Listing 9-10).
// if the user drags over our list, show
// that it allows copy and highlight for better feedback
function handleDragOverRacers(evt) {
evt.dataTransfer.dropEffect = "copy";
evt.stopPropagation();
evt.preventDefault();
racersList.className = "highlighted";
return false;
}
function handleDragOverVolunteers(evt) {
evt.dataTransfer.dropEffect = "copy";
evt.stopPropagation();
evt.preventDefault();
volunteersList.className = "highlighted";
return false;
}
These two handlers, while somewhat verbose, are listed in full to clarify our demo. The first handles dragover events in the racers list, and the second handles dragover
events identically in the volunteers list.
The first action we take is to set the dropEffect
to indicate that only copies are allowed on this node, not moves or links. This is a good practice, even though our original dragstart
handler already limited the drag-and-drop operation to be copy-only.
Next we prevent other handlers from accessing the event and cancel it. Canceling a dragover event has an important function: it tells the browser that the default operation—not allowing a drop here—is not valid. Essentially, we are telling the browser that it should not not allow a drop; and so, the drop is allowed. Although this may seem counter-intuitive, recall that preventDefault
is used to tell the browser not to do its normal built-in operation for an event. For example, calling preventDefault
on a click on a link tells the browser to not navigate to the link's reference. The specification designers could have created a new event or API for this dragover
, but they opted to keep to the API patterns that were already used throughout HTML.
We will also give the user visual feedback by changing the background color to yellow via the highlighted
CSS class whenever the user drags over our lists. The main work of the drag-and-drop is done in the drop handler, which we examine next in Listing 9-11.
// when the user drops on a target list, transfer the data
function handleDrop(evt) {
evt.preventDefault();
evt.stopPropagation();
var dropTarget = evt.target;
// use the text flavor to get the name of the dragged item
var text = evt.dataTransfer.getData("text/plain");
var group = volunteers;
var list = volunteersList;
// if the drop target list was the racer list, grab an extra
// flavor of data representing the member age and prepend it
if ((dropTarget.id != "volunteers") &&
(dropTarget.parentNode.id != "volunteers")) {
text = evt.dataTransfer.getData("text/html") + ": " + text;
group = racers;
list = racersList;
}
// for simplicity, fully clear the old list and reset it
if (group.indexOf(text) == -1) {
group.push(text);
group.sort();
// remove all old children
while (list.hasChildNodes()) {
list.removeChild(list.lastChild);
}
// push in all new children
[].forEach.call(group, function(person) {
var newChild = document.createElement("li");
newChild.textContent = person;
list.appendChild(newChild);
});
}
return false;
}
Once again, we start by preventing the default drop behavior and preventing the control from propagating to other handlers. The default drop event depends on the location and type of element dropped. For example, dropping an image dragged in from another source displays it in the browser window, and dropping a link into a window navigates to it by default. We want total control of drop behavior in our demo, so we cancel any default behaviors.
Recall that our demo shows how multiple data flavors set up in the dragstart
can be retrieved from a dropped element. Here, we see how that retrieval completes. By default, we get the plain text data representing the club member's name by using the text/plain MIME format. If the user drops into the volunteers list, this is sufficient.
However, if the user is dropping the club member into the racers list, we take one additional step to fetch the age of the club member, which we previously set using the text/html flavor during dragstart
. We prepend it to the club member's name to display both age and name in the racers list.
Our final block of code is a simple, albeit unoptimized, routine to clear out all previous members of the target list, add our new member (if he didn't exist already), sort, and refill the list. The end result is a sorted list containing the old members and the newly dropped member, if he was not present before.
Regardless of whether or not the user completed the drag-and-drop, we need a dragend handler to clean up (see Listing 9-12).
// make sure to clean up any drag operation
function handleDragEnd(evt) {
// restore the potential drop target styles
racersList.className = null;
volunteersList.className = null;
return false;
}
A dragend
handler is called at the end of the drag, whether or not a drop actually occurred. If the user canceled the drag or completed it, the dragend
handler is still called. This gives us a good place to clean up any state we changed at the beginning of the process. Not surprisingly, we reset the CSS classes of our lists to their default, unstyled state.
Sharing Is Caring
If you're thinking that handling all of the drag-and-drop events is complicated, you're not alone. The authors of the specification have designed an alternative, shorthand mechanism to support drop events: the dropzone attribute.
The dropzone provides developers with a compact way to register that an element is willing to accept drops without coding up lengthy event handlers. The attribute consists of a few space-separated patterns that, when provided, allow the browser to automatically wire up the drop behavior for you (see Table 9-2).
Borrowing from our example application, the racers list element could be specified as having the following attribute:
This provides a quick way of telling the browser that copy operations for elements that support either the plain text or HTML data format are allowed to drop on our list.
The dropzone
is not supported by most major browser vendors at the time of writing, but support for it is likely forthcoming.
If you've ever wanted an easier way to add files to your web application, or you've wondered how some of the newest sites allow you to drag files directly into a page and upload them, the answer is the HTML5 File API. Although the size and status of the entire W3C File API is out of scope for this discussion, many browsers already support a subset of the standard, which allows files to be dragged into an application.
Note The W3C File API is documented online at www.w3.org/TR/FileAPI
.
The File API contains functionality for asynchronously reading files in a web page, uploading them to servers while tracking process, and turning files into page elements. However, affiliated specifications such as drag-and-drop use a subset of the File API, and that is the area where we will focus our attention in this chapter.
Recall that we've already alluded to file drag-and-drop twice in this chapter. First, the dataTransfer
object contains a property named files
, which will contain a list of files attached to the drag, if appropriate. For example, if a user drags a file or set of files in from the desktop into your application's web page, the browser will fire drag-and-drop events where the dataTransfer.files
object has a value. Additionally, browsers that support the previously mentioned dropzone attribute allow files of specific MIME types to be valid drops on an element by using the f
: MIME type prefix.
Note Currently, the Safari browser only supports drag-and-drop operations for files. Drags initiated inside a page will fire most drag-and-drop events, but drop events only occur if the type of drag is a file.
As usual, you cannot access the files during most drag-and-drop events, because they are protected for security reasons. Although some browsers might let you get access to the list of files during drag events, no browser will let you get access to the file data. In addition, the dragstart, drag
, and dragend events that are fired at the drag source element are not triggered in a file drag-and-drop, as the source is the file system itself.
The file items in our file list support the following properties:
Let's walk through a simple example of file drag-and-drop where we will show the characteristics of any file dropped onto our page, shown in Figure 9-5. This code is contained in the fileDrag.html example included with the book.
The HTML for our demo is actually quite simple (see Listing 9-13).
<body>
<div id="droptarget">
<div id="status"></div>
</div>
</body>
We have only two elements in the page. A drop target where files will be dropped and a status display area.
As with our last example, we will register drag-and-drop event handlers during page load (see Listing 9-14).
var droptarget;
// set the status text in our display
function setStatus(text) {
document.getElementById("status").innerHTML = text;
}
// ...
function loadDemo() {
droptarget = document.getElementById("droptarget");
droptarget.className = "validtarget";
droptarget.addEventListener("dragenter", handleDragEnter, false);
droptarget.addEventListener("dragover", handleDragOver, false);
droptarget.addEventListener("dragleave", handleDragLeave, false);
droptarget.addEventListener("drop", handleDrop, false);
setStatus("Drag files into this area.");
}
window.addEventListener("load", loadDemo, false);
This time, the drop target receives all of the event handlers. Only a subset of handlers is needed, and we can ignore events that take place at the drag source.
When the user drags files into our drop target, we will display what we know about the drop candidates (see Listing 9-15).
// handle drag events in the drop target
function handleDragEnter(evt) {
// if the browser supports accessing the file
// list during drag, we display the file count
var files = evt.dataTransfer.files;
if (files)
setStatus("There are " + evt.dataTransfer.files.length +
" files in this drag.");
else
setStatus("There are unknown items in this drag.");
droptarget.className = "highlighted";
evt.stopPropagation();
evt.preventDefault();
return false;
}
Although some browsers allow access to the dataTransfer
files mid-drag, we will handle the case where that information is off-limits. When the count is known, we will display it in the status.
Handling dragover
and dragleave
events is straightforward (see Listing 9-16).
// preventing the default dragover behavior
// is necessary for successful drops
function handleDragOver(evt) {
evt.stopPropagation();
evt.preventDefault();
return false;
}
// reset the text and status when drags leave
function handleDragLeave(evt) {
setStatus("Drag files into this area.");
droptarget.className = "validtarget";
return false;
}
As always, we must cancel dragover
events to allow drops to be handled by our own code rather than the browser's default behavior, which is usually to display them inline. For a dragleave
, we only set the status text and style to indicate that drops are no longer valid when the mouse leaves. The bulk of our work is done in the drop handler (see Listing 9-17).
// handle the drop of files
function handleDrop(evt) {
// cancel the event to prevent viewing the file
evt.preventDefault();
evt.stopPropagation();
var filelist = evt.dataTransfer.files;
var message = "There were " + filelist.length + " files dropped.";
// show a detail list for each file in the drag
message += "<ol>";
[].forEach.call(filelist, function(file) {
message += "<li>";
message += "<strong>" + file.name + "</strong> ";
message += "(<em>" + file.type + "</em>) : ";
message += "size: " + file.size + " bytes - ";
message += "modified: " + file.lastModifiedDate;
message += "</li>";
});
message += "</ol>";
setStatus(message);
droptarget.className = "validtarget";
return false;
}
As discussed previously, it is necessary to cancel the event using preventDefault
so that the browser's default drop code is never triggered.
Then, because we have more access to data in the drop handler than during the drag, we can inspect the files
attached to the dataTransfer
and discover the characteristics of the dropped files. In our example, we will merely display the properties of the files, but with full use of the HTML5 File API, you can read in the contents for local display or upload them to the server powering your application.
Sometimes there are techniques that don't fit into our regular examples but which nonetheless apply to many types of HTML5 applications. We present to you a short, but common, practical extra here.
Usually, the browser will default the visual cursor indicator for a drag operation. An image or link will move with the cursor (sometimes sized down for practical viewing), or a ghosted image of the dragged element will hover at the drag position.
However, if you need to change the default drag image display, the API provides you with a simple API for doing just that. It is only possible to change the drag image during the dragstart handler—once again due to security concerns—but you can do so easily by simply passing the element that represents the appearance of the cursor to the dataTransfer
.
var dragImage = document.getElementById("happyTrails");
evt.dataTransfer.setDragImage(dragImage, 5, 10);
Note the offset coordinates passed to the setDragImage
call. These x and y coordinates tell the browser which pixel inside the image to use as the point underneath the mouse cursor. For example, by passing in the values 5 and 10 for x and y, respectively, the image will be positioned such that the cursor is 5 pixels from the left and 10 pixels from the top, as shown in Figure 9-6.
The drag image does not need to be an image, however. Any element can be set as the drag image; if it is not an image, the browser will create a visual snapshot of it to serve as the cursor display.
The drag-and-drop API can be a tricky one to master. It involves the correct handling of many events, some of which may be hard to manage if your drop target layout is complex. However, if you are looking for drag operations that cross windows or browsers, or even interact with the desktop, you will need to learn the subtleties of the API. By design, it combines the power of native application drag-and-drop while still working inside the security restrictions of an environment where data must be protected from third-party code.
For more information on using dropped files as application data, make sure to check out the W3C File API. In the next chapter, we will examine the Web Workers API, which will allow you to spawn background scripts outside of your main page to speed up execution and improve the user experience.