父元素的'dragleave'事件在拖动到子元素上时会触发。

192

概述

我有以下HTML结构,并已将dragenterdragleave事件附加到<div id="dropzone">元素上。

<div id="dropzone">
    <div id="dropzone-content">
        <div id="drag-n-drop">
            <div class="text">this is some text</div>
            <div class="text">this is a container with text and images</div>
        </div>
    </div>
</div>

问题

当我将文件拖到 <div id="dropzone"> 上时,会像预期的那样触发 dragenter 事件。但是,当我将鼠标移到子元素上时,例如 <div id="drag-n-drop">dragenter 事件会为 <div id="drag-n-drop"> 元素触发,然后为 <div id="dropzone"> 元素触发 dragleave 事件。

如果我再次悬停在 <div id="dropzone"> 元素上,则再次触发 dragenter 事件,这很好,但是接下来为刚离开的子元素触发 dragleave 事件,导致执行 removeClass 指令,这就不好了。

此行为存在两个问题:

  1. 我只将 dragenterdragleave 添加到 <div id="dropzone">,因此不理解为什么子元素也具有这些事件。

  2. 在悬停其子元素的同时仍在拖动 <div id="dropzone"> 元素,因此我不希望触发 dragleave

jsFiddle

这里是一个可以使用的 jsFiddle: http://jsfiddle.net/yYF3S/2/

问题

那么... 我如何使当我拖动文件到 <div id="dropzone"> 元素上时,即使我在任何子元素上拖动,dragleave 也不会触发... 它只应该在离开 <div id="dropzone"> 元素时触发... 在元素边界内的任何地方悬停/拖动都不应触发 dragleave 事件。

我需要这在支持 HTML5 拖放的浏览器中跨浏览器兼容,因此这个答案是不够的。

谷歌和 Dropbox 看起来已经解决了这个问题,但他们的源代码被压缩/混淆了,因此我无法从他们的实现中找到解决方法。


通过 e.stopPropagation(); 阻止事件传播。 - Blender
我想我已经是了...看看更新。 - Hristo
@Blender... 当然,给我几分钟! - Hristo
@Blender...我在我的问题中更新了一个fiddle。 - Hristo
@fguillen... 我的问题与jQuery UI无关。 - Hristo
显示剩余2条评论
22个回答

185

如果您不需要将事件绑定到子元素上,您可以始终使用pointer-events属性。

.child-elements {
  pointer-events: none;
}

4
最佳解决方案!我甚至没有想到可以用 CSS 以这样优雅的方式解决问题。非常感谢你! - serg66
26
这种方法唯一的缺点是会彻底禁用子元素的指针事件(比如,它们将无法再具有不同的 :hover 样式或单击事件处理程序)。如果你想保留这些事件,下面是我一直在使用的另一种解决方法:http://bensmithett.github.io/dragster/。 - Ben
2
使用 CSS 子选择器结合 .dropzone 以达到选中下拉区域内所有子元素的效果: .dropzone * {pointer-events: none;} - Philipp F
9
你已经在使用jQuery,所以只需对你正在拖动的元素使用$('.element').addClass('dragging-over'),在拖出时使用$('.element').removeClass('dragging-over')。然后在你的CSS中,你可以写.element.dragging-over * { pointer-events: none; }来移除所有子元素上的指针事件,只有当你拖动到该元素上方时才移除。 - Gavin
哇!完美的答案! - Daniel
显示剩余3条评论

65

我终于找到了一个让我满意的解决方案。实际上我找到了几种方法来做我想要的事情,但是没有一种像当前这个解决方案这样成功…在一个方案中,由于向#dropzone元素添加/删除边框,我经常遇到闪烁问题...在另一个方案中,如果你从浏览器悬停移开,边框永远不会被移除。

无论如何,我的最佳hacky解决方案如下:

var dragging = 0;

attachEvent(window, 'dragenter', function(event) {

    dragging++;
    $(dropzone).addClass('drag-n-drop-hover');

    event.stopPropagation();
    event.preventDefault();
    return false;
});

attachEvent(window, 'dragover', function(event) {

    $(dropzone).addClass('drag-n-drop-hover');

    event.stopPropagation();
    event.preventDefault();
    return false;
});

