import './style.css'; // ===== State ===== const state = { headers: [], rows: [], filePath: '', cursor: { row: 0, col: 0 }, selection: null, // { startRow, startCol, endRow, endCol } colWidths: [], // pixel widths per column isSelecting: false, editingHeader: -1, formulaBarFocused: false, formulaBarFromTable: false, }; // ===== DOM refs ===== const $ = (sel) => document.querySelector(sel); const tableHead = $('#table-head'); const tableBody = $('#table-body'); const formulaBar = $('#formula-bar'); const cellRef = $('#cell-ref'); const statusText = $('#status-text'); const commandPalette = $('#command-palette'); const commandInput = $('#command-input'); const commandList = $('#command-list'); const overlay = $('#overlay'); const tableContainer = $('#table-container'); const sortModal = $('#sort-modal'); const sortInput = $('#sort-columns-input'); const sortAutocomplete = $('#sort-autocomplete'); const sortConfirmBtn = $('#sort-confirm-btn'); const sortCancelBtn = $('#sort-cancel-btn'); // Default column width const DEFAULT_COL_WIDTH = 120; const MIN_COL_WIDTH = 40; const MAX_BEST_FIT = 150; // ===== Helpers ===== function colLabel(i) { // A, B, C ... Z, AA, AB ... let s = ''; let n = i; do { s = String.fromCharCode(65 + (n % 26)) + s; n = Math.floor(n / 26) - 1; } while (n >= 0); return s; } function cellRefStr(r, c) { return colLabel(c) + (r + 1); } function normalizeSelection() { if (!state.selection) return null; const { startRow, startCol, endRow, endCol } = state.selection; return { r1: Math.min(startRow, endRow), c1: Math.min(startCol, endCol), r2: Math.max(startRow, endRow), c2: Math.max(startCol, endCol), }; } function isCellSelected(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; } function getCellValue(r, c) { if (r < 0 || r >= state.rows.length) return ''; if (c < 0 || !state.rows[r] || c >= state.rows[r].length) return ''; return state.rows[r][c] || ''; } function setCellValue(r, c, val) { // Ensure row exists while (state.rows.length <= r) state.rows.push([]); // Ensure columns exist while (state.rows[r].length <= c) state.rows[r].push(''); state.rows[r][c] = val; } function ensureGridSize() { const numCols = state.headers.length; for (let i = 0; i < state.rows.length; i++) { while (state.rows[i].length < numCols) state.rows[i].push(''); } } function setStatus(msg) { statusText.textContent = msg; } // ===== Rendering ===== function render() { renderHead(); renderBody(); updateFormulaBar(); updateCellRef(); } function renderHead() { tableHead.innerHTML = ''; const tr = document.createElement('tr'); // Row number header const th0 = document.createElement('th'); th0.className = 'row-number-header'; th0.textContent = ''; tr.appendChild(th0); state.headers.forEach((h, i) => { const th = document.createElement('th'); th.style.width = (state.colWidths[i] || DEFAULT_COL_WIDTH) + 'px'; th.style.position = 'relative'; th.dataset.col = i; const content = document.createElement('div'); content.className = 'header-content'; const textSpan = document.createElement('span'); textSpan.className = 'header-text'; textSpan.textContent = h; content.appendChild(textSpan); th.appendChild(content); // Resize handle const handle = document.createElement('div'); handle.className = 'col-resize-handle'; handle.addEventListener('mousedown', (e) => startColResize(e, i)); handle.addEventListener('dblclick', (e) => { e.stopPropagation(); bestFitColumn(i); }); th.appendChild(handle); // Double click to edit header th.addEventListener('dblclick', (e) => { if (e.target.classList.contains('col-resize-handle')) return; startHeaderEdit(i); }); tr.appendChild(th); }); tableHead.appendChild(tr); } function renderBody() { tableBody.innerHTML = ''; const numCols = state.headers.length; state.rows.forEach((row, r) => { const tr = document.createElement('tr'); // Row number const tdNum = document.createElement('td'); tdNum.className = 'row-number'; tdNum.textContent = r + 1; tr.appendChild(tdNum); for (let c = 0; c < numCols; c++) { const td = document.createElement('td'); td.style.width = (state.colWidths[c] || DEFAULT_COL_WIDTH) + 'px'; td.textContent = row[c] || ''; td.dataset.row = r; td.dataset.col = c; if (isCellSelected(r, c)) { td.classList.add('selected'); } if (r === state.cursor.row && c === state.cursor.col) { td.classList.add('cursor-cell'); } tr.appendChild(td); } tableBody.appendChild(tr); }); } function updateSelectionClasses() { const tds = tableBody.querySelectorAll('td[data-row]'); tds.forEach(td => { const r = parseInt(td.dataset.row); const c = parseInt(td.dataset.col); td.classList.toggle('selected', isCellSelected(r, c)); td.classList.toggle('cursor-cell', r === state.cursor.row && c === state.cursor.col); }); updateFormulaBar(); updateCellRef(); } function updateFormulaBar() { if (!state.formulaBarFocused) { formulaBar.value = getCellValue(state.cursor.row, state.cursor.col); } } function updateCellRef() { if (state.headers.length > 0 && state.rows.length > 0) { cellRef.textContent = cellRefStr(state.cursor.row, state.cursor.col); } else { cellRef.textContent = ''; } } function scrollCursorIntoView() { const td = tableBody.querySelector( `td[data-row="${state.cursor.row}"][data-col="${state.cursor.col}"]` ); if (td) { td.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } } // ===== Header editing ===== function startHeaderEdit(colIndex) { state.editingHeader = colIndex; const ths = tableHead.querySelectorAll('th'); const th = ths[colIndex + 1]; // +1 for row number header if (!th) return; const content = th.querySelector('.header-content'); content.innerHTML = ''; const input = document.createElement('input'); input.type = 'text'; input.className = 'header-edit-input'; input.value = state.headers[colIndex]; content.appendChild(input); input.focus(); input.select(); const finish = () => { state.headers[colIndex] = input.value; state.editingHeader = -1; renderHead(); }; input.addEventListener('blur', finish); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { input.blur(); } if (e.key === 'Escape') { input.value = state.headers[colIndex]; // revert input.blur(); } e.stopPropagation(); }); } // ===== Column resizing ===== let resizeState = null; function startColResize(e, colIndex) { e.preventDefault(); e.stopPropagation(); const startX = e.clientX; const startWidth = state.colWidths[colIndex] || DEFAULT_COL_WIDTH; resizeState = { colIndex, startX, startWidth }; const handle = e.target; handle.classList.add('active'); const onMove = (ev) => { const diff = ev.clientX - startX; const newWidth = Math.max(MIN_COL_WIDTH, startWidth + diff); state.colWidths[colIndex] = newWidth; // Update widths live const ths = tableHead.querySelectorAll('th'); if (ths[colIndex + 1]) ths[colIndex + 1].style.width = newWidth + 'px'; const allTds = tableBody.querySelectorAll(`td[data-col="${colIndex}"]`); allTds.forEach(td => td.style.width = newWidth + 'px'); }; const onUp = () => { handle.classList.remove('active'); document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); resizeState = null; }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); } function bestFitColumn(colIndex) { // Measure the widest content in this column const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); ctx.font = '13px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; let maxW = ctx.measureText(state.headers[colIndex] || '').width; for (const row of state.rows) { const val = row[colIndex] || ''; const w = ctx.measureText(val).width; if (w > maxW) maxW = w; } // Add padding const fitted = Math.ceil(maxW) + 24; // 12px padding each side const width = Math.max(fitted, MAX_BEST_FIT); state.colWidths[colIndex] = width; render(); } function bestFitAllColumns() { for (let i = 0; i < state.headers.length; i++) { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); ctx.font = '13px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; let maxW = ctx.measureText(state.headers[i] || '').width; for (const row of state.rows) { const w = ctx.measureText(row[i] || '').width; if (w > maxW) maxW = w; } const fitted = Math.ceil(maxW) + 24; state.colWidths[i] = Math.max(fitted, MAX_BEST_FIT); } render(); } // ===== Cell click & drag selection ===== tableBody.addEventListener('mousedown', (e) => { const td = e.target.closest('td[data-row]'); if (!td) return; const r = parseInt(td.dataset.row); const c = parseInt(td.dataset.col); // Commit formula bar if it was focused if (state.formulaBarFocused) { commitFormulaBar(); } if (e.shiftKey) { // Extend selection state.selection = { startRow: state.cursor.row, startCol: state.cursor.col, endRow: r, endCol: c }; } else { state.cursor = { row: r, col: c }; state.selection = { startRow: r, startCol: c, endRow: r, endCol: c }; } state.isSelecting = true; updateSelectionClasses(); }); document.addEventListener('mousemove', (e) => { if (!state.isSelecting) return; const td = e.target.closest('td[data-row]'); if (!td) return; const r = parseInt(td.dataset.row); const c = parseInt(td.dataset.col); if (state.selection) { state.selection.endRow = r; state.selection.endCol = c; } updateSelectionClasses(); }); document.addEventListener('mouseup', () => { state.isSelecting = false; }); // ===== Keyboard navigation ===== function moveCursor(dr, dc, shift) { if (shift) { if (!state.selection) { state.selection = { startRow: state.cursor.row, startCol: state.cursor.col, endRow: state.cursor.row, endCol: state.cursor.col }; } const nr = Math.max(0, Math.min(state.rows.length - 1, state.selection.endRow + dr)); const nc = Math.max(0, Math.min(state.headers.length - 1, state.selection.endCol + dc)); state.selection.endRow = nr; state.selection.endCol = nc; } else { const nr = Math.max(0, Math.min(state.rows.length - 1, state.cursor.row + dr)); const nc = Math.max(0, Math.min(state.headers.length - 1, state.cursor.col + dc)); state.cursor = { row: nr, col: nc }; state.selection = null; } updateSelectionClasses(); scrollCursorIntoView(); } document.addEventListener('keydown', (e) => { // Command palette if ((e.metaKey || e.ctrlKey) && e.key === 'p') { e.preventDefault(); openCommandPalette(); return; } // If command palette is open, don't handle other keys if (!commandPalette.classList.contains('hidden')) return; // If editing a header, don't interfere if (state.editingHeader >= 0) return; // If formula bar is focused, handle Enter/Escape if (state.formulaBarFocused) { if (e.key === 'Enter') { e.preventDefault(); commitFormulaBar(); if (state.formulaBarFromTable) { formulaBar.blur(); tableContainer.focus(); } return; } if (e.key === 'Escape') { e.preventDefault(); cancelFormulaBar(); formulaBar.blur(); tableContainer.focus(); return; } return; // Let normal typing happen in formula bar } // Shortcuts const meta = e.metaKey || e.ctrlKey; if (meta && e.key === 's') { e.preventDefault(); executeCommand('save'); return; } if (meta && e.key === 'c') { e.preventDefault(); executeCommand('copy'); return; } if (meta && e.key === 'x') { e.preventDefault(); executeCommand('cut'); return; } if (meta && e.key === 'v') { e.preventDefault(); executeCommand('paste'); return; } if (meta && e.key === 'o') { e.preventDefault(); executeCommand('open'); return; } // Arrow keys if (e.key === 'ArrowUp') { e.preventDefault(); moveCursor(-1, 0, e.shiftKey); return; } if (e.key === 'ArrowDown') { e.preventDefault(); moveCursor(1, 0, e.shiftKey); return; } if (e.key === 'ArrowLeft') { e.preventDefault(); moveCursor(0, -1, e.shiftKey); return; } if (e.key === 'ArrowRight') { e.preventDefault(); moveCursor(0, 1, e.shiftKey); return; } // Tab if (e.key === 'Tab') { e.preventDefault(); moveCursor(0, e.shiftKey ? -1 : 1, false); return; } // Enter moves down if (e.key === 'Enter') { e.preventDefault(); moveCursor(1, 0, false); return; } // Cmd+Backspace deletes rows if (meta && e.key === 'Backspace') { e.preventDefault(); executeCommand('delete-row'); return; } // Delete/Backspace clears selected cells if (e.key === 'Delete' || e.key === 'Backspace') { e.preventDefault(); clearSelectedCells(); return; } // Escape clears selection if (e.key === 'Escape') { state.selection = null; updateSelectionClasses(); return; } // Typing a printable character: focus formula bar and start editing if (e.key.length === 1 && !meta && !e.altKey) { e.preventDefault(); formulaBar.value = ''; formulaBar.focus(); state.formulaBarFocused = true; state.formulaBarFromTable = true; // Insert the typed character formulaBar.value = e.key; // Move cursor to end formulaBar.setSelectionRange(formulaBar.value.length, formulaBar.value.length); return; } }); // ===== Formula bar ===== formulaBar.addEventListener('focus', () => { state.formulaBarFocused = true; // If not triggered by keyboard, this was a direct click if (!state.formulaBarFromTable) { formulaBar.value = getCellValue(state.cursor.row, state.cursor.col); } }); formulaBar.addEventListener('blur', () => { state.formulaBarFocused = false; state.formulaBarFromTable = false; }); function commitFormulaBar() { setCellValue(state.cursor.row, state.cursor.col, formulaBar.value); state.formulaBarFocused = false; state.formulaBarFromTable = false; render(); } function cancelFormulaBar() { formulaBar.value = getCellValue(state.cursor.row, state.cursor.col); state.formulaBarFocused = false; state.formulaBarFromTable = false; } // ===== Selection data extraction ===== function getSelectedData() { const sel = normalizeSelection(); if (!sel) { return { headers: [state.headers[state.cursor.col]], rows: [[getCellValue(state.cursor.row, state.cursor.col)]], isSingleCol: true }; } const headers = []; for (let c = sel.c1; c <= sel.c2; c++) { headers.push(state.headers[c] || ''); } const rows = []; for (let r = sel.r1; r <= sel.r2; r++) { const row = []; for (let c = sel.c1; c <= sel.c2; c++) { row.push(getCellValue(r, c)); } rows.push(row); } return { headers, rows, isSingleCol: sel.c1 === sel.c2 }; } function clearSelectedCells() { const sel = normalizeSelection(); if (!sel) { setCellValue(state.cursor.row, state.cursor.col, ''); } else { for (let r = sel.r1; r <= sel.r2; r++) { for (let c = sel.c1; c <= sel.c2; c++) { setCellValue(r, c, ''); } } } render(); } // ===== Commands ===== let commands = []; async function loadCommands() { try { commands = await window.go.main.CommandRegistry.GetCommands(); } catch (e) { console.error('Failed to load commands:', e); // Fallback commands commands = [ { ID: 'copy', Name: 'Copy', Shortcut: 'Cmd+C' }, { ID: 'cut', Name: 'Cut', Shortcut: 'Cmd+X' }, { ID: 'copy-markdown', Name: 'Copy as Markdown', Shortcut: '' }, { ID: 'copy-jira', Name: 'Copy as Jira', Shortcut: '' }, { ID: 'paste', Name: 'Paste', Shortcut: 'Cmd+V' }, { ID: 'resize-all', Name: 'Resize All Columns', Shortcut: '' }, { ID: 'open', Name: 'Open File', Shortcut: 'Cmd+O' }, { ID: 'save', Name: 'Save File', Shortcut: 'Cmd+S' }, { ID: 'open-up', Name: 'Insert Row Above', Shortcut: '' }, { ID: 'open-down', Name: 'Insert Row Below', Shortcut: '' }, { ID: 'open-left', Name: 'Insert Column Left', Shortcut: '' }, { ID: 'open-right', Name: 'Insert Column Right', Shortcut: '' }, { 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: 'delete-row', Name: 'Delete Row', Shortcut: 'Cmd+Backspace' }, ]; } } function openCommandPalette() { commandPalette.classList.remove('hidden'); overlay.classList.remove('hidden'); commandInput.value = ''; renderCommandList(''); commandInput.focus(); } function closeCommandPalette() { commandPalette.classList.add('hidden'); overlay.classList.add('hidden'); commandInput.value = ''; } let activeCommandIndex = 0; function renderCommandList(filter) { commandList.innerHTML = ''; const lower = filter.toLowerCase(); const filtered = commands .filter(c => c.Name.toLowerCase().includes(lower)) .sort((a, b) => a.Name.toLowerCase().localeCompare(b.Name.toLowerCase())); activeCommandIndex = 0; filtered.forEach((cmd, i) => { const div = document.createElement('div'); div.className = 'command-item' + (i === 0 ? ' active' : ''); div.innerHTML = `${cmd.Name}` + (cmd.Shortcut ? `${cmd.Shortcut}` : ''); div.addEventListener('click', () => { closeCommandPalette(); executeCommand(cmd.ID); }); commandList.appendChild(div); }); } commandInput.addEventListener('input', () => { renderCommandList(commandInput.value); }); commandInput.addEventListener('keydown', (e) => { const items = commandList.querySelectorAll('.command-item'); if (e.key === 'Escape') { e.preventDefault(); closeCommandPalette(); return; } if (e.key === 'ArrowDown') { e.preventDefault(); if (items.length === 0) return; items[activeCommandIndex]?.classList.remove('active'); activeCommandIndex = (activeCommandIndex + 1) % items.length; items[activeCommandIndex]?.classList.add('active'); items[activeCommandIndex]?.scrollIntoView({ block: 'nearest' }); return; } if (e.key === 'ArrowUp') { e.preventDefault(); if (items.length === 0) return; items[activeCommandIndex]?.classList.remove('active'); activeCommandIndex = (activeCommandIndex - 1 + items.length) % items.length; items[activeCommandIndex]?.classList.add('active'); items[activeCommandIndex]?.scrollIntoView({ block: 'nearest' }); return; } if (e.key === 'Enter') { e.preventDefault(); const items = commandList.querySelectorAll('.command-item'); if (items[activeCommandIndex]) { items[activeCommandIndex].click(); } return; } e.stopPropagation(); }); overlay.addEventListener('click', () => { if (!commandPalette.classList.contains('hidden')) closeCommandPalette(); if (!sortModal.classList.contains('hidden')) closeSortAdvanced(); }); // ===== Command execution ===== async function executeCommand(id) { switch (id) { case 'copy': await doCopy(); break; case 'cut': await doCut(); break; case 'copy-markdown': await doCopyMarkdown(); break; case 'copy-jira': await doCopyJira(); break; case 'paste': await doPaste(); break; case 'resize-all': bestFitAllColumns(); setStatus('All columns resized'); break; case 'open': await doOpen(); break; case 'save': await doSave(); break; case 'open-up': doInsertRowAbove(); break; case 'open-down': doInsertRowBelow(); break; case 'open-left': doInsertColLeft(); break; case 'open-right': doInsertColRight(); break; case 'sort-asc': doSort(true); break; case 'sort-desc': doSort(false); break; case 'sort-advanced': openSortAdvanced(); break; case 'match-row': doMatchRow(); break; case 'delete-row': doDeleteRow(); break; } } async function doCopy() { const data = getSelectedData(); let text; if (data.isSingleCol) { try { text = await window.go.main.App.FormatAsSingleColumn(data.rows); } catch { text = data.rows.map(r => r[0]).join('\n'); } } else { try { text = await window.go.main.App.FormatRowsAsCSV(data.rows); } catch { text = data.rows.map(r => r.join(',')).join('\n'); } } await navigator.clipboard.writeText(text); setStatus('Copied to clipboard'); } async function doCut() { await doCopy(); clearSelectedCells(); setStatus('Cut to clipboard'); } async function doCopyMarkdown() { const data = getSelectedData(); let text; try { text = await window.go.main.App.FormatAsMarkdown(data.headers, data.rows); } catch { text = '(Markdown format unavailable)'; } await navigator.clipboard.writeText(text); setStatus('Copied as Markdown'); } async function doCopyJira() { const data = getSelectedData(); let text; try { text = await window.go.main.App.FormatAsJira(data.headers, data.rows); } catch { text = '(Jira format unavailable)'; } await navigator.clipboard.writeText(text); setStatus('Copied as Jira'); } async function doPaste() { let text; try { text = await navigator.clipboard.readText(); } catch { setStatus('Failed to read clipboard'); return; } if (!text || !text.trim()) { setStatus('Clipboard is empty'); return; } const lines = text.trim().split('\n'); // Check if it looks like CSV: multi-line, consistent comma-separated columns let isCSV = false; if (lines.length > 1) { // Count commas outside quotes for each line const colCounts = lines.map(line => { let count = 1; let inQuote = false; for (const ch of line) { if (ch === '"') inQuote = !inQuote; else if (ch === ',' && !inQuote) count++; } return count; }); // Check if all lines have the same column count and > 1 const first = colCounts[0]; if (first > 1 && colCounts.every(c => c === first)) { // Also check no spaces before commas (heuristic for CSV-like) isCSV = true; } } let pasteRows; if (isCSV) { try { const parsed = await window.go.main.App.ParseCSVString(text); // Combine headers and rows since we're pasting into cells pasteRows = [parsed.Headers, ...parsed.Rows]; } catch { // Fallback: split by comma pasteRows = lines.map(l => l.split(',')); } } else { // Each line is a single-column row pasteRows = lines.map(l => [l]); } // Place at cursor position const startRow = state.cursor.row; const startCol = state.cursor.col; for (let r = 0; r < pasteRows.length; r++) { for (let c = 0; c < pasteRows[r].length; c++) { const targetRow = startRow + r; const targetCol = startCol + c; // Extend grid if needed while (state.rows.length <= targetRow) state.rows.push(new Array(state.headers.length).fill('')); while (state.headers.length <= targetCol) { state.headers.push(colLabel(state.headers.length)); state.colWidths.push(DEFAULT_COL_WIDTH); for (const row of state.rows) row.push(''); } setCellValue(targetRow, targetCol, pasteRows[r][c]); } } render(); setStatus(`Pasted ${pasteRows.length} rows`); } async function doOpen() { let filePath; try { filePath = await window.go.main.App.OpenFileDialog(); } catch (e) { setStatus('Open cancelled'); return; } if (!filePath) { setStatus('Open cancelled'); return; } await loadFile(filePath); } async function doSave() { if (state.filePath) { try { await window.go.main.App.SaveCurrentFile(state.headers, state.rows); setStatus('Saved: ' + state.filePath); } catch (e) { setStatus('Error saving: ' + e); } } else { let filePath; try { filePath = await window.go.main.App.SaveFileDialog(); } catch { setStatus('Save cancelled'); return; } if (!filePath) { setStatus('Save cancelled'); return; } try { await window.go.main.App.SaveCSV(filePath, state.headers, state.rows); state.filePath = filePath; setStatus('Saved: ' + filePath); } catch (e) { setStatus('Error saving: ' + e); } } } function doInsertRowAbove() { const r = state.cursor.row; const newRow = new Array(state.headers.length).fill(''); state.rows.splice(r, 0, newRow); render(); setStatus('Inserted row above'); } function doInsertRowBelow() { const r = state.cursor.row; const newRow = new Array(state.headers.length).fill(''); state.rows.splice(r + 1, 0, newRow); state.cursor.row = r + 1; render(); setStatus('Inserted row below'); } function doInsertColLeft() { const c = state.cursor.col; state.headers.splice(c, 0, colLabel(state.headers.length)); state.colWidths.splice(c, 0, DEFAULT_COL_WIDTH); for (const row of state.rows) { row.splice(c, 0, ''); } render(); setStatus('Inserted column left'); } function doInsertColRight() { const c = state.cursor.col + 1; state.headers.splice(c, 0, colLabel(state.headers.length)); state.colWidths.splice(c, 0, DEFAULT_COL_WIDTH); for (const row of state.rows) { row.splice(c, 0, ''); } state.cursor.col = c; render(); setStatus('Inserted column right'); } // ===== Match & Delete Row ===== 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++) { if ((state.rows[r][col] || '') === val) { matchingRows.push(r); } } if (matchingRows.length === 0) { setStatus('No matching rows'); 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, }; updateSelectionClasses(); scrollCursorIntoView(); const colName = state.headers[col] || colLabel(col); setStatus(`${matchingRows.length} row(s) matching ${colName} = "${val}"`); } function doDeleteRow() { const sel = normalizeSelection(); let startRow, endRow; if (sel) { startRow = sel.r1; endRow = sel.r2; } else { startRow = state.cursor.row; endRow = state.cursor.row; } const count = endRow - startRow + 1; state.rows.splice(startRow, count); // Ensure at least one row remains if (state.rows.length === 0) { state.rows.push(new Array(state.headers.length).fill('')); } // Adjust cursor state.cursor.row = Math.min(startRow, state.rows.length - 1); state.selection = null; render(); setStatus(`Deleted ${count} row(s)`); } // ===== Sorting ===== function doSort(ascending) { const col = state.cursor.col; state.rows.sort((a, b) => { const va = (a[col] || '').toLowerCase(); const vb = (b[col] || '').toLowerCase(); return ascending ? va.localeCompare(vb, undefined, { numeric: true }) : vb.localeCompare(va, undefined, { numeric: true }); }); render(); setStatus(`Sorted by ${state.headers[col] || colLabel(col)} ${ascending ? 'A-Z' : 'Z-A'}`); } // ===== Sort Advanced Modal ===== let acIndex = -1; function openSortAdvanced() { sortModal.classList.remove('hidden'); overlay.classList.remove('hidden'); sortInput.value = ''; hideSortAutocomplete(); sortInput.focus(); } function closeSortAdvanced() { sortModal.classList.add('hidden'); overlay.classList.add('hidden'); sortInput.value = ''; hideSortAutocomplete(); } function hideSortAutocomplete() { sortAutocomplete.classList.add('hidden'); sortAutocomplete.innerHTML = ''; acIndex = -1; } function getCurrentToken() { const val = sortInput.value; const cursor = sortInput.selectionStart; const before = val.substring(0, cursor); const lastComma = before.lastIndexOf(','); return { text: before.substring(lastComma + 1).trim(), start: lastComma + 1, cursorPos: cursor, }; } function getAlreadyChosen() { const parts = sortInput.value.split(','); const cursor = sortInput.selectionStart; const before = sortInput.value.substring(0, cursor); const tokenIdx = before.split(',').length - 1; return parts .map(p => p.trim().toLowerCase()) .filter((_, i) => i !== tokenIdx) .filter(p => p.length > 0); } function updateSortAutocomplete() { const { text } = getCurrentToken(); const lower = text.toLowerCase(); const chosen = getAlreadyChosen(); const matches = state.headers .filter(h => h.toLowerCase().includes(lower)) .filter(h => !chosen.includes(h.toLowerCase())) .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); if (matches.length === 0 || (matches.length === 1 && matches[0].toLowerCase() === lower)) { hideSortAutocomplete(); return; } sortAutocomplete.innerHTML = ''; sortAutocomplete.classList.remove('hidden'); acIndex = -1; matches.forEach((name, i) => { const div = document.createElement('div'); div.className = 'ac-item'; div.textContent = name; div.addEventListener('mousedown', (e) => { e.preventDefault(); // keep focus on input applySortCompletion(name); }); sortAutocomplete.appendChild(div); }); } function applySortCompletion(name) { const val = sortInput.value; const cursor = sortInput.selectionStart; const before = val.substring(0, cursor); const after = val.substring(cursor); const lastComma = before.lastIndexOf(','); const prefix = before.substring(0, lastComma + 1); const spacing = lastComma >= 0 ? ' ' : ''; const newVal = prefix + spacing + name + after; sortInput.value = newVal; const newCursor = (prefix + spacing + name).length; sortInput.setSelectionRange(newCursor, newCursor); hideSortAutocomplete(); sortInput.focus(); } function confirmSortAdvanced() { const names = sortInput.value .split(',') .map(s => s.trim()) .filter(s => s.length > 0); const colIndices = []; for (const name of names) { const idx = state.headers.findIndex( h => h.toLowerCase() === name.toLowerCase() ); if (idx === -1) { setStatus(`Unknown column: "${name}"`); return; } colIndices.push(idx); } if (colIndices.length === 0) { setStatus('No columns specified'); return; } state.rows.sort((a, b) => { for (const col of colIndices) { const va = (a[col] || '').toLowerCase(); const vb = (b[col] || '').toLowerCase(); const cmp = va.localeCompare(vb, undefined, { numeric: true }); if (cmp !== 0) return cmp; } return 0; }); closeSortAdvanced(); render(); setStatus('Sorted by ' + names.join(', ')); } sortInput.addEventListener('input', updateSortAutocomplete); sortInput.addEventListener('focus', updateSortAutocomplete); sortInput.addEventListener('blur', () => { // Small delay to allow mousedown on ac-item to fire setTimeout(hideSortAutocomplete, 150); }); sortInput.addEventListener('keydown', (e) => { const items = sortAutocomplete.querySelectorAll('.ac-item'); if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); if (!sortAutocomplete.classList.contains('hidden')) { hideSortAutocomplete(); } else { closeSortAdvanced(); } return; } if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); if (acIndex >= 0 && items[acIndex]) { applySortCompletion(items[acIndex].textContent); } else { confirmSortAdvanced(); } return; } if (e.key === 'ArrowDown' && items.length > 0) { e.preventDefault(); if (acIndex >= 0) items[acIndex]?.classList.remove('active'); acIndex = (acIndex + 1) % items.length; items[acIndex]?.classList.add('active'); items[acIndex]?.scrollIntoView({ block: 'nearest' }); return; } if (e.key === 'ArrowUp' && items.length > 0) { e.preventDefault(); if (acIndex >= 0) items[acIndex]?.classList.remove('active'); acIndex = (acIndex - 1 + items.length) % items.length; items[acIndex]?.classList.add('active'); items[acIndex]?.scrollIntoView({ block: 'nearest' }); return; } e.stopPropagation(); }); sortConfirmBtn.addEventListener('click', confirmSortAdvanced); sortCancelBtn.addEventListener('click', closeSortAdvanced); // ===== File loading ===== async function loadFile(filePath) { try { const data = await window.go.main.App.LoadCSV(filePath); state.headers = data.Headers || []; state.rows = data.Rows || []; state.filePath = data.FilePath || filePath; state.cursor = { row: 0, col: 0 }; state.selection = null; state.colWidths = state.headers.map(() => DEFAULT_COL_WIDTH); ensureGridSize(); render(); setStatus('Loaded: ' + filePath); } catch (e) { setStatus('Error loading: ' + e); } } function loadEmptySheet() { const cols = 10; const rows = 30; state.headers = Array.from({ length: cols }, (_, i) => colLabel(i)); state.rows = Array.from({ length: rows }, () => new Array(cols).fill('')); state.colWidths = new Array(cols).fill(DEFAULT_COL_WIDTH); state.cursor = { row: 0, col: 0 }; state.selection = null; state.filePath = ''; render(); } // ===== File drop ===== function setupFileDrop() { try { window.runtime.EventsOn('file-dropped', (filePath) => { if (filePath && filePath.toLowerCase().endsWith('.csv')) { loadFile(filePath); } else if (filePath) { // Try to load anyway loadFile(filePath); } }); } catch (e) { console.warn('File drop events not available:', e); } } // ===== Init ===== async function init() { await loadCommands(); setupFileDrop(); loadEmptySheet(); } init();