432

The problem I'm having is that the dragleave event of an element is fired when hovering a child element of that element. Also, dragenter is not fired when hovering back the parent element again.

I made a simplified fiddle: 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>

with the following 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");
                 }
                });

What it is supposed to do is notifying the user by making the drop div red when dragging something there. This works, but if you drag into the p child, the dragleave is fired and the div isn't red anymore. Moving back to the drop div also doesn't make it red again. It's necessary to move completely out of the drop div and drag back into it again to make it red.

Is it possible to prevent dragleave from firing when dragging into a child element?

2017 Update: TL;DR, Look up CSS pointer-events: none; as described in @H.D.'s answer below that works in modern browsers and IE11.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
pimvdb
  • 151,816
  • 78
  • 307
  • 352
  • The bug pimvdb reported still exists in Webkit as of May 2012. I've countered it by also adding a class in dragover, which isn't anywhere close to nice since it fires so often, but appears to patch the issue a bit. – ajm May 11 '12 at 18:35
  • 3
    @ajm: Thanks, that works to an extent. However, on Chrome, there is a flash when entering or leaving the child element, presumably because `dragleave` is still fired in that case. – pimvdb May 11 '12 at 19:08
  • I have opened a [jQuery UI bug](http://bugs.jqueryui.com/ticket/8513) _upvotes_ are welcome so they can decide to put resources on it – fguillen Aug 21 '12 at 18:07
  • @fguillen: I'm sorry but this has nothing to do with jQuery UI. In fact, jQuery isn't even needed to trigger the bug. I've filed a WebKit bug already but there is no update as of now. – pimvdb Aug 21 '12 at 18:09
  • @pimvdb, yep, I have seen the answer in my bug, which is the link to your WebKit bug?.. any how I can reproduce the same bug with FireFox :/ – fguillen Aug 21 '12 at 18:12
  • @fguillen: It's in the comments of the answer posted (which by the way has a workaround for Firefox). – pimvdb Aug 21 '12 at 19:59
  • Not having any css involve in this workaround feels more straightforward. Have done it with pointer events and I like better the counter option for now. – brittongr Oct 02 '17 at 07:18
  • 1
    I've found that the simplest solution to this really annoying problem, is to listen only to `enter` event on element, and when event fires - create an absolutely positioned overlay above given element, with only `leave` event listener. This removes neccesity of disabling pointer events on children (overlay takes over every drag event) and you are sure that `leave` will be fired when it should be. I've had this problem in vue component: always firing `leave` just after `enter`, didn't find out why really (children had pointer events set to none). – Przemysław Melnarowicz Mar 09 '20 at 12:24

41 Answers41

461

You just need to keep a reference counter, increment it when you get a dragenter, decrement when you get a dragleave. When the counter is at 0 - remove the class.

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

Note: In the drop event, reset counter to zero, and clear the added class.

You can run it here

Greg Gum
  • 33,478
  • 39
  • 162
  • 233
Woody
  • 7,578
  • 2
  • 21
  • 25
  • 10
    OMG this is the most obvious solution and only had ONE vote... Come on people, you can do better. I was thinking about that but after seeing the level of sophistication of the first few answers I almost discarded it. Did you have any drawbacks ? – Arthur Corenzan Jul 15 '14 at 00:16
  • Hi @ArthurCorenzan - thanks. No - it works fine, actually i figured out an even simpler way to do it, shown above. – Woody Aug 20 '14 at 13:55
  • Oh no, you just blew my first comment :P I liked the previous solution better (preventing the bubbling of dragleave event of child elements). – Arthur Corenzan Aug 20 '14 at 20:15
  • 1
    The first solution did not work generically e.g. if you had nested child elements, this way does and it's less code, a "win win" :) – Woody Aug 21 '14 at 07:00
  • Checkout this useful library for html5 drag and drop : https://github.com/stevendwood/html5-dropzone – Woody Nov 16 '14 at 09:46
  • 14
    This didn't work when the edge of the element being dragged touches the edge of another draggable element. For instance a sortable list; dragging the element downwards, over the next draggable item does not decrement the counter back to 0, instead it gets stucked at 1. But if I drag it sideways, out of any other draggable element, it works. I went with the `pointer-events: none` on the children. When I start dragging I append a class that has this property, when drag's over I remove the class. Worked nicely on Safari and Chrome, but not on Firefox. – Arthur Corenzan Mar 14 '15 at 14:37
  • That worked for me with Firefox 37 and Chromium 39, but not with IE11 (there are more dragenter than dragleave events). Actually, even the fiddle does not work with IE11. – Damien Apr 14 '15 at 12:28
  • 1
    Adding e.preventDefault() in dragenter(e) seemed to fix it for IE11. I got the idea while reading this: [The HTML5 drag and drop disaster](http://www.quirksmode.org/blog/archives/2009/09/the_html5_drag.html) – Damien Apr 14 '15 at 13:01
  • I updated the link. The reason it doesn't work on IE is because of the "text/plain" content type in the drag start once that's fixed it then refuses to fire any dragleave events if you don't cancel the dragenter event...another quirk – Woody Apr 14 '15 at 13:34
  • Dangerous hack. I've made a jQuery plugin that do the job simply. [Check my answer](http://stackoverflow.com/a/29869208/636561) – Luca Fagioli Apr 25 '15 at 18:57
  • @LucaFagioli - in what way is it a hack, and why would it be dangerous ? – Woody Apr 25 '15 at 21:48
  • @Woody It's a hack because to make it work in IE you need to add a special condition. Moreover you need a global variable for each selector you want to be firing the dragenter event, which mess your code. Finally, it's dangerous because if for some reason you need to call stopPropagation() on a child element this solution breaks. – Luca Fagioli Apr 25 '15 at 22:44
  • 1
    First of all, if you think you have a better answer to the posted problem, then write an answer, not a comment against the highest voted answer trying to tell everyone how smart you are and how dumb the answer and everyone who thinks it's the best answer is. Second of all, your fiddle obviously doesn't work since it only uses one counter to track the events on two elements, i mean how would you expect the code in the answer to work in that instance !? Clearly you need to track the events per element you want to listen on, which is what the questions asks. – Woody Apr 26 '15 at 18:39
  • doesn't work when leaving by the left side (when you have no margin) – caub Nov 18 '15 at 08:49
  • 28
    This worked for me in all browsers, but Firefox. I have a new solution which works everywhere in my case. On the first `dragenter` I save `event.currentTarget` in a new variable `dragEnterTarget`. As long as `dragEnterTarget` is set, I ignore further `dragenter` events, because they are from children. In all `dragleave` events I check `dragEnterTarget === event.target`. If this is false the event will be ignored as it was fired by a child. If this is true I reset `dragEnterTarget` to `undefined`. – Pipo Jan 06 '16 at 12:54
  • 5
    Great solution. I needed to reset the counter to 0 in the function that handles accepting the drop though, otherwise subsequent drags didn't work as expected. – Liam Johnston Jan 07 '16 at 01:45
  • Note that this does not work if any child stops propagation of the events. Here's my solution with document.elementFromPoint, which is even simpler: http://stackoverflow.com/a/35117158/1620264 (I haven't really used elementFromPoint before, so not 100% sure my solution is correct) – marcelj Jan 31 '16 at 17:53
  • This won't work if you've already dropped something. The counters will become out of sync - you need a way to determine if its a new drag or a continuation of an old drag. – B T Jul 18 '16 at 08:17
  • @BT didn't Liam Johnston's reset in the drop function help ? If not you could try adding a dragend listener that resets the counter. I think that dragend should fire on the thing being dragged, but the event should propagate so that you can catch it at the body. – Woody Jul 18 '16 at 09:32
  • 2
    @Woody I didn't see his comment in the huge list of comments here, but yes, that's the fix. Why not incorporate that into your answer? – B T Jul 18 '16 at 21:28
  • In order to support Firefox I've also added "ondragexit" listener to decrement counter if not 0. – BobTheBuilder Aug 21 '17 at 11:37
  • @BobTheBuilder, @B T can you suggest an edit and I'll add it to the example ? – Woody Aug 29 '17 at 11:35
  • 1
    To get this to work reliably, I had to (a) store the counter per-element rather than globally and (b) abandon the old counters when a new drag began. To do this, I used a global generation counter `var dragRefCountGen = 0;`, increment it in dragstart: `dragRefCountGen++;`, and reset the counter in dragenter when we're in a new drag: `if (this.dragRefCountGen != dragRefCountGen) { this.dragRefCount = 0; this.dragRefCountGen = dragRefCountGen; }`. From there, as before--increment `this.dragRefCount` in dragenter, decrement and check `this.dragRefCount` in dragleave. – Josh Bleecher Snyder Jan 30 '18 at 20:30
  • @Woody happy to suggest this as an edit if you'd like. – Josh Bleecher Snyder Jan 30 '18 at 20:33
  • @JoshBleecherSnyder can you post the full code as an answer please – Chin Apr 12 '19 at 18:09
  • This solution is not very reliable, because when you drag things fast, not all the elements in the tree will actually fire the event, which might result in the sum different then 0 after dragging in and out. This happens more often if you have a lot of other elements in the dom tree between the most external and the most internal element. – Matt Leonowicz May 16 '19 at 12:47
  • @MattLeonowicz I tried this solution with very complex elements and your concern did not manifest. – Emperor Eto Sep 27 '19 at 22:17
  • @Pipo What is `event.currentTarget`, is this a thing? I see `explicitOriginalTarget`, `originalTarget`, `relatedTarget`, but no `currentTarget`, at least on Firefox. – Bersan Jan 22 '20 at 17:36
  • This answer is like magic! It just works. Used this method for DropZone and a HTML table for highlighting rows (The dropzones) for file uploads. – tlorens Nov 17 '20 at 19:04
  • This solution is brilliant and works great on modern browsers. No one cares about IE. Not sure why there seem to be so many detractors. If you are doing proper component state management almost all the criticisms become moot. And to echo others - don't forget to reset on drop! – Emperor Eto May 05 '22 at 20:17
  • Looks like the only viable solution among those listed. In React don't forget to store the variable in a `useRef` hook to make sure it's not reset on rerender (also don't forget to reset it `onDrop` as @PeterMoore suggested! – sdvnksv Jun 01 '22 at 10:35
  • This works for most of the cases, saved my time – lakmal_sathyajith May 03 '23 at 10:23
  • Simple and brilliant! – esenkaya Jul 19 '23 at 20:33
174

Is it possible to prevent dragleave from firing when dragging into a child element?

Yes.

#drop * {pointer-events: none;}

That CSS seem to be enough for Chrome.

While using it with Firefox, the #drop shouldn't have text nodes directly (else there's a strange issue where a element "leave it to itself"), so I suggest to leave it with only one element (e.g., use a div inside #drop to put everything inside)

Here's a jsfiddle solving the original question (broken) example.

I've also made a simplified version forked from the @Theodore Brown example, but based only in this CSS.

Not all browsers have this CSS implemented, though: http://caniuse.com/pointer-events

Seeing the Facebook source code I could find this pointer-events: none; several times, however it's probably used together with graceful degradation fallbacks. At least it's so simple and solves the problem for a lot of environments.

Community
  • 1
  • 1
H.D.
  • 4,168
  • 1
  • 18
  • 15
  • The pointer-events property is the right solution going forward, but unfortunately it doesn't work in IE8-IE10, which are still widely used. Also, I should point out that your current jsFiddle doesn't even work in IE11, since it doesn't add the necessary event listeners and default behavior prevention. – Theodore Brown Sep 18 '13 at 21:39
  • 3
    Using pointer-events is indeed a good answer, I struggled a bit before finding out by myself, that answer should be higher. – floribon Aug 14 '14 at 00:50
  • Fantastic option for those of us lucky enough to not have to support old browsers. – Luke Hansford Oct 05 '15 at 07:48
  • 14
    What if your children is a Button? o.O – David Mar 15 '16 at 11:47
  • Works great - and you can still add the counter solution for older browsers. Thanks! – BurninLeo May 28 '16 at 18:53
  • this is the best solution so far, just assign an id to the parent element (e.g "parent_element") and in the css file `#parent_element * {pointer-events: none;}` – Alan Deep May 12 '18 at 00:00
  • 19
    it works only if you do not have other controller elements (edit, delete) inside the area, because this solution blocks them too.. – Zoltán Süle Oct 09 '18 at 15:12
  • This should be an accepted answer. Or answer in the top. – Konstantin Kozirev Mar 31 '20 at 11:22
  • 7
    For interactive child elements inside the drop target, such as a button: Add `pointer-events: none;` to a class. Apply the class to the drop target, using _ondragenter_. Then remove the class from the drop target in _ondragleave_. – sudoqux Jun 02 '20 at 12:55
  • @sudoqux You mean apply the task the *children* of the drop target using `.droptarget *` – sw1337 Sep 21 '20 at 18:11
  • But I want to keep my child elements interactive :( – Robo Robok Jun 12 '21 at 19:40
  • This worked for me, both Chrome and Firefox (as of 92.0). A workaround for the disadvantages is to toggle this CSS conditionally to it being dragged over. – Miguel Aug 18 '21 at 20:12
  • 2
    This is *ALMOST* perfect... With one exception: If the children are `input`s of type `checkbox` or `radio`, then `pointer-events: none` will have no effect. This is because `pointer-events` are only those that result in a change of *VALUE* on an element. In the case of `checkbox` and `radio`, these change their *STATE*, not their value. – Jack_Hu Mar 04 '22 at 12:40
  • By having a `boolean` variable like `isDragging`; you can set `pointer-event: 'none'` while `isDragging` is `true`. with this you will have both controller children and a dropzone wrapping around them. @ZoltánSüle – Mehdi Karimi Sep 17 '22 at 15:32
  • For me it helped to additionally set `.dragging-over * { position: relative; z-index: -1; pointer-events: none; }` on the children. – WoodrowShigeru May 29 '23 at 09:39
80

It has been quite some time after this question is asked and a lot of solutions (including ugly hacks) are provided.

I managed to fix the same problem I had recently thanks to the answer in this answer and thought it may be helpful to someone who comes through to this page. The whole idea is to store the evenet.target in ondrageenter everytime it is called on any of the parent or child elements. Then in ondragleave check if the current target (event.target) is equal to the object you stored in ondragenter.

The only case these two are matched is when your drag is leaving the browser window.

The reason that this works fine is when the mouse leaves an element (say el1) and enters another element (say el2), first the el2.ondragenter is called and then el1.ondragleave. Only when the drag is leaving/entering the browser window, event.target will be '' in both el2.ondragenter and el1.ondragleave.

Here is my working sample. I have tested it on IE9+, Chrome, Firefox and 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;
    };
})();

And here is a simple html page:

<!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>

With proper styling what I have done is to make the inner div (#file-drop-area) much bigger whenever a file is dragged into the screen so that the user can easily drop the files into the proper place.

Ali Motevallian
  • 1,250
  • 10
  • 15
  • 6
    This is the best solution, it's better than the counter (especially if you delegate events) and it works with draggable children as well. – Fred Oct 11 '15 at 21:03
  • 5
    This doesn't help tho for the problem of duplicate dragenter events – B T Jul 18 '16 at 08:24
  • 1
    Does this work for "children" that are css pseudo-elements or pseudo-classes? I couldn't get it to, but maybe I was doing it wrong. – Josh Bleecher Snyder Jan 30 '18 at 20:26
  • 1
    As of March 2020 I've had the best luck with this solution as well. Note: some linters may complain about the use of `==` over `===`. You are comparing the event target object references, so `===` works fine too. – Alex Mar 24 '20 at 18:43
  • 2
    Excellent solution - using React was as simple as tracking a new state to reference `onDragEnter` and check against in `onDragLeave`. This should be the accepted answer. – Justin K Apr 01 '22 at 12:13
  • 2
    So far the best answer and it is now 2022 Thank you @Ali Motevallian. – Adam Orłowski Nov 01 '22 at 21:03
60

Here, the simplest Cross-Browser solution (seriously):

jsfiddle <-- try dragging some file inside the box

You can do something like that:

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

In a few words, you create a "mask" inside the dropzone, with width & height inherited, position absolute, that will just show when the dragover starts.
So, after showing that mask, you can do the trick by attaching the others dragleave & drop events on it.

After leaving or dropping, you just hide the mask again.
Simple and without complications.

(Obs.: Greg Pettit advice -- You must be sure that the mask hover the entire box, including the border)

  • Not sure why, but it's not working consistently with Chrome. Sometimes leaving the area keeps the mask visible. – Greg Pettit May 23 '13 at 18:53
  • 3
    Actually, it's the border. Have the mask overlapping the border, with no border of its own, and it should work OK. – Greg Pettit May 23 '13 at 18:59
  • 1
    Note, your jsfiddle has a bug in it, in drag_drop, you should remove the hover class on "#box" not "#box-a" – entropy Dec 16 '13 at 10:30
  • This is a nice solution, but somehow it did not work for me. I finally figure out a workaround. For those of you who are looking for something else. you could try this: https://github.com/bingjie2680/jquery-draghover – bingjie2680 Jun 19 '14 at 12:04
  • No. I do not want to lose the control over the child elements. I've made a simple jQuery plugin that deals with the problem doing the job for us. [Check my answer](http://stackoverflow.com/a/29869208/636561). – Luca Fagioli Apr 25 '15 at 18:58
  • 1
    This solutions works nice when the actual mask is the `#drop::before` or `::after`. Also, beware that sometimes, when dragging fast, the "dragleave" fires before the "dragenter" has finished. If the dragenter add the class/pseudoelement, and the dragleave removes then, this can cause problem. – le hollandais volant Dec 03 '18 at 14:43
  • Following on from le hollandais volant's comment, I was STILL getting issues with flashing due to dropleave and dropenter even with Pointer-events: none . So I basically set a timeout on dropleave that's cleared by dropenter. Sorted. – Tod Sep 21 '20 at 13:37
55

This fairly simple solution is working for me so far, assuming your event is attached to each drag element individually.

if (evt.currentTarget.contains(evt.relatedTarget)) {
  return;
}
Kenneth Spencer
  • 1,392
  • 12
  • 15
  • This is great! thanks for sharing @kenneth, you saved my day! A lot of the other answers only apply if you have a single droppable zone. – antoni Feb 18 '20 at 04:50
  • 2
    For me, it works in Chrome and Firefox, but id doesn't work in Edge. Please see: http://jsfiddle.net/iwiss/t9pv24jo/ – iwis Mar 25 '20 at 13:40
  • If you're dragging over an `input` element `evt.relatedTarget` points to a `div` that is not attached to the document tree because of this ancient bug: https://bugs.chromium.org/p/chromium/issues/detail?id=68629 So the check should be `evt.relatedTarget.parentElement === null || evt.currentTarget.contains(evt.relatedTarget)`. – mono blaine Jul 29 '22 at 20:38
  • 2
    Yeah, I like this solution the best. No wonky coordinate checks, no sledge hammer CSS, no counting of pointers, no masking elements...this fixed my problem and did so elegantly. – Snowie Aug 18 '22 at 15:56
37

The "right" way to solve this issue is to disable pointer events on child elements of the drop target (as in @H.D.'s answer). Here's a jsFiddle I created which demonstrates this technique. Unfortunately, this doesn't work in versions of Internet Explorer prior to IE11, since they didn't support pointer events.

Luckily, I was able to come up with a workaround which does work in old versions of IE. Basically, it involves identifying and ignoring dragleave events which occur when dragging over child elements. Because the dragenter event is fired on child nodes before the dragleave event on the parent, separate event listeners can be added to each child node which add or remove an "ignore-drag-leave" class from the drop target. Then the drop target's dragleave event listener can simply ignore calls which occur when this class exists. Here's a jsFiddle demonstrating this workaround. It is tested and working in Chrome, Firefox, and IE8+.

Update:

I created a jsFiddle demonstrating a combined solution using feature detection, where pointer events are used if supported (currently Chrome, Firefox, and IE11), and the browser falls back to adding events to child nodes if pointer event support isn't available (IE8-10).

Theodore Brown
  • 907
  • 1
  • 11
  • 22
  • This answer shows a workaround for the undesirable firing but neglects the question "Is it possible to prevent dragleave from firing when dragging into a child element?" entirely. – H.D. Sep 03 '13 at 00:34
  • Behaviour can get strange when dragging a file from outside the browser. In Firefox I've got "Entering child -> Entering Parent -> Leaving child -> Entering child -> Leaving child" without leaving the parent, which left with the "over" class. Old IE would need an attachEvent replacement for the addEventListener. – H.D. Sep 03 '13 at 00:34
  • That solution depends strongly on the bubbling, the "false" in all addEventListener should be emphasized as essential (although that's the default behaviour), since many people may not know about that. – H.D. Sep 03 '13 at 00:35
  • That single draggable adds an effect to the dropzone that doesn't appear when the dropzone is used for other draggable objects that doesn't trigger the dragstart event. Perhaps using everything as a dropzone for the effect of dragging while keeping the real dropzone with other handlers would do that. – H.D. Sep 03 '13 at 00:35
  • 1
    @H.D. I updated my answer with info on using the pointer-events CSS property to prevent the `dragleave` event from firing in Chrome, Firefox, and IE11+. I also updated my other workaround to support IE8 and IE9, in addition to IE10. It is intentional that the dropzone effect is only added when dragging the "Drag me" link. Others can feel free to change this behavior as needed to support their use cases. – Theodore Brown Sep 18 '13 at 21:32
  • This will prevent childs from triggering "click" event. – standac May 06 '14 at 18:11
22

if you are using HTML5, you can get the parent's clientRect:

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

Then in the 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

broc.seib
  • 21,643
  • 8
  • 63
  • 62
azlar
  • 460
  • 1
  • 5
  • 12
  • 2
    Excellent answer. – broc.seib Jan 31 '18 at 14:50
  • Thanks buddy, i like the plain javascript approach. – Tim Gerhard Jun 26 '19 at 12:26
  • When using on elements having `border-radius`, moving the pointer close to the corner would *actually leave* the element, but this code would still think we are inside (we have left the element but we are still in the bounding rectangle). Then the `dragleave` event handler won't be called at all. – Jay Dadhania Dec 13 '19 at 11:22
  • This is the most robust way, imo. All other solutions here I've tried fail with some edge case depending on what the children elements are. – Hudon Mar 23 '23 at 16:27
18

A very simple solution is to use the pointer-events CSS property. Just set its value to none upon dragstart on every child element. These elements won't trigger mouse-related events anymore, so they won't catch the mouse over them and thus won't trigger the dragleave on the parent.

Don't forget to set this property back to auto when finishing the drag ;)

FruityFred
  • 241
  • 2
  • 8
18

A simple solution is to add the css rule pointer-events: none to the child component to prevent the trigger of ondragleave. See example:

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>
Alexandre Annic
  • 9,942
  • 5
  • 36
  • 50
  • I had this same problem and your solution works great. Tip for others, this was my case: if the child element you're trying to make ignore the **drag** event is the clone of the element being dragged (trying to achieve a visual preview), you can use this inside **dragstart** when creating the clone: `dragged = event.target;clone = dragged.cloneNode();clone.style.pointerEvents = 'none';` – Scaramouche Nov 02 '18 at 17:51
  • Nice, simple solution. Folks may want to consider just creating a class, `.no-pointer-events {pointer-events: none;}` then add `no-pointer-events` to each child element. Done. – Rob Nov 14 '20 at 00:55
  • 1
    This should be the accepted answer, it's the most simple and there are no hacks involved. :D – Ambrus Tóth Apr 20 '21 at 12:48
  • but what if the child component is a link? – Ayyash Nov 05 '21 at 14:16
16

The problem is that the dragleave event is being fired when the mouse goes in front of the child element.

I've tried various methods of checking to see if the e.target element is the same as the this element, but couldn't get any improvement.

The way I fixed this problem was a bit of a hack, but works 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');
               }
           }
Greg
  • 21,235
  • 17
  • 84
  • 107
  • 1
    Thanks! I can't get this to work in Chrome however. Could you provide a working fiddle of your hack? – pimvdb Jul 18 '12 at 19:31
  • 3
    I was thinking of doing it by checking the coords too. You did most of the work for me, thx :). I had to make some adjustments though: `if (e.x >= (rect.left + rect.width) || e.x <= rect.left || e.y >= (rect.top + rect.height) || e.y <= rect.top) ` – Christof Jul 04 '13 at 14:34
  • 1
    It won't work in Chrome because it's event doesn't have `e.x` and `e.y`. – Hengjie Sep 01 '13 at 11:25
  • 1
    I like this solution. Didn't work in Firefox first. But if you replace e.x with e.clientX and e.y with e.clientY it works. Also works in Chrome. – Nicole Stutz Nov 03 '13 at 15:16
  • Did not work in chrome for me, neither did what [Chris](http://stackoverflow.com/users/682583/chris) or [Daniel Stuts](http://stackoverflow.com/users/1645541/daniel-stutz) suggested – Timo Huovinen Mar 14 '14 at 06:07
  • In chrome use e.originalEvent.x and e.originalEvent.y in place of e.x and e.y. – stonea Jul 02 '15 at 15:50
  • Nice solution but it does not work properly if `e.currentTarget` outlines is not a rectangle. – Max May 01 '19 at 23:41
  • I use even simpler modification of @Christof code: `if (e.x <= rect.left || e.x >= rect.right || e.y <= rect.top || e.y >= rect.bottom)` – iwis Mar 25 '20 at 15:40
  • 1
    Similar to this, I used the entire document as my drop area so I was simply able to check `if (evt.x === 0 && evt.y === 0)` which seems to work perfectly in Chrome – Chris Barr May 24 '23 at 01:41
16

Very simple solution:

parent.addEventListener('dragleave', function(evt) {
    if (!parent.contains(evt.relatedTarget)) {
        // Here it is only dragleave on the parent
    }
}
Owen M
  • 2,585
  • 3
  • 17
  • 38
8

I was having the same issue and tried to use pk7s solution. It worked but it could be done a little bit better without any extra dom elements.

Basicly the idea is same - add an extra unvisible overlay over droppable area. Only lets do this without any extra dom elements. Here is the part were CSS pseudo-elements come to play.

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

This after rule will create a fully covered overlay for droppable area.

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

Here is the full solution: http://jsfiddle.net/F6GDq/8/

I hope it helps anyone with the same problem.

rasmusx
  • 887
  • 8
  • 14
7

And here it goes, a solution for 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!');
                    }
                })
