import { EventBus } from '../core/EventBus'; import { IEventBus, CalendarEvent, RenderContext } from '../types/CalendarTypes'; import { CoreEvents } from '../constants/CoreEvents'; import { calendarConfig } from '../core/CalendarConfig'; import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; import { EventManager } from '../managers/EventManager'; import { EventRendererStrategy } 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: EventRendererStrategy; private dateService: DateService; private dragMouseLeaveHeaderListener: ((event: Event) => void) | null = null; constructor(eventBus: IEventBus, eventManager: EventManager) { this.eventBus = eventBus; this.eventManager = eventManager; // Cache strategy at initialization const calendarType = calendarConfig.getCalendarMode(); this.strategy = CalendarTypeFactory.getEventRenderer(calendarType); // Initialize DateService const timezone = calendarConfig.getTimezone?.(); this.dateService = new DateService(timezone); 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).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', (event: Event) => { const { originalElement: draggedElement, sourceColumn, finalPosition, target } = (event as CustomEvent).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).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).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).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).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(); // 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('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); } }