当悬停在子元素上时,HTML5 dragleave 事件会被触发。

441
我遇到的问题是,当鼠标悬停在元素的子元素上时,该元素的`dragleave`事件触发了。而且,再次悬停在父元素上时,`dragenter`也没有触发。
我创建了一个简化版本的代码片段:http://jsfiddle.net/pimvdb/HU6Mk/1/
HTML:
<div id="drag" draggable="true">drag me</div>

<hr>

<div id="drop">
    drop here
    <p>child</p>
    parent
</div>

使用以下JavaScript代码:

$('#drop').bind({
                 dragenter: function() {
                     $(this).addClass('red');
                 },

                 dragleave: function() {
                     $(this).removeClass('red');
                 }
                });

$('#drag').bind({
                 dragstart: function(e) {
                     e.allowedEffect = "copy";
                     e.setData("text/plain", "test");
                 }
                });
当拖曳物件到区域内时,它会让下拉式的div变成红色通知用户。然而如果你拖曳到子元素p中,dragleave事件就会被触发,导致div不再是红色的了。即使回到下拉式的div也不会再次变成红色。必须完全移出下拉式的div并再次拖曳回来才能将其变为红色。
是否有可能防止在拖动进入子元素时触发dragleave事件?
2017更新:简而言之,在现代浏览器和IE11中工作的H.D.所描述的方法是查找CSS pointer-events:none;

3
@ajm:谢谢,这在某种程度上起作用。然而,在Chrome中,进入或离开子元素时会出现闪烁,可能是因为在这种情况下仍会触发 dragleave - pimvdb
@fguillen:很抱歉,这与 jQuery UI 没有任何关系。事实上,甚至不需要使用 jQuery 就能触发此错误。我已经提交了一个 WebKit 错误报告,但目前还没有更新。 - pimvdb
@pimvdb,是的,我在我的 bug 中看到了答案,那个链接是你的 WebKit bug 吗?无论如何,我可以用 FireFox 复现同样的 bug :/ - fguillen
在这个解决方案中没有涉及任何CSS,感觉更加直接。使用指针事件完成了它,我现在更喜欢计数器选项。 - brittongr
1
我发现这个非常烦人的问题的最简单的解决方案是,仅侦听元素上的“enter”事件,并在事件触发时,在给定元素上创建一个绝对定位的覆盖层,只使用“leave”事件侦听器。这样可以避免在子元素上禁用指针事件(覆盖层接管每个拖动事件),并确保在应该时触发“leave”。我在vue组件中遇到了这个问题:始终在“enter”后立即触发“leave”,但实际上不知道为什么(子元素将指针事件设置为none)。 - Przemysław Melnarowicz
显示剩余3条评论
41个回答

467

你只需要保持一个引用计数器,在获取dragenter时增加它,在获取dragleave时减少它。当计数器为0时,移除该类。

var counter = 0;

$('#drop').bind({
    dragenter: function(ev) {
        ev.preventDefault(); // needed for IE
        counter++;
        $(this).addClass('red');
    },

    dragleave: function() {
        counter--;
        if (counter === 0) { 
            $(this).removeClass('red');
        }
    }
});

注意:在拖放事件中,将计数器重置为零,并清除添加的类。

您可以在这里运行它。


10
天哪,这是最显而易见的解决方案,只有一个投票......拜托各位,你们可以更好。我曾经也想过这个,但在看到前几个答案的复杂程度后,我差点放弃了它。你有什么缺点吗? - Arthur Corenzan
14
当拖动的元素边缘碰到另一个可拖动元素的边缘时,这种方法不起作用。例如,在可排序列表中,向下拖动元素时,经过下一个可拖动项时,计数器不会归零,而是卡在1。但如果我将它向侧面拖动,使其脱离其他可拖动元素,则它可以正常工作。我使用了子元素上的 pointer-events: none。当我开始拖动时,我附加了一个具有此属性的类,当拖动结束时,我删除了该类。在Safari和Chrome上工作得很好,但在Firefox上不行。 - Arthur Corenzan
29
这个方法在所有浏览器中都有效,但在Firefox中无效。我的新解决方案可以在我的情况下在所有地方运作。在第一个 dragenter 事件中,我将 event.currentTarget 保存在一个新变量 dragEnterTarget 中。只要 dragEnterTarget 被设置,我就会忽略后续的 dragenter 事件,因为它们是来自子元素的。在所有 dragleave 事件中,我会检查 dragEnterTarget === event.target。如果为假,则将忽略此事件,因为它由子元素触发。如果为真,则重置 dragEnterTargetundefined - Pipo
5
好的解决方案。我需要在处理接受拖放的函数中将计数器重置为0,否则后续的拖动操作无法按预期工作。 - Liam Johnston
2
@Woody 我在这里的大量评论中没有看到他的评论,但是,那就是解决方法。为什么不将它合并到你的回答中呢? - B T
显示剩余28条评论

176

当拖动到子元素时,是否可能防止dragleave事件触发?

是的。

#drop * {pointer-events: none;}

这个 CSS 在 Chrome 中似乎已经足够了。

然而,在 Firefox 中使用时,#drop 内部不应该直接包含文本节点(否则会出现奇怪的问题,其中一个元素 "离开了它自己"),所以建议只留下一个元素(例如,在 #drop 中使用 div 将所有东西放在里面)。

这里有一个 jsfiddle 解决了原问题(错误)的示例

我还制作了一个简化版本,从@Theodore Brown示例中派生出来,但仅基于此 CSS。

然而,并非所有浏览器都实现了这个 CSS:http://caniuse.com/pointer-events

通过查看 Facebook 的源代码,我发现多次使用了 pointer-events: none;,但它可能与优雅降级回退一起使用。至少它很简单,可以解决很多环境下的问题。


3
使用pointer-events确实是一个很好的答案,我在自己找到答案之前挣扎了一会儿,这个回答应该更高。 - floribon
14
如果你的孩子是一个按钮,那会怎样?o.O - David
19
只有在该区域内没有其他控制元素(例如编辑、删除)时,此解决方案才有效,因为该解决方案也会阻止它们的使用。 - Zoltán Süle
7
对于在放置目标内的交互式子元素,例如按钮:添加pointer-events: none;到一个类中。使用_ondragenter_将该类应用于放置目标。然后在_ondragleave_中从放置目标中移除该类。 - sudoqux
2
这几乎是完美的...只有一个例外:如果子元素是类型为“checkbox”或“radio”的“input”,那么“pointer-events: none”将没有效果。这是因为“pointer-events”仅限于导致元素值更改的事件。在“checkbox”和“radio”的情况下,它们会更改其状态而不是值。 - Jack_Hu
显示剩余10条评论

85

这个问题已经问了相当长的时间,并提供了许多解决方案(包括丑陋的黑客)。我最近遇到的同样的问题得到了这个答案的帮助,我想这可能对浏览此页面的某些人有所帮助。 整个思路是在任何父元素或子元素上调用ondrageenter时将event.target存储起来。然后在ondragleave中检查当前目标(event.target)是否等于您在ondragenter中存储的对象。

这两种情况仅在拖动离开浏览器窗口时才匹配。

这个方法能够正常工作的原因是当鼠标离开一个元素(比如el1)并进入另一个元素(比如el2)时,先调用el2.ondragenter然后再调用el1.ondragleave。只有当拖动离开/进入浏览器窗口时,el2.ondragenterel1.ondragleave中的event.target都将是''

这是我的工作示例。我已经在IE9+、Chrome、Firefox和Safari上进行了测试。

(function() {
    var bodyEl = document.body;
    var flupDiv = document.getElementById('file-drop-area');

    flupDiv.onclick = function(event){
        console.log('HEy! some one clicked me!');
    };

    var enterTarget = null;

    document.ondragenter = function(event) {
        console.log('on drag enter: ' + event.target.id);
        enterTarget = event.target;
        event.stopPropagation();
        event.preventDefault();
        flupDiv.className = 'flup-drag-on-top';
        return false;
    };

    document.ondragleave = function(event) {
        console.log('on drag leave: currentTarget: ' + event.target.id + ', old target: ' + enterTarget.id);
        //Only if the two target are equal it means the drag has left the window
        if (enterTarget == event.target){
            event.stopPropagation();
            event.preventDefault();
            flupDiv.className = 'flup-no-drag';         
        }
    };
    document.ondrop = function(event) {
        console.log('on drop: ' + event.target.id);
        event.stopPropagation();
        event.preventDefault();
        flupDiv.className = 'flup-no-drag';
        return false;
    };
})();

这里是一个简单的 HTML 页面:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Multiple File Uploader</title>
<link rel="stylesheet" href="my.css" />
</head>
<body id="bodyDiv">
    <div id="cntnr" class="flup-container">
        <div id="file-drop-area" class="flup-no-drag">blah blah</div>
    </div>
    <script src="my.js"></script>
</body>
</html>

