Friday, August 8, 2008

$.event.special.drag

This is a jquery special event implementation of a drag event model. It is intended for use by developers who don't need one bloated script full of idiot-proof logic and a million different options. For people who plan a drag interaction model and decide how to set up pages and position elements, and don't need a script to figure that out...

Downloads - jQuery 1.2.x or 1.3.x required.

This plugin is designed to work seamlessly with $.special.event.drop, though it is not required.

To begin, let us look at the anatomy of a drag event model using standard DOM events...

  1. "mousedown" - The user depresses a mouse button within the draggable element
  2. "mousemove" - The user holds the mouse button and moves the mouse
  3. "mouseup" - The user releases the mousebutton

Makes sense? Now consider the interesting moments of a drag interaction...

  1. "dragstart" - Dragging begins, after the mouse has moved past some tolerance threshold
  2. "drag" - The mouse is moving, element dragging
  3. "dragend" - The dragging stops

This plugin simplifies all of this, by taking care of the DOM events when you bind a "drag" event handler, and triggering any other handlers at the appropriate time.

This plugin does NOT move elements! You have to do that yourself, but I think that is best in many cases (containment, snapping to a grid, axis restriction, and non-linear movement, to name a few). This plugin does update many helpful event properties for your use...

  • event.dragTarget - The originating element of the drag event
  • event.dragProxy - The proxy element or dragTarget
  • event.cursorOffsetX - The horizontal difference between the click and element position
  • event.cursorOffsetY - The vertical difference between the click and element position
  • event.offsetX - The adjusted (by cursorOffset) dragged horizontal element position
  • event.offsetY - The adjusted (by cursorOffset) dragged vertical element position
// bind a drag event, update position $( elems ).bind( 'drag', function( event ){ $( this ).css({ top:event.offsetY, left:event.offsetX }); });

Demo - Try dragging the box around...

To achieve more complex behaviors, consider handling these relatively simple events.

  • dragstart - return false to prevent drag, return an element, jquery collection, or selector string to set the proxy element which will be accessible from event.dragProxy, and will be considered for drop target tolerance
  • drag - return false to prevent further dragging and immediately trigger "dragend"
  • dragend - a drag callback
// bind a dragstart event, return the proxy element $( elems ).bind( 'dragstart', function( event ){ return $( this ).clone().appendTo( this.parentNode ); }); // bind a drag event, update proxy position $( elems ).bind( 'drag', function( event ){ $( event.dragProxy ).css({ top:event.offsetY, left:event.offsetX }); }); // bind a dragend event, remove proxy $( elems ).bind( 'dragend', function( event ){ $( event.dragProxy ).fadeOut(); });

Demo - Try dragging the box around, observe the use of a proxy...

There is one parameter for customizing drag interaction, jQuery.event.special.drag.distance (default: 0), and it is used to define the length in pixels that must be moved before triggering "dragstart." This property is captured at the time the drag event is bound, and in jQuery 1.3, you will be able pass options using the already existing "bind" data argument.

// bind with data parameters (jQuery 1.3) $( elems ).bind( "drag", { distance:10 }, function(){ ... });

Lastly, there is an overloaded jquery method called "drag" which takes zero to three arguments.

// 0 args, trigger "drag" handler $( elems ).drag(); // 1 arg, binds "drag" handler $( elems ).drag( dragFn ); // 2 args, binds "dragstart" and "drag" handlers $( elems ).drag( dragstartFn, dragFn ); // 3 args, binds "dragstart" and "drag" and "dragend" handlers $( elems ).drag( dragstartFn, dragFn, dragendFn );

Please direct any feedback to http://groups.google.com/group/threedubmedia

28 comments:

ahot said...

Thanks for your drag plugin!
I've tried to make drag ability with position:fixed styles. Here is result: link

ryan said...

thanks!! works great!!

Elijah Insua said...

This is great, I have found a couple issues though.

1) I seem to get MUCH better performance when I change line 52 to document instead of document.body (not sure if this is a compatibility issue, I'm using FF3 btw)

2) While trying to use input elements inside of a drag-able box this plugin steals clicks/selection events.

Thanks for the plugin, I am currently looking at how to fix this issue...

Elijah Insua said...

I added a:

if ($(event.target).filter(":input").length > 0) { return true; }

To jquery.event.drag.js line 49. Basically skips the drag init if it was in fact an input that was clicked.

hope this helps someone...

3wme said...

Elijah,

Thanks for the positive feedback. To answer your points:

- You are absolutely correct about document, instead of document.body i have this fixed for the next release.

- the idea of this code is deliberately not to overload with features like the suggestion you provide. This drag implementation is intended to be very basic and the developer can manually control things like constraint and snapping and such. That said, to acheive your desired result, the events "dragstart" and "drag" can both return false to cancel the drag action, so that is where you can check event.target for elements.

Hope this helps.

Elijah Insua said...

I'm completely with you on the keeping the code small and flexible. That is the reason I'm using this plugin in the firstplace :)

