Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML5

PointerJS | Mouse Capturing by DOM Elements - Efficiently

5.00/5 (2 votes)
17 Feb 2017CPOL5 min read 10K  
Implementation of mouse capturing for HTML5 Broswsers

Introduction

Mouse capturing is a very important feature in Visuals development. When a visual object captures the mouse, all mouse related events are treated as if the object with mouse capture perform the event, even if the mouse pointer is over another object or even completely outside the viewport. Unfortunately, this feature is available only on Internet Explorer and Firefox !!!

Background

To understand how pointerjs works, you must have an idea about event propagation phases (Capture and Bubble) and CustomEvents.

Technology Comparison

Other mouse APIs as DragDrop and PointerLock APIs have more or less a similar functionality but to be accurate, there are big differences in their target scope and use cases.

1. Pointer Lock API

From MDN, provides input methods based on the movement of the mouse over time (i.e., deltas), not just the absolute position of the mouse cursor in the viewport. It gives you access to raw mouse movement, locks the target of mouse events to a single element, eliminates limits on how far mouse movement can go in a single direction, and removes the cursor from view. It is ideal for first person 3D games.

Why not to use Pointer Lock API as a workaround?

  • It is suitable for 3D games.
  • It hides the cursor.

2. Drag Drop API

From MDN, User can select draggable elements with a mouse, drag the elements to a droppable element, and drop those elements by releasing the mouse button. A translucent representation of the draggable elements follows the mouse pointer during the drag operation.

Why not to use Drag Drop API as a workaround ?

  • It is suitable for data and file transfer.
  • It creates another translucent visual representation of the draggable element which is not suitable for many cases
  • Events are available only on the draggable and droppable elements.
  • It can move your data outside your application if data is dropped outside your web page even on native app.

PointerJS

PointerJS is a small library that provides a convenient, lightweight and cross-browser implementation for Mouse capturing. It is pure JavaScript so it can work on anything JavaScript.

The idea behind pointerjs is detecting pointer events in capture state at document level and redirect them to the captured DOM if found.

Lacks testing. you can use PointerJS in production at your own risk.

How It Works

As a basic idea, PointerJS adds listeners on document object for all pointer related events to detect the native browser events as (mousemove, mousedown, mouseup, mousewheel, click). These listeners do the following:

  1. Prevent current event propagation if an element is captured or passes it normally if there is no captured element.
  2. Creates a new CustomEvent and passes it to the captured object if found.
  3. Saves the pointer clientX, clientY as the current mouse position so we can know the mouse position on other events like keyboard events.
JavaScript
var documentCaptureHandler = function (e) {
    lastPointerPosition.x = e.clientX;
    lastPointerPosition.y = e.clientY;
    if (captured) {
        //stop this event, their is a captured element
        e.stopPropagation();
        e.preventDefault();
        //try to get the capture event for current event
        //(e.g if event = mousedown , the return event will be capturemousedown)
        //to split events raised by PointerJS from native events
        var captureEvent = getCaptureEvent(e.type, e);
        // if event not found in raiseEvents it will be null.
        if (captureEvent) {
            captured.dispatchEvent(captureEvent);
        }
    }
};

Examples

Drag

Object dragging depends on adding the difference between old and current mouse position to the current object position...

JavaScript
obj.left = obj.left + mouseX - oldMouseX;
obj.top = obj.top + mouseY - oldMouseY;

...capture the mouse on mouse down => move object on mouse move => release the object on mouse up.

To be noted that after calculating the new position, we set the current mouse position as the old position for the next move.

JavaScript
p = pp;

The code is as follows:

JavaScript
<html>
<head>
    <script src="../js/index.js"></script>
    <script src="../js/utility.js"></script>
    <script src="../js/capture.js"></script>
</head>

<body>
    <style>

        .dv { 
            left:0px;
            top : 0px;
            width : 100px;
            height : 100px;
            position:absolute;
            background : red;
        }

    </style>

<div id="dv" class="dv">
</div>

