From 7124011da9c95cf67989289638d4e4907d7c8524 Mon Sep 17 00:00:00 2001 From: biss Date: Sat, 15 Aug 2026 18:57:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=88=90=E7=BB=A9=E5=BD=95=E5=85=A5=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/utils/scoreGridNavigation.ts | 67 ++++++++++++++ web/src/views/ExperimentGradesView.vue | 116 +++++++++++++++++-------- web/src/views/GradesView.vue | 105 +++++++++++++++++----- 3 files changed, 234 insertions(+), 54 deletions(-) create mode 100644 web/src/utils/scoreGridNavigation.ts diff --git a/web/src/utils/scoreGridNavigation.ts b/web/src/utils/scoreGridNavigation.ts new file mode 100644 index 0000000..f375150 --- /dev/null +++ b/web/src/utils/scoreGridNavigation.ts @@ -0,0 +1,67 @@ +import { nextTick } from 'vue' + +type Direction = 'up' | 'down' | 'left' | 'right' + +/** + * Makes an editable score table behave like a spreadsheet without changing + * the normal keyboard behaviour of selects, switches, or free-text fields. + */ +export function useScoreGridNavigation(grid: string) { + function handleScoreGridKeydown(event: KeyboardEvent) { + const directionByKey: Record = { + Enter: 'down', + ArrowUp: 'up', + ArrowDown: 'down', + ArrowLeft: 'left', + ArrowRight: 'right', + } + const direction = directionByKey[event.key] + if (!direction) return + + const current = event.currentTarget as HTMLElement + const row = Number(current.dataset.scoreRow) + const column = Number(current.dataset.scoreColumn) + if (!Number.isFinite(row) || !Number.isFinite(column)) return + + const cells = Array.from( + document.querySelectorAll(`[data-score-grid="${grid}"]`), + ).filter((cell) => { + const input = cell.querySelector('input') + return input && !input.disabled && !input.readOnly + }) + + const findCell = (targetRow: number, targetColumn: number) => + cells.find((cell) => + Number(cell.dataset.scoreRow) === targetRow + && Number(cell.dataset.scoreColumn) === targetColumn) + + let target: HTMLElement | undefined + if (direction === 'left' || direction === 'right') { + const rowCells = cells + .filter((cell) => Number(cell.dataset.scoreRow) === row) + .sort((left, right) => Number(left.dataset.scoreColumn) - Number(right.dataset.scoreColumn)) + const currentIndex = rowCells.indexOf(current) + target = rowCells[currentIndex + (direction === 'left' ? -1 : 1)] + } else { + const columnCells = cells + .filter((cell) => Number(cell.dataset.scoreColumn) === column) + .sort((top, bottom) => Number(top.dataset.scoreRow) - Number(bottom.dataset.scoreRow)) + const currentIndex = columnCells.indexOf(current) + target = columnCells[currentIndex + (direction === 'up' ? -1 : 1)] + ?? findCell(row + (direction === 'up' ? -1 : 1), column) + } + + // Keep arrow keys from changing an input-number value; navigation is the + // expected spreadsheet action even at an edge where no next cell exists. + event.preventDefault() + if (!target) return + + void nextTick(() => { + const input = target?.querySelector('input') + input?.focus() + input?.select() + }) + } + + return { handleScoreGridKeydown } +} diff --git a/web/src/views/ExperimentGradesView.vue b/web/src/views/ExperimentGradesView.vue index 2051b25..a414ba2 100644 --- a/web/src/views/ExperimentGradesView.vue +++ b/web/src/views/ExperimentGradesView.vue @@ -1,5 +1,5 @@