Fixes event overlap detection and stacking logic

Updates the event overlap detection to accurately determine when events overlap in time, fixing incorrect stacking behavior.

Implements column sharing for events starting within 30 minutes of each other.

Applies stacking only when events truly overlap in time but start times differ by more than 30 minutes.

Removes unnecessary data attributes and simplifies styling for stacked events, improving code cleanliness and performance.
This commit is contained in:
Janus Knudsen 2025-09-04 19:22:26 +02:00
parent ff067cfac3
commit 6afea2571b
4 changed files with 361 additions and 90 deletions

View file

@ -147,10 +147,11 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
clone.style.pointerEvents = 'none';
clone.style.opacity = '0.8';
// Keep original dimensions (height stays the same)
const rect = originalEvent.getBoundingClientRect();
clone.style.width = rect.width + 'px';
clone.style.height = rect.height + 'px';
// Dragged event skal have fuld kolonne bredde
clone.style.left = '2px';
clone.style.right = '2px';
clone.style.width = '';
clone.style.height = originalEvent.style.height || `${originalEvent.getBoundingClientRect().height}px`;
return clone;
}
@ -230,6 +231,11 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
private handleDragStart(originalElement: HTMLElement, eventId: string, mouseOffset: any, column: string): void {
this.originalEvent = originalElement;
// Remove stacking styling from original event before creating clone
if (this.overlapManager.isStackedEvent(originalElement)) {
this.overlapManager.removeStackedStyling(originalElement);
}
// Create clone
this.draggedClone = this.createEventClone(originalElement);
@ -293,6 +299,9 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
return;
}
// Remove original event from any existing groups first
this.removeEventFromExistingGroups(this.originalEvent);
// Fade out original
this.fadeOutAndRemove(this.originalEvent);
@ -306,8 +315,10 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
this.draggedClone.style.pointerEvents = '';
this.draggedClone.style.opacity = '';
this.draggedClone.style.userSelect = '';
this.draggedClone.style.zIndex = '';
// Behold z-index hvis det er et stacked event
// Detect overlaps with other events in the target column and reposition if needed
this.detectAndHandleOverlaps(this.draggedClone, finalColumn);
// Clean up
this.draggedClone = null;
@ -315,6 +326,196 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
}
/**
* Remove event from any existing groups and cleanup empty containers
*/
private removeEventFromExistingGroups(eventElement: HTMLElement): void {
const eventGroup = this.overlapManager.getEventGroup(eventElement);
if (eventGroup) {
const eventId = eventElement.dataset.eventId;
if (eventId) {
this.overlapManager.removeFromEventGroup(eventGroup, eventId);
// Gendan normal kolonne bredde efter fjernelse fra group
this.restoreNormalEventStyling(eventElement);
}
} else if (this.overlapManager.isStackedEvent(eventElement)) {
// Remove stacking styling if it's a stacked event
this.overlapManager.removeStackedStyling(eventElement);
}
}
/**
* Restore normal event styling (full column width)
*/
private restoreNormalEventStyling(eventElement: HTMLElement): void {
eventElement.style.position = 'absolute';
eventElement.style.left = '2px';
eventElement.style.right = '2px';
eventElement.style.width = '';
// Behold z-index for stacked events
}
/**
* Detect overlaps with other events in target column and handle repositioning
*/
private detectAndHandleOverlaps(droppedElement: HTMLElement, targetColumn: string): void {
// Find target column element
const columnElement = document.querySelector(`swp-day-column[data-date="${targetColumn}"]`);
if (!columnElement) return;
const eventsLayer = columnElement.querySelector('swp-events-layer');
if (!eventsLayer) return;
// Get all existing events in the column (excluding the dropped element)
const existingEvents = Array.from(eventsLayer.querySelectorAll('swp-event'))
.filter(el => el !== droppedElement) as HTMLElement[];
// Convert dropped element to CalendarEvent using its NEW position
const droppedEvent = this.elementToCalendarEventWithNewPosition(droppedElement, targetColumn);
if (!droppedEvent) return;
// Check if dropped event overlaps with any existing events
let hasOverlaps = false;
const overlappingEvents: CalendarEvent[] = [droppedEvent];
for (const existingElement of existingEvents) {
const existingEvent = this.elementToCalendarEvent(existingElement);
if (!existingEvent) continue;
const overlapType = this.overlapManager.detectOverlap(droppedEvent, existingEvent);
if (overlapType !== OverlapType.NONE) {
hasOverlaps = true;
overlappingEvents.push(existingEvent);
}
}
// Only re-render if there are actual overlaps
if (!hasOverlaps) {
// No overlaps - just update the dropped element's dataset with new times
this.updateElementDataset(droppedElement, droppedEvent);
return;
}
// There are overlaps - group and re-render overlapping events
const overlapGroups = this.overlapManager.groupOverlappingEvents(overlappingEvents);
// Remove overlapping events from DOM
const overlappingEventIds = new Set(overlappingEvents.map(e => e.id));
existingEvents
.filter(el => overlappingEventIds.has(el.dataset.eventId || ''))
.forEach(el => el.remove());
droppedElement.remove();
// Re-render overlapping events with proper grouping
overlapGroups.forEach(group => {
if (group.type === OverlapType.COLUMN_SHARING && group.events.length > 1) {
this.renderColumnSharingGroup(group, eventsLayer);
} else if (group.type === OverlapType.STACKING && group.events.length > 1) {
this.renderStackedEvents(group, eventsLayer);
} else {
group.events.forEach(event => {
const eventElement = this.createEventElement(event);
this.positionEvent(eventElement, event);
eventsLayer.appendChild(eventElement);
});
}
});
}
/**
* Update element's dataset with new times after successful drop
*/
private updateElementDataset(element: HTMLElement, event: CalendarEvent): void {
element.dataset.start = event.start;
element.dataset.end = event.end;
// Update the time display
const timeElement = element.querySelector('swp-event-time');
if (timeElement) {
const startTime = this.formatTime(event.start);
const endTime = this.formatTime(event.end);
timeElement.textContent = `${startTime} - ${endTime}`;
}
}
/**
* Convert DOM element to CalendarEvent using its NEW position after drag
*/
private elementToCalendarEventWithNewPosition(element: HTMLElement, targetColumn: string): CalendarEvent | null {
const eventId = element.dataset.eventId;
const title = element.dataset.title;
const type = element.dataset.type;
const originalDuration = element.dataset.originalDuration;
if (!eventId || !title || !type) {
return null;
}
// Calculate new start/end times based on current position
const currentTop = parseFloat(element.style.top) || 0;
const durationMinutes = originalDuration ? parseInt(originalDuration) : 60;
// Convert position to time
const gridSettings = calendarConfig.getGridSettings();
const hourHeight = gridSettings.hourHeight;
const dayStartHour = gridSettings.dayStartHour;
// Calculate minutes from grid start
const minutesFromGridStart = (currentTop / hourHeight) * 60;
const actualStartMinutes = (dayStartHour * 60) + minutesFromGridStart;
const actualEndMinutes = actualStartMinutes + durationMinutes;
// Create ISO date strings for the target column
const targetDate = new Date(targetColumn + 'T00:00:00');
const startDate = new Date(targetDate);
startDate.setMinutes(startDate.getMinutes() + actualStartMinutes);
const endDate = new Date(targetDate);
endDate.setMinutes(endDate.getMinutes() + actualEndMinutes);
return {
id: eventId,
title: title,
start: startDate.toISOString(),
end: endDate.toISOString(),
type: type,
allDay: false,
syncStatus: 'synced',
metadata: {
duration: durationMinutes
}
};
}
/**
* Convert DOM element to CalendarEvent for overlap detection
*/
private elementToCalendarEvent(element: HTMLElement): CalendarEvent | null {
const eventId = element.dataset.eventId;
const title = element.dataset.title;
const start = element.dataset.start;
const end = element.dataset.end;
const type = element.dataset.type;
const duration = element.dataset.duration;
if (!eventId || !title || !start || !end || !type) {
return null;
}
return {
id: eventId,
title: title,
start: start,
end: end,
type: type,
allDay: false,
syncStatus: 'synced', // Default to synced for existing events
metadata: {
duration: duration ? parseInt(duration) : 60
}
};
}
/**
* Handle conversion to all-day event
*/
@ -492,7 +693,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
});
// Debug: Verify events were actually added
const renderedEvents = eventsLayer.querySelectorAll('swp-event, .event-group');
const renderedEvents = eventsLayer.querySelectorAll('swp-event, swp-event-group');
} else {
}
});
@ -731,7 +932,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
}
/**
* Render stacked events with reduced width
* Render stacked events with margin-left offset
*/
protected renderStackedEvents(group: any, container: Element): void {
// Sort events by duration - longer events render first (background), shorter events on top
@ -753,10 +954,10 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
container.appendChild(eventElement);
underlyingElement = eventElement;
} else {
// Shorter events are stacked with reduced width and higher z-index
// All stacked events use the SAME underlying element (the longest one)
// Shorter events are stacked with margin-left offset and higher z-index
// Each subsequent event gets more margin: 15px, 30px, 45px, etc.
if (underlyingElement) {
this.overlapManager.createStackedEvent(eventElement, underlyingElement);
this.overlapManager.createStackedEvent(eventElement, underlyingElement, index);
}
container.appendChild(eventElement);
// DO NOT update underlyingElement - keep it as the longest event
@ -814,7 +1015,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
}
clearEvents(container?: HTMLElement): void {
const selector = 'swp-event, .event-group';
const selector = 'swp-event, swp-event-group';
const existingEvents = container
? container.querySelectorAll(selector)
: document.querySelectorAll(selector);