The Browser Whisperer RSS Feed


How to Create a JavaScript Framework: Part 17

Drag and Drop

Being able to move an element on the page to another location is one of the fundamental functions for building powerful and efficient user interface. Scrollbars, sliders, sortable lists, layouts that can be manually rearranged — these are some of the interface conventions that users have come to expect in Web applications. This all starts with tracking the position of the mouse cursor when it clicks on the dragable element. There are so many implementations of drag libraries out there. Some of them are not so good, and others are excellent. The better ones are tied to their respective libraries: Scripta.culo.us, jQuery, Moofx, YUI, etc. I really didn't want to reinvent the wheel. There is one library, DOM-drag, written by Aaron Boodman, which is practically the mother of all drag libraries. It has been adopted and evolved since it its release. It is a simple and object oriented library. I therefore refactored it to work with my library. One thing about the original implementation really bugged me. Since the methods determine the position of the element being dragged by its inline style values, it required initial position values set inline. You could  attach the initial values inline, or you could attach them programmatically. If you did't, Drag would set initial values for you of top: 0px; left: 0px. This could sometimes cause unexpected results. I therefore modified its behavior to use my library to determine the element's true position values using vx.getStyle and attach those automatically during initialization. This means you can set the position values with even an external CSS file and Drag will initialize the target element with the correct values. This was a more unobtrusive solution.

In order to make an element dragable, first it must have positioning. This can be either absolute or relative. Optionally you could refactor these methods to handle static positioning and alter the target element's position by updating its left and top margins, but that could have adverse affects on your layout. There is no reason to do this. Instead use relative positioning.

The Drag object has five methods: init, start, drag, end and fixE. The init method setups up everything to make the target element dragable. This method can take a total of 10 arguments. Only the first, elem, is required and indicates what is to be dragged. To create a more complex dragable object, say with a handle, the first argument, elem, would refer to the handle, and the second argument, elemParent, would refer to the parent element that the handle would drag. Please note that handles must always be a child node of the element to be dragged. To create a slider that moves another element, you would update that element's coordinates with the values generated by the slider. I've done that in the examples file with a faux scroll box.

Init can also take four optional parameters to limit the region in which the target element is dragged: minX, maxX, minY, maxY. By passing a minX value of 0, you set the starting point for the drag at left: 0px. A minY value of 0 would do the same for the top. The maxX and maxY arguments will set the maximum distance that the element can be dragged. So, as an example, passing the arguments 0, 200, 0, 0, would allow the target element to be dragged horizontally a distance of 200 pixels inside its parent's coordinates. Similarly, values of 0, 0, 200 would allow the element to be dragged vertically for a total of 200 pixels. And finally, passing these values, 40,200,40,200, restrict the dragging to a region with those coordinates. The arguments bSwapHorzRef and bSwapVertRef allow you to swap the coordinate system between CSS coordinates from the defaults, which are left and top, to their opposites: bottom and right.

And finally, I have to say this was a major headache getting it to work with IE. It worked fine in Firefox and Safari, but IE would through exceptions, crash, or when moving one object, IE would also move another object in random, unpredictable ways. It took many late nights to track down the issues causing this and find workarounds. Now it works fine in IE too. Phew!

When I get some time, I intend to add support for knowing when the element was dragged onto another element, as for a shopping cart. Right now to do that, you would need to write a function to determine if the coordinates of the dragged element match the coordinates of the drop target and then fire that function with onDragEnd.

You can download the latest version with examples and play with them. Don't forget to look at the source code.

Drag

 

//////////////////////////////////////////////////////////

//

//  Adopted from original by Aaron Boodman: www.youngpup.net

//  I encapsulated it further and took some variables out of the

//  global namespace and bound them to the Drag object. I also

//  changed how the initial inline values were set up so that

//  it automatically gets the true position values defined

//  on the element by CSS.

//

//////////////////////////////////////////////////////////

 

