Refactor renderer interfaces and update type names

Renames renderer interfaces to use 'I' prefix for clarity
Updates type references across related files
Improves type consistency in rendering strategies
This commit is contained in:
Janus C. H. Knudsen 2025-11-01 16:28:45 +01:00
parent cda201301c
commit 1ae4f00f2b
7 changed files with 157 additions and 39 deletions

View file

@ -22,9 +22,9 @@ import { HeaderManager } from './managers/HeaderManager';
import { ConfigManager } from './managers/ConfigManager'; import { ConfigManager } from './managers/ConfigManager';
// Import renderers // Import renderers
import { DateHeaderRenderer, type HeaderRenderer } from './renderers/HeaderRenderer'; import { DateHeaderRenderer, type IHeaderRenderer } from './renderers/DateHeaderRenderer';
import { DateColumnRenderer, type ColumnRenderer } from './renderers/ColumnRenderer'; import { DateColumnRenderer, type ColumnRenderer } from './renderers/ColumnRenderer';
import { DateEventRenderer, type EventRendererStrategy } from './renderers/EventRenderer'; import { DateEventRenderer, type IEventRenderer } from './renderers/EventRenderer';
import { AllDayEventRenderer } from './renderers/AllDayEventRenderer'; import { AllDayEventRenderer } from './renderers/AllDayEventRenderer';
import { GridRenderer } from './renderers/GridRenderer'; import { GridRenderer } from './renderers/GridRenderer';
import { NavigationRenderer } from './renderers/NavigationRenderer'; import { NavigationRenderer } from './renderers/NavigationRenderer';
@ -87,9 +87,9 @@ async function initializeCalendar(): Promise<void> {
builder.registerInstance(eventBus).as<IEventBus>(); builder.registerInstance(eventBus).as<IEventBus>();
// Register renderers // Register renderers
builder.registerType(DateHeaderRenderer).as<HeaderRenderer>().singleInstance(); builder.registerType(DateHeaderRenderer).as<IHeaderRenderer>().singleInstance();
builder.registerType(DateColumnRenderer).as<ColumnRenderer>().singleInstance(); builder.registerType(DateColumnRenderer).as<ColumnRenderer>().singleInstance();
builder.registerType(DateEventRenderer).as<EventRendererStrategy>().singleInstance(); builder.registerType(DateEventRenderer).as<IEventRenderer>().singleInstance();
// Register core services and utilities // Register core services and utilities
builder.registerType(DateService).as<DateService>().singleInstance(); builder.registerType(DateService).as<DateService>().singleInstance();

View file

@ -1,7 +1,7 @@
import { eventBus } from '../core/EventBus'; import { eventBus } from '../core/EventBus';
import { CalendarConfig } from '../core/CalendarConfig'; import { CalendarConfig } from '../core/CalendarConfig';
import { CoreEvents } from '../constants/CoreEvents'; import { CoreEvents } from '../constants/CoreEvents';
import { HeaderRenderer, HeaderRenderContext } from '../renderers/HeaderRenderer'; import { IHeaderRenderer, HeaderRenderContext } from '../renderers/DateHeaderRenderer';
import { DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload, HeaderReadyEventPayload } from '../types/EventTypes'; import { DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload, HeaderReadyEventPayload } from '../types/EventTypes';
import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
@ -11,10 +11,10 @@ import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
* Uses dependency injection for renderer strategy * Uses dependency injection for renderer strategy
*/ */
export class HeaderManager { export class HeaderManager {
private headerRenderer: HeaderRenderer; private headerRenderer: IHeaderRenderer;
private config: CalendarConfig; private config: CalendarConfig;
constructor(headerRenderer: HeaderRenderer, config: CalendarConfig) { constructor(headerRenderer: IHeaderRenderer, config: CalendarConfig) {
this.headerRenderer = headerRenderer; this.headerRenderer = headerRenderer;
this.config = config; this.config = config;

View file

@ -4,7 +4,7 @@ import { EventLayout } from '../utils/AllDayLayoutEngine';
import { ColumnBounds } from '../utils/ColumnDetectionUtils'; import { ColumnBounds } from '../utils/ColumnDetectionUtils';
import { EventManager } from '../managers/EventManager'; import { EventManager } from '../managers/EventManager';
import { DragStartEventPayload } from '../types/EventTypes'; import { DragStartEventPayload } from '../types/EventTypes';
import { EventRendererStrategy } from './EventRenderer'; import { IEventRenderer } from './EventRenderer';
export class AllDayEventRenderer { export class AllDayEventRenderer {

View file

@ -6,7 +6,7 @@ import { DateService } from '../utils/DateService';
/** /**
* Interface for header rendering strategies * Interface for header rendering strategies
*/ */
export interface HeaderRenderer { export interface IHeaderRenderer {
render(calendarHeader: HTMLElement, context: HeaderRenderContext): void; render(calendarHeader: HTMLElement, context: HeaderRenderContext): void;
} }
@ -22,7 +22,7 @@ export interface HeaderRenderContext {
/** /**
* Date-based header renderer (original functionality) * Date-based header renderer (original functionality)
*/ */
export class DateHeaderRenderer implements HeaderRenderer { export class DateHeaderRenderer implements IHeaderRenderer {
private dateService!: DateService; private dateService!: DateService;
render(calendarHeader: HTMLElement, context: HeaderRenderContext): void { render(calendarHeader: HTMLElement, context: HeaderRenderContext): void {
@ -33,8 +33,8 @@ export class DateHeaderRenderer implements HeaderRenderer {
calendarHeader.appendChild(allDayContainer); calendarHeader.appendChild(allDayContainer);
// Initialize date service with timezone and locale from config // Initialize date service with timezone and locale from config
const timezone = config.getTimezone?.() || 'Europe/Copenhagen'; const timezone = config.getTimezone();
const locale = config.getLocale?.() || 'da-DK'; const locale = config.getLocale();
this.dateService = new DateService(config); this.dateService = new DateService(config);
const workWeekSettings = config.getWorkWeekSettings(); const workWeekSettings = config.getWorkWeekSettings();

View file

@ -13,7 +13,7 @@ import { EventLayoutCoordinator, GridGroupLayout, StackedEventLayout } from '../
/** /**
* Interface for event rendering strategies * Interface for event rendering strategies
*/ */
export interface EventRendererStrategy { export interface IEventRenderer {
renderEvents(events: CalendarEvent[], container: HTMLElement): void; renderEvents(events: CalendarEvent[], container: HTMLElement): void;
clearEvents(container?: HTMLElement): void; clearEvents(container?: HTMLElement): void;
handleDragStart?(payload: DragStartEventPayload): void; handleDragStart?(payload: DragStartEventPayload): void;
@ -29,7 +29,7 @@ export interface EventRendererStrategy {
/** /**
* Date-based event renderer * Date-based event renderer
*/ */
export class DateEventRenderer implements EventRendererStrategy { export class DateEventRenderer implements IEventRenderer {
private dateService: DateService; private dateService: DateService;
private stackManager: EventStackManager; private stackManager: EventStackManager;

View file

@ -2,7 +2,7 @@ import { EventBus } from '../core/EventBus';
import { IEventBus, CalendarEvent, RenderContext } from '../types/CalendarTypes'; import { IEventBus, CalendarEvent, RenderContext } from '../types/CalendarTypes';
import { CoreEvents } from '../constants/CoreEvents'; import { CoreEvents } from '../constants/CoreEvents';
import { EventManager } from '../managers/EventManager'; import { EventManager } from '../managers/EventManager';
import { EventRendererStrategy } from './EventRenderer'; import { IEventRenderer } from './EventRenderer';
import { SwpEventElement } from '../elements/SwpEventElement'; import { SwpEventElement } from '../elements/SwpEventElement';
import { DragStartEventPayload, DragMoveEventPayload, DragEndEventPayload, DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload, DragMouseEnterColumnEventPayload, DragColumnChangeEventPayload, HeaderReadyEventPayload, ResizeEndEventPayload } from '../types/EventTypes'; import { DragStartEventPayload, DragMoveEventPayload, DragEndEventPayload, DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload, DragMouseEnterColumnEventPayload, DragColumnChangeEventPayload, HeaderReadyEventPayload, ResizeEndEventPayload } from '../types/EventTypes';
import { DateService } from '../utils/DateService'; import { DateService } from '../utils/DateService';
@ -14,7 +14,7 @@ import { ColumnBounds } from '../utils/ColumnDetectionUtils';
export class EventRenderingService { export class EventRenderingService {
private eventBus: IEventBus; private eventBus: IEventBus;
private eventManager: EventManager; private eventManager: EventManager;
private strategy: EventRendererStrategy; private strategy: IEventRenderer;
private dateService: DateService; private dateService: DateService;
private dragMouseLeaveHeaderListener: ((event: Event) => void) | null = null; private dragMouseLeaveHeaderListener: ((event: Event) => void) | null = null;
@ -22,7 +22,7 @@ export class EventRenderingService {
constructor( constructor(
eventBus: IEventBus, eventBus: IEventBus,
eventManager: EventManager, eventManager: EventManager,
strategy: EventRendererStrategy, strategy: IEventRenderer,
dateService: DateService dateService: DateService
) { ) {
this.eventBus = eventBus; this.eventBus = eventBus;

View file

@ -7,8 +7,76 @@ import { CoreEvents } from '../constants/CoreEvents';
import { TimeFormatter } from '../utils/TimeFormatter'; import { TimeFormatter } from '../utils/TimeFormatter';
/** /**
* GridRenderer - Centralized DOM rendering for calendar grid * GridRenderer - Centralized DOM rendering for calendar grid structure
* Optimized to reduce redundant DOM operations and improve performance *
* 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 { export class GridRenderer {
private cachedGridContainer: HTMLElement | null = null; private cachedGridContainer: HTMLElement | null = null;
@ -27,6 +95,16 @@ export class GridRenderer {
this.config = config; 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)
*/
public renderGrid( public renderGrid(
grid: HTMLElement, grid: HTMLElement,
currentDate: Date, currentDate: Date,
@ -43,8 +121,6 @@ export class GridRenderer {
// Only clear and rebuild if grid is empty (first render) // Only clear and rebuild if grid is empty (first render)
if (grid.children.length === 0) { if (grid.children.length === 0) {
this.createCompleteGridStructure(grid, currentDate, view); this.createCompleteGridStructure(grid, currentDate, view);
// Setup grid-related event listeners on first render
// this.setupGridEventListeners();
} else { } else {
// Optimized update - only refresh dynamic content // Optimized update - only refresh dynamic content
this.updateGridContent(grid, currentDate, view); this.updateGridContent(grid, currentDate, view);
@ -52,7 +128,19 @@ export class GridRenderer {
} }
/** /**
* Create complete grid structure in one operation * 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
*/ */
private createCompleteGridStructure( private createCompleteGridStructure(
grid: HTMLElement, grid: HTMLElement,
@ -80,6 +168,14 @@ export class GridRenderer {
grid.appendChild(fragment); 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 { private createOptimizedTimeAxis(): HTMLElement {
const timeAxis = document.createElement('swp-time-axis'); const timeAxis = document.createElement('swp-time-axis');
const timeAxisContent = document.createElement('swp-time-axis-content'); const timeAxisContent = document.createElement('swp-time-axis-content');
@ -96,11 +192,23 @@ export class GridRenderer {
} }
timeAxisContent.appendChild(fragment); timeAxisContent.appendChild(fragment);
timeAxisContent.style.top = '-1px'; timeAxisContent.style.top = '-1px';
timeAxis.appendChild(timeAxisContent); timeAxis.appendChild(timeAxisContent);
return timeAxis; 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
* @returns Complete grid container element
*/
private createOptimizedGridContainer( private createOptimizedGridContainer(
currentDate: Date, currentDate: Date,
view: CalendarView view: CalendarView
@ -127,14 +235,20 @@ export class GridRenderer {
scrollableContent.appendChild(timeGrid); scrollableContent.appendChild(timeGrid);
gridContainer.appendChild(scrollableContent); gridContainer.appendChild(scrollableContent);
console.log('✅ GridRenderer: Created grid container with header');
return gridContainer; return gridContainer;
} }
/** /**
* Render column container with view awareness * Delegates column rendering to the injected ColumnRenderer strategy
*
* This is where the Strategy Pattern is applied:
* - DateColumnRenderer iterates dates and creates day columns
* - Could be swapped with other implementations (e.g., ResourceColumnRenderer)
*
* @param columnContainer - Empty container to populate
* @param currentDate - Current view date
* @param view - View type
*/ */
private renderColumnContainer( private renderColumnContainer(
columnContainer: HTMLElement, columnContainer: HTMLElement,
@ -151,6 +265,13 @@ export class GridRenderer {
/** /**
* Optimized update of grid content without full rebuild * 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
*/ */
private updateGridContent( private updateGridContent(
grid: HTMLElement, grid: HTMLElement,
@ -165,17 +286,18 @@ export class GridRenderer {
} }
} }
/** /**
* Create navigation grid container for slide animations * Creates a new grid for slide animations during navigation
* Now uses same implementation as initial load for consistency *
* 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
*/ */
public createNavigationGrid(parentContainer: HTMLElement, weekStart: Date): HTMLElement { 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 = this.dateService.addDays(weekStart, 6);
// Use SAME method as initial load - respects workweek settings // Use SAME method as initial load - respects workweek settings
const newGrid = this.createOptimizedGridContainer(weekStart, 'week'); const newGrid = this.createOptimizedGridContainer(weekStart, 'week');
@ -189,10 +311,6 @@ export class GridRenderer {
// Add to parent container // Add to parent container
parentContainer.appendChild(newGrid); parentContainer.appendChild(newGrid);
console.log('Grid created using createOptimizedGridContainer:', newGrid);
console.log('Grid creation complete - caller will emit GRID_RENDERED');
console.groupEnd();
return newGrid; return newGrid;
} }
} }