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",
|
"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,
|
||||||
|
|
|
||||||
|
|
@ -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, så 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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue