133 lines
5.0 KiB
JavaScript
133 lines
5.0 KiB
JavaScript
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});
|