Excel-style column filters, full-page Custom View, card field editor
Filters (web/excel-filter.js), modeled on Excel's AutoFilter: - Every column header gets a dropdown: Sort A to Z / Z to A, Clear Filter From "<column>", Search, a checkbox list of the column's values with (Select All) and (Blanks), OK / Cancel. Values come from rows passing the other columns' filters; filters on several columns combine. Tags and software lists are split into individual items. - The button shows a funnel when filtered and an arrow when sorted, and a "Clear filters (n)" button appears beside the group's copy buttons - Replaces the old per-column filter row (which also removes the gap between header and filter rows); hooks the existing filter passes so global search, counts and worker re-renders keep working - Menus are fixed-position on <body>, so sticky headers and neighbouring tables can't cover them; on phones they open as a bottom sheet - Workbench stacks get the same menus (sorting done client-side) - Custom View's filter icon opens the same menu Layout: - Sticky headers use separated borders so no hairline gap opens - Phones/tablets: a sticky bar of column chips (with active counts and "Clear all") opens the filter menus - Custom View fills the page like the other views on desktop; it still scrolls sideways when wide, with its header pinned under the group header - Custom View on phones/tablets: a "Card fields" sheet to choose, order and remove the fields shown on each card Demo: first visit puts 6 PCs (one per migration stage) on the Workbench.
This commit is contained in:
1 parent
d24be4a2d6
commit
b216ce43f2
5 files changed
+788
-99
No files matched your search
+19
-28
@@ -45,33 +45,6 @@
|
||||
thead.querySelectorAll('tr.filter-row').forEach(apply);
|
||||
for (const body of table.tBodies) for (const row of body.rows) apply(row);
|
||||
|
||||
addFilterToggle(table, thead.querySelector('tr.filter-row'));
|
||||
}
|
||||
|
||||
// Column filters take a lot of room above every card group, so they fold behind a
|
||||
// button that also shows how many filters are active.
|
||||
function addFilterToggle(table, filterRow) {
|
||||
if (!filterRow) return;
|
||||
let btn = table.previousElementSibling;
|
||||
if (!btn || !btn.classList.contains('card-filter-toggle')) {
|
||||
btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'card-filter-toggle';
|
||||
btn.addEventListener('click', () => { table.classList.toggle('show-filters'); updateToggle(table, btn); });
|
||||
table.parentNode.insertBefore(btn, table);
|
||||
filterRow.addEventListener('input', () => updateToggle(table, btn));
|
||||
filterRow.addEventListener('change', () => updateToggle(table, btn));
|
||||
}
|
||||
updateToggle(table, btn);
|
||||
}
|
||||
function updateToggle(table, btn) {
|
||||
const row = table.tHead && table.tHead.querySelector('tr.filter-row');
|
||||
if (!row) return;
|
||||
const active = Array.from(row.querySelectorAll('input[type="text"]')).filter(i => i.value.trim()).length
|
||||
+ row.querySelectorAll('input[type="checkbox"]:checked').length;
|
||||
const open = table.classList.contains('show-filters');
|
||||
const html = `${open ? 'Hide' : 'Show'} filters${active ? ` <span class="count">${active}</span>` : ''}`;
|
||||
if (btn.innerHTML !== html) btn.innerHTML = html;
|
||||
}
|
||||
|
||||
// --- Sticky offsets ---------------------------------------------------------------
|
||||
@@ -99,8 +72,25 @@
|
||||
const gh = groupHeaderFor(table);
|
||||
const ghH = gh && getComputedStyle(gh).position === 'sticky' ? gh.getBoundingClientRect().height : 0;
|
||||
// 1px overlap tucks the table header under the group header: no hairline gap
|
||||
setVar(table, '--sticky-top', Math.max(0, navH + ghH - 1) + 'px');
|
||||
const top = Math.max(0, navH + ghH - 1) + 'px';
|
||||
setVar(table, '--sticky-top', top);
|
||||
if (table.parentElement) setVar(table.parentElement, '--sticky-top', top); // for the filter chip bar
|
||||
});
|
||||
pinCustomHeader();
|
||||
}
|
||||
|
||||
// Custom View can be wider than the page, so it scrolls sideways in its own box, and a
|
||||
// sideways-scrolling box can't contain a position:sticky header. Instead its header is
|
||||
// shifted down by exactly the amount needed to stay pinned under the group header.
|
||||
function pinCustomHeader() {
|
||||
const table = document.getElementById('table-custom');
|
||||
if (!table) return;
|
||||
if (cardsQuery.matches || !table.offsetParent || !table.tHead) { setVar(table, '--pin-y', '0px'); return; }
|
||||
const top = parseFloat(table.style.getPropertyValue('--sticky-top')) || 0;
|
||||
const r = table.getBoundingClientRect();
|
||||
const headH = table.tHead.getBoundingClientRect().height;
|
||||
const y = Math.max(0, Math.min(top - r.top, r.height - headH));
|
||||
setVar(table, '--pin-y', y + 'px');
|
||||
}
|
||||
|
||||
let queued = false;
|
||||
@@ -124,6 +114,7 @@
|
||||
// Groups and pages are shown by toggling display (not a DOM change), so also
|
||||
// re-measure as the user scrolls; writes only happen when a value changes.
|
||||
window.addEventListener('scroll', schedule, { passive: true });
|
||||
window.addEventListener('scroll', pinCustomHeader, { passive: true }); // same frame: no lag
|
||||
// The terminal dock opens, closes and resizes without a page resize
|
||||
if (window.ResizeObserver) {
|
||||
const watchDock = () => {
|
||||
|
||||
Reference in new issue
Block a user