However, I tried what you mentioned with limited success. It would seem that by the time the dragstart handler is called, the mousedown event has been handled (without the ability to override) and mousemove event has already been hijacked.

I was debating an implementation of a $.dragManager which accepts the same sort of filter as $.dropManager to avoid dragging on certain children elements

3wme said...

Please try the new 1.1 release...

- Fixed a bug where the text-selection attributes that were disabled on
"mousedown" were not being enabled on "mouseup" unless the element was dragged.

- Now restoring the event.target property that is captured on "mousedown" before
handling "dragstart" events. This fixes buggy behavior involving "dragstart"
and the "distance" setting, and attempting to capture "handle" elements.

- Modified the handler logic for the "dragstart" return value. The stack can
now continue directly into the "drag" handler call from "dragstart" instead of
waiting for the next "mousemove" event to fire.

- Added a "not" property to the "jQuery.event.special.drag" object which allows
the prevention of drag behavior for any event.target element that matches the
property value (selector). The default value is ":input" and when jQuery 1.3 is
released, this attribute will also be customizable through the "bind" method
using the optional "data" argument. Thanks to Elijah Insua for suggesting this
feature.

- Changed binding of "mousemove" and "mouseup" events from "document.body" to
"document"... This fixes buggy behavior when the body element does not cover
the entire window. Thanks to Jonah Fox (weepy) and Elijah Insua for pointing
out this bug.

Sarco said...

I just wanted to note... that since it changed from 1.0 to 1.1, there's an issue with dragging. It changes the location of the original object by a pixel or so in the direction of the drag (when using a proxy).

Elijah Insua said...

Sarco,

I just upgrade everything seems fine. I'm using both position:absolute and regular elements for dragging. Perhaps if you pasted a minimal example of your code to reproduce someone could come up with a fix for ya.

Elijah Insua said...

Just wanted to let you know, with the new changes this thing rocks.

Very quick, minimal, flexible.. exactly what I needed!

Thanks!

3wme said...

Sarco, you are absolutely correct. That incorrect behavior could also be seen in my own "proxy" demo. This was such a major bug that I released another update today.

Please check out the new 1.2 release!

Sarco said...

Hey Again,

I noticed that this totally junks itself when NOT used with;
position:absolute

Is there a way to fix that? I've been playing around with it to no luck but I'm trying to see what I can do.

magerio said...

hey, thx for this great plugin, its just what i needed : im writing app with many dialog window, they all have the same html structure, styled by css, every window has a title with different text which i want to use as handle for drag, but heres the problem: you in some way use text in .is() function to append the drag handle .. wouldnt it be easier to give there option to use jQuery selectors? Or am i getting this wrong?

Also there is another "problem" (just a little one): you use this construction to extend jQuery: $.fn.drag = ...., wouldnt it be better to use the recommended one: jQuery.fn.drag ? just a suggestion, nothing more

magerio said...

well, i use this code for apending drag now, which is not bad ...but :)

var windows = ['#window1','#window2']; //etc
for(i in windows) {
$(windows[i])
.bind('dragstart',function( event ){
return $(event.target).is($(windows[i] + ' > .window-title').html());
})
.bind('drag',function( event ){
$( this ).css({
top: event.offsetY,
left: event.offsetX
});
});
}

3wme said...

@sarco,

I am not sure what the issue you are having is, can you provide a test page or reproducible example of the symptoms.

@magerio

Not sure what you mean about drag handles... this plugin only provides hooks for you, the developer, to determine handles. The "is" method takes simple non-hierarchical css selector expressions as an argument.

The use of the dollar sign alias in the plugin code is safe because the code is wrapped in a function closure that gets passed jquery as an argument.

It is not recommended to use "for ... in" to iterate over arrays. Plus, jQuery can take multiple comma seperated selectors (just like css) in it's selector argument. And you could use a simple css selector to determine the handle. I would refine you code as follows...

$('#window1, #window2')
.bind('dragstart',function( event ){
return $(event.target).is('.window-title');
})
.bind('drag',function( event ){
$( this ).css({
top: event.offsetY,
left: event.offsetX
});
});

magerio said...

you are right about my loops, i was just trying something ..

thx for clearing this up for me

Kasper said...

Works like a charm in FF, but somehow IE7 is completely off limits for this one. Will this be fixed?

magerio said...

is there any way to prevent the click event when the drag is finished? thx for your help

Diederik said...

Nice stuff! :)

I did manage to find an edge case where I couldn't use this library, but also found a fix. The next mousemove event was called before the browser managed to repaint everything (which happens with a large tiled map or image).

Hence my feature request: using timers. This can reduce the number of times 'mousemove' is called. Each time the timer is fired, the offset since the previous call should be returned.

This was my final code:

---------------------------------

// Enable mouse dragging.
window._moveTimer = 0;
window._newX = 0;
window._newY = 0;
window._dragOrigin = null;
window._dragCursorX = null;
window._dragCursorY = null;
window._dragNewCursorX = null;
window._dragNewCursorY = null;

