Dynamic HTML: The Definitive Reference, 2rd Ed.Dynamic HTML: The Definitive ReferenceSearch this book

6.7. Dragging Elements

The final example in this chapter, Example 6-2, demonstrates how event bubbling lets document-level event handlers control dragging positioned elements on the page. To provide legacy support for readers of the first edition of this book, the code includes branches that accommodate the unique requirements of Netscape Navigator 4, whose layer objects may be positioned dynamically. While Navigator 4 does not employ event bubbling, it does allow for the equivalent of the W3C's event capture mechanism, albeit switched on with very different syntax. But all event processing takes place at the same node level for all browsers. In truth, if the dragging operation were being implemented in just one object model, the scripts and approach to the dragging control would be tailored to take advantage of the event and object features of that model. But the goal here is a completely cross-browser implementation that supports any browser capable of positioned elements. To that end, the code relies on a handful of routines from the DHTML API described in Chapter 4. Although Example 6-2 links in the entire library, you should also consider creating a separate .js library with a subset of the entire API needed to support the dragging operations, and thus minimize the amount of code that travels to the client.

The dragging system implemented in this example has a simple design behind it. Three mouse events—onmousedown, onmousemove, and onmouseup—control the action. Event handlers for all three events are assigned to the document node. Navigator 4 grabs the events before they reach their targets, while the IE and W3C event models wait for the events to bubble up from their targets.

Each event type has a specific role to play. The onmousedown event handler (the engage( ) function) validates that the event occurred on a draggable item and sets three global variables that the other event handlers will use. One variable, selectedObj, is a reference to the element that will be dragged. The other two variables, offsetX and offsetY, preserve the onmousedown event coordinates within the draggable element so that as the user moves the cursor to drag the element, the cursor maintains its spatial relationship within the element. The onmousemove event handler (dragIt( )) keeps the element in position with the moving cursor until the onmouseup event fires. At that point, the release( ) function removes the reference from selectedObj. Any time that variable is null, incidental onmousemove events that occur atop the elements don't affect their positions.

Assigning the event handlers to the document node offers two significant advantages. First, a mere three event handler assignments take care of as many draggable items as you want to place on the page. Second, while the user drags the element, element rendering may not refresh as quickly as the cursor moves, preventing the onmousemove or onmouseup events from firing on the draggable elements. But when the engage( ) function "switches on" dragging for a particular element, all onmousemove and onmouseup events for other elements bubble up to the document level (unless they are explicitly canceled), and the dragIt( ) and release( ) functions do their jobs because the dragging mode for the selected object is still switched on. As long as the user keeps the mouse button down, the draggable element will catch up to the cursor position.

All the dragging event handlers are assigned as properties in an init( ) function invoked by the onload event handler. The only platform-specific process taking place here involves setting the document.captureEvents( ) method to grab all mouse down and mouse move events that come in from Navigator 4.

The draggable elements in this example are two absolute-positioned div elements that contain img elements. The user can click on either image and drag that image (and its div) around the page. Wrapping the image with a div element helps with a couple of fine points about this example. For one, absolute-positioned div and span elements are better behaved in Navigator 4 than positioning other kinds of elements (layer elements are the best behaved, but they are unique to Navigator 4). But more important is demonstrating one way to assure that event targets inside a positioned element (the img elements are the targets here) translate into a reference to the container, because the container is the element that gets positioned (while its contents go along for the ride).

Notice in Example 6-2 how the IDs of the image event targets and their div s are related (imgA and imgAWrap). The filtering in the setSelectedElem( ) function uses the fact that the img element targets have name and src properties assigned to them. If that's the case for a particular onmousedown event, then the ID for the div wrapper get assembled from the target's name and the "Wrap" add-on. This approach certainly works, but its looseness is necessitated by the support required for Navigator 4's immature DOM. If this were being written for IE 5 (or later) and Netscape 6 (or later), a much more generalizable solution is possible and preferable. For example, you could assign an identifier to the class attribute of the img elements (e.g., draggable), and let setSelectedElem( ) simply look for the event target's className property that equals that identifier. If there's a match, you could immediately set the global selectedObj variable to the event target's parentNode property:

selectedObj = (target.className == "draggable") ? target.parentNode : null;

No other element referencing or naming games would be necessary.

Notice, too, that the setSelectedElem( ) function sets the stacking order of the element to an arbitrarily high number. You want a selected element to be atop all its peers as the user drags it around the screen.

Coordinate systems play a significant role in scripting the drag process. Ideally, the element should track from the point where the user clicks inside the element. This means that the location (top left corner) of the element must be offset (up and to the left) from the cursor position by the number of pixels of the click offset within that element. This information isn't as easy to come by as you might think. Not all event models report the offsets within a positioned container; those that do need further adjustments for document scrolling or inherent bugs. The tripartite branch in the engage( ) function takes care of the measurements for three event model implementations:

