I'd like to handle dragend
events differently depending on whether an element has been just dragged inside the browser window (or site resp.) or or outside, e.g. to an external file manager.
After I didn't find any attribute of the DragEvent
instance indicating whether it's inside or outside the sites context I started to arithmetically figure out if the corresponding mouse event still takes place inside the geometry of the site.
Eventually I might succeed with that approach (currently not working yet) but it has one major disadvantage (leaving alone its ugliness): the drop target window might be on top of the browser, so the geometry is no real indicator at all..
so.. how do I find out if a dragend
(or any other event I could use to store some state) is pointing outside of the browser window (or source site)?
I'd like to handle dragend
events differently depending on whether an element has been just dragged inside the browser window (or site resp.) or or outside, e.g. to an external file manager.
After I didn't find any attribute of the DragEvent
instance indicating whether it's inside or outside the sites context I started to arithmetically figure out if the corresponding mouse event still takes place inside the geometry of the site.
Eventually I might succeed with that approach (currently not working yet) but it has one major disadvantage (leaving alone its ugliness): the drop target window might be on top of the browser, so the geometry is no real indicator at all..
so.. how do I find out if a dragend
(or any other event I could use to store some state) is pointing outside of the browser window (or source site)?
3 Answers
Reset to default 4 +150I couldn't find any super straightforward ways to do this, but I could do it fairly concisely with a handful of listeners. If you're ok with having a variable to assist with the state then you can do something like this.
First, listen for the mouse leaving on a drag event. I've found the most reliable way to do this is to use a dragleave
listener and then examine the event data to make sure it's really leaving the window. This event runs a ton though, so we need to filter out the ones we need.
dragleave
runs every time any element's drop zone is left. To make sure that the drag event is just leaving the page, we can check the target to make sure leaving the html
or body
tag and going to null
. This explains how to see the correct targets for the event.
Right before the dragend
event, dragleave
is ran as though it left the window. This is problematic because it makes every drop seem as though it were out of the window. It seems that this behavior isn't well defined in the specs and there is some variation between how Firefox and Chrome handle this.
For Chrome, we can make the code in dragleave
run a cycle after the code in dragend
by wrapping it in a timeout with 0 seconds.
This doesn't work well in Firefox though because the dragend
event doesn't e as fast. Firefox does, however, set buttons
to 0 so we know it's the end of an event.
Here is what the dragleave
event listener might look like
window.addEventListener('dragleave', (e) => {
window.setTimeout(() => {
if ((e.target === document.documentElement || e.target === document.body) && e.relatedTarget == null && e.buttons != 0) {
outside = true;
}
});
});
From here we just need to do something similar to see when the drag re-enters the viewport. This won't run before dragend
like dragleave
so it is simpler.
window.addEventListener('dragenter', (e) => {
if ((e.target === document.documentElement || e.target === document.body) && e.relatedTarget == null) {
outside = false;
}
});
It's also a good idea to reset outside
every time the drag event starts.
element.addEventListener('dragstart', (e) => {
outside = false;
});
Now it's possible in the dragend
event listener to see where the drop ended.
element.addEventListener('dragend', (e) => {
console.log('Ended ' + (outside ? 'Outside' : 'Inside'));
});
Here is a snippet with everything put together (or fiddle)
Note #1: You'll need to drag the element out of the browser window, not just the demo window for it to appear as "outside".
Note #2: There has to be a better way for stopping the last dragleave
event, but after a few hours of trying other things, this seemed the most consistent and reliable.
const element = document.querySelector('div');
var outside = false;
element.addEventListener('dragend', (e) => {
console.log('Ended ' + (outside ? 'Outside' : 'Inside'));
});
element.addEventListener('dragstart', (e) => {
outside = false;
});
window.addEventListener('dragleave', (e) => {
window.setTimeout(() => {
if ((e.target === document.documentElement || e.target === document.body) && e.relatedTarget == null && e.buttons != 0) {
outside = true;
}
});
});
window.addEventListener('dragenter', (e) => {
if ((e.target === document.documentElement || e.target === document.body) && e.relatedTarget == null) {
outside = false;
}
});
div[draggable] {
width: fit-content;
margin-bottom: 32px;
padding: 16px 32px;
background-color: black;
color: white;
}
<div draggable="true">Drag Me</div>
This might help. You can click 'Run code snippet' to see how it works
Note: Increasing the offset would help detect the drag out sooner, but might affect precision (whether is has actually been dragged out or right on the edge)
/* events fired on the draggable target */
let offset = 2; // in case required
let width = window.innerWidth;
let height = window.innerHeight;
console.log('Starting screen width: ' + width);
console.log('Starting screen height: ' + height);
document.addEventListener("drag", function(event) {
let posX = event.pageX;
let posY = event.pageY;
console.log('X:' + posX + ' Y:' + posY)
let isExceedingWidth = posX >= (width - offset) || posX <= (0 + offset);
let isExceedingHeight = posY >= (height - offset) || posY <= (0 + offset);
if (isExceedingWidth || isExceedingHeight) {
console.log('dragged out');
} else {
console.log('in');
}
}, false);
#draggable {
width: fit-content;
padding: 1px;
height: fit-content;
text-align: center;
background: black;
color: white;
}
<div id="draggable" draggable="true">
Drag Me
</div>
Here is a possibly controversial, but more programmatic alternative that worked well for me.
Create a function wrapper that takes a callback that will fire every time something is dragged into or out of the window:
export interface OutsideWindow {(outside: boolean): void}
export interface OutsideCleanup {(): {(): void}}
export const whenOutsideWindow = (onChangeCB: OutsideWindow): OutsideCleanup => {
const windowDragLeaveHandler = () => onChangeCB(true);
window.addEventListener('dragleave', windowDragLeaveHandler);
const bodyDragLeaveHandler = (e: DragEvent) => e.stopPropagation();
document.body.addEventListener('dragleave', bodyDragLeaveHandler);
const windowDragEnterHandler = () => onChangeCB(false);
window.addEventListener('dragenter', windowDragEnterHandler);
return () => () => {
window.removeEventListener('dragleave', windowDragLeaveHandler);
document.body.removeEventListener('dragleave', bodyDragLeaveHandler);
window.removeEventListener('dragenter', windowDragEnterHandler);
}
}
A few things to note:
- This function passes a boolean value into its callback. In this case, it will pass a value, true, when outside the browser window and false when inside it
- It helps to know a little about bubbling and stopPrapogation(), but basically, we are making the window's child element, body, stop propagating the dragleave event, so it will never reach its parent, window
- This only works in one direction, so there is no way to do a similar thing for the dragenter event, thus this will fire every time the draggable is dragged iinto a child element.
- It is a good idea to cleanup any events you add to the DOM, so this function returns its cleanup function for you to call later.
- The return value of this function is a function that returns a function. More on that below
That all looks a little messy, but it provides a relatively clean interface to use
let outside = false;
let cleanup = () => {};
// Probably inside onDragStart()
const foo = (out) => { outside = out; }
cleanup = whenOutsideWindow( outside => foo(outside) );
...
// Later in your code, when you want to cleanup...
// Probably inside onDragEnd()
cleanup()();
No, the multiple calls to cleanup is not a mistake. Remember that whenOutsideWindow returns a function that returns a function. This is because when a function is returned in javascript, it is run immediately. I have no idea why. Hopefully someone in the ments can shed some light on that.
But the important things is that if whenOutsideWindow returned a simple void function...
export interface OutsideCleanup {(): void}
...our dragstart and dragend events would be added and removed immediately. We get around this by calling the function that is returned from the function that is returned by whenOutsideWindow.
cleanup()();