diff --git a/app_test.go b/app_test.go
index d566601..0781425 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) != 17 {
- t.Errorf("expected 12 commands, got %d", len(cmds))
+ if len(cmds) != 18 {
+ t.Errorf("expected 18 commands, got %d", len(cmds))
}
// Check that all have IDs
for _, cmd := range cmds {
diff --git a/commands.go b/commands.go
index 806a458..d79dbf2 100644
--- a/commands.go
+++ b/commands.go
@@ -34,6 +34,7 @@ func (c *CommandRegistry) GetCommands() []Command {
{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: "delete-row", Name: "Delete Row", Shortcut: "Cmd+Backspace"},
}
}
diff --git a/frontend/index.html b/frontend/index.html
index ad7ed40..1050183 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -39,6 +39,24 @@
+
+
Set Where
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/main.js b/frontend/src/main.js
index 8e3e3b1..38d0bb8 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -32,6 +32,13 @@ 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;
@@ -669,6 +676,7 @@ async function loadCommands() {
{ 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: 'delete-row', Name: 'Delete Row', Shortcut: 'Cmd+Backspace' },
];
}
@@ -755,6 +763,7 @@ 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 =====
@@ -776,6 +785,7 @@ async function executeCommand(id) {
case 'sort-desc': doSort(false); break;
case 'sort-advanced': openSortAdvanced(); break;
case 'match-cell': doMatchCell(); break;
+ case 'set-where': openSetWhere(); break;
case 'delete-row': doDeleteRow(); break;
}
}
@@ -1235,6 +1245,209 @@ 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 ea72bad..1e4eeda 100644
--- a/frontend/src/style.css
+++ b/frontend/src/style.css
@@ -391,6 +391,97 @@ 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);