source.fact.ngo a coherence.ngo project

assets/site.js

raw ↗ · AGPL-3.0

/* Progressive enhancement: the full archive remains readable without JavaScript. */ (() => { const root = document.documentElement; const toggle = document.querySelector('.mode-toggle'); function updateTheme() { const light = root.dataset.mode === 'light'; toggle.textContent = light ? 'Dark mode' : 'Light mode'; toggle.setAttribute('aria-label', light ? 'Switch to dark mode' : 'Switch to light mode'); } if (toggle) { toggle.hidden = false; updateTheme(); toggle.addEventListener('click', () => { root.dataset.mode = root.dataset.mode === 'light' ? 'dark' : 'light'; try { localStorage.setItem('fact-mode', root.dataset.mode); } catch (_) { /* Storage is optional. */ } updateTheme(); }); } const spotlight = document.querySelector('.hero-spotlight'); if (spotlight) { const slides = [...spotlight.querySelectorAll('.spotlight-slide')]; const choices = [...spotlight.querySelectorAll('[data-slide]')]; const pause = spotlight.querySelector('.spotlight-pause'); const motion = matchMedia('(prefers-reduced-motion: reduce)'); let active = 0, paused = motion.matches, hovering = false, focused = false, timer; spotlight.querySelector('.spotlight-controls').hidden = false; function show(index) { active = index; slides.forEach((slide,i)=>{slide.hidden=i!==active;}); choices.forEach((button,i)=>button.setAttribute('aria-pressed',String(i===active))); } function schedule() { clearInterval(timer); pause.textContent = paused ? 'Play' : 'Pause'; pause.setAttribute('aria-label', paused ? 'Start model rotation' : 'Pause model rotation'); if (!paused && !hovering && !focused && !document.hidden) { timer=setInterval(()=>show((active+1)%slides.length),6500); } } choices.forEach((button,i)=>button.addEventListener('click',()=>{paused=true;show(i);schedule();})); pause.addEventListener('click',()=>{paused=!paused;schedule();}); spotlight.addEventListener('mouseenter',()=>{hovering=true;schedule();}); spotlight.addEventListener('mouseleave',()=>{hovering=false;schedule();}); spotlight.addEventListener('focusin',()=>{focused=true;schedule();}); spotlight.addEventListener('focusout',event=>{focused=spotlight.contains(event.relatedTarget);schedule();}); document.addEventListener('visibilitychange',schedule); motion.addEventListener('change',()=>{if(motion.matches)paused=true;schedule();}); schedule(); } // Local launcher; Ko-fi is requested only after an intentional open. const support = document.querySelector('[data-support-open]'); const supportDialog = document.getElementById('support-dialog'); if (support && supportDialog && typeof supportDialog.showModal === 'function') { const frame = supportDialog.querySelector('iframe'); const loading = supportDialog.querySelector('.support-loading'); support.setAttribute('role', 'button'); support.setAttribute('aria-haspopup', 'dialog'); support.setAttribute('aria-controls', supportDialog.id); support.setAttribute('aria-expanded', 'false'); support.addEventListener('click', event => { if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; event.preventDefault(); supportDialog.showModal(); support.setAttribute('aria-expanded', 'true'); if (!frame.hasAttribute('src')) { loading.hidden = false; frame.src = frame.dataset.src; } }); support.addEventListener('keydown', event => { if (event.key === ' ') { event.preventDefault(); support.click(); } }); frame.addEventListener('load', () => { if (!frame.hasAttribute('src')) return; loading.hidden = true; frame.parentElement.setAttribute('data-loaded', ''); }); frame.addEventListener('error', () => { loading.textContent = 'Open Ko-fi using the link below.'; }); supportDialog.querySelector('.support-close').addEventListener('click', () => supportDialog.close()); supportDialog.addEventListener('close', () => support.setAttribute('aria-expanded', 'false')); supportDialog.addEventListener('click', event => { if (event.target !== supportDialog) return; const r = supportDialog.getBoundingClientRect(); if (event.clientX < r.left || event.clientX > r.right || event.clientY < r.top || event.clientY > r.bottom) supportDialog.close(); }); } const list = document.getElementById('rec-list'); if (!list) return; const records = [...list.querySelectorAll('.rec')].map(el => ({el, text:el.textContent.toLowerCase()})); const keys = ['domain', 'lens', 'stance', 'controversy']; const filters = keys.map(key => ({key, el:document.getElementById('f-' + key)})); const search = document.getElementById('f-search'); const more = document.getElementById('f-more'); let limit = 12; function apply() { const query = search.value.toLowerCase().trim(); const matches = records.filter(({el, text}) => (!query || text.includes(query)) && filters.every(({key,el:input}) => !input.value || el.dataset[key] === input.value)); const visible = new Set(matches.slice(0, limit)); records.forEach(record => { record.el.hidden = !visible.has(record); }); document.getElementById('f-count').textContent = Math.min(limit,matches.length) + ' of ' + matches.length + ' positions'; document.getElementById('f-empty').hidden = matches.length !== 0; more.hidden = matches.length <= limit; more.textContent = 'Show ' + Math.min(12, Math.max(0, matches.length-limit)) + ' more positions'; } function reset() { filters.forEach(({el})=>{el.value='';}); search.value=''; limit=12; } filters.forEach(({el})=>el.addEventListener('change',()=>{limit=12;apply();})); search.addEventListener('input',()=>{limit=12;apply();}); document.getElementById('f-reset').addEventListener('click',()=>{reset();apply();search.focus();}); more.addEventListener('click',()=>{limit+=12;apply();}); // Lens segmented tabs drive the (then hidden) lens select; same filter state, app-like browsing. const seg = document.getElementById('lens-seg'); const lensSelect = document.getElementById('f-lens'); if (seg && lensSelect) { seg.hidden = false; lensSelect.classList.add('seg-hidden'); seg.addEventListener('click', event => { const button = event.target.closest('[data-lens-tab]'); if (!button) return; seg.querySelectorAll('[data-lens-tab]').forEach(b => b.setAttribute('aria-pressed', b === button ? 'true' : 'false')); lensSelect.value = button.dataset.lensTab; lensSelect.dispatchEvent(new Event('change')); }); } function revealPosition() { const index = records.findIndex(({el})=>'#'+el.id===location.hash); if(index<0) return; reset(); limit = Math.max(12, index+1); apply(); records[index].el.scrollIntoView({block:'start'}); } apply(); revealPosition(); window.addEventListener('hashchange',revealPosition); })();