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:
parent
f5a6b80549
commit
5bdb2f578d
6 changed files with 1699 additions and 67 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue