移除对 Mousetrap 的依赖,改为使用自定义的 KeyboardManager 进行快捷键绑定和管理,同时更新相关类型定义,调整菜单项的快捷键配置逻辑。
This commit is contained in:
@@ -9,12 +9,10 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"mousetrap": "^1.6.5",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/node": "^22.10.1",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"sass-embedded": "^1.82.0",
|
||||
|
||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@@ -8,9 +8,6 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
mousetrap:
|
||||
specifier: ^1.6.5
|
||||
version: 1.6.5
|
||||
vue:
|
||||
specifier: ^3.5.13
|
||||
version: 3.5.13(typescript@5.7.2)
|
||||
@@ -18,9 +15,6 @@ importers:
|
||||
specifier: ^4.5.0
|
||||
version: 4.5.0(vue@3.5.13(typescript@5.7.2))
|
||||
devDependencies:
|
||||
'@types/mousetrap':
|
||||
specifier: ^1.6.15
|
||||
version: 1.6.15
|
||||
'@types/node':
|
||||
specifier: ^22.10.1
|
||||
version: 22.10.1
|
||||
@@ -304,9 +298,6 @@ packages:
|
||||
'@types/estree@1.0.6':
|
||||
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
|
||||
|
||||
'@types/mousetrap@1.6.15':
|
||||
resolution: {integrity: sha512-qL0hyIMNPow317QWW/63RvL1x5MVMV+Ru3NaY9f/CuEpCqrmb7WeuK2071ZY5hczOnm38qExWM2i2WtkXLSqFw==}
|
||||
|
||||
'@types/node@22.10.1':
|
||||
resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==}
|
||||
|
||||
@@ -385,9 +376,6 @@ packages:
|
||||
magic-string@0.30.15:
|
||||
resolution: {integrity: sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw==}
|
||||
|
||||
mousetrap@1.6.5:
|
||||
resolution: {integrity: sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==}
|
||||
|
||||
nanoid@3.3.8:
|
||||
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
@@ -766,8 +754,6 @@ snapshots:
|
||||
|
||||
'@types/estree@1.0.6': {}
|
||||
|
||||
'@types/mousetrap@1.6.15': {}
|
||||
|
||||
'@types/node@22.10.1':
|
||||
dependencies:
|
||||
undici-types: 6.20.0
|
||||
@@ -881,8 +867,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
mousetrap@1.6.5: {}
|
||||
|
||||
nanoid@3.3.8: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
import VirtualList from './VirtualList.vue'
|
||||
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
|
||||
import type { ListItem, MenuItem } from '@/types'
|
||||
import Mousetrap from 'mousetrap'
|
||||
import 'mousetrap/plugins/global-bind/mousetrap-global-bind'
|
||||
import { keyboardManager } from '@/utils/KeyboardManager'
|
||||
|
||||
/**
|
||||
* 组件属性接口
|
||||
@@ -54,7 +53,7 @@ const toastMessage = ref<string>('')
|
||||
/** 是否正在使用键盘导航菜单 */
|
||||
const isKeyboardNavigation = ref<boolean>(false)
|
||||
/** 快捷键处理函数映射 */
|
||||
const shortcutHandlers = new Map<string, () => void>()
|
||||
const shortcutHandlers = new Map<string, (e: KeyboardEvent) => void>()
|
||||
|
||||
// ===== 定时器 =====
|
||||
/** 鼠标悬停禁用定时器 */
|
||||
@@ -105,12 +104,10 @@ const closeMenu = (): void => {
|
||||
isKeyboardNavigation.value = false
|
||||
|
||||
// 清除快捷键绑定
|
||||
shortcutHandlers.forEach((_, shortcut) => {
|
||||
Mousetrap.unbind(shortcut)
|
||||
})
|
||||
keyboardManager.disable()
|
||||
shortcutHandlers.clear()
|
||||
|
||||
// 临时禁用鼠标悬停,防止菜单关闭时立即触发悬停效果
|
||||
// 临时禁用鼠标悬停,防止菜单关闭时<EFBFBD><EFBFBD><EFBFBD>即触发悬停效果
|
||||
temporaryDisableHover.value = true
|
||||
if (hoverDisableTimer) clearTimeout(hoverDisableTimer)
|
||||
hoverDisableTimer = window.setTimeout(() => {
|
||||
@@ -158,7 +155,7 @@ const handleMenuTriggerClick = (e: MouseEvent): void => {
|
||||
* - 左方向键/ESC:关闭菜单
|
||||
* - 上下方向键:在菜单打开时导航菜单项
|
||||
* - 回车键:在菜单打开时选择当前菜单项
|
||||
* @param e 键盘事件对象
|
||||
* @param e 键盘事件<EFBFBD><EFBFBD>象
|
||||
*/
|
||||
const handleKeyDown = (e: KeyboardEvent): void => {
|
||||
if (e.key === 'ArrowRight' && !showMenu.value && selectedItem.value) {
|
||||
@@ -181,7 +178,7 @@ const handleKeyDown = (e: KeyboardEvent): void => {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
isKeyboardNavigation.value = true
|
||||
// 向下选<EFBFBD><EFBFBD>菜单项,到底部时循环到顶部
|
||||
// 向下选择菜单项,到底部时循环到顶部
|
||||
selectedMenuIndex.value =
|
||||
selectedMenuIndex.value >= currentMenuItems.value.length - 1
|
||||
? 0
|
||||
@@ -232,10 +229,7 @@ onBeforeUnmount(() => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
if (toastTimer) clearTimeout(toastTimer)
|
||||
if (hoverDisableTimer) clearTimeout(hoverDisableTimer)
|
||||
// 清除所有快捷键绑定
|
||||
shortcutHandlers.forEach((_, shortcut) => {
|
||||
Mousetrap.unbind(shortcut)
|
||||
})
|
||||
keyboardManager.disable()
|
||||
})
|
||||
|
||||
// ===== 过渡钩子 =====
|
||||
@@ -257,46 +251,31 @@ const onAfterLeave = (el: Element): void => {
|
||||
(el as HTMLElement).style.display = ''
|
||||
}
|
||||
|
||||
// 在导入后添加 Mousetrap 配置
|
||||
Mousetrap.prototype.stopCallback = (e: KeyboardEvent, element: HTMLElement, combo: string) => {
|
||||
// 当菜单打开时,总是响应快捷键
|
||||
if (showMenu.value) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return false
|
||||
}
|
||||
// 否则使用默认行为
|
||||
return element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA'
|
||||
}
|
||||
|
||||
// 修改 bindShortcuts 函数
|
||||
const bindShortcuts = (): void => {
|
||||
// 清除之前的绑定
|
||||
shortcutHandlers.forEach((_, shortcut) => {
|
||||
Mousetrap.unbind(shortcut)
|
||||
keyboardManager.unbind(shortcut)
|
||||
})
|
||||
shortcutHandlers.clear()
|
||||
|
||||
// 只在菜单打开时绑定快捷键
|
||||
if (showMenu.value) {
|
||||
keyboardManager.enable()
|
||||
currentMenuItems.value.forEach((item) => {
|
||||
if (item.shortcut) {
|
||||
const handler = (e: ExtendedKeyboardEvent) => {
|
||||
// 阻止浏览器默认行为
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (selectedItem.value || !item.action.toString().includes('selectedItem')) {
|
||||
item.action(selectedItem.value)
|
||||
// 执行后关闭菜单
|
||||
closeMenu()
|
||||
}
|
||||
return false // 阻止事件冒泡
|
||||
}
|
||||
shortcutHandlers.set(item.shortcut, handler)
|
||||
Mousetrap.bindGlobal(item.shortcut, handler)
|
||||
keyboardManager.bind(item.shortcut, handler)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
keyboardManager.disable()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -438,7 +417,7 @@ const bindShortcuts = (): void => {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 工具栏:固定高度,顶部边框和阴影效果 */
|
||||
/* 工具栏固定高度,顶部边框和阴影效果 */
|
||||
.toolbar {
|
||||
height: 40px;
|
||||
background-color: #fff;
|
||||
|
||||
@@ -26,7 +26,7 @@ const initListData = () => {
|
||||
action: (item) => {
|
||||
console.log('打开项目:', item?.name)
|
||||
},
|
||||
shortcut: 'ctrl+o'
|
||||
shortcut: 'alt+o'
|
||||
},
|
||||
{
|
||||
id: 'edit',
|
||||
|
||||
116
src/utils/KeyboardManager.ts
Normal file
116
src/utils/KeyboardManager.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
type KeyboardCallback = (e: KeyboardEvent) => void
|
||||
|
||||
export class KeyboardManager {
|
||||
private shortcuts: Map<string, KeyboardCallback> = new Map()
|
||||
private pressedKeys: Set<string> = new Set()
|
||||
private enabled: boolean = false
|
||||
|
||||
constructor() {
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this)
|
||||
this.handleKeyUp = this.handleKeyUp.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用快捷键监听
|
||||
*/
|
||||
enable(): void {
|
||||
if (!this.enabled) {
|
||||
window.addEventListener('keydown', this.handleKeyDown)
|
||||
window.addEventListener('keyup', this.handleKeyUp)
|
||||
this.enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用快捷键监听
|
||||
*/
|
||||
disable(): void {
|
||||
if (this.enabled) {
|
||||
window.removeEventListener('keydown', this.handleKeyDown)
|
||||
window.removeEventListener('keyup', this.handleKeyUp)
|
||||
this.enabled = false
|
||||
this.pressedKeys.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定快捷键
|
||||
*/
|
||||
bind(shortcut: string, callback: KeyboardCallback): void {
|
||||
this.shortcuts.set(this.normalizeShortcut(shortcut), callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑快捷键
|
||||
*/
|
||||
unbind(shortcut: string): void {
|
||||
this.shortcuts.delete(this.normalizeShortcut(shortcut))
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有快捷键绑定
|
||||
*/
|
||||
clear(): void {
|
||||
this.shortcuts.clear()
|
||||
this.pressedKeys.clear()
|
||||
}
|
||||
|
||||
private handleKeyDown(e: KeyboardEvent): void {
|
||||
// 忽略在输入框中的按键
|
||||
if (this.shouldIgnoreInput(e)) {
|
||||
return
|
||||
}
|
||||
|
||||
const key = this.normalizeKey(e.key.toLowerCase())
|
||||
this.pressedKeys.add(key)
|
||||
|
||||
const currentShortcut = Array.from(this.pressedKeys).sort().join('+')
|
||||
const callback = this.shortcuts.get(currentShortcut)
|
||||
|
||||
if (callback) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
callback(e)
|
||||
}
|
||||
}
|
||||
|
||||
private handleKeyUp(e: KeyboardEvent): void {
|
||||
const key = this.normalizeKey(e.key.toLowerCase())
|
||||
this.pressedKeys.delete(key)
|
||||
}
|
||||
|
||||
private normalizeKey(key: string): string {
|
||||
const keyMap: Record<string, string> = {
|
||||
'control': 'ctrl',
|
||||
'command': 'cmd',
|
||||
'meta': 'cmd',
|
||||
'escape': 'esc',
|
||||
' ': 'space',
|
||||
'arrowup': 'up',
|
||||
'arrowdown': 'down',
|
||||
'arrowleft': 'left',
|
||||
'arrowright': 'right',
|
||||
}
|
||||
return keyMap[key] || key
|
||||
}
|
||||
|
||||
private normalizeShortcut(shortcut: string): string {
|
||||
return shortcut
|
||||
.toLowerCase()
|
||||
.split('+')
|
||||
.map(key => this.normalizeKey(key.trim()))
|
||||
.sort()
|
||||
.join('+')
|
||||
}
|
||||
|
||||
private shouldIgnoreInput(e: KeyboardEvent): boolean {
|
||||
const element = e.target as HTMLElement
|
||||
return element.tagName === 'INPUT' ||
|
||||
element.tagName === 'TEXTAREA' ||
|
||||
element.tagName === 'SELECT' ||
|
||||
element.isContentEditable
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const keyboardManager = new KeyboardManager()
|
||||
Reference in New Issue
Block a user