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

4

我写了一个名为Dragster的小型库来处理这个问题,它在除IE外的所有地方都可以正常工作(IE不支持DOM事件构造函数,但使用jQuery的自定义事件编写类似代码非常容易)。


非常有用(至少对我来说,我只关心Chrome)。 - M Katz

3
我写了一个名为drip-drop的拖放模块,可以解决这种奇怪的行为问题,还有其他问题。如果你正在寻找一个好的低级别的拖放模块,可以作为任何东西的基础(文件上传、应用内拖放、从或向外部源拖放),你应该查看这个模块:

https://github.com/fresheneesz/drip-drop

这是在drip-drop中实现你想要做的事情的方法:
$('#drop').each(function(node) {
  dripDrop.drop(node, {
    enter: function() {
      $(node).addClass('red')  
    },
    leave: function() {
      $(node).removeClass('red')
    }
  })
})
$('#drag').each(function(node) {
  dripDrop.drag(node, {
    start: function(setData) {
      setData("text", "test") // if you're gonna do text, just do 'text' so its compatible with IE's awful and restrictive API
      return "copy"
    },
    leave: function() {
      $(node).removeClass('red')
    }
  })
})

如果不使用库,计数器技术是我在drip-drop中使用的方法,尽管最高评分答案错过了重要步骤,这会导致除第一个滴水之外的所有东西都会出现问题。以下是正确的操作步骤:

var counter = 0;    
$('#drop').bind({
    dragenter: function(ev) {
        ev.preventDefault()
        counter++
        if(counter === 1) {
          $(this).addClass('red')
        }
    },

    dragleave: function() {
        counter--
        if (counter === 0) { 
            $(this).removeClass('red');
        }
    },
    drop: function() {
        counter = 0 // reset because a dragleave won't happen in this case
    }
});

3

我发现了一个简单的解决方法,所以分享一下。在我的情况下,它很有效。

jsfiddle可以试一下。

你实际上只需要使用dragenter事件就能实现这个功能,而且不需要注册dragleave。 所有你需要做的就是在拖放区域周围放置一个no-drop区域,就行了。

你也可以有嵌套式的拖放区域,这也完美地运行。 也可以查看嵌套的拖放区域

$('.dropzone').on("dragenter", function(e) {
  e.preventDefault();
  e.stopPropagation();
  $(this).addClass("over");
  $(".over").not(this).removeClass("over"); // in case of multiple dropzones
});

$('.dropzone-leave').on("dragenter", function(e) {
  e.preventDefault();
  e.stopPropagation();
  $(".over").removeClass("over");
});

// UPDATE
// As mar10 pointed out, the "Esc" key needs to be managed,
// the easiest approach is to detect the key and clean things up.

$(document).on('keyup', function(e){
  if (e.key === "Escape") {
    $(".over").removeClass("over");
  }
});

1
我尝试了这个解决方案,但应注意它有一个限制:当按Esc取消拖放操作时,没有人会从您的dropzone元素中删除“over”类。如果您尝试使用“dragleave”事件来解决此问题,那么您将遇到原始作者提出的第一个问题。因此,在悬停子元素和拖动时按Esc键都会在dropzone上触发“dragleave”事件。也许我们还需要监听Esc键,以从dropzone中删除“over”类... - mar10
嗨@mar10,感谢指出这个问题,我会更新我的答案。我认为我们可以使用dragend事件来处理这个问题,但我需要测试一下。 - Abubakar Azeem
1
我还没有找到一种方法来检测dragend事件是由释放鼠标按钮还是按下Esc键触发的,所以使用dragend事件会使逻辑变得复杂,因此简单的解决方案是像你说的那样检测Escape键。我已经更新了答案。 - Abubakar Azeem

