Swipe to open and close the menu on phones
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1dcf2707f3
commit
138a4e5d77
2 changed files with 72 additions and 2 deletions
|
|
@ -667,6 +667,76 @@ document.addEventListener("submit", async (e) => {
|
|||
|
||||
document.addEventListener("visibilitychange", () => { if (!document.hidden) refresh(); });
|
||||
|
||||
// --- swipe the menu open and shut (phones) --------------------------------
|
||||
// A mostly-sideways swipe drags the drawer with the finger; letting go snaps
|
||||
// it open or shut by distance or speed. Vertical scrolling is left alone.
|
||||
// Move and end listeners go on the touched element itself: touch events keep
|
||||
// going to it even if a re-render detaches it mid-swipe, and then they no
|
||||
// longer bubble up to the document.
|
||||
|
||||
(function swipeMenu() {
|
||||
const phone = matchMedia("(max-width: 900px)");
|
||||
let drag = null;
|
||||
|
||||
const reset = () => {
|
||||
$("#side").style.transform = $("#side").style.transition = "";
|
||||
$("#scrim").style.opacity = "";
|
||||
$("#app").classList.remove("dragging");
|
||||
};
|
||||
|
||||
document.addEventListener("touchstart", (e) => {
|
||||
if (drag) release();
|
||||
if (!phone.matches || e.touches.length !== 1 || !$("#modal").hidden || e.target.closest("input, textarea, select")) return;
|
||||
const t = e.touches[0];
|
||||
drag = { x: t.clientX, y: t.clientY, at: e.timeStamp, open: $("#app").classList.contains("open"), axis: null, dx: 0, target: e.target };
|
||||
drag.target.addEventListener("touchmove", move, { passive: false });
|
||||
drag.target.addEventListener("touchend", finish);
|
||||
drag.target.addEventListener("touchcancel", finish);
|
||||
}, { passive: true });
|
||||
|
||||
function release() {
|
||||
drag.target.removeEventListener("touchmove", move);
|
||||
drag.target.removeEventListener("touchend", finish);
|
||||
drag.target.removeEventListener("touchcancel", finish);
|
||||
if (drag.axis === "x") reset();
|
||||
drag = null;
|
||||
}
|
||||
|
||||
function move(e) {
|
||||
if (!drag) return;
|
||||
const t = e.touches[0];
|
||||
const dx = t.clientX - drag.x, dy = t.clientY - drag.y;
|
||||
if (!drag.axis) {
|
||||
if (Math.hypot(dx, dy) < 10) return;
|
||||
const sideways = Math.abs(dx) > Math.abs(dy) * 1.3 && (drag.open ? dx < 0 : dx > 0);
|
||||
drag.axis = sideways ? "x" : "y";
|
||||
if (!sideways) return;
|
||||
drag.width = $("#side").offsetWidth;
|
||||
$("#side").style.transition = "none";
|
||||
$("#app").classList.add("dragging");
|
||||
}
|
||||
if (drag.axis !== "x") return;
|
||||
e.preventDefault();
|
||||
drag.dx = dx;
|
||||
// 0 is fully open, -width fully shut.
|
||||
const offset = Math.max(-drag.width, Math.min(0, drag.open ? dx : dx - drag.width));
|
||||
$("#side").style.transform = `translateX(${offset}px)`;
|
||||
$("#scrim").style.opacity = String(1 + offset / drag.width);
|
||||
}
|
||||
|
||||
function finish(e) {
|
||||
if (!drag) return;
|
||||
const swiped = drag.axis === "x";
|
||||
const speed = drag.dx / Math.max(1, e.timeStamp - drag.at); // px per ms
|
||||
const open = drag.open
|
||||
? !(drag.dx < -drag.width * 0.35 || speed < -0.4)
|
||||
: drag.dx > drag.width * 0.35 || speed > 0.4;
|
||||
// Hand back to the stylesheet; its transition carries on from here.
|
||||
release();
|
||||
if (swiped) setMenu(open);
|
||||
}
|
||||
})();
|
||||
|
||||
(async function boot() {
|
||||
try {
|
||||
C = await api("/api/content");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue