Friday, August 8, 2008

$.event.special.drop

This plugin leverages the special event api, to add bind-able drop behavior events. This plugin is designed to work exclusively with $.event.special.drag, actually, it doesn't work without it.

Download - jQuery 1.2.6+ required, jquery.event.drag 1.0+ required.

This plugin compares "dragProxy" position and registered drop target positions asynchronously, based on some tolerance mode. Some plugins check positions over often on every mousemove event, which can hinder performance. This plugin uses a fixed delay and only compares positions while the mouse is moving.

The available events using this plugin are...

  • dropstart - When a drop target becomes active, depends on tolerance mode and dragProxy. Return false to prevent drop.
  • drop - When a dragged element is released within an active drop target. Return false to null the "dropTarget" event property prior to calling "dragend".
  • dropend - When a drop target becomes inactive.

The "dropTarget" event property is updated (in addition to drag event properties) to reflect the single active drop target element. When completing your drag/drop interaction, in the "dragend" handler the "dropTarget" property can be used to detrmine whether a drop happend or not.

The drop target position and size is captured and cached at the time of the drag event binding. If your application is highly dynamic, you can use the $.dropManage utility to filter and re-cache drop target element locations.

// bind a drop event handler $( elems ).bind( "drop", function(){ ... });

There are several stored tolerance modes already...

  • mouse - This mode is not explicitly defined, but is the fallback mode if no other mode is properly specified.
  • overlap - The target element which shares the most overlapping area with the proxy element wins.
  • intersect (Default) - The target element which contains the mouse (cursor) wins, else target element with most overlapping area wins.
  • fit - The proxy element is completely contained within target element bounds.
  • middle - The center point of the proxy element is contained within target element bounds.

Demo - Use the select to try different tolerance mode settings.

$.dropManage( options )

"dropManage" is a global utility function, that takes a single options argument in the form of an object literal. The options will extend the $.event.special.drop object, which allows you to use the following keys for configuration. The function returns a jquery collection of filtered drop target elements.

  • delay - The async timeout delay (default: 100) in milliseconds between target tolerance checks.
  • mode - Key string (default:'intersect') for stored tolerance modes.
  • tolerance - Create a function to make a custom tolerance mode.
  • filter - jQuery filter method argument (string expression or function) to filter the all currently bound drop target elements and cache the position and size.

The most typical place to use the $.dropManage function, is inside of a "dragstart" event. In dynamic applications, where the drop target elements will change size or position, you can re-locate elements quite easily. It also makes it easy to interact with a restricted subset of all bound drop target elements and different tolerance modes.

// filter drop elements, set new tolerance mode $.dropManage({ mode:'fit', filter:'.red' });

Demo - Drag the drop target to a different location, this demo uses "$.dropManage" to relocate elements on each "dragstart" event.

You can create and store your own custom tolerance modes be extending the "$.event.special.drop.modes" object with a new function. The function scope, when called, is the "$.event.special.drop" (this keyword). The function receives three arguments:

  • event - the most recent mousemove/drag event.
  • proxy - a location object representing the drag proxy element.
  • target - a location object representing the drop target element.

A location object is a handy object literal that is returned from the "$.event.special.drop.locate" helper function. The location object values are:

  • elem - the DOM element that is represented.
  • L - the LEFT position of the element.
  • W - the WIDTH of the element
  • R - the RIGHT position of the element (Left + Width).
  • T - the TOP position of the element.
  • B - the BOTTOM position of the element (Top + Height).
  • H - the HEIGHT of the element.

Another helper function is "$.event.special.drop.contains" which takes two arguments: the target location, and the test location. The first argument should always be the drop element "location" object, the second argument could be the drag proxy location or a 2 value array representing XY coordinates respectively. The "contains" function returns true or false.

Using the helper functions to determine you custom tolerance mode, your tolerance function can return the target location object of the outright winning element, or null to continue comparing. In addition, you can set the "best" parameter of the "$.event.special.drop" object to continue comparing all drop targets before choosing a winner. Hopefully the examples below will help make things more clear.

// tolerance function scope is '$.event.special.drop' // 'fit' tolerance mode uses the 'contains' function $.event.special.drop.modes['fit'] = function( event, proxy, target ){ // returns an outright winner, or null return this.contains( target, proxy ) ? target : null; }; // 'intersect' mode is a complex example $.event.special.drop.modes['intersect'] = function( event, proxy, target ){ // returns an outright winner if cursor is inside return this.contains( target, [ event.pageX, event.pageY ] ) ? // else reverts to 'overlap' mode target : this.modes['overlap'].apply( this, arguments ); };

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

// 0 args, trigger "drop" handler $( elems ).drop(); // 1 arg, binds "drop" handler $( elems ).drop( dropFn ); // 2 args, binds "dropstart" and "drop" handlers $( elems ).drop( dropstartFn, dropFn ); // 3 args, binds "dropstart" and "drop" and "dropend" handlers $( elems ).drop( dropstartFn, dropFn, dropendFn );

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

