Today's Episode:
What A Drag
The web enabling project which is currently paying my family's mortgage, grocery bills, and Irish Step Dancing habit has in the past been the source of many forays into the world of Strange Code. Nothing I have done previously, though, quite prepared me for the problem which got dumped in my lap just the other week. Seems the previous version of the application had a fancy screen where the luser could design their own SQL query and fire it off to the back-end database. Piece of cake, I remember thinking to myself as I coded the initial work-up, A little bit of HTML <Form> action, some perl to manage the SQL inbound and outbound... Nooo problem... Then I took a closer look at how the previous interface worked. The lusers could drag and drop columns from the table which they were querying into form fields that specified the output columns, what criteria on which to limit data, and what columns on which to sort.
Okay, now it's a problem.
Many days of reading DHTML reference websites (and even more iterations of the edit-compile-link-swear cycle of program development popular with most programmers my age), victory was mine. If you're impatient and want to go play now, a demo page implementing my solution can be found here. If you aren't, go grab something to drink and I'll walk you through what I did when you get back. I'll wait.
You Want WHAT?
Our first order of business is picking exactly what to drag. Since I was developing on a Mozilla browser and a Linux workstation for lusers using Internet Explorer on Windows, whatever choice I made had to support the onmousedown handler across most major browsers. It also had to support the hover CSS pseudostyle, so the luser would have some sort of feedback. This basically left me with only the <A> element with which to work. So what can we do to work with these constraints?
Actually, we can do quite a lot, and all without the use of Javascript. The following stylesheet rules and HTML markup provide my starting point for drag and drop:
<style>
#list { background: #ffffff;
width: 10em;
border: inset white 2px; }
#list a { color: #000000;
cursor: default;
display: block;
text-decoration: none; }
#list a:hover { background: #000000;
color: #ffffff; }
</style>
...
<div id="list">
<a onmousedown="grabIt(event, this)">field1</a>
<a onmousedown="grabIt(event, this)">field2</a>
<a onmousedown="grabIt(event, this)">field3</a>
<a onmousedown="grabIt(event, this)">field4</a>
<a onmousedown="grabIt(event, this)">field5</a>
<a onmousedown="grabIt(event, this)">field6</a>
<a onmousedown="grabIt(event, this)">field7</a>
</div>
The overall effect is sort of like a <select> element in a form -- albeit one with a lot more control over event capturing across all browsers.
Picking Up Where We Left Off
Each link in the above <div> element traps for the onmousedown event. Here is the code that runs for that event:
function grabIt(e, field) {
var x = field.offsetLeft;
var y = field.offsetTop;
// -- initialize drag buffer and store difference between its edges and the mouse
drag = document.getElementById("buffer");
drag.dx = IE? window.event.offsetX: (e.layerX - x);
drag.dy = IE? window.event.offsetY: (e.layerY - y);
// -- deactivate cloaking device
with (drag) {
style.top = y;
style.left = x;
style.visibility = "visible";
innerHTML = field.innerHTML;
}
// -- Capture mousemove and mouseup events on the page.
document.onmousemove = dragIt;
document.onmouseup = dropIt;
if (IE) {
window.event.cancelBubble = true;
window.event.returnValue = false;
} else {
e.preventDefault();
}
}
We start by caching the position of the link that captured the onmousedown event, because we're going to be needing it several times later. The next thing I do is initialize what I'm going to be calling the drag buffer. This is an initially invisible <div> element I have placed at the top of my form that I use to carry the chosen value from source to target when the luser starts their drag. We add two extra properties which store the current difference between the mouse position and the edges of the selected link so the buffer doesn't take any awkward jumps when the mouse moves. The cursorX() and cursorY() functions are simply custom functions which return the current x and y coordinates respectively of the mouse in a cross-browser fashion.
Next, we move the drag buffer directly over the chosen link, make it visible, and set its contents equal to those of the link. To the luser, it appears like they are now controlling an off-color version of the chosen link. We then set the document object to track all onmousemove and onmouseup events.
The actions at the end of the function are of particular note, because default browser behavior when a luser starts a drag operation is to highlight the text over which the mouse is dragging. Since we want to see our drag buffer move across the screen instead, we have to block all other event handling -- and propagation. The code above does what we want in a cross-browser environment, but has some bad side effects which we will discuss later.
Taking It With You
The onmousemove handler and friends ends up looking something like this:
//-----------------------------------------------------------------
// browser-independent routines for determining event position
//-----------------------------------------------------------------
function getX(e) { return IE? window.event.clientX: e.pageX; }
function getY(e) { return IE? window.event.clientY: e.pageY; }
...
//-----------------------------------------------------------------
// onmousemove handler
//-----------------------------------------------------------------
function dragIt(e) {
// -- Move drag element by the same amount the cursor has moved.
with (drag) {
style.left = getX(e) - dx;
style.top = getY(e) - dy;
}
// -- continue to block all other events until we're through
return false;
}
All we need to do while the mouse is moving and the drag buffer is active is to redraw the drag buffer in the new position. The getX() and getY() functions are simply custom functions which return the current x and y coordinates respectively of the mouse in a cross-browser fashion.
The Object of Our Affections
Now comes the fun part. We have to drop the contents of the drag buffer into the target field, which means we now have to worry about where the onmouseup event happens on our page. My usual preference for event handling is to let the event propagate through the document and set the appropriate handler for any element that wants to take a whack at it as it goes through (see my Strange Code article on tabs for an example of this philosophy). Now, remember what I mentioned previously about grabIt() having to block events? Here comes the consequence: we will be unable to determine the target <textarea> by event propagation at the end of the drag. So much for plan A...
The solution to this dilemma was somewhat devious. First, you will observe in the beginning of the example page source the following Javascript functions:
var field = new Array();
function pageInit() {
field[0] = document.getElementById("columns");
field[1] = document.getElementById("criteria");
field[2] = document.getElementById("sort");
}
The field array is a quick method of caching references to our target <textarea>s. Since we will be referring to each <textarea> several times while the page is displayed, we don't want to spend a whole lot of time or memory finding the target element each and every time we want to test it. To load the field array, pageInit() gets called by the onload handler of the <body> element.
Just Drop It
Now that we can discover what our drop target is, we can define the onmouseup handler:
function dropIt(e) {
// -- get position of this event
var X = getX(e);
var Y = getY(e);
// -- loop over possible targets and see if event occurred in each one
for(i = 0; i < field.length; i++) {
with (field[i]) {
var top = IE? (10 + offsetParent.offsetTop): offsetTop;
var bottom = top + offsetHeight;
var left = IE? (10 + offsetParent.offsetLeft): offsetLeft;
var right = left + offsetWidth;
}
if (X > left && X < right && Y > top && Y < bottom) {
field[i].value += drag.innerHTML + ", ";
break;
}
}
// -- engage cloaking device
drag.style.visibility = "hidden";
// -- Stop capturing mousemove & mouseup events.
document.onmousemove = null;
document.onmouseup = null;
}
We first determine the X and Y coordinates of the onmouseup event. Then, we loop over the field array, testing each element in turn to see if the event happened within its <textarea> of responsibility. If it has, the contents of the drag buffer are written to the value property of its referenced <textarea>. Finally, we hide the drag buffer and release control of event trapping back to the browser, completing the operation.
And there you have it. The entire application can be found here. To see how it all integrates together, I encourage you (as always) to use the View Source command on your browser.
BUGS FEATURES
The <div id="top"> element surrounding the drag area is really, really insistent on being at the top of the page. I tried incorporating the demo form at the bottom of this article, only to find that the controlling Javascripts literally ran away with the drag buffer. Hence, the reason I had to move the example code and markup to a separate page.
On IE5 for the Macintosh, the styles for the links that start the drag do not work as designed. You have to add a null document as a link destination (href="") for the styles to activate.
I generate position values for each <textarea> each time I hit dropIt(). A more memory- and speed-efficient version of this code would break those lines of code into their own function, which would then be invoked via the onload and onresize handlers of the <body> element. That way, we could cache the position values without worrying about the luser resizing his browser window and changing the dimensions and locations of the <textarea>s.
UPDATE!
This version of the DHTML drag&drop application has been overcome by events. Click here to see Son of Drag&Drop -- in Living Technicolor™