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:
parent
ff067cfac3
commit
6afea2571b
4 changed files with 361 additions and 90 deletions
|
|
@ -147,10 +147,11 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
clone.style.pointerEvents = 'none';
|
||||
clone.style.opacity = '0.8';
|
||||
|
||||
// Keep original dimensions (height stays the same)
|
||||
const rect = originalEvent.getBoundingClientRect();
|
||||
clone.style.width = rect.width + 'px';
|
||||
clone.style.height = rect.height + 'px';
|
||||
// Dragged event skal have fuld kolonne bredde
|
||||
clone.style.left = '2px';
|
||||
clone.style.right = '2px';
|
||||
clone.style.width = '';
|
||||
clone.style.height = originalEvent.style.height || `${originalEvent.getBoundingClientRect().height}px`;
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
|
@ -230,6 +231,11 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
private handleDragStart(originalElement: HTMLElement, eventId: string, mouseOffset: any, column: string): void {
|
||||
this.originalEvent = originalElement;
|
||||
|
||||
// Remove stacking styling from original event before creating clone
|
||||
if (this.overlapManager.isStackedEvent(originalElement)) {
|
||||
this.overlapManager.removeStackedStyling(originalElement);
|
||||
}
|
||||
|
||||
// Create clone
|
||||
this.draggedClone = this.createEventClone(originalElement);
|
||||
|
||||
|
|
@ -293,6 +299,9 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
return;
|
||||
}
|
||||
|
||||
// Remove original event from any existing groups first
|
||||
this.removeEventFromExistingGroups(this.originalEvent);
|
||||
|
||||
// Fade out original
|
||||
this.fadeOutAndRemove(this.originalEvent);
|
||||
|
||||
|
|
@ -306,8 +315,10 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
this.draggedClone.style.pointerEvents = '';
|
||||
this.draggedClone.style.opacity = '';
|
||||
this.draggedClone.style.userSelect = '';
|
||||
this.draggedClone.style.zIndex = '';
|
||||
// Behold z-index hvis det er et stacked event
|
||||
|
||||
// Detect overlaps with other events in the target column and reposition if needed
|
||||
this.detectAndHandleOverlaps(this.draggedClone, finalColumn);
|
||||
|
||||
// Clean up
|
||||
this.draggedClone = null;
|
||||
|
|
@ -315,6 +326,196 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove event from any existing groups and cleanup empty containers
|
||||
*/
|
||||
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);
|
||||
}
|
||||
} else if (this.overlapManager.isStackedEvent(eventElement)) {
|
||||
// Remove stacking styling if it's a stacked event
|
||||
this.overlapManager.removeStackedStyling(eventElement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore normal event styling (full column width)
|
||||
*/
|
||||
private restoreNormalEventStyling(eventElement: HTMLElement): void {
|
||||
eventElement.style.position = 'absolute';
|
||||
eventElement.style.left = '2px';
|
||||
eventElement.style.right = '2px';
|
||||
eventElement.style.width = '';
|
||||
// Behold z-index for stacked events
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect overlaps with other events in target column and handle repositioning
|
||||
*/
|
||||
private detectAndHandleOverlaps(droppedElement: HTMLElement, targetColumn: string): void {
|
||||
// Find target column element
|
||||
const columnElement = document.querySelector(`swp-day-column[data-date="${targetColumn}"]`);
|
||||
if (!columnElement) return;
|
||||
|
||||
const eventsLayer = columnElement.querySelector('swp-events-layer');
|
||||
if (!eventsLayer) return;
|
||||
|
||||
// Get all existing events in the column (excluding the dropped element)
|
||||
const existingEvents = Array.from(eventsLayer.querySelectorAll('swp-event'))
|
||||
.filter(el => el !== droppedElement) as HTMLElement[];
|
||||
|
||||
// Convert dropped element to CalendarEvent using its NEW position
|
||||
const droppedEvent = this.elementToCalendarEventWithNewPosition(droppedElement, targetColumn);
|
||||
if (!droppedEvent) return;
|
||||
|
||||
// Check if dropped event overlaps with any existing events
|
||||
let hasOverlaps = false;
|
||||
const overlappingEvents: CalendarEvent[] = [droppedEvent];
|
||||
|
||||
for (const existingElement of existingEvents) {
|
||||
const existingEvent = this.elementToCalendarEvent(existingElement);
|
||||
if (!existingEvent) continue;
|
||||
|
||||
const overlapType = this.overlapManager.detectOverlap(droppedEvent, existingEvent);
|
||||
if (overlapType !== OverlapType.NONE) {
|
||||
hasOverlaps = true;
|
||||
overlappingEvents.push(existingEvent);
|
||||
}
|
||||
}
|
||||
|
||||
// Only re-render if there are actual overlaps
|
||||
if (!hasOverlaps) {
|
||||
// No overlaps - just update the dropped element's dataset with new times
|
||||
this.updateElementDataset(droppedElement, droppedEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
// There are overlaps - group and re-render overlapping events
|
||||
const overlapGroups = this.overlapManager.groupOverlappingEvents(overlappingEvents);
|
||||
|
||||
// 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);
|
||||
eventsLayer.appendChild(eventElement);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update element's dataset with new times after successful drop
|
||||
*/
|
||||
private updateElementDataset(element: HTMLElement, event: CalendarEvent): void {
|
||||
element.dataset.start = event.start;
|
||||
element.dataset.end = event.end;
|
||||
|
||||
// Update the time display
|
||||
const timeElement = element.querySelector('swp-event-time');
|
||||
if (timeElement) {
|
||||
const startTime = this.formatTime(event.start);
|
||||
const endTime = this.formatTime(event.end);
|
||||
timeElement.textContent = `${startTime} - ${endTime}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert DOM element to CalendarEvent using its NEW position after drag
|
||||
*/
|
||||
private elementToCalendarEventWithNewPosition(element: HTMLElement, targetColumn: string): CalendarEvent | null {
|
||||
const eventId = element.dataset.eventId;
|
||||
const title = element.dataset.title;
|
||||
const type = element.dataset.type;
|
||||
const originalDuration = element.dataset.originalDuration;
|
||||
|
||||
if (!eventId || !title || !type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate new start/end times based on current position
|
||||
const currentTop = parseFloat(element.style.top) || 0;
|
||||
const durationMinutes = originalDuration ? parseInt(originalDuration) : 60;
|
||||
|
||||
// Convert position to time
|
||||
const gridSettings = calendarConfig.getGridSettings();
|
||||
const hourHeight = gridSettings.hourHeight;
|
||||
const dayStartHour = gridSettings.dayStartHour;
|
||||
|
||||
// Calculate minutes from grid start
|
||||
const minutesFromGridStart = (currentTop / hourHeight) * 60;
|
||||
const actualStartMinutes = (dayStartHour * 60) + minutesFromGridStart;
|
||||
const actualEndMinutes = actualStartMinutes + durationMinutes;
|
||||
|
||||
// Create ISO date strings for the target column
|
||||
const targetDate = new Date(targetColumn + 'T00:00:00');
|
||||
const startDate = new Date(targetDate);
|
||||
startDate.setMinutes(startDate.getMinutes() + actualStartMinutes);
|
||||
|
||||
const endDate = new Date(targetDate);
|
||||
endDate.setMinutes(endDate.getMinutes() + actualEndMinutes);
|
||||
|
||||
return {
|
||||
id: eventId,
|
||||
title: title,
|
||||
start: startDate.toISOString(),
|
||||
end: endDate.toISOString(),
|
||||
type: type,
|
||||
allDay: false,
|
||||
syncStatus: 'synced',
|
||||
metadata: {
|
||||
duration: durationMinutes
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert DOM element to CalendarEvent for overlap detection
|
||||
*/
|
||||
private elementToCalendarEvent(element: HTMLElement): CalendarEvent | null {
|
||||
const eventId = element.dataset.eventId;
|
||||
const title = element.dataset.title;
|
||||
const start = element.dataset.start;
|
||||
const end = element.dataset.end;
|
||||
const type = element.dataset.type;
|
||||
const duration = element.dataset.duration;
|
||||
|
||||
if (!eventId || !title || !start || !end || !type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: eventId,
|
||||
title: title,
|
||||
start: start,
|
||||
end: end,
|
||||
type: type,
|
||||
allDay: false,
|
||||
syncStatus: 'synced', // Default to synced for existing events
|
||||
metadata: {
|
||||
duration: duration ? parseInt(duration) : 60
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle conversion to all-day event
|
||||
*/
|
||||
|
|
@ -492,7 +693,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
});
|
||||
|
||||
// Debug: Verify events were actually added
|
||||
const renderedEvents = eventsLayer.querySelectorAll('swp-event, .event-group');
|
||||
const renderedEvents = eventsLayer.querySelectorAll('swp-event, swp-event-group');
|
||||
} else {
|
||||
}
|
||||
});
|
||||
|
|
@ -731,7 +932,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
}
|
||||
|
||||
/**
|
||||
* Render stacked events with reduced width
|
||||
* Render stacked events with margin-left offset
|
||||
*/
|
||||
protected renderStackedEvents(group: any, container: Element): void {
|
||||
// Sort events by duration - longer events render first (background), shorter events on top
|
||||
|
|
@ -753,10 +954,10 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
container.appendChild(eventElement);
|
||||
underlyingElement = eventElement;
|
||||
} else {
|
||||
// Shorter events are stacked with reduced width and higher z-index
|
||||
// All stacked events use the SAME underlying element (the longest one)
|
||||
// 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);
|
||||
this.overlapManager.createStackedEvent(eventElement, underlyingElement, index);
|
||||
}
|
||||
container.appendChild(eventElement);
|
||||
// DO NOT update underlyingElement - keep it as the longest event
|
||||
|
|
@ -814,7 +1015,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
}
|
||||
|
||||
clearEvents(container?: HTMLElement): void {
|
||||
const selector = 'swp-event, .event-group';
|
||||
const selector = 'swp-event, swp-event-group';
|
||||
const existingEvents = container
|
||||
? container.querySelectorAll(selector)
|
||||
: document.querySelectorAll(selector);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue