import { TimeFormatter } from '../utils/TimeFormatter'; /** * 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 { constructor(columnRenderer, dateService, config, workHoursManager) { this.cachedGridContainer = null; this.cachedTimeAxis = null; this.dateService = dateService; this.columnRenderer = columnRenderer; this.config = config; this.workHoursManager = workHoursManager; } /** * 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 dates - Array of dates to render as columns * @param events - All events for the period */ renderGrid(grid, currentDate, view = 'week', dates = [], events = []) { 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, dates, events); } else { // Optimized update - only refresh dynamic content this.updateGridContent(grid, currentDate, view, dates, events); } } /** * 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 dates - Array of dates to render */ createCompleteGridStructure(grid, currentDate, view, dates, events) { // 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, view, dates, events); 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 */ createOptimizedTimeAxis() { 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 currentDate - Current view date * @param view - View type * @param dates - Array of dates to render * @returns Complete grid container element */ createOptimizedGridContainer(dates, events) { 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, dates, events); timeGrid.appendChild(columnContainer); scrollableContent.appendChild(timeGrid); gridContainer.appendChild(scrollableContent); return gridContainer; } /** * Renders columns by iterating through dates * * GridRenderer creates column structure with work hours styling. * Event rendering is handled by EventRenderingService listening to GRID_RENDERED. * * @param columnContainer - Empty container to populate * @param dates - Array of dates to render * @param events - All events for the period (passed through, not used here) */ renderColumnContainer(columnContainer, dates, events) { // Iterate through dates and render each column structure dates.forEach(date => { // Create column with data-date attribute const column = document.createElement('swp-day-column'); column.dataset.date = this.dateService.formatISODate(date); // Apply work hours styling this.applyWorkHoursStyling(column, date); // Add events layer (events will be rendered by EventRenderingService) const eventsLayer = document.createElement('swp-events-layer'); column.appendChild(eventsLayer); columnContainer.appendChild(column); }); } /** * Apply work hours styling to a column */ applyWorkHoursStyling(column, date) { const workHours = this.workHoursManager.getWorkHoursForDate(date); if (workHours === 'off') { column.setAttribute('data-day-off', 'true'); } else { column.removeAttribute('data-day-off'); // Calculate non-work hours overlay positions const nonWorkStyle = this.workHoursManager.calculateNonWorkHoursStyle(workHours); if (nonWorkStyle) { column.style.setProperty('--before-work-height', `${nonWorkStyle.beforeWorkHeight}px`); column.style.setProperty('--after-work-top', `${nonWorkStyle.afterWorkTop}px`); } } } /** * 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 dates - Array of dates to render * @param events - All events for the period */ updateGridContent(grid, currentDate, view, dates, events) { // Update column container if needed const columnContainer = grid.querySelector('swp-day-columns'); if (columnContainer) { columnContainer.innerHTML = ''; this.renderColumnContainer(columnContainer, dates, events); } } /** * 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. * * @param parentContainer - Container for the new grid * @param weekStart - Start date of the new week * @returns New grid element ready for animation */ createNavigationGrid(parentContainer, weekStart) { // Use SAME method as initial load - respects workweek settings const newGrid = this.createOptimizedGridContainer(weekStart, '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); return newGrid; } } //# sourceMappingURL=GridRenderer.js.map