PointerJS | 高效地按DOM元素捕获鼠标





5.00/5 (2投票s)
在HTML5浏览器中实现鼠标捕获。
引言
在视觉开发中,鼠标捕获是一项非常重要的功能。当一个视觉对象捕获鼠标时,所有与鼠标相关的事件都会被视为由具有鼠标捕获的对象触发,即使鼠标指针位于另一个对象之上,甚至完全位于视口
之外。不幸的是,此功能仅在 Internet Explorer 和 Firefox 中可用!!!
背景
要理解 pointerjs 的工作原理,您必须了解事件传播阶段(捕获和冒泡)以及 CustomEvents。
技术对比
其他鼠标 API,如DragDrop
和PointerLock
API,功能或多或少相似,但准确地说,它们的目标范围和用例存在很大差异。
1. Pointer Lock API
根据MDN的说法,它提供基于鼠标随时间移动(即差值)的输入方法,而不仅仅是鼠标光标在视口中的绝对位置。它允许您访问原始鼠标移动,将鼠标事件的目标锁定到单个元素,消除鼠标单向移动距离的限制,并隐藏光标。它非常适合第一人称 3D 游戏。
为什么不使用Pointer Lock API作为一种解决方法?
- 它适用于 3D 游戏。
- 它会隐藏光标。
2. Drag Drop API
根据MDN的说法,用户可以使用鼠标选择可拖动元素,将元素拖动到可放置元素上,然后通过释放鼠标按钮来放置这些元素。在拖动操作期间,一个半透明的可拖动元素的表示会跟随鼠标指针。
为什么不使用Drag Drop API作为一种解决方法?
- 它适用于数据和文件传输。
- 它会创建一个可拖动元素的另一个半透明视觉表示,这不适用于许多情况。
- 事件仅在可拖动和可放置元素上可用。
- 如果数据被拖放到您的网页之外,即使是原生应用程序,它也可能将您的数据移出您的应用程序。
PointerJS
PointerJS 是一个小型库,它提供了一个便捷、轻量级且跨浏览器的鼠标捕获实现。它是纯 JavaScript,因此可以用于任何 JavaScript 环境。
pointerjs 的理念是在文档级别检测捕获状态下的指针事件,并将它们重定向到找到的已捕获 DOM。
缺乏测试。您可能需要自行承担风险在生产环境中使用 PointerJS。
工作原理
基本来说,PointerJS 在文档对象上添加了所有指针相关事件的监听器,以检测浏览器原生事件,如(mousemove
、mousedown
、mouseup
、mousewheel
、click
)。这些监听器执行以下操作:
- 如果元素已被捕获,则阻止当前事件传播,否则正常传递。
- 创建一个新的
CustomEvent
,如果找到已捕获的元素,则将其传递给该元素。 - 将指针的
clientX
、clientY
保存为当前的鼠标位置,以便我们可以在键盘事件等其他事件中知道鼠标位置。
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);
}
}
};
示例
拖动
对象拖动取决于将旧鼠标位置与当前鼠标位置的差值添加到当前对象位置...
obj.left = obj.left + mouseX - oldMouseX;
obj.top = obj.top + mouseY - oldMouseY;
... 在 mouse down 时捕获鼠标 => 在 mouse move 时移动对象 => 在 mouse up 时释放对象。
需要注意的是,在计算新位置后,我们将当前鼠标位置设置为下一个移动的旧位置。
p = pp;
代码如下
<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>
绘制
<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>
调整大小
<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
性能
PointerJS 为每个需要监听的鼠标事件创建一个监听器。默认配置会在文档对象的捕获阶段创建(mousedown
、mousemove
、mouseover
、mouseup
、mousewheel
、mouseenter
、mouseleave
和click
)监听器。
通常情况下,这些监听器不会影响性能,只要您遵循最佳实践。只有一个例外是“mousemove
”。mousemove
事件有所不同,因为它会在鼠标每次在文档上移动时触发新事件并执行所有mousemove
监听器,它可能每秒触发数十到数百次。幸运的是,PointerJS 会处理这一点,并且在没有捕获元素的情况下,每个事件触发仅执行最少的 3 行代码。
//store the current pointer position
lastPointerPosition.x = e.clientX;
lastPointerPosition.y = e.clientY;
if (captured) {
// do some logic
}
因此,性能影响被最小化到普通指针输入处理可以在不到 1 秒的时间内执行 100,000,000 次的水平。所以在使用 PointerJS 时无需担心性能。
如果一个元素被捕获,我们则执行一些额外的逻辑来重定向事件。这并不会增加很多代码行;
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);
}
}
大小
pointer.min.js 文件小于 3kb。
问题
光标
当一个元素被捕获时,PointerJS 不会阻止鼠标移到其他对象上时发生的指针样式更改,因为光标样式是由浏览器在事件之外处理的。欢迎任何建议。
强制鼠标释放和视口外鼠标抬起
通常,鼠标在mousedown
时被应用程序窗口捕获,在mouseup
时释放。浏览器窗口也遵循相同的规则。在 mousedown 时,浏览器窗口捕获鼠标,将其事件传递给当前标签页,然后再传递给document
对象。
由于 JavaScript 无法获取包装窗口的捕获状态,因此原生操作系统上的捕获状态与 JavaScript 捕获包装器(PointerJS)之间存在分离。
典型的使用场景是在 mousedown 时捕获鼠标,在 mouseup 时释放。但是,在某些情况下,用户会进行 mousedown 操作,然后将鼠标移出视口,然后按下 ESC 键或 ALT + TAB。这不会触发mouseup
事件,但会释放操作系统上的原生捕获。其结果是 PointerJS 仍然保持一个已捕获的元素,并将所有事件重定向到它,而窗口本身并没有捕获鼠标。如果用户在mouseup
时没有释放捕获,也会发生相同的情况。