Refactors grid and navigation rendering

Attempt 1
This commit is contained in:
Janus Knudsen 2025-08-17 22:54:00 +02:00
parent 6026d28e6f
commit 32ee35eb02
10 changed files with 436 additions and 811 deletions

View file

@ -24,7 +24,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
// clearEvents() would remove events from all containers, breaking the animation
// Events are now rendered directly into the new container without clearing
// Events should already be filtered by DataManager - no need to filter here
// Events should already be filtered by EventManager - no need to filter here
console.log('BaseEventRenderer: Rendering', events.length, 'pre-filtered events');
// Find columns in the specific container

View file

@ -0,0 +1,148 @@
import { EventBus } from '../core/EventBus';
import { IEventBus, CalendarEvent, RenderContext } from '../types/CalendarTypes';
import { EventTypes } from '../constants/EventTypes';
import { StateEvents } from '../types/CalendarState';
import { calendarConfig } from '../core/CalendarConfig';
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
import { EventManager } from '../managers/EventManager';
import { EventRendererStrategy } from './EventRenderer';
/**
* EventRenderer - Render events i DOM med positionering using Strategy Pattern
* Håndterer event positioning og overlap detection
*/
export class EventRenderer {
private eventBus: IEventBus;
private eventManager: EventManager;
private strategy: EventRendererStrategy;
constructor(eventBus: IEventBus, eventManager: EventManager) {
this.eventBus = eventBus;
this.eventManager = eventManager;
// Cache strategy at initialization
const calendarType = calendarConfig.getCalendarMode();
this.strategy = CalendarTypeFactory.getEventRenderer(calendarType);
this.setupEventListeners();
}
/**
* Render events in a specific container for a given period
*/
public renderEvents(context: RenderContext): void {
console.log('EventRenderer: Rendering events for period', {
startDate: context.startDate,
endDate: context.endDate,
container: context.container
});
// Get events from EventManager for the period
const events = this.eventManager.getEventsForPeriod(
context.startDate,
context.endDate
);
console.log(`EventRenderer: Found ${events.length} events for period`);
if (events.length === 0) {
console.log('EventRenderer: No events to render for this period');
return;
}
// Use cached strategy to render events in the specific container
this.strategy.renderEvents(events, context.container, calendarConfig);
console.log(`EventRenderer: Successfully rendered ${events.length} events`);
}
private setupEventListeners(): void {
// Event-driven rendering: React to grid and container events
this.eventBus.on(EventTypes.GRID_RENDERED, (event: Event) => {
console.log('EventRenderer: Received GRID_RENDERED event');
this.handleGridRendered(event as CustomEvent);
});
this.eventBus.on(EventTypes.CONTAINER_READY_FOR_EVENTS, (event: Event) => {
console.log('EventRenderer: Received CONTAINER_READY_FOR_EVENTS event');
this.handleContainerReady(event as CustomEvent);
});
this.eventBus.on(EventTypes.VIEW_CHANGED, (event: Event) => {
console.log('EventRenderer: Received VIEW_CHANGED event');
this.handleViewChanged(event as CustomEvent);
});
// Handle calendar type changes - update cached strategy
this.eventBus.on(EventTypes.CALENDAR_TYPE_CHANGED, () => {
const calendarType = calendarConfig.getCalendarMode();
this.strategy = CalendarTypeFactory.getEventRenderer(calendarType);
console.log(`EventRenderer: Updated strategy to ${calendarType}`);
});
}
/**
* Handle GRID_RENDERED event - render events in the current grid
*/
private handleGridRendered(event: CustomEvent): void {
const { container, startDate, endDate } = event.detail;
if (!container) {
console.error('EventRenderer: No container in GRID_RENDERED event', event.detail);
return;
}
// Use period from event or fallback to calculated period
const periodStart = startDate;
const periodEnd = endDate;
this.renderEvents({
container: container,
startDate: periodStart,
endDate: periodEnd
});
}
/**
* Handle CONTAINER_READY_FOR_EVENTS event - render events in pre-rendered container
*/
private handleContainerReady(event: CustomEvent): void {
const { container, startDate, endDate } = event.detail;
if (!container || !startDate || !endDate) {
console.error('EventRenderer: Invalid CONTAINER_READY_FOR_EVENTS event data', event.detail);
return;
}
this.renderEvents({
container: container,
startDate: new Date(startDate),
endDate: new Date(endDate)
});
}
/**
* Handle VIEW_CHANGED event - clear and re-render for new view
*/
private handleViewChanged(event: CustomEvent): void {
// Clear all existing events since view structure may have changed
this.clearEvents();
// New rendering will be triggered by subsequent GRID_RENDERED event
console.log('EventRenderer: Cleared events for view change, waiting for GRID_RENDERED');
}
private clearEvents(container?: HTMLElement): void {
console.log(`EventRenderer: Clearing events`, container ? 'in container' : 'globally');
this.strategy.clearEvents(container);
}
public refresh(container?: HTMLElement): void {
// Clear events in specific container or globally
this.clearEvents(container);
}
public destroy(): void {
this.clearEvents();
}
}

