Skip to content

useKeyPress

作用

监听键盘按键,支持组合键,支持按键别名

原理

监听 keydown 或者 keyup 事件,在回调对 keyFilter 配置进行判断,如果触发了配置的场景,则触发我们传入的 eventHandler 回调

源码

ts
import useLatest from '../useLatest'
import { isFunction, isNumber, isString } from '../utils'
import type { BasicTarget } from '../utils/domTarget'
import { getTargetElement } from '../utils/domTarget'
import useDeepCompareEffectWithTarget from '../utils/useDeepCompareWithTarget'
import isAppleDevice from '../utils/isAppleDevice'

export type KeyType = number | string
export type KeyPredicate = (event: KeyboardEvent) => KeyType | boolean | undefined
export type KeyFilter = KeyType | KeyType[] | ((event: KeyboardEvent) => boolean)
export type KeyEvent = 'keydown' | 'keyup'

export type Target = BasicTarget<HTMLElement | Document | Window>

export type Options = {
  events?: KeyEvent[]
  target?: Target
  exactMatch?: boolean
  useCapture?: boolean
}

// 键盘事件 keyCode 别名
const aliasKeyCodeMap = {
  '0': 48,
  '1': 49,
  '2': 50,
  '3': 51,
  '4': 52,
  '5': 53,
  '6': 54,
  '7': 55,
  '8': 56,
  '9': 57,
  backspace: 8,
  tab: 9,
  enter: 13,
  shift: 16,
  ctrl: 17,
  alt: 18,
  pausebreak: 19,
  capslock: 20,
  esc: 27,
  space: 32,
  pageup: 33,
  pagedown: 34,
  end: 35,
  home: 36,
  leftarrow: 37,
  uparrow: 38,
  rightarrow: 39,
  downarrow: 40,
  insert: 45,
  delete: 46,
  a: 65,
  b: 66,
  c: 67,
  d: 68,
  e: 69,
  f: 70,
  g: 71,
  h: 72,
  i: 73,
  j: 74,
  k: 75,
  l: 76,
  m: 77,
  n: 78,
  o: 79,
  p: 80,
  q: 81,
  r: 82,
  s: 83,
  t: 84,
  u: 85,
  v: 86,
  w: 87,
  x: 88,
  y: 89,
  z: 90,
  leftwindowkey: 91,
  rightwindowkey: 92,
  meta: isAppleDevice ? [91, 93] : [91, 92],
  selectkey: 93,
  numpad0: 96,
  numpad1: 97,
  numpad2: 98,
  numpad3: 99,
  numpad4: 100,
  numpad5: 101,
  numpad6: 102,
  numpad7: 103,
  numpad8: 104,
  numpad9: 105,
  multiply: 106,
  add: 107,
  subtract: 109,
  decimalpoint: 110,
  divide: 111,
  f1: 112,
  f2: 113,
  f3: 114,
  f4: 115,
  f5: 116,
  f6: 117,
  f7: 118,
  f8: 119,
  f9: 120,
  f10: 121,
  f11: 122,
  f12: 123,
  numlock: 144,
  scrolllock: 145,
  semicolon: 186,
  equalsign: 187,
  comma: 188,
  dash: 189,
  period: 190,
  forwardslash: 191,
  graveaccent: 192,
  openbracket: 219,
  backslash: 220,
  closebracket: 221,
  singlequote: 222,
}

// 修饰键
const modifierKey = {
  ctrl: (event: KeyboardEvent) => event.ctrlKey,
  shift: (event: KeyboardEvent) => event.shiftKey,
  alt: (event: KeyboardEvent) => event.altKey,
  meta: (event: KeyboardEvent) => {
    if (event.type === 'keyup') {
      return aliasKeyCodeMap.meta.includes(event.keyCode)
    }
    return event.metaKey
  },
}

// 判断合法的按键类型
function isValidKeyType(value: unknown): value is string | number {
  return isString(value) || isNumber(value)
}

// 根据 event 计算激活键数量
function countKeyByEvent(event: KeyboardEvent) {
  const countOfModifier = Object.keys(modifierKey).reduce((total, key) => {
    if (modifierKey[key](event)) {
      return total + 1
    }

    return total
  }, 0)

  // 16 17 18 91 92 是修饰键的 keyCode,如果 keyCode 是修饰键,那么激活数量就是修饰键的数量,如果不是,那么就需要 +1
  return [16, 17, 18, 91, 92].includes(event.keyCode) ? countOfModifier : countOfModifier + 1
}

/**
 * 判断按键是否激活
 * @param [event: KeyboardEvent]键盘事件
 * @param [keyFilter: any] 当前键
 * @returns string | number | boolean
 */
function genFilterKey(event: KeyboardEvent, keyFilter: KeyType, exactMatch: boolean) {
  // 浏览器自动补全 input 的时候,会触发 keyDown、keyUp 事件,但此时 event.key 等为空
  if (!event.key) {
    return false
  }

  // 数字类型直接匹配事件的 keyCode
  if (isNumber(keyFilter)) {
    return event.keyCode === keyFilter ? keyFilter : false
  }

  // 字符串依次判断是否有组合键
  const genArr = keyFilter.split('.')
  let genLen = 0

  for (const key of genArr) {
    // 组合键
    const genModifier = modifierKey[key]
    // keyCode 别名
    const aliasKeyCode: number | number[] = aliasKeyCodeMap[key.toLowerCase()]

    if ((genModifier && genModifier(event)) || (aliasKeyCode && aliasKeyCode === event.keyCode)) {
      genLen++
    }
  }

  /**
   * 需要判断触发的键位和监听的键位完全一致,判断方法就是触发的键位里有且等于监听的键位
   * genLen === genArr.length 能判断出来触发的键位里有监听的键位
   * countKeyByEvent(event) === genArr.length 判断出来触发的键位数量里有且等于监听的键位数量
   * 主要用来防止按组合键其子集也会触发的情况,例如监听 ctrl+a 会触发监听 ctrl 和 a 两个键的事件。
   */
  if (exactMatch) {
    return genLen === genArr.length && countKeyByEvent(event) === genArr.length ? keyFilter : false
  }
  return genLen === genArr.length ? keyFilter : false
}

/**
 * 键盘输入预处理方法
 * @param [keyFilter: any] 当前键
 * @returns () => Boolean
 */
function genKeyFormatter(keyFilter: KeyFilter, exactMatch: boolean): KeyPredicate {
  if (isFunction(keyFilter)) {
    return keyFilter
  }
  if (isValidKeyType(keyFilter)) {
    return (event: KeyboardEvent) => genFilterKey(event, keyFilter, exactMatch)
  }
  if (Array.isArray(keyFilter)) {
    return (event: KeyboardEvent) => keyFilter.find((item) => genFilterKey(event, item, exactMatch))
  }
  return () => Boolean(keyFilter)
}

const defaultEvents: KeyEvent[] = ['keydown']

// 键盘事件 Hook
function useKeyPress(
  keyFilter: KeyFilter,
  eventHandler: (event: KeyboardEvent, key: KeyType) => void,
  option?: Options,
) {
  const { events = defaultEvents, target, exactMatch = false, useCapture = false } = option || {}
  // 保存回调函数的引用
  const eventHandlerRef = useLatest(eventHandler)
  // 保存键盘事件的引用
  const keyFilterRef = useLatest(keyFilter)

  // 监听键盘事件
  useDeepCompareEffectWithTarget(
    () => {
      // 获取目标元素
      const el = getTargetElement(target, window)
      if (!el) {
        return
      }

      const callbackHandler = (event: KeyboardEvent) => {
        const genGuard = genKeyFormatter(keyFilterRef.current, exactMatch)
        const keyGuard = genGuard(event)
        const firedKey = isValidKeyType(keyGuard) ? keyGuard : event.key

        // 如果当前键盘事件不是我们监听的键盘事件,则直接返回
        if (keyGuard) {
          return eventHandlerRef.current?.(event, firedKey)
        }
      }

      // 添加事件监听
      for (const eventName of events) {
        el?.addEventListener?.(eventName, callbackHandler, useCapture)
      }

      // 组件卸载时移除事件监听
      return () => {
        for (const eventName of events) {
          el?.removeEventListener?.(eventName, callbackHandler, useCapture)
        }
      }
    },
    [events],
    target,
  )
}

export default useKeyPress

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