鼠标与指针事件
鼠标事件有哪些, 一次点击的顺序是什么?
| 事件 | 触发时机 |
|---|---|
mousedown / mouseup | 按键按下 / 释放 |
mouseover / mouseout | 指针进入 / 离开元素 (含子元素边界) |
mousemove | 指针在元素上移动 |
click / dblclick | 左键 mousedown 后同元素 mouseup; 短时间内两次 click |
contextmenu | 右键按下 (键盘菜单键也触发, 所以不完全属于鼠标) |
- 顺序: 一次左键点击固定为
mousedown->mouseup->click; 手机、平板会模拟这些事件, 不保证真有鼠标; - 总览见 事件类型;
如何判断按下的鼠标键与修饰键?
event.button: 一个按键一个值,mousedown/mouseup上的任意键都会触发, 必须靠它区分;
| 按键 | event.button |
|---|---|
| 左键 (primary) | 0 |
| 中键 (auxiliary) | 1 |
| 右键 (secondary) | 2 |
| X1 (后退) | 3 |
| X2 (前进) | 4 |
event.buttons: 位掩码, 表示当前所有按下的键, 极少用;event.which(1 左、2 中、3 右) 是老的非标准写法, 已废弃;- 不必判断的场合:
click只来自左键,contextmenu只来自右键; - 修饰键:
shiftKey、altKey、ctrlKey、metaKey(Mac 的Cmd), 按下时为true; - 坐标: 视口坐标
clientX/clientY(不随滚动改变, 类似position:fixed), 文档坐标pageX/pageY, 换算见 坐标与滚动定位; - Mac 差异: 组合键习惯用
Cmd, 且Ctrl+左键被系统当成右键 (触发contextmenu而非click), 所以判断写成if (event.ctrlKey || event.metaKey); - 没有键盘的设备: 修饰键只能当补充, 必须有不用修饰键的入口;
如何阻止 mousedown 引起的文本选择?
- 问题: 双击, 或按住左键拖动, 会顺带选中文字;
- 解法: 在
mousedown上阻止默认行为, 这两种选择就都不会发生;
<b ondblclick="alert('Click!')" onmousedown="return false">Double-click me</b>
- 残留: 文字本身仍可选中, 只是选择必须从文字前后开始, 通常可以接受; 想彻底禁止复制则用
oncopy处理程序;
Pointer Events 与鼠标事件如何对应?
| Pointer 事件 | 对应的鼠标事件 |
|---|---|
pointerdown / pointerup | mousedown / mouseup |
pointermove | mousemove |
pointerover、pointerout、pointerenter、pointerleave | mouseover、mouseout、mouseenter、mouseleave |
pointercancel、gotpointercapture、lostpointercapture | 无 |
- 迁移: 把代码里的
mouse改成pointer就能继续支持鼠标, 并顺带支持触摸与手写笔; - 属性: 与鼠标事件相同 (
clientX/Y、target等) 再加指针专属属性; - 演进: 只有鼠标事件 -> 触摸设备模拟鼠标 -> 触摸事件 -> Pointer Events 统一所有指向设备;
- 前提: 所有主流浏览器已支持 Pointer Events Level 2; 只有 IE10、Safari 12 以下需要旧事件或 polyfill;
- 兼容: 部分场景要补
touch-action: none, 否则浏览器手势或原生拖放接管后会触发pointercancel;
pointerId 与 pointerType 如何支持多指针?
pointerId: 每个触点一个唯一标识, 由浏览器生成; 是按"每根手指"分配, 不是每台设备一个;pointerType:"mouse"、"pen"或"touch", 可按设备类型分支处理;isPrimary: 主指针 (多指触摸里的第一根手指) 为true;- 多点流程: 第一根手指
pointerdown且isPrimary=true; 其余每根手指各一个isPrimary=false、pointerId互不相同的pointerdown; 5 根手指同时按下就有 5 组坐标, 之后的pointermove/pointerup靠同一个pointerId归位; - 设备信息:
width/height(接触面积, 鼠标恒为 1)、pressure(0~1, 不支持时按下为0.5)、tiltX/tiltY/twist(笔) 等, 多数设备用不上; - 单点设备: 鼠标永远同一个
pointerId且isPrimary=true;
setPointerCapture 解决什么问题?
- 场景: 自制滑块拖动; 鼠标方案要把
mousemove挂到document上才能跟住指针, 途中滑过别的元素会误触发它们的处理程序; - 做法:
thumb.setPointerCapture(event.pointerId)把该pointerId的后续事件全部重定向到thumb;
thumb.onpointerdown = (event) => {
thumb.setPointerCapture(event.pointerId); // 直到 pointerup 都以 thumb 为目标
thumb.onpointermove = (event) => {
thumb.style.left = event.clientX - slider.getBoundingClientRect().left + "px";
};
thumb.onpointerup = () => {
thumb.onpointermove = null;
thumb.onpointerup = null; // 捕获已在 pointerup 时自动释放, 不必手动 release
};
};
- 释放时机:
pointerup/pointercancel自动释放, 元素被移出文档自动释放, 或手动releasePointerCapture(pointerId); - 好处: 不用在
document上加删处理程序, 拖动期间不会误触其它元素的处理程序; 只改target/currentTarget,clientX/clientY仍是真实值; - 配套事件:
gotpointercapture在建立捕获时触发,lostpointercapture在释放时触发;
pointercancel 何时触发, 如何避免?
- 含义: 正在进行的指针交互被中止, 之后不再产生指针事件;
- 原因: 指针硬件被关闭; 设备方向改变 (平板旋转); 浏览器判定为手势或缩放平移并自己接管;
- 典型: 拖动图片时浏览器原生 drag-and-drop 抢走流程, 立刻触发
pointercancel, 之后再无pointermove; - 避免: 关掉原生拖放
ball.ondragstart = () => false, 触摸设备再加 CSS#ball { touch-action: none; }; 这样浏览器不再接管, 拖放逻辑在鼠标与触摸设备上都能按预期工作;