attachEvent(window, 'dragleave', function(event) {

    dragging--;
    if (dragging === 0) {
        $(dropzone).removeClass('drag-n-drop-hover');
    }

    event.stopPropagation();
    event.preventDefault();
    return false;
});
这个方法效果还不错,但在Firefox中出现了问题,因为Firefox会双重调用 dragenter 事件,导致计数器错误。尽管如此,这并不是一种非常优雅的解决方案。
然后我偶然发现了这个问题:如何在Firefox中检测拖出窗口外的 dragleave 事件 所以我采用了这个答案并将其应用到我的情况:
$.fn.dndhover = function(options) {

    return this.each(function() {

        var self = $(this);
        var collection = $();

        self.on('dragenter', function(event) {
            if (collection.size() === 0) {
                self.trigger('dndHoverStart');
            }
            collection = collection.add(event.target);
        });

        self.on('dragleave', function(event) {
            /*
             * Firefox 3.6 fires the dragleave event on the previous element
             * before firing dragenter on the next one so we introduce a delay
             */
            setTimeout(function() {
                collection = collection.not(event.target);
                if (collection.size() === 0) {
                    self.trigger('dndHoverEnd');
                }
            }, 1);
        });
    });
};

$('#dropzone').dndhover().on({
    'dndHoverStart': function(event) {

        $('#dropzone').addClass('drag-n-drop-hover');

        event.stopPropagation();
        event.preventDefault();
        return false;
    },
    'dndHoverEnd': function(event) {

        $('#dropzone').removeClass('drag-n-drop-hover');

        event.stopPropagation();
        event.preventDefault();
        return false;
    }
});

这很干净、优雅,似乎在我测试过的所有浏览器中都可以工作(还没有测试IE)。


这是目前为止最好的解决方案。唯一的问题是当你添加一个文件,删除它并拖动另一个文件到它上面时,这段代码不起作用。只有在提高HoverEnd并再次提高HoverStart后,这段代码才能再次工作。 - MysticEarth
@MysticEarth,你能否组合一个 jsFiddle 来演示它不起作用的情况? - Hristo
1
谢谢你的回答。这真的很有帮助,因为HTML5拖放界面和事件监听器可能会非常痛苦。这是处理疯狂事件的最佳方式,特别是当您想要处理多个投放区元素的实例时。 - Nico O
为什么我们需要调用stopPropagation() preventDefault()和return false。仅返回false不就足够了吗? - Dejo
虽然有些冗余,但调用 stopPropagation()preventDefault() 更可取。我会放弃 return false - Peeja

46

虽然有点丑,但它却起作用了!...

在你的“dragenter”处理程序中存储事件目标(在闭包内或其他变量中),然后在你的“dragleave”处理程序中仅当event.target === 你存储的那个时才执行你的代码。

如果你的“dragenter”在不想要它进入的时候触发(例如在离开子元素后再次进入),那么在鼠标离开父元素之前,最后一次触发是在父元素上的,因此父元素将始终是打算离开时的最终“dragenter”。

(function () {

    var droppable = $('#droppable'),
        lastenter;

    droppable.on("dragenter", function (event) {
        lastenter = event.target;
        droppable.addClass("drag-over");            
    });

    droppable.on("dragleave", function (event) {
        if (lastenter === event.target) {
            droppable.removeClass("drag-over");
        }
    });

}());

1
仅适用于Chrome浏览器,并且仅在假定子元素与容器之间存在物理间隔的情况下有效。 - hacklikecrack
看起来可以工作,谢谢!需要注意的是,我必须将顶级 div (#dropzone) 中的任何文本都包装在一个 span 中。不要问我为什么,但如果我不这样做,那么每当我拖动字符时,dragleave 事件就会触发。 - Harry Cutts
5
我最喜欢这里的答案,一点也不让人感到丑陋 :) - Matt Way
1
在我的电脑上,IE、Chrome和FF都可以运行。 - Vicentiu Bacioiu
1
如果拖动是从子元素开始的话,这种方法就不起作用。例如,从另一个窗口拖动某些内容,使得拖动操作在子元素内部开始。 - FINDarkside
显示剩余6条评论

