Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to fix wrongly positioned draggable helpers for connected sortables (partially caused by floated/relative positioned parent elements)?

Preface

I'm experiencing a problem where the draggable helper is being offset incorrectly, when using draggables + sortables that are placed in floating, relative positioned parent elements. The floated parent elements are Bootstrap columns, where multiple sortable lists are placed in one column, and a list of draggables is placed in another.

Example

Here's a working example snippet

$('.sortable').sortable({
  connectWith: '.sortable',
  revert: 600,
  forcePlaceholderSize: true,
  placeholder: 'ui-sortable-placeholder',
  tolerance: 'pointer'
}).disableSelection();

$('.draggable').draggable({
  connectToSortable: '.sortable',
  helper: 'clone',
  revert: true
}).disableSelection();
.sortable-container {
  display: inline-block;
  width: 100px;
  vertical-align: top;
}
.sortable {
  cursor: move;
  margin: 0;
  padding: 0;
  list-style-type: none;
  vertical-align: top;
  border: 1px solid #000;
}
.ui-sortable-placeholder {
  background: #ff0000;
}
#draggables {
  margin: 0;
  padding: 0;
  list-style-type: none;
}
.draggable {
  margin: 4px;
  cursor: move;
  color: #fff;
  background: #5dd1ff;
}
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.11.3/jquery-ui.min.js"></script>

<div class='container-fluid'>
  <div class="row">
    <div class="col-xs-3">
      <p>foo</p>
      <p>bar</p>
    </div>
    <div class="col-xs-3">
      <p>foo</p>
      <p>bar</p>
    </div>
    <div class="col-xs-6">
      <p>foo</p>
      <p>bar</p>
    </div>
  </div>
  <div class='row'>
    <div class='col-xs-6'>
      <div id='sortables'>
        <div class='sortable-container'>
          <ul class='sortable'>
            <li>sortable 1</li>
            <li>sortable 2</li>
            <li>sortable 3</li>
            <li>sortable 4</li>
            <li>sortable 5</li>
            <li>sortable 6</li>
          </ul>
        </div>
        <div class='sortable-container'>
          <ul class='sortable'>
            <li>sortable 1</li>
            <li>sortable 2</li>
            <li>sortable 3</li>
            <li>sortable 4</li>
            <li>sortable 5</li>
            <li>sortable 6</li>
          </ul>
        </div>
        <div class='sortable-container'>
          <ul class='sortable'>
            <li>sortable 1</li>
            <li>sortable 2</li>
            <li>sortable 3</li>
            <li>sortable 4</li>
            <li>sortable 5</li>
            <li>sortable 6</li>
          </ul>
        </div>
      </div>
    </div>

    <div class='col-xs-6'>
      <ul id='draggables'>
        <li class='draggable'>draggable 1</li>
        <li class='draggable'>draggable 2</li>
        <li class='draggable'>draggable 3</li>
      </ul>
    </div>
  </div>
</div>

Update 16 Nov 2015 I've modified the code example to better reflect my actual usage context, where there are further rows above the one that holds the draggables/sortables.

a screencast, and a still image showing what happens

enter image description here

Further explanation

When dragging one of the draggables from right hand side column over one of the sortable lists on the left hand side, and not dropping it, but dragging it further out of the sortable lists bounding box, the helper is being positioned incorrectly, it shifts a few hundred pixels to the left, as if it's incorrectly incorporating some kind of offset (looks like it could be the the original draggable position).

Interestingly this doesn't occur when the draggables are placed in the same parent element as the sortables, at least it won't shift horizontally, but vertically when moving the draggable fast up/down or left/right in/out of the sortable list.

The horizontal shift is somehow related to floated/relative positioned parent elements, disabling the floating or the relative positioning fixes it. However, I'd like to keep this as is, and find another fix/workaround instead. The vertical shifting also happens when floating/relative positioning isn't involved, so I guess there's a little more to this problem.

Update 15 Nov 2015 - jQuery UI changes

