280 lines
6.2 KiB
TypeScript
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
|
|
}
|
|
} |