Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

jQuery UI sortable tolerance option not working as expected

Here is the part from jQuery UI documentation for the tolerance option:

This is the way the reordering behaves during drag. Possible values: 'intersect', 'pointer'. In some setups, 'pointer' is more natural.

intersect: draggable overlaps the droppable at least 50%

The default is intersect, but if the mouse pointer is not above the sortable items, the sort does not happen, no matter that the dragged element is at least 50% over another sortable item (the functionality I expect) This happens on the demo page as well (linked above)

Is this a bug in jQuery UI, or am I understanding it wrong?

like image 884
istvan.halmen Avatar asked May 17 '12 13:05

istvan.halmen


4 Answers

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

like image 118
istvan.halmen Avatar answered Oct 23 '22 09:10

istvan.halmen


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.

like image 41
RichH Avatar answered Oct 23 '22 08:10

RichH


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;
    }
  });
}
like image 42
Jack McDade Avatar answered Oct 23 '22 09:10

Jack McDade


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.

like image 40
vladimir83 Avatar answered Oct 23 '22 09:10

vladimir83