// the list beside the paper: selection, and folding. a section opens when the selection moves // into it and closes again when it leaves, unless you opened it by hand with its triangle. export interface Tree { /** select the entry linking to #key ("" is the top of the page) */ select(key: string): void; /** entries in list order that aren't folded away, for ↑↓ */ visible(): string[]; } /** `side` is Tree.astro's column: the title and the list */ export function tree(side: HTMLElement): Tree { const list = side.querySelector(".tree")!; const links = new Map([...side.querySelectorAll("[data-to]")].map((a) => [a.dataset.to!, a])); const open = new Set(); let auto: string | null = null; const fold = () => list.querySelectorAll("[data-grp]").forEach((g) => { const isOpen = open.has(g.dataset.grp!); g.classList.toggle("open", isOpen); g.querySelector("[data-fold]")?.setAttribute("aria-expanded", String(isOpen)); }); list.addEventListener("click", (e) => { const btn = (e.target as HTMLElement).closest("[data-fold]"); if (!btn) return; const g = btn.dataset.fold!; open.has(g) ? open.delete(g) : open.add(g); if (auto === g) auto = null; fold(); }); return { select(key) { const link = links.get(key); links.forEach((a, k) => a.classList.toggle("sel", k === key)); const group = link?.closest("[data-grp]")?.dataset.grp ?? null; if (auto && auto !== group) open.delete(auto); if (group && !open.has(group)) { open.add(group); auto = group; } fold(); // scroll the list only, never the page around it if (link) { const r = link.getBoundingClientRect(), box = list.getBoundingClientRect(); if (r.top < box.top) list.scrollTop -= box.top - r.top; else if (r.bottom > box.bottom) list.scrollTop += r.bottom - box.bottom; } }, visible: () => [...links].filter(([, a]) => a.offsetParent).map(([k]) => k), }; }