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:
- Prevent current event propagation if an element is captured or passes it normally if there is no captured element.
- Creates a new
CustomEvent
and passes it to the captured object if found. - Saves the pointer
clientX
, clientY
as the current mouse position so we can know the mouse position on other events like keyboard events.
var documentCaptureHandler = function (e) {
lastPointerPosition.x = e.clientX;
lastPointerPosition.y = e.clientY;
if (captured) {
e.stopPropagation();
e.preventDefault();
var captureEvent = getCaptureEvent(e.type, e);
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...
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.
p = pp;
The code is as follows:
<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 };
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>
<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>
<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;
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;
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.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.
lastPointerPosition.x = e.clientX;
lastPointerPosition.y = e.clientY;
if (captured) {
}
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;
if (captured) {
e.stopPropagation();
e.preventDefault();
var captureEvent = getCaptureEvent(e.type, e);
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
.