import { Configuration } from '../configurations/CalendarConfig'; import { CalendarView } from '../types/CalendarTypes'; import { IColumnRenderer, IColumnRenderContext } from './ColumnRenderer'; import { eventBus } from '../core/EventBus'; import { DateService } from '../utils/DateService'; import { CoreEvents } from '../constants/CoreEvents'; import { TimeFormatter } from '../utils/TimeFormatter'; import { IColumnInfo } from '../types/ColumnDataSource'; /** * GridRenderer - Centralized DOM rendering for calendar grid structure * * ARCHITECTURE OVERVIEW: * ===================== * GridRenderer is responsible for creating and managing the complete DOM structure * of the calendar grid. It follows the Strategy Pattern by delegating specific * rendering tasks to specialized renderers (DateHeaderRenderer, ColumnRenderer). * * RESPONSIBILITY HIERARCHY: * ======================== * GridRenderer (this file) * ├─ Creates overall grid skeleton * ├─ Manages time axis (hour markers) * └─ Delegates to specialized renderers: * ├─ DateHeaderRenderer → Renders date headers * └─ ColumnRenderer → Renders day columns * * DOM STRUCTURE CREATED: * ===================== * * ← GridRenderer * ← GridRenderer * 00:00 ← GridRenderer (iterates hours) * * ← GridRenderer * ← GridRenderer creates container * ← DateHeaderRenderer (iterates dates) * * ← GridRenderer * ← GridRenderer * ← GridRenderer * ← GridRenderer creates container * ← ColumnRenderer (iterates dates) * * * * * * * RENDERING FLOW: * ============== * 1. renderGrid() - Entry point called by GridManager * ├─ First render: createCompleteGridStructure() * └─ Updates: updateGridContent() * * 2. createCompleteGridStructure() * ├─ Creates header spacer * ├─ Creates time axis (calls createOptimizedTimeAxis) * └─ Creates grid container (calls createOptimizedGridContainer) * * 3. createOptimizedGridContainer() * ├─ Creates calendar header container * ├─ Creates scrollable content structure * └─ Creates column container (calls renderColumnContainer) * * 4. renderColumnContainer() * └─ Delegates to ColumnRenderer.render() * └─ ColumnRenderer iterates dates and creates columns * * OPTIMIZATION STRATEGY: * ===================== * - Caches DOM references (cachedGridContainer, cachedTimeAxis) * - Uses DocumentFragment for batch DOM insertions * - Only updates changed content on re-renders * - Delegates specialized tasks to strategy renderers * * USAGE EXAMPLE: * ============= * const gridRenderer = new GridRenderer(columnRenderer, dateService, config); * gridRenderer.renderGrid(containerElement, new Date(), 'week'); */ export class GridRenderer { private cachedGridContainer: HTMLElement | null = null; private cachedTimeAxis: HTMLElement | null = null; private dateService: DateService; private columnRenderer: IColumnRenderer; private config: Configuration; constructor( columnRenderer: IColumnRenderer, dateService: DateService, config: Configuration ) { this.dateService = dateService; this.columnRenderer = columnRenderer; this.config = config; } /** * Main entry point for rendering the complete calendar grid * * This method decides between full render (first time) or optimized update. * It caches the grid reference for performance. * * @param grid - Container element where grid will be rendered * @param currentDate - Base date for the current view (e.g., any date in the week) * @param view - Calendar view type (day/week/month) * @param columns - Array of columns to render (each column contains its events) */ public renderGrid( grid: HTMLElement, currentDate: Date, view: CalendarView = 'week', columns: IColumnInfo[] = [] ): 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, view, columns); } else { // Optimized update - only refresh dynamic content this.updateGridContent(grid, currentDate, view, columns); } } /** * Creates the complete grid structure from scratch * * Uses DocumentFragment for optimal performance by minimizing reflows. * Creates all child elements in memory first, then appends everything at once. * * Structure created: * 1. Header spacer (placeholder for alignment) * 2. Time axis (hour markers 00:00-23:00) * 3. Grid container (header + scrollable content) * * @param grid - Parent container * @param currentDate - Current view date * @param view - View type * @param columns - Array of columns to render (each column contains its events) */ private createCompleteGridStructure( grid: HTMLElement, currentDate: Date, view: CalendarView, columns: IColumnInfo[] ): 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(columns, currentDate); this.cachedGridContainer = gridContainer; fragment.appendChild(gridContainer); // Append all at once to minimize reflows grid.appendChild(fragment); } /** * Creates the time axis with hour markers * * Iterates from dayStartHour to dayEndHour (configured in GridSettings). * Each marker shows the hour in the configured time format. * * @returns Time axis element with all hour markers */ private createOptimizedTimeAxis(): HTMLElement { const timeAxis = document.createElement('swp-time-axis'); const timeAxisContent = document.createElement('swp-time-axis-content'); const gridSettings = this.config.gridSettings; const startHour = gridSettings.dayStartHour; const endHour = gridSettings.dayEndHour; const fragment = document.createDocumentFragment(); for (let hour = startHour; hour < endHour; hour++) { const marker = document.createElement('swp-hour-marker'); const date = new Date(2024, 0, 1, hour, 0); marker.textContent = TimeFormatter.formatTime(date); fragment.appendChild(marker); } timeAxisContent.appendChild(fragment); timeAxisContent.style.top = '-1px'; timeAxis.appendChild(timeAxisContent); return timeAxis; } /** * Creates the main grid container with header and columns * * This is the scrollable area containing: * - Calendar header (dates/resources) - created here, populated by DateHeaderRenderer * - Time grid (grid lines + day columns) - structure created here * - Column container - created here, populated by ColumnRenderer * * @param columns - Array of columns to render (each column contains its events) * @param currentDate - Current view date * @returns Complete grid container element */ private createOptimizedGridContainer( columns: IColumnInfo[], currentDate: Date ): 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, columns, currentDate); timeGrid.appendChild(columnContainer); scrollableContent.appendChild(timeGrid); gridContainer.appendChild(scrollableContent); return gridContainer; } /** * Renders columns by delegating to ColumnRenderer * * GridRenderer delegates column creation to ColumnRenderer. * Event rendering is handled by EventRenderingService listening to GRID_RENDERED. * * @param columnContainer - Empty container to populate * @param columns - Array of columns to render (each column contains its events) * @param currentDate - Current view date */ private renderColumnContainer( columnContainer: HTMLElement, columns: IColumnInfo[], currentDate: Date ): void { // Delegate to ColumnRenderer this.columnRenderer.render(columnContainer, { columns: columns, config: this.config, currentDate: currentDate }); } /** * Optimized update of grid content without full rebuild * * Only updates the column container content, leaving the structure intact. * This is much faster than recreating the entire grid. * * @param grid - Existing grid element * @param currentDate - New view date * @param view - View type * @param columns - Array of columns to render (each column contains its events) */ private updateGridContent( grid: HTMLElement, currentDate: Date, view: CalendarView, columns: IColumnInfo[] ): void { // Update column container if needed const columnContainer = grid.querySelector('swp-day-columns'); if (columnContainer) { columnContainer.innerHTML = ''; this.renderColumnContainer(columnContainer as HTMLElement, columns, currentDate); } } /** * Creates a new grid for slide animations during navigation * * Used by NavigationManager for smooth week-to-week transitions. * Creates a complete grid positioned absolutely for animation. * * Note: Positioning is handled by Animation API, not here. * Events will be rendered by EventRenderingService when GRID_RENDERED emits. * * @param parentContainer - Container for the new grid * @param columns - Array of columns to render * @param currentDate - Current view date * @returns New grid element ready for animation */ public createNavigationGrid(parentContainer: HTMLElement, columns: IColumnInfo[], currentDate: Date): HTMLElement { // Create grid structure (events are in columns, rendered by EventRenderingService) const newGrid = this.createOptimizedGridContainer(columns, currentDate); // 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); return newGrid; } }