Skip to content

useLongPress

作用

监听目标元素的长按事件。

原理

判断当前是否支持 touch 事件,假如支持,则监听 touchstarttouchend 事件。假如不支持,则监听 mousedownmouseupmouseleave 事件。根据定时器设置标识,判断是否达到长按,触发回调,从而实现长按事件

源码

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

如有转载或 CV 的请标注本站原文地址