---
// starlight's search, with each result's excerpt cut down to a little context
// before its first match, so the match survives the one-line clamp in tangled.css.
// excerpts that start or stop mid-sentence are marked so the stylesheet can add
// an ellipsis at that end. results show a page of cards at a time, the last
// one faded, and scrolling it into view shows the next page, asking pagefind
// for more once the shown cards run low. the arrow keys move through the results
// while focus stays in the input, and enter opens the active one. each card's
// title is prefixed with its page's title, unless the card is the page itself
import Default from "@astrojs/starlight/components/Search.astro";
---
<Default />
<script>
const context = 32;
const pageSize = 7;
const seen = new WeakSet<Text>();
const cut = new WeakSet<Text>();
const trim = (excerpt: Element) => {
const lead = excerpt.firstChild;
if (lead instanceof Text && lead.nextSibling && !seen.has(lead)) {
seen.add(lead);
if (lead.data.length > context) {
const tail = lead.data.slice(-context);
lead.data = tail.slice(tail.indexOf(" ") + 1);
cut.add(lead);
}
}
const text = excerpt.textContent?.trim() ?? "";
const startsMidSentence = /^\p{Ll}/u.test(text);
excerpt.toggleAttribute("data-clipped", (lead instanceof Text && cut.has(lead)) || startsMidSentence);
excerpt.toggleAttribute("data-trails", !/[.!?:]$/.test(text));
};
const results = document.querySelector<HTMLElement>("#starlight__search");
if (results) {
let active: Element | undefined;
let chosen = false;
let limit = pageSize;
const allCards = () => results.querySelectorAll(".pagefind-ui__result-nested");
const cards = () => [...results.querySelectorAll(".pagefind-ui__result-nested:not([data-overflow])")];
const pagefindMore = () => results.querySelector<HTMLButtonElement>(".pagefind-ui__button");
const frame = results.closest(".dialog-frame");
let pending = false;
let count = 0;
let watched: Element | undefined;
const loadMore = () => {
const all = allCards().length;
if (all > limit) limit += pageSize;
const button = pagefindMore();
if (button && !pending && all < limit + pageSize) {
pending = true;
button.click();
}
paginate();
};
const watcher = new IntersectionObserver((entries) => entries.some((e) => e.isIntersecting) && loadMore(), {
root: frame,
rootMargin: "0px 0px 120px 0px",
});
const paginate = () => {
const all = allCards();
all.forEach((card, i) => {
card.toggleAttribute("data-overflow", i >= limit);
card.removeAttribute("data-faded");
});
const last = all.length > limit || pagefindMore() ? all[Math.min(limit, all.length) - 1] : undefined;
last?.setAttribute("data-faded", "");
if (last !== watched) {
watcher.disconnect();
watched = last;
if (last) watcher.observe(last);
}
};
const activate = (card: Element | undefined, scroll = false) => {
active?.removeAttribute("data-active");
active = card;
active?.setAttribute("data-active", "");
if (scroll) active?.scrollIntoView({ block: "nearest" });
};
const label = () => {
results.querySelectorAll(".pagefind-ui__result").forEach((result) => {
const page = result.querySelector(".pagefind-ui__result-inner > .pagefind-ui__result-title")?.textContent?.trim();
result.querySelectorAll(".pagefind-ui__result-nested .pagefind-ui__result-link").forEach((link) => {
const own = link.textContent?.trim();
if (page && own && own !== page) link.setAttribute("data-page", page);
else link.removeAttribute("data-page");
});
});
};
new MutationObserver(() => {
if (allCards().length !== count) {
count = allCards().length;
pending = false;
}
paginate();
label();
results.querySelectorAll(".pagefind-ui__result-excerpt").forEach(trim);
if (!chosen || !active?.isConnected) activate(cards()[0]);
}).observe(results, { childList: true, subtree: true });
results.addEventListener("input", () => {
chosen = false;
pending = false;
limit = pageSize;
activate(undefined);
});
results.addEventListener("pointermove", (e) => {
const card = (e.target as Element).closest(".pagefind-ui__result-nested");
if (card && card !== active) {
chosen = true;
activate(card);
}
});
results.addEventListener("keydown", (e) => {
if (!(e.target instanceof HTMLInputElement)) return;
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
const all = cards();
if (!all.length) return;
e.preventDefault();
chosen = true;
const i = active ? all.indexOf(active) : -1;
const next = e.key === "ArrowDown" ? Math.min(i + 1, all.length - 1) : Math.max(i - 1, 0);
activate(all[next], true);
} else if (e.key === "Enter" && !e.isComposing && active) {
e.preventDefault();
active.querySelector<HTMLAnchorElement>("a")?.click();
}
});
}
</script>