Calendar/src/managers/NavigationManager.ts

258 lines
8.5 KiB
TypeScript
Raw Normal View History

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;
2025-10-15 00:58:29 +02:00
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<INavButtonClickedEventPayload>).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;
}
2025-09-23 23:51:21 +02:00
// 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
});
});
}
}