Aldekein
  • 3,538
  • 2
  • 29
  • 33
  • Does it go into «if (typeof event.clientX === 'undefined')»? – Aldekein Dec 17 '12 at 10:19
  • 1
    Worked nicely but there could be another window over the browser, so getting the mouse location and comparing it to the rectangular screen area isn't enough. – H.D. Aug 14 '13 at 11:32
  • I agree with @H.D. Also, this will cause problems when element has large `border-radius` as I explained in my comment on @azlar's answer above. – Jay Dadhania Dec 13 '19 at 11:26
7

Here's another solution using 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');
   }
}

Hope this works, here's a fiddle.

marcelj
  • 598
  • 5
  • 14
  • 3
    This didn't work for me out of the box. I needed to use `event.clientX, event.clientY` instead, since they're relative to the viewport and not the page. I hope that helps another lost soul out there. – Jon Abrams Jun 07 '16 at 22:34
7

You can fix it in Firefox with a little inspiration from the jQuery source code:

dragleave: function(e) {
    var related = e.relatedTarget,
        inside = false;

    if (related !== this) {

        if (related) {
            inside = jQuery.contains(this, related);
        }

        if (!inside) {

            $(this).removeClass('red');
        }
    }

}

Unfortunately it doesn't work in Chrome because relatedTarget appears not to exist on dragleave events, and I assume you're working in Chrome because your example did't work in Firefox. Here's a version with the above code implemented.

