2025-07-24 22:17:38 +02:00
|
|
|
import { IEventBus } from '../types/CalendarTypes.js';
|
|
|
|
|
import { DateUtils } from '../utils/DateUtils.js';
|
|
|
|
|
import { EventTypes } from '../constants/EventTypes.js';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
private currentWeek: Date;
|
|
|
|
|
private targetWeek: Date;
|
|
|
|
|
private animationQueue: number = 0;
|
|
|
|
|
|
|
|
|
|
constructor(eventBus: IEventBus) {
|
2025-08-09 00:31:44 +02:00
|
|
|
console.log('🧭 NavigationManager: Constructor called');
|
2025-07-24 22:17:38 +02:00
|
|
|
this.eventBus = eventBus;
|
|
|
|
|
this.currentWeek = DateUtils.getWeekStart(new Date(), 0); // Sunday start like POC
|
|
|
|
|
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
|
|
|
|
|
console.log('NavigationManager: Waiting for CALENDAR_INITIALIZED before updating DOM');
|
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
|
|
|
|
|
this.eventBus.on(EventTypes.CALENDAR_INITIALIZED, () => {
|
|
|
|
|
console.log('NavigationManager: Received CALENDAR_INITIALIZED, updating week info');
|
|
|
|
|
this.updateWeekInfo();
|
|
|
|
|
});
|
|
|
|
|
|
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
|
|
|
|
|
this.eventBus.on(EventTypes.NAVIGATE_TO_DATE, (event: Event) => {
|
|
|
|
|
const customEvent = event as CustomEvent;
|
|
|
|
|
const targetDate = new Date(customEvent.detail.date);
|
|
|
|
|
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 = DateUtils.getWeekStart(today, 0);
|
|
|
|
|
|
|
|
|
|
// 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 = DateUtils.getWeekStart(date, 0);
|
|
|
|
|
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
|
|
|
/**
|
|
|
|
|
* POC-style animation transition - creates new grid container and slides it in
|
|
|
|
|
*/
|
2025-07-24 22:17:38 +02:00
|
|
|
private animateTransition(direction: 'prev' | 'next', targetWeek: Date): void {
|
2025-08-12 00:07:39 +02:00
|
|
|
const container = document.querySelector('swp-calendar-container');
|
|
|
|
|
const currentGrid = container?.querySelector('swp-grid-container');
|
2025-07-24 22:17:38 +02:00
|
|
|
|
2025-08-12 00:07:39 +02:00
|
|
|
if (!container || !currentGrid) {
|
|
|
|
|
console.warn('NavigationManager: Required DOM elements not found');
|
2025-07-24 22:17:38 +02:00
|
|
|
return;
|
|
|
|
|
}
|
2025-08-12 00:07:39 +02:00
|
|
|
|
|
|
|
|
console.log(`NavigationManager: Starting ${direction} animation to ${targetWeek.toDateString()}`);
|
|
|
|
|
|
|
|
|
|
// Create new grid container (POC approach)
|
|
|
|
|
const newGrid = document.createElement('swp-grid-container');
|
|
|
|
|
newGrid.innerHTML = `
|
|
|
|
|
<swp-calendar-header></swp-calendar-header>
|
|
|
|
|
<swp-scrollable-content>
|
|
|
|
|
<swp-time-grid>
|
|
|
|
|
<swp-grid-lines></swp-grid-lines>
|
|
|
|
|
<swp-day-columns></swp-day-columns>
|
|
|
|
|
</swp-time-grid>
|
|
|
|
|
</swp-scrollable-content>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// Position new grid off-screen (POC positioning)
|
|
|
|
|
newGrid.style.position = 'absolute';
|
|
|
|
|
newGrid.style.top = '0';
|
|
|
|
|
newGrid.style.left = '0';
|
|
|
|
|
newGrid.style.width = '100%';
|
|
|
|
|
newGrid.style.height = '100%';
|
|
|
|
|
newGrid.style.transform = direction === 'next' ? 'translateX(100%)' : 'translateX(-100%)';
|
|
|
|
|
|
|
|
|
|
// Add to container
|
|
|
|
|
container.appendChild(newGrid);
|
|
|
|
|
|
|
|
|
|
// Render new content for target week
|
|
|
|
|
this.renderWeekContent(newGrid, targetWeek);
|
|
|
|
|
|
|
|
|
|
// Animate transition (POC animation)
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
// Slide out current grid
|
|
|
|
|
(currentGrid as HTMLElement).style.transform = direction === 'next' ? 'translateX(-100%)' : 'translateX(100%)';
|
|
|
|
|
(currentGrid as HTMLElement).style.opacity = '0.5';
|
2025-07-24 22:17:38 +02:00
|
|
|
|
2025-08-12 00:31:02 +02:00
|
|
|
// Cleanup: Remove all old grids except the new one after animation
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
const allGrids = container.querySelectorAll('swp-grid-container');
|
|
|
|
|
// Keep only the newest grid (last one), remove all others
|
|
|
|
|
for (let i = 0; i < allGrids.length - 1; i++) {
|
|
|
|
|
allGrids[i].remove();
|
|
|
|
|
}
|
|
|
|
|
}, 450);
|
|
|
|
|
|
2025-08-12 00:07:39 +02:00
|
|
|
// Slide in new grid
|
|
|
|
|
newGrid.style.transform = 'translateX(0)';
|
|
|
|
|
|
2025-08-12 00:31:02 +02:00
|
|
|
// Wait for new grid animation to complete before updating state
|
2025-07-24 22:17:38 +02:00
|
|
|
setTimeout(() => {
|
2025-08-12 00:07:39 +02:00
|
|
|
newGrid.style.position = 'relative';
|
|
|
|
|
|
2025-08-12 00:31:02 +02:00
|
|
|
// Only now is the animation truly complete
|
2025-07-24 22:17:38 +02:00
|
|
|
this.currentWeek = new Date(targetWeek);
|
|
|
|
|
this.animationQueue--;
|
|
|
|
|
|
2025-08-12 00:31:02 +02:00
|
|
|
// If this was the last queued animation, ensure we're in sync
|
2025-07-24 22:17:38 +02:00
|
|
|
if (this.animationQueue === 0) {
|
|
|
|
|
this.currentWeek = new Date(this.targetWeek);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update week info and notify other managers
|
|
|
|
|
this.updateWeekInfo();
|
|
|
|
|
this.eventBus.emit(EventTypes.WEEK_CHANGED, {
|
|
|
|
|
weekStart: this.currentWeek,
|
|
|
|
|
weekEnd: DateUtils.addDays(this.currentWeek, 6)
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-12 00:07:39 +02:00
|
|
|
console.log(`NavigationManager: Completed ${direction} animation`);
|
2025-08-12 00:31:02 +02:00
|
|
|
}, 400); // Wait for slide-in animation to complete
|
2025-08-12 00:07:39 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Render week content in the new grid container
|
|
|
|
|
*/
|
|
|
|
|
private renderWeekContent(gridContainer: HTMLElement, weekStart: Date): void {
|
|
|
|
|
const header = gridContainer.querySelector('swp-calendar-header');
|
|
|
|
|
const dayColumns = gridContainer.querySelector('swp-day-columns');
|
|
|
|
|
|
|
|
|
|
if (!header || !dayColumns) return;
|
|
|
|
|
|
|
|
|
|
// Clear existing content
|
|
|
|
|
header.innerHTML = '';
|
|
|
|
|
dayColumns.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
// Render headers for target week
|
|
|
|
|
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
|
|
|
for (let i = 0; i < 7; i++) {
|
|
|
|
|
const date = new Date(weekStart);
|
|
|
|
|
date.setDate(date.getDate() + i);
|
|
|
|
|
|
|
|
|
|
const headerElement = document.createElement('swp-day-header');
|
|
|
|
|
if (this.isToday(date)) {
|
|
|
|
|
headerElement.dataset.today = 'true';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
headerElement.innerHTML = `
|
|
|
|
|
<swp-day-name>${days[date.getDay()]}</swp-day-name>
|
|
|
|
|
<swp-day-date>${date.getDate()}</swp-day-date>
|
|
|
|
|
`;
|
|
|
|
|
headerElement.dataset.date = this.formatDate(date);
|
|
|
|
|
|
|
|
|
|
header.appendChild(headerElement);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render day columns for target week
|
|
|
|
|
for (let i = 0; i < 7; i++) {
|
|
|
|
|
const column = document.createElement('swp-day-column');
|
|
|
|
|
const date = new Date(weekStart);
|
|
|
|
|
date.setDate(date.getDate() + i);
|
|
|
|
|
column.dataset.date = this.formatDate(date);
|
|
|
|
|
|
|
|
|
|
const eventsLayer = document.createElement('swp-events-layer');
|
|
|
|
|
column.appendChild(eventsLayer);
|
|
|
|
|
|
|
|
|
|
dayColumns.appendChild(column);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NOTE: Removed POC event emission to prevent interference with production code
|
|
|
|
|
// POC events should not trigger production event rendering
|
|
|
|
|
// this.eventBus.emit(EventTypes.WEEK_CONTENT_RENDERED, { ... });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Utility functions (from POC)
|
|
|
|
|
private formatDate(date: Date): string {
|
|
|
|
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private isToday(date: Date): boolean {
|
|
|
|
|
const today = new Date();
|
|
|
|
|
return date.toDateString() === today.toDateString();
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private updateWeekInfo(): void {
|
|
|
|
|
const weekNumber = DateUtils.getWeekNumber(this.currentWeek);
|
|
|
|
|
const weekEnd = DateUtils.addDays(this.currentWeek, 6);
|
|
|
|
|
const dateRange = DateUtils.formatDateRange(this.currentWeek, weekEnd);
|
|
|
|
|
|
|
|
|
|
// Update week info in DOM
|
|
|
|
|
const weekNumberElement = document.querySelector('swp-week-number');
|
|
|
|
|
const dateRangeElement = document.querySelector('swp-date-range');
|
|
|
|
|
|
|
|
|
|
if (weekNumberElement) {
|
|
|
|
|
weekNumberElement.textContent = `Week ${weekNumber}`;
|
2025-08-09 00:31:44 +02:00
|
|
|
console.log('NavigationManager: Updated week number:', `Week ${weekNumber}`);
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('NavigationManager: swp-week-number element not found in DOM');
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (dateRangeElement) {
|
|
|
|
|
dateRangeElement.textContent = dateRange;
|
2025-08-09 00:31:44 +02:00
|
|
|
console.log('NavigationManager: Updated date range:', dateRange);
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('NavigationManager: swp-date-range element not found in DOM');
|
2025-07-24 22:17:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Notify other managers about week info update
|
|
|
|
|
this.eventBus.emit(EventTypes.WEEK_INFO_UPDATED, {
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
this.eventBus.emit(EventTypes.WEEK_CHANGED, {
|
|
|
|
|
weekStart: this.currentWeek,
|
|
|
|
|
weekEnd: DateUtils.addDays(this.currentWeek, 6)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|