diff --git a/package.json b/package.json index 8d2adb6..6c9af5b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.77.1", + "version": "1.77.2", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "main": "index.js", "scripts": { diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index 273eb5c..e7a1f9f 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -4106,36 +4106,18 @@ export default function WeeklyView() { }; }, []); // eslint-disable-line react-hooks/exhaustive-deps - // Touch long-press drag-to-create: touchmove + touchend handlers - // Stored as refs so the onTouchStart can attach/detach them only when needed - // (avoids a permanent non-passive touchmove listener that breaks scroll/sticky) - const touchMoveHandlerRef = useRef<((e: TouchEvent) => void) | null>(null); - const touchMoveDragHandlerRef = useRef<((e: TouchEvent) => void) | null>(null); - const touchEndHandlerRef = useRef<(() => void) | null>(null); - const cleanupTouchListeners = useCallback(() => { - if (touchMoveHandlerRef.current) { - document.removeEventListener('touchmove', touchMoveHandlerRef.current); - touchMoveHandlerRef.current = null; - } - if (touchMoveDragHandlerRef.current) { - document.removeEventListener('touchmove', touchMoveDragHandlerRef.current); - touchMoveDragHandlerRef.current = null; - } - if (touchEndHandlerRef.current) { - document.removeEventListener('touchend', touchEndHandlerRef.current); - document.removeEventListener('touchcancel', touchEndHandlerRef.current); - touchEndHandlerRef.current = null; - } - }, []); + // Touch long-press drag-to-create: listeners are attached ONCE at mount (not per-slot-touch). + // This avoids any DOM manipulation inside onTouchStart on slot divs, which can + // confuse iOS Safari's scroll-intent detection and block vertical scrolling. + // The slot onTouchStart only updates touchLongPressRef (pure ref, no DOM side effects). + const longPressDragActiveRef = useRef(false); // true when non-passive drag listener is attached - // Called when long-press activates: swap passive cancel-listener for non-passive drag-listener const upgradeToDragListeners = useCallback(() => { - if (touchMoveHandlerRef.current) { - document.removeEventListener('touchmove', touchMoveHandlerRef.current); - touchMoveHandlerRef.current = null; - } + if (longPressDragActiveRef.current) return; + longPressDragActiveRef.current = true; + // Non-passive listener added only when drag is actually active (500ms hold) const handleDragMove = (e: TouchEvent) => { - // Long-press active: prevent scrolling and track slot under finger + if (!longPressDragActiveRef.current) return; e.preventDefault(); const touch = e.touches[0]; const el = document.elementFromPoint(touch.clientX, touch.clientY); @@ -4154,22 +4136,31 @@ export default function WeeklyView() { } } }; - touchMoveDragHandlerRef.current = handleDragMove; document.addEventListener('touchmove', handleDragMove, { passive: false }); + // Store for cleanup + (upgradeToDragListeners as any)._handler = handleDragMove; }, []); - const attachTouchListeners = useCallback(() => { - cleanupTouchListeners(); + const cancelDragListeners = useCallback(() => { + if (!longPressDragActiveRef.current) return; + longPressDragActiveRef.current = false; + const handler = (upgradeToDragListeners as any)._handler; + if (handler) { + document.removeEventListener('touchmove', handler); + (upgradeToDragListeners as any)._handler = null; + } + }, [upgradeToDragListeners]); - // Phase 1: passive listener — only cancels long-press if finger moves (no scroll blocking) + // Permanent global passive touchmove + touchend listeners (attached once at mount) + useEffect(() => { const handlePassiveTouchMove = (e: TouchEvent) => { const lp = touchLongPressRef.current; if (!lp || lp.activated) return; const touch = e.touches[0]; + // Cancel long-press if finger moves more than 10px (user is scrolling) if (Math.abs(touch.clientX - lp.startX) > 10 || Math.abs(touch.clientY - lp.startY) > 10) { clearTimeout(lp.timerId); touchLongPressRef.current = null; - cleanupTouchListeners(); } }; @@ -4179,7 +4170,9 @@ export default function WeeklyView() { clearTimeout(lp.timerId); touchLongPressRef.current = null; } - cleanupTouchListeners(); + + // Deactivate drag listener if it was upgraded + cancelDragListeners(); const drag = slotDragRef.current; slotDragRef.current = null; @@ -4208,12 +4201,18 @@ export default function WeeklyView() { }); }; - touchMoveHandlerRef.current = handlePassiveTouchMove; - touchEndHandlerRef.current = handleTouchEnd; document.addEventListener('touchmove', handlePassiveTouchMove, { passive: true }); - document.addEventListener('touchend', handleTouchEnd); - document.addEventListener('touchcancel', handleTouchEnd); - }, [cleanupTouchListeners]); + document.addEventListener('touchend', handleTouchEnd, { passive: true }); + document.addEventListener('touchcancel', handleTouchEnd, { passive: true }); + return () => { + document.removeEventListener('touchmove', handlePassiveTouchMove); + document.removeEventListener('touchend', handleTouchEnd); + document.removeEventListener('touchcancel', handleTouchEnd); + }; + }, [cancelDragListeners]); // eslint-disable-line react-hooks/exhaustive-deps + + // No-op: onTouchStart on slots only updates the ref — no DOM side effects here + const attachTouchListeners = useCallback(() => { /* listeners are permanent, see useEffect above */ }, []); // Handle recurring event drag confirm (this/all) const handleRecurringDragConfirm = async (editMode: 'this' | 'future' | 'all') => {