Files
lexgo/learner/src/composables/reviewShortcuts.ts
T
ila e069894c4e feat: 补齐桌面与手机体验、显示偏好与键盘操作 (#14)
- 主题(浅色/深色/跟随系统)与正文字号(标准/大/特大)按账号存在本机,
  账号切换不串、退出回默认;深色走 html[data-theme] 与 Element Plus 的 html.dark
- style.css 收敛为语义调色板::root 的 53 个变量是文件内仅有的颜色字面量,
  其余规则全部走 var(),暗色只覆盖变量
- 字号经 --reader-font-scale 只作用于阅读面,不做全局缩放
- 阅读位置按账号+章节保存滚动比例与该章 content_sha256,正文换版本后不恢复
- 复习页键盘:空格/Enter 显示答案、1/2/3 评分;输入控件与聚焦按钮的按键不被劫持
- 站点头部新增「显示」控件,七个学习页面共用
- Playwright 新增 390×844 hasTouch 的 mobile 项目与移动用例(无溢出、可返回、
  触摸滑动不误开面板、深色与字号持久化、账号隔离);新增主题变量回归用例
- Wiki 记录 Architecture、Business-Rules、Local-Development 与需求更新
2026-09-15 15:11:30 +08:00

59 lines
2.5 KiB
TypeScript

import { onScopeDispose } from 'vue'
export type ReviewShortcut = 'reveal' | 'correct' | 'wrong' | 'again'
export interface ReviewShortcutOptions {
/** Called for a shortcut that applies in the current card state. */
onShortcut: (action: ReviewShortcut) => void
/** False while the card is busy or an answer failed; then no shortcut is accepted. */
enabled: () => boolean
/** Whether the answer is already on screen. */
revealed: () => boolean
}
const GRADE_KEYS: Record<string, ReviewShortcut> = { '1': 'correct', '2': 'wrong', '3': 'again' }
/**
* True when the key belongs to whatever currently has the focus instead of the review.
*
* Typing fields and dropdowns own every key. A focused button or link owns Space and Enter,
* because the browser already activates it there and a second handler would answer twice — but a
* digit is not a button key, so grading from the keyboard keeps working while a button is focused.
*/
export function ignoresShortcut(target: EventTarget | null, key: string): boolean {
const element = target as HTMLElement | null
if (!element || typeof element.closest !== 'function') return false
if (element.isContentEditable) return true
if (element.closest('input, textarea, select, [contenteditable="true"], .el-select, .el-dropdown') !== null) return true
if (key === ' ' || key === 'Enter') {
return element.closest('button, a[href], [role="button"]') !== null
}
return false
}
/** Maps one key press to a review action, or null when the key is not ours. */
export function shortcutFor(key: string, revealed: boolean): ReviewShortcut | null {
if (key === ' ' || key === 'Enter') return revealed ? null : 'reveal'
if (!revealed) return null
return GRADE_KEYS[key] ?? null
}
/**
* Keyboard operations for the due review: Space or Enter shows the answer, then 1, 2 and 3 grade
* it. A modifier key means the learner is using a browser or system shortcut, so it is left alone.
*/
export function useReviewShortcuts(options: ReviewShortcutOptions) {
function onKeydown(event: KeyboardEvent): void {
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return
if (!options.enabled()) return
if (ignoresShortcut(event.target, event.key)) return
const action = shortcutFor(event.key, options.revealed())
if (action === null) return
event.preventDefault()
options.onShortcut(action)
}
document.addEventListener('keydown', onKeydown)
onScopeDispose(() => document.removeEventListener('keydown', onKeydown))
}