31 comments:

Jakub Łukomski said...

i've noticed a small bug in the drop event.
If I bind drop to an object and then move that object, it's still treated as if it was in it's previous location (which makes it pretty annoying when you move items around in a list of some kind)

The easiest working solution I've found is to simply unbind the old drop events after moving and bind new ones, but it's still just a hack, as moving items should make drop behave accordingly

3wme said...

Thanks Jakub, but that is a deliberate feature of this plugin. I apologize for not completing this documentation before you left this comment. There is a utility function called "$.dropManage" which re-caches target positions, which allows developers to help manage their application performance more closely to suit there needs. I have recently updated this article, so hopefully this feature is a little more clear.

Scott said...

Anyway of speeding this up for a large number of drop boxes? I've got a calendar style page with 250 drop boxes and everytime you start a drag it takes about 5 seconds before the drag box starts to move. Firebug seems to think it is curCSS in jquery that is slowing it down.

Felix said...

Nice script!
But I meet a problem: when I have more than one dragable objects, the script works strangely, the event.offsetX/Y is much higher than its actual value.
For example, while the event.offsetX/Y for the first item is ok, event.offsetX/Y for the second item might be 200/300, event.offsetX/Y for the third item might be 400/500...

Could you check this and make a demo for multiple dragable items? Thanks.

d3r1v3d said...

Excellent work!

One thing that kind of threw me for a loop is the need to explicitly call the [jQuery_element].drop(...) method to hook in the drop functionality for certain elements. This is at odds with the way you can initialize drag elements by just binding 'drag' events.

To summarize, it would be more intuitive if you could enable drop functionality for specific elements by binding them to arbitrary 'drag' events.

Hope that made sense. Kudos!

Anonymous said...

is there a way to retrieve the ID value of the div being dropped?

i have a project that has a few drop divs. and records that you drag onto those divs. i would like to set it up that when you drag the record onto the drop divs it via ajax writes to the database to say which div was dragged where.

william said...

lol so there IS a name field here... im anon above :P soz

basicaly... i have to create a layout planner thingy... a whole lot of records.. when you drag a record onto a page. it needs to update the database with what page you dragged the record to. and remove the record from the list. and show the record on the page.

say i drag the record creatively labeled "record 1" to "page 3" then page 3 needs to show "record 1" in it.. at which point it needs to have written to the database aswell.

im still stuck on it showing "record 1" was dragged onto the page.

.bind( 'drop', function( event ){ $( this ).append('added'); })

i presume i need to put the code in there... but how do i refrence the dragged element?

all the records are wrapped in div tags. with the divs ID = to the ID value of the record. if i can set it up that when you drop a record it calls an ajax function to write to the database... on sucess it loads the page div again. but im kinda lost as to where i would have to put the ajax function etc

this is what i have so far... hope somone can help

$(function(){
$('.advert').bind( 'dragstart', function( event ){
$.dropManage({ mode: 'intersect' });
return $( this ).clone() .appendTo( this.parentNode )
.css({ opacity: .5, top: event.offsetY, left: event.offsetX }); })
.bind( 'drag', function( event ){
$( event.dragProxy ).css({ top:event.offsetY, left:event.offsetX });
})
.bind( 'dragend', function( event ){
$( event.dragProxy ).fadeOut();
});

$('.page').bind( 'dropstart',function(){ $( this ).addClass('active'); })
.bind( 'drop', function( event ){ $( this ).append('added'); })
.bind( 'dropend', function( event ){ $( this ).removeClass('active'); });
});

what i have already

3wme said...

william...

You already have the drop target element scope inside of the "drop" event handler, so you already have access to any attributes associated with that element... To access the "dragged" element, there are some additional event properties: "dragTarget", "dragProxy", and "dropTarget" (which you could use within the "dragend" handler).

Hope this helps.

william said...

oki im a noob.. lol sorry to bother but how would i impliment it?

william said...

wow... ok so been playing around with it... think i got it working... not to sure if its all correct and all. gonna try finish it up... will post the completed code :D

this plugin rocks

WilliamStam said...

oki so i bit the bullet and logged into blogger... 3wme you have msn or an email addy i could mebbe correspond with you on other than the comments field here?

im starting to feel bad bout the 4 now 5 "spam" posts lol

Thomas said...

Any example how can I use 'dropTarget'? I have the same problem as William. On my page is two drag and drop list. I need disable posibilities drop elements from first list on second list target.

WilliamStam said...

Thomas...

just change the $('.advert') parts...

the part that says this is the drag div and the drop div.. i have my drag div set as a class of "advert" and the drop div has a class of "page"

so your code would start with something like

