72 lines
3.2 KiB
JavaScript
72 lines
3.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 })
|
|
// panel — the filter panel element (becomes the sheet on mobile)
|
|
// label — text on the pill and the sheet header (e.g. "Filters")
|
|
// 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" }) {
|
|
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();
|
|
|
|
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,
|
|
};
|
|
}
|