import { ICalendarEvent, IEventBus } from '../../types/CalendarTypes'; import { EventService } from '../../storage/events/EventService'; import { DateService } from '../../core/DateService'; import { IGridConfig } from '../../core/IGridConfig'; import { calculateEventPosition } from '../../utils/PositionUtils'; import { CoreEvents } from '../../constants/CoreEvents'; import { IDragColumnChangePayload } from '../../types/DragTypes'; /** * EventRenderer - Renders calendar events to the DOM * * CLEAN approach: * - Only data-id attribute on event element * - innerHTML contains only visible content * - Event data retrieved via EventService when needed */ export class EventRenderer { constructor( private eventService: EventService, private dateService: DateService, private gridConfig: IGridConfig, private eventBus: IEventBus ) { this.setupDragListeners(); } /** * Setup listeners for drag-drop events */ private setupDragListeners(): void { this.eventBus.on(CoreEvents.EVENT_DRAG_COLUMN_CHANGE, (e) => { const payload = (e as CustomEvent).detail; this.handleColumnChange(payload); }); } /** * Handle event moving to a new column during drag */ private handleColumnChange(payload: IDragColumnChangePayload): void { const eventsLayer = payload.newColumn.querySelector('swp-events-layer'); if (!eventsLayer) return; // Move element to new column eventsLayer.appendChild(payload.element); // Preserve Y position payload.element.style.top = `${payload.currentY}px`; } /** * Render events for visible dates into day columns * @param container - Calendar container element * @param filter - Filter with 'date' and optionally 'resource' arrays */ async render(container: HTMLElement, filter: Record): Promise { const visibleDates = filter['date'] || []; if (visibleDates.length === 0) return; // Get date range for query const startDate = new Date(visibleDates[0]); const endDate = new Date(visibleDates[visibleDates.length - 1]); endDate.setHours(23, 59, 59, 999); // Fetch events from IndexedDB const events = await this.eventService.getByDateRange(startDate, endDate); // Find day columns const dayColumns = container.querySelector('swp-day-columns'); if (!dayColumns) return; const columns = dayColumns.querySelectorAll('swp-day-column'); // Render events into each column based on data attributes columns.forEach(column => { const dateKey = (column as HTMLElement).dataset.date; const columnResourceId = (column as HTMLElement).dataset.resourceId; if (!dateKey) return; // Filter events for this column const columnEvents = events.filter(event => { // Must match date if (this.dateService.getDateKey(event.start) !== dateKey) return false; // If column has resourceId, event must match if (columnResourceId && event.resourceId !== columnResourceId) return false; // If no resourceId on column but resources in filter, show all // (this handles 'simple' view without resources) return true; }); // Get or create events layer let eventsLayer = column.querySelector('swp-events-layer'); if (!eventsLayer) { eventsLayer = document.createElement('swp-events-layer'); column.appendChild(eventsLayer); } // Clear existing events eventsLayer.innerHTML = ''; // Render each timed event columnEvents.forEach(event => { if (!event.allDay) { const eventElement = this.createEventElement(event); eventsLayer!.appendChild(eventElement); } }); }); } /** * Create a single event element * * CLEAN approach: * - Only data-id for lookup * - Visible content in innerHTML only */ private createEventElement(event: ICalendarEvent): HTMLElement { const element = document.createElement('swp-event'); // Only essential data attribute element.dataset.id = event.id; // Calculate position const position = calculateEventPosition(event.start, event.end, this.gridConfig); element.style.top = `${position.top}px`; element.style.height = `${position.height}px`; // Color class based on event type const colorClass = this.getColorClass(event); if (colorClass) { element.classList.add(colorClass); } // Visible content only element.innerHTML = ` ${this.dateService.formatTimeRange(event.start, event.end)} ${this.escapeHtml(event.title)} ${event.description ? `${this.escapeHtml(event.description)}` : ''} `; return element; } /** * Get color class based on event type */ private getColorClass(event: ICalendarEvent): string { const typeColors: Record = { 'customer': 'is-blue', 'vacation': 'is-green', 'break': 'is-amber', 'meeting': 'is-purple', 'blocked': 'is-red' }; return typeColors[event.type] || 'is-blue'; } /** * Escape HTML to prevent XSS */ private escapeHtml(text: string): string { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } }