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)
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');
this.handleDragEnd(dragEndPayload);
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)
if (dragEndPayload.target === 'swp-day-column' && dragEndPayload.originalElement?.hasAttribute('data-allday')) {
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)
throw new Error('Undefined start or end - date');
console.log('🔄 AllDayManager: Converting timed event to all-day', { eventId, targetDate });
const startDate = new Date(start);
const endDate = new Date(end);
// Create new dates preserving time
const newStart = new Date(targetDate);
newStart.setHours(clone.start.getHours(), clone.start.getMinutes(), 0, 0);
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
throw new Error('Ugyldig start eller slut-dato i dataset');
}
const newEnd = new Date(targetDate);
newEnd.setHours(clone.end.getHours(), clone.end.getMinutes(), 0, 0);
// Use differenceInCalendarDays for proper calendar day calculation
// This correctly handles timezone differences and DST changes
return differenceInCalendarDays(endDate, startDate);
};
// Update event in repository
await this.eventManager.updateEvent(eventId, {
start: newStart,
end: newEnd,
allDay: true
});
if (dragEndEvent.draggedClone == null)
return;
// Remove original timed event
this.fadeOutAndRemove(dragEndEvent.originalElement);
// 2. Normalize clone ID
dragEndEvent.draggedClone.dataset.eventId = dragEndEvent.draggedClone.dataset.eventId?.replace('clone-', '');
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 = {
// Add to current all-day events and recalculate layout
const newEvent: ICalendarEvent = {
id: eventId,
title: dragEndEvent.draggedClone.dataset.title || '',
start: newStartDate,
end: newEndDate,
type: eventType,
title: clone.title,
start: newStart,
end: newEnd,
type: clone.type,
allDay: true,
syncStatus: 'synced'
};
// Use current events + dropped event for calculation
const tempEvents = [
...this.currentAllDayEvents.filter(event => event.id !== eventId),
droppedEvent
];
const updatedEvents = [...this.currentAllDayEvents, newEvent];
const newLayouts = this.calculateAllDayEventsLayout(updatedEvents, this.currentWeekDates);
this.allDayEventRenderer.renderAllDayEventsForPeriod(newLayouts);
// 4. Calculate new layouts for ALL events
const newLayouts = this.calculateAllDayEventsLayout(tempEvents, this.currentWeekDates);
// Animate height
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);
/**
* Handle all-day all-day drop (moving within header)
*/
private async handleDragEnd(dragEndEvent: IDragEndEventPayload): Promise<void> {
if (!dragEndEvent.draggedClone || !dragEndEvent.finalPosition.column) return;
if (currentGridArea !== layout.gridArea) {
let element = container?.querySelector(`[data-event-id="${layout.calenderEvent.id}"]`) as HTMLElement;
if (element) {
const clone = dragEndEvent.draggedClone as SwpAllDayEventElement;
const eventId = clone.eventId.replace('clone-', '');
const targetDate = dragEndEvent.finalPosition.column.date;
element.classList.add('transitioning');
element.style.gridArea = layout.gridArea;
element.style.gridRow = layout.row.toString();
element.style.gridColumn = `${layout.startColumn} / ${layout.endColumn + 1}`;
// Calculate duration in days
const durationDays = differenceInCalendarDays(clone.end, clone.start);
element.classList.remove('max-event-overflow-hide');
element.classList.remove('max-event-overflow-show');
// Create new dates preserving time
const newStart = new Date(targetDate);
newStart.setHours(clone.start.getHours(), clone.start.getMinutes(), 0, 0);
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');
const newEnd = new Date(targetDate);
newEnd.setDate(newEnd.getDate() + durationDays);
newEnd.setHours(clone.end.getHours(), clone.end.getMinutes(), 0, 0);
// 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');
dragEndEvent.draggedClone.style.zIndex = '';
dragEndEvent.draggedClone.style.cursor = '';
dragEndEvent.draggedClone.style.opacity = '';
// 7. Apply highlight class to show the dropped event with highlight color
dragEndEvent.draggedClone.classList.add('highlight');
// 8. CRITICAL FIX: Update event in repository to mark as allDay=true
// This ensures EventManager's repository has correct state
// Without this, the event will reappear in grid on re-render
// Update event in repository
await this.eventManager.updateEvent(eventId, {
start: newStartDate,
end: newEndDate,
start: newStart,
end: newEnd,
allDay: true
});
// Remove original and fade out
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();
}
/**