import { CalendarConfig } from '../core/CalendarConfig'; import { ResourceCalendarData, CalendarView } from '../types/CalendarTypes'; import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; import { HeaderRenderContext } from './HeaderRenderer'; import { ColumnRenderContext } from './ColumnRenderer'; import { eventBus } from '../core/EventBus'; import { DateCalculator } from '../utils/DateCalculator'; /** * GridRenderer - Centralized DOM rendering for calendar grid * Optimized to reduce redundant DOM operations and improve performance */ export class GridRenderer { private config: CalendarConfig; private headerEventListener: ((event: Event) => void) | null = null; private cachedGridContainer: HTMLElement | null = null; private cachedCalendarHeader: HTMLElement | null = null; private cachedTimeAxis: HTMLElement | null = null; constructor(config: CalendarConfig) { this.config = config; } /** * 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); } 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 = this.config.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 with caching const calendarHeader = document.createElement('swp-calendar-header'); this.renderCalendarHeader(calendarHeader, currentDate, resourceData, view); this.cachedCalendarHeader = calendarHeader; 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); return gridContainer; } /** * Render calendar header with view awareness */ private renderCalendarHeader( calendarHeader: HTMLElement, currentDate: Date, resourceData: ResourceCalendarData | null, view: CalendarView ): void { const calendarType = this.config.getCalendarMode(); const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType); const context: HeaderRenderContext = { currentWeek: currentDate, // HeaderRenderer expects currentWeek property config: this.config, resourceData: resourceData }; headerRenderer.render(calendarHeader, context); // Always ensure all-day containers exist for all days headerRenderer.ensureAllDayContainers(calendarHeader); // Setup optimized event listener this.setupOptimizedHeaderEventListener(calendarHeader); } /** * Render column container with view awareness */ private renderColumnContainer( columnContainer: HTMLElement, currentDate: Date, resourceData: ResourceCalendarData | null, view: CalendarView ): void { const calendarType = this.config.getCalendarMode(); const columnRenderer = CalendarTypeFactory.getColumnRenderer(calendarType); const context: ColumnRenderContext = { currentWeek: currentDate, // ColumnRenderer expects currentWeek property config: this.config, 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 { // Use cached elements if available const calendarHeader = this.cachedCalendarHeader || grid.querySelector('swp-calendar-header'); if (calendarHeader) { // Clear and re-render header content calendarHeader.innerHTML = ''; this.renderCalendarHeader(calendarHeader as HTMLElement, currentDate, resourceData, view); } // 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 optimized event delegation listener with better performance */ private setupOptimizedHeaderEventListener(calendarHeader: HTMLElement): void { // Remove existing listener if any if (this.headerEventListener) { calendarHeader.removeEventListener('mouseover', this.headerEventListener); } // Create optimized listener with throttling let lastEmitTime = 0; const throttleDelay = 16; // ~60fps this.headerEventListener = (event) => { const now = Date.now(); if (now - lastEmitTime < throttleDelay) { return; // Throttle events for better performance } lastEmitTime = now; const target = event.target as HTMLElement; // Optimized element detection const dayHeader = target.closest('swp-day-header'); const allDayContainer = target.closest('swp-allday-container'); if (dayHeader || allDayContainer) { let hoveredElement: HTMLElement; let targetDate: string | undefined; if (dayHeader) { hoveredElement = dayHeader as HTMLElement; targetDate = hoveredElement.dataset.date; } else if (allDayContainer) { hoveredElement = allDayContainer as HTMLElement; // Optimized day calculation using cached header rect const headerRect = calendarHeader.getBoundingClientRect(); const dayHeaders = calendarHeader.querySelectorAll('swp-day-header'); const mouseX = (event as MouseEvent).clientX - headerRect.left; const dayWidth = headerRect.width / dayHeaders.length; const dayIndex = Math.max(0, Math.min(dayHeaders.length - 1, Math.floor(mouseX / dayWidth))); const targetDayHeader = dayHeaders[dayIndex] as HTMLElement; targetDate = targetDayHeader?.dataset.date; } else { return; } // Get header renderer once and cache const calendarType = this.config.getCalendarMode(); const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType); eventBus.emit('header:mouseover', { element: hoveredElement, targetDate, headerRenderer }); } }; // Add the optimized listener calendarHeader.addEventListener('mouseover', this.headerEventListener); } /** * Clean up cached elements and event listeners */ public destroy(): void { // Clean up event listeners if (this.headerEventListener && this.cachedCalendarHeader) { this.cachedCalendarHeader.removeEventListener('mouseover', this.headerEventListener); } // Clear cached references this.cachedGridContainer = null; this.cachedCalendarHeader = null; this.cachedTimeAxis = null; this.headerEventListener = null; } }