Fixes event overlap detection and stacking logic

Updates the event overlap detection to accurately determine when events overlap in time, fixing incorrect stacking behavior.

Implements column sharing for events starting within 30 minutes of each other.

Applies stacking only when events truly overlap in time but start times differ by more than 30 minutes.

Removes unnecessary data attributes and simplifies styling for stacked events, improving code cleanliness and performance.
This commit is contained in:
Janus Knudsen 2025-09-04 19:22:26 +02:00
parent ff067cfac3
commit 6afea2571b
4 changed files with 361 additions and 90 deletions

View file

@ -26,24 +26,39 @@ export class EventOverlapManager {
private nextZIndex = 100;
/**
* Detect overlap mellem events baseret start tid
* Detect overlap mellem events baseret faktisk time overlap og start tid forskel
*/
public detectOverlap(event1: CalendarEvent, event2: CalendarEvent): OverlapType {
// Først: Tjek om events overlapper i tid
if (!this.eventsOverlapInTime(event1, event2)) {
return OverlapType.NONE;
}
// Events overlapper i tid - nu tjek start tid forskel
const start1 = new Date(event1.start).getTime();
const start2 = new Date(event2.start).getTime();
const timeDiffMinutes = Math.abs(start1 - start2) / (1000 * 60);
// Samme start tid = column sharing
if (timeDiffMinutes === 0) {
return OverlapType.COLUMN_SHARING;
}
// Mere end 30 min forskel = stacking
// Over 30 min start forskel = stacking
if (timeDiffMinutes > EventOverlapManager.STACKING_TIME_THRESHOLD_MINUTES) {
return OverlapType.STACKING;
}
// Indenfor 30 min start forskel = column sharing
return OverlapType.COLUMN_SHARING;
}
return OverlapType.NONE;
/**
* Tjek om to events faktisk overlapper i tid
*/
private eventsOverlapInTime(event1: CalendarEvent, event2: CalendarEvent): boolean {
const start1 = new Date(event1.start).getTime();
const end1 = new Date(event1.end).getTime();
const start2 = new Date(event2.start).getTime();
const end2 = new Date(event2.end).getTime();
// Events overlapper hvis de deler mindst ét tidspunkt
return !(end1 <= start2 || end2 <= start1);
}
/**
@ -95,17 +110,12 @@ export class EventOverlapManager {
* Opret flexbox container for column sharing events
*/
public createEventGroup(events: CalendarEvent[], position: { top: number; height: number }): HTMLElement {
const container = document.createElement('div');
container.className = 'event-group';
const container = document.createElement('swp-event-group');
container.style.position = 'absolute';
container.style.top = `${position.top}px`;
container.style.height = `${position.height}px`;
// Ingen højde på gruppen - kun på individuelle events
container.style.left = '2px';
container.style.right = '2px';
// Data attributter for debugging og styling
container.dataset.eventCount = events.length.toString();
container.dataset.overlapType = OverlapType.COLUMN_SHARING;
return container;
}
@ -114,17 +124,16 @@ export class EventOverlapManager {
* Tilføj event til eksisterende event group
*/
public addToEventGroup(container: HTMLElement, eventElement: HTMLElement): void {
// Fjern absolute positioning fra event da flexbox håndterer layout
eventElement.style.position = 'relative';
eventElement.style.top = '';
eventElement.style.left = '';
eventElement.style.right = '';
// Sørg for at event har korrekt højde baseret på varighed
const duration = eventElement.dataset.duration;
if (duration) {
const durationMinutes = parseInt(duration);
const gridSettings = { hourHeight: 80 }; // Fra config
const height = (durationMinutes / 60) * gridSettings.hourHeight;
eventElement.style.height = `${height - 3}px`; // -3px som andre events
}
container.appendChild(eventElement);
// Opdater event count
const currentCount = parseInt(container.dataset.eventCount || '0');
container.dataset.eventCount = (currentCount + 1).toString();
}
/**
@ -138,68 +147,57 @@ export class EventOverlapManager {
eventElement.style.position = 'absolute';
eventElement.remove();
// Opdater event count
const currentCount = parseInt(container.dataset.eventCount || '0');
const newCount = Math.max(0, currentCount - 1);
container.dataset.eventCount = newCount.toString();
// Tæl resterende events
const remainingEvents = container.querySelectorAll('swp-event');
const remainingCount = remainingEvents.length;
// Cleanup hvis tom container
if (newCount === 0) {
if (remainingCount === 0) {
container.remove();
return true; // Container blev fjernet
}
// Hvis kun ét event tilbage, konvertér tilbage til normal event
if (newCount === 1) {
const remainingEvent = container.querySelector('swp-event') as HTMLElement;
if (remainingEvent) {
// Gendan normal event positioning
remainingEvent.style.position = 'absolute';
remainingEvent.style.top = container.style.top;
remainingEvent.style.left = '2px';
remainingEvent.style.right = '2px';
// Indsæt før container og fjern container
container.parentElement?.insertBefore(remainingEvent, container);
container.remove();
return true; // Container blev fjernet
}
if (remainingCount === 1) {
const remainingEvent = remainingEvents[0] as HTMLElement;
// Gendan normal event positioning
remainingEvent.style.position = 'absolute';
remainingEvent.style.top = container.style.top;
remainingEvent.style.left = '2px';
remainingEvent.style.right = '2px';
// Indsæt før container og fjern container
container.parentElement?.insertBefore(remainingEvent, container);
container.remove();
return true; // Container blev fjernet
}
return false; // Container blev ikke fjernet
}
/**
* Opret stacked event med reduceret bredde
* Opret stacked event med margin-left offset
*/
public createStackedEvent(eventElement: HTMLElement, underlyingElement: HTMLElement): void {
// Beregn reduceret bredde baseret på swp-events-layer (som har den korrekte fulde bredde)
// Underlying event skal beholde sin fulde bredde
const eventsLayer = underlyingElement.parentElement;
const columnWidth = eventsLayer ? eventsLayer.offsetWidth : 200; // fallback
const stackedWidth = Math.max(50, columnWidth - EventOverlapManager.STACKING_WIDTH_REDUCTION_PX);
public createStackedEvent(eventElement: HTMLElement, underlyingElement: HTMLElement, stackLevel: number = 1): void {
// Brug margin-left i stedet for width manipulation
const marginLeft = stackLevel * EventOverlapManager.STACKING_WIDTH_REDUCTION_PX;
eventElement.style.width = `${stackedWidth}px`;
eventElement.style.marginLeft = `${marginLeft}px`;
eventElement.style.left = '2px';
eventElement.style.right = '2px';
eventElement.style.left = 'auto';
eventElement.style.width = '';
eventElement.style.zIndex = this.getNextZIndex().toString();
// Data attributter
eventElement.dataset.overlapType = OverlapType.STACKING;
eventElement.dataset.stackedWidth = stackedWidth.toString();
}
/**
* Fjern stacking styling fra event
*/
public removeStackedStyling(eventElement: HTMLElement): void {
eventElement.style.marginLeft = '';
eventElement.style.width = '';
eventElement.style.right = '';
eventElement.style.left = '2px';
eventElement.style.right = '2px';
eventElement.style.zIndex = '';
delete eventElement.dataset.overlapType;
delete eventElement.dataset.stackedWidth;
}
/**
@ -249,20 +247,20 @@ export class EventOverlapManager {
* Check if element is part of an event group
*/
public isInEventGroup(element: HTMLElement): boolean {
return element.closest('.event-group') !== null;
return element.closest('swp-event-group') !== null;
}
/**
* Check if element is a stacked event
*/
public isStackedEvent(element: HTMLElement): boolean {
return element.dataset.overlapType === OverlapType.STACKING;
return element.style.marginLeft !== '' && element.style.marginLeft !== '0px';
}
/**
* Get event group container for an event element
*/
public getEventGroup(eventElement: HTMLElement): HTMLElement | null {
return eventElement.closest('.event-group') as HTMLElement;
return eventElement.closest('swp-event-group') as HTMLElement;
}
}