Calendar/src/renderers/EventRendererManager.ts
Janus C. H. Knudsen 1ae4f00f2b Refactor renderer interfaces and update type names
Renames renderer interfaces to use 'I' prefix for clarity
Updates type references across related files
Improves type consistency in rendering strategies
2025-11-01 16:28:45 +01:00

380 lines
No EOL
14 KiB
TypeScript

import { EventBus } from '../core/EventBus';
import { IEventBus, CalendarEvent, RenderContext } from '../types/CalendarTypes';
import { CoreEvents } from '../constants/CoreEvents';
import { EventManager } from '../managers/EventManager';
import { IEventRenderer } from './EventRenderer';
import { SwpEventElement } from '../elements/SwpEventElement';
import { DragStartEventPayload, DragMoveEventPayload, DragEndEventPayload, DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload, DragMouseEnterColumnEventPayload, DragColumnChangeEventPayload, HeaderReadyEventPayload, ResizeEndEventPayload } from '../types/EventTypes';
import { DateService } from '../utils/DateService';
import { ColumnBounds } from '../utils/ColumnDetectionUtils';
/**
* EventRenderingService - Render events i DOM med positionering using Strategy Pattern
* Håndterer event positioning og overlap detection
*/
export class EventRenderingService {
private eventBus: IEventBus;
private eventManager: EventManager;
private strategy: IEventRenderer;
private dateService: DateService;
private dragMouseLeaveHeaderListener: ((event: Event) => void) | null = null;
constructor(
eventBus: IEventBus,
eventManager: EventManager,
strategy: IEventRenderer,
dateService: DateService
) {
this.eventBus = eventBus;
this.eventManager = eventManager;
this.strategy = strategy;
this.dateService = dateService;
this.setupEventListeners();
}
/**
* Render events in a specific container for a given period
*/
public renderEvents(context: RenderContext): void {
// Clear existing events in the specific container first
this.strategy.clearEvents(context.container);
// Get events from EventManager for the period
const events = 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) => {
this.handleGridRendered(event as CustomEvent);
});
this.eventBus.on(CoreEvents.VIEW_CHANGED, (event: Event) => {
this.handleViewChanged(event as CustomEvent);
});
// Handle all drag events and delegate to appropriate renderer
this.setupDragEventListeners();
}
/**
* Handle GRID_RENDERED event - render events in the current grid
*/
private handleGridRendered(event: CustomEvent): void {
const { container, startDate, endDate } = event.detail;
if (!container || !startDate || !endDate) {
return;
}
this.renderEvents({
container,
startDate,
endDate
});
}
/**
* Handle VIEW_CHANGED event - clear and re-render for new view
*/
private handleViewChanged(event: CustomEvent): void {
// Clear all existing events since view structure may have changed
this.clearEvents();
// New rendering will be triggered by subsequent GRID_RENDERED event
}
/**
* Setup all drag event listeners - moved from EventRenderer for better separation of concerns
*/
private setupDragEventListeners(): void {
this.setupDragStartListener();
this.setupDragMoveListener();
this.setupDragEndListener();
this.setupDragColumnChangeListener();
this.setupDragMouseLeaveHeaderListener();
this.setupDragMouseEnterColumnListener();
this.setupResizeEndListener();
this.setupNavigationCompletedListener();
}
private setupDragStartListener(): void {
this.eventBus.on('drag:start', (event: Event) => {
const dragStartPayload = (event as CustomEvent<DragStartEventPayload>).detail;
if (dragStartPayload.originalElement.hasAttribute('data-allday')) {
return;
}
if (dragStartPayload.originalElement && this.strategy.handleDragStart && dragStartPayload.columnBounds) {
this.strategy.handleDragStart(dragStartPayload);
}
});
}
private setupDragMoveListener(): void {
this.eventBus.on('drag:move', (event: Event) => {
let dragEvent = (event as CustomEvent<DragMoveEventPayload>).detail;
if (dragEvent.draggedClone.hasAttribute('data-allday')) {
return;
}
if (this.strategy.handleDragMove) {
this.strategy.handleDragMove(dragEvent);
}
});
}
private setupDragEndListener(): void {
this.eventBus.on('drag:end', (event: Event) => {
const { originalElement: draggedElement, sourceColumn, finalPosition, target } = (event as CustomEvent<DragEndEventPayload>).detail;
const finalColumn = finalPosition.column;
const finalY = finalPosition.snappedY;
const eventId = draggedElement.dataset.eventId || '';
// Only handle day column drops for EventRenderer
if (target === 'swp-day-column' && finalColumn) {
// Find dragged clone - use draggedElement as original
const draggedClone = document.querySelector(`swp-day-column swp-event[data-event-id="clone-${eventId}"]`) as HTMLElement;
if (draggedElement && draggedClone && this.strategy.handleDragEnd) {
this.strategy.handleDragEnd(eventId, draggedElement, draggedClone, finalColumn, finalY);
}
// Update event data in EventManager with new position from clone
if (draggedClone) {
const swpEvent = draggedClone as SwpEventElement;
const newStart = swpEvent.start;
const newEnd = swpEvent.end;
this.eventManager.updateEvent(eventId, {
start: newStart,
end: newEnd
});
console.log('📝 EventRendererManager: Updated event in EventManager', {
eventId,
newStart,
newEnd
});
}
// Re-render affected columns for stacking/grouping (now with updated data)
this.reRenderAffectedColumns(sourceColumn, finalColumn);
}
// Clean up any remaining day event clones
const dayEventClone = document.querySelector(`swp-day-column swp-event[data-event-id="clone-${eventId}"]`);
if (dayEventClone) {
dayEventClone.remove();
}
});
}
private setupDragColumnChangeListener(): void {
this.eventBus.on('drag:column-change', (event: Event) => {
let columnChangeEvent = (event as CustomEvent<DragColumnChangeEventPayload>).detail;
// Filter: Only handle events where clone is NOT an all-day event (normal timed events)
if (columnChangeEvent.draggedClone && columnChangeEvent.draggedClone.hasAttribute('data-allday')) {
return;
}
if (this.strategy.handleColumnChange) {
this.strategy.handleColumnChange(columnChangeEvent);
}
});
}
private setupDragMouseLeaveHeaderListener(): void {
this.dragMouseLeaveHeaderListener = (event: Event) => {
const { targetDate, mousePosition, originalElement, draggedClone: cloneElement } = (event as CustomEvent<DragMouseLeaveHeaderEventPayload>).detail;
if (cloneElement)
cloneElement.style.display = '';
console.log('🚪 EventRendererManager: Received drag:mouseleave-header', {
targetDate,
originalElement: originalElement,
cloneElement: cloneElement
});
};
this.eventBus.on('drag:mouseleave-header', this.dragMouseLeaveHeaderListener);
}
private setupDragMouseEnterColumnListener(): void {
this.eventBus.on('drag:mouseenter-column', (event: Event) => {
const payload = (event as CustomEvent<DragMouseEnterColumnEventPayload>).detail;
// Only handle if clone is an all-day event
if (!payload.draggedClone.hasAttribute('data-allday')) {
return;
}
console.log('🎯 EventRendererManager: Received drag:mouseenter-column', {
targetColumn: payload.targetColumn,
snappedY: payload.snappedY,
calendarEvent: payload.calendarEvent
});
// Delegate to strategy for conversion
if (this.strategy.handleConvertAllDayToTimed) {
this.strategy.handleConvertAllDayToTimed(payload);
}
});
}
private setupResizeEndListener(): void {
this.eventBus.on('resize:end', (event: Event) => {
const { eventId, element } = (event as CustomEvent<ResizeEndEventPayload>).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;
this.eventManager.updateEvent(eventId, {
start: newStart,
end: newEnd
});
console.log('📝 EventRendererManager: Updated event after resize', {
eventId,
newStart,
newEnd
});
// Find the column for this event
const columnElement = element.closest('swp-day-column') as HTMLElement;
if (columnElement) {
const columnDate = columnElement.dataset.date;
if (columnDate) {
// Re-render the column to recalculate stacking/grouping
this.renderSingleColumn(columnDate);
}
}
});
}
private setupNavigationCompletedListener(): void {
this.eventBus.on(CoreEvents.NAVIGATION_COMPLETED, () => {
// Delegate to strategy if it handles navigation
if (this.strategy.handleNavigationCompleted) {
this.strategy.handleNavigationCompleted();
}
});
}
/**
* Re-render affected columns after drag to recalculate stacking/grouping
*/
private reRenderAffectedColumns(sourceColumn: ColumnBounds | null, targetColumn: ColumnBounds | null): void {
const columnsToRender = new Set<string>();
// Add source column if exists
if (sourceColumn) {
columnsToRender.add(sourceColumn.date);
}
// Add target column if exists and different from source
if (targetColumn && targetColumn.date !== sourceColumn?.date) {
columnsToRender.add(targetColumn.date);
}
// Re-render each affected column
columnsToRender.forEach(columnDate => {
this.renderSingleColumn(columnDate);
});
}
/**
* Render events for a single column by re-rendering entire container
*/
private renderSingleColumn(columnDate: string): void {
// Find the column element
const columnElement = document.querySelector(`swp-day-column[data-date="${columnDate}"]`) as HTMLElement;
if (!columnElement) {
console.warn('EventRendererManager: Column not found', { columnDate });
return;
}
// Find the parent container (swp-day-columns)
const container = columnElement.closest('swp-day-columns') as HTMLElement;
if (!container) {
console.warn('EventRendererManager: Container not found');
return;
}
// Get all columns in container to determine date range
const allColumns = Array.from(container.querySelectorAll<HTMLElement>('swp-day-column'));
if (allColumns.length === 0) return;
// Get date range from first and last column
const firstColumnDate = allColumns[0].dataset.date;
const lastColumnDate = allColumns[allColumns.length - 1].dataset.date;
if (!firstColumnDate || !lastColumnDate) return;
const startDate = this.dateService.parseISO(`${firstColumnDate}T00:00:00`);
const endDate = this.dateService.parseISO(`${lastColumnDate}T23:59:59.999`);
// Re-render entire container (this will recalculate stacking for all columns)
this.renderEvents({
container,
startDate,
endDate
});
console.log('🔄 EventRendererManager: Re-rendered container for column', {
columnDate,
startDate: firstColumnDate,
endDate: lastColumnDate
});
}
private clearEvents(container?: HTMLElement): void {
this.strategy.clearEvents(container);
}
public refresh(container?: HTMLElement): void {
this.clearEvents(container);
}
}