289 lines
12 KiB
JavaScript
289 lines
12 KiB
JavaScript
|
|
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:
|
||
|
|
* =====================
|
||
|
|
* <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 {
|
||
|
|
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
|