Files
Wilson Freitas 3c4fe97145 Fix tag cloud filtering: add event listener to tag cloud tags
The tag cloud buttons were not triggering filters because the event
listener was only attached to the table body. Refactored tag click
handling into a shared function and added a separate listener for the
tag cloud outside the table.

Now clicking any tag (table or cloud) applies the filter correctly.
2026-03-28 13:05:55 -03:00

328 lines
8.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* awesome-quant search, filter, sort, expand */
(function () {
"use strict";
const $ = (sel, ctx = document) => ctx.querySelector(sel);
const $$ = (sel, ctx = document) => [...ctx.querySelectorAll(sel)];
const searchInput = $("#search");
const filterBar = $("#filter-bar");
const filterValue = $("#filter-value");
const filterClear = $("#filter-clear");
const noResults = $("#no-results");
const resultsCount = $("#results-count");
const tableBody = $("tbody", $("#project-table"));
const sortHeaders = $$("th[data-sort]");
let activeFilter = { type: "", value: "" };
let currentSort = { key: "", dir: "" };
// ===== Theme =====
const themeToggle = $(".theme-toggle");
function getPreferredTheme() {
const stored = localStorage.getItem("theme");
if (stored) return stored;
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
function applyTheme(theme) {
document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem("theme", theme);
}
applyTheme(getPreferredTheme());
themeToggle.addEventListener("click", () => {
const current = document.documentElement.getAttribute("data-theme");
applyTheme(current === "dark" ? "light" : "dark");
});
// ===== Helpers =====
function getRows() {
return $$(".row", tableBody);
}
function getExpandRow(row) {
return row.nextElementSibling;
}
function collapseAll() {
for (const row of getRows()) {
row.classList.remove("expanded");
const expand = getExpandRow(row);
if (expand) expand.hidden = true;
}
}
// ===== Search & Filter =====
let searchTimeout;
function applyFilters() {
const query = searchInput.value.trim().toLowerCase();
let visible = 0;
collapseAll();
for (const row of getRows()) {
const expand = getExpandRow(row);
const text = (
row.textContent +
" " +
(expand ? expand.textContent : "")
).toLowerCase();
const languages = (row.dataset.languages || "").split(" ");
const category = row.dataset.category || "";
const sources = row.dataset.sources || "";
let show = true;
// Search
if (query && !text.includes(query)) show = false;
// Tag filter
if (show && activeFilter.value) {
const ft = activeFilter.type;
const fv = activeFilter.value;
if (ft === "language" && !languages.includes(fv)) show = false;
if (ft === "category" && category !== fv) show = false;
if (ft === "source" && !sources.split(" ").includes(fv)) show = false;
}
row.hidden = !show;
if (expand) expand.hidden = true;
if (show) {
visible++;
const numCell = $(".col-num", row);
if (numCell) numCell.textContent = visible;
}
}
noResults.hidden = visible > 0;
resultsCount.textContent =
query || activeFilter.value
? `Showing ${visible} project${visible !== 1 ? "s" : ""}`
: "";
// Sync filter bar
if (activeFilter.value) {
filterValue.textContent = activeFilter.value;
filterBar.style.display = "flex";
} else {
filterBar.style.display = "none";
}
syncURL();
}
searchInput.addEventListener("input", () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(applyFilters, 120);
});
filterClear.addEventListener("click", () => {
activeFilter = { type: "", value: "" };
applyFilters();
});
// ===== Tag Click Handler =====
function handleTagClick(e) {
const tag = e.target.closest(".tag");
if (tag) {
e.stopPropagation();
const type = tag.dataset.filterType;
const value = tag.dataset.filterValue;
// Toggle off if same filter
if (activeFilter.type === type && activeFilter.value === value) {
activeFilter = { type: "", value: "" };
} else {
activeFilter = { type, value };
}
applyFilters();
return;
}
}
// Listen for tag clicks on table rows
tableBody.addEventListener("click", handleTagClick);
// Listen for tag clicks on tag cloud (outside table)
const tagCloud = $(".tag-cloud");
if (tagCloud) {
tagCloud.addEventListener("click", handleTagClick);
}
// ===== Row Expand =====
tableBody.addEventListener("click", (e) => {
if (e.target.closest(".tag") || e.target.closest("a")) return;
const row = e.target.closest(".row");
if (!row) return;
const expand = getExpandRow(row);
if (!expand) return;
const isExpanded = row.classList.contains("expanded");
for (const r of getRows()) {
if (r !== row) {
r.classList.remove("expanded");
const ex = getExpandRow(r);
if (ex) ex.hidden = true;
}
}
if (isExpanded) {
row.classList.remove("expanded");
expand.hidden = true;
} else {
row.classList.add("expanded");
expand.hidden = false;
}
});
tableBody.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
const row = e.target.closest(".row");
if (row) {
e.preventDefault();
row.click();
}
}
});
// ===== Sort =====
function getSortValue(row, key) {
if (key === "name") {
return ($(".col-name a", row)?.textContent || "").toLowerCase();
}
if (key === "stars") {
return parseInt(row.dataset.stars || "0", 10);
}
if (key === "update") {
return ($(".last-update", row)?.textContent || "").trim();
}
return "";
}
function doSort(key, dir) {
const rows = getRows();
const pairs = rows.map((r) => [r, getExpandRow(r)]);
if (!dir) {
pairs.sort((a, b) => {
const ai = parseInt(a[0].dataset.originalIndex || "0");
const bi = parseInt(b[0].dataset.originalIndex || "0");
return ai - bi;
});
} else {
pairs.sort((a, b) => {
const va = getSortValue(a[0], key);
const vb = getSortValue(b[0], key);
let cmp;
if (typeof va === "number" && typeof vb === "number") {
cmp = va - vb;
} else {
cmp = String(va).localeCompare(String(vb));
}
return dir === "asc" ? cmp : -cmp;
});
}
for (const [row, expand] of pairs) {
tableBody.appendChild(row);
if (expand) tableBody.appendChild(expand);
}
applyFilters();
}
for (const th of sortHeaders) {
th.addEventListener("click", () => {
const key = th.dataset.sort;
let nextDir;
if (currentSort.key !== key) {
nextDir = key === "name" ? "asc" : "desc";
} else if (currentSort.dir === "asc") {
nextDir = "desc";
} else if (currentSort.dir === "desc") {
nextDir = key === "name" ? "" : "asc";
} else {
nextDir = key === "name" ? "asc" : "desc";
}
for (const h of sortHeaders) {
h.classList.remove("asc", "desc");
}
if (nextDir) {
th.classList.add(nextDir);
}
currentSort = { key: nextDir ? key : "", dir: nextDir };
doSort(key, nextDir);
});
}
// Store original indices
getRows().forEach((r, i) => (r.dataset.originalIndex = i));
// ===== Keyboard Shortcuts =====
document.addEventListener("keydown", (e) => {
if (e.key === "/" && !e.ctrlKey && !e.metaKey) {
const active = document.activeElement;
if (
active &&
(active.tagName === "INPUT" ||
active.tagName === "SELECT" ||
active.tagName === "TEXTAREA")
)
return;
e.preventDefault();
searchInput.focus();
}
if (e.key === "Escape") {
if (document.activeElement === searchInput) {
if (searchInput.value) {
searchInput.value = "";
applyFilters();
} else {
searchInput.blur();
}
}
}
});
// ===== URL State =====
function syncURL() {
const params = new URLSearchParams();
if (searchInput.value) params.set("q", searchInput.value);
if (activeFilter.value) {
params.set("filter_type", activeFilter.type);
params.set("filter", activeFilter.value);
}
const qs = params.toString();
const url = qs ? `?${qs}` : location.pathname;
history.replaceState(null, "", url);
}
function restoreURL() {
const params = new URLSearchParams(location.search);
if (params.has("q")) searchInput.value = params.get("q");
if (params.has("filter")) {
activeFilter = {
type: params.get("filter_type") || "category",
value: params.get("filter"),
};
}
if (params.toString()) applyFilters();
}
restoreURL();
})();