dramaling-app/apps/web/src/composables/useKeyboard.ts

280 lines
6.2 KiB
TypeScript

import { ref, onMounted, onUnmounted } from 'vue'
export interface KeyboardShortcut {
key: string
code: string
description: string
action: () => void
preventDefault?: boolean
ctrlKey?: boolean
shiftKey?: boolean
altKey?: boolean
metaKey?: boolean
}
export interface KeyboardOptions {
ignoreInputs?: boolean
ignoreContentEditable?: boolean
}
export function useKeyboard(options: KeyboardOptions = {}) {
const shortcuts = ref<Map<string, KeyboardShortcut>>(new Map())
const isEnabled = ref(true)
const lastKeyPressed = ref<string>('')
const keySequence = ref<string[]>([])
const defaultOptions: Required<KeyboardOptions> = {
ignoreInputs: true,
ignoreContentEditable: true,
...options
}
// 檢查是否應該忽略按鍵事件
const shouldIgnoreEvent = (event: KeyboardEvent): boolean => {
if (!isEnabled.value) return true
const target = event.target as HTMLElement
// 檢查是否在輸入框中
if (defaultOptions.ignoreInputs) {
if (target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement ||
target instanceof HTMLSelectElement) {
return true
}
}
// 檢查是否在可編輯元素中
if (defaultOptions.ignoreContentEditable) {
if (target.contentEditable === 'true') {
return true
}
}
return false
}
// 生成快捷鍵的唯一標識符
const generateShortcutKey = (shortcut: Omit<KeyboardShortcut, 'action' | 'description'>): string => {
const modifiers = []
if (shortcut.ctrlKey) modifiers.push('ctrl')
if (shortcut.shiftKey) modifiers.push('shift')
if (shortcut.altKey) modifiers.push('alt')
if (shortcut.metaKey) modifiers.push('meta')
return [...modifiers, shortcut.code.toLowerCase()].join('+')
}
// 檢查事件是否匹配快捷鍵
const matchesShortcut = (event: KeyboardEvent, shortcut: KeyboardShortcut): boolean => {
return (
event.code === shortcut.code &&
!!event.ctrlKey === !!shortcut.ctrlKey &&
!!event.shiftKey === !!shortcut.shiftKey &&
!!event.altKey === !!shortcut.altKey &&
!!event.metaKey === !!shortcut.metaKey
)
}
// 註冊快捷鍵
const register = (shortcut: KeyboardShortcut) => {
const key = generateShortcutKey(shortcut)
shortcuts.value.set(key, shortcut)
}
// 批量註冊快捷鍵
const registerMultiple = (shortcutList: KeyboardShortcut[]) => {
shortcutList.forEach(shortcut => register(shortcut))
}
// 取消註冊快捷鍵
const unregister = (code: string, modifiers?: {
ctrlKey?: boolean
shiftKey?: boolean
altKey?: boolean
metaKey?: boolean
}) => {
const key = generateShortcutKey({
code,
key: '',
...modifiers
})
shortcuts.value.delete(key)
}
// 清空所有快捷鍵
const clear = () => {
shortcuts.value.clear()
}
// 啟用/禁用快捷鍵
const enable = () => {
isEnabled.value = true
}
const disable = () => {
isEnabled.value = false
}
const toggle = () => {
isEnabled.value = !isEnabled.value
}
// 獲取所有已註冊的快捷鍵
const getShortcuts = () => {
return Array.from(shortcuts.value.values())
}
// 按鍵事件處理器
const handleKeydown = (event: KeyboardEvent) => {
if (shouldIgnoreEvent(event)) return
lastKeyPressed.value = event.code
keySequence.value.push(event.code)
// 限制序列長度
if (keySequence.value.length > 5) {
keySequence.value.shift()
}
// 查找匹配的快捷鍵
for (const shortcut of shortcuts.value.values()) {
if (matchesShortcut(event, shortcut)) {
if (shortcut.preventDefault !== false) {
event.preventDefault()
}
try {
shortcut.action()
} catch (error) {
console.error('快捷鍵執行錯誤:', error)
}
break
}
}
}
// 常用快捷鍵預設集
const presets = {
// 詞彙學習相關
vocabulary: [
{
key: 'Space',
code: 'Space',
description: '播放/暫停音頻',
action: () => {}
},
{
key: 'ArrowRight',
code: 'ArrowRight',
description: '下一個詞彙',
action: () => {}
},
{
key: 'ArrowLeft',
code: 'ArrowLeft',
description: '上一個詞彙',
action: () => {}
},
{
key: 'h',
code: 'KeyH',
description: '顯示/隱藏幫助',
action: () => {}
},
{
key: 'a',
code: 'KeyA',
description: '切換自動播放',
action: () => {}
},
{
key: 'r',
code: 'KeyR',
description: '重播音頻',
action: () => {}
}
] as KeyboardShortcut[],
// 練習模式相關
practice: [
{
key: 'Enter',
code: 'Enter',
description: '提交答案',
action: () => {}
},
{
key: 'n',
code: 'KeyN',
description: '下一題',
action: () => {}
},
{
key: 's',
code: 'KeyS',
description: '跳過題目',
action: () => {}
},
{
key: 'Escape',
code: 'Escape',
description: '退出練習',
action: () => {}
}
] as KeyboardShortcut[],
// 通用導航
navigation: [
{
key: 'Escape',
code: 'Escape',
description: '返回上一頁',
action: () => {}
},
{
key: 'f',
code: 'KeyF',
description: '全螢幕模式',
action: () => {}
},
{
key: '/',
code: 'Slash',
description: '搜索',
action: () => {}
}
] as KeyboardShortcut[]
}
// 生命週期
onMounted(() => {
document.addEventListener('keydown', handleKeydown)
})
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown)
})
return {
// 狀態
shortcuts,
isEnabled,
lastKeyPressed,
keySequence,
// 方法
register,
registerMultiple,
unregister,
clear,
enable,
disable,
toggle,
getShortcuts,
// 預設集
presets
}
}