Refactor event rendering with column-based event management

Improves event rendering by integrating event filtering directly into column data sources

Key changes:
- Moves event filtering responsibility to IColumnDataSource
- Simplifies event rendering pipeline by pre-filtering events per column
- Supports both date and resource-based calendar modes
- Enhances drag and drop event update mechanism

Optimizes calendar rendering performance and flexibility
This commit is contained in:
Janus C. H. Knudsen 2025-11-22 23:38:52 +01:00
parent eeaeddeef8
commit 17909696ed
9 changed files with 179 additions and 250 deletions

View file

@ -1,11 +1,12 @@
import { IEventBus, ICalendarEvent, IRenderContext } from '../types/CalendarTypes';
import { IEventBus } from '../types/CalendarTypes';
import { IColumnInfo, IColumnDataSource } from '../types/ColumnDataSource';
import { CoreEvents } from '../constants/CoreEvents';
import { EventManager } from '../managers/EventManager';
import { IEventRenderer } from './EventRenderer';
import { SwpEventElement } from '../elements/SwpEventElement';
import { IDragStartEventPayload, IDragMoveEventPayload, IDragEndEventPayload, IDragMouseEnterHeaderEventPayload, IDragMouseLeaveHeaderEventPayload, IDragMouseEnterColumnEventPayload, IDragColumnChangeEventPayload, IHeaderReadyEventPayload, IResizeEndEventPayload } from '../types/EventTypes';
import { IDragStartEventPayload, IDragMoveEventPayload, IDragEndEventPayload, IDragMouseEnterHeaderEventPayload, IDragMouseLeaveHeaderEventPayload, IDragMouseEnterColumnEventPayload, IDragColumnChangeEventPayload, IResizeEndEventPayload } from '../types/EventTypes';
import { DateService } from '../utils/DateService';
import { IColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
/**
* EventRenderingService - Render events i DOM med positionering using Strategy Pattern
* Håndterer event positioning og overlap detection
@ -14,6 +15,7 @@ export class EventRenderingService {
private eventBus: IEventBus;
private eventManager: EventManager;
private strategy: IEventRenderer;
private dataSource: IColumnDataSource;
private dateService: DateService;
private dragMouseLeaveHeaderListener: ((event: Event) => void) | null = null;
@ -22,54 +24,18 @@ export class EventRenderingService {
eventBus: IEventBus,
eventManager: EventManager,
strategy: IEventRenderer,
dataSource: IColumnDataSource,
dateService: DateService
) {
this.eventBus = eventBus;
this.eventManager = eventManager;
this.strategy = strategy;
this.dataSource = dataSource;
this.dateService = dateService;
this.setupEventListeners();
}
/**
* Render events in a specific container for a given period
*/
public async renderEvents(context: IRenderContext): Promise<void> {
// Clear existing events in the specific container first
this.strategy.clearEvents(context.container);
// Get events from EventManager for the period
const events = await this.eventManager.getEventsForPeriod(
context.startDate,
context.endDate
);
if (events.length === 0) {
return;
}
// Filter events by type - only render timed events here
const timedEvents = events.filter(event => !event.allDay);
console.log('🎯 EventRenderingService: Event filtering', {
totalEvents: events.length,
timedEvents: timedEvents.length,
allDayEvents: events.length - timedEvents.length
});
// Render timed events using existing strategy
if (timedEvents.length > 0) {
this.strategy.renderEvents(timedEvents, context.container);
}
// Emit EVENTS_RENDERED event for filtering system
this.eventBus.emit(CoreEvents.EVENTS_RENDERED, {
events: events,
container: context.container
});
}
private setupEventListeners(): void {
this.eventBus.on(CoreEvents.GRID_RENDERED, (event: Event) => {
@ -89,6 +55,7 @@ export class EventRenderingService {
/**
* Handle GRID_RENDERED event - render events in the current grid
* Events are now pre-filtered per column by IColumnDataSource
*/
private handleGridRendered(event: CustomEvent): void {
const { container, columns } = event.detail;
@ -97,17 +64,23 @@ export class EventRenderingService {
return;
}
// Extract dates from columns
const dates = columns.map((col: any) => col.data as Date);
// Render events directly from columns (pre-filtered by IColumnDataSource)
this.renderEventsFromColumns(container, columns);
}
// Calculate startDate and endDate from dates array
const startDate = dates[0];
const endDate = dates[dates.length - 1];
/**
* Render events from pre-filtered columns
* Each column already contains its events (filtered by IColumnDataSource)
*/
private renderEventsFromColumns(container: HTMLElement, columns: IColumnInfo[]): void {
this.strategy.clearEvents(container);
this.strategy.renderEvents(columns, container);
this.renderEvents({
container,
startDate,
endDate
// Emit EVENTS_RENDERED for filtering system
const allEvents = columns.flatMap(col => col.events);
this.eventBus.emit(CoreEvents.EVENTS_RENDERED, {
events: allEvents,
container: container
});
}
@ -166,29 +139,42 @@ export class EventRenderingService {
private setupDragEndListener(): void {
this.eventBus.on('drag:end', async (event: Event) => {
const { originalElement, draggedClone, originalSourceColumn, finalPosition, target } = (event as CustomEvent<IDragEndEventPayload>).detail;
const { originalElement, draggedClone, finalPosition, target } = (event as CustomEvent<IDragEndEventPayload>).detail;
const finalColumn = finalPosition.column;
const finalY = finalPosition.snappedY;
let element = draggedClone as SwpEventElement;
// Only handle day column drops for EventRenderer
// Only handle day column drops
if (target === 'swp-day-column' && finalColumn) {
const element = draggedClone as SwpEventElement;
if (originalElement && draggedClone && this.strategy.handleDragEnd) {
this.strategy.handleDragEnd(originalElement, draggedClone, finalColumn, finalY);
}
await this.eventManager.updateEvent(element.eventId, {
// Build update payload based on mode
const updatePayload: { start: Date; end: Date; allDay: boolean; resourceId?: string } = {
start: element.start,
end: element.end,
allDay: false
});
};
// Re-render affected columns for stacking/grouping (now with updated data)
await this.reRenderAffectedColumns(originalSourceColumn, finalColumn);
if (this.dataSource.isResource()) {
// Resource mode: update resourceId, keep existing date
updatePayload.resourceId = finalColumn.identifier;
} else {
// Date mode: update date from column, keep existing time
const newDate = this.dateService.parseISO(finalColumn.identifier);
const startTimeMinutes = this.dateService.getMinutesSinceMidnight(element.start);
const endTimeMinutes = this.dateService.getMinutesSinceMidnight(element.end);
updatePayload.start = this.dateService.createDateAtTime(newDate, startTimeMinutes);
updatePayload.end = this.dateService.createDateAtTime(newDate, endTimeMinutes);
}
await this.eventManager.updateEvent(element.eventId, updatePayload);
// Trigger full refresh to re-render with updated data
this.eventBus.emit(CoreEvents.REFRESH_REQUESTED, {});
}
});
}
@ -252,27 +238,14 @@ export class EventRenderingService {
this.eventBus.on('resize:end', async (event: Event) => {
const { eventId, element } = (event as CustomEvent<IResizeEndEventPayload>).detail;
// Update event data in EventManager with new end time from resized element
const swpEvent = element as SwpEventElement;
const newStart = swpEvent.start;
const newEnd = swpEvent.end;
await this.eventManager.updateEvent(eventId, {
start: newStart,
end: newEnd
start: swpEvent.start,
end: swpEvent.end
});
console.log('📝 EventRendererManager: Updated event after resize', {
eventId,
newStart,
newEnd
});
const dateIdentifier = newStart.toISOString().split('T')[0];
let columnBounds = ColumnDetectionUtils.getColumnBoundsByIdentifier(dateIdentifier);
if (columnBounds)
await this.renderSingleColumn(columnBounds);
// Trigger full refresh to re-render with updated data
this.eventBus.emit(CoreEvents.REFRESH_REQUESTED, {});
});
}
@ -286,68 +259,6 @@ export class EventRenderingService {
}
/**
* Re-render affected columns after drag to recalculate stacking/grouping
*/
private async reRenderAffectedColumns(originalSourceColumn: IColumnBounds | null, targetColumn: IColumnBounds | null): Promise<void> {
// Re-render original source column if exists
if (originalSourceColumn) {
await this.renderSingleColumn(originalSourceColumn);
}
// Re-render target column if exists and different from source
if (targetColumn && targetColumn.identifier !== originalSourceColumn?.identifier) {
await this.renderSingleColumn(targetColumn);
}
}
/**
* Clear events in a single column's events layer
*/
private clearColumnEvents(eventsLayer: HTMLElement): void {
const existingEvents = eventsLayer.querySelectorAll('swp-event');
const existingGroups = eventsLayer.querySelectorAll('swp-event-group');
existingEvents.forEach(event => event.remove());
existingGroups.forEach(group => group.remove());
}
/**
* Render events for a single column
*/
private async renderSingleColumn(column: IColumnBounds): Promise<void> {
// Get events for just this column's date
const dateString = column.identifier;
const columnStart = this.dateService.parseISO(`${dateString}T00:00:00`);
const columnEnd = this.dateService.parseISO(`${dateString}T23:59:59.999`);
// Get events from EventManager for this single date
const events = await this.eventManager.getEventsForPeriod(columnStart, columnEnd);
// Filter to timed events only
const timedEvents = events.filter(event => !event.allDay);
// Get events layer within this specific column
const eventsLayer = column.element.querySelector('swp-events-layer') as HTMLElement;
if (!eventsLayer) {
console.warn('EventRendererManager: Events layer not found in column');
return;
}
// Clear only this column's events
this.clearColumnEvents(eventsLayer);
// Render events for this column using strategy
if (this.strategy.renderSingleColumnEvents) {
this.strategy.renderSingleColumnEvents(column, timedEvents);
}
console.log('🔄 EventRendererManager: Re-rendered single column', {
columnDate: column.identifier,
eventsCount: timedEvents.length
});
}
private clearEvents(container?: HTMLElement): void {
this.strategy.clearEvents(container);
}