Refactors all-day event drag-and-drop handling

Improves event conversion logic between timed and all-day events during drag operations

Simplifies event movement and conversion algorithms
Adds specific handling for timed → all-day and all-day → all-day drops
Enhances event repositioning and layout recalculation
This commit is contained in:
Janus C. H. Knudsen 2025-11-11 22:40:04 +01:00
parent 9987873601
commit 03746afca4

View file

@ -116,12 +116,19 @@ export class AllDayManager {
}); });
// Handle all-day → all-day drops (within header) // Handle all-day → all-day drops (within header)
if (dragEndPayload.target === 'swp-day-header') { if (dragEndPayload.target === 'swp-day-header' && dragEndPayload.originalElement?.hasAttribute('data-allday')) {
console.log('✅ AllDayManager: Handling all-day → all-day drop'); console.log('✅ AllDayManager: Handling all-day → all-day drop');
this.handleDragEnd(dragEndPayload); this.handleDragEnd(dragEndPayload);
return; return;
} }
// Handle timed → all-day conversion (dropped in header)
if (dragEndPayload.target === 'swp-day-header' && !dragEndPayload.originalElement?.hasAttribute('data-allday')) {
console.log('🔄 AllDayManager: Timed → all-day conversion on drop');
this.handleTimedToAllDayDrop(dragEndPayload);
return;
}
// Handle all-day → timed conversion (dropped in column) // Handle all-day → timed conversion (dropped in column)
if (dragEndPayload.target === 'swp-day-column' && dragEndPayload.originalElement?.hasAttribute('data-allday')) { if (dragEndPayload.target === 'swp-day-column' && dragEndPayload.originalElement?.hasAttribute('data-allday')) {
const eventId = dragEndPayload.originalElement.dataset.eventId; const eventId = dragEndPayload.originalElement.dataset.eventId;
@ -474,130 +481,94 @@ export class AllDayManager {
} }
private async handleDragEnd(dragEndEvent: IDragEndEventPayload): Promise<void> { /**
* Handle timed all-day conversion on drop
*/
private async handleTimedToAllDayDrop(dragEndEvent: IDragEndEventPayload): Promise<void> {
if (!dragEndEvent.draggedClone || !dragEndEvent.finalPosition.column) return;
const getEventDurationDays = (start: string | undefined, end: string | undefined): number => { const clone = dragEndEvent.draggedClone as SwpAllDayEventElement;
const eventId = clone.eventId.replace('clone-', '');
const targetDate = dragEndEvent.finalPosition.column.date;
if (!start || !end) console.log('🔄 AllDayManager: Converting timed event to all-day', { eventId, targetDate });
throw new Error('Undefined start or end - date');
const startDate = new Date(start); // Create new dates preserving time
const endDate = new Date(end); const newStart = new Date(targetDate);
newStart.setHours(clone.start.getHours(), clone.start.getMinutes(), 0, 0);
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { const newEnd = new Date(targetDate);
throw new Error('Ugyldig start eller slut-dato i dataset'); newEnd.setHours(clone.end.getHours(), clone.end.getMinutes(), 0, 0);
}
// Use differenceInCalendarDays for proper calendar day calculation // Update event in repository
// This correctly handles timezone differences and DST changes await this.eventManager.updateEvent(eventId, {
return differenceInCalendarDays(endDate, startDate); start: newStart,
}; end: newEnd,
allDay: true
});
if (dragEndEvent.draggedClone == null) // Remove original timed event
return; this.fadeOutAndRemove(dragEndEvent.originalElement);
// 2. Normalize clone ID // Add to current all-day events and recalculate layout
dragEndEvent.draggedClone.dataset.eventId = dragEndEvent.draggedClone.dataset.eventId?.replace('clone-', ''); const newEvent: ICalendarEvent = {
dragEndEvent.draggedClone.style.pointerEvents = ''; // Re-enable pointer events
dragEndEvent.originalElement.dataset.eventId += '_';
let eventId = dragEndEvent.draggedClone.dataset.eventId;
let eventDate = dragEndEvent.finalPosition.column?.date;
let eventType = dragEndEvent.draggedClone.dataset.type;
if (eventDate == null || eventId == null || eventType == null)
return;
const durationDays = getEventDurationDays(dragEndEvent.draggedClone.dataset.start, dragEndEvent.draggedClone.dataset.end);
// Get original dates to preserve time
const originalStartDate = new Date(dragEndEvent.draggedClone.dataset.start!);
const originalEndDate = new Date(dragEndEvent.draggedClone.dataset.end!);
// Create new start date with the new day but preserve original time
const newStartDate = new Date(eventDate);
newStartDate.setHours(originalStartDate.getHours(), originalStartDate.getMinutes(), originalStartDate.getSeconds(), originalStartDate.getMilliseconds());
// Create new end date with the new day + duration, preserving original end time
const newEndDate = new Date(eventDate);
newEndDate.setDate(newEndDate.getDate() + durationDays);
newEndDate.setHours(originalEndDate.getHours(), originalEndDate.getMinutes(), originalEndDate.getSeconds(), originalEndDate.getMilliseconds());
// Update data attributes with new dates (convert to UTC)
dragEndEvent.draggedClone.dataset.start = this.dateService.toUTC(newStartDate);
dragEndEvent.draggedClone.dataset.end = this.dateService.toUTC(newEndDate);
const droppedEvent: ICalendarEvent = {
id: eventId, id: eventId,
title: dragEndEvent.draggedClone.dataset.title || '', title: clone.title,
start: newStartDate, start: newStart,
end: newEndDate, end: newEnd,
type: eventType, type: clone.type,
allDay: true, allDay: true,
syncStatus: 'synced' syncStatus: 'synced'
}; };
// Use current events + dropped event for calculation const updatedEvents = [...this.currentAllDayEvents, newEvent];
const tempEvents = [ const newLayouts = this.calculateAllDayEventsLayout(updatedEvents, this.currentWeekDates);
...this.currentAllDayEvents.filter(event => event.id !== eventId), this.allDayEventRenderer.renderAllDayEventsForPeriod(newLayouts);
droppedEvent
];
// 4. Calculate new layouts for ALL events // Animate height
const newLayouts = this.calculateAllDayEventsLayout(tempEvents, this.currentWeekDates); this.checkAndAnimateAllDayHeight();
// 5. Apply differential updates - compare with DOM instead of currentLayouts
let container = this.getAllDayContainer();
newLayouts.forEach((layout) => {
// Get current gridArea from DOM
const currentGridArea = this.getGridAreaFromDOM(layout.calenderEvent.id);
if (currentGridArea !== layout.gridArea) {
let element = container?.querySelector(`[data-event-id="${layout.calenderEvent.id}"]`) as HTMLElement;
if (element) {
element.classList.add('transitioning');
element.style.gridArea = layout.gridArea;
element.style.gridRow = layout.row.toString();
element.style.gridColumn = `${layout.startColumn} / ${layout.endColumn + 1}`;
element.classList.remove('max-event-overflow-hide');
element.classList.remove('max-event-overflow-show');
if (layout.row > ALL_DAY_CONSTANTS.MAX_COLLAPSED_ROWS)
if (!this.isExpanded)
element.classList.add('max-event-overflow-hide');
else
element.classList.add('max-event-overflow-show');
// Remove transition class after animation
setTimeout(() => element.classList.remove('transitioning'), 200);
} }
}
});
// 6. Clean up drag styles from the dropped clone /**
dragEndEvent.draggedClone.classList.remove('dragging'); * Handle all-day all-day drop (moving within header)
dragEndEvent.draggedClone.style.zIndex = ''; */
dragEndEvent.draggedClone.style.cursor = ''; private async handleDragEnd(dragEndEvent: IDragEndEventPayload): Promise<void> {
dragEndEvent.draggedClone.style.opacity = ''; if (!dragEndEvent.draggedClone || !dragEndEvent.finalPosition.column) return;
// 7. Apply highlight class to show the dropped event with highlight color const clone = dragEndEvent.draggedClone as SwpAllDayEventElement;
dragEndEvent.draggedClone.classList.add('highlight'); const eventId = clone.eventId.replace('clone-', '');
const targetDate = dragEndEvent.finalPosition.column.date;
// 8. CRITICAL FIX: Update event in repository to mark as allDay=true // Calculate duration in days
// This ensures EventManager's repository has correct state const durationDays = differenceInCalendarDays(clone.end, clone.start);
// Without this, the event will reappear in grid on re-render
// Create new dates preserving time
const newStart = new Date(targetDate);
newStart.setHours(clone.start.getHours(), clone.start.getMinutes(), 0, 0);
const newEnd = new Date(targetDate);
newEnd.setDate(newEnd.getDate() + durationDays);
newEnd.setHours(clone.end.getHours(), clone.end.getMinutes(), 0, 0);
// Update event in repository
await this.eventManager.updateEvent(eventId, { await this.eventManager.updateEvent(eventId, {
start: newStartDate, start: newStart,
end: newEndDate, end: newEnd,
allDay: true allDay: true
}); });
// Remove original and fade out
this.fadeOutAndRemove(dragEndEvent.originalElement); this.fadeOutAndRemove(dragEndEvent.originalElement);
this.checkAndAnimateAllDayHeight(); // Recalculate and re-render ALL events
const updatedEvents = this.currentAllDayEvents.map(e =>
e.id === eventId ? { ...e, start: newStart, end: newEnd } : e
);
const newLayouts = this.calculateAllDayEventsLayout(updatedEvents, this.currentWeekDates);
this.allDayEventRenderer.renderAllDayEventsForPeriod(newLayouts);
// Animate height - this also handles overflow classes!
this.checkAndAnimateAllDayHeight();
} }
/** /**