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();
}
}