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

This commit is contained in:
Joshua Laymon 2025-09-07 17:18:36 +00:00
parent f4239571fc
commit f3cc3fbe78

117
web/static/js/swipe-nav.js Normal file
View File

@ -0,0 +1,117 @@
/* 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 });
}
})();