2
"dragleave"事件在鼠标指针退出目标容器的拖放区域时触发。
这是很有道理的,因为在许多情况下,只有父级可以被拖放,而不是子元素。我认为event.stopPropogation()应该处理这种情况,但似乎并没有起到作用。
上述解决方案似乎对大多数情况都有效,但在那些不支持dragenter / dragleave事件的子元素(如iframe)中失败了。
一种解决方法是检查event.relatedTarget,并验证其是否位于容器内,然后忽略dragleave事件,就像我在这里所做的那样:
function isAncestor(node, target) {
    if (node === target) return false;
    while(node.parentNode) {
        if (node.parentNode === target)
            return true;
        node=node.parentNode;
    }
    return false;
}

var container = document.getElementById("dropbox");
container.addEventListener("dragenter", function() {
    container.classList.add("dragging");
});

container.addEventListener("dragleave", function(e) {
    if (!isAncestor(e.relatedTarget, container))
        container.classList.remove("dragging");
});

您可以在这里找到一个有效的演示示例!


2

经过多个小时的努力,我成功实现了预期的建议。我只想在拖动文件时提供提示,而document dragover和dragleave会在Chrome浏览器上引起痛苦的闪烁。

这是我解决问题的方法,并为用户提供了适当的提示。

$(document).on('dragstart dragenter dragover', function(event) {    
    // Only file drag-n-drops allowed, http://jsfiddle.net/guYWx/16/
    if ($.inArray('Files', event.originalEvent.dataTransfer.types) > -1) {
        // Needed to allow effectAllowed, dropEffect to take effect
        event.stopPropagation();
        // Needed to allow effectAllowed, dropEffect to take effect
        event.preventDefault();

        $('.dropzone').addClass('dropzone-hilight').show();     // Hilight the drop zone
        dropZoneVisible= true;

        // http://www.html5rocks.com/en/tutorials/dnd/basics/
        // http://api.jquery.com/category/events/event-object/
        event.originalEvent.dataTransfer.effectAllowed= 'none';
        event.originalEvent.dataTransfer.dropEffect= 'none';

         // .dropzone .message
        if($(event.target).hasClass('dropzone') || $(event.target).hasClass('message')) {
            event.originalEvent.dataTransfer.effectAllowed= 'copyMove';
            event.originalEvent.dataTransfer.dropEffect= 'move';
        } 
    }
}).on('drop dragleave dragend', function (event) {  
    dropZoneVisible= false;

    clearTimeout(dropZoneTimer);
    dropZoneTimer= setTimeout( function(){
        if( !dropZoneVisible ) {
            $('.dropzone').hide().removeClass('dropzone-hilight'); 
        }
    }, dropZoneHideDelay); // dropZoneHideDelay= 70, but anything above 50 is better
});

2
我为此苦苦挣扎了很久,即使阅读了所有这些答案,但我认为我可以与您分享我的解决方案,因为我觉得它可能是一种更简单的方法,虽然有些不同。我的想法是完全省略dragleave事件监听器,并使用每个新的dragenter事件触发拖放行为,同时确保不会不必要地触发dragenter事件。
在下面的示例中,我有一个表格,希望能够通过拖放API相互交换表格行内容。在dragenter时,将向当前拖动元素的行元素添加CSS类以突出显示它,在dragleave时,将删除此类。
示例:
非常基本的HTML表格:
<table>
  <tr>
    <td draggable="true" class="table-cell">Hello</td>
  </tr>
  <tr>
    <td draggable="true" clas="table-cell">There</td>
  </tr>
</table>

而dragenter事件处理程序函数被添加到每个表格单元格上(除了dragstartdragoverdropdragend处理程序之外,它们不特定于此问题,因此未在此处复制):

/*##############################################################################
##                              Dragenter Handler                             ##
##############################################################################*/

// When dragging over the text node of a table cell (the text in a table cell),
// while previously being over the table cell element, the dragleave event gets
// fired, which stops the highlighting of the currently dragged cell. To avoid
// this problem and any coding around to fight it, everything has been
// programmed with the dragenter event handler only; no more dragleave needed

// For the dragenter event, e.target corresponds to the element into which the
// drag enters. This fact has been used to program the code as follows:

var previousRow = null;

