[ Team LiB ] Previous Section Next Section

13.11 Creating a Draggable Element

NN 4, IE 4

13.11.1 Problem

You want a user to be able to click on and drag an element from one location on the page to another.

13.11.2 Solution

Use both the DHTML API from Recipe 13.3 and the dragImg.js library (Example 13-4 in the Discussion) to set up images to drag around the page. The dragImg.js library is wired to expect an img element surrounded by a div element whose class attribute is set to draggable. Moreover, the IDs of related img and div elements must be defined such that the identifier for the div element is the same as the identifier of the img element plus the letters Wrap (the div elements act as wrappers for their img elements). For example, here is the HTML for a pair of images that are set to be draggable:

<div id="imgAWrap" class="draggable"><img id="imgA" name="imgA" src="widget1.jpg" 
width="120" height="90" border="0" alt="Primary draggable widget"></div>
<div id="imgBWrap" class="draggable"><img id="imgB" name="imgB" src="widget2.jpg" 
width="120" height="90" border="0" alt="Secondary draggable widget"></div>

The library actually repositions the div elements, but the mouse events that initiate and control dragging go to the img elements inside the div elements.

Set up the div images to be positioned elements by way of style sheet definitions. For example, the style sheets for the two div elements shown here are as follows:

<style type="text/css">
  #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>

All other initializations and event handler assignments are performed by the two event handlers assigned to the body's onload event handler, one for the DHTML API library and one for the dragImg.js library:

<body onload="initDHTMLAPI( ); initDrag( );">

13.11.3 Discussion

Example 13-4 shows the dragImg.js library code. It should be linked into your page following the DHTML API library, as follows:

<script language="JavaScript" type="text/javascript" src="DHTMLapi.js"></script>
<script language="JavaScript" type="text/javascript" src="dragImg.js"></script>

The library accommodates as many draggable images as you like on the page, without conflicting with static images.

Example 13-4. The dragImg.js library for dragging images on the page
// 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" : "" 
        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;
}
   
// Turn selected element on
function engage(evt) {
    evt = (evt) ? evt : event;
    setSelectedElem(evt);
    if (selectedObj) {
        if (document.body && document.body.setCapture) {
            // engage event capture in IE/Win
            document.body.setCapture();
        }
        if (evt.pageX) {
            offsetX = evt.pageX - ((selectedObj.offsetLeft) ? 
                      selectedObj.offsetLeft : selectedObj.left);
            offsetY = evt.pageY - ((selectedObj.offsetTop) ? 
                      selectedObj.offsetTop : selectedObj.top);
        } else if (typeof evt.offsetX != "undefined") {
            offsetX = evt.offsetX - ((evt.offsetX < -2) ? 
                      0 : document.body.scrollLeft);
            offsetX -= (document.body.parentElement && 
                     document.body.parentElement.scrollLeft) ? 
                     document.body.parentElement.scrollLeft : 0
            offsetY = evt.offsetY - ((evt.offsetY < -2) ? 
                      0 : document.body.scrollTop);
            offsetY -= (document.body.parentElement && 
                     document.body.parentElement.scrollTop) ? 
                     document.body.parentElement.scrollTop : 0
        } else if (typeof evt.clientX != "undefined") {
            offsetX = evt.clientX - ((selectedObj.offsetLeft) ? 
                      selectedObj.offsetLeft : 0);
            offsetY = evt.clientY - ((selectedObj.offsetTop) ? 
                      selectedObj.offsetTop : 0);
        }
        return false;
    }
}
   
// 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 off
function release(evt) {
    if (selectedObj) {
        setZIndex(selectedObj, 0);
        if (document.body && document.body.releaseCapture) {
            // stop event capture in IE/Win
            document.body.releaseCapture();
        }
        selectedObj = null;
    }
}
   
// Assign event handlers used by both Navigator and IE
function initDrag( ) {
    if (document.layers) {
        // turn on event capture for these events in NN4 event model
        document.captureEvents(Event.MOUSEDOWN | Event.MOUSEMOVE | Event.MOUSEUP);
        return;
    } else if (document.body & document.body.addEventListener) {
        // turn on event capture for these events in W3C DOM event model
        document.addEventListener("mousedown", engage, true);
        document.addEventListener("mousemove", dragIt, true);
        document.addEventListener("mouseup", release, true);
        return;
    }
    document.onmousedown = engage;
    document.onmousemove = dragIt;
    document.onmouseup = release;
    return;
}

The library begins by declaring a few globals that convey information between initial activation of the drag and the actual drag operation. One, selectedObj, maintains a reference of the element being dragged. The offsetX and offsetY pair get set in the engage( ) function, and are used constantly during the positioning tasks while dragging.

Invoked by the engage( ) function, setSelectedElem( ) examines details of every mousedown event on the page to determine whether the clicked item is an img element (because it has an src property that is backward-compatible). If so, a browser-specific reference to the draggable parent element is preserved in the selectedObj global. If you are designing strictly for W3C DOM compatibility, a better way to identify valid target elements is to assign them a class name in the HTML and look for a match of the className property (e.g., look for target.className == "draggable").

The setSelectedElement( ) function is invoked in the engage( ) function (the first destination of mousedown event processing). This is followed by switching on mouse event capture for Windows versions of IE (to improve event-processing performance). Next comes calculating and preserving the offset distance between the mousedown event and the top-left corner of the positioned element. This action keeps the element at the same position (relative to the cursor) during the drag action. The number of branches is necessary to accommodate a wide range of browsers, including IE for the Macintosh (which doesn't always operate like IE for Windows), Navigator 4, and the CSS-compatibility mode of IE 6 (the scroll-related properties of document.body.parentElement).

The function that actually moves the element along with the cursor (in response to the mousemove event) is the dragIt( ) function. It calculates the absolute position of the element, adjusted by the preserved cursor offset. Then it uses the DHTML API shiftTo( ) function to set the element's momentary position. When the user releases the mouse button, the release( ) function restores the stacking order of the dragged element to its original value and the selectedObj global variable is nulled out.

Initialization in the initDrag() function assigns a variety of event handlers depending on the browser's event model support. Because this library is compatible back to Netscape 4's unique event model, one branch turns on that browser's event capture mode. W3C DOM event model browsers (such as the Mozilla family) assign event handlers by way of the addEventListener() method, setting the third parameter to true to turn on event capture mode. All other browsers, including IE, have three mouse event handlers at the document level.

It may seem odd at first to define the mouse event handlers at the document level, rather than right in the draggable div elements themselves. The reason for this is that users can drag the elements faster than the browser can update the position of the element. When the cursor escapes the rectangle of the dragged element, the mousemove event no longer fires on the div or img element. Neither does the mouseup event, when it occurs outside the element. Thus, the dragged element still thinks it's the selected element, but cannot respond to the mouse motion until the cursor comes back into the element's region. Placing the events at the document level ensures that the mouse events reach their event handler functions, even if the events occur outside of the positioned element rectangles. Most browsers running this library will be using their own version of event capture to begin processing the events before any event propagation occurs. But other browsers, such as IE for the Macintosh, take advantage of event bubbling so that events initially firing in the positioned element bubble up to the document level for processing.

If you want to limit the region within which an element can be dragged, you can define a rectangular boundary and keep the element within that zone. To accomplish this, first define a global object with coordinate points of the space:

var zone = {left:20, top:20, right:400, bottom:400};

Then modify the dragIt( ) function in Example 13-4 so that it won't allow dragging outside of the zone:

function dragIt(evt) {
    evt = (evt) ? evt : event;
    var x, y, width, height;
    if (selectedObj) {
        if (evt.pageX) {
            x = evt.pageX - offsetX;
            y = evt.pageY - offsetY;
        } else if (evt.clientX || evt.clientY) {
            x = evt.clientX - offsetX;
            y = evt.clientY - offsetY;
        }
        width = getObjectWidth(selectedObj);
        height = getObjectHeight(selectedObj);
        x = (x < zone.left) ? zone.left : 
           ((x + width > zone.right) ? zone.right - width : x);
        y = (y < zone.top) ? zone.top : 
           ((y + height > zone.bottom) ? zone.bottom - height : y);
        shiftTo(selectedObj, x, y);
        evt.cancelBubble = true;
        return false;
    }
}

The modifications take advantage of the DHTML API's functions that easily obtain the width and height of the positioned element. Then the values of the intended coordinates are tested against the zone's points. If the intended position is outside the box, the coordinate value is set to the maximum value along the edge. This allows an element to reach an edge in one axis and still be draggable up and down along the edge.

13.11.4 See Also

Recipe 13.3 for the required DHTML API library; Recipe 11.13 for IE 6 CSS-compatibility mode issues.

    [ Team LiB ] Previous Section Next Section