152 lines
5.8 KiB
JavaScript
152 lines
5.8 KiB
JavaScript
// 目录站交互:搜索、按层/依赖筛选、复制命令。无依赖、无构建。
|
||
(() => {
|
||
const search = document.getElementById('search');
|
||
const count = document.getElementById('count');
|
||
const empty = document.getElementById('empty');
|
||
const sections = [...document.querySelectorAll('.layer-section')];
|
||
const cards = [...document.querySelectorAll('.card')];
|
||
const reqFilters = [...document.querySelectorAll('.chip[data-requires]')];
|
||
const platformFilters = [...document.querySelectorAll('.chip[data-platform]')];
|
||
const layerFilters = [...document.querySelectorAll('.seg[data-layer-filter]')];
|
||
// 主题切换:写进 <html data-theme> 并记住;引导脚本已在首帧前设好初值。
|
||
const toggle = document.getElementById('theme-toggle');
|
||
toggle?.addEventListener('click', () => {
|
||
const root = document.documentElement;
|
||
const next = root.dataset.theme === 'dark' ? 'light' : 'dark';
|
||
root.dataset.theme = next;
|
||
try {
|
||
localStorage.setItem('pi-hub-theme', next);
|
||
} catch {
|
||
/* 隐私模式下存不了就只在本次会话生效 */
|
||
}
|
||
});
|
||
|
||
// 键盘:/ 聚焦搜索,Esc 清空并退出;筛选状态写进 URL(可分享/可回退)。
|
||
document.addEventListener('keydown', event => {
|
||
const typing = /^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement?.tagName ?? '');
|
||
if (event.key === '/' && !typing) {
|
||
event.preventDefault();
|
||
search?.focus();
|
||
search?.select();
|
||
} else if (event.key === 'Escape' && (typing || search?.value)) {
|
||
if (search) {
|
||
search.value = '';
|
||
apply();
|
||
if (typing) search.blur();
|
||
}
|
||
}
|
||
});
|
||
|
||
const syncHash = () => {
|
||
const hash = activeLayers.size ? `#${[...activeLayers].sort().join(',')}` : '';
|
||
if (location.hash !== hash) history.replaceState(null, '', `${location.pathname}${location.search}${hash}`);
|
||
};
|
||
const readHash = () => {
|
||
const wanted = location.hash.replace(/^#/, '').split(',').map(s => s.trim()).filter(Boolean);
|
||
for (const chip of platformFilters) {
|
||
chip.addEventListener('click', () => {
|
||
const value = chip.dataset.platform;
|
||
if (activePlatforms.has(value)) activePlatforms.delete(value);
|
||
else activePlatforms.add(value);
|
||
chip.classList.toggle('active', activePlatforms.has(value));
|
||
chip.setAttribute('aria-pressed', String(activePlatforms.has(value)));
|
||
apply();
|
||
});
|
||
}
|
||
for (const chip of layerFilters) {
|
||
const layer = chip.dataset.layerFilter;
|
||
if (wanted.includes(layer)) {
|
||
activeLayers.add(layer);
|
||
chip.classList.add('active');
|
||
chip.setAttribute('aria-pressed', 'true');
|
||
}
|
||
}
|
||
};
|
||
for (const chip of layerFilters) {
|
||
chip.addEventListener('click', () => {
|
||
chip.setAttribute('aria-pressed', String(activeLayers.has(chip.dataset.layerFilter)));
|
||
syncHash();
|
||
});
|
||
}
|
||
window.addEventListener('hashchange', () => location.reload());
|
||
readHash();
|
||
if (activeLayers.size) apply();
|
||
|
||
if (!cards.length) return;
|
||
const activeRequires = new Set();
|
||
const activeLayers = new Set();
|
||
const activePlatforms = new Set();
|
||
|
||
const apply = () => {
|
||
const query = (search?.value ?? '').trim().toLowerCase();
|
||
let shown = 0;
|
||
for (const card of cards) {
|
||
const haystack = (card.dataset.search ?? '').toLowerCase();
|
||
const matchQuery = !query || haystack.includes(query);
|
||
const matchRequires = activeRequires.size === 0 || [...activeRequires].every(name => haystack.includes(name.toLowerCase()));
|
||
const matchLayer = activeLayers.size === 0 || activeLayers.has(card.dataset.layer);
|
||
const cardPlatforms = (card.dataset.platforms ?? '').split(' ').filter(Boolean);
|
||
const matchPlatform = activePlatforms.size === 0 || [...activePlatforms].some(value => cardPlatforms.includes(value));
|
||
const visible = matchQuery && matchRequires && matchLayer && matchPlatform;
|
||
card.hidden = !visible;
|
||
if (visible) shown += 1;
|
||
}
|
||
for (const section of sections) {
|
||
section.hidden = ![...section.querySelectorAll('.card')].some(card => !card.hidden);
|
||
}
|
||
if (count) count.textContent = String(shown);
|
||
if (empty) empty.hidden = shown !== 0;
|
||
const clear = document.getElementById('clear-filters');
|
||
if (clear) clear.hidden = activeLayers.size + activeRequires.size + activePlatforms.size === 0;
|
||
};
|
||
|
||
search?.addEventListener('input', apply);
|
||
for (const chip of reqFilters) {
|
||
chip.addEventListener('click', () => {
|
||
const name = chip.dataset.requires;
|
||
if (activeRequires.has(name)) activeRequires.delete(name);
|
||
else activeRequires.add(name);
|
||
chip.classList.toggle('active', activeRequires.has(name));
|
||
apply();
|
||
});
|
||
}
|
||
for (const chip of layerFilters) {
|
||
chip.addEventListener('click', () => {
|
||
const layer = chip.dataset.layerFilter;
|
||
if (activeLayers.has(layer)) activeLayers.delete(layer);
|
||
else activeLayers.add(layer);
|
||
chip.classList.toggle('active', activeLayers.has(layer));
|
||
apply();
|
||
});
|
||
}
|
||
|
||
for (const button of document.querySelectorAll('.copy')) {
|
||
const label = button.textContent;
|
||
button.addEventListener('click', async () => {
|
||
const text = button.dataset.copy ?? '';
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
button.textContent = '已复制';
|
||
} catch {
|
||
button.textContent = '按 ⌘C';
|
||
}
|
||
setTimeout(() => (button.textContent = label), 1500);
|
||
});
|
||
}
|
||
|
||
document.getElementById('clear-filters')?.addEventListener('click', () => {
|
||
activeLayers.clear();
|
||
activeRequires.clear();
|
||
activePlatforms.clear();
|
||
for (const chip of [...layerFilters, ...reqFilters, ...platformFilters]) {
|
||
chip.classList.remove('active');
|
||
chip.setAttribute('aria-pressed', 'false');
|
||
}
|
||
if (search) search.value = '';
|
||
history.replaceState(null, '', location.pathname);
|
||
apply();
|
||
});
|
||
|
||
apply();
|
||
})();
|