feat(blog): live in-page filter via data-search attributes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Josh Bairstow 2026-06-02 01:27:02 +10:00
parent 8423759f03
commit 468d4e460d

View file

@ -80,3 +80,65 @@
} }
} }
</style> </style>
<script>
// Lives on /blog. No-ops gracefully on any other page (selectors return empty).
const input = document.querySelector<HTMLInputElement>(".binput");
if (input) {
const rows = Array.from(
document.querySelectorAll<HTMLElement>("[data-search]")
);
const featured = document.querySelector<HTMLElement>("[data-featured]");
const yearHeads = Array.from(
document.querySelectorAll<HTMLElement>("[data-year-head]")
);
const empty = document.querySelector<HTMLElement>("[data-empty]");
const emptyQuery = empty?.querySelector<HTMLElement>("[data-empty-query]");
function applyFilter() {
const raw = input!.value.toLowerCase().trim();
const tokens = raw.length ? raw.split(/\s+/) : [];
const isFiltering = tokens.length > 0;
let visibleCount = 0;
for (const row of rows) {
const haystack = row.dataset.search ?? "";
const isFeatured = row === featured;
let visible: boolean;
if (!isFiltering) {
visible = true;
} else if (isFeatured) {
// Featured hides whenever there's a query, regardless of match.
visible = false;
} else {
visible = tokens.every((t) => haystack.includes(t));
}
row.classList.toggle("is-hidden", !visible);
if (visible && !isFeatured) visibleCount += 1;
}
// Hide year heads whose following rows are all hidden.
for (const head of yearHeads) {
let anyVisible = false;
let node: Element | null = head.nextElementSibling;
while (node && !node.matches("[data-year-head]")) {
if (node.matches("[data-search]") && !node.classList.contains("is-hidden")) {
anyVisible = true;
break;
}
node = node.nextElementSibling;
}
head.classList.toggle("is-hidden", !anyVisible);
}
// Toggle empty-state message.
if (empty) {
const showEmpty = isFiltering && visibleCount === 0;
empty.hidden = !showEmpty;
if (showEmpty && emptyQuery) emptyQuery.textContent = input!.value.trim();
}
}
input.addEventListener("input", applyFilter);
}
</script>