Skip to content

Instantly share code, notes, and snippets.

@ZacharyBear
Last active December 3, 2025 07:04
Show Gist options
  • Select an option

  • Save ZacharyBear/a67cfe0637a35ad07b375ee9882c04f6 to your computer and use it in GitHub Desktop.

Select an option

Save ZacharyBear/a67cfe0637a35ad07b375ee9882c04f6 to your computer and use it in GitHub Desktop.
KeyBinding hook in Vue3
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