Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
143023864X_HT5.pdf
Скачиваний:
8
Добавлен:
21.02.2016
Размер:
7.98 Mб
Скачать

CHAPTER 9 WORKING WITH DRAG-AND-DROP

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.

Building an Application with Drag-and-Drop

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.

225

CHAPTER 9 WORKING WITH DRAG-AND-DROP

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.

Figure 9-4. Example page showing racers sorted into lists

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

Listing 9-2. Markup Displaying Draggable Member Names and Ages

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

226

CHAPTER 9 WORKING WITH DRAG-AND-DROP

<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 datanotation is a standard way to store non-standard attributes on an HTML element.

Our next section contains the target lists (see Listing 9-3).

Listing 9-3. Markup for Drop List Targets

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

Listing 9-4. Styles for Drag-and-Drop Demo

#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

227

CHAPTER 9 WORKING WITH DRAG-AND-DROP

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

Listing 9-5. List Item Declarations

//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).

Listing 9-6. Event Handler Registration

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

228

CHAPTER 9 WORKING WITH DRAG-AND-DROP

});

}

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

Brian says: “One of the more counter-intuitive aspects of the drag-and-drop specification is the order of events. Although you might expect that a dragged item would exit one target before it entered another, you’d be wrong!

The order of events fired during a drag from element A into element B is that a dragenter event is fired on element B before the dragleave is fired on element A. This maintains consistency with the HTML mouse event specification, but it is one of the odder aspects of the design. There are more of these quirks ahead, to be sure.”

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

229

s

CHAPTER 9 WORKING WITH DRAG-AND-DROP

Listing 9-7. dragstart Event Handler

// 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).

Listing 9-8. dragenter and dragleave Event Handlers

//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) {

230

CHAPTER 9 WORKING WITH DRAG-AND-DROP

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.

Listing 9-9. dragover Handler for Outer Container

//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).

Listing 9-10. dragover Handler for Target Lists

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

231

CHAPTER 9 WORKING WITH DRAG-AND-DROP

}

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.

Listing 9-11. Drop Handler for Target Lists

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

232

CHAPTER 9 WORKING WITH DRAG-AND-DROP

}

// 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).

Listing 9-12. dragend Handler for Clean Up

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

}

233