import { IEventBus } from '../types/CalendarTypes.js'; import { EventRenderingService } from '../renderers/EventRendererManager.js'; import { DateCalculator } from '../utils/DateCalculator.js'; import { CoreEvents } from '../constants/CoreEvents.js'; import { NavigationRenderer } from '../renderers/NavigationRenderer.js'; import { calendarConfig } from '../core/CalendarConfig.js'; /** * NavigationManager handles calendar navigation (prev/next/today buttons) * with simplified CSS Grid approach */ export class NavigationManager { private eventBus: IEventBus; private navigationRenderer: NavigationRenderer; private dateCalculator: DateCalculator; private currentWeek: Date; private targetWeek: Date; private animationQueue: number = 0; constructor(eventBus: IEventBus, eventRenderer: EventRenderingService) { console.log('🧭 NavigationManager: Constructor called'); this.eventBus = eventBus; this.dateCalculator = new DateCalculator(calendarConfig); this.navigationRenderer = new NavigationRenderer(eventBus, calendarConfig, eventRenderer); this.currentWeek = this.dateCalculator.getISOWeekStart(new Date()); this.targetWeek = new Date(this.currentWeek); this.init(); } private init(): void { this.setupEventListeners(); // Don't update week info immediately - wait for DOM to be ready console.log('NavigationManager: Waiting for CALENDAR_INITIALIZED before updating DOM'); } private setupEventListeners(): void { // Initial DOM update when calendar is initialized this.eventBus.on(CoreEvents.INITIALIZED, () => { console.log('NavigationManager: Received CALENDAR_INITIALIZED, updating week info'); this.updateWeekInfo(); }); // Listen for navigation button clicks document.addEventListener('click', (e) => { const target = e.target as HTMLElement; const navButton = target.closest('[data-action]') as HTMLElement; if (!navButton) return; const action = navButton.dataset.action; switch (action) { case 'prev': this.navigateToPreviousWeek(); break; case 'next': this.navigateToNextWeek(); break; case 'today': this.navigateToToday(); break; } }); // Listen for external navigation requests this.eventBus.on(CoreEvents.DATE_CHANGED, (event: Event) => { const customEvent = event as CustomEvent; const dateFromEvent = customEvent.detail.date || customEvent.detail.currentDate; // Validate date before processing if (!dateFromEvent) { console.warn('NavigationManager: No date provided in DATE_CHANGED event', customEvent.detail); return; } const targetDate = new Date(dateFromEvent); if (isNaN(targetDate.getTime())) { console.warn('NavigationManager: Invalid date in DATE_CHANGED event', dateFromEvent); return; } this.navigateToDate(targetDate); }); } private navigateToPreviousWeek(): void { this.targetWeek.setDate(this.targetWeek.getDate() - 7); const weekToShow = new Date(this.targetWeek); this.animationQueue++; this.animateTransition('prev', weekToShow); } private navigateToNextWeek(): void { this.targetWeek.setDate(this.targetWeek.getDate() + 7); const weekToShow = new Date(this.targetWeek); this.animationQueue++; this.animateTransition('next', weekToShow); } private navigateToToday(): void { const today = new Date(); const todayWeekStart = this.dateCalculator.getISOWeekStart(today); // Reset to today this.targetWeek = new Date(todayWeekStart); const currentTime = this.currentWeek.getTime(); const targetTime = todayWeekStart.getTime(); if (currentTime < targetTime) { this.animationQueue++; this.animateTransition('next', todayWeekStart); } else if (currentTime > targetTime) { this.animationQueue++; this.animateTransition('prev', todayWeekStart); } } private navigateToDate(date: Date): void { const weekStart = this.dateCalculator.getISOWeekStart(date); this.targetWeek = new Date(weekStart); const currentTime = this.currentWeek.getTime(); const targetTime = weekStart.getTime(); if (currentTime < targetTime) { this.animationQueue++; this.animateTransition('next', weekStart); } else if (currentTime > targetTime) { this.animationQueue++; this.animateTransition('prev', weekStart); } } /** * Animation transition using pre-rendered containers when available */ private animateTransition(direction: 'prev' | 'next', targetWeek: Date): void { const container = document.querySelector('swp-calendar-container'); const currentGrid = container?.querySelector('swp-grid-container:not([data-prerendered])'); if (!container || !currentGrid) { console.warn('NavigationManager: Required DOM elements not found'); return; } console.group(`🎬 NAVIGATION ANIMATION: ${direction} to ${targetWeek.toDateString()}`); console.log('1. Creating new container with events...'); let newGrid: HTMLElement; // Always create a fresh container for consistent behavior newGrid = this.navigationRenderer.renderContainer(container as HTMLElement, targetWeek); console.log('2. Starting slide animation...'); // Clear any existing transforms before animation newGrid.style.transform = ''; (currentGrid as HTMLElement).style.transform = ''; // Animate transition using Web Animations API const slideOutAnimation = (currentGrid as HTMLElement).animate([ { transform: 'translateX(0)', opacity: '1' }, { transform: direction === 'next' ? 'translateX(-100%)' : 'translateX(100%)', opacity: '0.5' } ], { duration: 400, easing: 'ease-in-out', fill: 'forwards' }); const slideInAnimation = newGrid.animate([ { transform: direction === 'next' ? 'translateX(100%)' : 'translateX(-100%)' }, { transform: 'translateX(0)' } ], { duration: 400, easing: 'ease-in-out', fill: 'forwards' }); // Handle animation completion slideInAnimation.addEventListener('finish', () => { console.log('3. Animation finished, cleaning up...'); // Cleanup: Remove all old grids except the new one const allGrids = container.querySelectorAll('swp-grid-container'); for (let i = 0; i < allGrids.length - 1; i++) { allGrids[i].remove(); } // Reset positioning newGrid.style.position = 'relative'; newGrid.removeAttribute('data-prerendered'); // Update state this.currentWeek = new Date(targetWeek); this.animationQueue--; // If this was the last queued animation, ensure we're in sync if (this.animationQueue === 0) { this.currentWeek = new Date(this.targetWeek); } // Update week info and notify other managers this.updateWeekInfo(); // Emit period change event for ScrollManager this.eventBus.emit(CoreEvents.PERIOD_CHANGED, { direction, weekStart: this.currentWeek }); console.log('✅ Animation completed successfully'); console.groupEnd(); }); } private updateWeekInfo(): void { const weekNumber = this.dateCalculator.getWeekNumber(this.currentWeek); const weekEnd = this.dateCalculator.addDays(this.currentWeek, 6); const dateRange = this.dateCalculator.formatDateRange(this.currentWeek, weekEnd); // Notify other managers about week info update - DOM manipulation should happen via events this.eventBus.emit(CoreEvents.WEEK_CHANGED, { weekNumber, dateRange, weekStart: this.currentWeek, weekEnd }); } /** * Get current week start date */ getCurrentWeek(): Date { return new Date(this.currentWeek); } /** * Get target week (where navigation is heading) */ getTargetWeek(): Date { return new Date(this.targetWeek); } /** * Check if navigation animation is in progress */ isAnimating(): boolean { return this.animationQueue > 0; } /** * Force navigation to specific week without animation */ setWeek(weekStart: Date): void { this.currentWeek = new Date(weekStart); this.targetWeek = new Date(weekStart); this.updateWeekInfo(); } // Rendering methods moved to NavigationRenderer for better separation of concerns }