Abstracts column data source with interface Updates managers to use configuration-driven view and date Decouples datasource creation from individual managers Improves flexibility and dependency injection for calendar components
258 lines
No EOL
8.5 KiB
TypeScript
258 lines
No EOL
8.5 KiB
TypeScript
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<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;
|
|
}
|
|
|
|
// 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
|
|
});
|
|
|
|
});
|
|
}
|
|
} |