fix(calendar): prevent event resize jump on drag start

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 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-14 15:07:43 +01:00
parent 6bea47d4da
commit cab1905a2c
3 changed files with 61 additions and 14 deletions

View file

@ -140,6 +140,7 @@
let resizeOriginalEnd = $state<Date | null>(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);

View file

@ -97,6 +97,7 @@
let resizeOriginalEnd = $state<Date | null>(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;

View file

@ -96,6 +96,7 @@
let resizeOriginalEnd = $state<Date | null>(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