Enhances calendar flexibility by introducing group-based column spanning and improving cross-mode event handling Adds support for: - Dynamic column grouping in date and resource modes - Consistent event drag-and-drop across different calendar views - More robust all-day event layout calculations Improves event management logic to handle resource and date mode transitions more elegantly
324 lines
No EOL
12 KiB
TypeScript
324 lines
No EOL
12 KiB
TypeScript
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:
|
|
* =====================
|
|
* <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;
|
|
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;
|
|
}
|
|
} |