Skip to content

Commit 3b3d7b6

Browse files
Add selectable column filters to CSV row viewer (#121)
### Motivation - Provide users an easy way to choose which CSV columns are shown when browsing rows so they can focus on relevant fields. - Ensure all columns are selected by default while supporting CSVs that contain extra data columns beyond the header. - Add a convenient master toggle to quickly select or deselect all visible columns. ### Description - Add a new `column-filter-card` UI block with a `Select all` checkbox and a responsive grid of per-column checkboxes to `csv-row-viewer.html`. - Introduce `columnLabels` and `selectedColumns` state and compute `longestRowLength` to include extra columns present in data rows beyond headers. - Implement `renderColumnFilters()` to populate per-column inputs (with `data-column-index`) and set the master checkbox `indeterminate` state, and wire change handlers for both per-column and master toggles. - Update `renderRow()` to only render selected columns and to show a fallback row when no columns are selected. ------ [Codex Task](https://chatgpt.com/codex/tasks/task_e_69c3c10bf9f083259da44882f42bdb88)
1 parent f311ef0 commit 3b3d7b6

1 file changed

Lines changed: 109 additions & 8 deletions

File tree

csv-row-viewer.html

Lines changed: 109 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,36 @@
6363
margin-top: 1.25rem;
6464
}
6565

