当悬停在子元素上时,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个回答

16

非常简单的解决方案:

parent.addEventListener('dragleave', function(evt) {
    if (!parent.contains(evt.relatedTarget)) {
        // Here it is only dragleave on the parent
    }
}

2
这在Safari 12.1中似乎无法工作:https://jsfiddle.net/6d0qc87m/ - Adam Taylor

8

我遇到了相同的问题并尝试使用pk7s的解决方案。它有效,但可以更好地完成,而不需要任何额外的dom元素。

基本上,思路是相同的 - 在可投放区域上添加一个额外的不可见覆盖层。只是我们可以在没有任何额外dom元素的情况下实现这一点。这就是CSS伪元素发挥作用的部分。

Javascript

var dragOver = function (e) {
    e.preventDefault();
    this.classList.add('overlay');
};

var dragLeave = function (e) {
    this.classList.remove('overlay');
};


var dragDrop = function (e) {
    this.classList.remove('overlay');
    window.alert('Dropped');
};

var dropArea = document.getElementById('box');

dropArea.addEventListener('dragover', dragOver, false);
dropArea.addEventListener('dragleave', dragLeave, false);
dropArea.addEventListener('drop', dragDrop, false);

CSS

这个 after 规则将为可放置区域创建一个完全覆盖的叠加层。

#box.overlay:after {
    content:'';
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    z-index: 1;
}

这里是完整的解决方案:http://jsfiddle.net/F6GDq/8/。希望它能帮助到有同样问题的人。

1
偶尔在Chrome上无法正常工作(无法正确捕获dragleave)。 - Timo Huovinen
1
#box 必须设置为相对定位才能正确工作;但是我成功地得到了这个解决方案,非常干净利落。谢谢! - nephiw

7

您可以从jQuery源代码获得一些灵感,在Firefox中修复它:

dragleave: function(e) {
    var related = e.relatedTarget,
        inside = false;

    if (related !== this) {

        if (related) {
            inside = jQuery.contains(this, related);
        }

        if (!inside) {

            $(this).removeClass('red');
        }
    }

}

很遗憾,这个方法在Chrome浏览器中无法工作,因为在dragleave事件中貌似不存在relatedTarget,我猜你使用的是Chrome浏览器,因为你的示例在Firefox中不起作用。点击此处查看包含以上代码的版本。


非常感谢,但实际上我正在尝试在Chrome中解决这个问题。 - pimvdb
5
我看到你已经记录了一个错误(bug),在这里我只是留个参考链接,以防其他人遇到这个问题。 - robertc
我确实这样做了,但是我忘记在这里添加链接。谢谢你帮忙补充。 - pimvdb
Chrome的bug已经在此期间被修复。 - phk

7

这里有另一种使用document.elementFromPoint的解决方案:

 dragleave: function(event) {
   var event = event.originalEvent || event;
   var newElement = document.elementFromPoint(event.pageX, event.pageY);
   if (!this.contains(newElement)) {
     $(this).removeClass('red');
   }
}

希望这能够起作用,这里有一个 fiddle


3
初始状态下,这对我并没有起作用。我需要使用event.clientX, event.clientY,因为它们是相对于视口而不是页面的。希望这能帮助其他迷失的灵魂。 - Jon Abrams

7

接下来介绍一种适用于Chrome的解决方案:

.bind('dragleave', function(event) {
                    var rect = this.getBoundingClientRect();
                    var getXY = function getCursorPosition(event) {
                        var x, y;

                        if (typeof event.clientX === 'undefined') {
                            // try touch screen
                            x = event.pageX + document.documentElement.scrollLeft;
                            y = event.pageY + document.documentElement.scrollTop;
                        } else {
                            x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
                            y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop;
                        }

                        return { x: x, y : y };
                    };

                    var e = getXY(event.originalEvent);

                    // Check the mouseEvent coordinates are outside of the rectangle
                    if (e.x > rect.left + rect.width - 1 || e.x < rect.left || e.y > rect.top + rect.height - 1 || e.y < rect.top) {
                        console.log('Drag is really out of area!');
                    }
                })

它是否进入了“if (typeof event.clientX ==='undefined')”? - Aldekein
1
工作得很好,但浏览器上可能会有另一个窗口,因此仅获取鼠标位置并将其与矩形屏幕区域进行比较是不够的。 - H.D.
我同意@H.D.的看法。另外,正如我在上面@azlar的答案评论中所解释的那样,当元素具有大的border-radius时,这将会引起问题。 - Jay Dadhania

5

只需检查被拖动的元素是否是一个子元素,如果是,则不要移除你的“dragover”样式类。非常简单,适用于我:

 $yourElement.on('dragleave dragend drop', function(e) {
      if(!$yourElement.has(e.target).length){
           $yourElement.removeClass('is-dragover');
      }
  })

看起来对我来说是最简单的解决方案,而且它解决了我的问题。谢谢! - Francesco Marchetti-Stasi

