diff --git a/app_test.go b/app_test.go
index 0781425..d566601 100644
--- a/app_test.go
+++ b/app_test.go
@@ -98,8 +98,8 @@ func TestParseCSVString(t *testing.T) {
func TestGetCommands(t *testing.T) {
reg := NewCommandRegistry()
cmds := reg.GetCommands()
- if len(cmds) != 18 {
- t.Errorf("expected 18 commands, got %d", len(cmds))
+ if len(cmds) != 17 {
+ t.Errorf("expected 12 commands, got %d", len(cmds))
}
// Check that all have IDs
for _, cmd := range cmds {
diff --git a/commands.go b/commands.go
index d79dbf2..d946fee 100644
--- a/commands.go
+++ b/commands.go
@@ -33,8 +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-cell", Name: "Match Cell", Shortcut: "Cmd+M"},
- {ID: "set-where", Name: "Set Where", Shortcut: ""},
+ {ID: "match-row", Name: "Match Row", Shortcut: ""},
{ID: "delete-row", Name: "Delete Row", Shortcut: "Cmd+Backspace"},
}
}
diff --git a/frontend/index.html b/frontend/index.html
index 1050183..ad7ed40 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -39,24 +39,6 @@
-
-
Set Where
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/frontend/src/main.js b/frontend/src/main.js
index 38d0bb8..8968f03 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -7,7 +7,6 @@ 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,
@@ -32,13 +31,6 @@ const sortInput = $('#sort-columns-input');
const sortAutocomplete = $('#sort-autocomplete');
const sortConfirmBtn = $('#sort-confirm-btn');
const sortCancelBtn = $('#sort-cancel-btn');
-const swModal = $('#set-where-modal');
-const swMatchCol = $('#sw-match-col');
-const swColAutocomplete = $('#sw-col-autocomplete');
-const swMatchValues = $('#sw-match-values');
-const swSetValue = $('#sw-set-value');
-const swConfirmBtn = $('#sw-confirm-btn');
-const swCancelBtn = $('#sw-cancel-btn');
// Default column width
const DEFAULT_COL_WIDTH = 120;
@@ -61,10 +53,6 @@ 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;
@@ -77,7 +65,6 @@ 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;
@@ -358,8 +345,6 @@ tableBody.addEventListener('mousedown', (e) => {
commitFormulaBar();
}
- clearMatchSelection();
-
if (e.shiftKey) {
// Extend selection
state.selection = {
@@ -399,7 +384,6 @@ document.addEventListener('mouseup', () => {
// ===== Keyboard navigation =====
function moveCursor(dr, dc, shift) {
- clearMatchSelection();
if (shift) {
if (!state.selection) {
state.selection = {
@@ -486,11 +470,6 @@ 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; }
@@ -529,7 +508,6 @@ document.addEventListener('keydown', (e) => {
// Escape clears selection
if (e.key === 'Escape') {
state.selection = null;
- clearMatchSelection();
updateSelectionClasses();
return;
}
@@ -578,29 +556,6 @@ 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 {
@@ -629,14 +584,6 @@ 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, '');
@@ -675,8 +622,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-cell', Name: 'Match Cell', Shortcut: 'Cmd+M' },
- { ID: 'set-where', Name: 'Set Where', Shortcut: '' },
+ { ID: 'match-row', Name: 'Match Row', Shortcut: '' },
{ ID: 'delete-row', Name: 'Delete Row', Shortcut: 'Cmd+Backspace' },
];
}
@@ -763,7 +709,6 @@ commandInput.addEventListener('keydown', (e) => {
overlay.addEventListener('click', () => {
if (!commandPalette.classList.contains('hidden')) closeCommandPalette();
if (!sortModal.classList.contains('hidden')) closeSortAdvanced();
- if (!swModal.classList.contains('hidden')) closeSetWhere();
});
// ===== Command execution =====
@@ -784,8 +729,7 @@ async function executeCommand(id) {
case 'sort-asc': doSort(true); break;
case 'sort-desc': doSort(false); break;
case 'sort-advanced': openSortAdvanced(); break;
- case 'match-cell': doMatchCell(); break;
- case 'set-where': openSetWhere(); break;
+ case 'match-row': doMatchRow(); break;
case 'delete-row': doDeleteRow(); break;
}
}
@@ -1001,24 +945,33 @@ function doInsertColRight() {
}
// ===== Match & Delete Row =====
-function doMatchCell() {
- const val = getCellValue(state.cursor.row, state.cursor.col);
- const matched = new Set();
+function doMatchRow() {
+ const col = state.cursor.col;
+ const val = getCellValue(state.cursor.row, col);
+ const matchingRows = [];
for (let r = 0; r < state.rows.length; r++) {
- for (let c = 0; c < state.headers.length; c++) {
- if ((state.rows[r][c] || '') === val) {
- matched.add(`${r},${c}`);
- }
+ if ((state.rows[r][col] || '') === val) {
+ matchingRows.push(r);
}
}
- if (matched.size === 0) {
- setStatus('No matching cells');
+ if (matchingRows.length === 0) {
+ setStatus('No matching rows');
return;
}
- state.selection = null;
- state.selectedCells = matched;
+ // 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,
+ };
updateSelectionClasses();
- setStatus(`${matched.size} cell(s) matching "${val}"`);
+ scrollCursorIntoView();
+ const colName = state.headers[col] || colLabel(col);
+ setStatus(`${matchingRows.length} row(s) matching ${colName} = "${val}"`);
}
function doDeleteRow() {
@@ -1245,209 +1198,6 @@ sortInput.addEventListener('keydown', (e) => {
sortConfirmBtn.addEventListener('click', confirmSortAdvanced);
sortCancelBtn.addEventListener('click', closeSortAdvanced);
-// ===== Set Where Modal =====
-let swAcIndex = -1;
-
-function openSetWhere() {
- swModal.classList.remove('hidden');
- overlay.classList.remove('hidden');
- swMatchCol.value = '';
- swMatchValues.value = '';
- swSetValue.value = '';
- hideSwAutocomplete();
- swMatchCol.focus();
-}
-
-function closeSetWhere() {
- swModal.classList.add('hidden');
- overlay.classList.add('hidden');
- swMatchCol.value = '';
- swMatchValues.value = '';
- swSetValue.value = '';
- hideSwAutocomplete();
-}
-
-function hideSwAutocomplete() {
- swColAutocomplete.classList.add('hidden');
- swColAutocomplete.innerHTML = '';
- swAcIndex = -1;
-}
-
-function updateSwAutocomplete() {
- const text = swMatchCol.value.trim().toLowerCase();
- const matches = state.headers
- .filter(h => h.toLowerCase().includes(text))
- .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
-
- if (matches.length === 0 || (matches.length === 1 && matches[0].toLowerCase() === text)) {
- hideSwAutocomplete();
- return;
- }
-
- swColAutocomplete.innerHTML = '';
- swColAutocomplete.classList.remove('hidden');
- swAcIndex = -1;
-
- matches.forEach((name) => {
- const div = document.createElement('div');
- div.className = 'ac-item';
- div.textContent = name;
- div.addEventListener('mousedown', (e) => {
- e.preventDefault();
- swMatchCol.value = name;
- hideSwAutocomplete();
- swMatchValues.focus();
- });
- swColAutocomplete.appendChild(div);
- });
-}
-
-function confirmSetWhere() {
- const colName = swMatchCol.value.trim();
- const matchColIdx = state.headers.findIndex(
- h => h.toLowerCase() === colName.toLowerCase()
- );
- if (matchColIdx === -1) {
- setStatus(`Unknown column: "${colName}"`);
- return;
- }
-
- const matchVals = new Set(
- swMatchValues.value.split('\n').map(s => s.trim()).filter(s => s.length > 0)
- );
- if (matchVals.size === 0) {
- setStatus('No match values specified');
- return;
- }
-
- const setValue = swSetValue.value;
- const targetCol = state.cursor.col;
- let count = 0;
-
- for (let r = 0; r < state.rows.length; r++) {
- const cellVal = state.rows[r][matchColIdx] || '';
- if (matchVals.has(cellVal)) {
- setCellValue(r, targetCol, setValue);
- count++;
- }
- }
-
- closeSetWhere();
- render();
- const targetColName = state.headers[targetCol] || colLabel(targetCol);
- setStatus(`Set ${count} cell(s) in ${targetColName}`);
-}
-
-swMatchCol.addEventListener('input', updateSwAutocomplete);
-swMatchCol.addEventListener('focus', updateSwAutocomplete);
-swMatchCol.addEventListener('blur', () => {
- setTimeout(hideSwAutocomplete, 150);
-});
-
-swMatchCol.addEventListener('keydown', (e) => {
- const items = swColAutocomplete.querySelectorAll('.ac-item');
-
- if (e.key === 'Escape') {
- e.preventDefault();
- e.stopPropagation();
- if (!swColAutocomplete.classList.contains('hidden')) {
- hideSwAutocomplete();
- } else {
- closeSetWhere();
- }
- return;
- }
-
- if (e.key === 'Enter') {
- e.preventDefault();
- e.stopPropagation();
- if (swAcIndex >= 0 && items[swAcIndex]) {
- swMatchCol.value = items[swAcIndex].textContent;
- hideSwAutocomplete();
- swMatchValues.focus();
- } else {
- // Move focus to match values
- swMatchValues.focus();
- }
- return;
- }
-
- if (e.key === 'ArrowDown' && items.length > 0) {
- e.preventDefault();
- if (swAcIndex >= 0) items[swAcIndex]?.classList.remove('active');
- swAcIndex = (swAcIndex + 1) % items.length;
- items[swAcIndex]?.classList.add('active');
- items[swAcIndex]?.scrollIntoView({ block: 'nearest' });
- return;
- }
-
- if (e.key === 'ArrowUp' && items.length > 0) {
- e.preventDefault();
- if (swAcIndex >= 0) items[swAcIndex]?.classList.remove('active');
- swAcIndex = (swAcIndex - 1 + items.length) % items.length;
- items[swAcIndex]?.classList.add('active');
- items[swAcIndex]?.scrollIntoView({ block: 'nearest' });
- return;
- }
-
- if (e.key === 'Tab') {
- e.preventDefault();
- if (swAcIndex >= 0 && items[swAcIndex]) {
- swMatchCol.value = items[swAcIndex].textContent;
- hideSwAutocomplete();
- }
- swMatchValues.focus();
- return;
- }
-
- e.stopPropagation();
-});
-
-swMatchValues.addEventListener('keydown', (e) => {
- if (e.key === 'Escape') {
- e.preventDefault();
- e.stopPropagation();
- closeSetWhere();
- return;
- }
- // Tab to next field
- if (e.key === 'Tab' && !e.shiftKey) {
- e.preventDefault();
- swSetValue.focus();
- return;
- }
- if (e.key === 'Tab' && e.shiftKey) {
- e.preventDefault();
- swMatchCol.focus();
- return;
- }
- e.stopPropagation();
-});
-
-swSetValue.addEventListener('keydown', (e) => {
- if (e.key === 'Escape') {
- e.preventDefault();
- e.stopPropagation();
- closeSetWhere();
- return;
- }
- if (e.key === 'Enter') {
- e.preventDefault();
- e.stopPropagation();
- confirmSetWhere();
- return;
- }
- if (e.key === 'Tab' && e.shiftKey) {
- e.preventDefault();
- swMatchValues.focus();
- return;
- }
- e.stopPropagation();
-});
-
-swConfirmBtn.addEventListener('click', confirmSetWhere);
-swCancelBtn.addEventListener('click', closeSetWhere);
-
// ===== File loading =====
async function loadFile(filePath) {
try {
diff --git a/frontend/src/style.css b/frontend/src/style.css
index 1e4eeda..ea72bad 100644
--- a/frontend/src/style.css
+++ b/frontend/src/style.css
@@ -391,97 +391,6 @@ td.cursor-cell {
background: #1a54a9;
}
-/* Set Where modal */
-#set-where-modal {
- position: fixed;
- top: 80px;
- left: 50%;
- transform: translateX(-50%);
- width: 460px;
- background: #fff;
- border: 1px solid var(--border);
- border-radius: 8px;
- box-shadow: 0 8px 32px rgba(0,0,0,0.2);
- z-index: 100;
- overflow: visible;
-}
-
-#set-where-modal.hidden {
- display: none;
-}
-
-.sw-input-wrap {
- position: relative;
-}
-
-#sw-match-col {
- width: 100%;
- padding: 8px 10px;
- font-size: 14px;
- font-family: inherit;
- border: 1px solid var(--border);
- border-radius: 4px;
- outline: none;
- box-sizing: border-box;
-}
-
-#sw-match-col:focus {
- border-color: var(--selected-border);
- box-shadow: 0 0 0 2px rgba(34,102,204,0.15);
-}
-
-#sw-col-autocomplete {
- position: absolute;
- left: 0;
- right: 0;
- top: 100%;
- margin-top: 2px;
- background: #fff;
- border: 1px solid var(--border);
- border-radius: 4px;
- box-shadow: 0 4px 12px rgba(0,0,0,0.12);
- max-height: 180px;
- overflow-y: auto;
- z-index: 110;
-}
-
-#sw-col-autocomplete.hidden {
- display: none;
-}
-
-#sw-match-values {
- width: 100%;
- padding: 8px 10px;
- font-size: 14px;
- font-family: inherit;
- border: 1px solid var(--border);
- border-radius: 4px;
- outline: none;
- resize: vertical;
- box-sizing: border-box;
-}
-
-#sw-match-values:focus {
- border-color: var(--selected-border);
- box-shadow: 0 0 0 2px rgba(34,102,204,0.15);
-}
-
-#sw-set-value {
- width: 100%;
- padding: 8px 10px;
- font-size: 14px;
- font-family: inherit;
- border: 1px solid var(--border);
- border-radius: 4px;
- outline: none;
- box-sizing: border-box;
-}
-
-#sw-set-value:focus {
- border-color: var(--selected-border);
- box-shadow: 0 0 0 2px rgba(34,102,204,0.15);
-}
-
/* Drag over style */
#table-container.drag-over {
background: var(--selected-bg);