通过适当的样式设计,我所做的是,当文件被拖入屏幕时,使内部div (#file-drop-area)变得更大,以便用户可以轻松地将文件放置在正确的位置。


6
这是最佳解决方案,比计数器更好(特别是如果您委派事件),而且它也适用于可拖动的子元素。 - Fred
5
这对于重复的dragenter事件问题并没有帮助。 - B T
1
这对于 CSS 伪元素或伪类中的“子元素”有效吗?我无法做到,但也许是我的方法不对。 - Josh Bleecher Snyder
1
截至2020年3月,我也遇到了最好的解决方案。注意:一些代码检查工具可能会抱怨使用==而不是===。但是,由于你正在比较事件目标对象引用,所以===也可以正常工作。 - Alex
2
优秀的解决方案 - 使用React很简单,只需跟踪一个新状态以引用onDragEnter并在onDragLeave中进行检查即可。这应该是被接受的答案。 - Justin K
2
到目前为止,最好的答案是2022年了。谢谢@Ali Motevallian。 - Adam Orłowski

61

这里提供了最简单的跨浏览器解决方案(真的):

jsfiddle <-- 尝试将某个文件拖到框内

你可以像这样做:

var dropZone= document.getElementById('box');
var dropMask = document.getElementById('drop-mask');

dropZone.addEventListener('dragover', drag_over, false);
dropMask.addEventListener('dragleave', drag_leave, false);
dropMask.addEventListener('drop', drag_drop, false);
简单来说,您在拖放区内创建一个“遮罩”,宽度和高度继承,位置为绝对定位,只有当拖放开始时才会显示。
因此,在显示该遮罩后,您可以通过将其他的dragleave和drop事件附加到该遮罩上来进行操作。

离开或放置后,您只需再次隐藏遮罩。
简单而没有复杂性。

(注:Greg Pettit建议--您必须确保遮罩覆盖整个框,包括边框)


3
实际上是边框的问题。让口罩重叠边框,没有自己的边框,应该就可以正常使用。 - Greg Pettit
1
注意,你的 jsfiddle 存在一个 bug,在 drag_drop 中,你应该移除 "#box-a" 上的 hover 类而不是 "#box"。 - entropy
这是一个不错的解决方案,但不知何故对我无效。最终我想出了一个变通方法。对于那些正在寻找其他东西的人,你可以尝试这个:https://github.com/bingjie2680/jquery-draghover - bingjie2680
不,我不想失去对子元素的控制。我编写了一个简单的jQuery插件来解决这个问题,为我们完成这项工作。查看我的答案 - Luca Fagioli
1
当实际掩码为#drop::before::after时,此解决方案效果很好。还要注意,有时在快速拖动时,“dragleave”会在“dragenter”完成之前触发。如果dragenter添加类/伪元素,而dragleave将其删除,则可能会导致问题。 - le hollandais volant
显示剩余2条评论

57

目前,这种相当简单的解决方案对我来说运作良好,假设您的事件是分别附加到每个拖动元素上的。

if (evt.currentTarget.contains(evt.relatedTarget)) {
  return;
}

太好了!感谢分享,@kenneth,你救了我的一天!很多其他答案只适用于您拥有单个可放置区域的情况。 - antoni
2
对我来说,在Chrome和Firefox中它可以工作,但在Edge中无法工作。请参见:http://jsfiddle.net/iwiss/t9pv24jo/ - iwis
如果您正在拖动一个 input 元素,evt.relatedTarget 指向一个未附加到文档树的 div,因为这个古老的 bug:https://bugs.chromium.org/p/chromium/issues/detail?id=68629。所以检查应该是 evt.relatedTarget.parentElement === null || evt.currentTarget.contains(evt.relatedTarget) - mono blaine
2
是的,我最喜欢这个解决方案。没有奇怪的坐标检查,没有大锤式的CSS,没有指针计数,也没有遮罩元素……这个解决方案优雅地解决了我的问题。 - Snowie

37

解决这个问题的“正确”方法是在放置目标的子元素上禁用指针事件(如@H.D.的答案所示)。这里是我创建的一个jsFiddle,演示了这种技术。不幸的是,在IE11之前的版本中,这种方法无法使用,因为它们不支持指针事件

幸运的是,我能够想出一种解决方法,可以在旧版IE中使用。基本上,它涉及到识别和忽略拖动过程中发生在子元素上的dragleave事件。由于dragenter事件在父节点上的dragleave事件之前被触发在子节点上,因此可以向每个子节点添加单独的事件监听器,从而向放置目标添加或删除“ignore-drag-leave”类。然后,放置目标的dragleave事件监听器可以简单地忽略存在此类时发生的调用。这里有一个演示此解决方法的jsFiddle。已在Chrome、Firefox和IE8+中进行测试并工作正常。

更新:

我创建了一个演示组合解决方案的jsFiddle, 使用特征检测,如果支持指针事件(目前Chrome,Firefox和IE11),则使用指针事件,如果不支持指针事件,则浏览器会回退到将事件添加到子节点上(IE8-10)。


这个答案展示了一个解决不良触发的方法,但完全忽略了“当拖动到子元素时,是否可能防止dragleave被触发?”这个问题。 - H.D.
当从浏览器外部拖动文件时,行为可能会变得奇怪。在Firefox中,我得到了“进入子项->进入父项->离开子项->进入子项->离开子项”,而没有离开父项,这使其保留了“over”类。旧版IE需要一个attachEvent替换addEventListener。 - H.D.
该解决方案强烈依赖于事件冒泡,所有addEventListener中的“false”应被强调为必要的(尽管这是默认行为),因为许多人可能不知道这一点。 - H.D.
那个可拖动的单个元素为放置区域添加了一个效果,但当放置区域用于其他不触发dragstart事件的可拖动对象时,该效果并不会出现。也许将所有东西都作为放置区域来使用以实现拖动效果,同时保留真正的放置区域与其他处理程序即可解决这个问题。 - H.D.
1
@H.D. 我更新了我的答案,提供了使用pointer-events CSS属性来防止Chrome、Firefox和IE11+触发dragleave事件的信息。我还更新了我的另一个解决方法,以支持IE8和IE9,除了IE10。拖放区域效果仅在拖动“拖我”链接时添加是有意义的。其他人可以根据需要自由更改此行为以支持其用例。 - Theodore Brown
这将防止子元素触发“click”事件。 - standac

22

如果您正在使用HTML5,则可以获取父元素的clientRect:

let rect = document.getElementById("drag").getBoundingClientRect();

然后在parent.dragleave()中:

dragleave(e) {
    if(e.clientY < rect.top || e.clientY >= rect.bottom || e.clientX < rect.left || e.clientX >= rect.right) {
        //real leave
    }
}

这里有一个 jsfiddle


2
优秀的答案。 - broc.seib
谢谢伙计,我喜欢纯JavaScript的方法。 - Tim Gerhard
当使用具有border-radius的元素时,将指针移动到靠近角落的位置实际上会离开该元素,但是此代码仍然认为我们在内部(我们已经离开了该元素,但仍在边界矩形内)。然后,dragleave事件处理程序将根本不会被调用。 - Jay Dadhania
这是我认为最强大的方法。我尝试过其他所有解决方案,但根据子元素是什么,它们都会在某些边缘情况下失败。 - Hudon

18

一个非常简单的解决方案是使用pointer-events CSS属性。只需要在每个子元素上的dragstart事件中将其值设置为none。这些元素将不再触发与鼠标相关的事件,因此它们不会捕获鼠标并触发父元素上的dragleave事件。

完成拖动后,别忘了将此属性设置回auto ;)


18

一个简单的解决方案是在子组件中添加CSS规则pointer-events: none,以防止触发ondragleave。示例如下:

function enter(event) {
  document.querySelector('div').style.border = '1px dashed blue';
}

function leave(event) {
  document.querySelector('div').style.border = '';
}
div {
  border: 1px dashed silver;
  padding: 16px;
  margin: 8px;
}

article {
  border: 1px solid silver;
  padding: 8px;
  margin: 8px;
}

p {
  pointer-events: none;
  background: whitesmoke;
}
<article draggable="true">drag me</article>

<div ondragenter="enter(event)" ondragleave="leave(event)">
  drop here
  <p>child not triggering dragleave</p>
</div>


我遇到了同样的问题,你的解决方案非常好。给其他人的提示,这是我的情况:如果您尝试使子元素忽略拖动事件的克隆元素(尝试实现视觉预览)是正在被拖动的元素的克隆,则可以在创建克隆体时在dragstart中使用以下内容: dragged = event.target;clone = dragged.cloneNode();clone.style.pointerEvents = 'none'; - Scaramouche
不错,简单明了的解决方案。大家可以考虑创建一个类 .no-pointer-events {pointer-events: none;},然后将 no-pointer-events 添加到每个子元素中。完成。 - Rob
1
这应该是被接受的答案,它是最简单的,没有任何技巧涉及。 :D - Ambrus Tóth
但是如果子组件是一个链接呢? - Ayyash

16
问题在于当鼠标移动到子元素的前面时,会触发"dragleave"事件。
我尝试了各种方法来检查"e.target"元素是否与"this"元素相同,但都没有得到任何改进。
我解决这个问题的方式有点取巧,但是可以百分之百地解决。
dragleave: function(e) {
               // Get the location on screen of the element.
               var rect = this.getBoundingClientRect();

               // Check the mouseEvent coordinates are outside of the rectangle
               if(e.x > rect.left + rect.width || e.x < rect.left
               || e.y > rect.top + rect.height || e.y < rect.top) {
                   $(this).removeClass('red');
               }
           }

1
谢谢!然而我无法在Chrome中使其工作。你能提供一个可用的fiddle来展示你的hack吗? - pimvdb
3
我在考虑通过检查坐标来完成它。你已经帮我做了大部分工作,谢谢 :). 不过我需要做一些调整:if (e.x >= (rect.left + rect.width) || e.x <= rect.left || e.y >= (rect.top + rect.height) || e.y <= rect.top) - Christof
1
它在Chrome中无法工作,因为它的事件没有e.xe.y - Hengjie
1
我喜欢这个解决方案。一开始在Firefox中没有起作用。但是如果你将e.x替换为e.clientX,将e.y替换为e.clientY,它就可以工作了。在Chrome中也可以工作。 - Nicole Stutz
1
类似于这样,我将整个文档用作我的拖放区域,因此我只需检查 if (evt.x === 0 && evt.y === 0),在Chrome中似乎完美运行。 - FiniteLooper
显示剩余4条评论

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