Illustrations/web/static/js/swipe-nav.js

117 lines
4.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Swipe/Keyboard navigation for Entry View
- Respects localStorage 'swipeNavEnabled' (default ON)
- Touch swipe on mobile, ArrowLeft/ArrowRight on keyboards
- Edge protection and input-safe
*/
(function () {
// Feature flag (client-side). Default ON.
let swipeEnabled = true;
try { swipeEnabled = (localStorage.getItem('swipeNavEnabled') !== 'false'); } catch (e) {}
// Only attach if enabled
if (!swipeEnabled) return;
// Forms to submit for previous/next (your existing IDs)
const prevForm = document.getElementById('navPrevForm');
const nextForm = document.getElementById('navNextForm');
// Create toast + hints lazily to keep HTML clean
const toast = document.createElement('div');
toast.id = 'toast-swipe-error';
toast.style.cssText = [
'position:fixed', 'left:50%', 'bottom:16px', 'transform:translateX(-50%)',
'padding:10px 14px', 'border-radius:10px', 'background:#111827', 'color:#fff',
'box-shadow:0 6px 20px rgba(0,0,0,.25)', 'opacity:0', 'pointer-events:none',
'transition:opacity .25s', 'z-index:1200', 'font-size:14px'
].join(';');
toast.textContent = "Can't navigate here.";
document.body.appendChild(toast);
function showToast() {
toast.style.opacity = '1';
setTimeout(() => { toast.style.opacity = '0'; }, 1200);
}
// Only show swipe hints on touch devices, once per session
const isTouch = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
let hintL, hintR;
if (isTouch) {
try {
if (!sessionStorage.getItem('swipeHintShown')) {
sessionStorage.setItem('swipeHintShown', '1');
const base = 'position:fixed;width:34px;height:34px;display:flex;align-items:center;justify-content:center;' +
'background:rgba(0,0,0,.35);color:#fff;border-radius:999px;box-shadow:0 6px 20px rgba(0,0,0,.25);' +
'z-index:1100;opacity:0;transform:scale(.9);transition:opacity .25s,transform .25s;top:45%';
hintL = document.createElement('div');
hintL.style.cssText = base + ';left:10px;';
hintL.textContent = '';
document.body.appendChild(hintL);
hintR = document.createElement('div');
hintR.style.cssText = base + ';right:10px;';
hintR.textContent = '';
document.body.appendChild(hintR);
setTimeout(() => { hintL.style.opacity = '1'; hintR.style.opacity = '1'; hintL.style.transform = 'scale(1)'; hintR.style.transform = 'scale(1)'; }, 300);
setTimeout(() => { hintL.style.opacity = '0'; hintR.style.opacity = '0'; }, 2200);
}
} catch (e) {}
}
function navigate(dir) {
try {
if (dir < 0 && prevForm) { prevForm.submit(); return true; }
if (dir > 0 && nextForm) { nextForm.submit(); return true; }
} catch (e) {}
showToast();
return false;
}
// Keyboard shortcuts
document.addEventListener('keydown', function (e) {
if (!swipeEnabled) return;
const tag = (e.target && e.target.tagName) || '';
if (/INPUT|TEXTAREA|SELECT/.test(tag)) return;
if (e.key === 'ArrowLeft') { e.preventDefault(); navigate(-1); }
if (e.key === 'ArrowRight') { e.preventDefault(); navigate(1); }
});
// Touch swipe (edge-protected, vertical tolerance)
if (isTouch) {
let startX = 0, startY = 0, tracking = false;
const EDGE = 20, THRESH = 40;
const area = document.querySelector('main.page') || document.body;
function isInteractive(el) {
return !!(el.closest && el.closest('input, textarea, select, button, a, [contenteditable], .no-swipe'));
}
area.addEventListener('touchstart', function (e) {
const t = e.touches[0];
if (!t) return;
if (isInteractive(e.target)) return;
startX = t.clientX; startY = t.clientY; tracking = true;
}, { passive: true });
area.addEventListener('touchmove', function (e) {
if (!tracking) return;
const t = e.touches[0];
if (!t) return;
const dx = t.clientX - startX;
const dy = t.clientY - startY;
if (Math.abs(dx) > THRESH && Math.abs(dy) < 28) {
// Edge-protection: avoid accidental from extreme edges
if (startX < EDGE && dx > 0) { tracking = false; return; }
if (startX > (window.innerWidth - EDGE) && dx < 0) { tracking = false; return; }
e.preventDefault();
navigate(dx > 0 ? -1 : 1);
tracking = false;
}
}, { passive: false });
area.addEventListener('touchend', function () { tracking = false; }, { passive: true });
}
})();