2025-07-29 00:52:01 +02:00
|
|
|
// Custom scroll management for calendar week container
|
|
|
|
|
|
|
|
|
|
import { eventBus } from '../core/EventBus';
|
|
|
|
|
import { calendarConfig } from '../core/CalendarConfig';
|
|
|
|
|
import { EventTypes } from '../constants/EventTypes';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Manages custom scrolling functionality for the calendar
|
|
|
|
|
*/
|
|
|
|
|
export class ScrollManager {
|
2025-07-29 21:22:13 +02:00
|
|
|
// Vertical scrolling
|
2025-07-29 00:52:01 +02:00
|
|
|
private rightColumn: HTMLElement | null = null;
|
|
|
|
|
private scrollHandle: HTMLElement | null = null;
|
|
|
|
|
private scrollableContent: HTMLElement | null = null;
|
|
|
|
|
private calendarContainer: HTMLElement | null = null;
|
|
|
|
|
private timeAxis: HTMLElement | null = null;
|
|
|
|
|
private resizeObserver: ResizeObserver | null = null;
|
|
|
|
|
private isDragging: boolean = false;
|
|
|
|
|
private dragStartY: number = 0;
|
|
|
|
|
private scrollStartTop: number = 0;
|
|
|
|
|
private maxScrollTop: number = 0;
|
|
|
|
|
private handleHeight: number = 40;
|
|
|
|
|
|
2025-07-29 21:22:13 +02:00
|
|
|
// Horizontal scrolling
|
2025-07-29 23:01:00 +02:00
|
|
|
private bottomMiddleSpacer: HTMLElement | null = null;
|
2025-07-29 21:22:13 +02:00
|
|
|
private horizontalScrollHandle: HTMLElement | null = null;
|
|
|
|
|
private weekHeader: HTMLElement | null = null;
|
|
|
|
|
private isHorizontalDragging: boolean = false;
|
|
|
|
|
private dragStartX: number = 0;
|
|
|
|
|
private scrollStartLeft: number = 0;
|
|
|
|
|
private maxScrollLeft: number = 0;
|
|
|
|
|
private horizontalHandleWidth: number = 40;
|
|
|
|
|
|
2025-07-29 00:52:01 +02:00
|
|
|
constructor() {
|
|
|
|
|
this.init();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private init(): void {
|
|
|
|
|
this.subscribeToEvents();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private subscribeToEvents(): void {
|
|
|
|
|
// Initialize scroll when grid is rendered
|
|
|
|
|
eventBus.on(EventTypes.GRID_RENDERED, () => {
|
|
|
|
|
console.log('ScrollManager: Received GRID_RENDERED event');
|
|
|
|
|
this.setupScrolling();
|
|
|
|
|
});
|
|
|
|
|
|
2025-07-29 21:22:13 +02:00
|
|
|
// Handle mouse events for dragging (both vertical and horizontal)
|
2025-07-29 00:52:01 +02:00
|
|
|
document.addEventListener('mousemove', this.handleMouseMove.bind(this));
|
|
|
|
|
document.addEventListener('mouseup', this.handleMouseUp.bind(this));
|
|
|
|
|
|
|
|
|
|
// Handle window resize
|
|
|
|
|
window.addEventListener('resize', () => {
|
|
|
|
|
this.updateScrollableHeight();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Setup scrolling functionality after grid is rendered
|
|
|
|
|
*/
|
|
|
|
|
private setupScrolling(): void {
|
|
|
|
|
this.findElements();
|
|
|
|
|
|
|
|
|
|
if (this.rightColumn && this.scrollableContent && this.calendarContainer) {
|
|
|
|
|
this.setupResizeObserver();
|
|
|
|
|
this.updateScrollableHeight();
|
|
|
|
|
this.createScrollHandle();
|
|
|
|
|
this.hideNativeScrollbar();
|
|
|
|
|
this.setupScrollSynchronization();
|
|
|
|
|
this.calculateScrollBounds();
|
|
|
|
|
this.updateHandlePosition();
|
|
|
|
|
}
|
2025-07-29 21:22:13 +02:00
|
|
|
|
|
|
|
|
// Setup horizontal scrolling
|
2025-07-29 23:01:00 +02:00
|
|
|
if (this.bottomMiddleSpacer && this.scrollableContent && this.weekHeader) {
|
2025-07-29 21:22:13 +02:00
|
|
|
this.createHorizontalScrollHandle();
|
|
|
|
|
this.setupHorizontalScrollSynchronization();
|
|
|
|
|
this.calculateHorizontalScrollBounds();
|
|
|
|
|
this.updateHorizontalHandlePosition();
|
|
|
|
|
}
|
2025-07-29 00:52:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Find DOM elements needed for scrolling
|
|
|
|
|
*/
|
|
|
|
|
private findElements(): void {
|
|
|
|
|
this.rightColumn = document.querySelector('swp-right-column');
|
|
|
|
|
this.scrollableContent = document.querySelector('swp-scrollable-content');
|
|
|
|
|
this.calendarContainer = document.querySelector('swp-calendar-container');
|
|
|
|
|
this.timeAxis = document.querySelector('swp-time-axis');
|
2025-07-29 21:22:13 +02:00
|
|
|
|
|
|
|
|
// Horizontal scrolling elements
|
2025-07-29 23:01:00 +02:00
|
|
|
this.bottomMiddleSpacer = document.querySelector('swp-bottom-middle-spacer');
|
2025-07-29 21:22:13 +02:00
|
|
|
this.weekHeader = document.querySelector('swp-week-header');
|
|
|
|
|
|
|
|
|
|
console.log('ScrollManager: Found elements:', {
|
|
|
|
|
rightColumn: !!this.rightColumn,
|
2025-07-29 23:01:00 +02:00
|
|
|
bottomMiddleSpacer: !!this.bottomMiddleSpacer,
|
2025-07-29 21:22:13 +02:00
|
|
|
scrollableContent: !!this.scrollableContent,
|
|
|
|
|
weekHeader: !!this.weekHeader
|
|
|
|
|
});
|
2025-07-29 00:52:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create and add scroll handle to right column
|
|
|
|
|
*/
|
|
|
|
|
private createScrollHandle(): void {
|
|
|
|
|
if (!this.rightColumn) return;
|
|
|
|
|
|
|
|
|
|
// Remove existing handle if any
|
|
|
|
|
const existingHandle = this.rightColumn.querySelector('swp-scroll-handle');
|
|
|
|
|
if (existingHandle) {
|
|
|
|
|
existingHandle.remove();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create new handle
|
|
|
|
|
this.scrollHandle = document.createElement('swp-scroll-handle');
|
|
|
|
|
this.scrollHandle.addEventListener('mousedown', this.handleMouseDown.bind(this));
|
|
|
|
|
|
|
|
|
|
this.rightColumn.appendChild(this.scrollHandle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate scroll bounds based on content and container heights
|
|
|
|
|
*/
|
|
|
|
|
private calculateScrollBounds(): void {
|
|
|
|
|
if (!this.scrollableContent || !this.rightColumn) return;
|
|
|
|
|
|
|
|
|
|
const contentHeight = this.scrollableContent.scrollHeight;
|
|
|
|
|
const containerHeight = this.scrollableContent.clientHeight;
|
|
|
|
|
// Use container height as track height since right column should match scrollable area
|
|
|
|
|
const trackHeight = containerHeight;
|
|
|
|
|
|
|
|
|
|
console.log('ScrollManager Debug:');
|
|
|
|
|
console.log('- contentHeight (scrollHeight):', contentHeight);
|
|
|
|
|
console.log('- containerHeight (clientHeight):', containerHeight);
|
|
|
|
|
console.log('- trackHeight (using containerHeight):', trackHeight);
|
|
|
|
|
console.log('- scrollableContent element:', this.scrollableContent);
|
|
|
|
|
|
|
|
|
|
this.maxScrollTop = Math.max(0, contentHeight - containerHeight);
|
|
|
|
|
|
|
|
|
|
// Calculate proportional handle height based on content ratio
|
|
|
|
|
if (contentHeight > 0 && containerHeight > 0) {
|
|
|
|
|
const visibleRatio = containerHeight / contentHeight;
|
|
|
|
|
this.handleHeight = Math.max(20, Math.min(trackHeight * visibleRatio, trackHeight - 10));
|
|
|
|
|
} else {
|
|
|
|
|
this.handleHeight = 40; // fallback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('- maxScrollTop:', this.maxScrollTop);
|
|
|
|
|
console.log('- visibleRatio:', (containerHeight / contentHeight).toFixed(3));
|
|
|
|
|
console.log('- calculated handleHeight:', this.handleHeight);
|
|
|
|
|
|
|
|
|
|
// Update handle height in DOM
|
|
|
|
|
if (this.scrollHandle) {
|
|
|
|
|
this.scrollHandle.style.height = `${this.handleHeight}px`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle mouse down on scroll handle
|
|
|
|
|
*/
|
|
|
|
|
private handleMouseDown(e: MouseEvent): void {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this.isDragging = true;
|
|
|
|
|
this.dragStartY = e.clientY;
|
|
|
|
|
|
|
|
|
|
if (this.scrollHandle && this.scrollableContent) {
|
|
|
|
|
this.scrollHandle.classList.add('dragging');
|
|
|
|
|
this.scrollStartTop = this.scrollableContent.scrollTop;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle mouse move during drag
|
|
|
|
|
*/
|
|
|
|
|
private handleMouseMove(e: MouseEvent): void {
|
2025-07-29 21:22:13 +02:00
|
|
|
// Handle vertical dragging
|
|
|
|
|
if (this.isDragging && this.scrollHandle && this.scrollableContent) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
const deltaY = e.clientY - this.dragStartY;
|
|
|
|
|
// Use container height as track height
|
|
|
|
|
const trackHeight = this.scrollableContent.clientHeight - this.handleHeight;
|
|
|
|
|
|
|
|
|
|
// Ensure trackHeight is positive to avoid division by zero
|
|
|
|
|
if (trackHeight > 0) {
|
|
|
|
|
const scrollRatio = deltaY / trackHeight;
|
|
|
|
|
const newScrollTop = this.scrollStartTop + (scrollRatio * this.maxScrollTop);
|
|
|
|
|
|
|
|
|
|
// Clamp scroll position
|
|
|
|
|
const clampedScrollTop = Math.max(0, Math.min(newScrollTop, this.maxScrollTop));
|
|
|
|
|
|
|
|
|
|
// Apply scroll to content
|
|
|
|
|
this.scrollableContent.scrollTop = clampedScrollTop;
|
|
|
|
|
|
|
|
|
|
// Update handle position (this will also trigger time-axis sync via scroll event)
|
|
|
|
|
this.updateHandlePosition();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-29 00:52:01 +02:00
|
|
|
|
2025-07-29 21:22:13 +02:00
|
|
|
// Handle horizontal dragging
|
|
|
|
|
if (this.isHorizontalDragging && this.horizontalScrollHandle && this.scrollableContent) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
const deltaX = e.clientX - this.dragStartX;
|
|
|
|
|
// Use container width as track width
|
|
|
|
|
const trackWidth = this.scrollableContent.clientWidth - this.horizontalHandleWidth;
|
|
|
|
|
|
|
|
|
|
// Ensure trackWidth is positive to avoid division by zero
|
|
|
|
|
if (trackWidth > 0) {
|
|
|
|
|
const scrollRatio = deltaX / trackWidth;
|
|
|
|
|
const newScrollLeft = this.scrollStartLeft + (scrollRatio * this.maxScrollLeft);
|
|
|
|
|
|
|
|
|
|
// Clamp scroll position
|
|
|
|
|
const clampedScrollLeft = Math.max(0, Math.min(newScrollLeft, this.maxScrollLeft));
|
|
|
|
|
|
|
|
|
|
// Apply scroll to content
|
|
|
|
|
this.scrollableContent.scrollLeft = clampedScrollLeft;
|
|
|
|
|
|
|
|
|
|
// Update handle position (this will also trigger week-header sync via scroll event)
|
|
|
|
|
this.updateHorizontalHandlePosition();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-29 00:52:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle mouse up to end drag
|
|
|
|
|
*/
|
|
|
|
|
private handleMouseUp(e: MouseEvent): void {
|
2025-07-29 21:22:13 +02:00
|
|
|
// Handle vertical drag end
|
|
|
|
|
if (this.isDragging) {
|
|
|
|
|
this.isDragging = false;
|
|
|
|
|
|
|
|
|
|
if (this.scrollHandle) {
|
|
|
|
|
this.scrollHandle.classList.remove('dragging');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle horizontal drag end
|
|
|
|
|
if (this.isHorizontalDragging) {
|
|
|
|
|
this.isHorizontalDragging = false;
|
|
|
|
|
|
|
|
|
|
if (this.horizontalScrollHandle) {
|
|
|
|
|
this.horizontalScrollHandle.classList.remove('dragging');
|
|
|
|
|
}
|
2025-07-29 00:52:01 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update handle position based on current scroll
|
|
|
|
|
*/
|
|
|
|
|
private updateHandlePosition(): void {
|
|
|
|
|
if (!this.scrollHandle || !this.scrollableContent) return;
|
|
|
|
|
|
|
|
|
|
const scrollTop = this.scrollableContent.scrollTop;
|
|
|
|
|
const scrollRatio = this.maxScrollTop > 0 ? scrollTop / this.maxScrollTop : 0;
|
|
|
|
|
// Use container height as track height
|
|
|
|
|
const trackHeight = this.scrollableContent.clientHeight - this.handleHeight;
|
|
|
|
|
const handleTop = Math.max(0, Math.min(scrollRatio * trackHeight, trackHeight));
|
|
|
|
|
|
|
|
|
|
this.scrollHandle.style.top = `${handleTop}px`;
|
|
|
|
|
|
|
|
|
|
// Debug logging for handle position
|
|
|
|
|
if (scrollTop % 200 === 0) { // Log every 200px to avoid spam
|
|
|
|
|
console.log(`ScrollManager: Handle position - scrollTop: ${scrollTop}, ratio: ${scrollRatio.toFixed(3)}, handleTop: ${handleTop.toFixed(1)}, trackHeight: ${trackHeight}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Scroll to specific position
|
|
|
|
|
*/
|
|
|
|
|
scrollTo(scrollTop: number): void {
|
|
|
|
|
if (!this.scrollableContent) return;
|
|
|
|
|
|
|
|
|
|
const clampedScrollTop = Math.max(0, Math.min(scrollTop, this.maxScrollTop));
|
|
|
|
|
this.scrollableContent.scrollTop = clampedScrollTop;
|
|
|
|
|
this.updateHandlePosition();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Scroll to specific hour
|
|
|
|
|
*/
|
|
|
|
|
scrollToHour(hour: number): void {
|
|
|
|
|
const hourHeight = calendarConfig.get('hourHeight');
|
|
|
|
|
const dayStartHour = calendarConfig.get('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 week header height
|
|
|
|
|
const weekHeader = document.querySelector('swp-week-header');
|
|
|
|
|
const headerHeight = weekHeader ? weekHeader.getBoundingClientRect().height : 80;
|
|
|
|
|
|
|
|
|
|
// Calculate available height for scrollable content
|
|
|
|
|
const availableHeight = containerRect.height - headerHeight;
|
|
|
|
|
|
2025-07-29 23:17:52 +02:00
|
|
|
// Calculate available width (container width minus time-axis and scrollbar)
|
|
|
|
|
const availableWidth = containerRect.width - 60 - 20; // 60px time-axis, 20px scrollbar
|
|
|
|
|
|
2025-07-29 00:52:01 +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 00:52:01 +02:00
|
|
|
|
2025-07-29 23:17:52 +02:00
|
|
|
// Set the height and width on scrollable content
|
2025-07-29 00:52:01 +02:00
|
|
|
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`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Recalculate scroll bounds after dimension changes
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.calculateScrollBounds();
|
|
|
|
|
this.calculateHorizontalScrollBounds();
|
|
|
|
|
this.updateHandlePosition();
|
|
|
|
|
this.updateHorizontalHandlePosition();
|
|
|
|
|
}, 0);
|
2025-07-29 00:52:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Hide native scrollbar while keeping scroll functionality
|
2025-07-29 21:22:13 +02:00
|
|
|
* Note: Scrollbar hiding is now handled in CSS file
|
2025-07-29 00:52:01 +02:00
|
|
|
*/
|
|
|
|
|
private hideNativeScrollbar(): void {
|
2025-07-29 21:22:13 +02:00
|
|
|
// Scrollbar hiding is now handled in CSS file
|
|
|
|
|
// No JavaScript needed here anymore
|
2025-07-29 00:52:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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();
|
|
|
|
|
this.updateHandlePosition();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Synchronize time axis position with scrollable content
|
|
|
|
|
*/
|
|
|
|
|
private syncTimeAxisPosition(): void {
|
|
|
|
|
if (!this.scrollableContent || !this.timeAxis) return;
|
|
|
|
|
|
|
|
|
|
const scrollTop = this.scrollableContent.scrollTop;
|
|
|
|
|
|
|
|
|
|
// Use transform for smooth performance
|
|
|
|
|
this.timeAxis.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`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 21:22:13 +02:00
|
|
|
/**
|
2025-07-29 23:01:00 +02:00
|
|
|
* Create and add horizontal scroll handle to bottom middle spacer
|
2025-07-29 21:22:13 +02:00
|
|
|
*/
|
|
|
|
|
private createHorizontalScrollHandle(): void {
|
2025-07-29 23:01:00 +02:00
|
|
|
if (!this.bottomMiddleSpacer) return;
|
2025-07-29 21:22:13 +02:00
|
|
|
|
|
|
|
|
// Remove existing handle if any
|
2025-07-29 23:01:00 +02:00
|
|
|
const existingHandle = this.bottomMiddleSpacer.querySelector('swp-horizontal-scroll-handle');
|
2025-07-29 21:22:13 +02:00
|
|
|
if (existingHandle) {
|
|
|
|
|
existingHandle.remove();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create new handle
|
|
|
|
|
this.horizontalScrollHandle = document.createElement('swp-horizontal-scroll-handle');
|
|
|
|
|
this.horizontalScrollHandle.addEventListener('mousedown', this.handleHorizontalMouseDown.bind(this));
|
|
|
|
|
|
2025-07-29 23:01:00 +02:00
|
|
|
this.bottomMiddleSpacer.appendChild(this.horizontalScrollHandle);
|
2025-07-29 21:22:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate horizontal scroll bounds based on content and container widths
|
|
|
|
|
*/
|
|
|
|
|
private calculateHorizontalScrollBounds(): void {
|
2025-07-29 23:01:00 +02:00
|
|
|
if (!this.scrollableContent || !this.bottomMiddleSpacer) return;
|
2025-07-29 21:22:13 +02:00
|
|
|
|
|
|
|
|
const contentWidth = this.scrollableContent.scrollWidth;
|
|
|
|
|
const containerWidth = this.scrollableContent.clientWidth;
|
|
|
|
|
const trackWidth = containerWidth;
|
|
|
|
|
|
|
|
|
|
console.log('ScrollManager Horizontal Debug:');
|
|
|
|
|
console.log('- contentWidth (scrollWidth):', contentWidth);
|
|
|
|
|
console.log('- containerWidth (clientWidth):', containerWidth);
|
|
|
|
|
console.log('- trackWidth (using containerWidth):', trackWidth);
|
|
|
|
|
|
|
|
|
|
this.maxScrollLeft = Math.max(0, contentWidth - containerWidth);
|
|
|
|
|
|
|
|
|
|
// Calculate proportional handle width based on content ratio
|
|
|
|
|
if (contentWidth > 0 && containerWidth > 0) {
|
|
|
|
|
const visibleRatio = containerWidth / contentWidth;
|
|
|
|
|
this.horizontalHandleWidth = Math.max(20, Math.min(trackWidth * visibleRatio, trackWidth - 10));
|
|
|
|
|
} else {
|
|
|
|
|
this.horizontalHandleWidth = 40; // fallback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('- maxScrollLeft:', this.maxScrollLeft);
|
|
|
|
|
console.log('- visibleRatio:', (containerWidth / contentWidth).toFixed(3));
|
|
|
|
|
console.log('- calculated horizontalHandleWidth:', this.horizontalHandleWidth);
|
|
|
|
|
|
|
|
|
|
// Update handle width in DOM
|
|
|
|
|
if (this.horizontalScrollHandle) {
|
|
|
|
|
this.horizontalScrollHandle.style.width = `${this.horizontalHandleWidth}px`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle mouse down on horizontal scroll handle
|
|
|
|
|
*/
|
|
|
|
|
private handleHorizontalMouseDown(e: MouseEvent): void {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this.isHorizontalDragging = true;
|
|
|
|
|
this.dragStartX = e.clientX;
|
|
|
|
|
|
|
|
|
|
if (this.horizontalScrollHandle && this.scrollableContent) {
|
|
|
|
|
this.horizontalScrollHandle.classList.add('dragging');
|
|
|
|
|
this.scrollStartLeft = this.scrollableContent.scrollLeft;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update horizontal handle position based on current scroll
|
|
|
|
|
*/
|
|
|
|
|
private updateHorizontalHandlePosition(): void {
|
|
|
|
|
if (!this.horizontalScrollHandle || !this.scrollableContent) return;
|
|
|
|
|
|
|
|
|
|
const scrollLeft = this.scrollableContent.scrollLeft;
|
|
|
|
|
const scrollRatio = this.maxScrollLeft > 0 ? scrollLeft / this.maxScrollLeft : 0;
|
|
|
|
|
const trackWidth = this.scrollableContent.clientWidth - this.horizontalHandleWidth;
|
|
|
|
|
const handleLeft = Math.max(0, Math.min(scrollRatio * trackWidth, trackWidth));
|
|
|
|
|
|
|
|
|
|
this.horizontalScrollHandle.style.left = `${handleLeft}px`;
|
|
|
|
|
|
|
|
|
|
// Debug logging for handle position
|
|
|
|
|
if (scrollLeft % 200 === 0) { // Log every 200px to avoid spam
|
|
|
|
|
console.log(`ScrollManager: Horizontal handle position - scrollLeft: ${scrollLeft}, ratio: ${scrollRatio.toFixed(3)}, handleLeft: ${handleLeft.toFixed(1)}, trackWidth: ${trackWidth}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Setup horizontal scroll synchronization between scrollable content and week header
|
|
|
|
|
*/
|
|
|
|
|
private setupHorizontalScrollSynchronization(): void {
|
|
|
|
|
if (!this.scrollableContent || !this.weekHeader) return;
|
|
|
|
|
|
|
|
|
|
console.log('ScrollManager: Setting up horizontal scroll synchronization');
|
|
|
|
|
|
|
|
|
|
// Listen to horizontal scroll events
|
|
|
|
|
this.scrollableContent.addEventListener('scroll', () => {
|
|
|
|
|
this.syncWeekHeaderPosition();
|
|
|
|
|
this.updateHorizontalHandlePosition();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Synchronize week header position with scrollable content horizontal scroll
|
|
|
|
|
*/
|
|
|
|
|
private syncWeekHeaderPosition(): void {
|
|
|
|
|
if (!this.scrollableContent || !this.weekHeader) return;
|
|
|
|
|
|
|
|
|
|
const scrollLeft = this.scrollableContent.scrollLeft;
|
|
|
|
|
|
|
|
|
|
// Use transform for smooth performance
|
|
|
|
|
this.weekHeader.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 week-header to scrollLeft: ${scrollLeft}px`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 00:52:01 +02:00
|
|
|
/**
|
|
|
|
|
* Cleanup resources
|
|
|
|
|
*/
|
|
|
|
|
destroy(): void {
|
|
|
|
|
if (this.resizeObserver) {
|
|
|
|
|
this.resizeObserver.disconnect();
|
|
|
|
|
this.resizeObserver = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|