$("#dragViewport").mousedown(
function( startEvent )
{
window._dragOrigin = __getMyViewportPosition(); // My function, returns x/y
window._dragCursorX = startEvent.pageX;
window._dragCursorY = startEvent.pageY;
$(this).addClass("dragged");

$(document).mousemove( _dragMove ).mouseup( _dragEnd );
}
);

function _dragMove( moveEvent )
{
// Get new position.
// Calculate own offset, not using moveEvent.offsetX here.
// Avoids 'swing' due repaining time.
window._dragNewCursorX = moveEvent.pageX;
window._dragNewCursorY = moveEvent.pageY;
window._newX = ( window._dragOrigin.x - ( window._dragNewCursorX - window._dragCursorX ) );
window._newY = ( window._dragOrigin.y - ( window._dragNewCursorY - window._dragCursorY ) );

// Delay move, allow repaints.
if( window._moveTimer ) return;
window._moveTimer = setTimeout( function()
{
clearTimeout( window._moveTimer );
__setMyViewportPosition( window._newX, window._newY ); // My function
window._moveTimer = 0;

// Reset origin, reduces the effect of fast mouse moves.
window._dragOrigin = { x: window._newX, y: window._newY };
window._dragCursorX = window._dragNewCursorX;
window._dragCursorY = window._dragNewCursorY;
}, 20 );
}

function _dragEnd( endEvent )
{
$(this).removeClass("dragged");
$(document).unbind("mousemove mouseup");
}

---------------------------------

I hope you can incorporate the intent of this code in your library. :)

José Mata said...

hello

Thank you for this pluggin, it's working great!

I had just one issue:

IE7 TextRange (document.selection.TextRange object) implementation breaks (or breaks jquery..) when doing DOM manipulation inside an active range (like creating the dragProxy).

I fixed adding the line:

if ($.browser.msie) document.selection.empty(); // IE again...

To the selectable(elem, bool) function.

Now it works great!


Thanks a lot for the pluggin!

max said...

another amazing plugin! one quick observation, on IE7 (likely 6 as well, hopefully not 8 but hope and IE in the same phrase usually leads to laughter, so assume the worst) if one maintains a drag and releases the button out of the window, the dragend callback is not sent. this becomes a problem when the cursor is moved back the window, the drag callback continues to be called for normal mousemoves - leaving us in a bit of a zombie drag state after the button had been released.

i'm not a huge fan of browser specific workarounds, part of me feels going to great lengths to do so only allows poor implementations of html/css/js to continue to limp along, but i suspect a reasonable workaround (though not likely appropriate for the plugin itself) would be to add an event listener for mouseout on the document and perform the dragend there.



all these great plugins, i do wonder what you've put them together to create...

emelendez said...

I have problems with this plugin and new released jQuery 1.3. Any plans for updating this awesome plugin "special.drag"???

thanxs in advance...

SG said...

thanks for a beautiful and useful plugin. I'm having one problem with it though which I was hoping someone can help me fix.

I have an element foo, which the user can interact with by both dragging, or clicking.

I have
$("foo").bind('dragend', dragged());
$("foo").bind('click', clicked());

dragged() and clicked() are functions that do different things. When foo is clicked (without dragging), then clicked() is executed and there's no problem.

The problem is when foo is dragged, dragged() gets executed, but then clicked() is also executed as well.

Is there a way to handle "click" and "dragend" events separately?

3wme said...

If you use a proxy element, the click event does not fire, otherwise you need to programmatically determine when to prevent clicks... Sorry, there is no way to stopPropagation, as they are completely seperate events that are firing and bubbling.

White Rose said...

Nice tips :-) Thanks!

I found that using offsetX and offsetY for top-left is not enough if we're using margin-top and margin-left. so, I try this:

var top = parseInt(parent.css("margin-top"));
var left = parseInt(parent.css("margin-left"));
$(this).parent().css({ top: event.offsetY - top, left: event.offsetX - left });


Hope that it can help if you get the same problem.

Brian said...

I want to reiterate that I'm having the same problem as Max w/ IE. When the user goes outside of the document area, the drag temporarily stops, then resumes once the user comes back into the document area (even if the mouse button has been released). Anyone found a workaround for this?

Thanks!
Brian

adolfojunior said...

Hi, i'm using your plugin for make element drag!! thanks by the code :)

so.. i think i found a problem... when my 'startdrag' function, return false, indicating that nothing is dragged.. the plugin don't detach de events mousemove and mouseup of the document, so the drag can't be stopped or restarted. because of this, the chaining of more than one drag listeners is stopped.

it's a problem?

sorry my bad english! i'm learning :)

Thanks

Collin said...

:D I've always loved this plugin.

BEST D&D api in the world. This is how I would hope to code it myself.

I tried to use jQuery UI for complex things like snapping behaviors. But jQuery UI is sadly slow and bloated.

Might be nice for the unwashed masses. But I don't think jQuery needs a monolithic plugin library. Smarter, more concise pieces are where its at!