import { IEventBus, CalendarView } from '../types/CalendarTypes'; import { EventRenderingService } from '../renderers/EventRendererManager'; import { DateService } from '../utils/DateService'; import { CoreEvents } from '../constants/CoreEvents'; import { WeekInfoRenderer } from '../renderers/WeekInfoRenderer'; import { GridRenderer } from '../renderers/GridRenderer'; import { INavButtonClickedEventPayload } from '../types/EventTypes'; import { IColumnDataSource } from '../types/ColumnDataSource'; import { Configuration } from '../configurations/CalendarConfig'; export class NavigationManager { private eventBus: IEventBus; private weekInfoRenderer: WeekInfoRenderer; private gridRenderer: GridRenderer; private dateService: DateService; private config: Configuration; private dataSource: IColumnDataSource; private currentWeek: Date; private targetWeek: Date; private animationQueue: number = 0; constructor( eventBus: IEventBus, eventRenderer: EventRenderingService, gridRenderer: GridRenderer, dateService: DateService, weekInfoRenderer: WeekInfoRenderer, config: Configuration, dataSource: IColumnDataSource ) { this.eventBus = eventBus; this.dateService = dateService; this.weekInfoRenderer = weekInfoRenderer; this.gridRenderer = gridRenderer; this.config = config; this.currentWeek = this.getISOWeekStart(new Date()); this.targetWeek = new Date(this.currentWeek); this.dataSource = dataSource; this.init(); } private init(): void { this.setupEventListeners(); } /** * Get the start of the ISO week (Monday) for a given date * @param date - Any date in the week * @returns The Monday of the ISO week */ private getISOWeekStart(date: Date): Date { const weekBounds = this.dateService.getWeekBounds(date); return this.dateService.startOfDay(weekBounds.start); } private setupEventListeners(): void { // Listen for filter changes and apply to pre-rendered grids this.eventBus.on(CoreEvents.FILTER_CHANGED, (e: Event) => { const detail = (e as CustomEvent).detail; this.weekInfoRenderer.applyFilterToPreRenderedGrids(detail); }); // Listen for navigation button clicks from NavigationButtons this.eventBus.on(CoreEvents.NAV_BUTTON_CLICKED, (event: Event) => { const { direction, newDate } = (event as CustomEvent).detail; // Navigate to the new date with animation this.navigateToDate(newDate, direction); }); // Listen for external navigation requests this.eventBus.on(CoreEvents.DATE_CHANGED, (event: Event) => { const customEvent = event as CustomEvent; const dateFromEvent = customEvent.detail.currentDate; // Validate date before processing if (!dateFromEvent) { console.warn('NavigationManager: No date provided in DATE_CHANGED event'); return; } const targetDate = new Date(dateFromEvent); // Use DateService validation const validation = this.dateService.validateDate(targetDate); if (!validation.valid) { console.warn('NavigationManager: Invalid date received:', validation.error); return; } this.navigateToDate(targetDate); }); // Listen for event navigation requests this.eventBus.on(CoreEvents.NAVIGATE_TO_EVENT, (event: Event) => { const customEvent = event as CustomEvent; const { eventDate, eventStartTime } = customEvent.detail; if (!eventDate || !eventStartTime) { console.warn('NavigationManager: Invalid event navigation data'); return; } this.navigateToEventDate(eventDate, eventStartTime); }); } /** * Navigate to specific event date and emit scroll event after navigation */ private navigateToEventDate(eventDate: Date, eventStartTime: string): void { const weekStart = this.getISOWeekStart(eventDate); this.targetWeek = new Date(weekStart); const currentTime = this.currentWeek.getTime(); const targetTime = weekStart.getTime(); // Store event start time for scrolling after navigation const scrollAfterNavigation = () => { // Emit scroll request after navigation is complete this.eventBus.emit('scroll:to-event-time', { eventStartTime }); }; if (currentTime < targetTime) { this.animationQueue++; this.animateTransition('next', weekStart); // Listen for navigation completion to trigger scroll this.eventBus.once(CoreEvents.NAVIGATION_COMPLETED, scrollAfterNavigation); } else if (currentTime > targetTime) { this.animationQueue++; this.animateTransition('prev', weekStart); // Listen for navigation completion to trigger scroll this.eventBus.once(CoreEvents.NAVIGATION_COMPLETED, scrollAfterNavigation); } else { // Already on correct week, just scroll scrollAfterNavigation(); } } private navigateToDate(date: Date, direction?: 'next' | 'previous' | 'today'): void { const weekStart = this.getISOWeekStart(date); this.targetWeek = new Date(weekStart); const currentTime = this.currentWeek.getTime(); const targetTime = weekStart.getTime(); // Use provided direction or calculate based on time comparison let animationDirection: 'next' | 'prev'; if (direction === 'next') { animationDirection = 'next'; } else if (direction === 'previous') { animationDirection = 'prev'; } else if (direction === 'today') { // For "today", determine direction based on current position animationDirection = currentTime < targetTime ? 'next' : 'prev'; } else { // Fallback: calculate direction animationDirection = currentTime < targetTime ? 'next' : 'prev'; } if (currentTime !== targetTime) { this.animationQueue++; this.animateTransition(animationDirection, weekStart); } } /** * Animation transition using pre-rendered containers when available */ private animateTransition(direction: 'prev' | 'next', targetWeek: Date): void { const container = document.querySelector('swp-calendar-container') as HTMLElement; const currentGrid = document.querySelector('swp-calendar-container swp-grid-container:not([data-prerendered])') as HTMLElement; if (!container || !currentGrid) { return; } // Reset all-day height BEFORE creating new grid to ensure base height const root = document.documentElement; root.style.setProperty('--all-day-row-height', '0px'); let newGrid: HTMLElement; console.group('🔧 NavigationManager.refactored'); console.log('Calling GridRenderer instead of NavigationRenderer'); console.log('Target week:', targetWeek); // Update DataSource with target week and get columns this.dataSource.setCurrentDate(targetWeek); const columns = this.dataSource.getColumns(); // Always create a fresh container for consistent behavior newGrid = this.gridRenderer.createNavigationGrid(container, columns); console.groupEnd(); // Clear any existing transforms before animation newGrid.style.transform = ''; currentGrid.style.transform = ''; // Animate transition using Web Animations API const slideOutAnimation = currentGrid.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', () => { // 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); } // Emit navigation completed event this.eventBus.emit(CoreEvents.NAVIGATION_COMPLETED, { direction, newDate: this.currentWeek }); }); } }