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

0
使用这段代码http://jsfiddle.net/HU6Mk/258/
$('#drop').bind({
         dragenter: function() {
             $(this).addClass('red');
         },

         dragleave: function(event) {
             var x = event.clientX, y = event.clientY,
                 elementMouseIsOver = document.elementFromPoint(x, y);
             if(!$(elementMouseIsOver).closest('.red').length) {
                 $(this).removeClass('red');
             }
        }
    });

0

您需要移除拖动目标的所有子对象的指针事件。

function disableChildPointerEvents(targetObj) {
        var cList = parentObj.childNodes
        for (i = 0; i < cList.length; ++i) {
            try{
                cList[i].style.pointerEvents = 'none'
                if (cList[i].hasChildNodes()) disableChildPointerEvents(cList[i])
            } catch (err) {
                //
            }
        }
    }

0

我找到了一个类似但更优雅的解决方案,这是我的解决方案:

$(document).on({
    dragenter: function(e) {
        e.stopPropagation();
        e.preventDefault();
        $("#dragging").show();
    },
    dragover: function(e) {
        e.stopPropagation();
        e.preventDefault();
    },
    dragleave: function(e) {
        e.stopPropagation();
        e.preventDefault();
        if (e.clientX <= 0 ||
            // compare clientX with the width of browser viewport
            e.clientX >= $(window).width() ||
            e.clientY <= 0 ||
            e.clientY >= $(window).height())
            $("#dragging").hide();
    }
});

这个方法可以检测鼠标是否离开了页面。 在Chrome和Edge中运行良好。


0

尝试使用event.eventPhase。只有在目标被输入时,它才会设置为2(Event.AT_TARGET),否则它将设置为3(Event.BUBBLING_PHASE)。

我已经使用eventPhase来绑定或解除绑定dragleave事件。

$('.dropzone').on('dragenter', function(e) {

  if(e.eventPhase === Event.AT_TARGET) {

    $('.dropzone').addClass('drag-over');

    $('.dropzone').on('dragleave', function(e) {
      $('.dropzone').removeClass('drag-over');
    });

  }else{

    $('.dropzone').off('dragleave');

  }
})

Guido


0
   private onDragLeave(e) {
     e.preventDefault();
     e.stopPropagation();

    if (this.isEventFromChild(e)) {
      return;
    }

    this.isDragOver = false;
  }
  
  private isEventFromChild(e): boolean {
    //relatedTarget is null on Safari, so we take it from coordinates
    const relatedTarget = e.relatedTarget || document.elementFromPoint(e.clientX, e.clientY);
    return e.currentTarget.contains(relatedTarget);
  }

0

这里有另一种基于事件时间的方法。

子元素派发的dragenter事件可以被父元素捕获,而且它总是在dragleave之前发生。这两个事件之间的时间非常短,比任何可能的人类鼠标操作都要短。因此,想法是记住dragenter发生的时间,并过滤掉在“不太快”之后发生的dragleave事件...

这个简短的例子适用于Chrome和Firefox:

var node = document.getElementById('someNodeId'),
    on   = function(elem, evt, fn) { elem.addEventListener(evt, fn, false) },
    time = 0;

on(node, 'dragenter', function(e) {
    e.preventDefault();
    time = (new Date).getTime();
    // Drag start
})

on(node, 'dragleave', function(e) {
    e.preventDefault();
    if ((new Date).getTime() - time > 5) {
         // Drag end
    }
})

0

我能够使用dragleave上的超时来完成这个操作。与其他采用此方法的答案不同,我认为在dragover上重置此超时非常关键,以避免闪烁。

我只写了你需要的函数,然后使用你选择的框架进行绑定。

let dragoverTimeout;
const onDragOver = (e: Event) => {
  e.preventDefault();
  e.stopPropagation();

  if (dragoverTimeout) {
    window.clearTimeout(dragoverTimeout);
    dragoverTimeout = null;
  }
  // Add your class here
}

const onDragLeave = (e: Event) => {
  e.preventDefault();
  e.stopPropagation();

  if (!dragoverTimeout) {
    dragoverTimeout = window.setTimeout(() => {
      // Remove your class here
    }, 100);
  }
}

const onDrop = (e) => {
  e.preventDefault();
  e.stopPropagation();

  const files = e.dataTransfer.files;
  // Remove your class here

  if (files.length > 0) {
    this.uploadFile(files);
  }
}

0

1
欢迎来到 Stack Overflow!请确保参观 [导览],它会指引您正确的方向。我还想请您不要使用非永久性指示器来引用帖子(您能否自己修复这个问题?即使现在我也不确定您是在引用哪些答案)。 - IWonderWhatThisAPIDoes

0
这是我的解决方案(https://jsfiddle.net/42mh0fd5/8):
<div id="droppable">
    <div id="overlay"></div>
    <a href="">test child 1</a>
    <br /><br />
    <button>test child 2</button>
    <br /><br />
    <button>test child 3</button>
    <br />
</div>
<p id="draggable" draggable="true">This element is draggable.</p>


<script type="text/javascript">
var dropElem = document.getElementById('droppable');
var overlayElem = document.getElementById('overlay');

overlayElem.addEventListener('drop', function(ev) {
    ev.preventDefault();
    console.log('drop', ev.dataTransfer.files)
    overlayElem.classList.remove('dragover')
    dropElem.classList.add('dropped')
    console.log('drop')
}, false);

overlayElem.addEventListener('dragover', function(ev) {
    ev.preventDefault();
}, false);

overlayElem.addEventListener('dragleave', function(ev) {
    console.log('dragleave')
    overlayElem.classList.remove('dragover')
}, false);

dropElem.addEventListener('dragenter', function(ev) {
    console.log('dragenter')
    overlayElem.classList.add('dragover')
}, false);
</script>

<style>
#draggable{
    padding:5px;
    background: #fec;
    display: inline-block;
}
#droppable{
    width: 300px;
    background: #eef;
    border: 1px solid #ccd;
    position: relative;
    text-align: center;
    padding:30px;
}
#droppable.dropped{
    background: #fee;
}
#overlay.dragover{
    content:"";
    position: absolute;
    z-index: 1;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    background: rgba(0,0,0,.2);
    border:3px dashed #999;
}
</style>

0

我看到了一些关于使用getBoundingclientRect()来检测“真正”的离开的答案。但是,这些解决方案没有考虑到任何在其父元素的DOM结构中但在渲染时位于外部的元素(例如带有bottom: 100%等的导航箭头)。这个可以:

element.addEventListener('dragleave', function (e) {
    if (!this.contains(document.elementFromPoint(e.clientX, e.clientY))) {    
        // do something
    }
}

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