This commit is contained in:
Janus Knudsen 2025-09-09 14:35:21 +02:00
parent 727a6ec53a
commit 72019a3d9a
15 changed files with 1056 additions and 1230 deletions

View file

@ -25,67 +25,59 @@ export interface StackLink {
}
export class SimpleEventOverlapManager {
private static readonly STACKING_TIME_THRESHOLD_MINUTES = 30;
private static readonly STACKING_WIDTH_REDUCTION_PX = 15;
/**
* Detect overlap type between two events - simplified logic
* Detect overlap type between two DOM elements - pixel-based logic
*/
public detectOverlap(event1: CalendarEvent, event2: CalendarEvent): OverlapType {
if (!this.eventsOverlapInTime(event1, event2)) {
public resolveOverlapType(element1: HTMLElement, element2: HTMLElement): OverlapType {
const top1 = parseInt(element1.style.top) || 0;
const height1 = parseInt(element1.style.height) || 0;
const bottom1 = top1 + height1;
const top2 = parseInt(element2.style.top) || 0;
const height2 = parseInt(element2.style.height) || 0;
const bottom2 = top2 + height2;
// Check if events overlap in pixel space
const tolerance = 2;
if (bottom1 <= (top2 + tolerance) || bottom2 <= (top1 + tolerance)) {
return OverlapType.NONE;
}
const timeDiffMinutes = Math.abs(
new Date(event1.start).getTime() - new Date(event2.start).getTime()
) / (1000 * 60);
// Events overlap - check start position difference for overlap type
const startDifference = Math.abs(top1 - top2);
return timeDiffMinutes > SimpleEventOverlapManager.STACKING_TIME_THRESHOLD_MINUTES
? OverlapType.STACKING
: OverlapType.COLUMN_SHARING;
// Over 40px start difference = stacking
if (startDifference > 40) {
return OverlapType.STACKING;
}
// Within 40px start difference = column sharing
return OverlapType.COLUMN_SHARING;
}
/**
* Simple time overlap check
*/
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();
return !(end1 <= start2 || end2 <= start1);
}
/**
* Group overlapping events - much cleaner algorithm
* Group overlapping elements - pixel-based algorithm
*/
public groupOverlappingEvents(events: CalendarEvent[]): OverlapGroup[] {
const groups: OverlapGroup[] = [];
const processed = new Set<string>();
public groupOverlappingElements(elements: HTMLElement[]): HTMLElement[][] {
const groups: HTMLElement[][] = [];
const processed = new Set<HTMLElement>();
for (const event of events) {
if (processed.has(event.id)) continue;
for (const element of elements) {
if (processed.has(element)) continue;
// Find all events that overlap with this one
const overlapping = events.filter(other => {
if (processed.has(other.id)) return false;
return other.id === event.id || this.detectOverlap(event, other) !== OverlapType.NONE;
// Find all elements that overlap with this one
const overlapping = elements.filter(other => {
if (processed.has(other)) return false;
return other === element || this.resolveOverlapType(element, other) !== OverlapType.NONE;
});
// Mark all as processed
overlapping.forEach(e => processed.add(e.id));
overlapping.forEach(e => processed.add(e));
// Determine group type
const overlapType = overlapping.length > 1
? this.detectOverlap(overlapping[0], overlapping[1])
: OverlapType.NONE;
groups.push({
type: overlapType,
events: overlapping,
position: this.calculateGroupPosition(overlapping)
});
groups.push(overlapping);
}
return groups;
@ -96,14 +88,6 @@ export class SimpleEventOverlapManager {
*/
public createEventGroup(events: CalendarEvent[], position: { top: number; height: number }): HTMLElement {
const container = document.createElement('swp-event-group');
container.style.cssText = `
position: absolute;
top: ${position.top}px;
left: 2px;
right: 2px;
display: flex;
gap: 2px;
`;
return container;
}
@ -204,7 +188,7 @@ export class SimpleEventOverlapManager {
const nextLink = this.getStackLink(nextElement);
// CRITICAL: Check if prev and next actually overlap without the middle element
const actuallyOverlap = this.checkPixelOverlap(prevElement, nextElement);
const actuallyOverlap = this.resolveOverlapType(prevElement, nextElement);
if (!actuallyOverlap) {
// CHAIN BREAKING: prev and next don't overlap - break the chain
@ -346,24 +330,7 @@ export class SimpleEventOverlapManager {
const eventElement = container.querySelector(`swp-event[data-event-id="${eventId}"]`) as HTMLElement;
if (!eventElement) return false;
// Calculate correct absolute position for standalone event
const startTime = eventElement.dataset.start;
if (startTime) {
const startDate = new Date(startTime);
const gridSettings = calendarConfig.getGridSettings();
const startMinutes = startDate.getHours() * 60 + startDate.getMinutes();
const dayStartMinutes = gridSettings.dayStartHour * 60;
const top = ((startMinutes - dayStartMinutes) / 60) * gridSettings.hourHeight;
// Convert back to absolute positioning
eventElement.style.position = 'absolute';
eventElement.style.top = `${top + 1}px`;
eventElement.style.left = '2px';
eventElement.style.right = '2px';
eventElement.style.flex = '';
eventElement.style.minWidth = '';
}
// Simply remove the element - no position calculation needed since it's being removed
eventElement.remove();
// Handle remaining events
@ -378,22 +345,15 @@ export class SimpleEventOverlapManager {
if (remainingCount === 1) {
const remainingEvent = remainingEvents[0] as HTMLElement;
// Convert last event back to absolute positioning
const remainingStartTime = remainingEvent.dataset.start;
if (remainingStartTime) {
const remainingStartDate = new Date(remainingStartTime);
const gridSettings = calendarConfig.getGridSettings();
const remainingStartMinutes = remainingStartDate.getHours() * 60 + remainingStartDate.getMinutes();
const dayStartMinutes = gridSettings.dayStartHour * 60;
const remainingTop = ((remainingStartMinutes - dayStartMinutes) / 60) * gridSettings.hourHeight;
remainingEvent.style.position = 'absolute';
remainingEvent.style.top = `${remainingTop + 1}px`;
remainingEvent.style.left = '2px';
remainingEvent.style.right = '2px';
remainingEvent.style.flex = '';
remainingEvent.style.minWidth = '';
}
// Convert last event back to absolute positioning - use current pixel position
const currentTop = parseInt(remainingEvent.style.top) || 0;
remainingEvent.style.position = 'absolute';
remainingEvent.style.top = `${currentTop}px`;
remainingEvent.style.left = '2px';
remainingEvent.style.right = '2px';
remainingEvent.style.flex = '';
remainingEvent.style.minWidth = '';
container.parentElement?.insertBefore(remainingEvent, container);
container.remove();
@ -471,33 +431,6 @@ export class SimpleEventOverlapManager {
});
}
/**
* Calculate position for group - simplified calculation
*/
private calculateGroupPosition(events: CalendarEvent[]): { top: number; height: number } {
if (events.length === 0) return { top: 0, height: 0 };
const times = events.flatMap(e => [
new Date(e.start).getTime(),
new Date(e.end).getTime()
]);
const earliestStart = Math.min(...times);
const latestEnd = Math.max(...times);
const startDate = new Date(earliestStart);
const endDate = new Date(latestEnd);
const gridSettings = calendarConfig.getGridSettings();
const startMinutes = startDate.getHours() * 60 + startDate.getMinutes();
const endMinutes = endDate.getHours() * 60 + endDate.getMinutes();
const dayStartMinutes = gridSettings.dayStartHour * 60;
const top = ((startMinutes - dayStartMinutes) / 60) * gridSettings.hourHeight;
const height = ((endMinutes - startMinutes) / 60) * gridSettings.hourHeight;
return { top, height };
}
/**
* Utility methods - simple DOM traversal
@ -537,22 +470,4 @@ export class SimpleEventOverlapManager {
return document.querySelector(`swp-event[data-event-id="${eventId}"]`) as HTMLElement;
}
/**
* Check if two elements overlap in pixel space
*/
private checkPixelOverlap(element1: HTMLElement, element2: HTMLElement): boolean {
if (!element1 || !element2) return false;
const top1 = parseFloat(element1.style.top) || 0;
const height1 = parseFloat(element1.style.height) || 0;
const bottom1 = top1 + height1;
const top2 = parseFloat(element2.style.top) || 0;
const height2 = parseFloat(element2.style.height) || 0;
const bottom2 = top2 + height2;
// Add tolerance for small gaps (borders, etc)
const tolerance = 2;
return !(bottom1 <= (top2 + tolerance) || bottom2 <= (top1 + tolerance));
}
}