with Ges Seger

STRAAANGE COOODE

Today's Episode:

Son Of Drag&Drop

This was it. This was the day I finally started testing on the Drag&drop SQL query generator that had been responsible for not one but two Strange Code episodes earlier in the year. The server modules were written and debugged. The page was in place. It was time.

I loaded the page -- OK, the server module really works instead of just being syntactically correct. Then I pressed the mouse on a field to drag it over to the query form.

You guessed it -- nothing happened. Stop reading ahead, will you?

Seems that in the months between my original posting of the drag&drop episode and my first tests, Base Computer Support had upgraded everyone's copy of Internet Explorer from version 5.5 to version 6. This of course broke my carefully-crafted example to pieces. Grumbling terrible things about Microsoft and the 88th Comm Group under my breath, I set forth to upgrade my drag&drop application.

WALKTHROUGH

This is the second episode in a row where I've had to go somewhat out-of-format for the code walkthrough. This was because the original episode from which I derived this sequel was perhaps the largest example I have posted to date and a lot happens in all its functions and event handlers. Rather than go through the entire code from scratch again, I'll touch upon the things that have changed between the original episode and the sequel.

"But I Thought You Didn't Like Tables!"

I never said I didn't like tables. I said "Nested tables are usually a sign from God that you need to rethink your HTML." There's a difference. Bonus points if you can name the episode in which I first used this aphorism.

What prompted this question is that in the sequel I have everything arranged in a 2x2 <TABLE>. I didn't do this because I could, but because I needed to. Our developers at work use browsers running the Gecko rendering engine to check their work, while our luserbase runs IE version 6. Folks, if you need to do cross-browser HTML positioning, I've found there's nothing better for that task than using a <TABLE>. If you doubt me, read pages 473-474 of Goodman's Dynamic HTML (2nd Edition) and ponder what it would take to implement all that exception handling in a Javascript-based DHTML API.

"What's With The Changes In getX() and getY()?"

Old Version

      function getX(e) { return IE? window.event.clientX: e.pageX; }
      function getY(e) { return IE? window.event.clientY: e.pageY; }
    

New Version

      function getX(e) {
      	var x = 0;
        if (IE) {
          x = e.clientX;
          if (!Mac) {
            x += document.documentElement.scrollTop;
            x += document.body.scrollTop;
		  }
        } else {
          x = e.pageX + window.scrollX;
        }
        return x
      }
      
      function getY(e) {
        var y = 0;
        if (IE) {
          y = e.clientY;
          if (!Mac) {
            y += document.documentElement.scrollTop;
            y += document.body.scrollTop;
		  }
        } else {
          y = e.pageY + window.scrollY;
        }
        return y;
      }
    

While I was mucking around with the code, I finally had an excuse to solve that long-standing problem with document scrolling described in the original episode's BUGS FEATURES section. I also made it more platform-robust while I was at it.

"Hey, You Moved grabIt()!"

The original code implemented grabIt() in an onmousedown handler within each link of the pick list. Rather than do that, this version of the code has been moved to a onmousedown handler in the enclosing <DIV> tag above all the links. By adding a few lines at the top of grabIt() to determine the link on which the luser originally clicked, we now have a more easily-maintained application.

"What's With The Other Changes In grabIt()?"

Old Version

      function grabIt(e, field) {
        var x = field.offsetParent.offsetLeft;
        var y = field.offsetTop + field.offsetParent.offsetTop;

        // -- initialize drag object 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;

	    // -- block all other events
        if (IE) {
          window.event.cancelBubble = true;
          window.event.returnValue  = false;
        } else {
          e.preventDefault();
        }
        return false;
      }
    

New Version

      function grabIt(e) {

        // -- event and target element references are browser-specific.  UGH
        var e      = e? e: window.event;
        var field  = IE? e.srcElement: e.target;   
        var target = getRect(field);	// target element position offsets

        // -- initialize drag object and store difference between its edges and the mouse
        drag    = document.getElementById("buffer");
        drag.dx = getX(e) - target.left;
        drag.dy = getY(e) - target.top;

        // -- deactivate cloaking device
        with (drag) {
          style.top        = target.top;
	      style.left       = target.left;
          style.width      = target.right  - target.left;
          style.visibility = "visible";
          innerHTML        = field.innerHTML;
        }

        // -- Capture mousemove and mouseup events on the page.
        document.onmousemove = dragIt;
        document.onmouseup   = dropIt;

	// -- block all other events
        if (IE) {
          e.cancelBubble = true;
          e.returnValue  = false;
        } else {
          e.preventDefault();
        }
        return false;
      }
    

