Implements event overlap rendering
Adds logic to handle event overlaps in the calendar view. It introduces two patterns: column sharing for events with the same start time (rendered using flexbox) and stacking for events with a >30 min difference (rendered with reduced width and z-index). It also introduces deep linking to specific events via URL parameters.
This commit is contained in:
parent
7a1c776bc1
commit
ff067cfac3
11 changed files with 837 additions and 16 deletions
|
|
@ -6,6 +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';
|
||||
|
||||
/**
|
||||
* Interface for event rendering strategies
|
||||
|
|
@ -20,6 +21,7 @@ export interface EventRendererStrategy {
|
|||
*/
|
||||
export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||
protected dateCalculator: DateCalculator;
|
||||
protected overlapManager: EventOverlapManager;
|
||||
|
||||
// Drag and drop state
|
||||
private draggedClone: HTMLElement | null = null;
|
||||
|
|
@ -30,6 +32,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
DateCalculator.initialize(calendarConfig);
|
||||
}
|
||||
this.dateCalculator = dateCalculator || new DateCalculator();
|
||||
this.overlapManager = new EventOverlapManager();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -230,10 +233,16 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
// Create clone
|
||||
this.draggedClone = this.createEventClone(originalElement);
|
||||
|
||||
// Add to current column
|
||||
// Add to current column's events layer (not directly to column)
|
||||
const columnElement = document.querySelector(`swp-day-column[data-date="${column}"]`);
|
||||
if (columnElement) {
|
||||
columnElement.appendChild(this.draggedClone);
|
||||
const eventsLayer = columnElement.querySelector('swp-events-layer');
|
||||
if (eventsLayer) {
|
||||
eventsLayer.appendChild(this.draggedClone);
|
||||
} else {
|
||||
// Fallback to column if events layer not found
|
||||
columnElement.appendChild(this.draggedClone);
|
||||
}
|
||||
}
|
||||
|
||||
// Make original semi-transparent
|
||||
|
|
@ -262,10 +271,16 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
private handleColumnChange(eventId: string, newColumn: string): void {
|
||||
if (!this.draggedClone) return;
|
||||
|
||||
// Move clone to new column
|
||||
// Move clone to new column's events layer
|
||||
const newColumnElement = document.querySelector(`swp-day-column[data-date="${newColumn}"]`);
|
||||
if (newColumnElement && this.draggedClone.parentElement !== newColumnElement) {
|
||||
newColumnElement.appendChild(this.draggedClone);
|
||||
if (newColumnElement) {
|
||||
const eventsLayer = newColumnElement.querySelector('swp-events-layer');
|
||||
if (eventsLayer && this.draggedClone.parentElement !== eventsLayer) {
|
||||
eventsLayer.appendChild(this.draggedClone);
|
||||
} else if (!eventsLayer && this.draggedClone.parentElement !== newColumnElement) {
|
||||
// Fallback to column if events layer not found
|
||||
newColumnElement.appendChild(this.draggedClone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -458,12 +473,26 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
|
||||
const eventsLayer = column.querySelector('swp-events-layer');
|
||||
if (eventsLayer) {
|
||||
columnEvents.forEach(event => {
|
||||
this.renderEvent(event, 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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Debug: Verify events were actually added
|
||||
const renderedEvents = eventsLayer.querySelectorAll('swp-event');
|
||||
const renderedEvents = eventsLayer.querySelectorAll('swp-event, .event-group');
|
||||
} else {
|
||||
}
|
||||
});
|
||||
|
|
@ -679,9 +708,114 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
return !(event1End < event2Span.startColumn || event2End < event1Span.startColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render column sharing group with flexbox container
|
||||
*/
|
||||
protected renderColumnSharingGroup(group: any, container: Element): void {
|
||||
const groupContainer = this.overlapManager.createEventGroup(group.events, group.position);
|
||||
|
||||
// Render each event in the group
|
||||
group.events.forEach((event: CalendarEvent) => {
|
||||
const eventElement = this.createEventElement(event);
|
||||
this.overlapManager.addToEventGroup(groupContainer, eventElement);
|
||||
});
|
||||
|
||||
container.appendChild(groupContainer);
|
||||
|
||||
// Emit event for debugging/logging
|
||||
eventBus.emit('overlap:group-created', {
|
||||
type: 'column_sharing',
|
||||
eventCount: group.events.length,
|
||||
events: group.events.map((e: CalendarEvent) => e.id)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render stacked events with reduced width
|
||||
*/
|
||||
protected renderStackedEvents(group: any, container: Element): void {
|
||||
// Sort events by duration - longer events render first (background), shorter events on top
|
||||
// This way shorter events are more visible and get higher z-index
|
||||
const sortedEvents = [...group.events].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)
|
||||
});
|
||||
|
||||
let underlyingElement: HTMLElement | null = null;
|
||||
|
||||
sortedEvents.forEach((event: CalendarEvent, index: number) => {
|
||||
const eventElement = this.createEventElement(event);
|
||||
this.positionEvent(eventElement, event);
|
||||
|
||||
if (index === 0) {
|
||||
// First (longest duration) event renders normally at full width - UNCHANGED
|
||||
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)
|
||||
if (underlyingElement) {
|
||||
this.overlapManager.createStackedEvent(eventElement, underlyingElement);
|
||||
}
|
||||
container.appendChild(eventElement);
|
||||
// DO NOT update underlyingElement - keep it as the longest event
|
||||
}
|
||||
});
|
||||
|
||||
// Emit event for debugging/logging
|
||||
eventBus.emit('overlap:events-stacked', {
|
||||
type: 'stacking',
|
||||
eventCount: group.events.length,
|
||||
events: group.events.map((e: CalendarEvent) => e.id)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create event element without positioning
|
||||
*/
|
||||
protected createEventElement(event: CalendarEvent): HTMLElement {
|
||||
const eventElement = document.createElement('swp-event');
|
||||
eventElement.dataset.eventId = event.id;
|
||||
eventElement.dataset.title = event.title;
|
||||
eventElement.dataset.start = event.start;
|
||||
eventElement.dataset.end = event.end;
|
||||
eventElement.dataset.type = event.type;
|
||||
eventElement.dataset.duration = event.metadata?.duration?.toString() || '60';
|
||||
|
||||
// Format time for display using unified method
|
||||
const startTime = this.formatTime(event.start);
|
||||
const endTime = this.formatTime(event.end);
|
||||
|
||||
// Calculate duration in minutes
|
||||
const startDate = new Date(event.start);
|
||||
const endDate = new Date(event.end);
|
||||
const durationMinutes = (endDate.getTime() - startDate.getTime()) / (1000 * 60);
|
||||
|
||||
// Create event content
|
||||
eventElement.innerHTML = `
|
||||
<swp-event-time data-duration="${durationMinutes}">${startTime} - ${endTime}</swp-event-time>
|
||||
<swp-event-title>${event.title}</swp-event-title>
|
||||
`;
|
||||
|
||||
return eventElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Position event element
|
||||
*/
|
||||
protected positionEvent(eventElement: HTMLElement, event: CalendarEvent): void {
|
||||
const position = this.calculateEventPosition(event);
|
||||
eventElement.style.position = 'absolute';
|
||||
eventElement.style.top = `${position.top + 1}px`;
|
||||
eventElement.style.height = `${position.height - 3}px`;
|
||||
eventElement.style.left = '2px';
|
||||
eventElement.style.right = '2px';
|
||||
}
|
||||
|
||||
clearEvents(container?: HTMLElement): void {
|
||||
const selector = 'swp-event';
|
||||
const existingEvents = container
|
||||
const selector = 'swp-event, .event-group';
|
||||
const existingEvents = container
|
||||
? container.querySelectorAll(selector)
|
||||
: document.querySelectorAll(selector);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue