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 { AllDayEventRenderer } from './AllDayEventRenderer'; import { DragStartEventPayload, DragMoveEventPayload, DragEndEventPayload, DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload, HeaderReadyEventPayload } from '../types/EventTypes'; /** * 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 allDayEventRenderer: AllDayEventRenderer; 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 all-day event renderer this.allDayEventRenderer = new AllDayEventRenderer(); 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); }); // Listen for header ready - when dates are populated with period data this.eventBus.on('header:ready', (event: Event) => { const { startDate, endDate } = (event as CustomEvent).detail; console.log('๐ŸŽฏ EventRendererManager: Header ready with period data', { startDate: startDate.toISOString(), endDate: endDate.toISOString() }); // Render all-day events using period from header this.renderAllDayEventsForPeriod(startDate, endDate); }); // Handle all drag events and delegate to appropriate renderer this.setupDragEventListeners(); // Listen for conversion from all-day event to time event this.eventBus.on('drag:convert-to-time_event', (event: Event) => { const { draggedElement, mousePosition, column } = (event as CustomEvent).detail; console.log('๐Ÿ”„ EventRendererManager: Received drag:convert-to-time_event', { draggedElement: draggedElement?.dataset.eventId, mousePosition, column }); this.handleConvertToTimeEvent(draggedElement, mousePosition, column); }); } /** * Handle GRID_RENDERED event - render events in the current grid */ private handleGridRendered(event: CustomEvent): void { const { container, startDate, endDate, currentDate, isNavigation } = event.detail; if (!container) { return; } let periodStart: Date; let periodEnd: Date; if (startDate && endDate) { // Direct date format - use as provided periodStart = startDate; periodEnd = endDate; } else if (currentDate) { return; } else { return; } this.renderEvents({ container: container, startDate: periodStart, endDate: periodEnd }); } /** * Handle CONTAINER_READY_FOR_EVENTS event - render events in pre-rendered container */ private handleContainerReady(event: CustomEvent): void { const { container, startDate, endDate } = event.detail; if (!container || !startDate || !endDate) { return; } this.renderEvents({ container: container, startDate: new Date(startDate), endDate: new Date(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 { // Handle drag start this.eventBus.on('drag:start', (event: Event) => { const { draggedElement, mouseOffset, column } = (event as CustomEvent).detail; // Use the draggedElement directly - no need for DOM query if (draggedElement && this.strategy.handleDragStart && column) { const eventId = draggedElement.dataset.eventId || ''; this.strategy.handleDragStart(draggedElement, eventId, mouseOffset, column); } }); // Handle drag move this.eventBus.on('drag:move', (event: Event) => { const { draggedElement, snappedY, column, mouseOffset } = (event as CustomEvent).detail; if (this.strategy.handleDragMove && column) { const eventId = draggedElement.dataset.eventId || ''; this.strategy.handleDragMove(eventId, snappedY, column, mouseOffset); } }); // Handle drag auto-scroll this.eventBus.on('drag:auto-scroll', (event: Event) => { const { draggedElement, snappedY } = (event as CustomEvent).detail; if (this.strategy.handleDragAutoScroll) { const eventId = draggedElement.dataset.eventId || ''; this.strategy.handleDragAutoScroll(eventId, snappedY); } }); // Handle drag end events and delegate to appropriate renderer this.eventBus.on('drag:end', (event: Event) => { const { draggedElement, 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); } } // 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(); } }); // Handle click (when drag threshold not reached) this.eventBus.on('event:click', (event: Event) => { const { draggedElement } = (event as CustomEvent).detail; // Use draggedElement directly - no need for DOM query if (draggedElement && this.strategy.handleEventClick) { const eventId = draggedElement.dataset.eventId || ''; this.strategy.handleEventClick(eventId, draggedElement); } }); // Handle column change this.eventBus.on('drag:column-change', (event: Event) => { const { draggedElement, newColumn } = (event as CustomEvent).detail; if (this.strategy.handleColumnChange) { const eventId = draggedElement.dataset.eventId || ''; this.strategy.handleColumnChange(eventId, newColumn); } }); this.dragMouseLeaveHeaderListener = (event: Event) => { const { targetDate, mousePosition, originalElement, 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); // Handle navigation period change this.eventBus.on(CoreEvents.NAVIGATION_COMPLETED, () => { // Delegate to strategy if it handles navigation if (this.strategy.handleNavigationCompleted) { this.strategy.handleNavigationCompleted(); } }); } /** * Handle conversion from all-day event to time event */ private handleConvertToTimeEvent(draggedElement: HTMLElement, mousePosition: any, column: string): void { // Use the provided draggedElement directly const allDayClone = draggedElement; const draggedEventId = draggedElement?.dataset.eventId?.replace('clone-', '') || ''; // Use SwpEventElement factory to create day event from all-day event const dayEventElement = SwpEventElement.fromAllDayElement(allDayClone as HTMLElement); const dayElement = dayEventElement.getElement(); // Remove the all-day clone - it's no longer needed since we're converting to day event allDayClone.remove(); // Set clone ID dayElement.dataset.eventId = `clone-${draggedEventId}`; // Find target column const columnElement = document.querySelector(`swp-day-column[data-date="${column}"]`); if (!columnElement) { console.warn('EventRendererManager: Target column not found', { column }); return; } // Find events layer in the column const eventsLayer = columnElement.querySelector('swp-events-layer'); if (!eventsLayer) { console.warn('EventRendererManager: Events layer not found in column'); return; } // Add to events layer eventsLayer.appendChild(dayElement); // Position based on mouse Y coordinate const columnRect = columnElement.getBoundingClientRect(); const relativeY = Math.max(0, mousePosition.y - columnRect.top); dayElement.style.top = `${relativeY}px`; // Set drag styling dayElement.style.zIndex = '1000'; dayElement.style.cursor = 'grabbing'; dayElement.style.opacity = ''; dayElement.style.transform = ''; console.log('โœ… EventRendererManager: Converted all-day event to time event', { draggedEventId, column, mousePosition, relativeY }); } /** * Render all-day events for specific period using AllDayEventRenderer */ private renderAllDayEventsForPeriod(startDate: Date, endDate: Date): void { // Get events from EventManager for the period const events = this.eventManager.getEventsForPeriod(startDate, endDate); // Filter for all-day events const allDayEvents = events.filter(event => event.allDay); console.log('๐Ÿ—๏ธ EventRenderingService: Rendering all-day events', { period: { start: startDate.toISOString(), end: endDate.toISOString() }, count: allDayEvents.length, events: allDayEvents.map(e => ({ id: e.id, title: e.title })) }); // Clear existing all-day events first this.clearAllDayEvents(); // Render each all-day event allDayEvents.forEach(event => { const renderedElement = this.allDayEventRenderer.renderAllDayEvent(event); if (renderedElement) { console.log('โœ… EventRenderingService: Rendered all-day event', { id: event.id, title: event.title, element: renderedElement.tagName }); } else { console.warn('โŒ EventRenderingService: Failed to render all-day event', { id: event.id, title: event.title }); } }); // Check and adjust all-day container height after rendering this.eventBus.emit('allday:checkHeight'); } /** * Clear only all-day events */ private clearAllDayEvents(): void { const allDayContainer = document.querySelector('swp-allday-container'); if (allDayContainer) { allDayContainer.querySelectorAll('swp-event').forEach(event => event.remove()); } } private clearEvents(container?: HTMLElement): void { this.strategy.clearEvents(container); // Also clear all-day events this.clearAllDayEvents(); } public refresh(container?: HTMLElement): void { // Clear events in specific container or globally this.clearEvents(container); } public destroy(): void { this.clearEvents(); } }