// Grid structure management - Simple CSS Grid Implementation import { eventBus } from '../core/EventBus'; import { calendarConfig } from '../core/CalendarConfig'; import { EventTypes } from '../constants/EventTypes'; import { DateUtils } from '../utils/DateUtils'; /** * Grid position interface */ interface GridPosition { minutes: number; time: string; y: number; } /** * Manages the calendar grid structure using simple CSS Grid */ export class GridManager { private container: HTMLElement | null = null; private grid: HTMLElement | null = null; private currentWeek: Date | null = null; constructor() { this.init(); } private init(): void { this.findElements(); this.subscribeToEvents(); // Set initial current week to today if not set if (!this.currentWeek) { this.currentWeek = this.getWeekStart(new Date()); console.log('GridManager: Set initial currentWeek to', this.currentWeek); // Render initial grid this.render(); } } private getWeekStart(date: Date): Date { // Use DateUtils for consistent week calculation (Sunday = 0) return DateUtils.getWeekStart(date, 0); } private findElements(): void { this.grid = document.querySelector('swp-calendar-container'); } private subscribeToEvents(): void { // Re-render grid on config changes eventBus.on(EventTypes.CONFIG_UPDATE, (e: Event) => { const detail = (e as CustomEvent).detail; if (['dayStartHour', 'dayEndHour', 'hourHeight', 'view', 'weekDays'].includes(detail.key)) { this.render(); } }); // Re-render on view change eventBus.on(EventTypes.VIEW_CHANGE, () => { this.render(); }); // Re-render on period change eventBus.on(EventTypes.PERIOD_CHANGE, (e: Event) => { const detail = (e as CustomEvent).detail; this.currentWeek = detail.week; this.render(); }); // Handle week changes from NavigationManager eventBus.on(EventTypes.WEEK_CHANGED, (e: Event) => { const detail = (e as CustomEvent).detail; this.currentWeek = detail.weekStart; this.render(); }); // Handle grid clicks this.setupGridInteractions(); } /** * Render the complete grid structure */ render(): void { if (!this.grid) return; this.updateGridStyles(); this.renderGrid(); // Emit grid rendered event console.log('GridManager: Emitting GRID_RENDERED event'); eventBus.emit(EventTypes.GRID_RENDERED); console.log('GridManager: GRID_RENDERED event emitted'); } /** * 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; } // Clear existing grid and rebuild POC structure this.grid.innerHTML = ''; // Create POC structure: header-spacer + time-axis + week-container + right-column + bottom spacers this.createHeaderSpacer(); this.createRightHeaderSpacer(); this.createTimeAxis(); this.createWeekContainer(); this.createRightColumn(); this.createBottomRow(); 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 right header spacer for scrollbar alignment */ private createRightHeaderSpacer(): void { if (!this.grid) return; const rightHeaderSpacer = document.createElement('swp-right-header-spacer'); this.grid.appendChild(rightHeaderSpacer); } /** * Create right column for scrollbar area */ private createRightColumn(): void { if (!this.grid) return; const rightColumn = document.createElement('swp-right-column'); this.grid.appendChild(rightColumn); } /** * Create time axis (positioned beside week 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 startHour = calendarConfig.get('dayStartHour'); const endHour = calendarConfig.get('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 week container with header and scrollable content like in POC */ private createWeekContainer(): void { if (!this.grid || !this.currentWeek) return; const weekContainer = document.createElement('swp-week-container'); // Create week header const weekHeader = document.createElement('swp-week-header'); this.renderWeekHeaders(weekHeader); weekContainer.appendChild(weekHeader); // 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 day columns const dayColumns = document.createElement('swp-day-columns'); this.renderDayColumns(dayColumns); timeGrid.appendChild(dayColumns); scrollableContent.appendChild(timeGrid); weekContainer.appendChild(scrollableContent); this.grid.appendChild(weekContainer); } /** * Create bottom row with spacers */ private createBottomRow(): void { if (!this.grid) return; // Bottom spacer (left) const bottomSpacer = document.createElement('swp-bottom-spacer'); this.grid.appendChild(bottomSpacer); // Bottom middle spacer const bottomMiddleSpacer = document.createElement('swp-bottom-middle-spacer'); this.grid.appendChild(bottomMiddleSpacer); // Right bottom spacer const rightBottomSpacer = document.createElement('swp-right-bottom-spacer'); this.grid.appendChild(rightBottomSpacer); } /** * Render week headers like in POC */ private renderWeekHeaders(weekHeader: HTMLElement): void { if (!this.currentWeek) return; const dates = this.getWeekDates(this.currentWeek); const weekDays = calendarConfig.get('weekDays'); const daysToShow = dates.slice(0, weekDays); daysToShow.forEach((date) => { const header = document.createElement('swp-day-header'); if (this.isToday(date)) { (header as any).dataset.today = 'true'; } header.innerHTML = ` ${this.getDayName(date)} ${date.getDate()} `; (header as any).dataset.date = this.formatDate(date); weekHeader.appendChild(header); }); } /** * Render day columns like in POC */ private renderDayColumns(dayColumns: HTMLElement): void { console.log('GridManager: renderDayColumns called'); if (!this.currentWeek) { console.log('GridManager: No currentWeek, returning'); return; } const dates = this.getWeekDates(this.currentWeek); const weekDays = calendarConfig.get('weekDays'); const daysToShow = dates.slice(0, weekDays); console.log('GridManager: About to render', daysToShow.length, 'day columns'); daysToShow.forEach((date, dayIndex) => { const column = document.createElement('swp-day-column'); (column as any).dataset.date = this.formatDate(date); const eventsLayer = document.createElement('swp-events-layer'); column.appendChild(eventsLayer); dayColumns.appendChild(column); }); } /** * Update grid CSS variables */ private updateGridStyles(): void { const root = document.documentElement; const config = calendarConfig.getAll(); // Set CSS variables root.style.setProperty('--hour-height', `${config.hourHeight}px`); root.style.setProperty('--minute-height', `${config.hourHeight / 60}px`); root.style.setProperty('--snap-interval', config.snapInterval.toString()); root.style.setProperty('--day-start-hour', config.dayStartHour.toString()); root.style.setProperty('--day-end-hour', config.dayEndHour.toString()); root.style.setProperty('--work-start-hour', config.workStartHour.toString()); root.style.setProperty('--work-end-hour', config.workEndHour.toString()); } /** * Setup grid interaction handlers for POC structure */ private setupGridInteractions(): void { if (!this.grid) return; // Click handler for day columns this.grid.addEventListener('click', (e: MouseEvent) => { // Ignore if clicking on an event if ((e.target as Element).closest('swp-event')) return; const dayColumn = (e.target as Element).closest('swp-day-column') as HTMLElement; if (!dayColumn) return; const position = this.getClickPosition(e, dayColumn); eventBus.emit(EventTypes.GRID_CLICK, { date: (dayColumn as any).dataset.date, time: position.time, minutes: position.minutes }); }); // Double click handler for day columns this.grid.addEventListener('dblclick', (e: MouseEvent) => { // Ignore if clicking on an event if ((e.target as Element).closest('swp-event')) return; const dayColumn = (e.target as Element).closest('swp-day-column') as HTMLElement; if (!dayColumn) return; const position = this.getClickPosition(e, dayColumn); eventBus.emit(EventTypes.GRID_DBLCLICK, { date: (dayColumn as any).dataset.date, time: position.time, minutes: position.minutes }); }); } /** * Get click position in day column (POC structure) */ private getClickPosition(event: MouseEvent, dayColumn: HTMLElement): GridPosition { const rect = dayColumn.getBoundingClientRect(); const y = event.clientY - rect.top; const hourHeight = calendarConfig.get('hourHeight'); const minuteHeight = hourHeight / 60; const snapInterval = calendarConfig.get('snapInterval'); const dayStartHour = calendarConfig.get('dayStartHour'); // Calculate total minutes from day start let totalMinutes = Math.floor(y / minuteHeight); // Snap to interval totalMinutes = Math.round(totalMinutes / snapInterval) * snapInterval; // Add day start offset totalMinutes += dayStartHour * 60; return { minutes: totalMinutes, time: this.minutesToTime(totalMinutes), y: y }; } /** * Scroll to specific hour */ scrollToHour(hour: number): void { if (!this.grid) return; const hourHeight = calendarConfig.get('hourHeight'); const dayStartHour = calendarConfig.get('dayStartHour'); const headerHeight = 80; // Header row height const scrollTop = headerHeight + ((hour - dayStartHour) * hourHeight); this.grid.scrollTop = scrollTop; } /** * Utility methods */ private formatHour(hour: number): string { const period = hour >= 12 ? 'PM' : 'AM'; const displayHour = hour > 12 ? hour - 12 : (hour === 0 ? 12 : hour); return `${displayHour} ${period}`; } private formatDate(date: Date): string { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; } private getDayName(date: Date): string { const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; return days[date.getDay()]; } private getWeekDates(weekStart: Date): Date[] { const dates: Date[] = []; for (let i = 0; i < 7; i++) { const date = new Date(weekStart); date.setDate(weekStart.getDate() + i); dates.push(date); } return dates; } private isToday(date: Date): boolean { const today = new Date(); return date.toDateString() === today.toDateString(); } private minutesToTime(totalMinutes: number): string { const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; const period = hours >= 12 ? 'PM' : 'AM'; const displayHour = hours > 12 ? hours - 12 : (hours === 0 ? 12 : hours); return `${displayHour}:${minutes.toString().padStart(2, '0')} ${period}`; } }