robertc
  • 74,533
  • 18
  • 193
  • 177
  • Thanks a lot, but indeed it's Chrome I'm trying to solve this problem in. – pimvdb Aug 19 '11 at 09:54
  • 5
    @pimvdb I see you've logged [a bug](https://bugs.webkit.org/show_bug.cgi?id=66547), I'll just leave a reference to it here in case anyone else comes across this answer. – robertc Aug 19 '11 at 13:58
  • I did indeed, but I forgot to add a link to it here. Thanks for doing that. – pimvdb Aug 19 '11 at 14:00
  • The Chrome bug has been fixed in the mean time. – phk Jul 14 '17 at 17:21
5

Not sure if this cross browser, but I tested in Chrome and it solves my problem:

I want to drag and drop a file over entire page, but my dragleave is fired when i drag over child element. My fix was to look at the x and y of mouse:

i have a div that overlays my entire page, when the page loads i hide it.

when you drag over document i show it, and when you drop on the parent it handles it, and when you leave the parent i check x and 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;
});
chrisallick
  • 1,330
  • 17
  • 18
5

An alternate working solution, a little simpler.

//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) {
Profet
  • 944
  • 1
  • 15
  • 22
  • This doesn't appear to always work in Chrome. I would on occasion receive `clientX` above 0 when the mouse is outside of the box. Granted, my elements are `position:absolute` – Hengjie Sep 01 '13 at 11:37
  • Does it happens all the time or only sometimes ? Because if the mouse is moving too fast (ig. outside the window), you might get wrong values. – Profet Sep 01 '13 at 22:19
  • It happens 90% of the time. There are rare cases (1 out of 10 times) where I can make it reach 0. I'll try again moving the mouse slower, but I couldn't say I was moving quickly (perhaps what you'd call normal speed). – Hengjie Sep 02 '13 at 13:22
  • Using dropzone.js a similiar aproach worked for me: `console.log(e.clientX + "/" + e.clientY ); if (e.clientX == 0 && e.clientY == 0 ) { console.log('REAL leave'); } ` – leosok Nov 11 '20 at 20:56
5

Just check if the dragged over element is a child, if it is, then don't remove your 'dragover' style class. Pretty simple and works for me:

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

I know this is a old question but wanted to add my preference. I deal with this by adding class triggered css :after element at a higher z-index then your content. This will filter out all the garbage.

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

Then just add the drag-over class on your first dragenter event and none of the child elements trigger the event any longer.

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

dragLeave(event){
 dropElement.classList.remove('drag-over');
}
Chris Kirby
  • 147
  • 1
  • 3
4

I've stumbled into the same problem and here's my solution - which I think is much easier then above. I'm not sure if it's crossbrowser (might depend on even bubbling order)

I'll use jQuery for simplicity, but solution should be framework independent.

The event bubbles to parent either way so given:

<div class="parent">Parent <span>Child</span></div>

We attach events

el = $('.parent')
setHover = function(){ el.addClass('hovered') }
onEnter  = function(){ setTimeout(setHover, 1) }
onLeave  = function(){ el.removeClass('hovered') } 
$('.parent').bind('dragenter', onEnter).bind('dragleave', onLeave)

And that's about it. :) it works because even though onEnter on child fires before onLeave on parent, we delay it slightly reversing the order, so class is removed first then reaplied after a milisecond.

Marcin Raczkowski
  • 1,500
  • 1
  • 18
  • 26
  • Only thing this snippet does is preventing the 'hovered' class to be removed by reapplying it the next tick cycle (making the 'dragleave' event useless). – null Oct 12 '13 at 08:30
  • It's not useless. If you leave parent it'll work as expected. The power of this solution is it's simplicity, it's not ideal or best there is. Better solution would be to mark an enter on a child, in onleave check if we just entered child, and if so not trigger leave event. It'll hovever need testing, extra guards, checing for grandchildren, etc. – Marcin Raczkowski Oct 15 '13 at 08:46
4

I've written a little library called Dragster to handle this exact issue, works everywhere except silently doing nothing in IE (which doesn't support DOM Event Constructors, but it'd be pretty easy to write something similar using jQuery's custom events)

