悬停子元素时触发HTML5 dragleave

pim*_*vdb 272 html javascript jquery drag-and-drop jquery-events

我遇到的问题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>
Run Code Online (Sandbox Code Playgroud)

使用以下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");
                 }
                });
Run Code Online (Sandbox Code Playgroud)

它应该做的是通过在div那里拖动东西时使滴红色来通知用户.这样做有效,但是如果你拖入p孩子,那dragleave就会被激发而且div不再是红色.回到跌落div也不会再次变红.有必要完全移出掉落div并再次拖回它以使其变红.

dragleave拖入子元素时是否可以防止触发?

2017更新: TL; DR,查看CSS pointer-events: none;,如下面@ HD的答案中所述,适用于现代浏览器和IE11.

Woo*_*ody 309

你只需要保留一个参考计数器,当你得到一个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');
        }
    }
});
Run Code Online (Sandbox Code Playgroud)

注意:在drop事件中,将counter重置为零,并清除添加的类.

你可以在这里运行它

  • 这适用于所有浏览器,但Firefox.我有一个新的解决方案,在我的情况下无处不在.在第一个'dragenter`中,我将`event.currentTarget`保存在一个新变量`dragEnterTarget`中.只要设置了`dragEnterTarget`,我就会忽略'dragenter`事件,因为它们来自儿童.在所有`dragleave`事件中,我检查`dragEnterTarget === event.target`.如果这是错误的,则事件将被忽略,因为它是由孩子触发的.如果这是真的,我将`dragEnterTarget`重置为`undefined`. (12认同)
  • 当被拖动的元素的边缘接触另一个可拖动元素的边缘时,这不起作用.例如,可排序列表; 向下拖动元素,在下一个可拖动的项目上不会将计数器减少到0,而是将其拉到1.但是如果我将它拖到任何其他可拖动元素的侧面,它就会起作用.我和孩子们一起去了`pointer-events:none`.当我开始拖动时,我追加一个具有此属性的类,当拖动时我删除了该类.在Safari和Chrome上工作得很好,但在Firefox上却没有. (9认同)
  • OMG这是最明显的解决方案,只有一票......来吧,你可以做得更好.我正在考虑这个问题,但在看到前几个答案的复杂程度后,我几乎把它丢弃了.你有任何缺点吗? (7认同)
  • 好的解决方案 我需要在处理接受丢弃的函数中将计数器重置为0,否则后续拖动不能按预期工作. (3认同)
  • @Woody我没有在这里的巨大评论列表中看到他的评论,但是,是的,这就是修复.为什么不把它纳入你的答案? (2认同)

H.D*_*.D. 75

在拖入子元素时是否可以防止dragleave触发?

是.

#drop * {pointer-events: none;}
Run Code Online (Sandbox Code Playgroud)

CSS似乎足以支持Chrome.

在Firefox中使用它时,#drop不应该直接有文本节点(否则有一个奇怪的问题,即元素"留给自己"),所以我建议只留下一个元素(例如,在里面使用div #drop把所有东西放进去)

这是解决原始问题(破碎)示例的jsfiddle.

我还从@Theodore Brown示例中分离了一个简化版本,但仅基于此CSS.

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

看到Facebook源代码我可以pointer-events: none;多次找到它,但它可能与优雅的降级后备一起使用.至少它是如此简单,并解决了很多环境的问题.

  • 仅在区域内没有其他控制器元素(编辑,删除)的情况下,它才起作用,因为此解决方案也会阻止它们。 (5认同)
  • 对于放置目标内的交互式子元素(例如按钮):将 `pointer-events: none;` 添加到类中。使用 _ondagenter_ 将类应用到放置目标。然后从 _ondragleave_ 中的放置目标中删除该类。 (4认同)
  • 如果您的孩子是纽扣怎么办?O (3认同)
  • 使用指针事件确实是一个很好的答案,我在自己发现之前有点挣扎,答案应该更高. (2认同)
  • 这几乎是完美的......但有一个例外:如果子项是“checkbox”或“radio”类型的“input”,则“pointer-events: none”将不起作用。这是因为“指针事件”仅是那些导致元素上 *VALUE* 发生更改的事件。对于“checkbox”和“radio”,这些会改变它们的*STATE*,而不是它们的值。 (2认同)

Die*_*chi 44

在这里,最简单的跨浏览器解决方案(认真):

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);
Run Code Online (Sandbox Code Playgroud)

简而言之,你在dropzone中创建一个"mask",继承宽度和高度,绝对位置,只会在dragover启动时显示.
因此,在显示该掩码后,您可以通过在其上附加其他dragleave&drop事件来完成这一操作.

离开或掉落后,你只需再次隐藏面具.
简单,没有并发症.

(观察员:Greg Pettit建议 - 你必须确保面具悬停在整个盒子上,包括边框)

  • 实际上,这是边界.让面具与边框重叠,没有自己的边框,它应该可以正常工作. (2认同)

The*_*own 34

解决此问题的"正确"方法是禁用放置目标的子元素上的指针事件(如@ HD的答案).这是我创建的一个jsFiddle,演示了这种技术.不幸的是,这在IE11之前的Internet Explorer版本中不起作用,因为它们不支持指针事件.

幸运的是,我能够提出一个在旧版IE 工作的解决方法.基本上,它涉及识别和忽略dragleave拖动子元素时发生的事件.因为dragenter事件是dragleave在父节点上的事件之前在子节点上触发的,所以可以将单独的事件侦听器添加到每个子节点,该节点从放置目标添加或删除"ignore-drag-leave"类.然后,drop target的dragleave事件侦听器可以简单地忽略此类存在时发生的调用.这是一个证明这种解决方法jsFiddle.它经过测试,可在Chrome,Firefox和IE8 +中使用.

更新:

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


Ali*_*ian 27

在提出这个问题并且提供了许多解决方案(包括丑陋的黑客攻击)之后已经有一段时间了.

我设法解决同样的问题,我在这最近已经感谢回答的答案,并认为这可能是有帮助的人谁谈到通过这个页面.整个想法是evenet.targetondrageenter每次调用父元素或子元素时存储它.然后ondragleave检查当前目标(event.target)是否等于您存储的对象ondragenter.

这两个匹配的唯一情况是当您的拖动离开浏览器窗口时.

这个工作正常的原因是当鼠标离开一个元素(比如说el1)并进入另一个元素(比方说el2)时,首先el1.ondragenter调用它然后调用el2.ondragleave.只有当拖动离开/进入浏览器窗口时,event.target才会同时''el1.ondragenterel2.ondragleave.

这是我的工作样本.我在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;
    };
})();
Run Code Online (Sandbox Code Playgroud)

这是一个简单的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>
Run Code Online (Sandbox Code Playgroud)

使用适当的样式,我所做的就是在将文件拖入屏幕时使内部div(#file-drop-area)大得多,以便用户可以轻松地将文件放到适当的位置.

  • 这是最好的解决方案,它比计数器更好(特别是如果你委托事件),它也适用于可拖动的孩子. (4认同)
  • 这对于重复的dragenter事件的问题没有帮助 (3认同)
  • 优秀的解决方案 - 使用 React 就像跟踪新状态以引用“onDragEnter”并在“onDragLeave”中检查一样简单。这应该是公认的答案。 (2认同)
  • 到目前为止最好的答案是 2022 年,谢谢@Ali Motevallian。 (2认同)

Gre*_*reg 14

问题是dragleave当鼠标进入子元素前面时会触发事件.

我已经尝试了各种方法来检查e.target元素是否与元素相同this,但无法得到任何改进.

我解决这个问题的方式有点像黑客,但100%工作.

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');
               }
           }
Run Code Online (Sandbox Code Playgroud)

  • 我正在考虑通过检查坐标来做到这一点.你完成了我的大部分工作,thx :).我不得不做一些调整:`if(ex> =(rect.left + rect.width)|| ex <= rect.left || ey> =(rect.top + rect.height)|| ey <= rect.top)` (2认同)

azl*_*lar 13

如果您使用的是HTML5,则可以获取父级的clientRect:

let rect = document.getElementById("drag").getBoundingClientRect();
Run Code Online (Sandbox Code Playgroud)

然后在parent.dragleave()中:

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

这是一个jsfiddle

  • 极好的答案。 (2认同)

小智 10

一个非常简单的解决方案是使用pointer-eventsCSS属性.只是它的值设置为none的dragstart每一个子元素.这些元素不再触发与鼠标相关的事件,因此它们不会将鼠标捕捉到它们上面,因此不会触发父上的dragleave.

auto完成拖动时不要忘记将此属性设置回;)


rob*_*rtc 8

您可以使用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');
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

不幸的是,它在Chrome中不起作用,因为事件中relatedTarget似乎不存在dragleave,我认为你在Chrome中工作,因为你的例子在Firefox中不起作用. 这是一个实现了上述代码的版本.

  • @pimvdb我看到你已经记录了[一个bug](https://bugs.webkit.org/show_bug.cgi?id=66547),我会在这里留下一个参考,以防其他人遇到这个答案. (5认同)

ras*_*usx 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);
Run Code Online (Sandbox Code Playgroud)

CSS

此后规则将为可放置区域创建完全覆盖的覆盖层。

#box.overlay:after {
    content:'';
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    z-index: 1;
}
Run Code Online (Sandbox Code Playgroud)

这是完整的解决方案:http://jsfiddle.net/F6GDq/8/

我希望它可以帮助任何遇到同样问题的人。


Ken*_*cer 8

到目前为止,假设您的事件分别附加到每个拖动元素,那么这种对我来说仍然很有效的解决方案。

if (evt.currentTarget.contains(evt.relatedTarget)) {
  return;
}
Run Code Online (Sandbox Code Playgroud)

  • 对我来说,它可以在 Chrome 和 Firefox 中使用,但 id 在 Edge 中不起作用。请参阅:http://jsfiddle.net/iwiss/t9pv24jo/ (2认同)
  • 是的,我最喜欢这个解决方案。没有不稳定的坐标检查,没有大锤CSS,没有指针计数,没有屏蔽元素......这解决了我的问题并且做得很优雅。 (2认同)

Ald*_*ein 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!');
                    }
                })
Run Code Online (Sandbox Code Playgroud)


Owe*_*n M 7

很简单的解决方案:

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

  • 这在 Safari 12.1 中似乎不起作用:https://jsfiddle.net/6d0qc87m/ (2认同)

mar*_*elj 6

这是使用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');
   }
}
Run Code Online (Sandbox Code Playgroud)

希望这有效,这是一个小提琴.

  • 这对我来说不起作用.我需要使用`event.clientX,event.clientY`,因为它们是相对于视口而不是页面.我希望能帮助另一个失去灵魂的人. (2认同)

Ale*_*nic 6

一种简单的解决方案是将css规则添加pointer-events: none到子组件中,以防止触发ondragleave。参见示例:

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

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

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

p {
  pointer-events: none;
  background: whitesmoke;
}
Run Code Online (Sandbox Code Playgroud)
<article draggable="true">drag me</article>

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


chr*_*ick 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;
});
Run Code Online (Sandbox Code Playgroud)


Pro*_*fet 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) {
Run Code Online (Sandbox Code Playgroud)


Ben*_*Ben 5

我已经写了一个名为Dragster的小库来处理这个问题,除了在IE中默默地做任何事情之外无处不在(它不支持DOM事件构造函数,但使用jQuery的自定义事件编写类似的东西很容易)


sof*_*ood 5

只需检查拖动的元素是否是子元素,如果是,则不要删除“dragover”样式类。非常简单并且对我有用:

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


Chr*_*rby 5

我知道这是一个老问题,但想添加我的偏好。我通过添加类触发的 css :after 元素来处理这个问题,其 z-index 高于您的内容。这将过滤掉所有垃圾。

.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;
}
Run Code Online (Sandbox Code Playgroud)

然后,只需在第一个 Dragenter 事件上添加拖拽类,所有子元素都不会再触发该事件。

dragEnter(event){
 dropElement.classList.add('drag-over');
}

dragLeave(event){
 dropElement.classList.remove('drag-over');
}
Run Code Online (Sandbox Code Playgroud)