Simplifies event overlap management

Refactors event overlap handling to use a DOM-centric approach with data attributes for stack tracking. This eliminates complex state management, reduces code complexity, and improves maintainability. Removes the previous Map-based linked list implementation.

The new approach offers better debugging, automatic memory management, and eliminates state synchronization bugs.

The solution maintains identical functionality with a significantly simpler implementation, focusing on DOM manipulation for visual stacking and column sharing.

Addresses potential performance concerns of DOM queries by scoping them to specific containers.
This commit is contained in:
Janus Knudsen 2025-09-04 23:35:19 +02:00
parent f5a6b80549
commit 5bdb2f578d
6 changed files with 1699 additions and 67 deletions

View file

@ -6,7 +6,7 @@ import { calendarConfig } from '../core/CalendarConfig';
import { DateCalculator } from '../utils/DateCalculator';
import { eventBus } from '../core/EventBus';
import { CoreEvents } from '../constants/CoreEvents';
import { EventOverlapManager, OverlapType } from '../managers/EventOverlapManager';
import { SimpleEventOverlapManager, OverlapType } from '../managers/SimpleEventOverlapManager';
/**
* Interface for event rendering strategies
@ -21,7 +21,7 @@ export interface EventRendererStrategy {
*/
export abstract class BaseEventRenderer implements EventRendererStrategy {
protected dateCalculator: DateCalculator;
protected overlapManager: EventOverlapManager;
protected overlapManager: SimpleEventOverlapManager;
// Drag and drop state
private draggedClone: HTMLElement | null = null;
@ -32,7 +32,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
DateCalculator.initialize(calendarConfig);
}
this.dateCalculator = dateCalculator || new DateCalculator();
this.overlapManager = new EventOverlapManager();
this.overlapManager = new SimpleEventOverlapManager();
}
/**
@ -229,9 +229,10 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
* Handle drag start event
*/
private handleDragStart(originalElement: HTMLElement, eventId: string, mouseOffset: any, column: string): void {
console.log('handleDragStart:', eventId);
this.originalEvent = originalElement;
// Remove stacking styling from original event before creating clone
// Remove stacking styling during drag
if (this.overlapManager.isStackedEvent(originalElement)) {
this.overlapManager.removeStackedStyling(originalElement);
}
@ -294,8 +295,10 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
* Handle drag end event
*/
private handleDragEnd(eventId: string, originalElement: HTMLElement, finalColumn: string, finalY: number): void {
console.log('handleDragEnd:', eventId);
if (!this.draggedClone || !this.originalEvent) {
console.log('Missing draggedClone or originalEvent');
return;
}
@ -331,16 +334,18 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
*/
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);
}
const eventId = eventElement.dataset.eventId;
if (eventGroup && eventId) {
// Remove from flexbox group
this.overlapManager.removeFromEventGroup(eventGroup, eventId);
} else if (this.overlapManager.isStackedEvent(eventElement)) {
// Remove stacking styling if it's a stacked event
// Remove stacking styling and restack others
this.overlapManager.removeStackedStyling(eventElement);
const container = eventElement.closest('swp-events-layer') as HTMLElement;
if (container) {
this.overlapManager.restackEventsInContainer(container);
}
}
}
@ -367,8 +372,9 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
const height2 = parseFloat(element2.style.height) || 0;
const bottom2 = top2 + height2;
// Check if events overlap in time (pixel space)
if (bottom1 <= top2 || bottom2 <= top1) {
// Check if events overlap in pixel space (with small tolerance for borders)
const tolerance = 2; // Account for borders and small gaps
if (bottom1 <= (top2 + tolerance) || bottom2 <= (top1 + tolerance)) {
return OverlapType.NONE;
}
@ -384,6 +390,85 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
return OverlapType.COLUMN_SHARING;
}
/**
* Detect and group overlapping events during initial rendering
*/
private detectAndGroupInitialEvents(renderedElements: HTMLElement[], container: Element): void {
const processedElements = new Set<HTMLElement>();
for (const element of renderedElements) {
if (processedElements.has(element)) continue;
const overlappingElements: HTMLElement[] = [element];
processedElements.add(element);
// Find alle elements der overlapper med dette element
for (const otherElement of renderedElements) {
if (otherElement === element || processedElements.has(otherElement)) continue;
const overlapType = this.detectPixelOverlap(element, otherElement);
if (overlapType !== OverlapType.NONE) {
overlappingElements.push(otherElement);
processedElements.add(otherElement);
}
}
// Hvis der er overlaps, group dem
if (overlappingElements.length > 1) {
const overlapType = this.detectPixelOverlap(overlappingElements[0], overlappingElements[1]);
// Fjern overlapping elements fra DOM
overlappingElements.forEach(el => el.remove());
// Konvertér til CalendarEvent objekter
const overlappingEvents: CalendarEvent[] = [];
for (const el of overlappingElements) {
const event = this.elementToCalendarEvent(el);
if (event) {
overlappingEvents.push(event);
}
}
if (overlapType === OverlapType.COLUMN_SHARING) {
// Create column sharing group
const groupContainer = this.overlapManager.createEventGroup(overlappingEvents, { top: 0, height: 0 });
overlappingEvents.forEach(event => {
const eventElement = this.createEventElement(event);
this.positionEvent(eventElement, event);
this.overlapManager.addToEventGroup(groupContainer, eventElement);
});
container.appendChild(groupContainer);
} else if (overlapType === OverlapType.STACKING) {
// Handle stacking
const sortedEvents = [...overlappingEvents].sort((a, b) => {
const durationA = new Date(a.end).getTime() - new Date(a.start).getTime();
const durationB = new Date(b.end).getTime() - new Date(b.start).getTime();
return durationB - durationA;
});
let underlyingElement: HTMLElement | null = null;
sortedEvents.forEach((event, index) => {
const eventElement = this.createEventElement(event);
this.positionEvent(eventElement, event);
if (index === 0) {
container.appendChild(eventElement);
underlyingElement = eventElement;
} else {
if (underlyingElement) {
this.overlapManager.createStackedEvent(eventElement, underlyingElement, index);
}
container.appendChild(eventElement);
}
});
}
}
}
}
/**
* Detect overlaps with other events in target column and handle repositioning
*/
@ -429,17 +514,30 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
// Check if dropped event overlaps with any existing events
let hasOverlaps = false;
const overlappingEvents: CalendarEvent[] = [];
let overlapType: OverlapType = OverlapType.NONE;
for (const existingElement of existingEvents) {
// Skip if it's the same event (comparing IDs)
if (existingElement.dataset.eventId === droppedEvent.id) continue;
const overlapType = this.detectPixelOverlap(droppedElement, existingElement);
if (overlapType !== OverlapType.NONE) {
const currentOverlapType = this.detectPixelOverlap(droppedElement, existingElement);
if (currentOverlapType !== OverlapType.NONE) {
hasOverlaps = true;
const existingEvent = this.elementToCalendarEvent(existingElement);
if (existingEvent) {
overlappingEvents.push(existingEvent);
// Use the first detected overlap type for consistency
if (overlapType === OverlapType.NONE) {
overlapType = currentOverlapType;
}
// CRITICAL FIX: Include the entire stack chain, not just the directly overlapping event
const stackChain = this.getFullStackChain(existingElement);
const alreadyIncludedIds = new Set(overlappingEvents.map(e => e.id));
for (const chainElement of stackChain) {
const chainEvent = this.elementToCalendarEvent(chainElement);
if (chainEvent && !alreadyIncludedIds.has(chainEvent.id)) {
overlappingEvents.push(chainEvent);
alreadyIncludedIds.add(chainEvent.id);
}
}
}
}
@ -454,30 +552,61 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
return;
}
// There are overlaps - group and re-render overlapping events
const overlapGroups = this.overlapManager.groupOverlappingEvents(overlappingEvents);
// There are overlaps - use the detected overlap type
// 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);
if (overlapType === OverlapType.COLUMN_SHARING) {
// Create column sharing group
const groupContainer = this.overlapManager.createEventGroup(overlappingEvents, { top: 0, height: 0 });
// 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();
// Add all events to the group
overlappingEvents.forEach(event => {
const eventElement = this.createEventElement(event);
this.positionEvent(eventElement, event);
this.overlapManager.addToEventGroup(groupContainer, eventElement);
});
eventsLayer.appendChild(groupContainer);
} else if (overlapType === OverlapType.STACKING) {
// Handle stacking - sort by duration and stack shorter events on top
const sortedEvents = [...overlappingEvents].sort((a, b) => {
const durationA = new Date(a.end).getTime() - new Date(a.start).getTime();
const durationB = new Date(b.end).getTime() - new Date(b.start).getTime();
return durationB - durationA; // Longer duration first (background)
});
// 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();
let underlyingElement: HTMLElement | null = null;
sortedEvents.forEach((event, index) => {
const eventElement = this.createEventElement(event);
this.positionEvent(eventElement, event);
if (index === 0) {
// First (longest duration) event renders normally at full width
eventsLayer.appendChild(eventElement);
});
}
});
underlyingElement = eventElement;
} else {
// Shorter events are stacked with margin-left offset and higher z-index
if (underlyingElement) {
this.overlapManager.createStackedEvent(eventElement, underlyingElement, index);
}
eventsLayer.appendChild(eventElement);
}
});
}
}
/**
@ -545,6 +674,40 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
};
}
/**
* Get the full stack chain for an event element
*/
private getFullStackChain(element: HTMLElement): HTMLElement[] {
const chain: HTMLElement[] = [];
// Find root of the stack chain (element with stackLevel 0 or no prev link)
let rootElement = element;
let rootLink = this.overlapManager.getStackLink(rootElement);
// Walk backwards to find root
while (rootLink?.prev) {
const prevElement = document.querySelector(`swp-event[data-event-id="${rootLink.prev}"]`) as HTMLElement;
if (!prevElement) break;
rootElement = prevElement;
rootLink = this.overlapManager.getStackLink(rootElement);
}
// Collect entire chain from root forward
let currentElement = rootElement;
while (currentElement) {
chain.push(currentElement);
const currentLink = this.overlapManager.getStackLink(currentElement);
if (!currentLink?.next) break;
const nextElement = document.querySelector(`swp-event[data-event-id="${currentLink.next}"]`) as HTMLElement;
if (!nextElement) break;
currentElement = nextElement;
}
return chain;
}
/**
* Convert DOM element to CalendarEvent for overlap detection
*/
@ -732,24 +895,19 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
const eventsLayer = column.querySelector('swp-events-layer');
if (eventsLayer) {
// Group events by overlap type
const overlapGroups = this.overlapManager.groupOverlappingEvents(columnEvents);
overlapGroups.forEach(group => {
if (group.type === OverlapType.COLUMN_SHARING && group.events.length > 1) {
// Create flexbox container for column sharing
this.renderColumnSharingGroup(group, eventsLayer);
} else if (group.type === OverlapType.STACKING && group.events.length > 1) {
// Render stacked events
this.renderStackedEvents(group, eventsLayer);
} else {
// Render normal single events
group.events.forEach(event => {
this.renderEvent(event, eventsLayer);
});
// Render events først, så vi kan få deres pixel positioner
const renderedElements: HTMLElement[] = [];
columnEvents.forEach(event => {
this.renderEvent(event, eventsLayer);
const eventElement = eventsLayer.querySelector(`swp-event[data-event-id="${event.id}"]`) as HTMLElement;
if (eventElement) {
renderedElements.push(eventElement);
}
});
// Nu detect overlaps baseret på pixel positioner
this.detectAndGroupInitialEvents(renderedElements, eventsLayer);
// Debug: Verify events were actually added
const renderedEvents = eventsLayer.querySelectorAll('swp-event, swp-event-group');
} else {
@ -1015,9 +1173,8 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
} else {
// 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, index);
}
// Use simplified stacking - no complex chain tracking
this.overlapManager.createStackedEvent(eventElement, underlyingElement!, index);
container.appendChild(eventElement);
// DO NOT update underlyingElement - keep it as the longest event
}