- 主题(浅色/深色/跟随系统)与正文字号(标准/大/特大)按账号存在本机, 账号切换不串、退出回默认;深色走 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 与需求更新
59 lines
2.5 KiB
TypeScript
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))
|
|
}
|