Ben
  • 669
  • 1
  • 9
  • 20
3

I wrote a drag-and-drop module called drip-drop that fixes this weirdo behavior, among others. If you're looking for a good low-level drag-and-drop module you can use as the basis for anything (file upload, in-app drag-and-drop, dragging from or to external sources), you should check this module out:

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

This is how you would do what you're trying to do in 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')
    }
  })
})

To do this without a library, the counter technique is what I used in drip-drop, tho the highest rated answer misses important steps that will cause things to break for everything except the first drop. Here's how to do it properly:

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
    }
});
B T
  • 57,525
  • 34
  • 189
  • 207
3

I found a simple solution to this problem so sharing it. It works well in my case.

jsfiddle try it.

You can actually achieve this only via the dragenter event and you don't even need to register a dragleave. All you need is to have a no-drop area around your dropzones and that's it.

You can also have nested dropzones and this works perfectly. Check this as well nested dropzones.

$('.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");
  }
});
Abubakar Azeem
  • 314
  • 1
  • 4
  • 14
  • 1
    I tried this solution, but it should be noted it has a limitation: when pressing Esc to cancel the drag&drop action, no one will remove the "over" class from your dropzone element. If you are trying to use "dragleave" event for this, you just go to the first problem the original author asked about. So both hovering a child element and pressing Esc key while dragging will raise "dragleave" event on the dropzone. Maybe we also need to listen for the Esc key to remove the "over" class from the dropzone... – mar10 Jul 20 '21 at 18:10
  • hi @mar10, thanks for pointing this issue, i will update my answer. i think we can use `dragend` event to handle this, but i will need to test that. – Abubakar Azeem Jul 22 '21 at 15:56
  • 1
    I haven't found a way to detect if the `dragend` event is triggered by releasing a mouse button or pressing the Esc key, so going with the `dragend` event will complicate the logic, so the simple solution is to detect the Escape key as you said. I've updated the answer. – Abubakar Azeem Jul 22 '21 at 16:36
