跳到主要内容

鼠标与指针事件

鼠标事件有哪些, 一次点击的顺序是什么?​

事件触发时机
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 / pointerupmousedown / mouseup
pointermovemousemove
pointerover、pointerout、pointerenter、pointerleavemouseover、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; }; 这样浏览器不再接管, 拖放逻辑在鼠标与触摸设备上都能按预期工作;