From fc884efa7132c5ab55962e62de1ec229edb3dee4 Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Sat, 4 Oct 2025 00:51:21 +0200 Subject: [PATCH] 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. --- src/data/mock-events.json | 2 +- src/renderers/EventRenderer.ts | 199 ++++++++++++++++++++++++++++----- 2 files changed, 173 insertions(+), 28 deletions(-) diff --git a/src/data/mock-events.json b/src/data/mock-events.json index 0430586..d00dc82 100644 --- a/src/data/mock-events.json +++ b/src/data/mock-events.json @@ -1887,7 +1887,7 @@ { "id": "146", "title": "Performance Test", - "start": "2025-09-29T09:00:00Z", + "start": "2025-09-29T08:15:00Z", "end": "2025-09-29T10:00:00Z", "type": "work", "allDay": false, diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index 63af422..a1e56b2 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -58,6 +58,7 @@ export class DateEventRenderer implements EventRendererStrategy { /** * Ny hovedfunktion til at håndtere event overlaps + * Finder transitivt overlappende events (hvis A overlapper B og B overlapper C, så er A, B, C i samme gruppe) * @param events - Events der skal renderes i kolonnen * @param container - Container element at rendere i */ @@ -70,35 +71,94 @@ export class DateEventRenderer implements EventRendererStrategy { return; } - // Track hvilke events der allerede er blevet processeret - const processedEvents = new Set(); + // Find alle overlap grupper (transitive overlaps) + const overlapGroups = this.findTransitiveOverlapGroups(events); - // Gå gennem hvert event og find overlaps - events.forEach((currentEvent, index) => { - // Skip events der allerede er processeret som del af en overlap gruppe - if (processedEvents.has(currentEvent.id)) { - return; - } - - 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); + // Render hver gruppe + overlapGroups.forEach(group => { + if (group.length === 1) { + // Enkelt event uden overlaps + const element = this.renderEvent(group[0]); 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>(); + + // Initialiser alle events + events.forEach(event => { + overlapMap.set(event.id, new Set()); + }); + + // 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(); + 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 { element.classList.add('dragging'); @@ -409,8 +469,11 @@ export class DateEventRenderer implements EventRendererStrategy { const overlappingEvents = this.overlapDetector.resolveOverlap(droppedEvent, existingEvents); if (overlappingEvents.length > 0) { - // Remove only affected events from DOM - const affectedEventIds = [droppedEvent.id, ...overlappingEvents.map(e => e.id)]; + // Collect ALL events in stack chains (not just direct overlaps) + 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 => { const eventId = (el as HTMLElement).dataset.eventId; if (eventId && affectedEventIds.includes(eventId)) { @@ -418,8 +481,8 @@ export class DateEventRenderer implements EventRendererStrategy { } }); - // Re-render affected events with overlap handling - const affectedEvents = [droppedEvent, ...overlappingEvents]; + // Re-render all affected events with overlap handling + const affectedEvents = [droppedEvent, ...allStackedEvents]; this.handleEventOverlaps(affectedEvents, eventsLayer); } else { // 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(); + const visitedIds = new Set(); + + // 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, + visitedIds: Set + ): 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 */