$(function(){
$('.advert')
.bind( 'dragstart', ............

and later in the script

$('.page')
.bind( 'drag',

just change the "$('.page')" parts

so basicaly you will have a script for each drag and drop "pair" you have... would be the easiest... i know im gonna get flamed for that but meh... so what

go go 3wme with an easier solution :P

WilliamStam said...

is there a way to drag and drop an already dropped div?

like in drag and drop a few records onto a div, if you decide that record doesnt belong there then you drag it onto another div

3wme said...

William, Thomas,

I have put together a complex example which will hopefully be helpful in answering all of your questions...

Drop Demo

WilliamStam said...
This comment has been removed by the author.
WilliamStam said...

that demo should be a lin in the official documentation... its friggin awesome :D

new lil issue. hope you can help

works a charm till i refresh. im currently using append in the head to populate all pages that have been dragged and dropped in the past... but once you "fill" the div with replicas of what the origional div says you cant drag and drop it between elements. this maybe cause its refrenced as "this" insted of a item refrence?

soz my terminology is a bit crooked. its almost 4 am :(

what i have so far

had to disable all the ajax calls as they will error cause i dont have the database etc on the server

basicaly... how to move those records in page 4 to other pages...

WilliamStam said...

oops forgot to actualy put the link in, soz

Thomas said...

I show you what exactly I'm trying to do.

http://www.zshare.net/image/51638775cbc31fa0/

The problem is possibility cross drag. It means I can drag items from top lists to bottom and from bottom to top.

Any idea?

WilliamStam said...

ahh k... mebbe just give it a different DIV name for the top and the bottom ones. or add in an if statement. so in your divs have a "rel='top'" and a "rel='bottom'"

in the js part add a if statement if drag targets rel attr doesnt = the drop targets rel then throw an error or something. should work then i supose :P

3wme said...

William,

You have an order of operation problem. You should append the "saved" info before you bind the events.

Thomas,

I do not understand the problem you are having.

WilliamStam said...

lol i figured as much.. ive resorted to actualy placing the data "in" the drop div. works like a charm

thomas wants it that the top drag and drop is isolated from the bottom. that you cant drag a record from the top to the bottom... i think.

3wme you have an email addy i can contact you on?

3wme said...

threedubmedia [at] gmail

WilliamStam said...

anyway to on drag start refrence the "parent" div where the drop was placed on?

like in drag adverts onto a page.. then when you start dragging it runs some code sending what page its being dragged FROM?

Charles said...

this is a great tool, and thanks for making it available. i think there may be a bug in, of all things, IE7 (imagine that).

when an object is dragged out of a drop-box, IE gives the error "member not found" on line 27, char 2883. everything continues to work, but the error repeats any time a dragged object is repositioned.

i double-checked, and it's happening on the demo page as well, not just my site.

Vivekanand Mishra said...

Hi Charles....

The problem you referring, i too faced but I have found the reason of that this is due to following code-

if ( this == event.dragTarget.parentNode ) return false;

I think, this property is not supported in IE7

so simply comment this line
evrything will work fine as it should

Charles said...

@Vivekanand Mishra:

perfect! thanks much!

Charles said...

another strange IE behavior.

my example page is here:
http://option8.com/fgi/drag.html

IN IE ONLY:
when one clicks on the background of one of the boxes at the bottom, it drags just fine. if, however, one clicks on the image within the box, the drag action aborts, leaving the "outline" version of the box hanging out at the bottom of the page.

my only thought is that, something is different in the way IE assigns the $(".drag") - including it somehow on child objects inside the .drag div?

Steen Nielsen said...

Great plugin! It really gives a lot of functionality without much work, while beeing very flexible.

As Charles and Vivekanand Mishra mentions, there is a error when using the plugin along with IE.. When using return false in the "dropstart" event, it throws an odd error.
On your demo (http://threedubmedia.com/demo/drop/) this also occurs.

It happens, when you drag a box from target A or from target B.
effectively it affects the behaviour so that you won't be able to move a box from A to B or from B to A, but not from A to A and not from B to B.

Of course, in any other browser than IE it works..

As Vivekanand Mishra writes, it's possible to just comment that line out or remove it.. But unfortunately I actually need this kind of functionality on the project I am working on.

If anyone found a workaround, so that IE will respect the intended action, please do share it with the rest of us :-)

3wme said...

Thanks for the feedback, a new release is now available which fixes the IE issue...

http://threedubmedia.googlecode.com/files/jquery.event.drop-1.2.zip

Please Enjoy.

Steen Nielsen said...

3wme, damn you are fast :) You solved it hours before I reported it... at least according to the timestamps ;)

Your new version fixed the problem, as you said and I have no more to say.. :)

Well, okay I have more to say.. What about a sortable plugin.. I bet you can make it at lot sleaker than the one with UI.. ;)

Great work..!