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",
|
"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",
|
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -2223,7 +2223,7 @@ export default function WeeklyView() {
|
|||||||
initialEndTime?: string;
|
initialEndTime?: string;
|
||||||
}>({ isOpen: false });
|
}>({ isOpen: false });
|
||||||
|
|
||||||
// Slot drag-to-create calendar event state
|
// Slot drag-to-create calendar event state (mouse + touch)
|
||||||
const slotDragJustEndedRef = useRef(false);
|
const slotDragJustEndedRef = useRef(false);
|
||||||
const slotDragRef = useRef<{
|
const slotDragRef = useRef<{
|
||||||
active: boolean;
|
active: boolean;
|
||||||
@ -2237,6 +2237,15 @@ export default function WeeklyView() {
|
|||||||
startSlot: string;
|
startSlot: string;
|
||||||
endSlot: string;
|
endSlot: string;
|
||||||
} | null>(null);
|
} | 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
|
// Calendar event resize/drag state
|
||||||
const [eventDragState, setEventDragState] = useState<{
|
const [eventDragState, setEventDragState] = useState<{
|
||||||
@ -3960,6 +3969,86 @@ export default function WeeklyView() {
|
|||||||
};
|
};
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
}, []); // 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)
|
// Handle recurring event drag confirm (this/all)
|
||||||
const handleRecurringDragConfirm = async (editMode: 'this' | 'future' | 'all') => {
|
const handleRecurringDragConfirm = async (editMode: 'this' | 'future' | 'all') => {
|
||||||
if (!pendingRecurringDrag) return;
|
if (!pendingRecurringDrag) return;
|
||||||
@ -7856,6 +7945,42 @@ export default function WeeklyView() {
|
|||||||
startY: e.clientY,
|
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) =>
|
onDragOver={(e) =>
|
||||||
!isProtected && !isOccupiedByTask &&
|
!isProtected && !isOccupiedByTask &&
|
||||||
handleDragOver(e, date.getDay(), slot)
|
handleDragOver(e, date.getDay(), slot)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user