悬停子元素时触发HTML5 dragleave

问题描述 投票:251回答:31

我遇到的问题是,当悬停该元素的子元素时,会触发元素的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更新:TL; DR,查看CSS pointer-events: none;,如@ H.D.的答案中所述,该答案适用于现代浏览器和IE11。

javascript jquery html5 javascript-events drag-and-drop
31个回答
298
投票

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

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

你可以运行它here


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!');
                    }
                })

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');
   }
}

希望这有效,这是一个fiddle


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 = '';
}
div {
  border: 1px dashed silver;
  padding: 16px;
  margin: 8px;
}

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

p {
  pointer-events: none;
  background: whitesmoke;
}
<article draggable="true">drag me</article>

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

5
投票

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


4
投票

不确定这是否是跨浏览器,但我在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;
});

4
投票

我遇到了同样的问题并尝试使用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

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

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

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

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


4
投票

只需检查被拖过的元素是否为子元素,如果是,则不要删除“dragover”样式类。非常简单,适合我:

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

4
投票

很简单的解决方案

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

3
投票

我偶然发现了同样的问题,这是我的解决方案 - 我认为这比上面容易得多。我不确定它是否是crossbrowser(可能依赖于冒泡的顺序)

我将使用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)

这就是它。 :)它的工作原理是因为即使在onEave之前onEnter on on on父级,我们也会延迟它稍微颠倒顺序,因此首先删除类然后在一个毫秒后重新获得。


2
投票

另一种工作解决方案,更简单一点。

//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) {

69
投票

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

是。

#drop * {pointer-events: none;}

CSS似乎足以支持Chrome。

在Firefox中使用它时,#drop不应该直接有文本节点(否则有一个奇怪的issue where a element "leave it to itself"),所以我建议只留下一个元素(例如,在#drop中使用div将所有内容放入其中)

Here's a jsfiddle解决了original question (broken) example

我也从@Theodore Brown的例子中分出了一个simplified version,但仅仅基于这个CSS。

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

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


2
投票

花了这么多时间后,我得到了与预期完全一致的建议。我想在文件被拖过时提供一个提示,而文档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
投票

我编写了一个名为drip-drop的拖放模块来修复这种古怪的行为等等。如果您正在寻找一个好的低级拖放模块,您可以将其用作任何内容的基础(文件上传,应用程序内拖放,从外部源或从外部源拖动),您应该检查这个模块输出:

https://github.com/fresheneesz/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')
    }
  })
})

为了在没有库的情况下执行此操作,计数器技术就是我在滴水中使用的,因为最高等级的答案错过了重要的步骤,这将导致除了第一滴之外的所有事情都会破坏。这是如何正确地做到这一点:

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
    }
});

2
投票

到目前为止,这个相当简单的解决方案对我有用,假设您的事件单独附加到每个拖动元素。

if (evt.currentTarget.contains(evt.relatedTarget)) {
  return;
}

1
投票

我遇到了类似的问题 - 当我在Google Chrome中悬停使用dropzone闪烁的子元素时,我的代码用于隐藏dropleave事件上的dropzone for body。

我能够通过安排隐藏dropzone的功能而不是立即调用它来解决这个问题。然后,如果触发另一个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);

1
投票

当鼠标指针退出目标容器的拖动区域时,会触发“dragleave”事件。

这很有意义,因为在很多情况下只有父母可能是可以辍学而不是后代。我认为event.stopPropogation()应该已经处理过这种情况,但似乎没有做到这一点。

上面提到的一些解决方案似乎对大多数情况都有效,但在那些不支持dragenter / dragleave事件的子节点(例如iframe)的情况下会失败。

1解决方法是检查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");
});

你可以找到一个工作小提琴here


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
投票

pimvdv ..

为什么不尝试使用drop而不是dragleave。它对我有用。希望这能解决你的问题。

请检查jsFiddle:http://jsfiddle.net/HU6Mk/118/

$('#drop').bind({
                 dragenter: function() {
                     $(this).addClass('red');
                 },

                 drop: function() {
                     $(this).removeClass('red');
                 }
                });

$('#drag').bind({
                 dragstart: function(e) {
                     e.allowedEffect = "copy";
                     e.setData("text/plain", "test");
                 }
                });

0
投票

您可以使用带有transitioning标志的超时并监听顶部元素。来自儿童活动的dragenter / dragleave将泡到容器中。

由于子元素上的dragenter在容器的dragleave之前触发,我们将标志显示为过渡1ms ... dragleave监听器将在1ms之前检查标志。

该标志仅在转换为子元素期间为真,并且在转换为(容器的)父元素时不会为真

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/


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
投票

使用此代码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');
             }
        }
    });

42
投票

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

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);

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

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

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


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');

  }
})

圭多


34
投票

解决此问题的“正确”方法是禁用放置目标的子元素上的指针事件(如@ H.D.的答案)。 Here's a jsFiddle I created which demonstrates this technique。不幸的是,这在IE11之前的Internet Explorer版本中不起作用,因为它们是didn't support pointer events

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

更新:

我使用特征检测创建了a jsFiddle demonstrating a combined solution,如果支持,则使用指针事件(当前是Chrome,Firefox和IE11),如果指针事件支持不可用,浏览器会回退到向子节点添加事件(IE8-10)。


26
投票

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

由于这个answer的答案,我设法解决了我最近遇到的同样的问题,并认为这可能对访问此页面的人有所帮助。整个想法是每次在任何父元素或子元素上调用时,将evenet.target存储在ondrageenter中。然后在ondragleave中检查当前目标(event.target)是否等于你存储在ondragenter中的对象。

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

这个工作正常的原因是当鼠标离开一个元素(比如el1)并进入另一个元素(比如el2)时,首先调用el1.ondragenter,然后调用el2.ondragleave。只有当阻力离开/进入浏览器窗口时,event.target才会在''el1.ondragenter中成为el2.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;
    };
})();

这是一个简单的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>

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


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');
               }
           }

9
投票

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

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


9
投票

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

let rect = document.getElementById("drag").getBoundingClientRect();

然后在parent.dragleave()中:

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

here is a jsfiddle


8
投票

您可以使用jQuery source code的一点灵感在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中不起作用,因为relatedTarget似乎不存在于dragleave事件中,并且我假设您在Chrome中工作,因为您的示例在Firefox中不起作用。 Here's a version实施了上述代码。

© www.soinside.com 2019 - 2024. All rights reserved.