{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "useKeyPress", "title": "useKeyPress", "description": "useKeyPress hook", "registryDependencies": [ "https://shadcn-ahooks.vercel.app/r/useLatest.json", "https://shadcn-ahooks.vercel.app/r/utils.json" ], "files": [ { "path": "packages/hooks/src/useKeyPress/index.ts", "content": "import useLatest from '../useLatest';\nimport { isFunction, isNumber, isString } from '../utils';\nimport type { BasicTarget } from '../utils/domTarget';\nimport { getTargetElement } from '../utils/domTarget';\nimport useDeepCompareEffectWithTarget from '../utils/useDeepCompareWithTarget';\nimport isAppleDevice from '../utils/isAppleDevice';\n\nexport type KeyType = number | string;\nexport type KeyPredicate = (event: KeyboardEvent) => KeyType | boolean | undefined;\nexport type KeyFilter = KeyType | KeyType[] | ((event: KeyboardEvent) => boolean);\nexport type KeyEvent = 'keydown' | 'keyup';\n\nexport type Target = BasicTarget;\n\nexport type Options = {\n events?: KeyEvent[];\n target?: Target;\n exactMatch?: boolean;\n useCapture?: boolean;\n};\n\n// 键盘事件 keyCode 别名\nconst aliasKeyCodeMap = {\n '0': 48,\n '1': 49,\n '2': 50,\n '3': 51,\n '4': 52,\n '5': 53,\n '6': 54,\n '7': 55,\n '8': 56,\n '9': 57,\n backspace: 8,\n tab: 9,\n enter: 13,\n shift: 16,\n // Legacy alias kept for compatibility; standard name is \"control\".\n ctrl: 17,\n control: 17,\n alt: 18,\n // Legacy alias kept for compatibility; standard name is \"pause\".\n pausebreak: 19,\n pause: 19,\n capslock: 20,\n // Legacy alias kept for compatibility; standard name is \"escape\".\n esc: 27,\n escape: 27,\n // Legacy alias kept for compatibility; standard name is \"spacebar\" (non-standard but widely used).\n space: 32,\n spacebar: 32,\n pageup: 33,\n pagedown: 34,\n end: 35,\n home: 36,\n // Legacy aliases kept for compatibility; standard names are \"arrowleft/arrowup/arrowright/arrowdown\".\n leftarrow: 37,\n arrowleft: 37,\n uparrow: 38,\n arrowup: 38,\n rightarrow: 39,\n arrowright: 39,\n downarrow: 40,\n arrowdown: 40,\n insert: 45,\n delete: 46,\n a: 65,\n b: 66,\n c: 67,\n d: 68,\n e: 69,\n f: 70,\n g: 71,\n h: 72,\n i: 73,\n j: 74,\n k: 75,\n l: 76,\n m: 77,\n n: 78,\n o: 79,\n p: 80,\n q: 81,\n r: 82,\n s: 83,\n t: 84,\n u: 85,\n v: 86,\n w: 87,\n x: 88,\n y: 89,\n z: 90,\n leftwindowkey: 91,\n rightwindowkey: 92,\n meta: isAppleDevice ? [91, 93] : [91, 92],\n // Legacy alias kept for compatibility; standard name is \"contextmenu\".\n selectkey: 93,\n contextmenu: 93,\n numpad0: 96,\n numpad1: 97,\n numpad2: 98,\n numpad3: 99,\n numpad4: 100,\n numpad5: 101,\n numpad6: 102,\n numpad7: 103,\n numpad8: 104,\n numpad9: 105,\n multiply: 106,\n add: 107,\n subtract: 109,\n decimalpoint: 110,\n divide: 111,\n f1: 112,\n f2: 113,\n f3: 114,\n f4: 115,\n f5: 116,\n f6: 117,\n f7: 118,\n f8: 119,\n f9: 120,\n f10: 121,\n f11: 122,\n f12: 123,\n numlock: 144,\n scrolllock: 145,\n semicolon: 186,\n equalsign: 187,\n comma: 188,\n dash: 189,\n period: 190,\n forwardslash: 191,\n graveaccent: 192,\n openbracket: 219,\n backslash: 220,\n closebracket: 221,\n singlequote: 222,\n};\n\n// 修饰键\nconst modifierKey = {\n ctrl: (event: KeyboardEvent) => event.ctrlKey,\n shift: (event: KeyboardEvent) => event.shiftKey,\n alt: (event: KeyboardEvent) => event.altKey,\n meta: (event: KeyboardEvent) => {\n if (event.type === 'keyup') {\n return aliasKeyCodeMap.meta.includes(event.keyCode);\n }\n return event.metaKey;\n },\n};\n\n// 判断合法的按键类型\nfunction isValidKeyType(value: unknown): value is string | number {\n return isString(value) || isNumber(value);\n}\n\n// 根据 event 计算激活键数量\nfunction countKeyByEvent(event: KeyboardEvent) {\n const countOfModifier = Object.keys(modifierKey).reduce((total, key) => {\n if ((modifierKey as any)[key](event)) {\n return total + 1;\n }\n\n return total;\n }, 0);\n\n // 16 17 18 91 92 是修饰键的 keyCode,如果 keyCode 是修饰键,那么激活数量就是修饰键的数量,如果不是,那么就需要 +1\n return [16, 17, 18, 91, 92].includes(event.keyCode) ? countOfModifier : countOfModifier + 1;\n}\n\n/**\n * 判断按键是否激活\n * @param [event: KeyboardEvent]键盘事件\n * @param [keyFilter: any] 当前键\n * @returns string | number | boolean\n */\nfunction genFilterKey(event: KeyboardEvent, keyFilter: KeyType, exactMatch: boolean) {\n // 浏览器自动补全 input 的时候,会触发 keyDown、keyUp 事件,但此时 event.key 等为空\n if (!event.key) {\n return false;\n }\n\n // 数字类型直接匹配事件的 keyCode\n if (isNumber(keyFilter)) {\n return event.keyCode === keyFilter ? keyFilter : false;\n }\n\n // 字符串依次判断是否有组合键\n const genArr = keyFilter.split('.');\n let genLen = 0;\n\n for (const key of genArr) {\n // 组合键\n const genModifier = (modifierKey as any)[key];\n // keyCode 别名\n const aliasKeyCode: number | number[] = (aliasKeyCodeMap as any)[key.toLowerCase()];\n\n if ((genModifier && genModifier(event)) || (aliasKeyCode && aliasKeyCode === event.keyCode)) {\n genLen++;\n }\n }\n\n /**\n * 需要判断触发的键位和监听的键位完全一致,判断方法就是触发的键位里有且等于监听的键位\n * genLen === genArr.length 能判断出来触发的键位里有监听的键位\n * countKeyByEvent(event) === genArr.length 判断出来触发的键位数量里有且等于监听的键位数量\n * 主要用来防止按组合键其子集也会触发的情况,例如监听 ctrl+a 会触发监听 ctrl 和 a 两个键的事件。\n */\n if (exactMatch) {\n return genLen === genArr.length && countKeyByEvent(event) === genArr.length ? keyFilter : false;\n }\n return genLen === genArr.length ? keyFilter : false;\n}\n\n/**\n * 键盘输入预处理方法\n * @param [keyFilter: any] 当前键\n * @returns () => Boolean\n */\nfunction genKeyFormatter(keyFilter: KeyFilter, exactMatch: boolean): KeyPredicate {\n if (isFunction(keyFilter)) {\n return keyFilter;\n }\n if (isValidKeyType(keyFilter)) {\n return (event: KeyboardEvent) => genFilterKey(event, keyFilter, exactMatch);\n }\n if (Array.isArray(keyFilter)) {\n return (event: KeyboardEvent) =>\n keyFilter.find((item) => genFilterKey(event, item, exactMatch));\n }\n return () => Boolean(keyFilter);\n}\n\nconst defaultEvents: KeyEvent[] = ['keydown'];\n\nfunction useKeyPress(\n keyFilter: KeyFilter,\n eventHandler: (event: KeyboardEvent, key: KeyType) => void,\n option?: Options,\n) {\n const { events = defaultEvents, target, exactMatch = false, useCapture = false } = option || {};\n const eventHandlerRef = useLatest(eventHandler);\n const keyFilterRef = useLatest(keyFilter);\n\n useDeepCompareEffectWithTarget(\n () => {\n const el = getTargetElement(target, window);\n if (!el) {\n return;\n }\n\n const callbackHandler = (event: Event) => {\n const keyEvent = event as KeyboardEvent;\n const genGuard = genKeyFormatter(keyFilterRef.current, exactMatch);\n const keyGuard = genGuard(keyEvent);\n const firedKey = isValidKeyType(keyGuard) ? keyGuard : keyEvent.key;\n\n if (keyGuard) {\n return eventHandlerRef.current?.(keyEvent, firedKey);\n }\n };\n\n for (const eventName of events) {\n el?.addEventListener?.(eventName, callbackHandler, useCapture);\n }\n return () => {\n for (const eventName of events) {\n el?.removeEventListener?.(eventName, callbackHandler, useCapture);\n }\n };\n },\n [events],\n target,\n );\n}\n\nexport default useKeyPress;\n", "type": "registry:file", "target": "src/hooks/ahooks/useKeyPress/index.ts" } ], "type": "registry:hook" }