Thursday, March 01, 2007

Simple draggable HTML using prototype.js

I know there are plenty of JavaScript packages out there to allow you to create draggable elements on your HTML page, but to me, most of them appear to be overly complicated for a simple function and are often part of a larger framework that you may not need. The solution I'm providing today does depends on the prototype.js package (I'm using version 1.5 currently) which allows the code size to stay smaller. Here's how to make any HTML element draggable in 5 easy steps.

  1. Add the class of draggable to whatever you want to move. Actually, you can use any class identifier you want, but draggable is what I'm looking for in my code.
  2. Give your draggable element a position style (position: absolute; or position: relative;) and an initial top and left value. Generally, you'd want to put this in your styles in an external CSS with the draggable class, but the top and left values need to be set as an attribute in the HTML. Hint: setting the position to relative with the top and left values set to 0px will allow it to start located where it normally would be in the browser. You'll probably want to give it a higher z-index and solid background-color as well, but that's up to you.
  3. Include the 3 drag event handling functions I'll describe below in your code somewhere
  4. Register all your draggable elements to the event listeners.
  5. Have fun dragging window elements around.

The event handlers

Before you can use the following functions, you need to define some variables to hold some information during the drag. If you make the subsequent functions part of another containing object, you can simply prepend all occurrences of these variables in the functions with this. and they will be stored in the containing object instead.
var dragObj;
var cursorStartX;
var cursorStartY;
var elementStartX;
var elementStartY;
var dragGofn;
var dragStopfn;
startDrag
startDrag will initialize a mouse drag event. It captures the initial location of the mouse when beginning the drag in order to calculate movement later. It also captures the current coordinates of the dragged element for correct positioning as the location of the mouse are most likely not the origin of the element being dragged. Finally, it associates the additional event observers to the element to capture the mouse movement (onmousemove) and release of the mouse button (onmouseup).
function startDrag(event) { 
    dragObj = Event.element(event);
    // if the target of the event is not a "draggable" element, then search
    // through it's parents for an element that is draggable.
    // if we can't find a draggable element before reaching the root document
    // element, then bail out
    while (!Element.hasClassName(dragObj,"draggable")) {
         dragObj = $(dragObj).up('.draggable');
    }
    cursorStartX = Event.pointerX(event);
    cursorStartY = Event.pointerY(event);
    elementStartX = parseInt(dragObj.style.left, 10);
    elementStartY = parseInt(dragObj.style.top, 10);
    
    Event.observe(dragObj, 'mousemove', dragGofn);
    Event.observe(dragObj, 'mouseup', dragStopfn);
}
dragGo
Once the drag event has been initiated by the startDrag function, the dragGo function will update the location of the element being dragged so that it moves around the screen with the mouse.
function dragGo(event) { 

    var x, y;
    x = Event.pointerX(event);
    y = Event.pointerY(event);

    // move the target object
    dragObj.style.left = (x - cursorStartX + elementStartX) + "px";
    dragObj.style.top = (y - cursorStartY + elementStartY) + "px";
    
    Event.stop(event);
}
dragStop
Once the user has release the mouse button, we need to stop observing the mouse movements, so the dragStop function will un-register the event handlers.
function dragStop(event) { 
    // Stop capturing mousemove and mouseup events.
    Event.stopObserving(dragObj, 'mousemove', dragGofn);
    Event.stopObserving(dragObj, 'mouseup', dragStopfn);
    dragObj = null;

    Event.stop(event);
}

Registering the draggable elements

You're almost ready to start moving items on the screen, but first you have to register all your draggable elements to the startDrag function so that they will be notified of the drag events. So, we utilize prototype's element-by-classname selector to find all the draggable elements and associate the event listener to them. Note: in order to un-register the event listeners, you have to have the handle to the event listener you used to register it originally. Thus, we used the variables dragGofn and dragStopfn to register and un-register the event listeners. These variables are defined here during the original event registration.
function registerEvents() {
    dragGofn = dragGo.bindAsEventListener();
    dragStopfn = dragStop.bindAsEventListener();
    
    $$(".draggable").each(function(draggableElem) {
        Event.observe(draggableElem, 'mousedown', startDrag.bindAsEventListener(), true);
    });
    
}
Note: the final true on the Event.observe tells prototype to request capturing instead of bubbling. This is necessary for this function to work in Safari (see Kir's blog (http://kirblog.idetalk.com/2006/06/safari-javascript-problems.html))

2 comments:

Anonymous said...

Thank you for the excellent article.

The example was something I was searching for. As I need light weight scripts even using prototype.js. But with extra libraries over it would start to make the CMS software bit too heavy for the purpose.

Hopefully other people will also find the article.

Wolfgang Jordan said...

Thanks, this is working well and is quite easy to use. I found, however, that it would be better to use "document" instead of "dragObj" in the mousemove event handler. Otherwise it's easy to drop the window accidentally when the cursor is moving to fast and leaves the dragObj.

Wolfgang