Looks like jQuery UI 1.11.4 changed the behavior a little, now it won't immediately shift horizontally the moment you leave the bounding box of a sortable, but you have to move between two or more sortables first. Other than that, the buggy behavior seems to be unchanged.

Update 16 Nov 2015 - appendTo option

Initially I've tried to use appendTo a workaround, as that way the helper would be kept outside of the lists, and while this is true for my original example code, it won't work with the updated example code, where further rows are placed above the ones that holds the draggables/sortables, they are causing the helper to shift vertically.

Question

Does anyone know where the problematic horizontal offset stems from exactly, and how to fix it while keep using floated/relative positioned parent elements?

And what about the vertical offsetting, seeing that this happens in the jQuery UI demo too, makes me think that this is a bug that isn't related to the styling of parent elements?

Update 15 Nov 2015 - Vertical offset problem located

The vertical offset seems to have to do with the margin applied on the draggables, without that it seems to work fine.

I've reported both as bugs, but I'm still looking for a fix/workaround that I can apply myself until this may or may not be fixed in the library.

http://bugs.jqueryui.com/ticket/14822
http://bugs.jqueryui.com/ticket/14806

like image 204
ndm Avatar asked Nov 13 '15 00:11

ndm


2 Answers

What is strange is that it seems to work better with jquery-ui 10.4. The difference is that in 10.4 the draggable helper stays in its original div, and is cloned into the sortables but hidden. So calculations are easier to make.

In 11.4, the helper is appended to the sortable over which it is dragged, which makes the precise offset calculations hard to follow. You constantly have to change the parent offset, and keep track of over which sortable it is, over which sortable it was, the position of the sortable and so on. And clearly there is a bug there.

One simple solution would be to get the connectToSortable plugin from 10.4. You'll have to check for unwanted side effects, but quickly it seems to be working. You can use a different name so that you keep the original. Like this:

