const MOBILE_SHEET_MQ = "(max-width: 768px)"; function getSheetParts(root) { if (!root) return null; const panel = root.querySelector( ".recipe-mobile-edit-sheet-panel, .ingredient-mobile-sheet-panel, .recipe-help-modal-panel" ); const body = root.querySelector( ".recipe-mobile-edit-sheet-body, .ingredient-mobile-sheet-body, .recipe-help-modal-body" ); const backdrop = root.querySelector(".recipe-mobile-edit-sheet-backdrop"); return panel ? { root, panel, body, backdrop } : null; } export function resetRecipeMobileSheetPresentation(root) { const parts = getSheetParts(root); if (!parts) return; const { panel, backdrop } = parts; panel.style.transform = ""; panel.style.transition = ""; panel.classList.remove("recipe-mobile-edit-sheet--dragging"); if (backdrop) { backdrop.style.opacity = ""; backdrop.style.transition = ""; } } function canStartSheetDrag(target, body) { if (target.closest(".recipe-mobile-edit-sheet-done")) return false; if (target.closest("button, input, select, textarea, a, label")) { return !!target.closest(".recipe-mobile-edit-sheet-grab, .recipe-mobile-edit-sheet-title"); } if (target.closest(".recipe-mobile-edit-sheet-grab, .recipe-mobile-edit-sheet-header")) { return true; } if (body?.contains(target) && body.scrollTop <= 0) { return true; } return false; } function bindSheetGestures(root, onClose, mobileQuery) { const parts = getSheetParts(root); if (!parts || typeof onClose !== "function") return; const { panel, body, backdrop } = parts; let dragState = null; function clearDragState() { dragState = null; panel.classList.remove("recipe-mobile-edit-sheet--dragging"); } function applyDrag(deltaY) { const offset = Math.max(0, deltaY); panel.style.transition = "none"; panel.style.transform = `translateY(${offset}px)`; if (backdrop) { backdrop.style.transition = "none"; backdrop.style.opacity = String(Math.max(0, 1 - offset / 280)); } } function snapBack() { panel.style.transition = "transform 0.24s ease"; panel.style.transform = ""; if (backdrop) { backdrop.style.transition = "opacity 0.24s ease"; backdrop.style.opacity = ""; } window.setTimeout(() => { panel.style.transition = ""; if (backdrop) backdrop.style.transition = ""; clearDragState(); }, 240); } function dismissWithAnimation() { panel.style.transition = "transform 0.22s ease"; panel.style.transform = "translateY(100%)"; if (backdrop) { backdrop.style.transition = "opacity 0.22s ease"; backdrop.style.opacity = "0"; } window.setTimeout(() => { resetRecipeMobileSheetPresentation(root); onClose(); }, 220); } function onPointerDown(event) { if (!mobileQuery.matches || root.hidden) return; if (event.pointerType === "mouse" && event.button !== 0) return; if (!canStartSheetDrag(event.target, body)) return; dragState = { pointerId: event.pointerId, startY: event.clientY, fromScrollableBody: !!(body && body.contains(event.target) && !event.target.closest(".recipe-mobile-edit-sheet-header, .recipe-mobile-edit-sheet-grab")), }; panel.classList.add("recipe-mobile-edit-sheet--dragging"); panel.setPointerCapture(event.pointerId); } function onPointerMove(event) { if (!dragState || event.pointerId !== dragState.pointerId) return; const deltaY = event.clientY - dragState.startY; if (dragState.fromScrollableBody) { if (deltaY <= 0 || (body && body.scrollTop > 0)) { clearDragState(); resetRecipeMobileSheetPresentation(root); return; } if (deltaY > 8) { dragState.fromScrollableBody = false; } else { return; } } if (deltaY > 0) { event.preventDefault(); applyDrag(deltaY); } } function onPointerEnd(event) { if (!dragState || event.pointerId !== dragState.pointerId) return; const deltaY = event.clientY - dragState.startY; const threshold = Math.min(96, panel.offsetHeight * 0.2); clearDragState(); if (deltaY > threshold) { dismissWithAnimation(); return; } snapBack(); } panel.addEventListener("pointerdown", onPointerDown); panel.addEventListener("pointermove", onPointerMove, { passive: false }); panel.addEventListener("pointerup", onPointerEnd); panel.addEventListener("pointercancel", onPointerEnd); } export function initRecipeMobileSheetGestures(handlers = {}) { const mobileQuery = window.matchMedia(MOBILE_SHEET_MQ); const sheets = [ { root: document.getElementById("ingredientMobileSheet"), onClose: handlers.closeIngredientMobileSheet, }, { root: document.getElementById("unloadingGroupMobileSheet"), onClose: handlers.closeUnloadingGroupMobileSheet, }, { root: document.getElementById("recipeHelpModal"), onClose: handlers.closeRecipeHelpModal, }, ]; sheets.forEach(({ root, onClose }) => bindSheetGestures(root, onClose, mobileQuery)); }