Last active
December 3, 2025 07:04
-
-
Save ZacharyBear/a67cfe0637a35ad07b375ee9882c04f6 to your computer and use it in GitHub Desktop.
KeyBinding hook in Vue3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const controlKeys = ['control', 'option', 'meta', 'shift'] | |
| const KeyBindingMapKey = 'keyBindings' | |
| /** | |
| * @typedef {(e: KeyboardEvent) => void} KeyboardEventHandler | |
| * @typedef {Map<string, KeyboardEventHandler> } BindingMap 按键映射表 | |
| */ | |
| /** | |
| * 使用快捷键 | |
| * | |
| * @param {string[]} keyBinding 快捷键序列,可以是操作键和其他数字/字母/字符按键 | |
| * 操作键有效值:`control`(Ctrl/Control)、`option`(Alt)、`meta`(Win/Command)、`shift`、 | |
| * 其他按键输入 code 值,请参考:{@link https://developer.mozilla.org/zh-CN/docs/Web/API/KeyboardEvent/code} | |
| * @param {(e: KeyboardEvent) => void} handler 处理程序 | |
| * | |
| * @example | |
| * useUndo(['control', 'KeyZ'], handleUndo) // Control + Z | |
| * useUndo(['control|meta', 'KeyZ'], handleUndo) // Control 或 Meta + Z | |
| * useUndo(['control', 'shift', 'KeyZ'], handleRedo) // Control + Shift + Z | |
| */ | |
| export const useKeyBinding = (keyBinding, handler) => { | |
| if (keyBinding.length === 0) throw Error('快捷键序列不能为空') | |
| // Handlers | |
| /** | |
| * @param {KeyboardEvent} e | |
| */ | |
| const handleKeydown = (e) => { | |
| /** | |
| * @type {BindingMap} | |
| */ | |
| const bindingMap = globalThis[KeyBindingMapKey] | |
| if (!bindingMap) { | |
| console.error('没有找到按键映射表') | |
| return | |
| } | |
| /** | |
| * @type {string[][]} | |
| */ | |
| const bindings = [...bindingMap.keys()].map((keyStr) => keyStr.split('+')) | |
| // 查找所有匹配的绑定 | |
| const matchedBindings = bindings.filter((binding) => { | |
| for (const key of binding) { | |
| const testControlKey = (k) => { | |
| if (k === 'control' && !e.ctrlKey) return false | |
| if (k === 'option' && !e.altKey) return false | |
| if (k === 'meta' && !e.metaKey) return false | |
| if (k === 'shift' && !e.shiftKey) return false | |
| return true | |
| } | |
| if (!testControlKey(key)) return false | |
| // Test conditional | |
| if (key.includes('|')) { | |
| const anyKeys = key.split('|') | |
| if (!anyKeys.some(testControlKey)) return false | |
| } | |
| } | |
| return binding.includes(e.code) | |
| }) | |
| if (!matchedBindings.length) return // 没有匹配到按键,跳过 | |
| // 多个组合匹配时,选最长的组合 | |
| const sorted = matchedBindings.sort((a, b) => b.length - a.length) | |
| bindingMap.get(sorted[0].join('+'))?.(e) | |
| } | |
| // 同步 | |
| onMounted(() => { | |
| // 注册到全局 | |
| if (!globalThis[KeyBindingMapKey]) { | |
| document.addEventListener('keydown', handleKeydown) | |
| globalThis[KeyBindingMapKey] = new Map() | |
| } | |
| /** | |
| * @type {BindingMap} | |
| */ | |
| const bindingMap = globalThis[KeyBindingMapKey] | |
| bindingMap.set(keyBinding.join('+'), handler) | |
| }) | |
| onBeforeUnmount(() => { | |
| document.removeEventListener('keydown', handleKeydown) | |
| // 注销 | |
| /** | |
| * @type {BindingMap} | |
| */ | |
| const bindingMap = globalThis[KeyBindingMapKey] | |
| bindingMap.delete(keyBinding.join('+')) | |
| if (bindingMap.size === 0) delete globalThis[KeyBindingMapKey] | |
| }) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment