import { CalendarConfig } from '../core/CalendarConfig'; import { ResourceCalendarData } from '../types/CalendarTypes'; import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; import { HeaderRenderContext } from './HeaderRenderer'; import { ColumnRenderContext } from './ColumnRenderer'; import { eventBus } from '../core/EventBus'; /** * GridRenderer - Handles DOM rendering for the calendar grid * Separated from GridManager to follow Single Responsibility Principle */ export class GridRenderer { private config: CalendarConfig; private headerEventListener: ((event: Event) => void) | null = null; constructor(config: CalendarConfig) { this.config = config; } /** * Render the complete grid structure */ public renderGrid( grid: HTMLElement, currentWeek: Date, resourceData: ResourceCalendarData | null ): void { if (!grid || !currentWeek) { return; } // Only clear and rebuild if grid is empty (first render) if (grid.children.length === 0) { // Create POC structure: header-spacer + time-axis + grid-container this.createHeaderSpacer(grid); this.createTimeAxis(grid); this.createGridContainer(grid, currentWeek, resourceData); } else { // Just update the calendar header for all-day events this.updateCalendarHeader(grid, currentWeek, resourceData); } } /** * Create header spacer to align time axis with week content */ private createHeaderSpacer(grid: HTMLElement): void { const headerSpacer = document.createElement('swp-header-spacer'); grid.appendChild(headerSpacer); } /** * Create time axis (positioned beside grid container) */ private createTimeAxis(grid: HTMLElement): void { 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; 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}`; timeAxisContent.appendChild(marker); } timeAxis.appendChild(timeAxisContent); grid.appendChild(timeAxis); } /** * Create grid container with header and scrollable content */ private createGridContainer( grid: HTMLElement, currentWeek: Date, resourceData: ResourceCalendarData | null ): void { const gridContainer = document.createElement('swp-grid-container'); // Create calendar header using Strategy Pattern const calendarHeader = document.createElement('swp-calendar-header'); this.renderCalendarHeader(calendarHeader, currentWeek, resourceData); gridContainer.appendChild(calendarHeader); // Create scrollable content 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 using Strategy Pattern const columnContainer = document.createElement('swp-day-columns'); this.renderColumnContainer(columnContainer, currentWeek, resourceData); timeGrid.appendChild(columnContainer); scrollableContent.appendChild(timeGrid); gridContainer.appendChild(scrollableContent); grid.appendChild(gridContainer); } /** * Render calendar header using Strategy Pattern */ private renderCalendarHeader( calendarHeader: HTMLElement, currentWeek: Date, resourceData: ResourceCalendarData | null ): void { const calendarType = this.config.getCalendarMode(); const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType); const context: HeaderRenderContext = { currentWeek: currentWeek, config: this.config, resourceData: resourceData }; headerRenderer.render(calendarHeader, context); // Always ensure all-day containers exist for all days headerRenderer.ensureAllDayContainers(calendarHeader); // Setup event listener for mouseover detection this.setupHeaderEventListener(calendarHeader); } /** * Render column container using Strategy Pattern */ private renderColumnContainer( columnContainer: HTMLElement, currentWeek: Date, resourceData: ResourceCalendarData | null ): void { const calendarType = this.config.getCalendarMode(); const columnRenderer = CalendarTypeFactory.getColumnRenderer(calendarType); const context: ColumnRenderContext = { currentWeek: currentWeek, config: this.config, resourceData: resourceData }; columnRenderer.render(columnContainer, context); } /** * Update only the calendar header without rebuilding entire grid */ private updateCalendarHeader( grid: HTMLElement, currentWeek: Date, resourceData: ResourceCalendarData | null ): void { const calendarHeader = grid.querySelector('swp-calendar-header'); if (!calendarHeader) return; // Clear existing content calendarHeader.innerHTML = ''; // Re-render headers using Strategy Pattern - this will also re-attach the event listener this.renderCalendarHeader(calendarHeader as HTMLElement, currentWeek, resourceData); } /** * Setup or re-setup event delegation listener on calendar header */ private setupHeaderEventListener(calendarHeader: HTMLElement): void { // Remove existing listener if any (stored reference approach) if (this.headerEventListener) { calendarHeader.removeEventListener('mouseover', this.headerEventListener); } // Create new listener function this.headerEventListener = (event) => { const target = event.target as HTMLElement; // Check what was hovered - could be day-header OR all-day-container 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) { // For all-day areas, we need to determine which day column we're over hoveredElement = allDayContainer as HTMLElement; // Calculate which day we're hovering over based on mouse position 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.floor(mouseX / dayWidth); const targetDayHeader = dayHeaders[dayIndex] as HTMLElement; targetDate = targetDayHeader?.dataset.date; } else { return; // No valid element found } // Get the header renderer for addToAllDay functionality const calendarType = this.config.getCalendarMode(); const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType); eventBus.emit('header:mouseover', { element: hoveredElement, targetDate, headerRenderer }); } }; // Add the new listener calendarHeader.addEventListener('mouseover', this.headerEventListener); } }