如何为连接的可排序对象修复错误定位的可拖动助手(部分原因是 floated/relative 定位的父元素)?

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

前言

我遇到了一个问题,当使用放置在浮动的、相对定位的父元素中的可拖动 + 可排序时,可拖动助手被错误地偏移。浮动的父元素是 Bootstrap 列,其中多个可排序列表放在一列中,而可拖动列表放在另一列中。

示例

这是一个有效的示例片段

$('.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>

2015 年 11 月 16 日更新 我修改了代码示例以更好地反映我的实际使用上下文,上面还有更多行持有 draggables/sortables.

的那个

a screencast,以及显示发生情况的静止图像

进一步说明

当将右侧栏中的一个可拖动对象拖到左侧的一个可排序列表上,而不是放下它,而是将其进一步拖出可排序列表边界框时,助手正在定位错误地,它向左移动了几百个像素,就好像它错误地包含了某种偏移量(看起来它可能是原始的可拖动位置)。

有趣的是,当可拖动元素与可排序元素放置在同一父元素中时,不会发生这种情况,至少它不会水平移动,但在快速移动可拖动元素时会垂直移动 up/down 或 left/right in/out 的可排序列表。

水平移动在某种程度上与 floated/relative 定位的父元素相关,禁用浮动或相对定位修复它。但是,我想保持原样,并找到另一个 fix/workaround。当不涉及 floating/relative 定位时,也会发生垂直移动,所以我想这个问题还有一些。

2015 年 11 月 15 日更新 - jQuery UI 更改

看起来 jQuery UI 1.11.4 稍微改变了行为,现在它不会在你离开可排序的边界框时立即水平移动,但你必须移动首先在两个或多个可排序对象之间。除此之外,错误行为似乎没有变化。

2015 年 11 月 16 日更新 - appendTo 选项

最初我尝试使用 appendTo 一种变通方法,因为这样助手将被保留在列表之外,虽然这对于我的原始示例代码是正确的,但它不适用于更新的示例代码,其中更多行放置在包含 draggables/sortables 的行上方,它们导致助手垂直移动。

问题

有谁知道有问题的水平偏移的确切根源在哪里,以及如何在继续使用 floated/relative 定位的父元素的同时修复它?

关于垂直偏移,看到这也发生在 the jQuery UI demo 中,让我认为这是一个与父元素的样式无关的错误?

2015 年 11 月 15 日更新 - 找到垂直偏移问题

垂直偏移似乎与 draggables 上应用的边距有关,没有它似乎工作正常。

我已将这两个问题报告为错误,但我仍在寻找 fix/workaround 我可以自己应用,直到这个问题可能会或可能不会在库中得到修复。

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

https://jsfiddle.net/tsLcjw9c/1/

我在 draggables 列表上设置了固定宽度 (100px),以便 draggable 助手与列表项具有相同的宽度,这样可以防止初始水平偏移。

你在可排序容器上拖动时得到的大水平偏移似乎确实是 jquery UI 中的一个错误,但它似乎也恰好是可排序容器宽度的 100% ,这就是我添加以下 css:

的原因
.sortable .ui-draggable-dragging:not(.ui-sortable-helper){
    margin-left:100%;
}

您在进入和离开可排序列表后得到的垂直偏移似乎是由可拖动项目的 4px 边距引起的,我将其删除。您可以使用透明边框和 box-sizing:border-box; 达到相同的效果;或者添加填充并将背景剪辑设置为填充框。

最后,您希望在放下项目时发生的 600 毫秒动画在我的浏览器中以一种奇怪的方式发生 (Chrome),它在最终动画到正确的位置之前去了 2 个不同的位置。这就是我现在禁用动画的原因。您可以尝试覆盖 sortable jQuery 中的 drop 事件,以便您手动向正确的位置设置动画。

这里是 jsfiddle 代码:

html

<div class='container-fluid'>
  <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>

jquery

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

$('.draggable').draggable({
  connectToSortable: '.sortable',
  helper: 'clone',
  revert: false
}).disableSelection();

css

.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 {
  cursor: move;
  color: #fff;
  background: #5dd1ff;
    width:100px;
    max-width:100%;
}
.sortable .ui-draggable-dragging:not(.ui-sortable-helper){
    margin-left:100%;
}

奇怪的是它似乎与 jquery-ui 10.4 配合使用效果更好。不同之处在于,在 10.4 中,可拖动助手 保留在其原始 div 中,并被克隆到 sortables 中但被隐藏了。所以计算更容易。

在 11.4 中,helper 被附加到它被拖动到的 sortable 上,这使得精确的偏移量计算难以遵循。您必须不断更改 parent 偏移量,并跟踪它是哪个可排序的,它是哪个可排序的,可排序的位置等等。显然那里有一个错误。

一个简单的解决方案是从 10.4 获取 connectToSortable 插件。您必须检查不需要的副作用,但很快它似乎就起作用了。您可以使用不同的名称以保留原始名称。像这样:

$.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)
    ...

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

编辑:

我不认为额外的 div 是导致问题的原因,它确实是 connectToSortable 在 jquery 11.4 中工作方式的错误这是导致问题的原因。为了允许在 sortables 中移动助手并仍然跟踪正确的偏移量,您需要在每次助手更改时重新调整一些数据 div。它的完成方式有两个缺陷:

第一个是有一个 refreshOffsets 方法 可拖动中的其他事件。例如,当您单击 可拖动。所以它试图根据 点击。但是当从可排序事件调用 refreshOffsets 时,它 弄乱点击偏移量。这可以通过改变来解决 refreshOffsets方法以免考虑event.pageX和Y。 像这样:

$.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;
    }

另一个问题的发生是因为您有很多 sortables。基本上 需要做的另一项操作是更改 parent 抵消。现在的做法是保存之前的 parent。通常它可以工作,但如果你移动得太快,序列 使得保存的 parent 是 sortable 而不是原始的 parent。您可以通过在拖动开始时保存 parent 来解决此问题,这在 任何情况似乎都更有意义。像这样:

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

看这里:http://jsfiddle.net/24a8q49j/1/

一个简单的 JavaScript 唯一解决方法是在拖动开始时将偏移量存储到鼠标,然后强制 ui.helper 具有与鼠标相同的偏移量 - 始终:

[...]
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();

参见updated jsfiddle

对于简单的情况,这是一个简单的解决方法,但如果您引入边界(助手不能离开的地方)等,则需要做更多的工作。


编辑:对于你的代码片段,我提出了另一种解决方案(因为另一个没有按预期工作 - 比我想象的更难获得初始偏移...):只需强制项目留在鼠标上将助手添加到正文后,无论初始偏移量如何。它不是 100% 好,因为开始拖动时它可能 "jump" 到鼠标 - 但它至少停留在那里 ...

$('.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>