<script>

    var lp = { x: 0, y: 0 };
    var p = { x: 0, y: 0 };

    var dvMouseDown = function (event) {
        console.log("mousedown");
        p = { x: event.clientX, y: event.clientY };
        PointerJS.CaptureHelper.capture(event.target);
    }

    var dvCaptureMouseMove = function (event) {
        console.log("capturemousemove");
        var pp = { x: event.clientX, y: event.clientY };
        //  console.log(event);
        lp = { x: lp.x + pp.x - p.x, y: lp.y + pp.y - p.y }
        p = pp;
        event.target.style.left = lp.x + "px";
        event.target.style.top = lp.y + "px";
    }

    var dvCaptureMouseUp = function (event) {
        console.log("capturemouseup");
        PointerJS.CaptureHelper.release();
    }

    var dv = document.getElementById('dv');

    dv.addEventListener('mousedown', dvMouseDown);
    dv.addEventListener('capturemousemove', dvCaptureMouseMove);
    dv.addEventListener('capturemouseup', dvCaptureMouseUp);

</script>
</body>
</html>

Draw

HTML
<html>
<head>
    <script src="../js/index.js"></script>
    <script src="../js/utility.js"></script>
    <script src="../js/capture.js"></script>
</head>

<body>
    <style>

        #container {
            left:0px;
            top : 0px;
            width : 300px;
            height : 300px;
            background : gray;
            cursor: pointer;
        }
        #rct { 
            left : 0px;
            top : 0px;
            position:absolute;
            background : red;
        }

    </style>

<div id="container">
    <div id="rct">
    </div>
</div>

<script>
    var clientPosition = { x: 0, y: 0 };
    var pos = { x: 0, y: 0 };
    var sz = { w: 0, h: 0 };

    var rct = document.getElementById('rct');
    var cont = document.getElementById('container');

    var setRect = function () {
        console.log("setRect");
        var x, y, r, b, w, h;

        x = Math.min(pos.x, pos.x + sz.w);
        y = Math.min(pos.y, pos.y + sz.h);
        r = Math.max(pos.x, pos.x + sz.w);
        b = Math.max(pos.y, pos.y + sz.h);

        w = r - x;
        h = b - y;

        rct.style.left = x + "px";
        rct.style.top = y + "px";
        rct.style.width = w + "px";
        rct.style.height = h + "px";
    }

    var contMouseDown = function (event) {
        console.log("mousedown");
        console.log(event);
        clientPosition = { x: event.clientX, y: event.clientY };
        pos = { x: event.x, y: event.y };
        sz = { w: 0, h: 0 };

        setRect();

        PointerJS.CaptureHelper.capture(cont);
    }

    var contCaptureMouseMove = function (event) {
        console.log("capturemousemove");
        var p = { x: event.clientX, y: event.clientY };
        sz.w = p.x - clientPosition.x;
        sz.h = p.y - clientPosition.y;
        setRect();
    }

    var contCaptureMouseUp = function (event) {
        console.log("capturemouseup");
        PointerJS.CaptureHelper.release();
    }

    cont.addEventListener('mousedown', contMouseDown);
    cont.addEventListener('capturemousemove', contCaptureMouseMove);
    cont.addEventListener('capturemouseup', contCaptureMouseUp);

</script>
</body>
</html>

Resize

HTML
<html>

<head>
    <script src="../js/index.js"></script>
    <script src="../js/utility.js"></script>
    <script src="../js/capture.js"></script>
</head>

<body>
    <style>
        #rct { 
            left : 0px;
            top : 0px;
            position:absolute;
            background : red;
        }

        .rsz-point {
            background: white;
            border: black 1.5px solid;
            border-radius : 2px;
            width : 8px;
            height : 8px;
            position : absolute;
        } 

    </style>

<div id="rct">
</div>

<div id="bottom" class="rsz-point">
</div>
<div id="right" class="rsz-point">
</div>