View file

@ -0,0 +1,182 @@
import { CalendarConfig } from '../core/CalendarConfig';
import { ResourceCalendarData } from '../types/CalendarTypes';
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
import { HeaderRenderContext } from './HeaderRenderer';
import { ColumnRenderContext } from './ColumnRenderer';
/**
* GridRenderer - Handles DOM rendering for the calendar grid
* Separated from GridManager to follow Single Responsibility Principle
*/
export class GridRenderer {
private config: CalendarConfig;
constructor(config: CalendarConfig) {
this.config = config;
}
/**
* Render the complete grid structure
*/
public renderGrid(
grid: HTMLElement,
currentWeek: Date,
resourceData: ResourceCalendarData | null,
allDayEvents: any[]
): void {
console.log('GridRenderer: renderGrid called', {
hasGrid: !!grid,
hasCurrentWeek: !!currentWeek,
currentWeek: currentWeek
});
if (!grid || !currentWeek) {
console.warn('GridRenderer: Cannot render - missing grid or currentWeek');
return;
}
// Only clear and rebuild if grid is empty (first render)
if (grid.children.length === 0) {
console.log('GridRenderer: First render - creating grid structure');
// Create POC structure: header-spacer + time-axis + grid-container
this.createHeaderSpacer(grid);
this.createTimeAxis(grid);
this.createGridContainer(grid, currentWeek, resourceData, allDayEvents);
} else {
console.log('GridRenderer: Re-render - updating existing structure');
// Just update the calendar header for all-day events
this.updateCalendarHeader(grid, currentWeek, resourceData, allDayEvents);
}
console.log('GridRenderer: Grid rendered successfully with POC structure');
}
/**
* Create header spacer to align time axis with week content
*/
private createHeaderSpacer(grid: HTMLElement): void {
const headerSpacer = document.createElement('swp-header-spacer');
grid.appendChild(headerSpacer);
}
/**
* Create time axis (positioned beside grid container)
*/
private createTimeAxis(grid: HTMLElement): void {
const timeAxis = document.createElement('swp-time-axis');
const timeAxisContent = document.createElement('swp-time-axis-content');
const gridSettings = this.config.getGridSettings();
const startHour = gridSettings.dayStartHour;
const endHour = gridSettings.dayEndHour;
console.log('GridRenderer: Creating time axis - startHour:', startHour, 'endHour:', endHour);
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}`;
timeAxisContent.appendChild(marker);
}
timeAxis.appendChild(timeAxisContent);
grid.appendChild(timeAxis);
}
/**
* Create grid container with header and scrollable content
*/
private createGridContainer(
grid: HTMLElement,
currentWeek: Date,
resourceData: ResourceCalendarData | null,
allDayEvents: any[]
): void {
const gridContainer = document.createElement('swp-grid-container');
// Create calendar header using Strategy Pattern
const calendarHeader = document.createElement('swp-calendar-header');
this.renderCalendarHeader(calendarHeader, currentWeek, resourceData, allDayEvents);
gridContainer.appendChild(calendarHeader);
// Create scrollable content
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 using Strategy Pattern
const columnContainer = document.createElement('swp-day-columns');
this.renderColumnContainer(columnContainer, currentWeek, resourceData);
timeGrid.appendChild(columnContainer);
scrollableContent.appendChild(timeGrid);
gridContainer.appendChild(scrollableContent);
grid.appendChild(gridContainer);
}
/**
* Render calendar header using Strategy Pattern
*/
private renderCalendarHeader(
calendarHeader: HTMLElement,
currentWeek: Date,
resourceData: ResourceCalendarData | null,
allDayEvents: any[]
): void {
const calendarType = this.config.getCalendarMode();
const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType);
const context: HeaderRenderContext = {
currentWeek: currentWeek,
config: this.config,
allDayEvents: allDayEvents,
resourceData: resourceData
};
headerRenderer.render(calendarHeader, context);
}
/**
* Render column container using Strategy Pattern
*/
private renderColumnContainer(
columnContainer: HTMLElement,
currentWeek: Date,
resourceData: ResourceCalendarData | null
): void {
console.log('GridRenderer: renderColumnContainer called');
const calendarType = this.config.getCalendarMode();
const columnRenderer = CalendarTypeFactory.getColumnRenderer(calendarType);
const context: ColumnRenderContext = {
currentWeek: currentWeek,
config: this.config,
resourceData: resourceData
};
columnRenderer.render(columnContainer, context);
}
/**
* Update only the calendar header without rebuilding entire grid
*/
private updateCalendarHeader(
grid: HTMLElement,
currentWeek: Date,
resourceData: ResourceCalendarData | null,
allDayEvents: any[]
): void {
const calendarHeader = grid.querySelector('swp-calendar-header');
if (!calendarHeader) return;
// Clear existing content
calendarHeader.innerHTML = '';
// Re-render headers using Strategy Pattern
this.renderCalendarHeader(calendarHeader as HTMLElement, currentWeek, resourceData, allDayEvents);
}
}

View file

@ -0,0 +1,110 @@
import { CalendarConfig } from '../core/CalendarConfig';
import { ResourceCalendarData } from '../types/CalendarTypes';
/**
* GridStyleManager - Manages CSS variables and styling for the grid
* Separated from GridManager to follow Single Responsibility Principle
*/
export class GridStyleManager {
private config: CalendarConfig;
constructor(config: CalendarConfig) {
this.config = config;
}
/**
* Update all grid CSS variables
*/
public updateGridStyles(resourceData: ResourceCalendarData | null = null): void {
const root = document.documentElement;
const gridSettings = this.config.getGridSettings();
const calendar = document.querySelector('swp-calendar') as HTMLElement;
const calendarType = this.config.getCalendarMode();
// Set CSS variables for time and grid measurements
this.setTimeVariables(root, gridSettings);
// Set column count based on calendar type
const columnCount = this.calculateColumnCount(calendarType, resourceData);
root.style.setProperty('--grid-columns', columnCount.toString());
// Set column width based on fitToWidth setting
this.setColumnWidth(root, gridSettings);
// Set fitToWidth data attribute for CSS targeting
if (calendar) {
calendar.setAttribute('data-fit-to-width', gridSettings.fitToWidth.toString());
}
console.log('GridStyleManager: Updated grid styles with', columnCount, 'columns for', calendarType, 'calendar');
}
/**
* Set time-related CSS variables
*/
private setTimeVariables(root: HTMLElement, gridSettings: any): void {
root.style.setProperty('--hour-height', `${gridSettings.hourHeight}px`);
root.style.setProperty('--minute-height', `${gridSettings.hourHeight / 60}px`);
root.style.setProperty('--snap-interval', gridSettings.snapInterval.toString());
root.style.setProperty('--day-start-hour', gridSettings.dayStartHour.toString());
root.style.setProperty('--day-end-hour', gridSettings.dayEndHour.toString());
root.style.setProperty('--work-start-hour', gridSettings.workStartHour.toString());
root.style.setProperty('--work-end-hour', gridSettings.workEndHour.toString());
}
/**
* Calculate number of columns based on calendar type and view
*/
private calculateColumnCount(calendarType: string, resourceData: ResourceCalendarData | null): number {
if (calendarType === 'resource' && resourceData) {
return resourceData.resources.length;
} else if (calendarType === 'date') {
const dateSettings = this.config.getDateViewSettings();
switch (dateSettings.period) {
case 'day':
return 1;
case 'week':
return dateSettings.weekDays;
case 'month':
return 7;
default:
return dateSettings.weekDays;
}
}
return 7; // Default
}
/**
* Set column width based on fitToWidth setting
*/
private setColumnWidth(root: HTMLElement, gridSettings: any): void {
if (gridSettings.fitToWidth) {
root.style.setProperty('--day-column-min-width', '50px'); // Small min-width allows columns to fit available space
} else {
root.style.setProperty('--day-column-min-width', '250px'); // Default min-width for horizontal scroll mode
}
}
/**
* Update spacer heights based on all-day events
*/
public updateSpacerHeights(allDayEventCount: number = 1): void {
const eventHeight = 26; // Height per all-day event in pixels
const padding = 0; // Top/bottom padding
const allDayHeight = allDayEventCount > 0 ? (allDayEventCount * eventHeight) + padding : 0;
// Set CSS variable for dynamic spacer height
document.documentElement.style.setProperty('--all-day-row-height', `${allDayHeight}px`);
console.log('GridStyleManager: Updated --all-day-row-height to', `${allDayHeight}px`, 'for', allDayEventCount, 'events');
}
/**
* Get current column count
*/
public getColumnCount(resourceData: ResourceCalendarData | null = null): number {
const calendarType = this.config.getCalendarMode();
return this.calculateColumnCount(calendarType, resourceData);
}
}

View file

@ -0,0 +1,119 @@
import { IEventBus } from '../types/CalendarTypes';
import { EventTypes } from '../constants/EventTypes';
import { DateUtils } from '../utils/DateUtils';
/**
* NavigationRenderer - Handles DOM rendering for navigation containers
* Separated from NavigationManager to follow Single Responsibility Principle
*/
export class NavigationRenderer {
private eventBus: IEventBus;
constructor(eventBus: IEventBus) {
this.eventBus = eventBus;
}
/**
* Render a complete container with content and events
*/
public renderContainer(parentContainer: HTMLElement, weekStart: Date): HTMLElement {
console.log('NavigationRenderer: Rendering new container for week:', weekStart.toDateString());
// Create new grid container
const newGrid = document.createElement('swp-grid-container');
newGrid.innerHTML = `
<swp-calendar-header></swp-calendar-header>
<swp-scrollable-content>
<swp-time-grid>
<swp-grid-lines></swp-grid-lines>
<swp-day-columns></swp-day-columns>
</swp-time-grid>
</swp-scrollable-content>
`;
// Position new grid - 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);
// Render week content (headers and columns)
this.renderWeekContentInContainer(newGrid, weekStart);
// Emit event to trigger event rendering
const weekEnd = DateUtils.addDays(weekStart, 6);
this.eventBus.emit(EventTypes.CONTAINER_READY_FOR_EVENTS, {
container: newGrid,
startDate: weekStart,
endDate: weekEnd
});
return newGrid;
}
/**
* Render week content in specific container
*/
private renderWeekContentInContainer(gridContainer: HTMLElement, weekStart: Date): void {
const header = gridContainer.querySelector('swp-calendar-header');
const dayColumns = gridContainer.querySelector('swp-day-columns');
if (!header || !dayColumns) return;
// Clear existing content
header.innerHTML = '';
dayColumns.innerHTML = '';
// Render headers for target week
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
for (let i = 0; i < 7; i++) {
const date = new Date(weekStart);
date.setDate(date.getDate() + i);
const headerElement = document.createElement('swp-day-header');
if (this.isToday(date)) {
headerElement.dataset.today = 'true';
}
headerElement.innerHTML = `
<swp-day-name>${days[date.getDay()]}</swp-day-name>
<swp-day-date>${date.getDate()}</swp-day-date>
`;
headerElement.dataset.date = this.formatDate(date);
header.appendChild(headerElement);
}
// Render day columns for target week
for (let i = 0; i < 7; i++) {
const column = document.createElement('swp-day-column');
const date = new Date(weekStart);
date.setDate(date.getDate() + i);
column.dataset.date = this.formatDate(date);
const eventsLayer = document.createElement('swp-events-layer');
column.appendChild(eventsLayer);
dayColumns.appendChild(column);
}
}
/**
* Utility method to format date
*/
private formatDate(date: Date): string {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}
/**
* Check if date is today
*/
private isToday(date: Date): boolean {
const today = new Date();
return date.toDateString() === today.toDateString();
}
}