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 <shelley@exe.dev>
This commit is contained in:
parent
0e68de4278
commit
df8ade2c4d
|
|
@ -33,7 +33,7 @@ func (c *CommandRegistry) GetCommands() []Command {
|
||||||
{ID: "sort-asc", Name: "Sort A-Z", Shortcut: ""},
|
{ID: "sort-asc", Name: "Sort A-Z", Shortcut: ""},
|
||||||
{ID: "sort-desc", Name: "Sort Z-A", Shortcut: ""},
|
{ID: "sort-desc", Name: "Sort Z-A", Shortcut: ""},
|
||||||
{ID: "sort-advanced", Name: "Sort Advanced", 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"},
|
{ID: "delete-row", Name: "Delete Row", Shortcut: "Cmd+Backspace"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ const state = {
|
||||||
filePath: '',
|
filePath: '',
|
||||||
cursor: { row: 0, col: 0 },
|
cursor: { row: 0, col: 0 },
|
||||||
selection: null, // { startRow, startCol, endRow, endCol }
|
selection: null, // { startRow, startCol, endRow, endCol }
|
||||||
|
selectedCells: null, // Set of "r,c" strings for non-contiguous selection (Match Cell)
|
||||||
colWidths: [], // pixel widths per column
|
colWidths: [], // pixel widths per column
|
||||||
isSelecting: false,
|
isSelecting: false,
|
||||||
editingHeader: -1,
|
editingHeader: -1,
|
||||||
|
|
@ -53,6 +54,10 @@ function cellRefStr(r, c) {
|
||||||
return colLabel(c) + (r + 1);
|
return colLabel(c) + (r + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearMatchSelection() {
|
||||||
|
state.selectedCells = null;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeSelection() {
|
function normalizeSelection() {
|
||||||
if (!state.selection) return null;
|
if (!state.selection) return null;
|
||||||
const { startRow, startCol, endRow, endCol } = state.selection;
|
const { startRow, startCol, endRow, endCol } = state.selection;
|
||||||
|
|
@ -65,6 +70,7 @@ function normalizeSelection() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCellSelected(r, c) {
|
function isCellSelected(r, c) {
|
||||||
|
if (state.selectedCells) return state.selectedCells.has(`${r},${c}`);
|
||||||
const sel = normalizeSelection();
|
const sel = normalizeSelection();
|
||||||
if (!sel) return r === state.cursor.row && c === state.cursor.col;
|
if (!sel) return r === state.cursor.row && c === state.cursor.col;
|
||||||
return r >= sel.r1 && r <= sel.r2 && c >= sel.c1 && c <= sel.c2;
|
return r >= sel.r1 && r <= sel.r2 && c >= sel.c1 && c <= sel.c2;
|
||||||
|
|
@ -345,6 +351,8 @@ tableBody.addEventListener('mousedown', (e) => {
|
||||||
commitFormulaBar();
|
commitFormulaBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearMatchSelection();
|
||||||
|
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
// Extend selection
|
// Extend selection
|
||||||
state.selection = {
|
state.selection = {
|
||||||
|
|
@ -384,6 +392,7 @@ document.addEventListener('mouseup', () => {
|
||||||
|
|
||||||
// ===== Keyboard navigation =====
|
// ===== Keyboard navigation =====
|
||||||
function moveCursor(dr, dc, shift) {
|
function moveCursor(dr, dc, shift) {
|
||||||
|
clearMatchSelection();
|
||||||
if (shift) {
|
if (shift) {
|
||||||
if (!state.selection) {
|
if (!state.selection) {
|
||||||
state.selection = {
|
state.selection = {
|
||||||
|
|
@ -470,6 +479,11 @@ document.addEventListener('keydown', (e) => {
|
||||||
executeCommand('open');
|
executeCommand('open');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (meta && e.key === 'm') {
|
||||||
|
e.preventDefault();
|
||||||
|
executeCommand('match-cell');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Arrow keys
|
// Arrow keys
|
||||||
if (e.key === 'ArrowUp') { e.preventDefault(); moveCursor(-1, 0, e.shiftKey); return; }
|
if (e.key === 'ArrowUp') { e.preventDefault(); moveCursor(-1, 0, e.shiftKey); return; }
|
||||||
|
|
@ -508,6 +522,7 @@ document.addEventListener('keydown', (e) => {
|
||||||
// Escape clears selection
|
// Escape clears selection
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
state.selection = null;
|
state.selection = null;
|
||||||
|
clearMatchSelection();
|
||||||
updateSelectionClasses();
|
updateSelectionClasses();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -556,6 +571,29 @@ function cancelFormulaBar() {
|
||||||
|
|
||||||
// ===== Selection data extraction =====
|
// ===== Selection data extraction =====
|
||||||
function getSelectedData() {
|
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();
|
const sel = normalizeSelection();
|
||||||
if (!sel) {
|
if (!sel) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -584,6 +622,14 @@ function getSelectedData() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSelectedCells() {
|
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();
|
const sel = normalizeSelection();
|
||||||
if (!sel) {
|
if (!sel) {
|
||||||
setCellValue(state.cursor.row, state.cursor.col, '');
|
setCellValue(state.cursor.row, state.cursor.col, '');
|
||||||
|
|
@ -622,7 +668,7 @@ async function loadCommands() {
|
||||||
{ ID: 'sort-asc', Name: 'Sort A-Z', Shortcut: '' },
|
{ ID: 'sort-asc', Name: 'Sort A-Z', Shortcut: '' },
|
||||||
{ ID: 'sort-desc', Name: 'Sort Z-A', Shortcut: '' },
|
{ ID: 'sort-desc', Name: 'Sort Z-A', Shortcut: '' },
|
||||||
{ ID: 'sort-advanced', Name: 'Sort Advanced', 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' },
|
{ 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-asc': doSort(true); break;
|
||||||
case 'sort-desc': doSort(false); break;
|
case 'sort-desc': doSort(false); break;
|
||||||
case 'sort-advanced': openSortAdvanced(); break;
|
case 'sort-advanced': openSortAdvanced(); break;
|
||||||
case 'match-row': doMatchRow(); break;
|
case 'match-cell': doMatchCell(); break;
|
||||||
case 'delete-row': doDeleteRow(); break;
|
case 'delete-row': doDeleteRow(); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -945,33 +991,24 @@ function doInsertColRight() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Match & Delete Row =====
|
// ===== Match & Delete Row =====
|
||||||
function doMatchRow() {
|
function doMatchCell() {
|
||||||
const col = state.cursor.col;
|
const val = getCellValue(state.cursor.row, state.cursor.col);
|
||||||
const val = getCellValue(state.cursor.row, col);
|
const matched = new Set();
|
||||||
const matchingRows = [];
|
|
||||||
for (let r = 0; r < state.rows.length; r++) {
|
for (let r = 0; r < state.rows.length; r++) {
|
||||||
if ((state.rows[r][col] || '') === val) {
|
for (let c = 0; c < state.headers.length; c++) {
|
||||||
matchingRows.push(r);
|
if ((state.rows[r][c] || '') === val) {
|
||||||
|
matched.add(`${r},${c}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (matchingRows.length === 0) {
|
if (matched.size === 0) {
|
||||||
setStatus('No matching rows');
|
setStatus('No matching cells');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Select from first to last matching row, spanning all columns
|
state.selection = null;
|
||||||
const first = matchingRows[0];
|
state.selectedCells = matched;
|
||||||
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,
|
|
||||||
};
|
|
||||||
updateSelectionClasses();
|
updateSelectionClasses();
|
||||||
scrollCursorIntoView();
|
setStatus(`${matched.size} cell(s) matching "${val}"`);
|
||||||
const colName = state.headers[col] || colLabel(col);
|
|
||||||
setStatus(`${matchingRows.length} row(s) matching ${colName} = "${val}"`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function doDeleteRow() {
|
function doDeleteRow() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue