Improves event overlap handling

Refactors event rendering to handle transitive overlaps, ensuring that entire chains of overlapping events are correctly processed.

Fixes an issue where only direct overlaps were re-rendered after a drag and drop, leading to inconsistent stacking.
Now collects and re-renders all events in the stack.
This commit is contained in:
Janus C. H. Knudsen 2025-10-04 00:51:21 +02:00
parent 9bc082eed4
commit fc884efa71
2 changed files with 173 additions and 28 deletions

View file

@ -1887,7 +1887,7 @@
{ {
"id": "146", "id": "146",
"title": "Performance Test", "title": "Performance Test",
"start": "2025-09-29T09:00:00Z", "start": "2025-09-29T08:15:00Z",
"end": "2025-09-29T10:00:00Z", "end": "2025-09-29T10:00:00Z",
"type": "work", "type": "work",
"allDay": false, "allDay": false,

View file

@ -58,6 +58,7 @@ export class DateEventRenderer implements EventRendererStrategy {
/** /**
* Ny hovedfunktion til at håndtere event overlaps * Ny hovedfunktion til at håndtere event overlaps
* Finder transitivt overlappende events (hvis A overlapper B og B overlapper C, er A, B, C i samme gruppe)
* @param events - Events der skal renderes i kolonnen * @param events - Events der skal renderes i kolonnen
* @param container - Container element at rendere i * @param container - Container element at rendere i
*/ */
@ -70,35 +71,94 @@ export class DateEventRenderer implements EventRendererStrategy {
return; return;
} }
// Track hvilke events der allerede er blevet processeret // Find alle overlap grupper (transitive overlaps)
const processedEvents = new Set<string>(); const overlapGroups = this.findTransitiveOverlapGroups(events);
// Gå gennem hvert event og find overlaps // Render hver gruppe
events.forEach((currentEvent, index) => { overlapGroups.forEach(group => {
// Skip events der allerede er processeret som del af en overlap gruppe if (group.length === 1) {
if (processedEvents.has(currentEvent.id)) { // Enkelt event uden overlaps
return; const element = this.renderEvent(group[0]);
}
const remainingEvents = events.slice(index + 1);
const overlappingEvents = this.overlapDetector.resolveOverlap(currentEvent, remainingEvents);
if (overlappingEvents.length > 0) {
// Der er overlaps - opret stack links
const result = this.overlapDetector.decorateWithStackLinks(currentEvent, overlappingEvents);
this.renderOverlappingEvents(result, container);
// Marker alle events i overlap gruppen som processeret
overlappingEvents.forEach(event => processedEvents.add(event.id));
} else {
// Intet overlap - render normalt
const element = this.renderEvent(currentEvent);
container.appendChild(element); container.appendChild(element);
processedEvents.add(currentEvent.id); } else {
// Gruppe med overlaps - opret stack links
// Tag første event som "current" og resten som "overlapping"
const [firstEvent, ...restEvents] = group;
const result = this.overlapDetector.decorateWithStackLinks(firstEvent, restEvents);
this.renderOverlappingEvents(result, container);
} }
}); });
} }
/**
* Find alle grupper af transitivt overlappende events
* Bruger Union-Find algoritme til at finde sammenhængende komponenter
*/
private findTransitiveOverlapGroups(events: CalendarEvent[]): CalendarEvent[][] {
// Byg overlap graf
const overlapMap = new Map<string, Set<string>>();
// Initialiser alle events
events.forEach(event => {
overlapMap.set(event.id, new Set<string>());
});
// Find alle direkte overlaps
for (let i = 0; i < events.length; i++) {
for (let j = i + 1; j < events.length; j++) {
const event1 = events[i];
const event2 = events[j];
// Check om de overlapper
if (event1.start < event2.end && event1.end > event2.start) {
overlapMap.get(event1.id)!.add(event2.id);
overlapMap.get(event2.id)!.add(event1.id);
}
}
}
// Find sammenhængende komponenter via DFS
const visited = new Set<string>();
const groups: CalendarEvent[][] = [];
events.forEach(event => {
if (visited.has(event.id)) return;
// Start ny gruppe
const group: CalendarEvent[] = [];
const stack = [event.id];
while (stack.length > 0) {
const currentId = stack.pop()!;
if (visited.has(currentId)) continue;
visited.add(currentId);
// Find event objektet
const currentEvent = events.find(e => e.id === currentId);
if (currentEvent) {
group.push(currentEvent);
}
// Tilføj alle naboer til stack
const neighbors = overlapMap.get(currentId);
if (neighbors) {
neighbors.forEach(neighborId => {
if (!visited.has(neighborId)) {
stack.push(neighborId);
}
});
}
}
// Sortér gruppe efter start tid
group.sort((a, b) => a.start.getTime() - b.start.getTime());
groups.push(group);
});
return groups;
}
private applyDragStyling(element: HTMLElement): void { private applyDragStyling(element: HTMLElement): void {
element.classList.add('dragging'); element.classList.add('dragging');
@ -409,8 +469,11 @@ export class DateEventRenderer implements EventRendererStrategy {
const overlappingEvents = this.overlapDetector.resolveOverlap(droppedEvent, existingEvents); const overlappingEvents = this.overlapDetector.resolveOverlap(droppedEvent, existingEvents);
if (overlappingEvents.length > 0) { if (overlappingEvents.length > 0) {
// Remove only affected events from DOM // Collect ALL events in stack chains (not just direct overlaps)
const affectedEventIds = [droppedEvent.id, ...overlappingEvents.map(e => e.id)]; const allStackedEvents = this.collectAllStackedEvents(overlappingEvents, eventsLayer);
// Remove all affected events from DOM
const affectedEventIds = [droppedEvent.id, ...allStackedEvents.map(e => e.id)];
eventsLayer.querySelectorAll('swp-event').forEach(el => { eventsLayer.querySelectorAll('swp-event').forEach(el => {
const eventId = (el as HTMLElement).dataset.eventId; const eventId = (el as HTMLElement).dataset.eventId;
if (eventId && affectedEventIds.includes(eventId)) { if (eventId && affectedEventIds.includes(eventId)) {
@ -418,8 +481,8 @@ export class DateEventRenderer implements EventRendererStrategy {
} }
}); });
// Re-render affected events with overlap handling // Re-render all affected events with overlap handling
const affectedEvents = [droppedEvent, ...overlappingEvents]; const affectedEvents = [droppedEvent, ...allStackedEvents];
this.handleEventOverlaps(affectedEvents, eventsLayer); this.handleEventOverlaps(affectedEvents, eventsLayer);
} else { } else {
// Reset z-index for non-overlapping events // Reset z-index for non-overlapping events
@ -427,6 +490,88 @@ export class DateEventRenderer implements EventRendererStrategy {
} }
} }
/**
* Collect all events in stack chains for the given overlapping events
* This ensures we re-render entire stack chains, not just direct overlaps
*/
private collectAllStackedEvents(overlappingEvents: CalendarEvent[], eventsLayer: HTMLElement): CalendarEvent[] {
const allEvents = new Map<string, CalendarEvent>();
const visitedIds = new Set<string>();
// Add all directly overlapping events
overlappingEvents.forEach(event => {
allEvents.set(event.id, event);
visitedIds.add(event.id);
});
// For each overlapping event, traverse its stack chain
overlappingEvents.forEach(event => {
const element = eventsLayer.querySelector(`swp-event[data-event-id="${event.id}"]`) as HTMLElement;
if (!element?.dataset.stackLink) return;
try {
const stackData = JSON.parse(element.dataset.stackLink);
this.traverseStackChain(stackData, eventsLayer, allEvents, visitedIds);
} catch (e) {
console.warn('Failed to parse stackLink:', e);
}
});
return Array.from(allEvents.values());
}
/**
* Recursively traverse stack chain to find all connected events
*/
private traverseStackChain(
stackLink: StackLinkData,
eventsLayer: HTMLElement,
allEvents: Map<string, CalendarEvent>,
visitedIds: Set<string>
): void {
// Traverse previous events
if (stackLink.prev && !visitedIds.has(stackLink.prev)) {
visitedIds.add(stackLink.prev);
const prevElement = eventsLayer.querySelector(`swp-event[data-event-id="${stackLink.prev}"]`) as HTMLElement;
if (prevElement) {
const prevEvent = SwpEventElement.extractCalendarEventFromElement(prevElement);
if (prevEvent) {
allEvents.set(prevEvent.id, prevEvent);
// Continue traversing
if (prevElement.dataset.stackLink) {
try {
const prevStackData = JSON.parse(prevElement.dataset.stackLink);
this.traverseStackChain(prevStackData, eventsLayer, allEvents, visitedIds);
} catch (e) { }
}
}
}
}
// Traverse next events
if (stackLink.next && !visitedIds.has(stackLink.next)) {
visitedIds.add(stackLink.next);
const nextElement = eventsLayer.querySelector(`swp-event[data-event-id="${stackLink.next}"]`) as HTMLElement;
if (nextElement) {
const nextEvent = SwpEventElement.extractCalendarEventFromElement(nextElement);
if (nextEvent) {
allEvents.set(nextEvent.id, nextEvent);
// Continue traversing
if (nextElement.dataset.stackLink) {
try {
const nextStackData = JSON.parse(nextElement.dataset.stackLink);
this.traverseStackChain(nextStackData, eventsLayer, allEvents, visitedIds);
} catch (e) { }
}
}
}
}
}
/** /**
* Get all events in a column as CalendarEvent objects * Get all events in a column as CalendarEvent objects
*/ */