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:
parent
9bc082eed4
commit
fc884efa71
2 changed files with 173 additions and 28 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<string>();
|
||||
// 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<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 {
|
||||
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<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
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue