// 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<HTMLElement>(".tree")!;
const links = new Map([...side.querySelectorAll<HTMLAnchorElement>("[data-to]")].map((a) => [a.dataset.to!, a]));
const open = new Set<string>();
let auto: string | null = null;
const fold = () =>
list.querySelectorAll<HTMLElement>("[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<HTMLElement>("[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<HTMLElement>("[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),
};
}