Files
awesome-quant/static/main.js
T

328 lines
8.5 KiB
JavaScript
Raw Normal View History

2026-03-22 21:14:50 +00:00
/* 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();
2026-03-28 16:21:50 +00:00
const languages = (row.dataset.languages || "").split(" ");
2026-03-22 21:14:50 +00:00
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;
2026-03-28 16:21:50 +00:00
if (ft === "language" && !languages.includes(fv)) show = false;
2026-03-22 21:14:50 +00:00
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();
});
2026-03-28 16:21:50 +00:00
// ===== Tag Click Handler =====
function handleTagClick(e) {
2026-03-22 21:14:50 +00:00
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;
}
2026-03-28 16:21:50 +00:00
}
// 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);
}
2026-03-22 21:14:50 +00:00
// ===== 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();
})();