2025-07-24 22:17:38 +02:00
|
|
|
import { IEventBus } from '../types/CalendarTypes.js';
|
2025-08-20 00:39:31 +02:00
|
|
|
import { EventRenderingService } from '../renderers/EventRendererManager.js';
|
|
|
|
|
import { DateCalculator } from '../utils/DateCalculator.js';
|
2025-08-20 20:22:51 +02:00
|
|
|
import { CoreEvents } from '../constants/CoreEvents.js';
|
2025-08-17 22:54:00 +02:00
|
|
|
import { NavigationRenderer } from '../renderers/NavigationRenderer.js';
|
2025-08-18 22:27:17 +02:00
|
|
|
import { calendarConfig } from '../core/CalendarConfig.js';
|
2025-07-24 22:17:38 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* NavigationManager handles calendar navigation (prev/next/today buttons)
|
2025-07-25 23:31:25 +02:00
|
|
|
* with simplified CSS Grid approach
|
2025-07-24 22:17:38 +02:00
|
|
|
*/
|
|
|
|
|
export class NavigationManager {
|
|
|
|
|
private eventBus: IEventBus;
|
2025-08-17 22:54:00 +02:00
|
|
|
private navigationRenderer: NavigationRenderer;
|
2025-08-20 00:39:31 +02:00
|
|
|
private dateCalculator: DateCalculator;
|
2025-07-24 22:17:38 +02:00
|
|
|
private currentWeek: Date;
|
|
|
|
|
private targetWeek: Date;
|
|
|
|
|
private animationQueue: number = 0;
|
2025-09-03 18:15:33 +02:00
|
|
|
|
|
|
|
|
// Cached DOM elements to avoid redundant queries
|
|
|
|
|
private cachedCalendarContainer: HTMLElement | null = null;
|
|
|
|
|
private cachedCurrentGrid: HTMLElement | null = null;
|
2025-07-24 22:17:38 +02:00
|
|
|
|
2025-08-20 00:39:31 +02:00
|
|
|
constructor(eventBus: IEventBus, eventRenderer: EventRenderingService) {
|
2025-07-24 22:17:38 +02:00
|
|
|
this.eventBus = eventBus;
|
2025-09-03 18:38:52 +02:00
|
|
|
DateCalculator.initialize(calendarConfig);
|
|
|
|
|
this.dateCalculator = new DateCalculator();
|
2025-09-03 20:04:47 +02:00
|
|
|
this.navigationRenderer = new NavigationRenderer(eventBus, eventRenderer);
|
2025-09-03 18:38:52 +02:00
|
|
|
this.currentWeek = DateCalculator.getISOWeekStart(new Date());
|
2025-07-24 22:17:38 +02:00
|
|
|
this.targetWeek = new Date(this.currentWeek);
|
|
|
|
|
this.init();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private init(): void {
|
|
|
|
|
this.setupEventListeners();
|
2025-08-09 00:31:44 +02:00
|
|
|
// Don't update week info immediately - wait for DOM to be ready
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-03 18:15:33 +02:00
|
|
|
/**
|
|
|
|
|
* Get cached calendar container element
|
|
|
|
|
*/
|
|
|
|
|
private getCalendarContainer(): HTMLElement | null {
|
|
|
|
|
if (!this.cachedCalendarContainer) {
|
|
|
|
|
this.cachedCalendarContainer = document.querySelector('swp-calendar-container');
|
|
|
|
|
}
|
|
|
|
|
return this.cachedCalendarContainer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get cached current grid element
|
|
|
|
|
*/
|
|
|
|
|
private getCurrentGrid(): HTMLElement | null {
|
|
|
|
|
const container = this.getCalendarContainer();
|
|
|
|
|
if (!container) return null;
|
|
|
|
|
|
|
|
|
|
if (!this.cachedCurrentGrid) {
|
|
|
|
|
this.cachedCurrentGrid = container.querySelector('swp-grid-container:not([data-prerendered])');
|
|
|
|
|
}
|
|
|
|
|
return this.cachedCurrentGrid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clear cached DOM elements (call when DOM structure changes)
|
|
|
|
|
*/
|
|
|
|
|
private clearCache(): void {
|
|
|
|
|
this.cachedCalendarContainer = null;
|
|
|
|
|
this.cachedCurrentGrid = null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-24 22:17:38 +02:00
|
|
|
private setupEventListeners(): void {
|
2025-08-09 00:31:44 +02:00
|
|
|
// Initial DOM update when calendar is initialized
|
2025-08-20 21:38:54 +02:00
|
|
|
this.eventBus.on(CoreEvents.INITIALIZED, () => {
|
2025-08-09 00:31:44 +02:00
|
|
|
this.updateWeekInfo();
|
|
|
|
|
});
|
2025-08-23 00:01:59 +02:00
|
|
|
|
|
|
|
|
// 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.navigationRenderer.applyFilterToPreRenderedGrids(detail);
|
|
|
|
|
});
|
2025-08-09 00:31:44 +02:00
|
|
|
|
2025-07-24 22:17:38 +02:00
|
|
|
// 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
|
2025-08-20 21:38:54 +02:00
|
|
|
this.eventBus.on(CoreEvents.DATE_CHANGED, (event: Event) => {
|
2025-07-24 22:17:38 +02:00
|
|
|
const customEvent = event as CustomEvent;
|
2025-08-20 21:51:49 +02:00
|
|
|
const dateFromEvent = customEvent.detail.currentDate;
|
2025-08-20 21:38:54 +02:00
|
|
|
|
|
|
|
|
// Validate date before processing
|
|
|
|
|
if (!dateFromEvent) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const targetDate = new Date(dateFromEvent);
|
|
|
|
|
if (isNaN(targetDate.getTime())) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-24 22:17:38 +02:00
|
|
|
this.navigateToDate(targetDate);
|
|
|
|
|
});
|
2025-09-04 00:16:35 +02:00
|
|
|
|
|
|
|
|
// 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 = DateCalculator.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();
|
|
|
|
|
}
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
2025-09-03 18:38:52 +02:00
|
|
|
const todayWeekStart = DateCalculator.getISOWeekStart(today);
|
2025-07-24 22:17:38 +02:00
|
|
|
|
|
|
|
|
// 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 {
|
2025-09-03 18:38:52 +02:00
|
|
|
const weekStart = DateCalculator.getISOWeekStart(date);
|
2025-07-24 22:17:38 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-12 00:07:39 +02:00
|
|
|
/**
|
2025-08-16 00:51:12 +02:00
|
|
|
* Animation transition using pre-rendered containers when available
|
2025-08-12 00:07:39 +02:00
|
|
|
*/
|
2025-07-24 22:17:38 +02:00
|
|
|
private animateTransition(direction: 'prev' | 'next', targetWeek: Date): void {
|
2025-09-03 18:15:33 +02:00
|
|
|
const container = this.getCalendarContainer();
|
|
|
|
|
const currentGrid = this.getCurrentGrid();
|
2025-07-24 22:17:38 +02:00
|
|
|
|
2025-08-12 00:07:39 +02:00
|
|
|
if (!container || !currentGrid) {
|
2025-07-24 22:17:38 +02:00
|
|
|
return;
|
|
|
|
|
}
|
2025-08-12 00:07:39 +02:00
|
|
|
|
2025-08-20 00:39:31 +02:00
|
|
|
|
2025-08-16 00:51:12 +02:00
|
|
|
let newGrid: HTMLElement;
|
2025-08-12 00:07:39 +02:00
|
|
|
|
2025-08-16 00:51:12 +02:00
|
|
|
// Always create a fresh container for consistent behavior
|
2025-09-03 18:15:33 +02:00
|
|
|
newGrid = this.navigationRenderer.renderContainer(container, targetWeek);
|
2025-08-12 00:07:39 +02:00
|
|
|
|
2025-08-20 00:39:31 +02:00
|
|
|
|
2025-08-16 00:51:12 +02:00
|
|
|
// Clear any existing transforms before animation
|
|
|
|
|
newGrid.style.transform = '';
|
|
|
|
|
(currentGrid as HTMLElement).style.transform = '';
|
2025-08-12 00:07:39 +02:00
|
|
|
|
2025-08-13 23:37:23 +02:00
|
|
|
// 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', () => {
|
2025-08-20 00:39:31 +02:00
|
|
|
|
2025-08-13 23:37:23 +02:00
|
|
|
// 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';
|
2025-08-16 00:51:12 +02:00
|
|
|
newGrid.removeAttribute('data-prerendered');
|
2025-08-13 23:37:23 +02:00
|
|
|
|
2025-09-03 18:15:33 +02:00
|
|
|
// Clear cache since DOM structure changed
|
|
|
|
|
this.clearCache();
|
|
|
|
|
|
2025-08-13 23:37:23 +02:00
|
|
|
// Update state
|
|
|
|
|
this.currentWeek = new Date(targetWeek);
|
|
|
|
|
this.animationQueue--;
|
2025-07-24 22:17:38 +02:00
|
|
|
|
2025-08-13 23:37:23 +02:00
|
|
|
// If this was the last queued animation, ensure we're in sync
|
|
|
|
|
if (this.animationQueue === 0) {
|
|
|
|
|
this.currentWeek = new Date(this.targetWeek);
|
|
|
|
|
}
|
2025-08-12 00:31:02 +02:00
|
|
|
|
2025-08-13 23:37:23 +02:00
|
|
|
// Update week info and notify other managers
|
|
|
|
|
this.updateWeekInfo();
|
|
|
|
|
|
2025-08-20 20:22:51 +02:00
|
|
|
// Emit period change event for ScrollManager
|
2025-09-03 20:04:47 +02:00
|
|
|
this.eventBus.emit(CoreEvents.NAVIGATION_COMPLETED, {
|
2025-08-13 23:37:23 +02:00
|
|
|
direction,
|
2025-09-18 00:05:54 +02:00
|
|
|
currentDate: this.currentWeek
|
2025-08-13 23:37:23 +02:00
|
|
|
});
|
2025-08-12 00:07:39 +02:00
|
|
|
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-24 22:17:38 +02:00
|
|
|
private updateWeekInfo(): void {
|
2025-09-03 18:38:52 +02:00
|
|
|
const weekNumber = DateCalculator.getWeekNumber(this.currentWeek);
|
|
|
|
|
const weekEnd = DateCalculator.addDays(this.currentWeek, 6);
|
|
|
|
|
const dateRange = DateCalculator.formatDateRange(this.currentWeek, weekEnd);
|
2025-07-24 22:17:38 +02:00
|
|
|
|
2025-08-17 23:44:30 +02:00
|
|
|
// Notify other managers about week info update - DOM manipulation should happen via events
|
2025-09-03 20:04:47 +02:00
|
|
|
this.eventBus.emit(CoreEvents.PERIOD_INFO_UPDATE, {
|
2025-07-24 22:17:38 +02:00
|
|
|
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();
|
|
|
|
|
}
|
2025-08-16 00:51:12 +02:00
|
|
|
|
2025-08-17 22:54:00 +02:00
|
|
|
// Rendering methods moved to NavigationRenderer for better separation of concerns
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|