66+
.column-filter-card {
67+
display: grid;
68+
gap: 1rem;
69+
margin-bottom: 1.25rem;
70+
padding: 1rem;
71+
border: 1px solid var(--border-subtle, #d0d5dd);
72+
border-radius: 0.75rem;
73+
background: var(--background-subtle, #f8f9fb);
74+
}
75+
76+
.column-filter-header {
77+
display: flex;
78+
flex-wrap: wrap;
79+
gap: 0.75rem;
80+
justify-content: space-between;
81+
align-items: center;
82+
}
83+
84+
.column-checkboxes {
85+
display: grid;
86+
gap: 0.5rem;
87+
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
88+
}
89+
90+
.checkbox-row {
91+
display: inline-flex;
92+
align-items: center;
93+
gap: 0.5rem;
94+
}
95+
6696
.jump-group {
6797
display: inline-flex;
6898
align-items: center;
@@ -102,6 +132,17 @@ <h1>CSV Row Viewer</h1>
102132
<span id="file-summary"></span>
103133
</div>
104134

135+
<div id="column-filter-card" class="column-filter-card" hidden>
136+
<div class="column-filter-header">
137+
<strong>Visible columns</strong>
138+
<label class="checkbox-row" for="toggle-all-columns">
139+
<input id="toggle-all-columns" type="checkbox">
140+
<span>Select all</span>
141+
</label>
142+
</div>
143+
<div id="column-checkboxes" class="column-checkboxes"></div>
144+
</div>
145+
105146
<div class="table-container" aria-live="polite">
106147
<table>
107148
<thead>
@@ -142,10 +183,15 @@ <h1>CSV Row Viewer</h1>
142183
const nextButton = document.getElementById('next-row');
143184
const jumpInput = document.getElementById('jump-row');
144185
const jumpButton = document.getElementById('jump-button');
186+
const columnFilterCard = document.getElementById('column-filter-card');
187+
const columnCheckboxes = document.getElementById('column-checkboxes');
188+
const toggleAllColumns = document.getElementById('toggle-all-columns');
145189

146190
let headers = [];
147191
let rows = [];
148192
let currentIndex = 0;
193+
let columnLabels = [];
194+
let selectedColumns = [];
149195

150196
const parseCsv = (text) => {
151197
const output = [];
@@ -215,26 +261,47 @@ <h1>CSV Row Viewer</h1>
215261

216262
const renderRow = () => {
217263
const row = rows[currentIndex] || [];
218-
const totalColumns = Math.max(headers.length, row.length);
219-
220-
const mergedHeaders = Array.from({ length: totalColumns }, (_, index) => {
221-
if (headers[index]) {
222-
return headers[index];
264+
const visibleColumns = columnLabels.reduce((output, header, index) => {
265+
if (selectedColumns[index]) {
266+
output.push({ header, index });
223267
}
224-
return `Extra column ${index - headers.length + 1}`;
225-
});
268+
return output;
269+
}, []);
226270

227-
tableBody.innerHTML = mergedHeaders.map((header, index) => {
271+
tableBody.innerHTML = visibleColumns.map(({ header, index }) => {
228272
const value = row[index] ?? '—';
229273
return `\n<tr><th scope="row">${escapeHtml(header || `Column ${index + 1}`)}</th><td>${escapeHtml(value)}</td></tr>`;
230274
}).join('');
231275

276+
if (visibleColumns.length === 0) {
277+
tableBody.innerHTML = '<tr><td colspan="2">No columns selected.</td></tr>';
278+
}
279+
232280
rowIndicator.textContent = `Row ${currentIndex + 1} of ${rows.length}`;
233281
prevButton.disabled = currentIndex === 0;
234282
nextButton.disabled = currentIndex >= rows.length - 1;
235283
jumpInput.value = String(currentIndex + 1);
236284
};
237285

286+
const renderColumnFilters = () => {
287+
if (!columnLabels.length) {
288+
columnFilterCard.hidden = true;
289+
return;
290+
}
291+
292+
columnFilterCard.hidden = false;
293+
columnCheckboxes.innerHTML = columnLabels.map((label, index) => `
294+
<label class="checkbox-row" for="column-toggle-${index}">
295+
<input id="column-toggle-${index}" type="checkbox" data-column-index="${index}" ${selectedColumns[index] ? 'checked' : ''}>
296+
<span>${escapeHtml(label || `Column ${index + 1}`)}</span>
297+
</label>
298+
`).join('');
299+
300+
const allSelected = selectedColumns.every(Boolean);
301+
toggleAllColumns.checked = allSelected;
302+
toggleAllColumns.indeterminate = !allSelected && selectedColumns.some(Boolean);
303+
};
304+
238305
const escapeHtml = (value) => value
239306
.replaceAll('&', '&amp;')
240307
.replaceAll('<', '&lt;')
@@ -254,6 +321,14 @@ <h1>CSV Row Viewer</h1>
254321
headers = parsed[0];
255322
rows = parsed.slice(1);
256323
currentIndex = 0;
324+
const longestRowLength = rows.reduce((max, row) => Math.max(max, row.length), headers.length);
325+
columnLabels = Array.from({ length: longestRowLength }, (_, index) => {
326+
if (headers[index]) {
327+
return headers[index];
328+
}
329+
return `Extra column ${index - headers.length + 1}`;
330+
});
331+
selectedColumns = columnLabels.map(() => true);
257332

258333
viewer.hidden = false;
259334
fileSummary.textContent = `${file.name}${rows.length} data row${rows.length === 1 ? '' : 's'}`;
@@ -263,11 +338,14 @@ <h1>CSV Row Viewer</h1>
263338
jumpInput.min = '1';
264339
jumpInput.max = String(rows.length);
265340

341+
renderColumnFilters();
266342
renderRow();
267343
} catch (error) {
268344
viewer.hidden = true;
269345
headers = [];
270346
rows = [];
347+
columnLabels = [];
348+
selectedColumns = [];
271349
loadStatus.textContent = error.message || 'Unable to parse this CSV file.';
272350
loadStatus.classList.add('error');
273351
}
@@ -316,6 +394,29 @@ <h1>CSV Row Viewer</h1>
316394
jumpToRow();
317395
}
318396
});
397+
398+
columnCheckboxes.addEventListener('change', (event) => {
399+
const target = event.target;
400+
if (!(target instanceof HTMLInputElement)) {
401+
return;
402+
}
403+
404+
const index = Number.parseInt(target.dataset.columnIndex || '', 10);
405+
if (Number.isNaN(index)) {
406+
return;
407+
}
408+
409+
selectedColumns[index] = target.checked;
410+
renderColumnFilters();
411+
renderRow();
412+
});
413+
414+
toggleAllColumns.addEventListener('change', () => {
415+
const isChecked = toggleAllColumns.checked;
416+
selectedColumns = selectedColumns.map(() => isChecked);
417+
renderColumnFilters();
418+
renderRow();
419+
});
319420
})();
320421
</script>
321422
</body>

0 commit comments

Comments
 (0)