useLongPress
作用
监听目标元素的长按事件。
原理
判断当前是否支持
touch
事件,假如支持,则监听touchstart
和touchend
事件。假如不支持,则监听mousedown
、mouseup
和mouseleave
事件。根据定时器设置标识,判断是否达到长按,触发回调,从而实现长按事件
源码
ts
import { useRef } from 'react'
import useLatest from '../useLatest'
import type { BasicTarget } from '../utils/domTarget'
import { getTargetElement } from '../utils/domTarget'
import isBrowser from '../utils/isBrowser'
import useEffectWithTarget from '../utils/useEffectWithTarget'
type EventType = MouseEvent | TouchEvent
export interface Options {
delay?: number
moveThreshold?: { x?: number; y?: number }
onClick?: (event: EventType) => void
onLongPressEnd?: (event: EventType) => void
}
// 是否支持 touch 事件
const touchSupported =
isBrowser &&
// @ts-ignore
('ontouchstart' in window || (window.DocumentTouch && document instanceof DocumentTouch))
function useLongPress(
onLongPress: (event: EventType) => void,
target: BasicTarget,
{ delay = 300, moveThreshold, onClick, onLongPressEnd }: Options = {},
) {
// 保存长按事件回调
const onLongPressRef = useLatest(onLongPress)
// 保存点击事件回调
const onClickRef = useLatest(onClick)
// 保存长按结束事件回调
const onLongPressEndRef = useLatest(onLongPressEnd)
// 保存定时器
const timerRef = useRef<ReturnType<typeof setTimeout>>()
// 保存是否触发长按事件的标识
const isTriggeredRef = useRef(false)
// 保存上一次的位置
const pervPositionRef = useRef({ x: 0, y: 0 })
// 按下后移动阈值,超出则不触发长按事件
const hasMoveThreshold = !!(
(moveThreshold?.x && moveThreshold.x > 0) ||
(moveThreshold?.y && moveThreshold.y > 0)
)
useEffectWithTarget(
() => {
// 获取目标元素
const targetElement = getTargetElement(target)
// 如果目标元素不存在addEventListener方法,则直接返回
if (!targetElement?.addEventListener) {
return
}
// 如果支持 touch 事件,则监听 touchstart 和 touchend 事件
const overThreshold = (event: EventType) => {
const { clientX, clientY } = getClientPosition(event)
const offsetX = Math.abs(clientX - pervPositionRef.current.x)
const offsetY = Math.abs(clientY - pervPositionRef.current.y)
return !!(
(moveThreshold?.x && offsetX > moveThreshold.x) ||
(moveThreshold?.y && offsetY > moveThreshold.y)
)
}
// 获取当前的位置
function getClientPosition(event: EventType) {
if (event instanceof TouchEvent) {
return {
clientX: event.touches[0].clientX,
clientY: event.touches[0].clientY,
}
}
if (event instanceof MouseEvent) {
return {
clientX: event.clientX,
clientY: event.clientY,
}
}
console.warn('Unsupported event type')
return { clientX: 0, clientY: 0 }
}
// 开始长按
const onStart = (event: EventType) => {
// 如果已经触发了长按事件,则直接返回
if (hasMoveThreshold) {
const { clientX, clientY } = getClientPosition(event)
pervPositionRef.current.x = clientX
pervPositionRef.current.y = clientY
}
timerRef.current = setTimeout(() => {
onLongPressRef.current(event)
isTriggeredRef.current = true
}, delay)
}
// 移动
const onMove = (event: TouchEvent) => {
if (timerRef.current && overThreshold(event)) {
clearInterval(timerRef.current)
timerRef.current = undefined
}
}
// 结束
const onEnd = (event: EventType, shouldTriggerClick: boolean = false) => {
// 清除定时器
if (timerRef.current) {
clearTimeout(timerRef.current)
}
// 如果已经触发了长按事件,则触发长按结束事件
if (isTriggeredRef.current) {
onLongPressEndRef.current?.(event)
}
// 如果没有触发长按事件,且设置了点击事件,则触发点击事件
if (shouldTriggerClick && !isTriggeredRef.current && onClickRef.current) {
onClickRef.current(event)
}
// 重置标识
isTriggeredRef.current = false
}
// 结束并触发点击事件
const onEndWithClick = (event: EventType) => onEnd(event, true)
// 如果不支持 touch 事件,则监听 mousedown、mouseup 和 mouseleave 事件
if (!touchSupported) {
targetElement.addEventListener('mousedown', onStart)
targetElement.addEventListener('mouseup', onEndWithClick)
targetElement.addEventListener('mouseleave', onEnd)
if (hasMoveThreshold) targetElement.addEventListener('mousemove', onMove)
} else {
targetElement.addEventListener('touchstart', onStart)
targetElement.addEventListener('touchend', onEndWithClick)
if (hasMoveThreshold) targetElement.addEventListener('touchmove', onMove)
}
// 清除监听事件的方法
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current)
isTriggeredRef.current = false
}
if (!touchSupported) {
targetElement.removeEventListener('mousedown', onStart)
targetElement.removeEventListener('mouseup', onEndWithClick)
targetElement.removeEventListener('mouseleave', onEnd)
if (hasMoveThreshold) targetElement.removeEventListener('mousemove', onMove)
} else {
targetElement.removeEventListener('touchstart', onStart)
targetElement.removeEventListener('touchend', onEndWithClick)
if (hasMoveThreshold) targetElement.removeEventListener('touchmove', onMove)
}
}
},
[],
target,
)
}
export default useLongPress