// Custom scroll management for calendar week container import { eventBus } from '../core/EventBus'; import { calendarConfig } from '../core/CalendarConfig'; import { CoreEvents } from '../constants/CoreEvents'; /** * Manages scrolling functionality for the calendar using native scrollbars */ export class ScrollManager { private scrollableContent: HTMLElement | null = null; private calendarContainer: HTMLElement | null = null; private timeAxis: HTMLElement | null = null; private calendarHeader: HTMLElement | null = null; private resizeObserver: ResizeObserver | null = null; constructor() { this.init(); } private init(): void { this.subscribeToEvents(); } /** * Public method to initialize scroll after grid is rendered */ public initialize(): void { this.setupScrolling(); } private subscribeToEvents(): void { // Handle navigation animation completion - sync time axis position eventBus.on(CoreEvents.PERIOD_CHANGED, () => { this.syncTimeAxisPosition(); this.setupScrolling(); }); // Handle window resize window.addEventListener('resize', () => { this.updateScrollableHeight(); }); } /** * Setup scrolling functionality after grid is rendered */ private setupScrolling(): void { this.findElements(); if (this.scrollableContent && this.calendarContainer) { this.setupResizeObserver(); this.updateScrollableHeight(); this.setupScrollSynchronization(); } // Setup horizontal scrolling synchronization if (this.scrollableContent && this.calendarHeader) { this.setupHorizontalScrollSynchronization(); } } /** * Find DOM elements needed for scrolling */ private findElements(): void { this.scrollableContent = document.querySelector('swp-scrollable-content'); this.calendarContainer = document.querySelector('swp-calendar-container'); this.timeAxis = document.querySelector('swp-time-axis'); this.calendarHeader = document.querySelector('swp-calendar-header'); } /** * Scroll to specific position */ scrollTo(scrollTop: number): void { if (!this.scrollableContent) return; this.scrollableContent.scrollTop = scrollTop; } /** * Scroll to specific hour */ scrollToHour(hour: number): void { const gridSettings = calendarConfig.getGridSettings(); const hourHeight = gridSettings.hourHeight; const dayStartHour = gridSettings.dayStartHour; const scrollTop = (hour - dayStartHour) * hourHeight; this.scrollTo(scrollTop); } /** * Setup ResizeObserver to monitor container size changes */ private setupResizeObserver(): void { if (!this.calendarContainer) return; // Clean up existing observer if (this.resizeObserver) { this.resizeObserver.disconnect(); } this.resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { this.updateScrollableHeight(); } }); this.resizeObserver.observe(this.calendarContainer); } /** * Calculate and update scrollable content height dynamically */ private updateScrollableHeight(): void { if (!this.scrollableContent || !this.calendarContainer) return; // Get calendar container height const containerRect = this.calendarContainer.getBoundingClientRect(); // Find navigation height const navigation = document.querySelector('swp-calendar-nav'); const navHeight = navigation ? navigation.getBoundingClientRect().height : 0; // Find calendar header height const calendarHeaderElement = document.querySelector('swp-calendar-header'); const headerHeight = calendarHeaderElement ? calendarHeaderElement.getBoundingClientRect().height : 80; // Calculate available height for scrollable content const availableHeight = containerRect.height - headerHeight; // Calculate available width (container width minus time-axis) const availableWidth = containerRect.width - 60; // 60px time-axis // Set the height and width on scrollable content if (availableHeight > 0) { this.scrollableContent.style.height = `${availableHeight}px`; } if (availableWidth > 0) { this.scrollableContent.style.width = `${availableWidth}px`; } } /** * Setup scroll synchronization between scrollable content and time axis */ private setupScrollSynchronization(): void { if (!this.scrollableContent || !this.timeAxis) return; // Throttle scroll events for better performance let scrollTimeout: number | null = null; this.scrollableContent.addEventListener('scroll', () => { if (scrollTimeout) { cancelAnimationFrame(scrollTimeout); } scrollTimeout = requestAnimationFrame(() => { this.syncTimeAxisPosition(); }); }); } /** * Synchronize time axis position with scrollable content */ private syncTimeAxisPosition(): void { if (!this.scrollableContent || !this.timeAxis) return; const scrollTop = this.scrollableContent.scrollTop; const timeAxisContent = this.timeAxis.querySelector('swp-time-axis-content'); if (timeAxisContent) { // Use transform for smooth performance (timeAxisContent as HTMLElement).style.transform = `translateY(-${scrollTop}px)`; // Debug logging (can be removed later) if (scrollTop % 100 === 0) { // Only log every 100px to avoid spam } } } /** * Setup horizontal scroll synchronization between scrollable content and calendar header */ private setupHorizontalScrollSynchronization(): void { if (!this.scrollableContent || !this.calendarHeader) return; // Listen to horizontal scroll events this.scrollableContent.addEventListener('scroll', () => { this.syncCalendarHeaderPosition(); }); } /** * Synchronize calendar header position with scrollable content horizontal scroll */ private syncCalendarHeaderPosition(): void { if (!this.scrollableContent || !this.calendarHeader) return; const scrollLeft = this.scrollableContent.scrollLeft; // Use transform for smooth performance this.calendarHeader.style.transform = `translateX(-${scrollLeft}px)`; // Debug logging (can be removed later) if (scrollLeft % 100 === 0) { // Only log every 100px to avoid spam } } /** * Cleanup resources */ destroy(): void { if (this.resizeObserver) { this.resizeObserver.disconnect(); this.resizeObserver = null; } } }