Reputation: 792
It seems that when using two jquery UI droppables that touch each other, the droppable events are not fired correctly. If, while dragging a draggable from over one of the elements to just below it onto the next element, then the out event is fired for the first droppable, but the over event is not fired for the second. If you drop at this point, no drop event is fired.
An example is best. Try this fiddle (tested in IE7,8,9 and Chrome11). Make sure your browser's console log is open. If you drag the draggable over the first row, then slowly drag towards the second row, you'll soon see in the log that the out event for the first row is fired, but the over event for the second row is not. If you drop when this happens, the drop event is not fired.
It seems to just be a 1 pixel line in between the rows that causes the problem. Dragging one more pixel causes the over event to be fired, and the drop event to work correctly.
This looks like a bug to me, but I can't find anyone else who has used table rows as droppables and has reported the problem. I styled the table so you can see that the rows are indeed flush together with no space in between.
This is a big problem for me because in our app, the table rows are nested greedy droppables. So if the user drops when this happens, the drop is actually picked up by the outer droppable instead.
Also, we give feedback to the user in the draggable helper in the form of an icon and message that changes depending on the droppable you are over. When you drag between rows, it flickers for a moment, as it thinks you are not over any droppable when you actually are.
My questions:
@davin,
We did end up changing the drag function in $.ui.ddmanager to fix the event ordering. Our issue was we have nested greedy droppables. When you moved from one of these nested droppables to the other from bottom to top, the over event would actually fire for the parent last, causing bad things to happen.
So we added logic to basically check if moving from one nested greedy to another, and if so, not fire parent events.
Would it be too much to ask to have you look this over real quick and make sure our logic makes sense? There are two logical changes. If we moved from greedy child to greedy child:
Here's the code. See the lines dealing with the isParentStateChanged closure var, which we added:
drag: function(draggable, event) {
//If you have a highly dynamic page, you might try this option. It renders positions every time you move the mouse.
if(draggable.options.refreshPositions) $.ui.ddmanager.prepareOffsets(draggable, event);
var isParentStateChanged = false;
//Run through all droppables and check their positions based on specific tolerance options
$.each($.ui.ddmanager.droppables[draggable.options.scope] || [], function() {
if(this.options.disabled || this.greedyChild || !this.visible) return;
var intersects = $.ui.intersect(draggable, this, this.options.tolerance);
var c = !intersects && this.isover == 1 ? 'isout' : (intersects && this.isover == 0 ? 'isover' : null);
if(!c) return;
var parentInstance;
if (this.options.greedy && !isParentStateChanged) {
var parent = this.element.parents(':data(droppable):eq(0)');
if (parent.length) {
parentInstance = $.data(parent[0], 'droppable');
parentInstance.greedyChild = (c == 'isover' ? 1 : 0);
}
}
// we just moved into a greedy child
if (parentInstance && c == 'isover') {
isParentStateChanged = true;
parentInstance['isover'] = 0;
parentInstance['isout'] = 1;
parentInstance._out.call(parentInstance, event);
}
this[c] = 1; this[c == 'isout' ? 'isover' : 'isout'] = 0;
this[c == "isover" ? "_over" : "_out"].call(this, event);
// we just moved out of a greedy child
if (parentInstance && c == 'isout') {
if (!isParentStateChanged) {
parentInstance['isout'] = 0;
parentInstance['isover'] = 1;
parentInstance._over.call(parentInstance, event);
}
}
});
}
Upvotes: 1
Views: 1746
Reputation: 11
I had this problem with a project i was working on.
My solution was to see check how far the draggable was over each droppable. If the draggable is 50% over the top droppable then i assume the user wants to drop on the top droppable.
Similar for the bottom.
To do this i changed $.ui.intersect;
added vars - hw = droppable.proportions.width / 2, hh = droppable.proportions.height / 2, lhw = l + hw, thh = t + hh
then add some if statements
// going down
if(y2 < b && y2 >= thh){}
// going up
if(y1 > t && y1 <= thh){}
// covered
if(y1 <= t && y2 >= b){}
Upvotes: 0
Reputation: 45525
It's not a bug per se, it's a feature. It's all a matter of definitions. You've defined the tolerance of your droppable items to be pointer, which according to the docs is:
pointer: mouse pointer overlaps the droppable
When my mouse pointer is at (10,10) and the top left corner of my box ends at (10,10), is that overlapping? It depends on your definition. jQueryUI's definition is strong inequality, or strong overlap (see the relevant code). That makes sense (to me), since I'm not inside the box if I'm only on the edge, so I wouldn't want an event to fire.
Although if for your purposes you require weak inequality in the overlap condition (i.e. weak overlap), you can modify that line of code in your source, or override it, by adding:
$.ui.isOverAxis = function( x, reference, size ) {
return ( x >= reference ) && ( x <= ( reference + size ) );
};
Working example: http://jsfiddle.net/vwLhD/8/
Be aware that with weak inequality comes other bumps in the road: your out
event will fire after your over
event, so you might have two over events before a single out
has fired. That's not so hard to handle, but you need to make sure you deal with that case.
UPDATE:
It's important to note that if you add the code I pasted above it is going to affect all other ui widgets in the scope of $
if that's important. Maybe subbing $
could avoid that.
In any case, I have a second workaround that will solve the above issue entirely, and now on every mouse movement the pointer is either in or out of every element exclusively:
$.ui.isOverAxis2 = function( x, reference, size ) {
return ( x >= reference ) && ( x < ( reference + size ) );
};
$.ui.isOver = function( y, x, top, left, height, width ) {
return $.ui.isOverAxis2( y, top, height ) && $.ui.isOverAxis( x, left, width );
};
Working example: http://jsfiddle.net/vwLhD/10/
Essentially I've made the upper condition a weak inequality and the lower one a strong one. So the borders are entirely adjacent. Now the events fire almost perfectly. Almost and not entirely because the plugin still loops through the droppables in order, so if I'm dragging from top to bottom the firing order is good because first it detects that I have left the higher element, and then detects that I have entered the lower element, whereas dragging from bottom to top the order of firing is reversed - first it fires entering the higher and only then leaving the lower.
The difference between this and the previous workaround is that even though half the time the order is not good, it all happens in one tick, i.e. over-out or out-over are always together, the user can never get stuck as in the original case and first workaround.
You can further hone this to be absolutely perfect by changing the ui code to loop through the items first according to those that have the mouse over them, and only then the rest (in the $.ui.ddmanager
function). That way the mouse leave will always fire first. Alternatively you can swap the order and have the reverse order; whatever suits you better.
That certainly would solve your problem entirely.
Upvotes: 3
Reputation: 3989
Sounds like you might be dropping between rows which would mean you were dropping onto the table. Do you have your table borders collapsed? css border-collapse: collapsed;
Upvotes: 0