Offset values are stored as global variables in Example 6-2, so that the dragging action can use them for proper placement of the element under the cursor.

Making the element track the cursor in the dragIt( ) function also requires some calculation. Using the shiftTo( ) function from the DHTML API, the script sets the location of the element within the page (or client) space after each mouse movement.

Despite the amount of object detection branching taking place in Example 6-2, if you trimmed the scripts to work only in IE 5 (or later) and Netscape 6 (or later), and employed the className and parentNode tips described earlier, you could easily make the dragging functionality into a reusable library, nearly independent of the specific content on the page. The only care you'd have to exercise is assigning your chosen class identifier to the draggable image or area of the positioned element, such as a pseudo-titlebar of a "floating" palette. This example also works as-is only with positioned elements that use the body or document element (depending on your standards-compatibility mode) as the positioning context. Modifications would be needed to nest the positioned elements in other contexts.

Example 6-2. Dragging elements around the window

<html> 
<head> 
<title>It's a Drag</title> 
<style type="text/css">
  body {font-family:Ariel, sans-serif; text-align:right}
  #imgAWrap {position:absolute; left:50px; top:100px; width:120px; height:90px; 
             border:solid black 1px; z-index:0}
  #imgBWrap {position:absolute; left:110px; top:145px; width:120px; height:90px; 
             border:solid black 1px; z-index:0}
</style>
<script language="JavaScript" type="text/javascript" src="DHTML2api.js"></script>
<script language="JavaScript" type="text/javascript">
// Global holds reference to selected element
var selectedObj;
// Globals hold location of click relative to element
var offsetX, offsetY;
  
// Set global reference to element being engaged and dragged
function setSelectedElem(evt) {
    var target = (evt.target) ? evt.target : evt.srcElement;
    var divID = (target.name && target.src) ? target.name + "Wrap" : "";
    if (divID) {
        if (document.layers) {
            selectedObj = document.layers[divID];
        } else if (document.all) {
            selectedObj = document.all(divID);
        } else if (document.getElementById) {
            selectedObj = document.getElementById(divID);
        }
        setZIndex(selectedObj, 100);
        return;
    }
    selectedObj = null;
    return;
}
  
// Drag an element
function dragIt(evt) {
    evt = (evt) ? evt : event;
    if (selectedObj) {
        if (evt.pageX) {
            shiftTo(selectedObj, (evt.pageX - offsetX), (evt.pageY - offsetY));
        } else if (evt.clientX || evt.clientY) {
            shiftTo(selectedObj, (evt.clientX - offsetX), (evt.clientY - offsetY));
        }
        evt.cancelBubble = true;
        return false;
    }
}
// Turn selected element on
function engage(evt) {
    evt = (evt) ? evt : event;
    setSelectedElem(evt);
    if (selectedObj) {
        if (evt.pageX) {
            offsetX = evt.pageX - ((selectedObj.offsetLeft) ? 
                      selectedObj.offsetLeft : selectedObj.left);
            offsetY = evt.pageY - ((selectedObj.offsetTop) ? 
                      selectedObj.offsetTop : selectedObj.top);
        } else if (evt.offsetX || evt.offsetY) {
            offsetX = evt.offsetX - ((evt.offsetX < -2) ? 
                      0 : document.body.scrollLeft);
            offsetY = evt.offsetY - ((evt.offsetY < -2) ? 
                      0 : document.body.scrollTop);
        } else if (evt.clientX) {
            offsetX = evt.clientX - ((selectedObj.offsetLeft) ? 
                      selectedObj.offsetLeft : 0);
            offsetY = evt.clientY - ((selectedObj.offsetTop) ? 
                      selectedObj.offsetTop : 0);
        }
        return false;
    }
}
// Turn selected element off
function release(evt) {
    if (selectedObj) {
        setZIndex(selectedObj, 0);
        selectedObj = null;
    }
}
  
// Set event capture for Navigator 4
function setNSEventCapture( ) {
    document.captureEvents(Event.MOUSEDOWN | Event.MOUSEMOVE | Event.MOUSEUP);
}
// Assign event handlers used by both Navigator and IE
function init( ) {
    if (document.layers) {
        setNSEventCapture( );
    }
    document.onmousedown = engage;
    document.onmousemove = dragIt;
    document.onmouseup = release;
}
</script> 
</head> 
<body onload="initDHTMLAPI( ); init( );"> 
<h1>Element Dragging</h1> 
<hr>
<div id="imgAWrap" class="draggable"><img id="imgA" name="imgA" 
src="myImage1.jpg" width="120" height="90" border="0"></div>
<div id="imgBWrap" class="draggable"><img id="imgB" name="imgB" 
src="myImage33.jpg" width="120" height="90" border="0"></div>
</body> 
</html>


Library Navigation Links

Copyright © 2003 O'Reilly & Associates. All rights reserved.