Repository navigation
Expand file tree
/
Copy pathfilter.js
More file actions
90 lines (79 loc) · 3.06 KB
/
Copy pathfilter.js
File metadata and controls
90 lines (79 loc) · 3.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// Client-side gallery filtering. Pure DOM: reads data-* attributes off the
// pre-rendered cards and toggles [hidden]. No framework, no search index.
// The grid renders fully server-side, so it still works with JS disabled;
// this only adds interactivity on top. The filter state lives in the URL query
// (?q=&dept=&type=&tags=a,b), so a filtered view can be linked.
import { filterQuery, matches, readFilters } from '../lib/filter.ts';
const q = document.getElementById('q');
const dept = document.getElementById('dept');
const type = document.getElementById('type');
const cards = [...document.querySelectorAll('.row')];
const tagButtons = [...document.querySelectorAll('.tag-toggle')];
const resultLine = document.getElementById('result-line');
const srStatus = document.getElementById('sr-status');
const empty = document.getElementById('empty');
const activeTags = new Set();
// Restore the filters from the URL; a tag no button carries is dropped.
const initial = readFilters(location.search);
if (q) q.value = initial.q;
// A value with no option leaves the select blank (selectedIndex -1), so fall back to "all".
function restoreSelect(select, value) {
if (!select) return;
select.value = value;
if (select.selectedIndex < 0) select.value = '';
}
restoreSelect(dept, initial.dept);
restoreSelect(type, initial.type);
for (const btn of tagButtons) {
if (initial.tags.includes(btn.dataset.tag)) {
activeTags.add(btn.dataset.tag);
btn.setAttribute('aria-pressed', 'true');
}
}
function apply() {
const filters = { q: q?.value || '', dept: dept?.value || '', type: type?.value || '', tags: [...activeTags] };
let shown = 0;
for (const card of cards) {
const show = matches(
{
search: card.dataset.search || '',
department: card.dataset.department || '',
type: card.dataset.type || '',
tags: (card.dataset.tags || '').split('|').filter(Boolean),
},
filters,
);
card.hidden = !show;
if (show) shown++;
}
history.replaceState(history.state, '', `${location.pathname}${filterQuery(filters, location.search)}${location.hash}`);
if (resultLine) {
resultLine.textContent = `${shown} of ${cards.length} shown`;
}
if (srStatus) {
srStatus.textContent = shown === 0 ? 'No entries match those filters.' : `${shown} of ${cards.length} entries shown.`;
}
if (empty) empty.hidden = shown !== 0;
}
q?.addEventListener('input', apply);
dept?.addEventListener('change', apply);
type?.addEventListener('change', apply);
document.getElementById('clear-filters')?.addEventListener('click', () => {
if (q) q.value = '';
if (dept) dept.value = '';
if (type) type.value = '';
activeTags.clear();
for (const btn of tagButtons) btn.setAttribute('aria-pressed', 'false');
apply();
});
for (const btn of tagButtons) {
btn.addEventListener('click', () => {
const tag = btn.dataset.tag;
const pressed = btn.getAttribute('aria-pressed') === 'true';
btn.setAttribute('aria-pressed', String(!pressed));
if (pressed) activeTags.delete(tag);
else activeTags.add(tag);
apply();
});
}
apply();