import { calendarConfig } from '../core/CalendarConfig'; import { ResourceCalendarData, CalendarView } from '../types/CalendarTypes'; import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; import { ColumnRenderContext } from './ColumnRenderer'; import { eventBus } from '../core/EventBus'; import { DateService } from '../utils/DateService'; import { CoreEvents } from '../constants/CoreEvents'; /** * GridRenderer - Centralized DOM rendering for calendar grid * Optimized to reduce redundant DOM operations and improve performance */ export class GridRenderer { private cachedGridContainer: HTMLElement | null = null; private cachedTimeAxis: HTMLElement | null = null; private dateService: DateService; constructor() { const timezone = calendarConfig.getTimezone?.() || 'Europe/Copenhagen'; this.dateService = new DateService(timezone); } /** * Render the complete grid structure with view-aware optimization */ public renderGrid( grid: HTMLElement, currentDate: Date, resourceData: ResourceCalendarData | null, view: CalendarView = 'week' ): void { if (!grid || !currentDate) { return; } // Cache grid reference for performance this.cachedGridContainer = grid; // Only clear and rebuild if grid is empty (first render) if (grid.children.length === 0) { this.createCompleteGridStructure(grid, currentDate, resourceData, view); // Setup grid-related event listeners on first render // this.setupGridEventListeners(); } else { // Optimized update - only refresh dynamic content this.updateGridContent(grid, currentDate, resourceData, view); } } /** * Create complete grid structure in one operation */ private createCompleteGridStructure( grid: HTMLElement, currentDate: Date, resourceData: ResourceCalendarData | null, view: CalendarView ): void { // Create all elements in memory first for better performance const fragment = document.createDocumentFragment(); // Create header spacer const headerSpacer = document.createElement('swp-header-spacer'); fragment.appendChild(headerSpacer); // Create time axis with caching const timeAxis = this.createOptimizedTimeAxis(); this.cachedTimeAxis = timeAxis; fragment.appendChild(timeAxis); // Create grid container with caching const gridContainer = this.createOptimizedGridContainer(currentDate, resourceData, view); this.cachedGridContainer = gridContainer; fragment.appendChild(gridContainer); // Append all at once to minimize reflows grid.appendChild(fragment); } /** * Create optimized time axis with caching */ private createOptimizedTimeAxis(): HTMLElement { const timeAxis = document.createElement('swp-time-axis'); const timeAxisContent = document.createElement('swp-time-axis-content'); const gridSettings = calendarConfig.getGridSettings(); const startHour = gridSettings.dayStartHour; const endHour = gridSettings.dayEndHour; // Create all hour markers in memory first const fragment = document.createDocumentFragment(); for (let hour = startHour; hour < endHour; hour++) { const marker = document.createElement('swp-hour-marker'); const period = hour >= 12 ? 'PM' : 'AM'; const displayHour = hour > 12 ? hour - 12 : (hour === 0 ? 12 : hour); marker.textContent = `${displayHour} ${period}`; fragment.appendChild(marker); } timeAxisContent.appendChild(fragment); timeAxis.appendChild(timeAxisContent); return timeAxis; } /** * Create optimized grid container with header and scrollable content */ private createOptimizedGridContainer( currentDate: Date, resourceData: ResourceCalendarData | null, view: CalendarView ): HTMLElement { const gridContainer = document.createElement('swp-grid-container'); // Create calendar header as first child - always exists now! const calendarHeader = document.createElement('swp-calendar-header'); gridContainer.appendChild(calendarHeader); // Create scrollable content structure const scrollableContent = document.createElement('swp-scrollable-content'); const timeGrid = document.createElement('swp-time-grid'); // Add grid lines const gridLines = document.createElement('swp-grid-lines'); timeGrid.appendChild(gridLines); // Create column container const columnContainer = document.createElement('swp-day-columns'); this.renderColumnContainer(columnContainer, currentDate, resourceData, view); timeGrid.appendChild(columnContainer); scrollableContent.appendChild(timeGrid); gridContainer.appendChild(scrollableContent); console.log('✅ GridRenderer: Created grid container with header'); return gridContainer; } /** * Render column container with view awareness */ private renderColumnContainer( columnContainer: HTMLElement, currentDate: Date, resourceData: ResourceCalendarData | null, view: CalendarView ): void { const calendarType = calendarConfig.getCalendarMode(); const columnRenderer = CalendarTypeFactory.getColumnRenderer(calendarType); const context: ColumnRenderContext = { currentWeek: currentDate, // ColumnRenderer expects currentWeek property config: calendarConfig, resourceData: resourceData }; columnRenderer.render(columnContainer, context); } /** * Optimized update of grid content without full rebuild */ private updateGridContent( grid: HTMLElement, currentDate: Date, resourceData: ResourceCalendarData | null, view: CalendarView ): void { // Update column container if needed const columnContainer = grid.querySelector('swp-day-columns'); if (columnContainer) { columnContainer.innerHTML = ''; this.renderColumnContainer(columnContainer as HTMLElement, currentDate, resourceData, view); } } /** * Setup grid-only event listeners (column events) private setupGridEventListeners(): void { // Setup grid body mouseover listener for all-day to timed conversion this.setupGridBodyMouseOver(); } */ /** * Setup grid body mouseover listener for all-day to timed conversion private setupGridBodyMouseOver(): void { const grid = this.cachedGridContainer; if (!grid) return; const columnContainer = grid.querySelector('swp-day-columns'); if (!columnContainer) return; // Throttle for better performance let lastEmitTime = 0; const throttleDelay = 16; // ~60fps const gridBodyEventListener = (event: Event) => { const now = Date.now(); if (now - lastEmitTime < throttleDelay) { return; } lastEmitTime = now; const target = event.target as HTMLElement; const dayColumn = target.closest('swp-day-column'); if (dayColumn) { const targetColumn = (dayColumn as HTMLElement).dataset.date; if (targetColumn) { // Calculate Y position relative to the column const columnRect = dayColumn.getBoundingClientRect(); const mouseY = (event as MouseEvent).clientY; const targetY = mouseY - columnRect.top; eventBus.emit('column:mouseover', { targetColumn, targetY }); } } }; columnContainer.addEventListener('mouseover', gridBodyEventListener); // Store reference for cleanup (this as any).gridBodyEventListener = gridBodyEventListener; (this as any).cachedColumnContainer = columnContainer; } */ /** * Create navigation grid container for slide animations * Now uses same implementation as initial load for consistency */ public createNavigationGrid(parentContainer: HTMLElement, weekStart: Date): HTMLElement { console.group('🔧 GridRenderer.createNavigationGrid'); console.log('Week start:', weekStart); console.log('Parent container:', parentContainer); console.log('Using same grid creation as initial load'); const weekEnd = this.dateService.addDays(weekStart, 6); // Use SAME method as initial load - respects workweek and resource settings const newGrid = this.createOptimizedGridContainer(weekStart, null, 'week'); // Position new grid for animation - NO transform here, let Animation API handle it newGrid.style.position = 'absolute'; newGrid.style.top = '0'; newGrid.style.left = '0'; newGrid.style.width = '100%'; newGrid.style.height = '100%'; // Add to parent container parentContainer.appendChild(newGrid); console.log('Grid created using createOptimizedGridContainer:', newGrid); console.log('Emitting GRID_RENDERED'); eventBus.emit(CoreEvents.GRID_RENDERED, { container: newGrid, // Specific grid container, not parent currentDate: weekStart, startDate: weekStart, endDate: weekEnd, isNavigation: true // Flag to indicate this is navigation rendering }); console.groupEnd(); return newGrid; } }