Calendar/src/managers/ScrollManager.ts

296 lines
9.9 KiB
TypeScript
Raw Normal View History

// Custom scroll management for calendar week container
import { eventBus } from '../core/EventBus';
import { calendarConfig } from '../core/CalendarConfig';
import { EventTypes } from '../constants/EventTypes';
import { StateEvents } from '../types/CalendarState';
/**
* 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() {
console.log('📜 ScrollManager: Constructor called');
this.init();
}
private init(): void {
this.subscribeToEvents();
}
private subscribeToEvents(): void {
// Initialize scroll when grid is rendered
eventBus.on(StateEvents.GRID_RENDERED, () => {
console.log('ScrollManager: Received GRID_RENDERED event');
this.setupScrolling();
});
// Add safety check - if grid is already rendered when ScrollManager initializes
// This prevents race condition where GridManager renders before ScrollManager subscribes
//setTimeout(() => {
// const existingGrid = document.querySelector('swp-calendar-container');
// if (existingGrid && existingGrid.children.length > 0) {
// console.log('ScrollManager: Grid already exists, setting up scrolling');
// this.setupScrolling();
// }
//}, 0);
// Handle window resize
window.addEventListener('resize', () => {
this.updateScrollableHeight();
});
// Handle config updates for scrollbar styling
eventBus.on(EventTypes.CONFIG_UPDATE, (event: Event) => {
const { key } = (event as CustomEvent).detail;
if (key.startsWith('scrollbar')) {
this.applyScrollbarStyling();
}
});
}
/**
* Setup scrolling functionality after grid is rendered
*/
private setupScrolling(): void {
this.findElements();
if (this.scrollableContent && this.calendarContainer) {
this.setupResizeObserver();
this.updateScrollableHeight();
this.setupScrollSynchronization();
this.applyScrollbarStyling();
}
// Setup horizontal scrolling synchronization
if (this.scrollableContent && this.calendarHeader) {
this.setupHorizontalScrollSynchronization();
}
}
/**
* Apply scrollbar styling from configuration
*/
private applyScrollbarStyling(): void {
if (!this.scrollableContent) return;
// Get scrollbar configuration
const scrollbarWidth = calendarConfig.get('scrollbarWidth');
const scrollbarColor = calendarConfig.get('scrollbarColor');
const scrollbarTrackColor = calendarConfig.get('scrollbarTrackColor');
const scrollbarHoverColor = calendarConfig.get('scrollbarHoverColor');
const scrollbarBorderRadius = calendarConfig.get('scrollbarBorderRadius');
// Apply CSS custom properties to both the element and document root
const root = document.documentElement;
// Set on scrollable content
this.scrollableContent.style.setProperty('--scrollbar-width', `${scrollbarWidth}px`);
this.scrollableContent.style.setProperty('--scrollbar-color', scrollbarColor);
this.scrollableContent.style.setProperty('--scrollbar-track-color', scrollbarTrackColor);
this.scrollableContent.style.setProperty('--scrollbar-hover-color', scrollbarHoverColor);
this.scrollableContent.style.setProperty('--scrollbar-border-radius', `${scrollbarBorderRadius}px`);
// Also set on root for global access
root.style.setProperty('--scrollbar-width', `${scrollbarWidth}px`);
root.style.setProperty('--scrollbar-color', scrollbarColor);
root.style.setProperty('--scrollbar-track-color', scrollbarTrackColor);
root.style.setProperty('--scrollbar-hover-color', scrollbarHoverColor);
root.style.setProperty('--scrollbar-border-radius', `${scrollbarBorderRadius}px`);
console.log('ScrollManager: Applied scrollbar styling', {
width: `${scrollbarWidth}px`,
color: scrollbarColor,
trackColor: scrollbarTrackColor,
hoverColor: scrollbarHoverColor,
borderRadius: `${scrollbarBorderRadius}px`
});
}
/**
* 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');
console.log('ScrollManager: Found elements:', {
scrollableContent: !!this.scrollableContent,
calendarContainer: !!this.calendarContainer,
timeAxis: !!this.timeAxis,
calendarHeader: !!this.calendarHeader
});
}
/**
* 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) {
console.log('ScrollManager: Container resized', entry.contentRect);
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
2025-07-29 23:17:52 +02:00
console.log('ScrollManager: Dynamic height calculation');
console.log('- Container height:', containerRect.height);
console.log('- Navigation height:', navHeight);
console.log('- Header height:', headerHeight);
console.log('- Available height:', availableHeight);
2025-07-29 23:17:52 +02:00
console.log('- Available width:', availableWidth);
2025-07-29 23:17:52 +02:00
// Set the height and width on scrollable content
if (availableHeight > 0) {
this.scrollableContent.style.height = `${availableHeight}px`;
}
2025-07-29 23:17:52 +02:00
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;
console.log('ScrollManager: Setting up scroll synchronization');
// 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
2025-08-05 21:56:06 +02:00
(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
console.log(`ScrollManager: Synced time-axis to scrollTop: ${scrollTop}px`);
}
}
}
/**
* Setup horizontal scroll synchronization between scrollable content and calendar header
*/
private setupHorizontalScrollSynchronization(): void {
if (!this.scrollableContent || !this.calendarHeader) return;
console.log('ScrollManager: Setting up horizontal scroll synchronization');
// 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
console.log(`ScrollManager: Synced calendar-header to scrollLeft: ${scrollLeft}px`);
}
}
/**
* Cleanup resources
*/
destroy(): void {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
}
}