mirror of
https://github.com/vinta/awesome-python.git
synced 2026-03-24 01:24:43 +08:00
The smooth scroll behavior felt sluggish on a 505-row page. Instant jump is more responsive for a utility button. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
320 lines
9.5 KiB
JavaScript
320 lines
9.5 KiB
JavaScript
// 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');
|
|
|
|
// 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';
|
|
}
|
|
|
|
// Format all commit date cells
|
|
document.querySelectorAll('.col-commit[data-commit]').forEach(function (td) {
|
|
var time = td.querySelector('time');
|
|
if (time) time.textContent = relativeTime(td.dataset.commit);
|
|
});
|
|
|
|
// Store original row order for sort reset
|
|
rows.forEach(function (row, i) {
|
|
row._origIndex = i;
|
|
row._expandRow = row.nextElementSibling;
|
|
});
|
|
|
|
function collapseAll() {
|
|
var openRows = document.querySelectorAll('.table tbody tr.row.open');
|
|
openRows.forEach(function (row) {
|
|
row.classList.remove('open');
|
|
row.setAttribute('aria-expanded', 'false');
|
|
});
|
|
}
|
|
|
|
function applyFilters() {
|
|
var query = searchInput ? searchInput.value.toLowerCase().trim() : '';
|
|
var visibleCount = 0;
|
|
|
|
// Collapse all expanded rows on filter/search change
|
|
collapseAll();
|
|
|
|
rows.forEach(function (row) {
|
|
var 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;
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
row._searchText = text;
|
|
}
|
|
show = row._searchText.includes(query);
|
|
}
|
|
|
|
if (row.hidden !== !show) row.hidden = !show;
|
|
|
|
if (show) {
|
|
visibleCount++;
|
|
var numCell = row.cells[0];
|
|
if (numCell.textContent !== String(visibleCount)) {
|
|
numCell.textContent = String(visibleCount);
|
|
}
|
|
}
|
|
});
|
|
|
|
if (noResults) noResults.hidden = visibleCount > 0;
|
|
|
|
// 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);
|
|
});
|
|
|
|
// Filter bar
|
|
if (filterBar) {
|
|
if (activeFilter) {
|
|
filterBar.classList.add('visible');
|
|
if (filterValue) filterValue.textContent = activeFilter.value;
|
|
} else {
|
|
filterBar.classList.remove('visible');
|
|
}
|
|
}
|
|
|
|
updateURL();
|
|
}
|
|
|
|
function updateURL() {
|
|
var params = new URLSearchParams();
|
|
var query = searchInput ? searchInput.value.trim() : '';
|
|
if (query) params.set('q', query);
|
|
if (activeFilter) {
|
|
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);
|
|
}
|
|
var 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 === 'stars') {
|
|
var text = row.querySelector('.col-stars').textContent.trim().replace(/,/g, '');
|
|
var num = parseInt(text, 10);
|
|
return isNaN(num) ? -1 : num;
|
|
}
|
|
if (col === 'commit-time') {
|
|
var 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;
|
|
|
|
// Cache sort values once to avoid DOM queries per comparison
|
|
arr.forEach(function (row) {
|
|
row._sortVal = getSortValue(row, col);
|
|
});
|
|
|
|
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;
|
|
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;
|
|
var cmp = aVal - bVal;
|
|
if (cmp === 0) return a._origIndex - b._origIndex;
|
|
return order === 'desc' ? -cmp : cmp;
|
|
});
|
|
|
|
var frag = document.createDocumentFragment();
|
|
arr.forEach(function (row) {
|
|
frag.appendChild(row);
|
|
frag.appendChild(row._expandRow);
|
|
});
|
|
tbody.appendChild(frag);
|
|
applyFilters();
|
|
}
|
|
|
|
function updateSortIndicators() {
|
|
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);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Expand/collapse: event delegation on tbody
|
|
if (tbody) {
|
|
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;
|
|
|
|
var row = e.target.closest('tr.row');
|
|
if (!row) return;
|
|
|
|
var 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;
|
|
var row = e.target.closest('tr.row');
|
|
if (!row) return;
|
|
e.preventDefault();
|
|
row.click();
|
|
});
|
|
}
|
|
|
|
// Tag click: filter by category or group
|
|
tags.forEach(function (tag) {
|
|
tag.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
var type = tag.dataset.type;
|
|
var value = tag.dataset.value;
|
|
|
|
// Toggle: click same filter again to clear
|
|
if (activeFilter && activeFilter.type === type && activeFilter.value === value) {
|
|
activeFilter = null;
|
|
} else {
|
|
activeFilter = { type: type, value: value };
|
|
}
|
|
applyFilters();
|
|
});
|
|
});
|
|
|
|
// Clear filter
|
|
if (filterClear) {
|
|
filterClear.addEventListener('click', function () {
|
|
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';
|
|
if (activeSort && activeSort.col === col) {
|
|
if (activeSort.order === defaultOrder) activeSort = { col: col, order: altOrder };
|
|
else activeSort = { col: 'stars', order: 'desc' };
|
|
} else {
|
|
activeSort = { col: col, order: defaultOrder };
|
|
}
|
|
sortRows();
|
|
updateSortIndicators();
|
|
});
|
|
});
|
|
|
|
// Search input
|
|
if (searchInput) {
|
|
var 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) {
|
|
e.preventDefault();
|
|
searchInput.focus();
|
|
}
|
|
if (e.key === 'Escape' && document.activeElement === searchInput) {
|
|
searchInput.value = '';
|
|
activeFilter = null;
|
|
applyFilters();
|
|
searchInput.blur();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Back to top
|
|
var backToTop = document.querySelector('.back-to-top');
|
|
if (backToTop) {
|
|
backToTop.hidden = false;
|
|
var scrollTicking = false;
|
|
window.addEventListener('scroll', function () {
|
|
if (!scrollTicking) {
|
|
requestAnimationFrame(function () {
|
|
backToTop.classList.toggle('visible', window.scrollY > 600);
|
|
scrollTicking = false;
|
|
});
|
|
scrollTicking = true;
|
|
}
|
|
});
|
|
backToTop.addEventListener('click', function () {
|
|
window.scrollTo({ top: 0 });
|
|
});
|
|
}
|
|
|
|
// 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');
|
|
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')) {
|
|
activeSort = { col: sort, order: order };
|
|
}
|
|
if (q || cat || group || sort) {
|
|
sortRows();
|
|
}
|
|
updateSortIndicators();
|
|
})();
|