2025-07-24 22:17:38 +02:00
|
|
|
import { EventBus } from '../core/EventBus';
|
|
|
|
|
import { IEventBus, CalendarEvent } from '../types/CalendarTypes';
|
|
|
|
|
import { EventTypes } from '../constants/EventTypes';
|
|
|
|
|
import { calendarConfig } from '../core/CalendarConfig';
|
2025-08-02 23:59:52 +02:00
|
|
|
import { DateUtils } from '../utils/DateUtils';
|
2025-07-24 22:17:38 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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;
|
2025-08-02 00:28:45 +02:00
|
|
|
console.log('EventRenderer: Received EVENTS_LOADED with', events.length, 'events');
|
2025-07-25 23:31:25 +02:00
|
|
|
// 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();
|
2025-07-24 22:17:38 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.eventBus.on(EventTypes.VIEW_RENDERED, () => {
|
|
|
|
|
// Clear existing events when view changes
|
|
|
|
|
this.clearEvents();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-25 23:31:25 +02:00
|
|
|
private pendingEvents: CalendarEvent[] = [];
|
|
|
|
|
|
|
|
|
|
private tryRenderEvents(): void {
|
|
|
|
|
// Only render if we have both events and grid is ready
|
2025-08-02 00:28:45 +02:00
|
|
|
console.log('EventRenderer: tryRenderEvents called, pending events:', this.pendingEvents.length);
|
2025-07-25 23:31:25 +02:00
|
|
|
if (this.pendingEvents.length > 0) {
|
|
|
|
|
const dayColumns = document.querySelectorAll('swp-day-column');
|
2025-08-02 00:28:45 +02:00
|
|
|
console.log('EventRenderer: Found', dayColumns.length, 'day columns');
|
2025-07-25 23:31:25 +02:00
|
|
|
if (dayColumns.length > 0) {
|
|
|
|
|
this.renderEvents(this.pendingEvents);
|
|
|
|
|
this.pendingEvents = []; // Clear pending events after rendering
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-24 22:17:38 +02:00
|
|
|
private renderEvents(events: CalendarEvent[]): void {
|
2025-08-02 23:59:52 +02:00
|
|
|
console.log('EventRenderer: renderEvents called with', events.length, 'events');
|
2025-08-05 00:41:59 +02:00
|
|
|
console.log('EventRenderer: All events:', events.map(e => ({ title: e.title, allDay: e.allDay, start: e.start })));
|
2025-08-02 23:59:52 +02:00
|
|
|
|
2025-07-24 22:17:38 +02:00
|
|
|
// Clear existing events first
|
|
|
|
|
this.clearEvents();
|
|
|
|
|
|
2025-08-02 23:59:52 +02:00
|
|
|
// Get current week dates for filtering
|
|
|
|
|
const currentWeekDates = this.getCurrentWeekDates();
|
2025-08-05 00:41:59 +02:00
|
|
|
// Filter events for current week and exclude all-day events (handled by GridManager)
|
2025-08-02 23:59:52 +02:00
|
|
|
const currentWeekEvents = events.filter(event => {
|
2025-08-05 00:41:59 +02:00
|
|
|
// Skip all-day events - they are handled by GridManager
|
|
|
|
|
if (event.allDay) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-02 23:59:52 +02:00
|
|
|
const eventDate = new Date(event.start);
|
|
|
|
|
const eventDateStr = DateUtils.formatDate(eventDate);
|
|
|
|
|
const isInCurrentWeek = currentWeekDates.some(weekDate =>
|
|
|
|
|
DateUtils.formatDate(weekDate) === eventDateStr
|
|
|
|
|
);
|
|
|
|
|
return isInCurrentWeek;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
|
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
|
2025-07-24 22:17:38 +02:00
|
|
|
this.eventBus.emit(EventTypes.EVENT_RENDERED, {
|
2025-08-02 23:59:52 +02:00
|
|
|
count: currentWeekEvents.length
|
2025-07-24 22:17:38 +02:00
|
|
|
});
|
2025-08-05 00:41:59 +02:00
|
|
|
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-02 23:59:52 +02:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-24 22:17:38 +02:00
|
|
|
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);
|
2025-07-25 23:31:25 +02:00
|
|
|
eventElement.style.position = 'absolute';
|
2025-08-03 21:23:24 +02:00
|
|
|
eventElement.style.top = `${position.top + 1}px`;
|
|
|
|
|
eventElement.style.height = `${position.height - 1}px`;
|
2025-07-25 23:31:25 +02:00
|
|
|
eventElement.style.left = '2px';
|
|
|
|
|
eventElement.style.right = '2px';
|
|
|
|
|
eventElement.style.zIndex = '10';
|
2025-08-05 00:41:59 +02:00
|
|
|
|
|
|
|
|
// Style the event element
|
|
|
|
|
eventElement.style.backgroundColor = event.metadata?.color || '#3498db';
|
|
|
|
|
eventElement.style.color = 'white';
|
|
|
|
|
eventElement.style.fontSize = '12px';
|
|
|
|
|
eventElement.style.padding = '2px 4px';
|
|
|
|
|
eventElement.style.borderRadius = '3px';
|
|
|
|
|
eventElement.style.overflow = 'hidden';
|
|
|
|
|
eventElement.style.cursor = 'pointer';
|
2025-07-24 22:17:38 +02:00
|
|
|
|
|
|
|
|
// Format time for display
|
|
|
|
|
const startTime = this.formatTime(event.start);
|
|
|
|
|
const endTime = this.formatTime(event.end);
|
|
|
|
|
|
|
|
|
|
// Create event content
|
|
|
|
|
eventElement.innerHTML = `
|
|
|
|
|
<swp-event-time>${startTime} - ${endTime}</swp-event-time>
|
|
|
|
|
<swp-event-title>${event.title}</swp-event-title>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// Add event listeners
|
|
|
|
|
this.addEventListeners(eventElement, event);
|
|
|
|
|
|
|
|
|
|
container.appendChild(eventElement);
|
2025-08-05 00:41:59 +02:00
|
|
|
|
|
|
|
|
// Event successfully rendered
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private calculateEventPosition(event: CalendarEvent): { top: number; height: number } {
|
|
|
|
|
const startDate = new Date(event.start);
|
|
|
|
|
const endDate = new Date(event.end);
|
|
|
|
|
|
2025-08-05 00:41:59 +02:00
|
|
|
// Use dayStartHour to match time-axis positioning - this is the visible start hour (6 AM)
|
|
|
|
|
const dayStartHour = calendarConfig.get('dayStartHour'); // 6 (6 AM)
|
2025-07-24 22:17:38 +02:00
|
|
|
const hourHeight = calendarConfig.get('hourHeight');
|
|
|
|
|
|
2025-08-05 00:41:59 +02:00
|
|
|
// Calculate minutes from visible day start (6 AM, not midnight)
|
2025-08-02 23:59:52 +02:00
|
|
|
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);
|
2025-07-24 22:17:38 +02:00
|
|
|
|
2025-08-05 00:41:59 +02:00
|
|
|
// Convert to pixels - position relative to visible time-grid (starts at dayStartHour)
|
|
|
|
|
const top = startMinutes * (hourHeight / 60);
|
2025-07-24 22:17:38 +02:00
|
|
|
const height = duration * (hourHeight / 60);
|
|
|
|
|
|
2025-08-02 23:59:52 +02:00
|
|
|
|
2025-07-24 22:17:38 +02:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|