💥 feat!: add tag filtering for projects (#431)
- Card (project) images no longer require manual top/bottom margin adjustments for proper spacing. Action needed: Review existing card images as previous manual margin adjustments may now be unnecessary/excessive. - Sites using `cards.html` with tags will now load JavaScript by default when tags are present. To maintain no-JS behaviour, explicitly set `enable_cards_tag_filtering = false` in either `config.toml` or the `_index.md` file where `cards.html` is used.
This commit is contained in:
99
static/js/filterCards.js
Normal file
99
static/js/filterCards.js
Normal file
@@ -0,0 +1,99 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const cards = document.querySelectorAll('.card');
|
||||
const filterLinks = document.querySelectorAll('.filter-controls a');
|
||||
const allProjectsFilter = document.querySelector('#all-projects-filter');
|
||||
if (!cards.length || !filterLinks.length) return;
|
||||
allProjectsFilter.style.display = 'block';
|
||||
|
||||
// Create a Map for O(1) lookups of links by filter value.
|
||||
const linkMap = new Map(
|
||||
Array.from(filterLinks).map(link => [link.dataset.filter, link])
|
||||
);
|
||||
|
||||
// Pre-process cards data for faster filtering.
|
||||
const cardData = Array.from(cards).map(card => ({
|
||||
element: card,
|
||||
tags: card.dataset.tags?.toLowerCase().split(',').filter(Boolean) ?? []
|
||||
}));
|
||||
|
||||
function getTagSlugFromUrl(url) {
|
||||
return url.split('/').filter(Boolean).pop();
|
||||
}
|
||||
|
||||
function getFilterFromHash() {
|
||||
if (!window.location.hash) return 'all';
|
||||
const hash = decodeURIComponent(window.location.hash.slice(1));
|
||||
const matchingLink = Array.from(filterLinks).find(link =>
|
||||
getTagSlugFromUrl(link.getAttribute('href')) === hash
|
||||
);
|
||||
return matchingLink?.dataset.filter ?? 'all';
|
||||
}
|
||||
|
||||
function setActiveFilter(filterValue, updateHash = true) {
|
||||
if (updateHash) {
|
||||
if (filterValue === 'all') {
|
||||
history.pushState(null, '', window.location.pathname);
|
||||
} else {
|
||||
const activeLink = linkMap.get(filterValue);
|
||||
if (activeLink) {
|
||||
const tagSlug = getTagSlugFromUrl(activeLink.getAttribute('href'));
|
||||
history.pushState(null, '', `#${tagSlug}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
const isAll = filterValue === 'all';
|
||||
const display = isAll ? '' : 'none';
|
||||
const ariaHidden = isAll ? 'false' : 'true';
|
||||
requestAnimationFrame(() => {
|
||||
filterLinks.forEach(link => {
|
||||
const isActive = link.dataset.filter === filterValue;
|
||||
link.classList.toggle('active', isActive);
|
||||
link.setAttribute('aria-pressed', isActive);
|
||||
});
|
||||
if (isAll) {
|
||||
cardData.forEach(({ element }) => {
|
||||
element.style.display = display;
|
||||
element.setAttribute('aria-hidden', ariaHidden);
|
||||
});
|
||||
} else {
|
||||
cardData.forEach(({ element, tags }) => {
|
||||
const shouldShow = tags.includes(filterValue);
|
||||
element.style.display = shouldShow ? '' : 'none';
|
||||
element.setAttribute('aria-hidden', !shouldShow);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const filterContainer = filterLinks[0].parentElement.parentElement;
|
||||
filterContainer.addEventListener('click', e => {
|
||||
const link = e.target.closest('a');
|
||||
if (!link) return;
|
||||
e.preventDefault();
|
||||
const filterValue = link.dataset.filter;
|
||||
if (filterValue) setActiveFilter(filterValue);
|
||||
});
|
||||
|
||||
filterContainer.addEventListener('keydown', e => {
|
||||
const link = e.target.closest('a');
|
||||
if (!link) return;
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
link.click();
|
||||
}
|
||||
});
|
||||
|
||||
filterLinks.forEach(link => {
|
||||
link.setAttribute('role', 'button');
|
||||
link.setAttribute('aria-pressed', link.classList.contains('active'));
|
||||
});
|
||||
|
||||
window.addEventListener('popstate', () => {
|
||||
setActiveFilter(getFilterFromHash(), false);
|
||||
});
|
||||
|
||||
const initialFilter = getFilterFromHash();
|
||||
if (initialFilter !== 'all') {
|
||||
setActiveFilter(initialFilter, false);
|
||||
}
|
||||
});
|
Reference in New Issue
Block a user