From e45cdfd077cc982bad7de154f233fdc3fea6fff1 Mon Sep 17 00:00:00 2001 From: Wild Karl-Heinz Date: Wed, 2 Sep 2026 09:59:29 +0200 Subject: [PATCH] Keep the first-selected cell marked as active during a multi-cell selection The active cell (bold border/handle, formula bar target) used to track selection.focus, so it followed the mouse/cursor while dragging or shift-extending a range. Switch it to selection.anchor -- the first cell of the current selection action -- so it stays put while the range grows, matching Excel/Numbers. Tab/Enter data-entry flow already keeps anchor == focus while it moves the cursor, so that behaviour is unaffected. - document.ts: activeCell/activeCellName/activeCellInput read the anchor. - FormulaBar.vue: commits to the anchor cell to match what it displays. - TableGrid.vue: isActive() (border + handle) and F2/type-to-edit target the anchor; Home/End keep using the cursor's row via s.focus. Co-Authored-By: Claude Sonnet 5 --- src/components/FormulaBar.vue | 2 +- src/components/TableGrid.vue | 12 ++++++++---- src/store/__tests__/document.spec.ts | 4 +++- src/store/document.ts | 9 ++++++--- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/components/FormulaBar.vue b/src/components/FormulaBar.vue index 00ab705..620123d 100644 --- a/src/components/FormulaBar.vue +++ b/src/components/FormulaBar.vue @@ -17,7 +17,7 @@ watch( function commit() { const sel = store.doc.selection; - if (sel) store.setCellInput(sel.tableId, sel.focus.col, sel.focus.row, draft.value); + if (sel) store.setCellInput(sel.tableId, sel.anchor.col, sel.anchor.row, draft.value); } function onEnter() { commit(); diff --git a/src/components/TableGrid.vue b/src/components/TableGrid.vue index 7d366d3..e028d9e 100644 --- a/src/components/TableGrid.vue +++ b/src/components/TableGrid.vue @@ -111,8 +111,12 @@ const mine = computed( ); /** All selected rectangles (primary + any additional) for this table. */ const selRects = computed(() => (mine.value ? store.selectionRects(mine.value) : [])); +/** The active cell is the anchor -- the first cell of the current selection -- + * so it stays marked while a multi-cell selection is dragged/extended out + * from it (Excel/Numbers behaviour). During Tab/Enter data-entry flow the + * anchor is kept equal to the flowing cell, so this still tracks it there. */ const isActive = (c: number, r: number) => - !!mine.value && mine.value.focus.col === c && mine.value.focus.row === r; + !!mine.value && mine.value.anchor.col === c && mine.value.anchor.row === r; const inSelection = (c: number, r: number) => selRects.value.some((k) => c >= k.c0 && c <= k.c1 && r >= k.r0 && r <= k.r1); const colHot = (c: number) => selRects.value.some((k) => c >= k.c0 && c <= k.c1); @@ -481,7 +485,7 @@ function onKeydown(e: KeyboardEvent) { const s = mine.value; const t = table.value; if (!s || !t) return; - const { col, row } = s.focus; + const { row } = s.focus; // Home/End jump within the cursor's current row const ext = e.shiftKey; const mod = e.ctrlKey || e.metaKey; @@ -532,7 +536,7 @@ function onKeydown(e: KeyboardEvent) { case 'F2': e.preventDefault(); e.stopPropagation(); - return beginEdit(col, row); + return beginEdit(s.anchor.col, s.anchor.row); case 'Home': e.preventDefault(); e.stopPropagation(); @@ -566,7 +570,7 @@ function onKeydown(e: KeyboardEvent) { if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) { e.preventDefault(); e.stopPropagation(); - beginEdit(col, row, e.key); + beginEdit(s.anchor.col, s.anchor.row, e.key); } } } diff --git a/src/store/__tests__/document.spec.ts b/src/store/__tests__/document.spec.ts index bfdf98c..bfab37d 100644 --- a/src/store/__tests__/document.spec.ts +++ b/src/store/__tests__/document.spec.ts @@ -135,7 +135,9 @@ describe('seed document acceptance (canvas model)', () => { s.setSelection({ tableId: umsatz, anchor: { col: 1, row: 1 }, focus: { col: 1, row: 5 } }); expect(s.selectionAggregate.value.sum).toBe(6175); expect(s.selectionAggregate.value.count).toBe(5); - expect(s.activeCellName.value).toBe(`Umsatz::${toA1(1, 5)}`); + // the active cell is the anchor -- the first cell of the selection -- not + // wherever the selection was extended to. + expect(s.activeCellName.value).toBe(`Umsatz::${toA1(1, 1)}`); }); it('recalculation visits precedents before dependents across tables', () => { diff --git a/src/store/document.ts b/src/store/document.ts index a5366cd..329078d 100644 --- a/src/store/document.ts +++ b/src/store/document.ts @@ -993,18 +993,21 @@ function createStore() { }); } - const activeCell = computed(() => doc.selection?.focus ?? { col: 0, row: 0 }); + // The "active cell" is always the anchor -- the first cell of the current + // selection action (click, shift-click, drag start, ...) -- so it stays put + // while a multi-cell selection is being made, matching Excel/Numbers. + const activeCell = computed(() => doc.selection?.anchor ?? { col: 0, row: 0 }); const activeCellName = computed(() => { const sel = doc.selection; const t = sel ? tableById(sel.tableId) : activeTable.value; - const cell = sel?.focus ?? { col: 0, row: 0 }; + const cell = sel?.anchor ?? { col: 0, row: 0 }; return `${t?.name ?? '—'}::${toA1(cell.col, cell.row)}`; }); const activeCellInput = computed(() => { const sel = doc.selection; - return sel ? cellInput(sel.tableId, sel.focus.col, sel.focus.row) : ''; + return sel ? cellInput(sel.tableId, sel.anchor.col, sel.anchor.row) : ''; }); const selectionAggregate = computed(() => {