Determining the Droppable
After covering the basics of a drag and drop, it's time to look at detecting things we're dragging over. When it comes to a drag and drop, you normally want to have something to drop onto. Otherwise, just dragging an item around the screen can get boring. You want to interact with the page somehow. You might want to drag items into a shopping cart, or sort a list of items, or dock some widget on the screen.
To be able to interact with the page, you have to be able to determine where you are on the page and what element is under the cursor at any given time (either during the drag or after the drag is complete). There are two approaches to this:
- event delegation, and
- determining the element offset
One is extremely easy and performs fast, the other is much the opposite.
Event Delegation
Event Delegation can be a very handy way to handle events within a particular section of the page. You essentially attach an event handler farther up in the DOM tree than the elements you happen to want to track. Event bubbling will bubble the event, firing it for every element all the way up to the document
. In this way, you avoid having to attach an event handler for every single element you want to track.
For example, with the following DOM structure: document
→ body
→ div
→ ul
→ li
. If you had 100 list items within the list, you could attach a single event handler at the UL
level. Then using the target
property of the event object (srcElement
in Internet Explorer), you can retrieve the element furthest down the tree that fired the event.
Using the same structure, if we had multiple UL
's within that div
and wanted to figure out which list fired the event, we'd attach the event handler at the DIV
level. Then, because the target is the element furthest in the tree (the LI
), we'd just have to grab the parentNode
of the target.
Referring back to our anatomy, we have our mousemove event firing constantly while we drag. Want to know what element we're over? Just grab the target.
But here's the catch — and it's a big one.
If you're absolutely positioning an element (aka, the draggable) underneath the mouse cursor, as almost all drag and drop implementations do, then it is that element that will be returned as the target. Suddenly, event delegation has become completely useless. For that, we look to determining an element's offset, instead.
Determining the Element Offset
Each element in the DOM has an offset. First there's the offsetTop and offsetLeft of the element which tells you how far from the top and from the left the element is from its positioned parent. You'll normally have to loop from the element up to the body to total up the offsets of all positioned element up the tree. With that in hand, you'll know the X and Y value of the top-left corner.
You still don't know if the mouse is over the element. For that, you have to determine the position of the bottom-right corner. The element has an offsetWidth and an offsetHeight which tells us how wide and how high the element is. We add this to the X and Y value of the top-left corner. Then, if we take the X and Y values of the current mouse position, we can check if it's between the two points. If it is, we've now found our element.
Checking the offsets for every single element in the DOM just to determine when element we're over would be a monumental task. Which is why Script.aculo.us, mootools, and YUI (and undoubtedly any library) creates a cache of "droppables". You declare which elements will be the drop zones and then any time the mouse moves, the offsets are only calculated on those elements.
- YUI allows you to create droppable groups so that only certain draggables can be dropped upon them.
- Script.aculo.us has a draggable observer you can subscribe to. Every time a drag event fires, any of the observers will be notified. You could use this to create your own tracking and/or hit detection script.
Of course, the more elements you wish to drop on, the more intensive this process will be and things will inevitably begin to slow down again.
Optimizing Performance
Dragging the mouse around the screen fires off events on a continual basis. As a result, it's important to minimize or eliminate repetitive tasks.
Caching
One way to minimize lookups is to cache those values after you've looked them up. If nothing on the page will be moving (besides the draggable), store the element offsets to save having to look them up again. Once you have the offsets cached, you could even optimize cache lookups by any number of techniques.
Caching could be performed on the start of the drag or continually during the drag process. Which you decide to do will depend on how much information would need to get cached at any given time. Too many elements on start and the application will seem sluggish to start the drag but too many element lookups on drag and you'll begin to see a lag.
Create Fewer Droppables
Another trick is to artificially reduce the number of droppables. What you're doing is creating larger areas upon which to track that a mouse event has occurred within. From there, calculating the offsets of the elements within the droppable will allow you to narrow down your hit area even more. It's basically a tunneling approach but by dividing the workload up into more manageable chunks.
Prevent Redraws
One of the other factors that can create a hit on performance is screen redraws. If you're redrawing a large portion of the screen on a frequent basis due to whatever feedback mechanism you're providing (like changing a border to indicate an insertion point), it can bog the browser down. Even event delegation can suffer from this problem.
Final Words
All of the libraries are geared towards offset lookups and this approach is the most reliable approach across 80% of the implementations out there. It'll be the other 20% that require some creative solutions like choosing event delegation or thinking about how to minimize the repetitive calls.
Conversation
Why don't we just check where the the draggable was drop on endDrag event not on drag event ?
Radoslav: Yes, you could just leave the checking until the end. And that'll be fine if you don't need to provide any feedback while dragging. But you almost always want to provide some interaction to give the user some indication that they've entered a drop zone, via some highlight.
It is interesting to note, for those lucky enough to code for Firefox exclusively, that doing a quick:
Somewhere in your drag code will be sufficient to allow an onmousemove event to fire.
Sadly trying to do the same for internet explorer will either not work or result in terrible flicker. Not sure what other browsers will do with such silly code.
oops, Wanted to say that it would allow an onmouseover event to fire as well.
Collin:
The removeChild/appendChild combo could potentially disorganize your DOM tree. If you really need your events to fire, just call your event function directly rather than messing up your DOM (cache the elements that it needs to be fired on if necessary).
Andy,
Yes, I suppose I was approaching the problem with too powerful a tool.
One could simply set the display style to none and then back to whatever it was.
The point I was trying to get across is that firefox has a work-around for the obstacles of event delegation in a drag/drop situation.
In the minute instance your dragged element is hidden the onmouseover/onmousemove events of the elements underneath the dragged element will fire and the dragged element will not flicker.
A handy trick if you're in the right situation and don't want to muck around with determining offsets.
Though I do like some of the advantages of offsets. Such as determining what percentage of the draggable and the droppable are intersecting.
Ok, here is tip. Suppose we have table elements ( rows ) as draggables/droppables, and cells are data containers. In this case we could just perform drag action of any row from source table an dragOver over destination table whitch will satisfied condition isDraggedOver for example when row is over dropping zone (the other, destination, table) and when we perform below code, on mouseup event:
the draggedRow will change it's parent to destination table, and wil be disappeared from source table because it can not reside as single instance of it self in two diferent parents.
If you whant copy of that Object you have to do it like this:
This piece of code works on both, IE and FF. In this case you have a copy of dragged row in destination. Now you can drag again the same, original, row and append it to destinationTable as many times as you whant. It is nice solution for, lets say Shopping carts ;)
<hr />
As concerns offsets, and event delegates, instead of inventing wheel again, you should consider finaly use a wheel. There is a lot of js libs which waitng you to ride them, if they are to forward, big and have as concerns code for your project, you can cut them to functional pieces. One solution is JQuerys Interface. see http://interface.eyecon.ro/ .
Finaly, don't take me wrong, especially author of this article. I think this site is great for essential knowledge of javascript, maybe one of the best i have saw.