ogitrev.prototype.Drag = {

    // The current element being dragged.

    obj: null,

    // The initalization function for the object to be dragged.

    // elem is an element to use as a handle while dragging (optional).

    // elemParent is the element to be dragged, if not specified, 

    // the handle will be the element dragged.

    // minX, maxX, minY, maxY  are the min and max coordinates 

    // allowed for the element while dragging.

    // bSwapHorzRef will toggle the horizontal coordinate system from

    // referencing the left of the element to the right of the element.

    // bSwapVertRef will toggle the vertical coordinate system from

    // referencing the top of the element to the bottom of the element.

    init: function(elem, elemParent, minX, maxX, minY, maxY, bSwapHorzRef, 

        bSwapVertRef) {

        // Watch for the drag event to start.

        elem.onmousedown = ogitrev.prototype.Drag.start;

        // Figure out which coordinate system is being used.

        elem.hmode = bSwapHorzRef ? false : true ;

        elem.vmode = bSwapVertRef ? false : true ;

        // Figure out which element is acting as the draggable "handle."

        elem.root = elemParent && elemParent != null ? elemParent : elem ;

        // Initalize the specified coordinate system.

        // In order to keep track of the position of the dragged element,

        // we need to query the inline position values.

        // Therefore we query the element's style properties

        // to get those values and attach them inline on the element.

        if (elem.hmode && isNaN(parseInt(elem.root.style.left ))) {

           elem.root.style.left = vx.getStyle(elem.root, "left");

        }

        if (elem.vmode && isNaN(parseInt(elem.root.style.top ))) {

           elem.root.style.top = vx.getStyle(elem.root, "top")

        }

        if (!elem.hmode && isNaN(parseInt(elem.root.style.right ))) {

           elem.root.style.right = vx.getStyle(elem.root, "right")

        }

        if (!elem.vmode && isNaN(parseInt(elem.root.style.bottom))) {

           elem.root.style.bottom = vx.getStyle(elem.root, "bottom")

        }

        // Look to see if the user provided min/max x/y coordinates.

        elem.minX = typeof minX != 'undefined' ? minX : null;

        elem.minY = typeof minY != 'undefined' ? minY : null;

        elem.maxX = typeof maxX != 'undefined' ? maxX : null;

        elem.maxY = typeof maxY != 'undefined' ? maxY : null;

        // Check for any specified x and y coordinate mappers.

        elem.xMapper = fXMapper ? fXMapper : null;

        elem.yMapper = fYMapper ? fYMapper : null;

        // Add methods for user-defined functions.

        // The user can attach these to a symbol of the 

        // element being dragged:

        /*

            var targetElem = vx.byId("sliderTumb");

            targetElem.onDragEnd = function() {

               alert("You finished dragging!");

            }

        */

        // This will fire when you release the dragged element.

        elem.root.onDragStart = new Function();

        elem.root.onDragEnd  = new Function();

        // The following will fire continuously while the element

        // is being dragged. Useful if you want to create a slider

        // that can update some type of data as it is being dragged.

        elem.root.onDrag = new Function();

    },

    start: function(e) {

        // Figure out which object is being dragged.

        var elem = ogitrev.prototype.Drag.obj = this;

        // Normalize the event object.

        e = ogitrev.prototype.Drag.fixE(e);

        // Get the current x and y coordinates.

        ogitrev.prototype.Drag.y = parseInt(elem.vmode ? elem.root.style.top : 

           elem.root.style.bottom);

        ogitrev.prototype.Drag.x = parseInt(elem.hmode ? elem.root.style.left :

           elem.root.style.right );

        // Call the user's function with the current x and y coordinates.

        elem.root.onDragStart(ogitrev.prototype.Drag.x, ogitrev.prototype.Drag.y);

        // Remember the starting mouse position.

        elem.lastMouseX = e.clientX;

        elem.lastMouseY = e.clientY;

        // Do the following if the CSS coordinate system is being used.

        if (elem.hmode) {

            // Set the min and max coordiantes, where applicable.

            if (elem.minX != null) elem.minMouseX    = e.clientX - 

               ogitrev.prototype.Drag.x + elem.minX;

            if (elem.maxX != null) elem.maxMouseX    = elem.minMouseX + 

               elem.maxX - elem.minX;

        // Otherwise, use a traditional mathematical coordinate system.

        } else {

            if (elem.minX != null) elem.maxMouseX = -elem.minX + e.clientX + 

               ogitrev.prototype.Drag.x;

            if (elem.maxX != null) elem.minMouseX = -elem.maxX + e.clientX + 

               ogitrev.prototype.Drag.x;

        }

        // Do the following if the CSS coordinate system is being used.

        if (elem.vmode) {

            // Set the min and max coordiantes, where applicable.

            if (elem.minY != null) elem.minMouseY    = e.clientY - 

               ogitrev.prototype.Drag.y + elem.minY;

            if (elem.maxY != null) elem.maxMouseY    = elem.minMouseY + 

               elem.maxY - elem.minY;

        // Otherwise, we're using a traditional mathematical coordinate system.

        } else {

            if (elem.minY != null) elem.maxMouseY = -elem.minY + e.clientY + 

               ogitrev.prototype.Drag.y;

            if (elem.maxY != null) elem.minMouseY = -elem.maxY + e.clientY + 

               ogitrev.prototype.Drag.y;

        }

        // Watch for "drag" and "end" events.

        document.onmousemove = ogitrev.prototype.Drag.drag;

        document.onmouseup = ogitrev.prototype.Drag.end;

        return false;

    },

    // A function to watch for all movements of the mouse during the 

    // drag event.

    drag: function(e) {

        // Normalize the event object.

        e = ogitrev.prototype.Drag.fixE(e);

        // Get our reference to the element being dragged.

        var elem = ogitrev.prototype.Drag.obj;

        // Get the position of the mouse within the window.

        var ey = e.clientY;

        var ex = e.clientX;

        // Get the current x and y coordinates.

        ogitrev.prototype.Drag.y = parseInt(elem.vmode ? elem.root.style.top : 

           elem.root.style.bottom);

        ogitrev.prototype.Drag.x = parseInt(elem.hmode ? elem.root.style.left : 

           elem.root.style.right );

        var nx, ny;

        // If a minimum X position was set, make sure it doesn't go past that.

        if (elem.minX != null) ex = elem.hmode ? 

            Math.max(ex, elem.minMouseX) : Math.min(ex, elem.maxMouseX);

        // If a maximum X position was set, make sure it doesn't go past that.

        if (elem.maxX != null) ex = elem.hmode ? 

            Math.min(ex, elem.maxMouseX) : Math.max(ex, elem.minMouseX);

        // If a minimum Y position was set, make sure it doesn't go past that.

        if (elem.minY != null) ey = elem.vmode ? 

            Math.max(ey, elem.minMouseY) : Math.min(ey, elem.maxMouseY);

        // If a maximum Y position was set, make sure it doesn't go past that.

        if (elem.maxY != null) ey = elem.vmode ? 

            Math.min(ey, elem.maxMouseY) : Math.max(ey, elem.minMouseY);

        // Figure out the newly translated x and y coordinates.

        nx = ogitrev.prototype.Drag.x + ((ex - elem.lastMouseX) * (elem.hmode ? 

           1 : -1));

        ny = ogitrev.prototype.Drag.y + ((ey - elem.lastMouseY) * (elem.vmode ? 

           1 : -1));

        // Set the new x and y coordinates onto the element.

        ogitrev.prototype.Drag.obj.root.style[elem.hmode ? "left" : "right"] = 

           nx + "px";

        ogitrev.prototype.Drag.obj.root.style[elem.vmode ? "top" : "bottom"] = 

           ny + "px";

        // Remember  the last position of the mouse.

        ogitrev.prototype.Drag.obj.lastMouseX = ex;

        ogitrev.prototype.Drag.obj.lastMouseY = ey;

        // Call the user's onDrag function with the current x and y coordinates.

        ogitrev.prototype.Drag.obj.root.onDrag(nx, ny);

        return false;

    },

    // Function that handles the end of a drag event.

    end: function() {

        // No longer watch for mouse events (as the drag is done).

        document.onmousemove = null;

        document.onmouseup = null;

        // Call our special onDragEnd function with the x and y coordinates

        // of the element at the end of the drag event.

        ogitrev.prototype.Drag.obj.root.onDragEnd( 

            parseInt(ogitrev.prototype.Drag.obj.root.style[ogitrev.prototype.

               Drag.obj.hmode ? "left" : "right"]), 

            parseInt(ogitrev.prototype.Drag.obj.root.style[ogitrev.prototype.

               Drag.obj.vmode ? "top" : "bottom"]));

        // No longer watch the object for drags.

        ogitrev.prototype.Drag.obj = null;

    },

    // A function for normalizing the event object.

    fixE: function(e) {

        // If no event object exists, then this is IE, so provide IE's 

        // event object.

        if (typeof e == 'undefined') e = window.event;

        // If the element's properties aren't set, get the values from

        // the equivalent offset properties.

        if (typeof e.elemX == 'undefined') e.elemX = e.offsetX;

        if (typeof e.elemY == 'undefined') e.elemY = e.offsetY;

        return e;

    }

};

 
Posted by Robert Biggs | 0 Comments | Trackback Url | Bookmark with:        
Tags:

Links to this Post

Comments

Name:
URL:
Email:
Comments:

CAPTCHA Image Validation