41

起初,我赞成放弃使用pointer-events: none方法。但后来我问自己:

在拖动进行时,您真的需要在子元素上工作的指针事件吗?

在我的情况下,我的子元素中有很多事情要做,例如悬停以显示其他操作的按钮、内联编辑等等...然而,在拖动期间所有这些都是不必要的甚至是不需要的。

在我的情况下,我使用类似于以下内容的东西选择性地关闭父容器的所有子节点的指针事件:

  div.drag-target-parent-container.dragging-in-progress * {
    pointer-events: none;
  }

使用您喜欢的方法在dragEnter/dragLeave事件处理程序中添加/删除类dragging-in-progress,就像我所做的那样,或者在 dragStart 等事件中执行相同操作。


1
这个解决方案既是最简单的,也是(据我所知)最可靠的。只需在dragstart事件中添加类,并在dragend中删除它即可。 - cmann
这是一个相当不错的解决方案,但并非完美。如果拖动始于子元素(从另一个窗口拖动),dragEnter 将永远不会触发。当您移动到子元素时,可能也可以快速移动鼠标以使 dragEnter 未被触发,但无法百分之百确定。 - FINDarkside
我确认@FINDarkside的评论,即能够快速拖动以跳过一些事件并混乱进程。 - sw1337

12

这似乎是Chrome的一个bug。

我唯一想到的解决方法是创建一个透明的遮罩层元素来捕获你的事件:http://jsfiddle.net/yYF3S/10/

JS:

$(document).ready(function() {
    var dropzone = $('#overlay');

    dropzone.on('dragenter', function(event) {
        $('#dropzone-highlight').addClass('dnd-hover');
    });

    dropzone.on('dragleave', function(event) {
        $('#dropzone-highlight').removeClass('dnd-hover');
    });

});​

HTML:

<div id="dropzone-highlight">
    <div id="overlay"></div>

    <div id="dropzone" class="zone">
        <div id="drag-n-drop">
            <div class="text1">this is some text</div>
            <div class="text2">this is a container with text and images</div>
        </div>
    </div>
</div>

<h2 draggable="true">Drag me</h2>

谢谢你的回答,Blender。不过我已经尝试过了。这个解决方案的问题在于覆盖元素“覆盖”了子元素,而我需要与它们交互...因此,拥有一个覆盖层会禁用我需要与子元素交互的功能...可以将其想象为文件拖放框...您可以拖放文件,也可以单击输入元素来选择您的文件。 - Hristo
这是一个示例fiddle,您的解决方案无法满足... http://jsfiddle.net/UnsungHero97/yYF3S/11/ - Hristo
我困惑了。你为什么需要与子元素交互? - Blender
在我的特定情况下,“dropzone”的子元素是一个文件选择按钮...所以如果我不想拖放文件,我可以使用输入元素来选择它们。但是有了覆盖层...我无法点击输入元素。明白吗? - Hristo
@Blender 大多数情况下,我们需要从桌面拖动文件,而不是从页面中拖动元素,所以这种方式不起作用。 - Mārtiņš Briedis

5
问题在于,您放置在拖放区内的元素当然是拖放区的一部分。当您进入子元素时,您离开了父元素。解决这个问题并不容易。您可以尝试为子元素添加事件,并将您的类再次添加到父元素中。
$("#dropzone,#dropzone *").on('dragenter', function(event) {

    // add a class to #dropzone

    event.stopPropagation(); // might not be necessary
    event.preventDefault();
    return false;
});

您的事件仍将触发多次,但没有人会看到。

//编辑:使用dragmove事件永久覆盖dragleave事件:

$("#dropzone,#dropzone *").on('dragenter dragover', function(event) {

    // add a class to #dropzone

    event.stopPropagation(); // might not be necessary
    event.preventDefault();
    return false;
});

只为拖放区域定义dragleave事件。