function handleDragEnter(e) {
  // Assure that dragenter code is only executed when entering an element (and
  // for example not when entering a text node)
  if (e.target.nodeType === 1) {
    // Get the currently entered row
    let currentRow = this.closest('tr');
    // Check if the currently entered row is different from the row entered via
    // the last drag
    if (previousRow !== null) {
      if (currentRow !== previousRow) {
        // If so, remove the class responsible for highlighting it via CSS from
        // it
        previousRow.className = "";
      }
    }
    // Each time an HTML element is entered, add the class responsible for
    // highlighting it via CSS onto its containing row (or onto itself, if row)
    currentRow.className = "ready-for-drop";
    // To know which row has been the last one entered when this function will
    // be called again, assign the previousRow variable of the global scope onto
    // the currentRow from this function run
    previousRow = currentRow;
  }
}

非常基础的代码注释,适合初学者使用。希望这能帮助到你!请注意,为了使其正常工作,您当然需要将我上面提到的所有事件监听器添加到每个表格单元格上。

2

问题已解决..!

声明一个数组,例如:

targetCollection : any[] 

dragenter: function(e) {
    this.targetCollection.push(e.target); // For each dragEnter we are adding the target to targetCollection 
    $(this).addClass('red');
},

dragleave: function() {
    this.targetCollection.pop(); // For every dragLeave we will pop the previous target from targetCollection
    if(this.targetCollection.length == 0) // When the collection will get empty we will remove class red
    $(this).removeClass('red');
}

无需担心子元素。

1
我遇到了类似的问题——当悬停在子元素上时,我的代码会不断触发隐藏拖放区域的dragleave事件,导致在Google Chrome中出现闪烁的情况。
我通过调度隐藏拖放区域的函数来解决这个问题,而不是立即调用它。然后,如果另一个dragover或dragleave被触发,预定的函数调用将被取消。
body.addEventListener('dragover', function() {
    clearTimeout(body_dragleave_timeout);
    show_dropzone();
}, false);

body.addEventListener('dragleave', function() {
    clearTimeout(body_dragleave_timeout);
    body_dragleave_timeout = setTimeout(show_upload_form, 100);
}, false);

dropzone.addEventListener('dragover', function(event) {
    event.preventDefault();
    dropzone.addClass("hover");
}, false);

dropzone.addEventListener('dragleave', function(event) {
    dropzone.removeClass("hover");
}, false);

这也是我最终做的事情,但仍然有点不可靠。 - mpen

1
你可以使用带有 transitioning 标志的超时,并监听顶级元素。子事件中的 dragenter / dragleave 会冒泡到容器。
由于子元素上的 dragenter 在容器上的 dragleave 之前触发,因此我们将设置标志 show 为过渡状态 1 毫秒... dragleave 监听器将在 1 毫秒结束前检查标志。
该标志仅在转换到子元素期间为 true,并且在转换到父元素(容器的父元素)时不为 true。
var $el = $('#drop-container'),
    transitioning = false;

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

  // temporarily set the transitioning flag for 1 ms
  transitioning = true;
  setTimeout(function() {
    transitioning = false;
  }, 1);

  $el.toggleClass('dragging', true);

  e.preventDefault();
  e.stopPropagation();
});

// dragleave fires immediately after dragenter, before 1ms timeout
$el.on('dragleave', function(e) {

  // check for transitioning flag to determine if were transitioning to a child element
  // if not transitioning, we are leaving the container element
  if (transitioning === false) {
    $el.toggleClass('dragging', false);
  }

  e.preventDefault();
  e.stopPropagation();
});

// to allow drop event listener to work
$el.on('dragover', function(e) {
  e.preventDefault();
  e.stopPropagation();
});

$el.on('drop', function(e) {
  alert("drop!");
});

jsfiddle: http://jsfiddle.net/ilovett/U7mJj/

在这里可以找到一个名为jsfiddle的在线编程环境,链接是http://jsfiddle.net/ilovett/U7mJj/。

1
只需将此添加到子元素中即可:
pointer-events: none;

如果你能详细阐述一下你的回答,那就太好了。 - Sanmeet
只需将子元素的指针事件设置为none,当您拖动到其父元素上时,它将不会触发。 - Kullpoint

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