2

After spending so many hours I got that suggestion working exactly as intended. I wanted to provide a cue only when files were dragged over, and document dragover, dragleave was causing painful flickers on Chrome browser.

This is how I solved it, also throwing in proper cues for user.

$(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
});
Brad Larson
  • 170,088
  • 45
  • 397
  • 571
visitsb
  • 541
  • 4
  • 8
2

"dragleave" event is fired when mouse pointer exits the dragging area of the target container.

Which makes a lot of sense as in many cases only the parent may be droppable and not the descendants. I think event.stopPropogation() should have handled this case but seems like it doesn't do the trick.

Above mentioned some solutions do seem to work for most of the cases, but fails in case of those children which does not support dragenter / dragleave events, such as iframe.

1 workaround is to check the event.relatedTarget and verify if it resides inside the container then ignore the dragleave event as I have done here:

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

You can find a working fiddle here!

2

Solved ..!

Declare any array for ex:

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

No need to worry about child elements.

Pratham
  • 1,522
  • 1
  • 18
  • 33
2

I struggeled a LOT with this, even after reading through all of these answers, and thought I may share my solution with you, because I figured it may be one of the simpler approaches, somewhat different though. My thought was of simply omitting the dragleave event listener completely, and coding the dragleave behaviour with each new dragenter event fired, while making sure that dragenter events won't be fired unnecessarily.

