-
Notifications
You must be signed in to change notification settings - Fork 2
Add select-all checkbox to column chooser #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2f55eb3
2e8f915
7b33380
cf44ac1
438708f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,48 +18,73 @@ 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 { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Recommendation after a closer look: keep this scoped behavior (it is the Excel model #19 asked for) and just make the scope visible. The only gap is that the label stays 'Select all' while the action is scoped to the search. In syncMaster, when searchQuery is non-empty, set the master label to 'Select all matches' and back to 'Select all' when the search is empty. That makes the N / N count and the checked state honest. The global escape hatch already exists since clearing the search box restores the global master, so no separate Show-all button is needed (that was the clutter #19 wanted gone).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Brought back the global reveal behavior. Checking the master box with an empty search now clears all hidden items (Show all). When filtered, it stays scoped to the search results. |
||
| 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 { | ||
| const list = document.getElementById('col-chooser-list'); | ||
| 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', () => { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small cleanup: visibleColIndices() now owns the search predicate (q && !colLabel(...).toLowerCase().includes(q)), but buildList() still runs a byte-identical copy of it in its own render loop. No bug today, but the master count (from visibleColIndices) and the rendered rows (from buildList) derive 'which columns match' from two copies of the rule. If the match logic changes in one place they will disagree. Could buildList iterate over visibleColIndices() so there is one source of truth.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
buildList() now iterates visibleColIndices(), so the search-match rule lives in one place.