前言
最近修改了一个用户登录验证的组件,这个组件是一个滑块拼图组件,在 ios 移动端使用时发现滑动时整个页面会产生回弹效果,因此对这个组件进行了内部修改。
问题
在实现这类滑块时,一般会经历按下,移动和释放三个步骤,在组件的内部代码中通过 start(), move(), end() 来实现,并通过对应的事件进行绑定。组件是使用了 touch 和 mouse 事件来绑定这些函数,这样可以同时在 PC 和移动端使用。
比较奇怪的是组件绑定 move(), end() 这两个函数是绑定在 window 对象上的,这也解析了为什么会有页面的回弹效果,如果要取消回弹只需要在 body 元素上设置 touch-action: none 就可以了。
不过这样就有新的问题,为什么组件作者需要把事件绑定在 window 对象上呢?
移除 window 绑定
如果把 window 绑定移到滑块上,就会发现在移动端没有问题,但是在 PC 端当鼠标指针移出滑块时滑块就不再滑动了,很明显组件作者就是为了解决这个问题。但是这种解决方案其实不太好,一来会产生页面滑动的默认行为,而且假如有多个滑块还会产生冲突。
像这种移动目标脱离的问题,其实本身就有专门的 API 来解决,例如 input range 元素在 PC 端就可以脱离后滑动, 只需要使用元素的 setPointerCapture(pointerId) 就可以对特定的指针进行捕获,捕获后只要不释放那么事件的目标对象就不会变化,即使指针离开了目标元素。pointerId 来源于 PointerEvent 事件对象,PointerEvent 事件还有一个好处就是可以兼容移动端和 PC 端,这样就不用使用 touch 和 mouse 事件来分别定义。
demo
因为组件的业务代码太多了,所以我写了一个 demo 来简单展示滑块拖动的效果
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1.0, maximum-scale=2.0, minimum-scale=0.5">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="renderer" content="webkit">
<title>index</title>
<style>
.wrapper {
border: 1px solid #000;
}
body {
margin: 0;
}
.bar {
margin: auto;
width: 300px;
height: 30px;
outline: 1px black solid;
margin-top: 100px;
}
.block {
background-color: red;
height: 100%;
aspect-ratio: 1;
cursor: pointer;
transform: translateX(var(--left));
touch-action: none;
}
</style>
</head>
<body>
<div class="bar">
<div style="--left: 0;" onpointerdown="pointerdown(event)" onpointerup="pointerup(event)" class="block"></div>
</div>
<script>
let prevClientX = 0
const barEle = document.querySelector('.bar')
const blockEle = document.querySelector('.block')
function move(event) {
const clientX = event.clientX
const diff = clientX - prevClientX
const left = parseInt(blockEle.style.getPropertyValue('--left'))
const { width: barWidth } = barEle.getBoundingClientRect()
const { width: blockWidth } = blockEle.getBoundingClientRect()
let newLeft = left + diff
if (newLeft < 0) {
newLeft = 0
}
if (newLeft > (barWidth - blockWidth)) {
newLeft = barWidth - blockWidth
}
blockEle.style.setProperty('--left', newLeft + 'px')
prevClientX = clientX
}
function pointerdown (event) {
prevClientX = event.clientX
blockEle.addEventListener('pointermove', move)
event.target.setPointerCapture(event.pointerId)
}
function pointerup(event) {
blockEle.removeEventListener('pointermove', move)
prevClientX = 0
event.target.releasePointerCapture(event.pointerId)
}
</script>
</body>
</html>
这里还需要注意要特别设置滑块的样式为 touch-action: none; 不然移动端会触发 pointercancel 来强制取消滑块移动的监听,使用 touch-action 可以明确告知浏览器这个触摸行为的意图。具体原因可以查看这里 developer.mozilla.org/en-US/docs/…