In my example below, I have a table, where I want to be able to exchange table row contents with each other via drag & drop API. On dragenter, a CSS class shall be added to the row element into which you're currently dragging your element, to highlight it, and on dragleave, this class shall be removed.

Example:

Very basic HTML table:

<table>
  <tr>
    <td draggable="true" class="table-cell">Hello</td>
  </tr>
  <tr>
    <td draggable="true" clas="table-cell">There</td>
  </tr>
</table>

And the dragenter event handler function, added onto each table cell (aside dragstart, dragover, drop, and dragend handlers, which are not specific to this question, so not copied here):

/*##############################################################################
##                              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;
  }
}

Very basic comments left in code, such that this code suits for beginners too. Hope this will help you out! Note that you will of course need to add all the event listeners I mentioned above onto each table cell for this to work.

DevelJoe
  • 856
  • 1
  • 10
  • 24
1

You can use a timeout with a transitioning flag and listen on the top element. dragenter / dragleave from child events will bubble up to the container.

Since dragenter on the child element fires before dragleave of the container, we will set the flag show as transitioning for 1ms... the dragleave listener will check for the flag before the 1ms is up.

The flag will be true only during transitions to child elements, and will not be true when transitioning to a parent element (of the container)

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/

ilovett
  • 3,240
  • 33
  • 39
1

I had a similar problem — my code for hiding the dropzone on dragleave event for body was fired contatantly when hovering child elements making the dropzone flicker in Google Chrome.

I was able to solve this by scheduling the function for hiding dropzone instead of calling it right away. Then, if another dragover or dragleave is fired, the scheduled function call is cancelled.

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);
Arseny
  • 5,159
  • 4
  • 21
  • 24
1

Just add this to child elements:

pointer-events: none;
Kullpoint
  • 11
  • 2
0

Here is another approach based on the timing of events.

The dragenter event dispatched from the child element can be captured by the parent element and it always occurs before the dragleave. The timing between these two events is really short, shorter than any possible human mouse action. So, the idea is to memorize the time when a dragenter happens and filter dragleave events that occurs "not too quickly" after ...

This short example works on Chrome and 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
    }
})
smrtl
  • 604
  • 5
  • 7
0

You need to remove the pointer events for all child objects of the drag target.

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) {
                //
            }
        }
    }
EddieB
  • 4,991
  • 3
  • 23
  • 18
0

use this code 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');
             }
        }
    });
Mohammad Rezaei
  • 241
  • 3
  • 16
0

Just try to use the event.eventPhase. It will set to 2 (Event.AT_TARGET) only if the target ist entered, otherwise it is set to 3 (Event.BUBBLING_PHASE).

I'fe used the eventPhase to bind or unbind the dragleave Event.

$('.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

GNaruhn
  • 23
  • 5
0

I found a similar but more elegant solution to @azlar's answer, and this is my solution:

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

This method detects whether the mouse has left the page. It works well in Chrome and Edge.

zysite
  • 1
0

Here's my solution (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

I was able to do this using timeout on dragleave. Unlike other answers with this approach I think that it is crucial to reset this timeout on dragover to avoid flickering.

I'm writing just the functions you need then to bind using the framework of your choice

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);
  }
}
Martin Vich
  • 1,062
  • 1
  • 7
  • 22
0

The accepted answer will probably work in some cases, but if you have multiple overlaying child elements it will fall apart very quickly.

Depending on your use case, the top answer from this post could be much easier and cleaner

'dragleave' of parent element fires when dragging over children elements

  • 1
    Welcome to Stack Overflow! Make sure to take the [tour] as it should point you in the right direction. I'd also like to ask You [not to use nonpermanent indicators to refer to a post](https://meta.stackoverflow.com/questions/410553/how-to-improve-vague-references-to-comments-post-in-a-post-that-use-variables-li) (Could You please fix that one yourself? Even now I'm not certain which answers You were refering to). – IWonderWhatThisAPIDoes Aug 10 '21 at 17:56
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);
  }
Alex Nikulin
  • 8,194
  • 4
  • 35
  • 37
0

I see a couple answers about making use of getBoundingclientRect() to detect 'true' leave. But presented solutions don't take into account any elements that are within DOM structure of their parent but are outside while rendered (like nav arrows with bottom: 100% etc). This does:

element.addEventListener('dragleave', function (e) {
    if (!this.contains(document.elementFromPoint(e.clientX, e.clientY))) {    
        // do something
    }
}
pmad
  • 89
  • 6
-1

pimvdb..

Why don't you try out using drop instead of dragleave. It worked for me. hope this solves your problem.

Please check the 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");
                 }
                });
abhi
  • 7
  • 1
  • 2
    If the user changes their mind and drags the file back out of the browser, this will remain red. – ZachB May 02 '15 at 23:53