Question

I've seen several questions about just swapping two draggable elements on drop, but my situation is a little bit different.

Basically, I have a bunch of draggable elements laid out in a (non-linear) grid. The grid is, in many cases, going to be completely populated with data. We want to allow users to easily rearrange the grid elements, but because the grid is non-linear, using "sortable" isn't an option. And because the grid is fully populated, straight-up swapping two grid positions when one is dragged onto the other isn't the ideal solution (as we're not trying to make rearranging just three positions into some kind of brain teaser).

What we would LIKE to do is to have the normal "drag" operation occur when dragging a grid element (call it A), but when the element is then dropped onto the desired destination (which already has another grid item in it, call it B), then A should take B's place, and B should then "snap to" the mouse cursor, just as it would if the user had clicked and dragged it. This obviously messes up the mouse interactions just a bit, because now the user has no mouse buttons pressed but is dragging an element around, so to release the element he would either click the empty cell that A came from, or click another grid element C, at which point B would take C's place and C would now be "attached" to the mouse cursor.

I've pored over the "Draggable" documentation and source, and there doesn't seem to be a way to configure it exactly how I want out of the box, but I'm pretty sure I can extend the draggable widget and overwrite a couple of its methods to achieve what I want, rather than having to write my own solution from scratch. If it makes it any easier, it really doesn't matter to me if the mouse event used to trigger a drag in the first place is a click compared to a drag event.

To be clear, this is kind of an interaction diagram for what I'm looking for:

Original Setup - ^ is mouse cursor
[A]  ^  [C]
[B]     [D]

Now click and drag on A. Result is:
[ ]^A   [C]
[B]     [D] 

Now drag over B and release. Result is:
[ ]     [C]
[A]^B   [D]

B is now acting as if it's being dragged, but mouse button is not held down. Move
cursor over C and click mouse button. Result is:
[ ]     [B] ^C
[A]     [D]

B has taken C's place, and C is now following the mouse cursor. Again, mouse 
button is not being held down. Move cursor over empty cell and click mouse 
button. Result is:
[C] ^   [B]
[A]     [D]

Now there is nothing attached to the mouse cursor.
Was it helpful?

Solution 2

Adding this as a separate answer because it's a totally different approach to the problem.

After I got this working, I tried adding a few other new features, like applying a is-swap class to the draggable when it's hovering over a non-empty droppable, and applying a is-drop class to it when it's hovering over an empty one. But because our droppables are so close together, the over and out callbacks would get all confused when becoming over a new droppable before becoming out of the previous one.

I've realized that what I need to do, should I wish to continue making use of jQuery UI, is not to sub-class draggable or droppable, but rather just use them in a totally new widget. Draggable and droppable are both widgets that get instantiated on many different elements (e.g. each draggable is its own instance, and each droppable is its own instance), but what I need is one widget that contains many draggables and droppables. This will make it far easier to control their states as they interact with each other.

Right now my plan is to create a widget that can be instantiated on some kind of container element, and provide options that allow a consumer of the widget to target the desired elements as draggables and droppables, and simply utilize draggable and droppable in their "natural" state. This will allow me to do things like check the state of one droppable from the callback of another, and fix issues like the out and over timing problem I mentioned before.

I'm going to take a stab at implementing this in the next few days, and will update this answer once I have something to show.

Update: here's the much-simplified, much more accurate implementation using this approach. http://jsfiddle.net/isochronous/BQyx2/

OTHER TIPS

Well, since no one even tried answering my question, I managed to figure out a fairly easy way to accomplish this on my own. My savior, in this case, turned out to be the jquery.simulate , jquery.simulate.ext, and jquery.simulate.drag-n-drop plugins. Using these it's trivial to simulate a specific native browser event on a set of elements (or in the case of the drag-n-drop plugin, a single element). This means I was able to simply use the "drop" callback from jQueryUI droppable to accomplish what I needed:

drop: function (e, ui) {
    var $targ = $(e.target);
    ui.draggable.detach().appendTo($targ).css({top: 0, left: 0});
    $targ.find('.ui-draggable .handle').simulate('drag', {});
}

That's it! That's all it took. Here's a fiddle to demonstrate the solution in action: http://jsfiddle.net/isochronous/mD8uM/

Update: Turns out handling the "revert" behavior in draggable was actually the hardest part. I wound up creating my own widget that extends $.ui.draggable to deal with the issue. I've updated the fiddle and added comments to explain how it all works, just in case anyone is interested.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top