Calendar/src/renderers/GridRenderer.ts
Janus C. H. Knudsen 1e20e23e77 Uses optimized grid creation for navigation
Ensures navigation grid uses the same creation method
as the initial load.

This ensures workweek and resource settings are respected,
creating a more consistent experience.
2025-09-25 17:55:13 +02:00

287 lines
No EOL
9.4 KiB
TypeScript

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
* Now uses same implementation as initial load for consistency
*/
public createNavigationGrid(parentContainer: HTMLElement, weekStart: Date): HTMLElement {
console.group('🔧 GridRenderer.createNavigationGrid');
console.log('Week start:', weekStart);
console.log('Parent container:', parentContainer);
console.log('Using same grid creation as initial load');
const weekEnd = DateCalculator.addDays(weekStart, 6);
// Use SAME method as initial load - respects workweek and resource settings
const newGrid = this.createOptimizedGridContainer(weekStart, null, '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);
console.log('Grid created using createOptimizedGridContainer:', 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;
}
}