2
那样做并不能解决问题。真正的问题在于dragleave...当我离开一个子元素时,我可能仍然在父元素#dropzone内部,但这不会再次触发dragenter事件 :( - Hristo
啊对了...它又被触发了,但是,在它被触发后,dragleave事件会为刚离开的子元素触发,因此再次执行removeClass指令。 - Hristo
我无法仅为#dropzone定义dragleave...如果可以的话,我就不会有这个问题了。 - Hristo
不,我的意思是不要为子元素添加dragleave事件。 - Oliver
只需替换您的dragenter代码,让dragleave保持不变。子元素不应触发dragleave事件。 - Oliver
显示剩余5条评论

5
正如benr这个答案中提到的那样,您可以防止子节点触发事件,但如果您需要绑定一些事件,请执行以下操作:
#dropzone.dragover *{
   pointer-events: none;
}

并将以下代码添加到您的JS代码中:

$("#dropzone").on("dragover", function (event) {
   $("#dropzone").addClass("dragover");
});

$("#dropzone").on("dragleave", function (event) {
   $("#dropzone").removeClass("dragover");
});

4
对我来说,这导致覆盖层不断闪烁。 - Andrey Mikhaylov - lolmaus

4

我的建议是:在你的拖放区域上方隐藏一个图层,然后当你拖拽进入时显示它,并将其目标设置为拖拽离开。

演示:https://jsfiddle.net/t6q4shat/

HTML

<div class="drop-zone">
  <h2 class="drop-here">Drop here</h2>
  <h2 class="drop-now">Drop now!</h2>
  <p>Or <a href="#">browse a file</a></p>
  <div class="drop-layer"></div>
</div>

CSS

.drop-zone{
  padding:50px;
  border:2px dashed #999;
  text-align:center;
  position:relative;
}
.drop-layer{
  display:none;
  position:absolute;
  top:0;
  left:0;
  bottom:0;
  right:0;
  z-index:5;
}
.drop-now{
  display:none;
}

JS

$('.drop-zone').on('dragenter', function(e){
    $('.drop-here').css('display','none');
    $('.drop-now').css('display','block');
    $(this).find('.drop-layer').css('display','block');
    return false;
});

$('.drop-layer').on('dragleave', function(e){
    $('.drop-here').css('display','block');
    $('.drop-now').css('display','none');
    $(this).css('display','none');
    return false;
});

简单而有效!我一开始尝试使用stopPropagation()方法,但是我不喜欢将事件处理程序应用于所有子元素。这看起来很混乱,但这种方法非常好用且易于实现。好主意。 - Jon Catmull

3
如果您正在使用jQuery,请查看以下内容:https://github.com/dancork/jquery.event.dragout。这真的很棒。

创建了一个特殊事件来处理真正的dragleave功能。

HTML5 dragleave事件更像是mouseout。该插件被创建来复制mouseleave样式的功能,同时进行拖动。

用法示例:

$('#myelement').on('dragout',function(event){ // YOUR CODE });

编辑:实际上,我认为它并不依赖于jQuery,您可能可以直接使用该代码。

哇!Dragout 是唯一一个在我有很多子元素触发 dragleave 的布局中正常工作的东西,即使我没有离开父级拖放区域。 - Mark Kasson
在为这个问题奋斗了一个小时后,dragout 是唯一对我有效的解决方案。 - Jason Hazel

3

My version:

$(".dropzone").bind("dragover", function(e){
    console.log('dragover');
});

$(".dropzone").bind("dragleave", function(e) {
  var stopDrag = false;
  if (!e.relatedTarget) stopDrag = true;
  else {
    var parentDrop = $(e.relatedTarget).parents('.dropzone');
    if (e.relatedTarget != this && !parentDrop.length) stopDrag = true;
  }

  if (stopDrag) {
    console.log('dragleave');
  }
});

使用这种布局:

<div class="dropzone">
  <div class="inner-zone">Inner-zone</div>
</div>

我已经为e.targete.currentTargete.relatedTarget的元素类别制作了一个转储,适用于dragoverdragleave事件。

这向我展示,在离开父块(.dropzone)时,e.relatedTarget不是此块的子级,因此我知道我已经离开了投放区。


!e.relatedTarget 对我来说完美地运作了。谢谢 @mortalis - Rama Krishna
relatedTarget在Safari中失效了 :( - Jonas Stensved

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