Calendar/src/renderers/GridRenderer.ts

328 lines
12 KiB
TypeScript
Raw Normal View History

2025-11-03 22:04:37 +01:00
import { Configuration } from '../configurations/CalendarConfig';
import { CalendarView, ICalendarEvent } from '../types/CalendarTypes';
2025-11-03 21:30:50 +01:00
import { IColumnRenderer, IColumnRenderContext } from './ColumnRenderer';
import { eventBus } from '../core/EventBus';
import { DateService } from '../utils/DateService';
import { CoreEvents } from '../constants/CoreEvents';
2025-10-06 22:29:31 +02:00
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:
* =====================
* <swp-calendar-container>
* <swp-header-spacer /> GridRenderer
* <swp-time-axis> GridRenderer
* <swp-hour-marker>00:00</...> GridRenderer (iterates hours)
* </swp-time-axis>
* <swp-grid-container> GridRenderer
* <swp-calendar-header> GridRenderer creates container
* <swp-day-header /> DateHeaderRenderer (iterates dates)
* </swp-calendar-header>
* <swp-scrollable-content> GridRenderer
* <swp-time-grid> GridRenderer
* <swp-grid-lines /> GridRenderer
* <swp-day-columns> GridRenderer creates container
* <swp-day-column /> ColumnRenderer (iterates dates)
* </swp-day-columns>
* </swp-time-grid>
* </swp-scrollable-content>
* </swp-grid-container>
* </swp-calendar-container>
*
* 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;
2025-11-03 21:30:50 +01:00
private columnRenderer: IColumnRenderer;
private config: Configuration;
constructor(
2025-11-03 21:30:50 +01:00
columnRenderer: IColumnRenderer,
dateService: DateService,
2025-11-03 21:30:50 +01:00
config: Configuration
) {
this.dateService = dateService;
2025-10-15 00:58:29 +02:00
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 dates - Array of dates to render as columns
* @param events - All events for the period
*/
public renderGrid(
grid: HTMLElement,
currentDate: Date,
view: CalendarView = 'week',
columns: IColumnInfo[] = [],
events: ICalendarEvent[] = []
): void {
2025-10-06 22:29:31 +02:00
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, events);
} else {
// Optimized update - only refresh dynamic content
this.updateGridContent(grid, currentDate, view, columns, 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
*/
private createCompleteGridStructure(
grid: HTMLElement,
currentDate: Date,
view: CalendarView,
columns: IColumnInfo[],
events: ICalendarEvent[]
): void {
// Create all elements in memory first for better performance
const fragment = document.createDocumentFragment();
2025-10-06 22:29:31 +02:00
// Create header spacer
const headerSpacer = document.createElement('swp-header-spacer');
fragment.appendChild(headerSpacer);
2025-10-06 22:29:31 +02:00
// Create time axis with caching
const timeAxis = this.createOptimizedTimeAxis();
this.cachedTimeAxis = timeAxis;
fragment.appendChild(timeAxis);
2025-10-06 22:29:31 +02:00
// Create grid container with caching
const gridContainer = this.createOptimizedGridContainer(columns, events);
this.cachedGridContainer = gridContainer;
fragment.appendChild(gridContainer);
2025-10-06 22:29:31 +02:00
// 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');
2025-11-03 22:04:37 +01:00
const gridSettings = this.config.gridSettings;
const startHour = gridSettings.dayStartHour;
const endHour = gridSettings.dayEndHour;
2025-10-06 22:29:31 +02:00
const fragment = document.createDocumentFragment();
for (let hour = startHour; hour < endHour; hour++) {
const marker = document.createElement('swp-hour-marker');
2025-10-06 22:29:31 +02:00
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
*/
private createOptimizedGridContainer(
columns: IColumnInfo[],
events: ICalendarEvent[]
): HTMLElement {
const gridContainer = document.createElement('swp-grid-container');
2025-10-06 22:29:31 +02:00
// Create calendar header as first child - always exists now!
const calendarHeader = document.createElement('swp-calendar-header');
gridContainer.appendChild(calendarHeader);
2025-10-06 22:29:31 +02:00
// Create scrollable content structure
const scrollableContent = document.createElement('swp-scrollable-content');
const timeGrid = document.createElement('swp-time-grid');
2025-10-06 22:29:31 +02:00
// Add grid lines
const gridLines = document.createElement('swp-grid-lines');
timeGrid.appendChild(gridLines);
2025-10-06 22:29:31 +02:00
// Create column container
const columnContainer = document.createElement('swp-day-columns');
this.renderColumnContainer(columnContainer, columns, events);
timeGrid.appendChild(columnContainer);
2025-10-06 22:29:31 +02:00
scrollableContent.appendChild(timeGrid);
gridContainer.appendChild(scrollableContent);
2025-10-06 22:29:31 +02:00
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 dates - Array of dates to render
* @param events - All events for the period (passed through, not used here)
*/
private renderColumnContainer(
columnContainer: HTMLElement,
columns: IColumnInfo[],
events: ICalendarEvent[]
): void {
// Delegate to ColumnRenderer
this.columnRenderer.render(columnContainer, {
columns: columns,
config: this.config
});
}
/**
* 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
*/
private updateGridContent(
grid: HTMLElement,
currentDate: Date,
view: CalendarView,
columns: IColumnInfo[],
events: ICalendarEvent[]
): void {
// Update column container if needed
const columnContainer = grid.querySelector('swp-day-columns');
if (columnContainer) {
columnContainer.innerHTML = '';
this.renderColumnContainer(columnContainer as HTMLElement, columns, 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.
* Events will be rendered by EventRenderingService when GRID_RENDERED emits.
*
* @param parentContainer - Container for the new grid
* @param dates - Array of dates to render
* @returns New grid element ready for animation
*/
public createNavigationGrid(parentContainer: HTMLElement, columns: IColumnInfo[]): HTMLElement {
// Create grid structure without events (events rendered by EventRenderingService)
const newGrid = this.createOptimizedGridContainer(columns, []);
// 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);
2025-10-06 22:29:31 +02:00
return newGrid;
}
}