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 @@ - 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);