feedfetcher/page.js
File Type: text/plain
const filter = document.getElementById("filter");
const ageButtons = [...document.querySelectorAll(".filter-tools button")];
const count = document.getElementById("filter-count");
const empty = document.getElementById("no-matches");
const categoryRow = document.getElementById("categories");
const categoryButtons = [...document.querySelectorAll("#categories button")];
const channels = [...document.querySelectorAll(".channel")];
const groups = [...document.querySelectorAll(".group")];
function selectedAge() {
const pressed = ageButtons.find((button) => button.getAttribute("aria-pressed") === "true");
return pressed ? pressed.dataset.age : "";
}
function selectedCategory() {
const pressed = categoryButtons.find((button) => button.getAttribute("aria-pressed") === "true");
return pressed ? pressed.dataset.category : "";
}
function applyFilter() {
const query = filter.value.trim().toLocaleLowerCase();
const age = selectedAge();
const category = selectedCategory();
let shown = 0;
let feeds = 0;
for (const channel of channels) {
const inCategory = !category || channel.dataset.category === category;
const channelHit = !query || channel.dataset.channel.includes(query);
let matches = 0;
for (const item of channel.querySelectorAll(".item")) {
const textHit = channelHit || item.dataset.text.includes(query);
const ageHit = !age || item.classList.contains(age);
const hit = inCategory && textHit && ageHit;
item.classList.toggle("is-hidden", !hit);
if (hit) matches += 1;
}
channel.classList.toggle("is-hidden", matches === 0);
shown += matches;
if (matches) feeds += 1;
}
for (const group of groups) {
const visible = group.querySelector(".channel:not(.is-hidden)");
group.classList.toggle("is-hidden", !visible);
}
const filtering = query.length > 0 || age.length > 0 || category.length > 0;
count.textContent = shown + " entries · " + feeds + " feeds";
empty.classList.toggle("is-hidden", !filtering || shown > 0);
}
filter.addEventListener("input", applyFilter);
for (const button of ageButtons) {
button.addEventListener("click", () => {
const turnOn = button.getAttribute("aria-pressed") !== "true";
for (const other of ageButtons) other.setAttribute("aria-pressed", "false");
if (turnOn) button.setAttribute("aria-pressed", "true");
applyFilter();
});
}
for (const button of categoryButtons) {
button.addEventListener("click", () => {
for (const other of categoryButtons) other.setAttribute("aria-pressed", "false");
button.setAttribute("aria-pressed", "true");
applyFilter();
window.scrollTo({top: 0, behavior: "smooth"});
});
}
const toTop = document.getElementById("to-top");
window.addEventListener("scroll", () => {
toTop.classList.toggle("is-visible", window.scrollY > 320);
}, {passive: true});
toTop.addEventListener("click", () => window.scrollTo({top: 0, behavior: "smooth"}));
if (location.hash.length > 1) {
filter.value = decodeURIComponent(location.hash.slice(1));
applyFilter();
}
applyFilter();
filter.focus();
const scroller = categoryRow && categoryRow.parentElement;
function updateCategoryScroll() {
if (!scroller || !scroller.classList.contains("category-scroller")) return;
const max = categoryRow.scrollWidth - categoryRow.clientWidth;
scroller.classList.toggle("can-left", categoryRow.scrollLeft > 4);
scroller.classList.toggle("can-right", max - categoryRow.scrollLeft > 4);
}
const topbar = document.querySelector(".topbar");
const searchForm = document.querySelector(".search");
let compact = false;
function reserveTopbar() {
// Hold the expanded height so collapsing the bar does not pull the
// list up and jump the content under the cursor. The empty part of
// the bar ignores pointer events so items behind it stay clickable.
// A narrow layout is already compact, so reserve that shorter height.
if (window.matchMedia("(max-width: 680px)").matches) {
const saved = topbar.style.minHeight;
topbar.style.minHeight = "0";
const height = topbar.offsetHeight;
topbar.style.minHeight = saved;
topbar.style.setProperty("--topbar-height", height + "px");
return;
}
if (!compact) topbar.style.setProperty("--topbar-height", topbar.offsetHeight + "px");
}
function updateCompact() {
if (!topbar) return;
const next = window.scrollY > 40 && !window.matchMedia("(max-width: 680px)").matches;
if (next === compact) return;
compact = next;
topbar.classList.toggle("is-compact", compact);
if (searchForm) searchForm.classList.toggle("is-compact", compact);
updateCategoryScroll();
}
if (categoryRow) {
for (const caret of scroller.querySelectorAll(".caret")) {
let timer = 0;
const step = () => { categoryRow.scrollLeft += Number(caret.dataset.dir) * 8; };
const stop = () => { clearInterval(timer); timer = 0; };
caret.addEventListener("pointerdown", (event) => {
event.preventDefault();
step();
stop();
timer = setInterval(step, 16);
});
caret.addEventListener("pointerup", stop);
caret.addEventListener("pointerleave", stop);
caret.addEventListener("pointercancel", stop);
}
categoryRow.addEventListener("scroll", updateCategoryScroll, {passive: true});
window.addEventListener("resize", () => {
reserveTopbar();
updateCategoryScroll();
});
updateCategoryScroll();
}
window.addEventListener("scroll", updateCompact, {passive: true});
reserveTopbar();
updateCompact();