Here is the part from jQuery UI documentation for the tolerance
option:
This is the way the reordering behaves during drag. Possible valu
For those with the same problem here's the workaround I use:
$('#sortable').sortable({
axis: 'x',
sort: function (event, ui) {
var that = $(this),
w = ui.helper.outerWidth();
that.children().each(function () {
if ($(this).hasClass('ui-sortable-helper') || $(this).hasClass('ui-sortable-placeholder'))
return true;
// If overlap is more than half of the dragged item
var dist = Math.abs(ui.position.left - $(this).position().left),
before = ui.position.left > $(this).position().left;
if ((w - dist) > (w / 2) && (dist < w)) {
if (before)
$('.ui-sortable-placeholder', that).insertBefore($(this));
else
$('.ui-sortable-placeholder', that).insertAfter($(this));
return false;
}
});
},
});
This works for a horizontal sortable, for a vertical one change outerWidth
to outerHeight
and position.left
to position.top
everywhere.
Here's a complete working example
Here is my implementation of the sort
method for grid layout based on Pythagorean theorem and distance between compared elements' centers:
$("#sortable").sortable({
...
sort: sort_jQueryBug8342Fix
});
function sort_jQueryBug8342Fix(e, ui) {
// ui-sortable-helper: element being dragged
// ui-sortable-placeholder: invisible item on the original place of dragged item
var container = $(this);
var placeholder = container.children('.ui-sortable-placeholder');
var draggedCenterX = ui.helper.position().left + ui.helper.outerWidth()/2;
var draggedCenterY = ui.helper.position().top + ui.helper.outerHeight()/2;
container.children().each(function () {
var item = $(this);
if(!item.hasClass( 'ui-sortable-helper' ) && !item.hasClass( 'ui-sortable-placeholder' ) ) {
var itemCenterX = item.position().left + item.outerWidth()/2;
var itemCenterY = item.position().top + item.outerHeight()/2;
// Pythagorean theorem
var distanceBetweenCenters = Math.sqrt(Math.pow(itemCenterX - draggedCenterX, 2) + Math.pow(itemCenterY - draggedCenterY, 2));
var minDimension = Math.min(item.outerWidth(), item.outerHeight(), ui.helper.outerWidth(), ui.helper.outerHeight());
var overlaps = distanceBetweenCenters < (minDimension / 2);
if (overlaps) {
if (placeholder.index() > item.index()) {
placeholder.insertBefore(item);
} else {
placeholder.insertAfter(item);
}
container.sortable('refreshPositions');
return false;
}
}
});
}
It works great for grid layouts and items with close width and height sizes, like image thumbnails of 4:3 or 16:10 ratio. In this case it needs 50-60% overlap to trigger sorting. If you have very wide elements, like 300x50 pixels (or elements of different size), then I suppose it will work too, but you would have to overlap them by X axis more, like 90%.
Note: if you are sorting Dropzone.js elements, then && item.hasClass('dz-preview')
should be added to first if
statement.
Expanding on dioslaska's answer slightly. Here's an example to do (what I'll call) 125% overlap http://jsfiddle.net/yj1wwtsd/4/.
This only flips an element when you have gone past it by 25%. In my case I combine droppable and sortable and it feels better when the dragged item can completely overlap another (to allow dropping) before flipping.
Of particular note is the jQueryUI hack on the last line. Without this it always defaults to intersect tolerance, even though the sort method is trying to do something different. This may break with future versions of jQueryUI (tested with 1.11.2).
$('#sortable').sortable({
axis: 'x',
sort: function (event, ui) {
var that = $(this);
var w = ui.helper.outerWidth();
var centreOfDraggedItem = ui.position.left + w/2;
that.children().each(function () {
if ($(this).hasClass('ui-sortable-helper') || $(this).hasClass('ui-sortable-placeholder'))
return true;
var centreOfThisItem = $(this).position().left + w/2;
var dist = centreOfDraggedItem - centreOfThisItem;
var before = centreOfDraggedItem > centreOfThisItem;
if (Math.abs(dist) < w/2) {
if (before && dist > w/4) { // moving right
$('.ui-sortable-placeholder').insertAfter($(this));
return false;
}
if (!before && dist < -(w/4)) { // moving left
$('.ui-sortable-placeholder').insertBefore($(this));
return false;
}
}
});
},
});
$('#sortable').data('ui-sortable')._intersectsWithPointer = function () {return false;};
This happens exactly as you described in IE9, FF11 and Chrome18.
Intersect overlaps when mouse is at least 50% above.
I think this is the documentation who is not correct.
EDIT: I found no bug related to this in JQuery Bugtracker.
I have unevenly sized sortable items and this makes things event worse. I've modified dioslaska's code to change the order once the bottom of the handler passes the bottom of the child (when moving down) and the same for the tops ...
sort: function (event, ui) {
var that = $(this),
draggedHeight = ui.helper.outerHeight(),
originalTop = ui.originalPosition.top,
draggedTop = ui.helper.position().top,
draggedBottom = draggedTop + draggedHeight;
that.children().each(function () {
if ($(this).hasClass('ui-sortable-helper') || $(this).hasClass('ui-sortable-placeholder')) {
return true;
}
var childTop = $(this).position().top,
childHeight = $(this).outerHeight(),
childBottom = childTop + childHeight,
isChildAfter = originalTop < childTop,
largeTop,largeBottom,smallTop,smallBottom;
if (childHeight > draggedHeight) {
largeTop = childTop;
largeBottom = childTop + childHeight;
smallTop = draggedTop;
smallBottom = draggedBottom;
} else {
smallTop = childTop;
smallBottom = childTop + childHeight;
largeTop = draggedTop;
largeBottom = draggedBottom;
}
if (smallBottom > largeTop || smallTop < largeBottom) {
if (isChildAfter && draggedBottom >= childBottom) {
$('.ui-sortable-placeholder', that).insertAfter($(this));
} else if (!isChildAfter && draggedTop <= childTop) {
$('.ui-sortable-placeholder', that).insertBefore($(this));
return false;
}
}
});
}
You can see the overlap detection is a little trickier than before as you may be dragging a large item over a small item or vice versa.
It's not perfect, but it is a huge improvement over the default functionality.
Here's an addition to @dioslaska's function. If you need to sort in a grid (e.g. your elements are floated), you can check the child element's top position to see if it's in the same "row" like so:
sort: function (event, ui) {
var self = $(this),
width = ui.helper.outerWidth(),
top = ui.helper.position().top;//changed to ;
self.children().each(function () {
if ($(this).hasClass('ui-sortable-helper') || $(this).hasClass('ui-sortable-placeholder')) {
return true;
}
// If overlap is more than half of the dragged item
var distance = Math.abs(ui.position.left - $(this).position().left),
before = ui.position.left > $(this).position().left;
if ((width - distance) > (width / 2) && (distance < width) && $(this).position().top === top) {
if (before) {
$('.ui-sortable-placeholder', self).insertBefore($(this));
} else {
$('.ui-sortable-placeholder', self).insertAfter($(this));
}
return false;
}
});
}