feat: long-press drag-to-create calendar events on mobile
Tap and hold (~500ms) on an empty time slot to activate, then drag to select time range. Opens calendar event modal with pre-filled times. Includes haptic vibration feedback on activation. Normal tap and scroll gestures are not affected. v1.73.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
39934d23bf
commit
98a47a62de
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "my-weekly-todo-list",
|
||||
"version": "1.72.2",
|
||||
"version": "1.73.0",
|
||||
"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": {
|
||||
|
||||
@ -2223,7 +2223,7 @@ export default function WeeklyView() {
|
||||
initialEndTime?: string;
|
||||
}>({ isOpen: false });
|
||||
|
||||
// Slot drag-to-create calendar event state
|
||||
// Slot drag-to-create calendar event state (mouse + touch)
|
||||
const slotDragJustEndedRef = useRef(false);
|
||||
const slotDragRef = useRef<{
|
||||
active: boolean;
|
||||
@ -2237,6 +2237,15 @@ export default function WeeklyView() {
|
||||
startSlot: string;
|
||||
endSlot: string;
|
||||
} | null>(null);
|
||||
// Touch long-press state for mobile drag-to-create
|
||||
const touchLongPressRef = useRef<{
|
||||
timerId: ReturnType<typeof setTimeout>;
|
||||
startX: number;
|
||||
startY: number;
|
||||
date: Date;
|
||||
slot: string;
|
||||
activated: boolean;
|
||||
} | null>(null);
|
||||
|
||||
// Calendar event resize/drag state
|
||||
const [eventDragState, setEventDragState] = useState<{
|
||||
@ -3960,6 +3969,86 @@ export default function WeeklyView() {
|
||||
};
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// Touch long-press drag-to-create: touchmove + touchend on document
|
||||
useEffect(() => {
|
||||
const handleTouchMove = (e: TouchEvent) => {
|
||||
const lp = touchLongPressRef.current;
|
||||
if (!lp) return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
if (!lp.activated) {
|
||||
// Before long-press fires: cancel if finger moves too much (scrolling)
|
||||
if (Math.abs(touch.clientX - lp.startX) > 10 || Math.abs(touch.clientY - lp.startY) > 10) {
|
||||
clearTimeout(lp.timerId);
|
||||
touchLongPressRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Long-press active: prevent scrolling and track slot under finger
|
||||
e.preventDefault();
|
||||
const el = document.elementFromPoint(touch.clientX, touch.clientY);
|
||||
const slotEl = el?.closest('[data-slot]') as HTMLElement | null;
|
||||
if (slotEl) {
|
||||
const slot = slotEl.getAttribute('data-slot');
|
||||
if (slot) {
|
||||
const drag = slotDragRef.current;
|
||||
if (drag) {
|
||||
drag.currentSlot = slot;
|
||||
const dateStr = `${drag.date.getFullYear()}-${String(drag.date.getMonth() + 1).padStart(2, '0')}-${String(drag.date.getDate()).padStart(2, '0')}`;
|
||||
const startSlot = drag.startSlot <= slot ? drag.startSlot : slot;
|
||||
const endSlot = drag.startSlot <= slot ? slot : drag.startSlot;
|
||||
setSlotDragSelection({ dateStr, startSlot, endSlot });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
const lp = touchLongPressRef.current;
|
||||
if (lp) {
|
||||
clearTimeout(lp.timerId);
|
||||
touchLongPressRef.current = null;
|
||||
}
|
||||
|
||||
const drag = slotDragRef.current;
|
||||
slotDragRef.current = null;
|
||||
if (!drag || !drag.active) {
|
||||
setSlotDragSelection(null);
|
||||
return;
|
||||
}
|
||||
setSlotDragSelection(null);
|
||||
slotDragJustEndedRef.current = true;
|
||||
setTimeout(() => { slotDragJustEndedRef.current = false; }, 300);
|
||||
|
||||
const startSlot = drag.startSlot <= drag.currentSlot ? drag.startSlot : drag.currentSlot;
|
||||
const endSlot = drag.startSlot <= drag.currentSlot ? drag.currentSlot : drag.startSlot;
|
||||
|
||||
const [eh, em] = endSlot.split(':').map(Number);
|
||||
const endMinutes = eh * 60 + em + effectiveCellDurationRef.current;
|
||||
const endH = Math.floor(endMinutes / 60);
|
||||
const endM = endMinutes % 60;
|
||||
const endTime = `${String(endH).padStart(2, '0')}:${String(endM).padStart(2, '0')}`;
|
||||
|
||||
setCalendarEventModal({
|
||||
isOpen: true,
|
||||
initialDate: drag.date,
|
||||
initialStartTime: startSlot,
|
||||
initialEndTime: endTime,
|
||||
});
|
||||
};
|
||||
|
||||
// Use passive: false for touchmove so we can preventDefault after long-press activates
|
||||
document.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||||
document.addEventListener('touchend', handleTouchEnd);
|
||||
document.addEventListener('touchcancel', handleTouchEnd);
|
||||
return () => {
|
||||
document.removeEventListener('touchmove', handleTouchMove);
|
||||
document.removeEventListener('touchend', handleTouchEnd);
|
||||
document.removeEventListener('touchcancel', handleTouchEnd);
|
||||
};
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// Handle recurring event drag confirm (this/all)
|
||||
const handleRecurringDragConfirm = async (editMode: 'this' | 'future' | 'all') => {
|
||||
if (!pendingRecurringDrag) return;
|
||||
@ -7856,6 +7945,42 @@ export default function WeeklyView() {
|
||||
startY: e.clientY,
|
||||
};
|
||||
}}
|
||||
onTouchStart={(e) => {
|
||||
if (isProtected || isOccupiedByAnyTask) return;
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.closest('.calendar-event-block, .grid-task-block, .task-input-slot')) return;
|
||||
const touch = e.touches[0];
|
||||
const slotDate = new Date(date);
|
||||
const slotName = slot;
|
||||
// Cancel any existing long-press
|
||||
if (touchLongPressRef.current) {
|
||||
clearTimeout(touchLongPressRef.current.timerId);
|
||||
}
|
||||
const timerId = setTimeout(() => {
|
||||
// Long-press activated — vibrate for feedback
|
||||
if (navigator.vibrate) navigator.vibrate(50);
|
||||
slotDragRef.current = {
|
||||
active: true,
|
||||
date: slotDate,
|
||||
startSlot: slotName,
|
||||
currentSlot: slotName,
|
||||
startY: touch.clientY,
|
||||
};
|
||||
if (touchLongPressRef.current) {
|
||||
touchLongPressRef.current.activated = true;
|
||||
}
|
||||
const ds = `${slotDate.getFullYear()}-${String(slotDate.getMonth() + 1).padStart(2, '0')}-${String(slotDate.getDate()).padStart(2, '0')}`;
|
||||
setSlotDragSelection({ dateStr: ds, startSlot: slotName, endSlot: slotName });
|
||||
}, 500);
|
||||
touchLongPressRef.current = {
|
||||
timerId,
|
||||
startX: touch.clientX,
|
||||
startY: touch.clientY,
|
||||
date: slotDate,
|
||||
slot: slotName,
|
||||
activated: false,
|
||||
};
|
||||
}}
|
||||
onDragOver={(e) =>
|
||||
!isProtected && !isOccupiedByTask &&
|
||||
handleDragOver(e, date.getDay(), slot)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user