Compare commits
No commits in common. "15bd72e02b1972e6039d9c570663ea35c61bbc7e" and "0e68de42783b610268050962fa0bece040252e54" have entirely different histories.
15bd72e02b
...
0e68de4278
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,24 +39,6 @@
|
|||
<button id="sort-confirm-btn" class="modal-btn modal-btn-primary">Sort</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="set-where-modal" class="hidden">
|
||||
<div class="modal-title">Set Where</div>
|
||||
<div class="modal-body">
|
||||
<label for="sw-match-col">Match Column:</label>
|
||||
<div class="sw-input-wrap">
|
||||
<input type="text" id="sw-match-col" placeholder="Column name" autocomplete="off" />
|
||||
<div id="sw-col-autocomplete" class="hidden"></div>
|
||||
</div>
|
||||
<label for="sw-match-values" style="margin-top:10px">Match Values (one per line):</label>
|
||||
<textarea id="sw-match-values" rows="6" placeholder="value1 value2 value3"></textarea>
|
||||
<label for="sw-set-value" style="margin-top:10px">Set Value:</label>
|
||||
<input type="text" id="sw-set-value" placeholder="Value to set" />
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button id="sw-cancel-btn" class="modal-btn">Cancel</button>
|
||||
<button id="sw-confirm-btn" class="modal-btn modal-btn-primary">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="overlay" class="hidden"></div>
|
||||
</div>
|
||||
<script src="./src/main.js" type="module"></script>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue