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'; /** * 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; constructor() { } /** * 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 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); 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; } */ /** * Clean up cached elements and event listeners */ public destroy(): void { // Clean up grid-only event listeners // if ((this as any).gridBodyEventListener && (this as any).cachedColumnContainer) { // (this as any).cachedColumnContainer.removeEventListener('mouseover', (this as any).gridBodyEventListener); //} // Clear cached references this.cachedGridContainer = null; this.cachedTimeAxis = null; (this as any).gridBodyEventListener = null; (this as any).cachedColumnContainer = null; } }