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

@ -4,7 +4,7 @@ import { CalendarManager } from './managers/CalendarManager.js';
import { NavigationManager } from './managers/NavigationManager.js';
import { ViewManager } from './managers/ViewManager.js';
import { EventManager } from './managers/EventManager.js';
import { EventRenderer } from './managers/EventRenderer.js';
import { EventRenderer } from './renderers/EventRendererManager.js';
import { GridManager } from './managers/GridManager.js';
import { ScrollManager } from './managers/ScrollManager.js';
import { calendarConfig } from './core/CalendarConfig.js';

View file

@ -4,7 +4,7 @@ import { CalendarConfig } from '../core/CalendarConfig.js';
import { CalendarEvent, CalendarView, IEventBus } from '../types/CalendarTypes.js';
import { EventManager } from './EventManager.js';
import { GridManager } from './GridManager.js';
import { EventRenderer } from './EventRenderer.js';
import { EventRenderer } from '../renderers/EventRendererManager.js';
import { ScrollManager } from './ScrollManager.js';
/**

View file

@ -1,453 +0,0 @@
// Data management and API communication
import { eventBus } from '../core/EventBus';
import { EventTypes } from '../constants/EventTypes';
import { CalendarEvent, EventData, Period } from '../types/CalendarTypes';
/**
* Event creation data interface
*/
interface EventCreateData {
title: string;
type: string;
start: string;
end: string;
allDay: boolean;
description?: string;
}
/**
* Event update data interface
*/
interface EventUpdateData {
eventId: string;
changes: Partial<CalendarEvent>;
}
/**
* Manages data fetching and API communication
* Currently uses mock data until backend is implemented
*/
export class DataManager {
private baseUrl: string = '/api/events';
private useMockData: boolean = true; // Toggle this when backend is ready
private cache: Map<string, EventData> = new Map();
constructor() {
this.init();
}
private init(): void {
this.subscribeToEvents();
}
private subscribeToEvents(): void {
// Listen for period changes to fetch new data
eventBus.on(EventTypes.PERIOD_CHANGE, (e: Event) => {
this.fetchEventsForPeriod((e as CustomEvent).detail);
});
// Listen for event updates
eventBus.on(EventTypes.EVENT_UPDATE, (e: Event) => {
this.updateEvent((e as CustomEvent).detail);
});
// Listen for event creation
eventBus.on(EventTypes.EVENT_CREATE, (e: Event) => {
this.createEvent((e as CustomEvent).detail);
});
// Listen for event deletion
eventBus.on(EventTypes.EVENT_DELETE, (e: Event) => {
this.deleteEvent((e as CustomEvent).detail.eventId);
});
}
/**
* Fetch events for a specific period
*/
async fetchEventsForPeriod(period: Period): Promise<EventData> {
const cacheKey = `${period.start}-${period.end}`;
// Check cache first
if (this.cache.has(cacheKey)) {
const cachedData = this.cache.get(cacheKey)!;
eventBus.emit(EventTypes.DATA_FETCH_SUCCESS, cachedData);
return cachedData;
}
// Emit loading start
eventBus.emit(EventTypes.DATA_FETCH_START, { period });
try {
let data: EventData;
if (this.useMockData) {
// Simulate network delay
await this.delay(300);
data = this.getMockData(period);
} else {
// Real API call
const params = new URLSearchParams({
start: period.start,
end: period.end
});
const response = await fetch(`${this.baseUrl}?${params}`);
if (!response.ok) throw new Error('Failed to fetch events');
data = await response.json();
}
// Cache the data
this.cache.set(cacheKey, data);
// Emit success
eventBus.emit(EventTypes.DATA_FETCH_SUCCESS, data);
return data;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
eventBus.emit(EventTypes.DATA_FETCH_ERROR, { error: errorMessage });
throw error;
}
}
/**
* Filter events to only include those within the specified period
*/
public filterEventsForPeriod(events: CalendarEvent[], period: Period): CalendarEvent[] {
const startDate = new Date(period.start);
const endDate = new Date(period.end);
return events.filter(event => {
const eventStart = new Date(event.start);
const eventEnd = new Date(event.end);
// Include event if it overlaps with the period
return eventStart <= endDate && eventEnd >= startDate;
});
}
/**
* Get events filtered by period and optionally by all-day status
*/
public getFilteredEvents(period: Period, excludeAllDay: boolean = false): CalendarEvent[] {
const cacheKey = `${period.start}-${period.end}`;
const cachedData = this.cache.get(cacheKey);
if (!cachedData) {
console.warn('DataManager: No cached data found for period', period);
return [];
}
let filteredEvents = this.filterEventsForPeriod(cachedData.events, period);
if (excludeAllDay) {
filteredEvents = filteredEvents.filter(event => !event.allDay);
console.log(`DataManager: Filtered out all-day events, ${filteredEvents.length} non-all-day events remaining`);
}
return filteredEvents;
}
/**
* Create a new event
*/
async createEvent(eventData: EventCreateData): Promise<CalendarEvent> {
eventBus.emit(EventTypes.DATA_SYNC_START, { action: 'create' });
try {
if (this.useMockData) {
await this.delay(200);
const newEvent: CalendarEvent = {
id: `evt-${Date.now()}`,
title: eventData.title,
start: eventData.start,
end: eventData.end,
type: eventData.type,
allDay: eventData.allDay,
syncStatus: 'synced',
metadata: eventData.description ? { description: eventData.description } : undefined
};
// Clear cache to force refresh
this.cache.clear();
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
action: 'create',
event: newEvent
});
return newEvent;
} else {
// Real API call
const response = await fetch(this.baseUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(eventData)
});
if (!response.ok) throw new Error('Failed to create event');
const newEvent = await response.json();
this.cache.clear();
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
action: 'create',
event: newEvent
});
return newEvent;
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
eventBus.emit(EventTypes.DATA_SYNC_ERROR, {
action: 'create',
error: errorMessage
});
throw error;
}
}
/**
* Update an existing event
*/
async updateEvent(updateData: EventUpdateData): Promise<boolean> {
eventBus.emit(EventTypes.DATA_SYNC_START, { action: 'update' });
try {
if (this.useMockData) {
await this.delay(200);
// Clear cache to force refresh
this.cache.clear();
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
action: 'update',
eventId: updateData.eventId,
changes: updateData.changes
});
return true;
} else {
// Real API call
const response = await fetch(`${this.baseUrl}/${updateData.eventId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updateData.changes)
});
if (!response.ok) throw new Error('Failed to update event');
this.cache.clear();
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
action: 'update',
eventId: updateData.eventId
});
return true;
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
eventBus.emit(EventTypes.DATA_SYNC_ERROR, {
action: 'update',
error: errorMessage,
eventId: updateData.eventId
});
throw error;
}
}
/**
* Delete an event
*/
async deleteEvent(eventId: string): Promise<boolean> {
eventBus.emit(EventTypes.DATA_SYNC_START, { action: 'delete' });
try {
if (this.useMockData) {
await this.delay(200);
// Clear cache to force refresh
this.cache.clear();
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
action: 'delete',
eventId
});
return true;
} else {
// Real API call
const response = await fetch(`${this.baseUrl}/${eventId}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete event');
this.cache.clear();
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
action: 'delete',
eventId
});
return true;
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
eventBus.emit(EventTypes.DATA_SYNC_ERROR, {
action: 'delete',
error: errorMessage,
eventId
});
throw error;
}
}
/**
* Generate mock data for testing - only generates events within the specified period
*/
private getMockData(period: Period): EventData {
const events: CalendarEvent[] = [];
const types: string[] = ['meeting', 'meal', 'work', 'milestone'];
const titles: Record<string, string[]> = {
meeting: ['Team Standup', 'Client Meeting', 'Project Review', 'Sprint Planning', 'Design Review'],
meal: ['Breakfast', 'Lunch', 'Coffee Break', 'Dinner'],
work: ['Deep Work Session', 'Code Review', 'Documentation', 'Testing'],
milestone: ['Project Deadline', 'Release Day', 'Demo Day']
};
// Parse dates - only generate events within this exact period
const startDate = new Date(period.start);
const endDate = new Date(period.end);
console.log(`DataManager: Generating mock events for period ${period.start} to ${period.end}`);
// Generate some events for each day within the period
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
// Skip weekends for most events
const dayOfWeek = d.getDay();
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
if (isWeekend) {
// Maybe one or two events on weekends
if (Math.random() > 0.7) {
const type: string = 'meal';
const title = titles[type][Math.floor(Math.random() * titles[type].length)];
const hour = 12 + Math.floor(Math.random() * 4);
events.push({
id: `evt-${events.length + 1}`,
title,
type,
start: `${this.formatDate(d)}T${hour}:00:00`,
end: `${this.formatDate(d)}T${hour + 1}:00:00`,
allDay: false,
syncStatus: 'synced'
});
}
} else {
// Regular workday events
// Morning standup
if (Math.random() > 0.3) {
events.push({
id: `evt-${events.length + 1}`,
title: 'Team Standup',
type: 'meeting',
start: `${this.formatDate(d)}T09:00:00`,
end: `${this.formatDate(d)}T09:30:00`,
allDay: false,
syncStatus: 'synced'
});
}
// Lunch
events.push({
id: `evt-${events.length + 1}`,
title: 'Lunch',
type: 'meal',
start: `${this.formatDate(d)}T12:00:00`,
end: `${this.formatDate(d)}T13:00:00`,
allDay: false,
syncStatus: 'synced'
});
// Random afternoon events
const numAfternoonEvents = Math.floor(Math.random() * 3) + 1;
for (let i = 0; i < numAfternoonEvents; i++) {
const type = types[Math.floor(Math.random() * types.length)];
const title = titles[type][Math.floor(Math.random() * titles[type].length)];
const startHour = 13 + Math.floor(Math.random() * 4);
const duration = 1 + Math.floor(Math.random() * 2);
events.push({
id: `evt-${events.length + 1}`,
title,
type,
start: `${this.formatDate(d)}T${startHour}:${Math.random() > 0.5 ? '00' : '30'}:00`,
end: `${this.formatDate(d)}T${startHour + duration}:00:00`,
allDay: false,
syncStatus: Math.random() > 0.9 ? 'pending' : 'synced'
});
}
}
}
// Add a multi-day event if period spans multiple days
const daysDiff = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
if (daysDiff > 1) {
const midWeek = new Date(startDate);
midWeek.setDate(midWeek.getDate() + Math.min(2, daysDiff - 1));
events.push({
id: `evt-${events.length + 1}`,
title: 'Project Sprint',
type: 'milestone',
start: `${this.formatDate(startDate)}T00:00:00`,
end: `${this.formatDate(midWeek)}T23:59:59`,
allDay: true,
syncStatus: 'synced'
});
}
return {
events,
meta: {
start: period.start,
end: period.end,
total: events.length
}
};
}
/**
* Utility methods
*/
private formatDate(date: Date): string {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Clear all cached data
*/
clearCache(): void {
this.cache.clear();
}
/**
* Toggle between mock and real data
*/
setUseMockData(useMock: boolean): void {
this.useMockData = useMock;
this.clearCache();
}
}

View file

@ -6,9 +6,8 @@ import { EventTypes } from '../constants/EventTypes';
import { StateEvents } from '../types/CalendarState';
import { DateUtils } from '../utils/DateUtils';
import { ResourceCalendarData } from '../types/CalendarTypes';
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
import { HeaderRenderContext } from '../renderers/HeaderRenderer';
import { ColumnRenderContext } from '../renderers/ColumnRenderer';
import { GridRenderer } from '../renderers/GridRenderer';
import { GridStyleManager } from '../renderers/GridStyleManager';
/**
* Grid position interface
@ -28,9 +27,13 @@ export class GridManager {
private currentWeek: Date | null = null;
private allDayEvents: any[] = []; // Store all-day events for current week
private resourceData: ResourceCalendarData | null = null; // Store resource data for resource calendar
private gridRenderer: GridRenderer;
private styleManager: GridStyleManager;
constructor() {
console.log('🏗️ GridManager: Constructor called');
this.gridRenderer = new GridRenderer(calendarConfig);
this.styleManager = new GridStyleManager(calendarConfig);
this.init();
}
@ -106,7 +109,7 @@ export class GridManager {
if (detail.data && detail.data.calendarMode === 'resource') {
// Resource data will be passed in the state event
// For now just update grid styles
this.updateGridStyles();
this.styleManager.updateGridStyles(this.resourceData);
}
});
@ -135,10 +138,10 @@ export class GridManager {
}
console.log('GridManager: Starting render with grid element:', this.grid);
this.updateGridStyles();
this.renderGrid();
this.styleManager.updateGridStyles(this.resourceData);
this.gridRenderer.renderGrid(this.grid, this.currentWeek!, this.resourceData, this.allDayEvents);
const columnCount = this.getColumnCount();
const columnCount = this.styleManager.getColumnCount(this.resourceData);
console.log(`GridManager: Render complete - created ${columnCount} columns`);
// Emit GRID_RENDERED event to trigger event rendering
@ -152,181 +155,9 @@ export class GridManager {
});
}
/**
* Get current column count based on calendar mode
*/
private getColumnCount(): number {
const calendarType = calendarConfig.getCalendarMode();
if (calendarType === 'resource' && this.resourceData) {
return this.resourceData.resources.length;
} else if (calendarType === 'date') {
const dateSettings = calendarConfig.getDateViewSettings();
switch (dateSettings.period) {
case 'day': return 1;
case 'week': return dateSettings.weekDays;
case 'month': return 7;
default: return dateSettings.weekDays;
}
}
return 7; // Default
}
// Column count calculation moved to GridStyleManager
/**
* Render the complete grid using POC structure
*/
private renderGrid(): void {
console.log('GridManager: renderGrid called', {
hasGrid: !!this.grid,
hasCurrentWeek: !!this.currentWeek,
currentWeek: this.currentWeek
});
if (!this.grid || !this.currentWeek) {
console.warn('GridManager: Cannot render - missing grid or currentWeek');
return;
}
// Only clear and rebuild if grid is empty (first render)
if (this.grid.children.length === 0) {
console.log('GridManager: First render - creating grid structure');
// Create POC structure: header-spacer + time-axis + grid-container
this.createHeaderSpacer();
this.createTimeAxis();
this.createGridContainer();
} else {
console.log('GridManager: Re-render - updating existing structure');
// Just update the calendar header for all-day events
this.updateCalendarHeader();
}
console.log('GridManager: Grid rendered successfully with POC structure');
}
/**
* Create header spacer to align time axis with week content
*/
private createHeaderSpacer(): void {
if (!this.grid) return;
const headerSpacer = document.createElement('swp-header-spacer');
this.grid.appendChild(headerSpacer);
}
/**
* Create time axis (positioned beside grid container) like in POC
*/
private createTimeAxis(): void {
if (!this.grid) return;
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;
console.log('GridManager: 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);
this.grid.appendChild(timeAxis);
}
/**
* Create grid container with header and scrollable content using Strategy Pattern
*/
private createGridContainer(): void {
if (!this.grid || !this.currentWeek) return;
const gridContainer = document.createElement('swp-grid-container');
// Create calendar header using Strategy Pattern
const calendarHeader = document.createElement('swp-calendar-header');
this.renderCalendarHeader(calendarHeader);
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);
timeGrid.appendChild(columnContainer);
scrollableContent.appendChild(timeGrid);
gridContainer.appendChild(scrollableContent);
this.grid.appendChild(gridContainer);
}
/**
* Render calendar header using Strategy Pattern
*/
private renderCalendarHeader(calendarHeader: HTMLElement): void {
if (!this.currentWeek) return;
const calendarType = calendarConfig.getCalendarMode();
const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType);
const context: HeaderRenderContext = {
currentWeek: this.currentWeek,
config: calendarConfig,
allDayEvents: this.allDayEvents,
resourceData: this.resourceData
};
headerRenderer.render(calendarHeader, context);
// Update spacer heights based on all-day events
this.updateSpacerHeights();
}
/**
* Render column container using Strategy Pattern
*/
private renderColumnContainer(columnContainer: HTMLElement): void {
if (!this.currentWeek) return;
console.log('GridManager: renderColumnContainer called');
const calendarType = calendarConfig.getCalendarMode();
const columnRenderer = CalendarTypeFactory.getColumnRenderer(calendarType);
const context: ColumnRenderContext = {
currentWeek: this.currentWeek,
config: calendarConfig,
resourceData: this.resourceData
};
columnRenderer.render(columnContainer, context);
}
/**
* Update only the calendar header (for all-day events) without rebuilding entire grid
*/
private updateCalendarHeader(): void {
if (!this.grid || !this.currentWeek) return;
const calendarHeader = this.grid.querySelector('swp-calendar-header');
if (!calendarHeader) return;
// Clear existing content
calendarHeader.innerHTML = '';
// Re-render headers using Strategy Pattern
this.renderCalendarHeader(calendarHeader as HTMLElement);
}
// Grid rendering methods moved to GridRenderer
/**
* Update all-day events data and re-render if needed
@ -350,80 +181,11 @@ export class GridManager {
// Update only the calendar header if grid is already rendered
if (this.grid && this.grid.children.length > 0) {
this.updateCalendarHeader();
this.gridRenderer.renderGrid(this.grid, this.currentWeek!, this.resourceData, this.allDayEvents);
}
}
/**
* Update spacer heights based on all-day events presence
*/
private updateSpacerHeights(): void {
const allDayEventCount = 1;
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('GridManager: Updated --all-day-row-height to', `${allDayHeight}px`, 'for', allDayEventCount, 'events');
}
/**
* Update grid CSS variables
*/
private updateGridStyles(): void {
const root = document.documentElement;
const gridSettings = calendarConfig.getGridSettings();
const calendar = document.querySelector('swp-calendar') as HTMLElement;
const calendarType = calendarConfig.getCalendarMode();
// Set CSS variables
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());
// Set number of columns based on calendar type
let columnCount = 7; // Default for date mode
if (calendarType === 'resource' && this.resourceData) {
columnCount = this.resourceData.resources.length;
} else if (calendarType === 'date') {
const dateSettings = calendarConfig.getDateViewSettings();
// Calculate columns based on view type - business logic moved from config
switch (dateSettings.period) {
case 'day':
columnCount = 1;
break;
case 'week':
columnCount = dateSettings.weekDays;
break;
case 'month':
columnCount = 7;
break;
default:
columnCount = dateSettings.weekDays;
}
}
root.style.setProperty('--grid-columns', columnCount.toString());
// Set day column min width based on fitToWidth setting
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
}
// Set fitToWidth data attribute for CSS targeting
if (calendar) {
calendar.setAttribute('data-fit-to-width', gridSettings.fitToWidth.toString());
}
console.log('GridManager: Updated grid styles with', columnCount, 'columns for', calendarType, 'calendar');
}
// CSS management methods moved to GridStyleManager
/**
* Setup grid interaction handlers for POC structure

View file

@ -1,6 +1,7 @@
import { IEventBus } from '../types/CalendarTypes.js';
import { DateUtils } from '../utils/DateUtils.js';
import { EventTypes } from '../constants/EventTypes.js';
import { NavigationRenderer } from '../renderers/NavigationRenderer.js';
/**
* NavigationManager handles calendar navigation (prev/next/today buttons)
@ -8,6 +9,7 @@ import { EventTypes } from '../constants/EventTypes.js';
*/
export class NavigationManager {
private eventBus: IEventBus;
private navigationRenderer: NavigationRenderer;
private currentWeek: Date;
private targetWeek: Date;
private animationQueue: number = 0;
@ -15,6 +17,7 @@ export class NavigationManager {
constructor(eventBus: IEventBus) {
console.log('🧭 NavigationManager: Constructor called');
this.eventBus = eventBus;
this.navigationRenderer = new NavigationRenderer(eventBus);
this.currentWeek = DateUtils.getWeekStart(new Date(), 0); // Sunday start like POC
this.targetWeek = new Date(this.currentWeek);
this.init();
@ -130,7 +133,7 @@ export class NavigationManager {
// Always create a fresh container for consistent behavior
console.log('NavigationManager: Creating new container');
newGrid = this.renderContainer(container as HTMLElement, targetWeek);
newGrid = this.navigationRenderer.renderContainer(container as HTMLElement, targetWeek);
// Clear any existing transforms before animation
newGrid.style.transform = '';
@ -194,15 +197,7 @@ export class NavigationManager {
});
}
// Utility functions (from POC)
private formatDate(date: Date): string {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}
private isToday(date: Date): boolean {
const today = new Date();
return date.toDateString() === today.toDateString();
}
// Utility functions (from POC) - moved formatting to NavigationRenderer
private updateWeekInfo(): void {
const weekNumber = DateUtils.getWeekNumber(this.currentWeek);
@ -264,95 +259,5 @@ export class NavigationManager {
});
}
/**
* Render a complete container with content and events
*/
private renderContainer(parentContainer: HTMLElement, weekStart: Date): HTMLElement {
console.log('NavigationManager: 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 (with hardcoded test event)
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);
}
}
// Rendering methods moved to NavigationRenderer for better separation of concerns
}

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

@ -4,8 +4,8 @@ import { EventTypes } from '../constants/EventTypes';
import { StateEvents } from '../types/CalendarState';
import { calendarConfig } from '../core/CalendarConfig';
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
import { EventManager } from './EventManager';
import { EventRendererStrategy } from '../renderers/EventRenderer';
import { EventManager } from '../managers/EventManager';
import { EventRendererStrategy } from './EventRenderer';
/**
* EventRenderer - Render events i DOM med positionering using Strategy Pattern

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();
}
}