import { EventBus } from '../core/EventBus'; import { IEventBus, CalendarEvent } from '../types/CalendarTypes'; import { EventTypes } from '../constants/EventTypes'; import { calendarConfig } from '../core/CalendarConfig'; /** * EventRenderer - Render events i DOM med positionering * Håndterer event positioning og overlap detection */ export class EventRenderer { private eventBus: IEventBus; constructor(eventBus: IEventBus) { this.eventBus = eventBus; this.setupEventListeners(); } private setupEventListeners(): void { this.eventBus.on(EventTypes.EVENTS_LOADED, (event: Event) => { const customEvent = event as CustomEvent; const { events } = customEvent.detail; // Store events but don't render yet - wait for grid to be ready this.pendingEvents = events; this.tryRenderEvents(); }); this.eventBus.on(EventTypes.GRID_RENDERED, () => { // Grid is ready, now we can render events this.tryRenderEvents(); }); this.eventBus.on(EventTypes.VIEW_RENDERED, () => { // Clear existing events when view changes this.clearEvents(); }); } private pendingEvents: CalendarEvent[] = []; private tryRenderEvents(): void { // Only render if we have both events and grid is ready if (this.pendingEvents.length > 0) { const dayColumns = document.querySelectorAll('swp-day-column'); if (dayColumns.length > 0) { this.renderEvents(this.pendingEvents); this.pendingEvents = []; // Clear pending events after rendering } } } private renderEvents(events: CalendarEvent[]): void { console.log(`EventRenderer: Rendering ${events.length} events`); // Clear existing events first this.clearEvents(); // Group events by day for better rendering const eventsByDay = this.groupEventsByDay(events); // Render events for each day Object.entries(eventsByDay).forEach(([dayIndex, dayEvents]) => { this.renderDayEvents(parseInt(dayIndex), dayEvents); }); this.eventBus.emit(EventTypes.EVENT_RENDERED, { count: events.length }); } private groupEventsByDay(events: CalendarEvent[]): Record { const grouped: Record = {}; events.forEach(event => { const day = event.metadata?.day || 0; if (!grouped[day]) { grouped[day] = []; } grouped[day].push(event); }); return grouped; } private renderDayEvents(dayIndex: number, events: CalendarEvent[]): void { // Sort events by start time const sortedEvents = events.sort((a, b) => a.start.localeCompare(b.start)); sortedEvents.forEach(event => { // Find the appropriate events container for this event const eventContainer = this.findEventContainer(event, dayIndex); if (eventContainer) { this.renderEvent(event, eventContainer); } }); } private findEventContainer(event: CalendarEvent, dayIndex: number): Element | null { // Debug: Log what we're looking for console.log(`EventRenderer: Looking for day ${dayIndex} using POC structure`); // Check what day columns actually exist const dayColumns = document.querySelectorAll('swp-day-column'); console.log(`EventRenderer: Found ${dayColumns.length} day columns total`); // Check first few columns to see their attributes for (let i = 0; i < Math.min(3, dayColumns.length); i++) { const column = dayColumns[i] as HTMLElement; console.log(`Column ${i}:`, { dayIndex: column.dataset.dayIndex, date: column.dataset.date, tagName: column.tagName }); } // Find the day column that corresponds to the event's day const dayColumn = document.querySelector(`swp-day-column[data-dayIndex="${dayIndex}"]`); if (!dayColumn) { console.warn(`EventRenderer: Day column for day ${dayIndex} not found`); return null; } // Find the events layer within this day column const eventsLayer = dayColumn.querySelector('swp-events-layer'); if (!eventsLayer) { console.warn(`EventRenderer: Events layer not found in day column for day ${dayIndex}`); return null; } return eventsLayer; } private renderEvent(event: CalendarEvent, container: Element): void { const eventElement = document.createElement('swp-event'); eventElement.dataset.eventId = event.id; eventElement.dataset.type = event.type; // Calculate position based on time const position = this.calculateEventPosition(event); eventElement.style.position = 'absolute'; eventElement.style.top = `${position.top}px`; eventElement.style.height = `${position.height}px`; eventElement.style.left = '2px'; eventElement.style.right = '2px'; eventElement.style.zIndex = '10'; // Format time for display const startTime = this.formatTime(event.start); const endTime = this.formatTime(event.end); // Create event content eventElement.innerHTML = ` ${startTime} - ${endTime} ${event.title} `; // Add event listeners this.addEventListeners(eventElement, event); container.appendChild(eventElement); } private calculateEventPosition(event: CalendarEvent): { top: number; height: number } { const startDate = new Date(event.start); const endDate = new Date(event.end); const startHour = calendarConfig.get('dayStartHour'); const hourHeight = calendarConfig.get('hourHeight'); // Calculate minutes from day start const startMinutes = (startDate.getHours() - startHour) * 60 + startDate.getMinutes(); const duration = (endDate.getTime() - startDate.getTime()) / (1000 * 60); // Duration in minutes // Convert to pixels const top = startMinutes * (hourHeight / 60); const height = duration * (hourHeight / 60); return { top, height }; } private formatTime(isoString: string): string { const date = new Date(isoString); const hours = date.getHours(); const minutes = date.getMinutes(); const period = hours >= 12 ? 'PM' : 'AM'; const displayHours = hours % 12 || 12; const displayMinutes = minutes.toString().padStart(2, '0'); return `${displayHours}:${displayMinutes} ${period}`; } private addEventListeners(eventElement: HTMLElement, event: CalendarEvent): void { // Click handler eventElement.addEventListener('click', (e) => { e.stopPropagation(); this.eventBus.emit(EventTypes.EVENT_SELECTED, { event, element: eventElement }); }); // Hover effects are handled by CSS eventElement.addEventListener('mouseenter', () => { eventElement.style.zIndex = '20'; }); eventElement.addEventListener('mouseleave', () => { eventElement.style.zIndex = '10'; }); } private clearEvents(): void { const eventsLayers = document.querySelectorAll('swp-events-layer'); eventsLayers.forEach(layer => { layer.innerHTML = ''; }); } public refresh(): void { // Request fresh events from EventManager this.eventBus.emit(EventTypes.REFRESH_REQUESTED); } public destroy(): void { this.clearEvents(); } }