- Season filter: add a "Full year" select-all row above the four seasons (checked at 12 months, clears to none, indeterminate when partial). One shared change in seasonFilterDropdown/applySeasonMonthChange/syncSeasonChecks covers both pages; the summary already reads "All year" at 12. - Metric selector rides the filter sheet on phones: initFilterSheet gains a sheetOnly option that relocates given controls into the sheet at the 640px breakpoint and restores them inline on desktop (matchMedia; reparenting keeps listeners). Compare passes its "Distribution by" control, calendar its "Color the calendar by" block. - Compare comfort/tolerance slider moved out of the params panel into a standalone full-width card above the results, so it's always visible instead of buried in the mobile pill sheet. The params sheet now holds basis + season + date range. Frontend-only; no backend or payload change.
91 lines
4.2 KiB
JavaScript
91 lines
4.2 KiB
JavaScript
// Mobile filter access: a small floating "Filters" pill (fixed, reachable from
|
|
// anywhere on the page) that slides the page's existing filter panel up as a bottom
|
|
// sheet. Desktop is untouched — the pill, scrim and grip are display:none above the
|
|
// phone breakpoint, so the panel stays inline exactly as before (all styling lives in
|
|
// the @media(max-width:640px) block in style.css). Shared by compare + calendar.
|
|
//
|
|
// initFilterSheet({ panel, label, sheetOnly })
|
|
// panel — the filter panel element (becomes the sheet on mobile)
|
|
// label — text on the pill and the sheet header (e.g. "Filters")
|
|
// sheetOnly — elements that live inline on desktop but move INTO the sheet on
|
|
// mobile (e.g. a metric selector); restored to their slot on desktop
|
|
// returns { setActive(bool), close() }
|
|
|
|
const FUNNEL = `<svg class="ff-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 5h18M6 12h12M10 19h4"/></svg>`;
|
|
|
|
export function initFilterSheet({ panel, label = "Filters", sheetOnly = [] }) {
|
|
if (!panel) return { setActive() {}, close() {} };
|
|
|
|
// The pill lives in <body> so it floats over the whole page regardless of scroll.
|
|
const fab = document.createElement("button");
|
|
fab.type = "button";
|
|
fab.className = "filter-fab";
|
|
fab.setAttribute("aria-expanded", "false");
|
|
fab.innerHTML = `${FUNNEL}<span class="ff-label">${label}</span><span class="ff-dot" hidden aria-hidden="true"></span>`;
|
|
|
|
const scrim = document.createElement("div");
|
|
scrim.className = "filter-scrim";
|
|
scrim.hidden = true;
|
|
|
|
// A grab handle + title + close, prepended to the panel; only shown in sheet mode.
|
|
const grip = document.createElement("div");
|
|
grip.className = "filter-sheet-grip";
|
|
grip.innerHTML = `<span class="fsg-bar" aria-hidden="true"></span>` +
|
|
`<span class="fsg-title">${label}</span>` +
|
|
`<button type="button" class="fsg-close" aria-label="Close filters">✕</button>`;
|
|
panel.prepend(grip);
|
|
|
|
document.body.append(scrim, fab);
|
|
document.body.classList.add("has-filter-sheet"); // scopes the extra bottom padding
|
|
|
|
const isOpen = () => panel.classList.contains("open");
|
|
function open() {
|
|
panel.classList.add("open");
|
|
scrim.hidden = false;
|
|
fab.setAttribute("aria-expanded", "true");
|
|
}
|
|
function close() {
|
|
panel.classList.remove("open");
|
|
scrim.hidden = true;
|
|
fab.setAttribute("aria-expanded", "false");
|
|
}
|
|
|
|
fab.addEventListener("click", () => (isOpen() ? close() : open()));
|
|
scrim.addEventListener("click", close);
|
|
grip.querySelector(".fsg-close").addEventListener("click", close);
|
|
document.addEventListener("keydown", (e) => { if (e.key === "Escape" && isOpen()) close(); });
|
|
|
|
// The pill only makes sense once the page has data to filter — the panel starts
|
|
// hidden and the page reveals it. Mirror that onto the pill, and drop the sheet if
|
|
// the panel goes away.
|
|
const reflect = () => {
|
|
fab.classList.toggle("ready", !panel.hidden);
|
|
if (panel.hidden && isOpen()) close();
|
|
};
|
|
new MutationObserver(reflect).observe(panel, { attributes: true, attributeFilter: ["hidden"] });
|
|
reflect();
|
|
|
|
// Relocate `sheetOnly` controls into the sheet on phones and back to their inline
|
|
// slot on desktop, at the same 640px breakpoint the CSS uses. Each is remembered by
|
|
// its original parent + next sibling; reparenting preserves their event listeners.
|
|
const mq = window.matchMedia("(max-width: 640px)");
|
|
const homes = sheetOnly.filter(Boolean).map((el) => ({ el, parent: el.parentNode, next: el.nextSibling }));
|
|
function placeExtras() {
|
|
for (const h of homes) {
|
|
if (mq.matches) {
|
|
if (h.el.parentNode !== panel) panel.append(h.el); // append after the grip + existing controls
|
|
} else if (h.el.parentNode === panel) {
|
|
h.parent.insertBefore(h.el, h.next); // restore (next === null → append)
|
|
}
|
|
}
|
|
}
|
|
mq.addEventListener("change", placeExtras);
|
|
placeExtras();
|
|
|
|
return {
|
|
// Show a dot on the pill when a filter subset is active (so the applied filter is
|
|
// visible even with the sheet closed).
|
|
setActive(active) { fab.querySelector(".ff-dot").hidden = !active; },
|
|
close,
|
|
};
|
|
}
|