The most glaring change is in the removal of all my code to determine the position of the clicked list choice. It involves a lot of object checking and math, and is basically the same algorithm that I use twice more in dropIt(). So, I split that out into its own function called getRect(), which will be described in detail later.

I also calculate the width property of the drag buffer at this time. This is partly for aesthetic purposes, and partly because I can.

"What's With The Changes In dropIt()?"

Old Version

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

New Version

      function dropIt() {

        // -- engage cloaking device
        drag.style.visibility = "hidden";

        // -- calculate drag buffer's current bounding rectangle
        var rd = getRect(drag);

        // -- loop over all form elements
        for(i = 0; i < document.query.elements.length; i++) {

          // -- if we aren't a drag target, go to the next element
          var field = document.query.elements[i];
          if (! field.className.match(/dragTarget/)) continue;

          // -- is drag buffer over this target?
          var rt = getRect(field);
          var boundHorz = (rd.left > rt.left) && (rd.right  < rt.right);
          var boundVert = (rd.top  > rt.top)  && (rd.bottom < rt.bottom);
          if (boundHorz && boundVert) {
            field.value += drag.innerHTML + ", ";
            break;
          }
        }
        
        // -- IE5/Mac requires this so the drag element doesn't take up full screen width
        drag.innerText = '';

        // -- Stop capturing mousemove & mouseup events. 
        document.onmousemove = null;
        document.onmouseup   = null;
      }
    

There are three major changes in this code:

  1. I changed the location of the command that makes the drag buffer invisible from the end of the function to the beginning. The drag buffer now cloaks a fraction of a second before its value gets written to the target.
  2. I used a variation of code I developed for my forms validation episode to determine whether the form element was a legitimate drag target. I no longer need to run initialization code on initial page load for purposes of this particular application.
  3. Calculation of the bounding rectangles for both the target fields and the drag buffer was removed in favor of the getRect() function I am about to describe.
  4. Zeroing out of the drag buffer once we're through. This prevents a rendering bug in IE5/Mac from occurring.

Also, my discussion of why I couldn't use event bubbling and trapping in the original episode neglected one important consideration dictated by the internal structure of the document. I assumed back then that event bubbling went through levels based on their display position within the document. This was incorrect, for event bubbling goes through levels based on their position within the DOM node tree instead. The difference between display space and node space is a very subtle one, and in the case of using event handling for this application a very crucial one. If you View Source on either the original episode's example or this example, you will realize that in each case the drag buffer and its targets reside in totally different branches of the DOM node tree. The drag buffer may be over the target <TEXTAREA> on the luser's display when they release the mouse button, but as far as the node tree is concerned, it's never changed its location.

"What's With This getRect() Function?"

getRect was originally developed for this Strange Code episode, but ended up being just as useful for a previous episode on Combo Boxen. So, I premiered it there. You'll find a full explanation and development in that episode.

USAGE

Pretty much the same as described in the original episode, with the following exceptions:

As always, View Source is your friend. Use him wisely.

BUGS FEATURES

Only the usual second-order oddities in IE5/Mac's element position model compared to its Windows brethren. They don't really matter in this app, so I indulged in some personal laziness.

On a related note, I have no idea why IE5/Mac insisted on drawing the drag buffer as taking up the full screen width if there was any content in it upon decloaking -- and subsequently ignoring every programmatic effort to enforce a more sane width. I was significantly contributing to my receding hairline before I figured this out.

Galeon did not like it if I started my drags with the pointer actually on the link text. I had to click just to the right to get proper functioning.

getX(), getY(), getRect(), and the browser-sensing code at the top of the <SCRIPT> block in the header would be a good starting basis for an API library. They probably should be broken out into a separate file rather than be included within this page. Alternatively, if you've rolled your own DHTML API and have equivalent routines, you should see if they work as well as mine -- and use them if they do.

EXAMPLE

Available Columns

field1
field2
field3
field4
field5
field6
field7
Select... Distinct
Where...
Order By...