以前,指向网页上的对象很简单。您有鼠标,您移动鼠标,有时按按钮,就这样。所有非鼠标设备都被模拟为鼠标,开发者可以完全放心。
不过,简单并不一定意味着好。随着时间的推移,我们越来越认识到,并非所有设备都必须(或假装)是鼠标:您可以使用具有压感和倾斜感知功能的触控笔,获得极大的创作自由;您也可以使用手指,这样您只需设备和手即可;而且,何不同时使用多根手指?
我们已经推出了触摸事件来帮助我们解决此问题,但它们是专门用于触摸的完全独立的 API,因此如果您想同时支持鼠标和触摸,就必须编写两个单独的事件模型。Chrome 55 附带了一个新标准,该标准统一了这两种模型:指针事件。
单事件模型
指针事件统一了浏览器的指针输入模型,将触摸、触控笔和鼠标整合到一组事件中。例如:
document.addEventListener('pointermove',
ev => console.log('The pointer moved.'));
foo.addEventListener('pointerover',
ev => console.log('The pointer is now over foo.'));
以下是所有可用事件的列表,如果您熟悉鼠标事件,应该会觉得这些事件非常熟悉:
pointerover
|
指针已进入元素的边界框。
对于支持悬停的设备,此操作会立即发生;对于不支持悬停的设备,此操作会在 pointerdown 事件之前发生。
|
pointerenter
|
与 pointerover 类似,但不会向上传播,并且会以不同的方式处理子项。
规范详情。
|
pointerdown
|
指针已进入活动按钮状态,即按下按钮或建立接触,具体取决于输入设备的语义。 |
pointermove
|
指针已更改位置。 |
pointerup
|
指针已离开活动按钮状态。 |
pointercancel
|
发生了某种情况,指针不太可能再发出任何事件。这意味着,您应取消所有正在进行的操作,并返回中性输入状态。 |
pointerout
|
指针已离开元素或屏幕的边界框。此外,如果设备不支持悬停,则在 pointerup 后面也需要添加此属性。
|
pointerleave
|
与 pointerout 类似,但不会向上传播,并且会以不同的方式处理子项。
规范详情。
|
gotpointercapture
|
元素已收到指针捕获。 |
lostpointercapture
|
正在捕获的指针已释放。 |
不同的输入类型
通常,借助指针事件,您可以以与输入无关的方式编写代码,而无需为不同的输入设备注册单独的事件处理脚本。当然,您仍然需要注意输入类型之间的差异,例如悬停概念是否适用。不过,如果您确实想区分不同的输入设备类型(例如,为不同的输入提供单独的代码/功能),则可以使用 PointerEvent
接口的 pointerType
属性在同一事件处理脚本中执行此操作。例如,如果您要编写侧边抽屉导航栏,则可以为 pointermove
事件使用以下逻辑:
switch(ev.pointerType) {
case 'mouse':
// Do nothing.
break;
case 'touch':
// Allow drag gesture.
break;
case 'pen':
// Also allow drag gesture.
break;
default:
// Getting an empty string means the browser doesn't know
// what device type it is. Let's assume mouse and do nothing.
break;
}
默认操作
在支持触控的浏览器中,可以使用某些手势滚动、缩放或刷新页面。
对于触摸事件,您仍会在这些默认操作执行期间收到事件,例如,在用户滚动时,系统仍会触发 touchmove
。
使用指针事件时,每当触发滚动或缩放等默认操作时,您都会收到 pointercancel
事件,以便您知道浏览器已控制指针。例如:
document.addEventListener('pointercancel',
ev => console.log('Go home, the browser is in charge now.'));
内置速度:与触摸事件相比,此模型默认可提供更好的性能,因为您需要使用被动事件监听器才能实现相同的响应速度。
您可以使用 touch-action
CSS 属性阻止浏览器接管。将元素上的此属性设置为 none
会停用通过该元素启动的所有浏览器定义的操作。不过,还有一些其他值可用于更精细的控制,例如 pan-x
,可让浏览器对 x 轴上的移动做出响应,但对 y 轴上的移动不响应。Chrome 55 支持以下值:
auto
|
默认;浏览器可以执行任何默认操作。 |
none
|
浏览器不得执行任何默认操作。 |
pan-x
|
浏览器只能执行水平滚动默认操作。 |
pan-y
|
浏览器只能执行纵向滚动默认操作。 |
pan-left
|
浏览器只能执行水平滚动默认操作,并且只能将页面平移到左侧。 |
pan-right
|
浏览器只能执行水平滚动默认操作,并且只能将页面平移到右侧。 |
pan-up
|
浏览器只能执行垂直滚动默认操作,并且只能向上平移页面。 |
pan-down
|
浏览器只能执行垂直滚动默认操作,并且只能向下平移页面。 |
manipulation
|
浏览器只能执行滚动和缩放操作。 |
指针捕获
您是否曾花费数小时来调试损坏的 mouseup
事件,最后才发现问题在于用户在点击目标之外放开按钮?没有?好的,可能只是我的问题。
不过,到目前为止,还没有很好的方法来解决这个问题。当然,您可以在文档中设置 mouseup
处理脚本,并在应用中保存一些状态以跟踪各项内容。不过,这并不是最干净的解决方案,尤其是在您构建Web 组件并尝试将所有内容保持良好隔离的情况下。
指针事件提供了一个更好的解决方案:您可以捕获指针,以确保收到 pointerup
事件(或任何其他难以捉摸的事件)。
const foo = document.querySelector('#foo');
foo.addEventListener('pointerdown', ev => {
console.log('Button down, capturing!');
// Every pointer has an ID, which you can read from the event.
foo.setPointerCapture(ev.pointerId);
});
foo.addEventListener('pointerup',
ev => console.log('Button up. Every time!'));
浏览器支持
在撰写本文时,Internet Explorer 11、Microsoft Edge、Chrome 和 Opera 支持指针事件,Firefox 部分支持指针事件。您可以访问 caniuse.com 查看最新列表。
您可以使用指针事件 polyfill 来填补空白。或者,您也可以直接在运行时检查浏览器支持情况:
if (window.PointerEvent) {
// Yay, we can use pointer events!
} else {
// Back to mouse and touch events, I guess.
}
指针事件非常适合渐进式增强:只需修改初始化方法以执行上述检查,在 if
块中添加指针事件处理脚本,并将鼠标/触摸事件处理脚本移至 else
块即可。
欢迎试用,并与我们分享您的感受和想法!