import { EventBus } from '../core/EventBus'; import { IEventBus, CalendarEvent } from '../types/CalendarTypes'; import { EventTypes } from '../constants/EventTypes'; import { calendarConfig } from '../core/CalendarConfig'; import { DateUtils } from '../utils/DateUtils'; /** * 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; console.log('EventRenderer: Received EVENTS_LOADED with', events.length, 'events'); // 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 console.log('EventRenderer: tryRenderEvents called, pending events:', this.pendingEvents.length); if (this.pendingEvents.length > 0) { const dayColumns = document.querySelectorAll('swp-day-column'); console.log('EventRenderer: Found', dayColumns.length, 'day columns'); if (dayColumns.length > 0) { this.renderEvents(this.pendingEvents); this.pendingEvents = []; // Clear pending events after rendering } } } private renderEvents(events: CalendarEvent[]): void { console.log('EventRenderer: renderEvents called with', events.length, 'events'); // Clear existing events first this.clearEvents(); // Get current week dates for filtering const currentWeekDates = this.getCurrentWeekDates(); console.log('EventRenderer: Current week dates:', currentWeekDates.map(d => DateUtils.formatDate(d))); // Filter events for current week const currentWeekEvents = events.filter(event => { const eventDate = new Date(event.start); const eventDateStr = DateUtils.formatDate(eventDate); const isInCurrentWeek = currentWeekDates.some(weekDate => DateUtils.formatDate(weekDate) === eventDateStr ); console.log('EventRenderer: Event', event.title, 'on', eventDateStr, 'is in current week:', isInCurrentWeek); return isInCurrentWeek; }); console.log('EventRenderer: Rendering', currentWeekEvents.length, 'events for current week'); // Render each event in the correct day column currentWeekEvents.forEach(event => { const eventDate = new Date(event.start); const dayColumn = this.findDayColumn(eventDate); if (dayColumn) { const eventsLayer = dayColumn.querySelector('swp-events-layer'); if (eventsLayer) { console.log('EventRenderer: Rendering event', event.title, 'in day column for', DateUtils.formatDate(eventDate)); this.renderEvent(event, eventsLayer); } else { console.warn('EventRenderer: No events layer found in day column for', DateUtils.formatDate(eventDate)); } } else { console.warn('EventRenderer: No day column found for event date', DateUtils.formatDate(eventDate)); } }); // Emit event rendered this.eventBus.emit(EventTypes.EVENT_RENDERED, { count: currentWeekEvents.length }); } /** * Get current week dates (Sunday to Saturday) */ private getCurrentWeekDates(): Date[] { const today = new Date(); const weekStart = DateUtils.getWeekStart(today, 0); // Sunday start const dates: Date[] = []; for (let i = 0; i < 7; i++) { const date = DateUtils.addDays(weekStart, i); dates.push(date); } return dates; } /** * Find day column for specific date */ private findDayColumn(date: Date): HTMLElement | null { const dateStr = DateUtils.formatDate(date); const dayColumn = document.querySelector(`swp-day-column[data-date="${dateStr}"]`) as HTMLElement; console.log('EventRenderer: Looking for day column with date', dateStr, 'found:', !!dayColumn); return dayColumn; } 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); // Use dayStartHour to match time-axis positioning const dayStartHour = calendarConfig.get('dayStartHour'); // 0 (midnight) const hourHeight = calendarConfig.get('hourHeight'); // Calculate minutes from day start (midnight) const eventHour = startDate.getHours(); const eventMinutes = startDate.getMinutes(); const startMinutes = (eventHour - dayStartHour) * 60 + eventMinutes; // Calculate duration in minutes const duration = (endDate.getTime() - startDate.getTime()) / (1000 * 60); // Convert to pixels - this gives absolute position from top of time-grid const absoluteTop = startMinutes * (hourHeight / 60); const height = duration * (hourHeight / 60); // Get current scroll position to adjust for viewport const scrollableContent = document.querySelector('swp-scrollable-content') as HTMLElement; const scrollTop = scrollableContent ? scrollableContent.scrollTop : 0; // Calculate relative position within the visible viewport // Events are positioned relative to their day-column, not the scrollable content // So we use the absolute position directly const top = absoluteTop; console.log('EventRenderer: Position calculation for', event.title, { eventTime: `${eventHour}:${eventMinutes.toString().padStart(2, '0')}`, dayStartHour, startMinutes, duration, absoluteTop, scrollTop, finalTop: top, height }); 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(); } }