function createMobileSearchPreviewNavigation(options) { const buttons = options.buttons || {}; const targets = options.targets || {}; const focusTargets = options.focusTargets || {}; const listeners = new Map(); const prefersReducedMotion = options.prefersReducedMotion || (() => false); const targetNames = new Map(Object.entries(targets).map(([name, target]) => [target, name])); let replyAvailable = options.replyAvailable !== false; let observer = null; let navigationLockUntil = 0; function select(name) { Object.entries(buttons).forEach(([key, button]) => { if (!button) return; if (key === name) button.setAttribute('aria-current', 'location'); else button.removeAttribute('aria-current'); }); } function setReplyAvailable(available) { replyAvailable = Boolean(available); const button = buttons.reply; if (!button) return; if (replyAvailable) button.removeAttribute('aria-disabled'); else button.setAttribute('aria-disabled', 'true'); } function navigate(name) { if (name === 'reply' && !replyAvailable) return false; const target = targets[name]; if (!target) return false; target.scrollIntoView({ block: 'start', behavior: prefersReducedMotion() ? 'auto' : 'smooth', }); navigationLockUntil = Date.now() + 500; select(name); const focusTarget = focusTargets[name] || (name === 'reply' ? target : null); if (focusTarget) focusTarget.focus({preventScroll:true}); return true; } function reset(available = replyAvailable) { setReplyAvailable(available); select('overview'); } return { start() { Object.entries(buttons).forEach(([name, button]) => { if (!button || listeners.has(button)) return; const listener = event => { event.preventDefault(); navigate(name); }; listeners.set(button, listener); button.addEventListener('click', listener); }); reset(replyAvailable); const observe = options.observe || ((handler, observedTargets, root) => { if (typeof IntersectionObserver === 'undefined') return null; const instance = new IntersectionObserver(handler, { root, rootMargin: '-20% 0px -60% 0px', threshold: [0, 0.25, 0.5, 0.75, 1], }); observedTargets.forEach(target => instance.observe(target)); return instance; }); observer = observe(entries => { if (Date.now() < navigationLockUntil) return; const visible = entries .filter(entry => entry.isIntersecting && targetNames.has(entry.target)) .filter(entry => targetNames.get(entry.target) !== 'reply' || replyAvailable) .sort((left, right) => right.intersectionRatio - left.intersectionRatio)[0]; if (visible) select(targetNames.get(visible.target)); }, Array.from(targetNames.keys()).filter(Boolean), options.root || null); }, stop() { listeners.forEach((listener, button) => button.removeEventListener('click', listener)); listeners.clear(); if (observer) observer.disconnect(); observer = null; }, navigate, reset, select, setReplyAvailable, }; } function attachMobileSearchPreviewNavigation({document, window}) { const bySection = name => document.querySelector('[data-search-preview-section="' + name + '"]'); const preview = document.getElementById('search-preview'); const reply = document.getElementById('search-preview-reply-workspace'); const navigation = createMobileSearchPreviewNavigation({ root:document.querySelector('.search-preview-panel'), buttons:{ overview:bySection('overview'), conversation:bySection('conversation'), changes:bySection('changes'), reply:bySection('reply'), actions:bySection('actions'), }, targets:{ overview:document.getElementById('search-preview-overview'), conversation:document.getElementById('search-preview-conversation'), changes:document.getElementById('search-preview-review'), reply, actions:document.getElementById('search-preview-actions'), }, focusTargets:{reply:document.getElementById('search-preview-reply')}, replyAvailable:!reply.hidden, prefersReducedMotion:() => window.matchMedia('(prefers-reduced-motion: reduce)').matches, }); navigation.start(); new MutationObserver(() => navigation.setReplyAvailable(!reply.hidden)).observe(reply, { attributes:true, attributeFilter:['hidden'], }); new MutationObserver(() => { if (preview.classList.contains('open')) navigation.reset(!reply.hidden); }).observe(preview, {attributes:true, attributeFilter:['class']}); ['previous-search-result', 'next-search-result'].forEach(id => { document.getElementById(id).addEventListener('click', () => { setTimeout(() => navigation.reset(!reply.hidden), 0); }); }); return navigation; } if (typeof module !== 'undefined') module.exports = createMobileSearchPreviewNavigation; else attachMobileSearchPreviewNavigation({document, window});