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, IResizeEndEventPayload } from '../types/EventTypes'; import { DateService } from '../utils/DateService'; /** * 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 dataSource: IColumnDataSource; private dateService: DateService; private dragMouseLeaveHeaderListener: ((event: Event) => void) | null = null; constructor( 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(); } 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 * Events are now pre-filtered per column by IColumnDataSource */ private handleGridRendered(event: CustomEvent): void { const { container, columns } = event.detail; if (!container || !columns || columns.length === 0) { return; } // Render events directly from columns (pre-filtered by IColumnDataSource) this.renderEventsFromColumns(container, columns); } /** * 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); // Emit EVENTS_RENDERED for filtering system const allEvents = columns.flatMap(col => col.events); this.eventBus.emit(CoreEvents.EVENTS_RENDERED, { events: allEvents, container: container }); } /** * 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).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).detail; if (dragEvent.draggedClone.hasAttribute('data-allday')) { return; } if (this.strategy.handleDragMove) { this.strategy.handleDragMove(dragEvent); } }); } private setupDragEndListener(): void { this.eventBus.on('drag:end', async (event: Event) => { const { originalElement, draggedClone, finalPosition, target } = (event as CustomEvent).detail; const finalColumn = finalPosition.column; const finalY = finalPosition.snappedY; // 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); } // Build update payload based on mode const updatePayload: { start: Date; end: Date; allDay: boolean; resourceId?: string } = { start: element.start, end: element.end, allDay: false }; 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, {}); } }); } private setupDragColumnChangeListener(): void { this.eventBus.on('drag:column-change', (event: Event) => { let columnChangeEvent = (event as CustomEvent).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 { targetColumn, mousePosition, originalElement, draggedClone: cloneElement } = (event as CustomEvent).detail; if (cloneElement) cloneElement.style.display = ''; console.log('🚪 EventRendererManager: Received drag:mouseleave-header', { targetColumn: targetColumn?.identifier, 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).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', async (event: Event) => { const { eventId, element } = (event as CustomEvent).detail; const swpEvent = element as SwpEventElement; await this.eventManager.updateEvent(eventId, { start: swpEvent.start, end: swpEvent.end }); // Trigger full refresh to re-render with updated data this.eventBus.emit(CoreEvents.REFRESH_REQUESTED, {}); }); } private setupNavigationCompletedListener(): void { this.eventBus.on(CoreEvents.NAVIGATION_COMPLETED, () => { // Delegate to strategy if it handles navigation if (this.strategy.handleNavigationCompleted) { this.strategy.handleNavigationCompleted(); } }); } private clearEvents(container?: HTMLElement): void { this.strategy.clearEvents(container); } public refresh(container?: HTMLElement): void { this.clearEvents(container); } }