5
我知道这是一个老问题,但我想加上我的偏好。我通过在比您的内容更高的z-index上添加类触发的css :after元素来处理这个问题。这将过滤掉所有的垃圾。
.droppable{
    position: relative;
    z-index: 500;
}

.droppable.drag-over:after{
    content: "";
    display:block;
    position:absolute;
    left:0;
    right:0;
    top:0;
    bottom:0;
    z-index: 600;
}

然后只需在第一个dragenter事件上添加drag-over类,这样子元素就不再触发该事件了。
dragEnter(event){
 dropElement.classList.add('drag-over');
}

dragLeave(event){
 dropElement.classList.remove('drag-over');
}

5

我不确定这个方法是否适用于所有浏览器,但我在Chrome中测试过,它解决了我的问题:

我想将文件拖放到整个页面上,但当我拖动到子元素上时,dragleave事件被触发。我的解决方法是查看鼠标的x和y坐标:

我有一个覆盖整个页面的div,在页面加载时我将其隐藏。

当你拖动到文档上时,我显示它,当你将文件拖放到父元素上时,它会处理它,当你离开父元素时,我检查x和y坐标。

$('#draganddrop-wrapper').hide();

$(document).bind('dragenter', function(event) {
    $('#draganddrop-wrapper').fadeIn(500);
    return false;
});

$("#draganddrop-wrapper").bind('dragover', function(event) {
    return false;
}).bind('dragleave', function(event) {
    if( window.event.pageX == 0 || window.event.pageY == 0 ) {
        $(this).fadeOut(500);
        return false;
    }
}).bind('drop', function(event) {
    handleDrop(event);

    $(this).fadeOut(500);
    return false;
});

1
有点取巧,但很聪明。我喜欢。对我有用。 - cupcakekid
1
哦,我多么希望这个答案能够得到更多的投票!谢谢。 - Kirby

5
一个替代的工作解决方案,稍微简单一些。
//Note: Due to a bug with Chrome the 'dragleave' event is fired when hovering the dropzone, then
//      we must check the mouse coordinates to be sure that the event was fired only when 
//      leaving the window.
//Facts:
//  - [Firefox/IE] e.originalEvent.clientX < 0 when the mouse is outside the window
//  - [Firefox/IE] e.originalEvent.clientY < 0 when the mouse is outside the window
//  - [Chrome/Opera] e.originalEvent.clientX == 0 when the mouse is outside the window
//  - [Chrome/Opera] e.originalEvent.clientY == 0 when the mouse is outside the window
//  - [Opera(12.14)] e.originalEvent.clientX and e.originalEvent.clientY never get
//                   zeroed if the mouse leaves the windows too quickly.
if (e.originalEvent.clientX <= 0 || e.originalEvent.clientY <= 0) {

这在Chrome中似乎并不总是有效。有时当鼠标在框外时,我会收到clientX大于0的情况。尽管如此,我的元素是position:absolute - Hengjie
这是一直发生还是只是偶尔发生?因为如果鼠标移动得太快(例如在窗口外),你可能会得到错误的值。 - Profet
这种情况经常发生,大约有90%的时间会出现。偶尔也会有罕见的情况(10次中有1次)我可以使其达到0。我会尝试以更慢的速度移动鼠标再试一次,但不能说我是在快速移动(可能你会称之为正常速度)。 - Hengjie
使用dropzone.js,类似的方法对我很有效:console.log(e.clientX + "/" + e.clientY ); if (e.clientX == 0 && e.clientY == 0 ) { console.log('真正的离开'); } - leosok

4

我遇到了相同的问题,这是我的解决方案 - 我认为比上面的方法要简单得多。我不确定它是否跨浏览器(可能取决于甚至冒泡顺序)。

为了简便起见,我将使用jQuery,但解决方案应该独立于框架。

事件无论如何都会冒泡到父级,因此给定:

<div class="parent">Parent <span>Child</span></div>

我们附加事件。
el = $('.parent')
setHover = function(){ el.addClass('hovered') }
onEnter  = function(){ setTimeout(setHover, 1) }
onLeave  = function(){ el.removeClass('hovered') } 
$('.parent').bind('dragenter', onEnter).bind('dragleave', onLeave)

就是这样了。:) 它能够运行是因为尽管子元素上的 onEnter 在父元素上的 onLeave 之前触发,但我们稍微延迟一下顺序,所以类首先被移除,然后在一毫秒后重新应用。


这段代码的唯一作用是通过在下一个时钟周期重新应用“hovered”类来防止其被删除(使“dragleave”事件无效)。 - null
这不是无用的。如果你离开父元素,它将按预期工作。这种解决方案的威力在于它的简单性,它并不是最理想或最好的。更好的解决方案是在子元素上打上一个进入标记,在离开时检查我们是否刚进入子元素,如果是,则不触发离开事件。然而,这将需要测试、额外的防护措施、检查孙元素等。 - Marcin Raczkowski

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