$.ui.plugin.add("draggable", "connectToSortable104", {
    // You take the whole connectToSortable plugin from  
    // here: https://code.jquery.com/ui/1.10.4/jquery-ui.js  
    // In 11.4 you'll need to add draggable parameter 
    // for example: to the event methods,
    // start: function(event, ui, draggable)
    ...

See http://jsfiddle.net/gsnojkbc/2/

EDIT:

I don't think the additional div is what causes the problem, it's really a bug with the way the connectToSortable works in jquery 11.4 that is causing the issue. To allow moving the helper in the sortables and still keep track of the proper offset, you need to readjust some data each time the helper changes div. There's two flaws in the way it's done:

First one is that there's a refreshOffsets method that is common to other events in draggable. It's used for example when you click on a draggable. And so it tries to calculate the offset based on the click. But when calling refreshOffsets from the sortable event, it messes the click offset. This can be solved by changing refreshOffsets method so as not to consider the event.pageX and Y. Like this:

$.ui.draggable.prototype._refreshOffsetsSortable = function(event, draggable){

        this.offset = {
                top: this.positionAbs.top - this.margins.top,
                left: this.positionAbs.left - this.margins.left,
                scroll: false,
                parent: this._getParentOffset(),
                relative: this._getRelativeOffset()
            };

            this.offset.click = draggable.offset.click;
    }

The other problem happens because you have many sortables. Basically the other operation that needs to be done is change the parent offset. The way it's done right now is that it saves the previous parent. Normally it works but if you move too fast, the sequence makes it so that the saved parent is a sortable and not the original parent. You can fix this by saving the parent on drag start, which in any case makes seems to make more sense. Like this:

$.ui.plugin.add( "draggable", "connectToSortableFixed", {
    start: function( event, ui, draggable ) {
        var uiSortable = $.extend( {}, ui, {
            item: draggable.element
        });
        draggable._parent = this.parent();
...

See here: http://jsfiddle.net/24a8q49j/1/

like image 163
Julien Grégoire Avatar answered Sep 23 '22 06:09

Julien Grégoire


A simple JavaScript only workaround is to store the offset to the mouse when the dragging starts and then force the ui.helper to have the same offset to the mouse - always:

[...]
var offset_start = {};

$('.draggable').draggable({
  connectToSortable: '.sortable',
  helper: 'clone',
  revert: true,
  start: function(e, ui) {
      offset_start = {
          x: ui.position.left - e.pageX,
          y: ui.position.top - e.pageY
      }
  },
  drag: function(e, ui) {
      ui.position.top = e.pageY + offset_start.y
      ui.position.left = e.pageX + offset_start.x
  }
}).disableSelection();

See the updated jsfiddle.

For the simple case this is an easy workaround, but requires more work if you introduce borders (where the helper cannot leave) etc.


Edit: For your snippet I came up with another solution (because the other one does not work as expected - harder to get the initial offsets than I thought ...): Simply force the item to stay at the mouse pointer regardless of the initial offset after appending the helper to the body. It is not 100% nice, because when starting to drag it might "jump" to the mouse - but then it stays there at least ...

$('.draggable').draggable({
  connectToSortable: '.sortable',
  helper: 'clone',
  revert: 'invalid',
  appendTo: 'body',
  drag: function(e, ui) {
    if (!ui.helper.parent().is('body')) {
      ui.helper.appendTo($('body'));
    }
    ui.position.top = e.pageY - 10;
    ui.position.left = e.pageX - 25;
  }
}).disableSelection();

$('.sortable').sortable({
  connectWith: '.sortable',
  revert: 600,
  forcePlaceholderSize: true,
  placeholder: 'ui-sortable-placeholder',
  tolerance: 'pointer'
}).disableSelection();


$('.draggable').draggable({
  connectToSortable: '.sortable',
  helper: 'clone',
  revert: 'invalid',
  appendTo: 'body',
  scroll: false,
  drag: function(e, ui) {
    if (!ui.helper.parent().is('body')) {
      ui.helper.appendTo($('body'));
    }
    ui.position.top = e.pageY - 15;
    ui.position.left = e.pageX - 25;
  }
}).disableSelection()
.sortable-container {
  display: inline-block;
  width: 100px;
  vertical-align: top;
}
.sortable {
  cursor: move;
  margin: 0;
  padding: 0;
  list-style-type: none;
  vertical-align: top;
  border: 1px solid #000;
}
.ui-sortable-placeholder {
  background: #ff0000;
}
#draggables {
  margin: 0;
  padding: 0;
  list-style-type: none;
}
.draggable {
  margin: 4px;
  cursor: move;
  color: #fff;
  background: #5dd1ff;
}
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.11.3/jquery-ui.min.js"></script>

<div class='container-fluid'>
  <div class="row">
    <div class="col-xs-3">
      <p>foo</p>
      <p>bar</p>
    </div>
    <div class="col-xs-3">
      <p>foo</p>
      <p>bar</p>
    </div>
    <div class="col-xs-6">
      <p>foo</p>
      <p>bar</p>
    </div>
  </div>
  <div class='row'>
    <div class='col-xs-6'>
      <div id='sortables'>
        <div class='sortable-container'>
          <ul class='sortable'>
            <li>sortable 1</li>
            <li>sortable 2</li>
            <li>sortable 3</li>
            <li>sortable 4</li>
            <li>sortable 5</li>
            <li>sortable 6</li>
          </ul>
        </div>
        <div class='sortable-container'>
          <ul class='sortable'>
            <li>sortable 1</li>
            <li>sortable 2</li>
            <li>sortable 3</li>
            <li>sortable 4</li>
            <li>sortable 5</li>
            <li>sortable 6</li>
          </ul>
        </div>
        <div class='sortable-container'>
          <ul class='sortable'>
            <li>sortable 1</li>
            <li>sortable 2</li>
            <li>sortable 3</li>
            <li>sortable 4</li>
            <li>sortable 5</li>
            <li>sortable 6</li>
          </ul>
        </div>
      </div>
    </div>

    <div class='col-xs-6'>
      <ul id='draggables'>
        <li class='draggable'>draggable 1</li>
        <li class='draggable'>draggable 2</li>
        <li class='draggable'>draggable 3</li>
      </ul>
    </div>
  </div>
</div>
like image 25
Markus Avatar answered Sep 23 '22 06:09

Markus