I asked a related question here
I'm trying to stop click events if dragged.
I think the simplest version of this is dragging yourself. Basically IF the user presses down, then moves, then releases I don't want a click event.
Note, the code below is not trying to allow click, it's trying only to prevent it. I thought calling preventDefault
in mouseup
would tell the browser, don't do the default thing, that being sending a click
event because the user let up on the mouse.
let dragTarget;
let dragMouseStartX;
let dragMouseStartY;
let dragTargetStartX;
let dragTargetStartY;
const px = v => `${v}px`;
function dragStart(e) {
e.preventDefault();
e.stopPropagation();
dragTarget = this;
const rect = this.getBoundingClientRect();
dragMouseStartX = e.pageX;
dragMouseStartY = e.pageY;
dragTargetStartX = (window.scrollX + rect.left) | 0;
dragTargetStartY = (window.scrollY + rect.top) | 0;
window.addEventListener('mousemove', dragMove, {passive: false});
window.addEventListener('mouseup', dragStop, {passive: false});
}
function dragMove(e) {
if (dragTarget) {
e.preventDefault();
e.stopPropagation();
const x = dragTargetStartX + (e.pageX - dragMouseStartX);
const y = dragTargetStartY + (e.pageY - dragMouseStartY);
dragTarget.style.left = px(x);
dragTarget.style.top = px(y);
}
}
function dragStop(e) {
e.preventDefault();
e.stopPropagation();
dragTarget = undefined;
window.removeEventListener('mousemove', dragMove);
window.removeEventListener('mouseup', dragStop);
}
document.querySelector('.drag').addEventListener('mousedown', dragStart);
document.querySelector('.drag').addEventListener('click', () => {
console.log('clicked', new Date());
});
body { height: 100vh; }
.drag {
background: red;
color: white;
padding: 1em;
position: absolute;
user-select: none;
cursor: pointer;
}
<div class="drag">drag and release me</div>
One solution is to remove the click event and just do it myself in mouseup
. If there was no movement call whatever I was going to call for click
But, in my actual use case dragging is on the parent like this (you can drag red or blue)
let dragTarget;
let dragMouseStartX;
let dragMouseStartY;
let dragTargetStartX;
let dragTargetStartY;
const px = v => `${v}px`;
function dragStart(e) {
e.preventDefault();
e.stopPropagation();
dragTarget = this;
const rect = this.getBoundingClientRect();
dragMouseStartX = e.pageX;
dragMouseStartY = e.pageY;
dragTargetStartX = (window.scrollX + rect.left) | 0;
dragTargetStartY = (window.scrollY + rect.top) | 0;
window.addEventListener('mousemove', dragMove, {passive: false});
window.addEventListener('mouseup', dragStop, {passive: false});
}
function dragMove(e) {
if (dragTarget) {
e.preventDefault();
e.stopPropagation();
const x = dragTargetStartX + (e.pageX - dragMouseStartX);
const y = dragTargetStartY + (e.pageY - dragMouseStartY);
dragTarget.style.left = px(x);
dragTarget.style.top = px(y);
}
}
function dragStop(e) {
e.preventDefault();
e.stopPropagation();
dragTarget = undefined;
window.removeEventListener('mousemove', dragMove);
window.removeEventListener('mouseup', dragStop);
}
document.querySelector('.drag').addEventListener('mousedown', dragStart);
document.querySelector('.click').addEventListener('click', () => {
console.log('clicked', new Date());
});
body { height: 100vh; }
.drag {
background: blue;
color: white;
padding: 2em;
position: absolute;
user-select: none;
cursor: pointer;
}
.click {
padding: 1em;
background: red;
}
<div class="drag"><div class="click">drag and release me</div></div>
So now the 2 elements are not related directly but, if the user drags on red I don't want the inner element to get a click event. Also note in my real code there are lots of child elements that I don't want to receieve click events in the same way (parent is the drag target). Note: again, in the example above I'm just trying to stop all click events (calling preventDefault) and failing.
I can think of lots of hacky solutions, for example 2 are.
In the first mousemove event, search all children for click event listeners, remove all of them, on mouseup restore them (possibly after a timeout)
In mouseup, set a flag to ignore clicks and set a timeout to clear the flag, have all click listeners no-op if the flag is set.
Both of those require a bunch of coordination.
In the first, I'd need to write some kind of system to keep track of click handlers and the elements they are on so I can save and restore them so instead of elem.addEventListener('click', someHandler)
it would have to be more like registerClickListener(elem, someHandler)
. Not hard but if I forget then it fails.
In the second I'd have to remember to always check some global variable in every listener implemention.
elem.addEventListener('click', () => {
if (ignoreClicks) return;
...
})
Again if I forget then it fails. By forget I mean much much deeper in the DOM in unrelated code.
Both seem semi error prone so wondering if there is some other solution I'm overlooking that works like I thought preventDefault
would work.
I could wrap addEventListener
so for click handlers it adds a wrapper to filter out unwanted clicks. That's less error prone but it seems overkill.
Am I missing a simpler solution?