From 32ee35eb0290bb4eec8022912910d768050eb4b7 Mon Sep 17 00:00:00 2001 From: Janus Knudsen Date: Sun, 17 Aug 2025 22:54:00 +0200 Subject: [PATCH] Refactors grid and navigation rendering Attempt 1 --- src/index.ts | 2 +- src/managers/CalendarManager.ts | 2 +- src/managers/DataManager.ts | 453 ------------------ src/managers/GridManager.ts | 266 +--------- src/managers/NavigationManager.ts | 107 +---- src/renderers/EventRenderer.ts | 2 +- .../EventRendererManager.ts} | 4 +- src/renderers/GridRenderer.ts | 182 +++++++ src/renderers/GridStyleManager.ts | 110 +++++ src/renderers/NavigationRenderer.ts | 119 +++++ 10 files changed, 436 insertions(+), 811 deletions(-) delete mode 100644 src/managers/DataManager.ts rename src/{managers/EventRenderer.ts => renderers/EventRendererManager.ts} (97%) create mode 100644 src/renderers/GridRenderer.ts create mode 100644 src/renderers/GridStyleManager.ts create mode 100644 src/renderers/NavigationRenderer.ts diff --git a/src/index.ts b/src/index.ts index 5c57504..a63cea6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/managers/CalendarManager.ts b/src/managers/CalendarManager.ts index 6149df8..9c6f482 100644 --- a/src/managers/CalendarManager.ts +++ b/src/managers/CalendarManager.ts @@ -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'; /** diff --git a/src/managers/DataManager.ts b/src/managers/DataManager.ts deleted file mode 100644 index 9d5ac1f..0000000 --- a/src/managers/DataManager.ts +++ /dev/null @@ -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; -} - -/** - * 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 = 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 { - 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 { - 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 { - 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 { - 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 = { - 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 { - 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(); - } -} \ No newline at end of file diff --git a/src/managers/GridManager.ts b/src/managers/GridManager.ts index 40f3239..6aceac4 100644 --- a/src/managers/GridManager.ts +++ b/src/managers/GridManager.ts @@ -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 diff --git a/src/managers/NavigationManager.ts b/src/managers/NavigationManager.ts index 85d66b0..8536e26 100644 --- a/src/managers/NavigationManager.ts +++ b/src/managers/NavigationManager.ts @@ -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 = ` - - - - - - - - `; - - // 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 = ` - ${days[date.getDay()]} - ${date.getDate()} - `; - 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 } \ No newline at end of file diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index de2f312..1c71a39 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -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 diff --git a/src/managers/EventRenderer.ts b/src/renderers/EventRendererManager.ts similarity index 97% rename from src/managers/EventRenderer.ts rename to src/renderers/EventRendererManager.ts index 564c73a..c1d5798 100644 --- a/src/managers/EventRenderer.ts +++ b/src/renderers/EventRendererManager.ts @@ -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 diff --git a/src/renderers/GridRenderer.ts b/src/renderers/GridRenderer.ts new file mode 100644 index 0000000..2f60dd1 --- /dev/null +++ b/src/renderers/GridRenderer.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/renderers/GridStyleManager.ts b/src/renderers/GridStyleManager.ts new file mode 100644 index 0000000..c6cd417 --- /dev/null +++ b/src/renderers/GridStyleManager.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/renderers/NavigationRenderer.ts b/src/renderers/NavigationRenderer.ts new file mode 100644 index 0000000..365abf0 --- /dev/null +++ b/src/renderers/NavigationRenderer.ts @@ -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 = ` + + + + + + + + `; + + // 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 = ` + ${days[date.getDay()]} + ${date.getDate()} + `; + 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(); + } +} \ No newline at end of file