diff --git a/website/static/main.js b/website/static/main.js index da58fe2..f1150b0 100644 --- a/website/static/main.js +++ b/website/static/main.js @@ -1,40 +1,43 @@ // State -var activeFilter = null; // { type: "cat"|"group", value: "..." } -var activeSort = { col: 'stars', order: 'desc' }; -var searchInput = document.querySelector('.search'); -var filterBar = document.querySelector('.filter-bar'); -var filterValue = document.querySelector('.filter-value'); -var filterClear = document.querySelector('.filter-clear'); -var noResults = document.querySelector('.no-results'); -var rows = document.querySelectorAll('.table tbody tr.row'); -var tags = document.querySelectorAll('.tag'); -var tbody = document.querySelector('.table tbody'); +let activeFilter = null; // { type: "cat"|"group", value: "..." } +let activeSort = { col: "stars", order: "desc" }; +const searchInput = document.querySelector(".search"); +const filterBar = document.querySelector(".filter-bar"); +const filterValue = document.querySelector(".filter-value"); +const filterClear = document.querySelector(".filter-clear"); +const noResults = document.querySelector(".no-results"); +const rows = document.querySelectorAll(".table tbody tr.row"); +const tags = document.querySelectorAll(".tag"); +const tbody = document.querySelector(".table tbody"); function initRevealSections() { - var sections = document.querySelectorAll('[data-reveal]'); + const sections = document.querySelectorAll("[data-reveal]"); if (!sections.length) return; - if (!('IntersectionObserver' in window)) { + if (!("IntersectionObserver" in window)) { sections.forEach(function (section) { - section.classList.add('is-visible'); + section.classList.add("is-visible"); }); return; } - var 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', - }); + 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'; + section.classList.add("will-reveal"); + section.style.transitionDelay = Math.min(index * 70, 180) + "ms"; observer.observe(section); }); } @@ -43,34 +46,36 @@ initRevealSections(); // Pause hero animations when scrolled out of view (function () { - var hero = document.querySelector('.hero'); - if (!hero || !('IntersectionObserver' in window)) return; - var observer = new IntersectionObserver(function (entries) { - hero.classList.toggle('offscreen', !entries[0].isIntersecting); + 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); })(); // Relative time formatting function relativeTime(isoStr) { - var date = new Date(isoStr); - var now = new Date(); - var diffMs = now - date; - var diffHours = Math.floor(diffMs / 3600000); - var 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'; - var diffMonths = Math.floor(diffDays / 30); - if (diffMonths < 12) return diffMonths === 1 ? '1 month ago' : diffMonths + ' months ago'; - var diffYears = Math.floor(diffDays / 365); - return diffYears === 1 ? '1 year ago' : diffYears + ' years ago'; + 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"; } // Format all commit date cells -document.querySelectorAll('.col-commit[data-commit]').forEach(function (td) { - var time = td.querySelector('time'); +document.querySelectorAll(".col-commit[data-commit]").forEach(function (td) { + const time = td.querySelector("time"); if (time) time.textContent = relativeTime(td.dataset.commit); }); @@ -81,36 +86,37 @@ rows.forEach(function (row, i) { }); function collapseAll() { - var openRows = document.querySelectorAll('.table tbody tr.row.open'); + const openRows = document.querySelectorAll(".table tbody tr.row.open"); openRows.forEach(function (row) { - row.classList.remove('open'); - row.setAttribute('aria-expanded', 'false'); + row.classList.remove("open"); + row.setAttribute("aria-expanded", "false"); }); } function applyFilters() { - var query = searchInput ? searchInput.value.toLowerCase().trim() : ''; - var visibleCount = 0; + const query = searchInput ? searchInput.value.toLowerCase().trim() : ""; + let visibleCount = 0; // Collapse all expanded rows on filter/search change collapseAll(); rows.forEach(function (row) { - var show = true; + let show = true; // Category/group filter if (activeFilter) { - var attr = activeFilter.type === 'cat' ? row.dataset.cats : row.dataset.groups; - show = attr ? attr.split('||').indexOf(activeFilter.value) !== -1 : false; + const attr = + activeFilter.type === "cat" ? row.dataset.cats : row.dataset.groups; + show = attr ? attr.split("||").indexOf(activeFilter.value) !== -1 : false; } // Text search if (show && query) { if (!row._searchText) { - var text = row.textContent.toLowerCase(); - var next = row.nextElementSibling; - if (next && next.classList.contains('expand-row')) { - text += ' ' + next.textContent.toLowerCase(); + let text = row.textContent.toLowerCase(); + const next = row.nextElementSibling; + if (next && next.classList.contains("expand-row")) { + text += " " + next.textContent.toLowerCase(); } row._searchText = text; } @@ -121,7 +127,7 @@ function applyFilters() { if (show) { visibleCount++; - var numCell = row.cells[0]; + const numCell = row.cells[0]; if (numCell.textContent !== String(visibleCount)) { numCell.textContent = String(visibleCount); } @@ -132,19 +138,20 @@ function applyFilters() { // Update tag highlights tags.forEach(function (tag) { - var isActive = activeFilter - && tag.dataset.type === activeFilter.type - && tag.dataset.value === activeFilter.value; - tag.classList.toggle('active', isActive); + const isActive = + activeFilter && + tag.dataset.type === activeFilter.type && + tag.dataset.value === activeFilter.value; + tag.classList.toggle("active", isActive); }); // Filter bar if (filterBar) { if (activeFilter) { - filterBar.classList.add('visible'); + filterBar.classList.add("visible"); if (filterValue) filterValue.textContent = activeFilter.value; } else { - filterBar.classList.remove('visible'); + filterBar.classList.remove("visible"); } } @@ -152,40 +159,46 @@ function applyFilters() { } function updateURL() { - var params = new URLSearchParams(); - var query = searchInput ? searchInput.value.trim() : ''; - if (query) params.set('q', query); + const params = new URLSearchParams(); + const query = searchInput ? searchInput.value.trim() : ""; + if (query) params.set("q", query); if (activeFilter) { - params.set(activeFilter.type === 'cat' ? 'category' : 'group', activeFilter.value); + params.set( + activeFilter.type === "cat" ? "category" : "group", + activeFilter.value, + ); } - if (activeSort.col !== 'stars' || activeSort.order !== 'desc') { - params.set('sort', activeSort.col); - params.set('order', activeSort.order); + if (activeSort.col !== "stars" || activeSort.order !== "desc") { + params.set("sort", activeSort.col); + params.set("order", activeSort.order); } - var qs = params.toString(); - history.replaceState(null, '', qs ? '?' + qs : location.pathname); + const qs = params.toString(); + history.replaceState(null, "", qs ? "?" + qs : location.pathname); } function getSortValue(row, col) { - if (col === 'name') { - return row.querySelector('.col-name a').textContent.trim().toLowerCase(); + if (col === "name") { + return row.querySelector(".col-name a").textContent.trim().toLowerCase(); } - if (col === 'stars') { - var text = row.querySelector('.col-stars').textContent.trim().replace(/,/g, ''); - var num = parseInt(text, 10); + 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 === 'commit-time') { - var attr = row.querySelector('.col-commit').getAttribute('data-commit'); + if (col === "commit-time") { + const attr = row.querySelector(".col-commit").getAttribute("data-commit"); return attr ? new Date(attr).getTime() : 0; } return 0; } function sortRows() { - var arr = Array.prototype.slice.call(rows); - var col = activeSort.col; - var order = activeSort.order; + 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) { @@ -193,22 +206,22 @@ function sortRows() { }); arr.sort(function (a, b) { - var aVal = a._sortVal; - var bVal = b._sortVal; - if (col === 'name') { - var cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0; + 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; + 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; - var cmp = aVal - bVal; + const cmp = aVal - bVal; if (cmp === 0) return a._origIndex - b._origIndex; - return order === 'desc' ? -cmp : cmp; + return order === "desc" ? -cmp : cmp; }); - var frag = document.createDocumentFragment(); + const frag = document.createDocumentFragment(); arr.forEach(function (row) { frag.appendChild(row); frag.appendChild(row._expandRow); @@ -218,40 +231,43 @@ function sortRows() { } function updateSortIndicators() { - document.querySelectorAll('th[data-sort]').forEach(function (th) { - th.classList.remove('sort-asc', 'sort-desc'); + document.querySelectorAll("th[data-sort]").forEach(function (th) { + th.classList.remove("sort-asc", "sort-desc"); if (activeSort && th.dataset.sort === activeSort.col) { - th.classList.add('sort-' + activeSort.order); - th.setAttribute('aria-sort', activeSort.order === 'asc' ? 'ascending' : 'descending'); + th.classList.add("sort-" + activeSort.order); + th.setAttribute( + "aria-sort", + activeSort.order === "asc" ? "ascending" : "descending", + ); } else { - th.removeAttribute('aria-sort'); + th.removeAttribute("aria-sort"); } }); } // Expand/collapse: event delegation on tbody if (tbody) { - tbody.addEventListener('click', function (e) { + tbody.addEventListener("click", function (e) { // Don't toggle if clicking a link or tag button - if (e.target.closest('a') || e.target.closest('.tag')) return; + if (e.target.closest("a") || e.target.closest(".tag")) return; - var row = e.target.closest('tr.row'); + const row = e.target.closest("tr.row"); if (!row) return; - var isOpen = row.classList.contains('open'); + const isOpen = row.classList.contains("open"); if (isOpen) { - row.classList.remove('open'); - row.setAttribute('aria-expanded', 'false'); + row.classList.remove("open"); + row.setAttribute("aria-expanded", "false"); } else { - row.classList.add('open'); - row.setAttribute('aria-expanded', 'true'); + 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; - var row = e.target.closest('tr.row'); + 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(); @@ -260,13 +276,17 @@ if (tbody) { // Tag click: filter by category or group tags.forEach(function (tag) { - tag.addEventListener('click', function (e) { + tag.addEventListener("click", function (e) { e.preventDefault(); - var type = tag.dataset.type; - var value = tag.dataset.value; + const type = tag.dataset.type; + const value = tag.dataset.value; // Toggle: click same filter again to clear - if (activeFilter && activeFilter.type === type && activeFilter.value === value) { + if ( + activeFilter && + activeFilter.type === type && + activeFilter.value === value + ) { activeFilter = null; } else { activeFilter = { type: type, value: value }; @@ -277,31 +297,32 @@ tags.forEach(function (tag) { // Clear filter if (filterClear) { - filterClear.addEventListener('click', function () { + filterClear.addEventListener("click", function () { activeFilter = null; applyFilters(); }); } // No-results clear -var noResultsClear = document.querySelector('.no-results-clear'); +const noResultsClear = document.querySelector(".no-results-clear"); if (noResultsClear) { - noResultsClear.addEventListener('click', function () { - if (searchInput) searchInput.value = ''; + noResultsClear.addEventListener("click", function () { + if (searchInput) searchInput.value = ""; activeFilter = null; applyFilters(); }); } // Column sorting -document.querySelectorAll('th[data-sort]').forEach(function (th) { - th.addEventListener('click', function () { - var col = th.dataset.sort; - var defaultOrder = col === 'name' ? 'asc' : 'desc'; - var altOrder = defaultOrder === 'asc' ? 'desc' : 'asc'; +document.querySelectorAll("th[data-sort]").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 && activeSort.col === col) { - if (activeSort.order === defaultOrder) activeSort = { col: col, order: altOrder }; - else activeSort = { col: 'stars', order: 'desc' }; + if (activeSort.order === defaultOrder) + activeSort = { col: col, order: altOrder }; + else activeSort = { col: "stars", order: "desc" }; } else { activeSort = { col: col, order: defaultOrder }; } @@ -312,20 +333,27 @@ document.querySelectorAll('th[data-sort]').forEach(function (th) { // Search input if (searchInput) { - var searchTimer; - searchInput.addEventListener('input', function () { + let searchTimer; + searchInput.addEventListener("input", function () { clearTimeout(searchTimer); searchTimer = setTimeout(applyFilters, 150); }); // Keyboard shortcuts - document.addEventListener('keydown', function (e) { - if (e.key === '/' && !['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName) && !e.ctrlKey && !e.metaKey) { + 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 = ''; + if (e.key === "Escape" && document.activeElement === searchInput) { + searchInput.value = ""; activeFilter = null; applyFilters(); searchInput.blur(); @@ -334,24 +362,24 @@ if (searchInput) { } // Back to top -var backToTop = document.querySelector('.back-to-top'); -var resultsSection = document.querySelector('#library-index'); -var tableWrap = document.querySelector('.table-wrap'); -var stickyHeaderCell = backToTop ? backToTop.closest('th') : null; +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; - var tableRect = tableWrap.getBoundingClientRect(); - var headRect = stickyHeaderCell.getBoundingClientRect(); - var hasPassedHeader = tableRect.top <= 0 && headRect.bottom > 0; + const tableRect = tableWrap.getBoundingClientRect(); + const headRect = stickyHeaderCell.getBoundingClientRect(); + const hasPassedHeader = tableRect.top <= 0 && headRect.bottom > 0; - backToTop.classList.toggle('visible', hasPassedHeader); + backToTop.classList.toggle("visible", hasPassedHeader); } if (backToTop) { - var scrollTicking = false; - window.addEventListener('scroll', function () { + let scrollTicking = false; + window.addEventListener("scroll", function () { if (!scrollTicking) { requestAnimationFrame(function () { updateBackToTopVisibility(); @@ -361,12 +389,12 @@ if (backToTop) { } }); - window.addEventListener('resize', updateBackToTopVisibility); + window.addEventListener("resize", updateBackToTopVisibility); - backToTop.addEventListener('click', function () { - var target = searchInput || resultsSection; + backToTop.addEventListener("click", function () { + const target = searchInput || resultsSection; if (!target) return; - target.scrollIntoView({ behavior: 'smooth', block: 'center' }); + target.scrollIntoView({ behavior: "smooth", block: "center" }); if (searchInput) searchInput.focus(); }); @@ -375,16 +403,19 @@ if (backToTop) { // Restore state from URL (function () { - var params = new URLSearchParams(location.search); - var q = params.get('q'); - var cat = params.get('category'); - var group = params.get('group'); - var sort = params.get('sort'); - var order = params.get('order'); + const params = new URLSearchParams(location.search); + const q = params.get("q"); + const cat = params.get("category"); + const group = params.get("group"); + const sort = params.get("sort"); + const order = params.get("order"); if (q && searchInput) searchInput.value = q; - if (cat) activeFilter = { type: 'cat', value: cat }; - else if (group) activeFilter = { type: 'group', value: group }; - if ((sort === 'name' || sort === 'stars' || sort === 'commit-time') && (order === 'desc' || order === 'asc')) { + if (cat) activeFilter = { type: "cat", value: cat }; + else if (group) activeFilter = { type: "group", value: group }; + if ( + (sort === "name" || sort === "stars" || sort === "commit-time") && + (order === "desc" || order === "asc") + ) { activeSort = { col: sort, order: order }; } if (q || cat || group || sort) {