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