diff --git a/media/webview.css b/media/webview.css index e01bd73..dda2dce 100644 --- a/media/webview.css +++ b/media/webview.css @@ -1157,7 +1157,7 @@ body.vscode-high-contrast #grid-container.cm-on { .col-chooser-item { display: flex; align-items: center; - gap: 7px; + gap: 6px; padding: 4px 6px; font-size: 12px; cursor: pointer; @@ -1165,6 +1165,31 @@ body.vscode-high-contrast #grid-container.cm-on { } .col-chooser-item:hover { background: var(--vscode-list-hoverBackground, #2a2d2e); } .col-chooser-item input { cursor: pointer; margin: 0; flex: none; accent-color: var(--vscode-focusBorder, #007fd4); } +.col-chooser-master { + display: flex; + align-items: center; + gap: 6px; + padding: 6px; + margin: 6px 0; + font-size: 12px; + font-weight: 600; + cursor: pointer; + border-radius: 3px; + position: relative; +} +.col-chooser-master::after { + content: ''; + position: absolute; + left: 0; + right: 0; + bottom: -4px; + height: 1px; + background: var(--vscode-menu-border, #454545); +} +.col-chooser-master:hover { background: var(--vscode-list-hoverBackground, #2a2d2e); } +.col-chooser-master input { cursor: pointer; margin: 0; flex: none; accent-color: var(--vscode-focusBorder, #007fd4); } +.col-chooser-master-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.col-chooser-master-count { font-size: 11px; font-weight: 400; opacity: 0.6; flex: none; } .col-chooser-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .goto-error { margin-top: 6px; diff --git a/src/webview.ts b/src/webview.ts index c1fcb10..16228c6 100644 --- a/src/webview.ts +++ b/src/webview.ts @@ -174,10 +174,11 @@ export function getWebviewContent( Columns -
- - -
+
diff --git a/src/webview/features/column-chooser.ts b/src/webview/features/column-chooser.ts index e4fd98d..1fb11a1 100644 --- a/src/webview/features/column-chooser.ts +++ b/src/webview/features/column-chooser.ts @@ -3,13 +3,13 @@ import { closeAllPopups } from './popups'; // ── Column chooser (show / hide columns) ──────────────────────────────────── // A toolbar dropdown listing every data column with a checkbox to toggle its -// visibility. A search box filters the list by column name, and Show all / Hide -// all flip every column at once (Hide all, then search and re-check the few you -// want). Visibility is applied via AG Grid (setColumnsVisible) and mirrored into -// state.hiddenCols (0-based data-column indices) so it survives a grid rebuild — -// e.g. a paged-view page change re-runs buildGrid, which re-applies `hide` from -// this set. In-memory only (not persisted across reload), matching the freeze -// features. Column insert/delete clears the set (see delete-row-col). +// visibility. A search box filters the list by column name. A pinned tri-state +// "Select all" master checkbox above the list reflects and controls the columns. +// Visibility is applied via AG Grid (setColumnsVisible) and mirrored into state.hiddenCols +// (0-based data-column indices) so it survives a grid rebuild - e.g. a paged-view +// page change re-runs buildGrid, which re-applies `hide` from this set. In-memory +// only (not persisted across reload), matching the freeze features. Column +// insert/delete clears the set (see delete-row-col). let searchQuery = ''; @@ -18,34 +18,63 @@ function setColHidden(colIndex: number, hidden: boolean): void { else state.hiddenCols.delete(colIndex); state.gridApi?.setColumnsVisible(['col_' + colIndex], !hidden); updateButton(); + syncMaster(); } -function allColIds(): string[] { - const n = (state.data[0] ?? []).length; - const ids: string[] = []; - for (let c = 0; c < n; c++) ids.push('col_' + c); - return ids; +function colLabel(header: string[], c: number): string { + const name = header[c] ?? ''; + return name !== '' ? name : '(column ' + (c + 1) + ')'; +} + +function visibleColIndices(): number[] { + const header = state.data[0] ?? []; + const q = searchQuery.trim().toLowerCase(); + const out: number[] = []; + for (let c = 0; c < header.length; c++) { + if (q && !colLabel(header, c).toLowerCase().includes(q)) continue; + out.push(c); + } + return out; } -function showAll(): void { - state.hiddenCols.clear(); - state.gridApi?.setColumnsVisible(allColIds(), true); +function setVisibleColsHidden(hidden: boolean): void { + const cols = visibleColIndices(); + if (cols.length === 0) return; + if (!hidden && searchQuery.trim() === '') { + state.hiddenCols.clear(); + state.gridApi?.setColumnsVisible(allColIds(), true); + } else { + for (const c of cols) { + if (hidden) state.hiddenCols.add(c); + else state.hiddenCols.delete(c); + } + state.gridApi?.setColumnsVisible(cols.map(c => 'col_' + c), !hidden); + } buildList(); updateButton(); } -function hideAll(): void { +function allColIds(): string[] { const n = (state.data[0] ?? []).length; - state.hiddenCols.clear(); - for (let c = 0; c < n; c++) state.hiddenCols.add(c); - state.gridApi?.setColumnsVisible(allColIds(), false); - buildList(); - updateButton(); + const ids: string[] = []; + for (let c = 0; c < n; c++) ids.push('col_' + c); + return ids; } -function colLabel(header: string[], c: number): string { - const name = header[c] ?? ''; - return name !== '' ? name : '(column ' + (c + 1) + ')'; +function syncMaster(): void { + const cb = document.getElementById('col-chooser-master-cb') as HTMLInputElement | null; + const count = document.getElementById('col-chooser-master-count'); + const label = document.getElementById('col-chooser-master-label'); + if (!cb) return; + const cols = visibleColIndices(); + const visible = cols.reduce((n, c) => n + (state.hiddenCols.has(c) ? 0 : 1), 0); + const total = cols.length; + + cb.checked = total > 0 && visible === total; + cb.indeterminate = visible > 0 && visible < total; + cb.disabled = total === 0; + if (count) count.textContent = total > 0 ? `${visible} / ${total}` : ''; + if (label) label.textContent = searchQuery.trim() ? 'Select all matches' : 'Select all'; } function buildList(): void { @@ -53,13 +82,9 @@ function buildList(): void { if (!list) return; list.innerHTML = ''; const header = state.data[0] ?? []; - const q = searchQuery.trim().toLowerCase(); - let shown = 0; - for (let c = 0; c < header.length; c++) { - const label = colLabel(header, c); - if (q && !label.toLowerCase().includes(q)) continue; - shown++; + const cols = visibleColIndices(); + for (const c of cols) { const row = document.createElement('label'); row.className = 'col-chooser-item'; @@ -71,19 +96,21 @@ function buildList(): void { const span = document.createElement('span'); span.className = 'col-chooser-label'; - span.textContent = label; + span.textContent = colLabel(header, c); row.appendChild(cb); row.appendChild(span); list.appendChild(row); } - if (shown === 0) { + if (cols.length === 0) { const empty = document.createElement('div'); empty.className = 'csv-filter-empty'; empty.textContent = 'No matching columns'; list.appendChild(empty); } + + syncMaster(); } function updateButton(): void { @@ -99,10 +126,10 @@ function openChooser(): void { if (search) search.value = ''; buildList(); pop.classList.remove('hidden'); - const r = btn.getBoundingClientRect(); + const r = btn.getBoundingClientRect(); const pw = pop.offsetWidth || 220; const vw = window.innerWidth; - pop.style.top = (r.bottom + 4) + 'px'; + pop.style.top = (r.bottom + 4) + 'px'; pop.style.left = Math.max(4, Math.min(r.left, vw - pw - 4)) + 'px'; search?.focus(); } @@ -121,8 +148,8 @@ export function setupColumnChooser(): void { if (!wasOpen) openChooser(); }); - document.getElementById('col-chooser-showall')?.addEventListener('click', showAll); - document.getElementById('col-chooser-hideall')?.addEventListener('click', hideAll); + const master = document.getElementById('col-chooser-master-cb') as HTMLInputElement | null; + master?.addEventListener('change', () => setVisibleColsHidden(!master.checked)); const search = document.getElementById('col-chooser-search') as HTMLInputElement | null; search?.addEventListener('input', () => {