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