From df8ade2c4d59db493d1c6fa0b19cf22b45af6301 Mon Sep 17 00:00:00 2001 From: "exe.dev user" Date: Thu, 5 Mar 2026 03:06:03 +0000 Subject: [PATCH] Replace Match Row with Match Cell (Cmd+M) - New 'Match Cell' command selects all cells in the sheet whose value equals the current cell's value (non-contiguous selection) - Added state.selectedCells (Set of 'r,c' keys) for scatter selection - isCellSelected checks selectedCells first, then falls back to rectangular selection - getSelectedData/clearSelectedCells handle non-contiguous selections - Cmd+M keyboard shortcut mapped - Match selection cleared on click, arrow movement, or Escape Co-authored-by: Shelley --- commands.go | 2 +- frontend/src/main.js | 83 ++++++++++++++++++++++++++++++++------------ 2 files changed, 61 insertions(+), 24 deletions(-) diff --git a/commands.go b/commands.go index d946fee..806a458 100644 --- a/commands.go +++ b/commands.go @@ -33,7 +33,7 @@ func (c *CommandRegistry) GetCommands() []Command { {ID: "sort-asc", Name: "Sort A-Z", Shortcut: ""}, {ID: "sort-desc", Name: "Sort Z-A", Shortcut: ""}, {ID: "sort-advanced", Name: "Sort Advanced", Shortcut: ""}, - {ID: "match-row", Name: "Match Row", Shortcut: ""}, + {ID: "match-cell", Name: "Match Cell", Shortcut: "Cmd+M"}, {ID: "delete-row", Name: "Delete Row", Shortcut: "Cmd+Backspace"}, } } diff --git a/frontend/src/main.js b/frontend/src/main.js index 8968f03..8e3e3b1 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -7,6 +7,7 @@ const state = { filePath: '', cursor: { row: 0, col: 0 }, selection: null, // { startRow, startCol, endRow, endCol } + selectedCells: null, // Set of "r,c" strings for non-contiguous selection (Match Cell) colWidths: [], // pixel widths per column isSelecting: false, editingHeader: -1, @@ -53,6 +54,10 @@ function cellRefStr(r, c) { return colLabel(c) + (r + 1); } +function clearMatchSelection() { + state.selectedCells = null; +} + function normalizeSelection() { if (!state.selection) return null; const { startRow, startCol, endRow, endCol } = state.selection; @@ -65,6 +70,7 @@ function normalizeSelection() { } function isCellSelected(r, c) { + if (state.selectedCells) return state.selectedCells.has(`${r},${c}`); const sel = normalizeSelection(); if (!sel) return r === state.cursor.row && c === state.cursor.col; return r >= sel.r1 && r <= sel.r2 && c >= sel.c1 && c <= sel.c2; @@ -345,6 +351,8 @@ tableBody.addEventListener('mousedown', (e) => { commitFormulaBar(); } + clearMatchSelection(); + if (e.shiftKey) { // Extend selection state.selection = { @@ -384,6 +392,7 @@ document.addEventListener('mouseup', () => { // ===== Keyboard navigation ===== function moveCursor(dr, dc, shift) { + clearMatchSelection(); if (shift) { if (!state.selection) { state.selection = { @@ -470,6 +479,11 @@ document.addEventListener('keydown', (e) => { executeCommand('open'); return; } + if (meta && e.key === 'm') { + e.preventDefault(); + executeCommand('match-cell'); + return; + } // Arrow keys if (e.key === 'ArrowUp') { e.preventDefault(); moveCursor(-1, 0, e.shiftKey); return; } @@ -508,6 +522,7 @@ document.addEventListener('keydown', (e) => { // Escape clears selection if (e.key === 'Escape') { state.selection = null; + clearMatchSelection(); updateSelectionClasses(); return; } @@ -556,6 +571,29 @@ function cancelFormulaBar() { // ===== Selection data extraction ===== function getSelectedData() { + // Non-contiguous match selection: collect bounding box + if (state.selectedCells && state.selectedCells.size > 0) { + let minR = Infinity, maxR = -1, minC = Infinity, maxC = -1; + for (const key of state.selectedCells) { + const [r, c] = key.split(',').map(Number); + if (r < minR) minR = r; + if (r > maxR) maxR = r; + if (c < minC) minC = c; + if (c > maxC) maxC = c; + } + const headers = []; + for (let c = minC; c <= maxC; c++) headers.push(state.headers[c] || ''); + const rows = []; + for (let r = minR; r <= maxR; r++) { + const row = []; + for (let c = minC; c <= maxC; c++) { + row.push(state.selectedCells.has(`${r},${c}`) ? getCellValue(r, c) : ''); + } + rows.push(row); + } + return { headers, rows, isSingleCol: minC === maxC }; + } + const sel = normalizeSelection(); if (!sel) { return { @@ -584,6 +622,14 @@ function getSelectedData() { } function clearSelectedCells() { + if (state.selectedCells && state.selectedCells.size > 0) { + for (const key of state.selectedCells) { + const [r, c] = key.split(',').map(Number); + setCellValue(r, c, ''); + } + render(); + return; + } const sel = normalizeSelection(); if (!sel) { setCellValue(state.cursor.row, state.cursor.col, ''); @@ -622,7 +668,7 @@ async function loadCommands() { { ID: 'sort-asc', Name: 'Sort A-Z', Shortcut: '' }, { ID: 'sort-desc', Name: 'Sort Z-A', Shortcut: '' }, { ID: 'sort-advanced', Name: 'Sort Advanced', Shortcut: '' }, - { ID: 'match-row', Name: 'Match Row', Shortcut: '' }, + { ID: 'match-cell', Name: 'Match Cell', Shortcut: 'Cmd+M' }, { ID: 'delete-row', Name: 'Delete Row', Shortcut: 'Cmd+Backspace' }, ]; } @@ -729,7 +775,7 @@ async function executeCommand(id) { case 'sort-asc': doSort(true); break; case 'sort-desc': doSort(false); break; case 'sort-advanced': openSortAdvanced(); break; - case 'match-row': doMatchRow(); break; + case 'match-cell': doMatchCell(); break; case 'delete-row': doDeleteRow(); break; } } @@ -945,33 +991,24 @@ function doInsertColRight() { } // ===== Match & Delete Row ===== -function doMatchRow() { - const col = state.cursor.col; - const val = getCellValue(state.cursor.row, col); - const matchingRows = []; +function doMatchCell() { + const val = getCellValue(state.cursor.row, state.cursor.col); + const matched = new Set(); for (let r = 0; r < state.rows.length; r++) { - if ((state.rows[r][col] || '') === val) { - matchingRows.push(r); + for (let c = 0; c < state.headers.length; c++) { + if ((state.rows[r][c] || '') === val) { + matched.add(`${r},${c}`); + } } } - if (matchingRows.length === 0) { - setStatus('No matching rows'); + if (matched.size === 0) { + setStatus('No matching cells'); return; } - // Select from first to last matching row, spanning all columns - const first = matchingRows[0]; - const last = matchingRows[matchingRows.length - 1]; - state.cursor = { row: first, col: 0 }; - state.selection = { - startRow: first, - startCol: 0, - endRow: last, - endCol: state.headers.length - 1, - }; + state.selection = null; + state.selectedCells = matched; updateSelectionClasses(); - scrollCursorIntoView(); - const colName = state.headers[col] || colLabel(col); - setStatus(`${matchingRows.length} row(s) matching ${colName} = "${val}"`); + setStatus(`${matched.size} cell(s) matching "${val}"`); } function doDeleteRow() {