Search code examples
javascriptdom-eventspreventdefault

Detect onmouseup/onmousemove even if preventDefault is used


I asked this question: PDF.JS overlay can not be made draggable.

I noticed that PDF.js seemed to be blocking the document.onmousemove and document.onmouseup events from being fired.

I thought the solution would be to use the element's onmousemove and onmouseup however that comes with its problems.

For example, if you move your mouse too fast the element's onmousemove and onmouseup events will stop firing. Same things goes if you try and drag it out of bounds, the element's events will stop firing. You can try for yourself in the snippet below.

// Modified from https://www.w3schools.com/howto/howto_js_draggable.asp

function dragElement(elmnt, _bounds) {
    var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
    elmnt.onmousedown = dragMouseDown;
    let bounds = [..._bounds];
    bounds[2] -= +elmnt.style.width.slice(0, -2);
    bounds[3] -= +elmnt.style.height.slice(0, -2);
    
    function dragMouseDown(e)
    {
        e = e || window.event;
        e.preventDefault();
        pos3 = e.clientX;
        pos4 = e.clientY;
        elmnt.onmouseup = closeDragElement; // Originally document.onmouseup
        elmnt.onmousemove = elementDrag; // Originally document.onmousemove
    }
    
    function elementDrag(e)
    {
        e = e || window.event;
        e.preventDefault();
        pos1 = pos3 - e.clientX;
        pos2 = pos4 - e.clientY;
        pos3 = e.clientX;
        pos4 = e.clientY;
        let yC = elmnt.offsetTop - pos2, xC = elmnt.offsetLeft - pos1;
        
        if (xC < bounds[0]) xC = bounds[0];
        else if (xC > bounds[2]) xC = bounds[2];
        
        if (yC < bounds[1]) yC = bounds[1];
        else if (yC > bounds[3]) yC = bounds[3];
        
        elmnt.style.top = yC + "px";
        elmnt.style.left = xC + "px";
    }
    
    function closeDragElement()
    {
        elmnt.onmouseup = null; // Originally document.onmouseup
        elmnt.onmousemove = null; // Originally document.onmousemove
    }
}

dragElement(toDrag, [0, 0, 200, 200]);
<div style="
    position: absolute;
    left:     0;
    top:      0;
    width:    200px;
    height:   200px;
    border:   1px solid black
" onmousedown="event.preventDefault()" onmouseup="event.preventDefault()">
    <h1 style="text-align:center">My PDF</h1>
    <div style="
        position: absolute;
        left:     150px;
        top:      150px;
        width:    25px;
        height:   25px;
        border:   1px solid black;
    ">
    </div>
    <div id=toDrag style="
        cursor:   move;
        position: absolute;
        left:     10px;
        top:      25px;
        width:    25px;
        height:   25px;
        border:   1px solid black;
        background-color: #cac;
        border-radius:    25px
    ">
    </div>
</div>

My question is there anyway of detecting mousemove and mouseup even if event.preventDefault() has been fired.

My best guess would be something like:

document.querySelectorAll('*').forEach(e => e.addEventListener('mousemove', elementDrag);
document.querySelectorAll('*').forEach(e => e.addEventListener('mouseup', closeDragElement);

However, I worry that this could impact performance especially on mousemove.


Solution

  • DOM event propagation works in three phases: the capture phase, the target phase and the bubble phase. Roughly, the event first works its way down to the target, reaches the target and then works its way back up.

    EventTarget.addEventListener() has an optional parameter useCapture (see docs):

    useCapture Optional

    A boolean value indicating whether events of this type will be dispatched to the registered listener before being dispatched to any EventTarget beneath it in the DOM tree. Events that are bubbling upward through the tree will not trigger a listener designated to use capture. Event bubbling and capturing are two ways of propagating events that occur in an element that is nested within another element, when both elements have registered a handle for that event. The event propagation mode determines the order in which elements receive the event. See DOM Level 3 Events and JavaScript Event order for a detailed explanation. If not specified, useCapture defaults to false.

    That being said, you could try something like that to listen event on capture phase, instead of bubble phase:

    document.addEventListener('mousemove', elementDrag, true);
    document.addEventListener('mouseup', closeDragElement, true);