如何修复连接的可排序列表中错误定位的可拖动帮助器(部分原因是由浮动/相对定位的父元素引起的)?

19

前言

我遇到了一个问题,当使用放置在浮动、相对定位的父元素中的可拖拽+可排序元素时,可拖拽助手被错误地偏移。 浮动的父元素是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日更新 我已修改代码示例,以更好地反映我的实际使用情况,在拖动项/可排序项所在行的上方还有其他行的情况下。

一个屏幕录像和一张静态图片显示了发生的情况

enter image description here

进一步解释

当从右侧列拖动其中任意一个可拖动项到左侧可排序列表之一时,不放置,而是将其拖离可排序列表的边界框时,助手的位置会出现错误,它会向左移动几百个像素,就好像它错误地合并了某种偏移量(看起来像是原始可拖动项的位置)。

有趣的是,当可拖动项与可排序项放置在同一个父元素中时,水平方向上不会移位,但是在可排序列表中快速上下或左右移动可拖动项时,会发生垂直方向的移位。

水平移位与浮动/相对定位的父元素有关,禁用浮动或相对定位可以修复它。但是,我想保留这种状态,并找到另一种修复/解决方法。即使没有浮动/相对定位,垂直移位也会发生,所以我猜这个问题还有点其他问题。

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

看起来jQuery UI 1.11.4对行为进行了一些更改,现在当您离开可排序列表的边框时,它不会立即水平移位了,而是必须在两个或多个可排序项之间移动。除此之外,错误的行为似乎没有改变。

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

最初我尝试使用appendTo作为解决方法,因为那样助手将被保持在列表之外,虽然在我的原始示例代码中这是正确的,但在更新的示例代码中不起作用,因为拖动项/可排序项所在的行之上还有其他行,这些行会导致助手在垂直方向上移位。

问题

有人知道水平偏移的问题出现在哪里,如何在保持浮动/相对定位的父元素的同时修复它吗?

垂直偏移问题是什么原因呢? 这似乎是一个与父元素样式无关的错误,因为它也在jQuery UI演示中发生。

2015年11月15日更新 - 定位垂直偏移问题位置

垂直偏移似乎与拖动元素应用的外边距有关,如果没有外边距,它似乎可以正常工作。

我已经报告了这两个问题,但我仍在寻找一种修复/解决方法,以便在库中可能或可能不会修复这个问题之前自行应用。

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


你想要实现类似WordPress管理菜单的拖拽和排序功能吗? - Mitul
@Mitul,我不确定你具体指的是什么,但实际上这是一个类似日历的应用程序,预定义的元素可以被拖放到由多个预定义段(可排序)组成的日期中。然而,这并不重要,最终只是关于可拖动和可排序的分离浮动元素,因为我需要保持这种结构。 - ndm
你能展示一下当你拖动那个元素后期望的输出是什么吗?我已经看到了你的屏幕,最后部分展示了可拖拽的三号元素在三个可排序元素的下方,所以你想要把可拖拽元素展示在那里? - Mitul
@Mitul 预期的结果是在拖动时将可拖动助手保持在指针下方,并最终放置在可排序区域中(这可能会发生一次或多次,具体取决于它在应用程序中所代表的内容)。后者可以正常工作(请参见工作示例),唯一的问题是在拖动操作期间助手被错位。顺便说一句,我今晚必须离开,明天我可以回答更多问题。 - ndm
我曾经遇到过完全相同的问题。最终因为太多缺陷而采用了另一种方法。 - oshell
3个回答

6
奇怪的是,它似乎与jquery-ui 10.4一起使用效果更好。区别在于,在10.4中,可拖动的helper停留在其原始div中,并被克隆到可排序的div中但隐藏起来,因此计算更容易进行。
在11.4中,helper附加到它所拖动的可排序的div上,这使得精确的偏移量计算难以跟踪。您不断需要更改父级偏移量,并跟踪它所在的可排序的div,它曾经在哪个可排序的div上,可排序的div的位置等等。显然,存在一个错误。
一个简单的解决方案是从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并不是引起问题的原因,实际上是jquery 11.4中的connectToSortable的一个bug导致了这个问题。为了允许在sortables中移动helper并仍然跟踪正确的偏移量,每次helper更改div时都需要重新调整一些数据。它的做法有两个缺陷:

第一个是有一个在draggable中常见的refreshOffsets方法。例如当你点击一个draggable时。因此,它试图根据单击来计算偏移量。但是,从sortable事件调用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;
    }

另一个问题是由于您有很多可排序的元素。基本上,需要完成的其他操作是更改父级偏移量。目前的处理方式是保存先前的父级。通常情况下工作正常,但如果您移动得太快,序列会导致保存的父级是可排序元素而不是原始父级。您可以通过在拖动开始时保存父级来解决此问题,这样做似乎更合理。像这样:

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

现在这是一个有趣的观察。我尝试解决这个问题的第一件事是使用appendTo选项,以便将helper保持在列表之外,但它没有起作用,helper仍然被放错了位置。我现在已经尝试过我的和你的示例,它似乎完全正常工作。当我回到家中的机器时,我将检查我的应用程序上下文是否还有更多问题。 - ndm
好的,我找到了关于appendTo的问题,在我的应用程序中有更多的行(我将更新我的示例代码),这会导致助手在垂直方向上移动。进一步的测试表明,当整个内容本身嵌套在另一行/列中时(在我的应用程序中不是这种情况),它也会再次水平移动。然而,似乎将其附加到可排序容器(#sortables)中可以解决问题,但我还需要进行进一步的测试。暂时来说,请接受我的点赞,您的建议似乎也很好用,并引导我找到了可能的appendTo解决方法 :) - ndm
谢谢您进一步的见解,这让我对核心实际发生的事情有了很好的想法 :) 至于额外的div(行),我并不是说它们对整个问题负责,只是在使用appendTo选项时会受到它们的影响,导致错误行为。 - ndm

5

一个简单的纯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();

请查看更新后的 jsfiddle
对于简单的情况,这是一个容易的解决方法,但如果引入边框(helper 不能离开的地方),则需要更多的工作。
编辑:针对您的片段,我想出了另一种解决方法(因为另一种方法没有按预期工作——获取初始偏移量比我想象的要困难得多):在将 helper 添加到 body 后,强制该项目保留在鼠标指针处,而不管最初的偏移量如何。 这不是百分之百好的解决方案,因为开始拖动时它可能会“跳”到鼠标 - 但然后它至少会停留在那里...
$('.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>


这是一个不错的技巧,看起来在appendTo无法工作的情况下可能是一个可行的解决方法... 说到appendTo,此外使用appendTo: 'body'可以修复不正确的可拖动恢复(不是在列表中来回拖放,而只是“返回到可拖动项”)。 - ndm
很好,我已添加了这个选项。现在当拖放到列表中时,把前后动画作为一个很酷的功能展示给你的用户(这不是一个错误,而是一种功能),然后每个人都会很高兴;-)(直到它被上游修复) - Markus
我添加了 revert: 'invalid' - 现在它不再来回跳动了 - 编辑: 好吧,至少有时候...;-)(看起来jqueryUI的可拖动插件并不是最稳定和确定性的插件) - Markus
是的,它确实非常不稳定...最初我尝试使用回调来处理还原,因为invalid没有按预期工作,我注意到传递给函数的参数(应该是成功拖放时的目标参考)在从一个列表快速移动到另一个列表时经常是false,所以这是另一个错误报告吧。 - ndm

1

请查看 https://jsfiddle.net/tsLcjw9c/1/

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

在可排序容器上拖动时出现的大水平偏移确实似乎是jquery UI中的一个bug,但它也似乎恰好等于可排序容器的宽度的100%,因此我添加了以下css:

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

在进入和离开排序列表后得到的垂直偏移似乎是由于可拖动项的4像素边距引起的,我已将其删除。您可以通过使用透明边框和box-sizing:border-box;达到相同的效果,或者添加填充并将background-clip设置为padding-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%;
}

你所指的“_initial offset_”是什么?可拖动/辅助宽度似乎与任何偏移量都没有关系,至少在我的测试中不是这样。不幸的是,“100% margin workaround”似乎不能可靠地工作,应用此方法后,辅助有时会向另一个方向移动,即向右(http://i.imgur.com/0uiZv3k.gif)。 - ndm
哦,我想我测试得不够彻底。我所指的初始偏移是因为当我运行代码时,可拖动的列表项很宽(块状显示),并且在拖动处理程序时其宽度是文本的宽度,这就是为什么它会获得偏移量,因为宽度不相同。 - Hacktisch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接