From cab1905a2cbe4ef3b8e0430f8c6d44fe9433b0bb Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Sun, 14 Dec 2025 15:07:43 +0100 Subject: [PATCH] fix(calendar): prevent event resize jump on drag start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add resizeOffsetMinutes to track the difference between the snapped click position and the actual event boundary. This prevents events from immediately jumping when starting to resize via the top or bottom handles. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../lib/components/calendar/DayView.svelte | 25 ++++++++++++++----- .../components/calendar/MultiDayView.svelte | 25 ++++++++++++++++--- .../lib/components/calendar/WeekView.svelte | 25 ++++++++++++++++--- 3 files changed, 61 insertions(+), 14 deletions(-) diff --git a/apps/calendar/apps/web/src/lib/components/calendar/DayView.svelte b/apps/calendar/apps/web/src/lib/components/calendar/DayView.svelte index dfbf0dde2..8b1f4fac6 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/DayView.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/DayView.svelte @@ -140,6 +140,7 @@ let resizeOriginalEnd = $state(null); let resizePreviewTop = $state(0); let resizePreviewHeight = $state(0); + let resizeOffsetMinutes = $state(0); // Track if we actually moved during drag/resize (to prevent click on simple mousedown/up) let hasMoved = $state(false); @@ -276,10 +277,19 @@ resizeOriginalEnd = end; const startMinutes = start.getHours() * 60 + start.getMinutes(); + const endMinutes = end.getHours() * 60 + end.getMinutes(); const duration = differenceInMinutes(end, start); resizePreviewTop = minutesToPercent(startMinutes); resizePreviewHeight = (duration / (totalVisibleHours * 60)) * 100; + // Calculate offset between snapped click position and actual event boundary + const clickMinutes = getMinutesFromY(e.clientY); + if (edge === 'top') { + resizeOffsetMinutes = clickMinutes - startMinutes; + } else { + resizeOffsetMinutes = clickMinutes - endMinutes; + } + document.addEventListener('pointermove', handleResizeMove); document.addEventListener('pointerup', handleResizeEnd); } @@ -289,18 +299,19 @@ hasMoved = true; const mouseMinutes = getMinutesFromY(e.clientY); - const snappedMinutes = snapToGrid(mouseMinutes); + // Apply offset to prevent jumping when drag starts + const adjustedMinutes = snapToGrid(mouseMinutes - resizeOffsetMinutes); const origStartMinutes = resizeOriginalStart.getHours() * 60 + resizeOriginalStart.getMinutes(); const origEndMinutes = resizeOriginalEnd.getHours() * 60 + resizeOriginalEnd.getMinutes(); if (resizeEdge === 'top') { - const newStartMinutes = Math.min(snappedMinutes, origEndMinutes - SNAP_MINUTES); + const newStartMinutes = Math.min(adjustedMinutes, origEndMinutes - SNAP_MINUTES); const clampedStart = Math.max(firstVisibleHour * 60, newStartMinutes); resizePreviewTop = minutesToPercent(clampedStart); resizePreviewHeight = ((origEndMinutes - clampedStart) / (totalVisibleHours * 60)) * 100; } else { - const newEndMinutes = Math.max(snappedMinutes, origStartMinutes + SNAP_MINUTES); + const newEndMinutes = Math.max(adjustedMinutes, origStartMinutes + SNAP_MINUTES); const clampedEnd = Math.min(lastVisibleHour * 60, newEndMinutes); resizePreviewHeight = ((clampedEnd - origStartMinutes) / (totalVisibleHours * 60)) * 100; } @@ -313,7 +324,8 @@ } const mouseMinutes = getMinutesFromY(e.clientY); - const snappedMinutes = snapToGrid(mouseMinutes); + // Apply offset to prevent jumping + const adjustedMinutes = snapToGrid(mouseMinutes - resizeOffsetMinutes); const origStartMinutes = resizeOriginalStart.getHours() * 60 + resizeOriginalStart.getMinutes(); const origEndMinutes = resizeOriginalEnd.getHours() * 60 + resizeOriginalEnd.getMinutes(); @@ -324,7 +336,7 @@ if (resizeEdge === 'top') { const newStartMinutes = Math.max( firstVisibleHour * 60, - Math.min(snappedMinutes, origEndMinutes - SNAP_MINUTES) + Math.min(adjustedMinutes, origEndMinutes - SNAP_MINUTES) ); newStart = setHours(new Date(viewStore.currentDate), Math.floor(newStartMinutes / 60)); newStart = setMinutes(newStart, newStartMinutes % 60); @@ -332,7 +344,7 @@ } else { const newEndMinutes = Math.min( lastVisibleHour * 60, - Math.max(snappedMinutes, origStartMinutes + SNAP_MINUTES) + Math.max(adjustedMinutes, origStartMinutes + SNAP_MINUTES) ); newEnd = setHours(new Date(viewStore.currentDate), Math.floor(newEndMinutes / 60)); newEnd = setMinutes(newEnd, newEndMinutes % 60); @@ -362,6 +374,7 @@ resizeEvent = null; resizeOriginalStart = null; resizeOriginalEnd = null; + resizeOffsetMinutes = 0; hasMoved = false; document.removeEventListener('pointermove', handleDragMove); document.removeEventListener('pointerup', handleDragEnd); diff --git a/apps/calendar/apps/web/src/lib/components/calendar/MultiDayView.svelte b/apps/calendar/apps/web/src/lib/components/calendar/MultiDayView.svelte index 1a8910595..fba7ea22a 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/MultiDayView.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/MultiDayView.svelte @@ -97,6 +97,7 @@ let resizeOriginalEnd = $state(null); let resizePreviewTop = $state(0); let resizePreviewHeight = $state(0); + let resizeOffsetMinutes = $state(0); // Track if we actually moved during drag/resize (to prevent click on simple mousedown/up) let hasMoved = $state(false); @@ -431,10 +432,19 @@ // Set initial preview const startMinutes = start.getHours() * 60 + start.getMinutes(); + const endMinutes = end.getHours() * 60 + end.getMinutes(); const duration = differenceInMinutes(end, start); resizePreviewTop = minutesToPercent(startMinutes); resizePreviewHeight = (duration / (totalVisibleHours * 60)) * 100; + // Calculate offset between snapped click position and actual event boundary + const clickMinutes = getMinutesFromY(e.clientY); + if (edge === 'top') { + resizeOffsetMinutes = clickMinutes - startMinutes; + } else { + resizeOffsetMinutes = clickMinutes - endMinutes; + } + document.addEventListener('pointermove', handleResizeMove); document.addEventListener('pointerup', handleResizeEnd); } @@ -444,6 +454,8 @@ hasMoved = true; const currentMinutes = getMinutesFromY(e.clientY); + // Apply offset to prevent jumping when drag starts + const adjustedMinutes = currentMinutes - resizeOffsetMinutes; const originalStartMinutes = resizeOriginalStart.getHours() * 60 + resizeOriginalStart.getMinutes(); const originalEndMinutes = resizeOriginalEnd.getHours() * 60 + resizeOriginalEnd.getMinutes(); @@ -452,7 +464,7 @@ // Resize from bottom - change end time const newEndMinutes = Math.max( originalStartMinutes + 15, - Math.min(lastVisibleHour * 60, currentMinutes) + Math.min(lastVisibleHour * 60, adjustedMinutes) ); const newDuration = newEndMinutes - originalStartMinutes; resizePreviewHeight = (newDuration / (totalVisibleHours * 60)) * 100; @@ -460,7 +472,7 @@ // Resize from top - change start time const newStartMinutes = Math.max( firstVisibleHour * 60, - Math.min(originalEndMinutes - 15, currentMinutes) + Math.min(originalEndMinutes - 15, adjustedMinutes) ); const newDuration = originalEndMinutes - newStartMinutes; resizePreviewTop = minutesToPercent(newStartMinutes); @@ -477,11 +489,14 @@ resizeEvent = null; resizeOriginalStart = null; resizeOriginalEnd = null; + resizeOffsetMinutes = 0; hasMoved = false; return; } const currentMinutes = getMinutesFromY(e.clientY); + // Apply offset to prevent jumping + const adjustedMinutes = currentMinutes - resizeOffsetMinutes; const originalStartMinutes = resizeOriginalStart.getHours() * 60 + resizeOriginalStart.getMinutes(); const originalEndMinutes = resizeOriginalEnd.getHours() * 60 + resizeOriginalEnd.getMinutes(); @@ -492,7 +507,7 @@ if (resizeEdge === 'bottom') { const newEndMinutes = Math.max( originalStartMinutes + 15, - Math.min(lastVisibleHour * 60, currentMinutes) + Math.min(lastVisibleHour * 60, adjustedMinutes) ); const newHours = Math.floor(newEndMinutes / 60); const newMins = newEndMinutes % 60; @@ -501,7 +516,7 @@ } else { const newStartMinutes = Math.max( firstVisibleHour * 60, - Math.min(originalEndMinutes - 15, currentMinutes) + Math.min(originalEndMinutes - 15, adjustedMinutes) ); const newHours = Math.floor(newStartMinutes / 60); const newMins = newStartMinutes % 60; @@ -527,6 +542,7 @@ resizeEvent = null; resizeOriginalStart = null; resizeOriginalEnd = null; + resizeOffsetMinutes = 0; hasMoved = false; } @@ -789,6 +805,7 @@ resizeEvent = null; resizeOriginalStart = null; resizeOriginalEnd = null; + resizeOffsetMinutes = 0; isTaskDragging = false; draggedTask = null; taskDragTargetDay = null; diff --git a/apps/calendar/apps/web/src/lib/components/calendar/WeekView.svelte b/apps/calendar/apps/web/src/lib/components/calendar/WeekView.svelte index 9e2f3c9f5..38045db41 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/WeekView.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/WeekView.svelte @@ -96,6 +96,7 @@ let resizeOriginalEnd = $state(null); let resizePreviewTop = $state(0); let resizePreviewHeight = $state(0); + let resizeOffsetMinutes = $state(0); // Track if we actually moved during drag/resize (to prevent click on simple mousedown/up) let hasMoved = $state(false); @@ -438,10 +439,19 @@ // Set initial preview const startMinutes = start.getHours() * 60 + start.getMinutes(); + const endMinutes = end.getHours() * 60 + end.getMinutes(); const duration = differenceInMinutes(end, start); resizePreviewTop = minutesToPercent(startMinutes); resizePreviewHeight = (duration / (totalVisibleHours * 60)) * 100; + // Calculate offset between snapped click position and actual event boundary + const clickMinutes = getMinutesFromY(e.clientY); + if (edge === 'top') { + resizeOffsetMinutes = clickMinutes - startMinutes; + } else { + resizeOffsetMinutes = clickMinutes - endMinutes; + } + document.addEventListener('pointermove', handleResizeMove); document.addEventListener('pointerup', handleResizeEnd); } @@ -451,6 +461,8 @@ hasMoved = true; const currentMinutes = getMinutesFromY(e.clientY); + // Apply offset to prevent jumping when drag starts + const adjustedMinutes = currentMinutes - resizeOffsetMinutes; const originalStartMinutes = resizeOriginalStart.getHours() * 60 + resizeOriginalStart.getMinutes(); const originalEndMinutes = resizeOriginalEnd.getHours() * 60 + resizeOriginalEnd.getMinutes(); @@ -459,7 +471,7 @@ // Resize from bottom - change end time const newEndMinutes = Math.max( originalStartMinutes + 15, - Math.min(lastVisibleHour * 60, currentMinutes) + Math.min(lastVisibleHour * 60, adjustedMinutes) ); const newDuration = newEndMinutes - originalStartMinutes; resizePreviewHeight = (newDuration / (totalVisibleHours * 60)) * 100; @@ -467,7 +479,7 @@ // Resize from top - change start time const newStartMinutes = Math.max( firstVisibleHour * 60, - Math.min(originalEndMinutes - 15, currentMinutes) + Math.min(originalEndMinutes - 15, adjustedMinutes) ); const newDuration = originalEndMinutes - newStartMinutes; resizePreviewTop = minutesToPercent(newStartMinutes); @@ -484,11 +496,14 @@ resizeEvent = null; resizeOriginalStart = null; resizeOriginalEnd = null; + resizeOffsetMinutes = 0; hasMoved = false; return; } const currentMinutes = getMinutesFromY(e.clientY); + // Apply offset to prevent jumping + const adjustedMinutes = currentMinutes - resizeOffsetMinutes; const originalStartMinutes = resizeOriginalStart.getHours() * 60 + resizeOriginalStart.getMinutes(); const originalEndMinutes = resizeOriginalEnd.getHours() * 60 + resizeOriginalEnd.getMinutes(); @@ -499,7 +514,7 @@ if (resizeEdge === 'bottom') { const newEndMinutes = Math.max( originalStartMinutes + 15, - Math.min(lastVisibleHour * 60, currentMinutes) + Math.min(lastVisibleHour * 60, adjustedMinutes) ); const newHours = Math.floor(newEndMinutes / 60); const newMins = newEndMinutes % 60; @@ -508,7 +523,7 @@ } else { const newStartMinutes = Math.max( firstVisibleHour * 60, - Math.min(originalEndMinutes - 15, currentMinutes) + Math.min(originalEndMinutes - 15, adjustedMinutes) ); const newHours = Math.floor(newStartMinutes / 60); const newMins = newStartMinutes % 60; @@ -534,6 +549,7 @@ resizeEvent = null; resizeOriginalStart = null; resizeOriginalEnd = null; + resizeOffsetMinutes = 0; hasMoved = false; } @@ -812,6 +828,7 @@ resizeEvent = null; resizeOriginalStart = null; resizeOriginalEnd = null; + resizeOffsetMinutes = 0; hasMoved = false; } // Cancel task drag/resize