mirror of
https://github.com/vinta/awesome-python.git
synced 2026-10-02 08:23:10 +08:00
On section and subcategory pages, every row repeated its use case and current page as tags (and as the label under the name on phones), right under a heading that already said the same thing. They are now hidden in editorial order and shown again when a column sort flattens the groups. Co-Authored-By: Claude <noreply@anthropic.com>
472 lines
14 KiB
JavaScript
472 lines
14 KiB
JavaScript
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
|
|
function getScrollBehavior() {
|
|
return reducedMotion.matches ? "auto" : "smooth";
|
|
}
|
|
|
|
const table = document.querySelector(".table");
|
|
// Category pages list rows in editorial (README) order until a column is sorted
|
|
const defaultSort =
|
|
table && table.dataset.defaultSort === "editorial"
|
|
? { col: "editorial", order: "asc" }
|
|
: { col: "downloads", order: "desc" };
|
|
let activeSort = defaultSort;
|
|
const searchInput = document.querySelector(".search");
|
|
const noResults = document.querySelector(".no-results");
|
|
const rows = document.querySelectorAll(".table tbody tr.row");
|
|
const tbody = document.querySelector(".table tbody");
|
|
const groupRows = document.querySelectorAll(".table tbody tr.group-row");
|
|
|
|
function initRevealSections() {
|
|
const sections = document.querySelectorAll("[data-reveal]");
|
|
if (!sections.length) return;
|
|
|
|
if (!("IntersectionObserver" in window)) {
|
|
sections.forEach(function (section) {
|
|
section.classList.add("is-visible");
|
|
});
|
|
return;
|
|
}
|
|
|
|
const observer = new IntersectionObserver(
|
|
function (entries) {
|
|
entries.forEach(function (entry) {
|
|
if (!entry.isIntersecting) return;
|
|
entry.target.classList.add("is-visible");
|
|
observer.unobserve(entry.target);
|
|
});
|
|
},
|
|
{
|
|
threshold: 0.12,
|
|
rootMargin: "0px 0px -8% 0px",
|
|
},
|
|
);
|
|
|
|
sections.forEach(function (section, index) {
|
|
section.classList.add("will-reveal");
|
|
section.style.transitionDelay = Math.min(index * 70, 180) + "ms";
|
|
observer.observe(section);
|
|
});
|
|
}
|
|
|
|
initRevealSections();
|
|
|
|
// Smooth scroll without hash in URL
|
|
document.querySelectorAll("[data-scroll-to]").forEach(function (link) {
|
|
link.addEventListener("click", function (e) {
|
|
const el = document.getElementById(link.dataset.scrollTo);
|
|
if (!el) return;
|
|
e.preventDefault();
|
|
el.scrollIntoView({ behavior: getScrollBehavior() });
|
|
});
|
|
});
|
|
|
|
// Land at #library-index without leaving the hash in the URL
|
|
if (window.location.hash === "#library-index") {
|
|
const target = document.getElementById("library-index");
|
|
if (target) {
|
|
target.scrollIntoView();
|
|
}
|
|
history.replaceState(
|
|
null,
|
|
"",
|
|
window.location.pathname + window.location.search,
|
|
);
|
|
}
|
|
|
|
// Pause hero animations when scrolled out of view
|
|
(function () {
|
|
const hero = document.querySelector(".hero");
|
|
if (!hero || !("IntersectionObserver" in window)) return;
|
|
const observer = new IntersectionObserver(function (entries) {
|
|
hero.classList.toggle("offscreen", !entries[0].isIntersecting);
|
|
});
|
|
observer.observe(hero);
|
|
})();
|
|
|
|
function relativeTime(isoStr) {
|
|
const date = new Date(isoStr);
|
|
const now = new Date();
|
|
const diffMs = now - date;
|
|
const diffHours = Math.floor(diffMs / 3600000);
|
|
const diffDays = Math.floor(diffMs / 86400000);
|
|
if (diffHours < 1) return "just now";
|
|
if (diffHours < 24)
|
|
return diffHours === 1 ? "1 hour ago" : diffHours + " hours ago";
|
|
if (diffDays === 1) return "yesterday";
|
|
if (diffDays < 30) return diffDays + " days ago";
|
|
const diffMonths = Math.floor(diffDays / 30);
|
|
if (diffMonths < 12)
|
|
return diffMonths === 1 ? "1 month ago" : diffMonths + " months ago";
|
|
const diffYears = Math.floor(diffDays / 365);
|
|
return diffYears === 1 ? "1 year ago" : diffYears + " years ago";
|
|
}
|
|
|
|
document.querySelectorAll(".col-commit[data-commit]").forEach(function (td) {
|
|
const time = td.querySelector("time");
|
|
if (time) time.textContent = relativeTime(td.dataset.commit);
|
|
});
|
|
|
|
document
|
|
.querySelectorAll(".expand-commit time[datetime]")
|
|
.forEach(function (time) {
|
|
time.textContent = relativeTime(time.getAttribute("datetime"));
|
|
});
|
|
|
|
let currentGroupRow = null;
|
|
Array.prototype.forEach.call(tbody ? tbody.rows : [], function (tr) {
|
|
if (tr.classList.contains("group-row")) currentGroupRow = tr;
|
|
else if (tr.classList.contains("row")) tr._groupRow = currentGroupRow;
|
|
});
|
|
|
|
rows.forEach(function (row, i) {
|
|
row._origIndex = i;
|
|
let next = row.nextElementSibling;
|
|
if (next && next.classList.contains("desc-row")) {
|
|
row._descRow = next;
|
|
next = next.nextElementSibling;
|
|
}
|
|
row._expandRow = next;
|
|
});
|
|
|
|
function collapseAll() {
|
|
if (!tbody) return;
|
|
const openRows = tbody.querySelectorAll("tr.row.open");
|
|
openRows.forEach(function (row) {
|
|
row.classList.remove("open");
|
|
row.setAttribute("aria-expanded", "false");
|
|
});
|
|
}
|
|
|
|
function applyFilters() {
|
|
const query = searchInput ? searchInput.value.toLowerCase().trim() : "";
|
|
const descRowsVisible = !isIndexDocument;
|
|
let visibleCount = 0;
|
|
|
|
collapseAll();
|
|
|
|
rows.forEach(function (row) {
|
|
let show = true;
|
|
|
|
if (query) {
|
|
if (!row._searchText) {
|
|
let text = row.textContent.toLowerCase();
|
|
if (row._descRow) {
|
|
text += " " + row._descRow.textContent.toLowerCase();
|
|
}
|
|
if (row._expandRow) {
|
|
text += " " + row._expandRow.textContent.toLowerCase();
|
|
}
|
|
row._searchText = text;
|
|
}
|
|
show = row._searchText.includes(query);
|
|
}
|
|
|
|
if (row.hidden !== !show) row.hidden = !show;
|
|
if (row._descRow) {
|
|
const descHidden = !show || !descRowsVisible;
|
|
if (row._descRow.hidden !== descHidden) {
|
|
row._descRow.hidden = descHidden;
|
|
}
|
|
}
|
|
|
|
if (show) {
|
|
visibleCount++;
|
|
const numCell = row.cells[0];
|
|
if (numCell.textContent !== String(visibleCount)) {
|
|
numCell.textContent = String(visibleCount);
|
|
}
|
|
}
|
|
});
|
|
|
|
groupRows.forEach(function (groupRow) {
|
|
groupRow.hidden = true;
|
|
});
|
|
if (activeSort.col === "editorial") {
|
|
rows.forEach(function (row) {
|
|
if (!row.hidden && row._groupRow) row._groupRow.hidden = false;
|
|
});
|
|
}
|
|
|
|
if (noResults) noResults.hidden = visibleCount > 0;
|
|
|
|
updateURL();
|
|
}
|
|
|
|
const isIndexDocument =
|
|
location.pathname === "/" || location.pathname === "/index.html";
|
|
|
|
function buildQueryString() {
|
|
const params = new URLSearchParams();
|
|
const query = searchInput ? searchInput.value.trim() : "";
|
|
if (query) params.set("q", query);
|
|
if (activeSort.col !== defaultSort.col || activeSort.order !== defaultSort.order) {
|
|
params.set("sort", activeSort.col);
|
|
params.set("order", activeSort.order);
|
|
}
|
|
const qs = params.toString();
|
|
return qs ? "?" + qs : "";
|
|
}
|
|
|
|
function updateURL() {
|
|
if (!isIndexDocument) return;
|
|
history.replaceState(null, "", "/" + buildQueryString());
|
|
}
|
|
|
|
function getSortValue(row, col) {
|
|
// +1 keeps the first row above the "no value" cutoff in sortRows
|
|
if (col === "editorial") return row._origIndex + 1;
|
|
if (col === "name") {
|
|
return row.querySelector(".col-name a").textContent.trim().toLowerCase();
|
|
}
|
|
if (col === "stars") {
|
|
const text = row
|
|
.querySelector(".col-stars")
|
|
.textContent.trim()
|
|
.replace(/,/g, "");
|
|
const num = parseInt(text, 10);
|
|
return isNaN(num) ? -1 : num;
|
|
}
|
|
if (col === "downloads") {
|
|
const text = row
|
|
.querySelector(".col-downloads")
|
|
.textContent.trim()
|
|
.replace(/,/g, "");
|
|
const num = parseInt(text, 10);
|
|
return isNaN(num) ? -1 : num;
|
|
}
|
|
if (col === "commit-time") {
|
|
const attr = row.querySelector(".col-commit").getAttribute("data-commit");
|
|
return attr ? new Date(attr).getTime() : 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function sortRows() {
|
|
if (!tbody) return;
|
|
|
|
const arr = Array.prototype.slice.call(rows);
|
|
const col = activeSort.col;
|
|
const order = activeSort.order;
|
|
|
|
// Cache sort values once to avoid DOM queries per comparison
|
|
arr.forEach(function (row) {
|
|
row._sortVal = getSortValue(row, col);
|
|
});
|
|
|
|
arr.sort(function (a, b) {
|
|
const aVal = a._sortVal;
|
|
const bVal = b._sortVal;
|
|
if (col === "name") {
|
|
const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
if (cmp === 0) return a._origIndex - b._origIndex;
|
|
return order === "desc" ? -cmp : cmp;
|
|
}
|
|
if (aVal <= 0 && bVal <= 0) return a._origIndex - b._origIndex;
|
|
if (aVal <= 0) return 1;
|
|
if (bVal <= 0) return -1;
|
|
const cmp = aVal - bVal;
|
|
if (cmp === 0) return a._origIndex - b._origIndex;
|
|
return order === "desc" ? -cmp : cmp;
|
|
});
|
|
|
|
const frag = document.createDocumentFragment();
|
|
let lastGroupRow = null;
|
|
arr.forEach(function (row) {
|
|
if (col === "editorial" && row._groupRow && row._groupRow !== lastGroupRow) {
|
|
frag.appendChild(row._groupRow);
|
|
lastGroupRow = row._groupRow;
|
|
}
|
|
frag.appendChild(row);
|
|
if (row._descRow) frag.appendChild(row._descRow);
|
|
if (row._expandRow) frag.appendChild(row._expandRow);
|
|
});
|
|
tbody.appendChild(frag);
|
|
applyFilters();
|
|
}
|
|
|
|
const sortHeaders = document.querySelectorAll("th[data-sort]");
|
|
|
|
function updateSortIndicators() {
|
|
if (table) table.classList.toggle("sorted", activeSort.col !== "editorial");
|
|
sortHeaders.forEach(function (th) {
|
|
th.classList.remove("sort-asc", "sort-desc");
|
|
if (th.dataset.sort === activeSort.col) {
|
|
th.classList.add("sort-" + activeSort.order);
|
|
th.setAttribute(
|
|
"aria-sort",
|
|
activeSort.order === "asc" ? "ascending" : "descending",
|
|
);
|
|
} else {
|
|
th.removeAttribute("aria-sort");
|
|
}
|
|
});
|
|
}
|
|
|
|
// Expand/collapse: event delegation on tbody
|
|
if (tbody) {
|
|
tbody.addEventListener("click", function (e) {
|
|
// Don't toggle if clicking a link
|
|
if (e.target.closest("a")) return;
|
|
|
|
let row = e.target.closest("tr.row");
|
|
if (!row) {
|
|
const descRow = e.target.closest("tr.desc-row");
|
|
if (descRow) row = descRow.previousElementSibling;
|
|
}
|
|
if (!row) return;
|
|
|
|
const isOpen = row.classList.contains("open");
|
|
if (isOpen) {
|
|
row.classList.remove("open");
|
|
row.setAttribute("aria-expanded", "false");
|
|
} else {
|
|
row.classList.add("open");
|
|
row.setAttribute("aria-expanded", "true");
|
|
}
|
|
});
|
|
|
|
// Keyboard: Enter or Space on focused .row toggles expand
|
|
tbody.addEventListener("keydown", function (e) {
|
|
if (e.key !== "Enter" && e.key !== " ") return;
|
|
const row = e.target.closest("tr.row");
|
|
if (!row) return;
|
|
e.preventDefault();
|
|
row.click();
|
|
});
|
|
}
|
|
|
|
const noResultsClear = document.querySelector(".no-results-clear");
|
|
if (noResultsClear) {
|
|
noResultsClear.addEventListener("click", function () {
|
|
if (!isIndexDocument) {
|
|
window.location.href = "/";
|
|
return;
|
|
}
|
|
if (searchInput) searchInput.value = "";
|
|
applyFilters();
|
|
});
|
|
}
|
|
|
|
sortHeaders.forEach(function (th) {
|
|
th.addEventListener("click", function () {
|
|
const col = th.dataset.sort;
|
|
const defaultOrder = col === "name" ? "asc" : "desc";
|
|
const altOrder = defaultOrder === "asc" ? "desc" : "asc";
|
|
if (activeSort.col === col) {
|
|
if (activeSort.order === defaultOrder)
|
|
activeSort = { col: col, order: altOrder };
|
|
else activeSort = defaultSort;
|
|
} else {
|
|
activeSort = { col: col, order: defaultOrder };
|
|
}
|
|
sortRows();
|
|
updateSortIndicators();
|
|
});
|
|
});
|
|
|
|
// Group headings are hidden while sorted flat or filtered out by search, so a link to one must restore them first
|
|
document.addEventListener("click", function (e) {
|
|
const link = e.target.closest('a[href*="#"]');
|
|
if (!link || link.pathname !== location.pathname) return;
|
|
const heading = document.getElementById(decodeURIComponent(link.hash.slice(1)));
|
|
const groupRow = heading ? heading.closest(".group-row") : null;
|
|
if (!groupRow) return;
|
|
if (activeSort.col !== "editorial") {
|
|
activeSort = defaultSort;
|
|
sortRows();
|
|
updateSortIndicators();
|
|
}
|
|
if (groupRow.hidden && searchInput) {
|
|
searchInput.value = "";
|
|
applyFilters();
|
|
}
|
|
});
|
|
|
|
if (searchInput) {
|
|
let searchTimer;
|
|
searchInput.addEventListener("input", function () {
|
|
clearTimeout(searchTimer);
|
|
searchTimer = setTimeout(applyFilters, 150);
|
|
});
|
|
|
|
document.addEventListener("keydown", function (e) {
|
|
if (
|
|
e.key === "/" &&
|
|
!["INPUT", "TEXTAREA", "SELECT"].includes(
|
|
document.activeElement.tagName,
|
|
) &&
|
|
!e.ctrlKey &&
|
|
!e.metaKey
|
|
) {
|
|
e.preventDefault();
|
|
searchInput.focus();
|
|
}
|
|
if (e.key === "Escape" && document.activeElement === searchInput) {
|
|
searchInput.value = "";
|
|
applyFilters();
|
|
searchInput.blur();
|
|
}
|
|
});
|
|
}
|
|
|
|
const backToTop = document.querySelector(".back-to-top");
|
|
const resultsSection = document.querySelector("#library-index");
|
|
const tableWrap = document.querySelector(".table-wrap");
|
|
const stickyHeaderCell = backToTop ? backToTop.closest("th") : null;
|
|
|
|
function updateBackToTopVisibility() {
|
|
if (!backToTop || !tableWrap || !stickyHeaderCell) return;
|
|
|
|
const tableRect = tableWrap.getBoundingClientRect();
|
|
const headRect = stickyHeaderCell.getBoundingClientRect();
|
|
const hasPassedHeader = tableRect.top <= 0 && headRect.bottom > 0;
|
|
|
|
backToTop.classList.toggle("visible", hasPassedHeader);
|
|
}
|
|
|
|
if (backToTop) {
|
|
let scrollTicking = false;
|
|
window.addEventListener("scroll", function () {
|
|
if (!scrollTicking) {
|
|
requestAnimationFrame(function () {
|
|
updateBackToTopVisibility();
|
|
scrollTicking = false;
|
|
});
|
|
scrollTicking = true;
|
|
}
|
|
});
|
|
|
|
window.addEventListener("resize", updateBackToTopVisibility);
|
|
|
|
backToTop.addEventListener("click", function () {
|
|
const target = searchInput || resultsSection;
|
|
if (!target) return;
|
|
target.scrollIntoView({ behavior: getScrollBehavior(), block: "center" });
|
|
if (searchInput) searchInput.focus();
|
|
});
|
|
|
|
updateBackToTopVisibility();
|
|
}
|
|
|
|
(function () {
|
|
const params = new URLSearchParams(location.search);
|
|
const q = params.get("q");
|
|
const sort = params.get("sort");
|
|
const order = params.get("order");
|
|
if (q && searchInput) searchInput.value = q;
|
|
if (
|
|
(sort === "name" ||
|
|
sort === "stars" ||
|
|
sort === "downloads" ||
|
|
sort === "commit-time") &&
|
|
(order === "desc" || order === "asc")
|
|
) {
|
|
activeSort = { col: sort, order: order };
|
|
}
|
|
if (q || sort) {
|
|
sortRows();
|
|
}
|
|
updateSortIndicators();
|
|
})();
|