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 { DateCalculator } from '../utils/DateCalculator'; 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; 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 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; } */ /** * 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; } /** * Create navigation grid container for slide animations * Moved from NavigationRenderer to centralize grid creation */ public createNavigationGrid(parentContainer: HTMLElement, weekStart: Date): HTMLElement { console.group('🔧 GridRenderer.createNavigationGrid'); console.log('Week start:', weekStart); console.log('Parent container:', parentContainer); const weekEnd = DateCalculator.addDays(weekStart, 6); // Create new grid container const newGrid = document.createElement('swp-grid-container'); newGrid.innerHTML = ` `; // Position new grid - 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); this.renderWeekContentInNavigationGrid(newGrid, weekStart); console.log('Grid created:', 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; } /** * Render week content in navigation grid container * Moved from NavigationRenderer */ private renderWeekContentInNavigationGrid(gridContainer: HTMLElement, weekStart: Date): void { console.group('🔧 GridRenderer.renderWeekContentInNavigationGrid'); console.log('Grid container:', gridContainer); console.log('Week start:', weekStart); const header = gridContainer.querySelector('swp-calendar-header'); const dayColumns = gridContainer.querySelector('swp-day-columns'); if (!header || !dayColumns) { console.log('Missing header or dayColumns'); console.groupEnd(); return; } // Clear existing content header.innerHTML = ''; dayColumns.innerHTML = ''; // Get dates using DateCalculator const dates = DateCalculator.getWorkWeekDates(weekStart); // Render headers for target week dates.forEach((date, i) => { const headerElement = document.createElement('swp-day-header'); if (DateCalculator.isToday(date)) { headerElement.dataset.today = 'true'; } const dayName = DateCalculator.getDayName(date, 'short'); headerElement.innerHTML = ` ${dayName} ${date.getDate()} `; headerElement.dataset.date = DateCalculator.formatISODate(date); header.appendChild(headerElement); }); // Render day columns for target week dates.forEach(date => { const column = document.createElement('swp-day-column'); column.dataset.date = DateCalculator.formatISODate(date); const eventsLayer = document.createElement('swp-events-layer'); column.appendChild(eventsLayer); dayColumns.appendChild(column); }); console.log('Week content rendered'); console.groupEnd(); } }