<script>
    var p = { x: 0, h: 0 };
    var sz = { w: 300, h: 200 };

    var rct = document.getElementById('rct');
    var btm = document.getElementById('bottom');
    var rht = document.getElementById('right');

    var setPositions = function () {
        btm.style.left = sz.w / 2 - 5;
        btm.style.top = sz.h - 5; // current width - (width / 2 + border-width / 2)

        rht.style.left = sz.w - 5;
        rht.style.top = sz.h / 2 - 5;

        rct.style.width = sz.w + "px";
        rct.style.height = sz.h + "px";
    }

    var mouseDown = function (event) {
        console.log("mousedown");
        console.log(event);
        p = { x: event.clientX, y: event.clientY };

        PointerJS.CaptureHelper.capture(event.currentTarget);
        console.log(event.currentTarget);
        event.preventDefault();
        event.stopPropagation();
    }

    var rightCaptureMouseMove = function (event) {
        console.log("rightCaptureMouseMove");
        var pp = { x: event.clientX, y: event.clientY };
        sz.w += pp.x - p.x;
        p = pp;
        // sz.h += pp.y - p.y;
        setPositions();

        if (event.originalEvent) {
            event.originalEvent.preventDefault();
            event.originalEvent.stopPropagation();
        }
    }

    var bottomCaptureMouseMove = function (event) {
        console.log("bottomCaptureMouseMove");
        var pp = { x: event.clientX, y: event.clientY };
        // sz.w += pp.x - p.x;
        sz.h += pp.y - p.y;
        p = pp;
        setPositions();

        if (event.originalEvent) {
            event.originalEvent.preventDefault();
        }
    }
    var captureMouseUp = function (event) {
        console.log("capturemouseup");
        PointerJS.CaptureHelper.release();
        if (event.originalEvent) {
            event.originalEvent.preventDefault();
        }
    }

    rht.addEventListener('mousedown', mouseDown);
    rht.addEventListener('capturemousemove', rightCaptureMouseMove);
    rht.addEventListener('capturemouseup', captureMouseUp);

    btm.addEventListener('mousedown', mouseDown);
    btm.addEventListener('capturemousemove', bottomCaptureMouseMove);
    btm.addEventListener('capturemouseup', captureMouseUp);

    setPositions();

</script>

</body>

</html

Performance

PointerJS creates one listener for each required mouse event to be listened. Default configuration creates the (mousedown, mousemove, mouseover, mouseup, mousewheel, mouseenter, mouseleave and click) listeners on document object at capture stage.

All these listeners normally don't affect performance if you followed the best practices with one exception which is "mousemove". The mousemove event is different because it fires a new event and executes all mousemove listeners on each mouse move over the document it may fire tens to hundreds of times at one second. Fortunately, PointerJS takes care and executes a minimal of 3 lines of codes for each event fire if there is no captured element.

JavaScript
//store the current pointer position
lastPointerPosition.x = e.clientX;
lastPointerPosition.y = e.clientY;
if (captured) {
    // do some logic
}

So the performance hitting is minimized to a level that normal pointer input handling can be executed 100,000,000 of times in less than 1 second. so there is no need to care about performance when using PointerJS.

If an element was captured so we do some more logic to redirect the events. It is not a lot more lines;

JavaScript
if (captured) {
    //stop this event, their is a captured element
    e.stopPropagation();
    e.preventDefault();
    //try to get the capture event for current event (e.g if event = mousedown , 
    //the return event will be capturemousedown) to split events raised by PointerJS from native events
    var captureEvent = getCaptureEvent(e.type, e);
    // if event not found in raiseEvents it will be null.
    if (captureEvent) {
        captured.dispatchEvent(captureEvent);
    }
}

Size

pointer.min.js is less than 3kb.

Issues

Cursor

PointerJS doesn't prevent cursor change on moving on other objects when an element is captured. because cursor is handled by browser outside events. Any idea is welcome.

Forced Mouse Release and Mouse Up outside ViewPort

Normally, Mouse is captured by app window on mousedown and release onmouseup. Browser window also follows the same rule. On mousedown, the browser window captures the mouse, passes its events to current tab and then to the document object.

As JavaScript doesn't get the capture state for the wrapping window, there is separation between the capture state on the native OS and the JavaScript capture wrapper (PointerJS).

The typical usage case is that when capture and release on mouse down and up respectively. But, in some cases, the User does mouse down and then goes with mouse outside the viewport and then presses the ESC button or ALT + TAB. This doesn't fire a mouseup event, but it releases the native capture by the OS. The result of this is PointerJS still holds a captured element and redirects all events to it while the window itself doesn't capture the mouse. The same case will occur if user doesn't release the capture on mouseup.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)