Adds fixed scrollbars for improved navigation

Implements fixed scrollbars at the browser edges to enhance navigation within the calendar view. This ensures that the scrollbars remain visible regardless of the user's scroll position, providing consistent access to horizontal and vertical scrolling.

Removes the right header spacer and right column, integrating their functionality into the new fixed scrollbar components.

Additionally, synchronizes the week header position with the horizontal scroll, improving the user experience.

Scrollbar hiding is now handled in the CSS file.
This commit is contained in:
Janus Knudsen 2025-07-29 21:22:13 +02:00
parent 1822fa7287
commit 1d25ab7b53
3 changed files with 332 additions and 81 deletions

View file

@ -8,6 +8,7 @@ import { EventTypes } from '../constants/EventTypes';
* Manages custom scrolling functionality for the calendar
*/
export class ScrollManager {
// Vertical scrolling
private rightColumn: HTMLElement | null = null;
private scrollHandle: HTMLElement | null = null;
private scrollableContent: HTMLElement | null = null;
@ -20,6 +21,16 @@ export class ScrollManager {
private maxScrollTop: number = 0;
private handleHeight: number = 40;
// Horizontal scrolling
private bottomColumn: HTMLElement | null = null;
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;
constructor() {
this.init();
}
@ -35,7 +46,7 @@ export class ScrollManager {
this.setupScrolling();
});
// Handle mouse events for dragging
// Handle mouse events for dragging (both vertical and horizontal)
document.addEventListener('mousemove', this.handleMouseMove.bind(this));
document.addEventListener('mouseup', this.handleMouseUp.bind(this));
@ -60,6 +71,14 @@ export class ScrollManager {
this.calculateScrollBounds();
this.updateHandlePosition();
}
// Setup horizontal scrolling
if (this.bottomColumn && this.scrollableContent && this.weekHeader) {
this.createHorizontalScrollHandle();
this.setupHorizontalScrollSynchronization();
this.calculateHorizontalScrollBounds();
this.updateHorizontalHandlePosition();
}
}
/**
@ -70,6 +89,17 @@ export class ScrollManager {
this.scrollableContent = document.querySelector('swp-scrollable-content');
this.calendarContainer = document.querySelector('swp-calendar-container');
this.timeAxis = document.querySelector('swp-time-axis');
// Horizontal scrolling elements
this.bottomColumn = document.querySelector('swp-bottom-column');
this.weekHeader = document.querySelector('swp-week-header');
console.log('ScrollManager: Found elements:', {
rightColumn: !!this.rightColumn,
bottomColumn: !!this.bottomColumn,
scrollableContent: !!this.scrollableContent,
weekHeader: !!this.weekHeader
});
}
/**
@ -146,40 +176,75 @@ export class ScrollManager {
* Handle mouse move during drag
*/
private handleMouseMove(e: MouseEvent): void {
if (!this.isDragging || !this.scrollHandle || !this.scrollableContent) return;
// 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();
}
}
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) return;
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();
// 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();
}
}
}
/**
* Handle mouse up to end drag
*/
private handleMouseUp(e: MouseEvent): void {
if (!this.isDragging) return;
this.isDragging = false;
if (this.scrollHandle) {
this.scrollHandle.classList.remove('dragging');
// 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');
}
}
}
@ -286,22 +351,11 @@ export class ScrollManager {
/**
* Hide native scrollbar while keeping scroll functionality
* Note: Scrollbar hiding is now handled in CSS file
*/
private hideNativeScrollbar(): void {
if (!this.scrollableContent) return;
// Apply CSS to hide scrollbar
this.scrollableContent.style.scrollbarWidth = 'none'; // Firefox
(this.scrollableContent.style as any).msOverflowStyle = 'none'; // IE/Edge
// Add webkit scrollbar hiding
const style = document.createElement('style');
style.textContent = `
swp-scrollable-content::-webkit-scrollbar {
display: none; /* Chrome/Safari/Opera */
}
`;
document.head.appendChild(style);
// Scrollbar hiding is now handled in CSS file
// No JavaScript needed here anymore
}
/**
@ -344,6 +398,125 @@ export class ScrollManager {
}
}
/**
* Create and add horizontal scroll handle to bottom column
*/
private createHorizontalScrollHandle(): void {
if (!this.bottomColumn) return;
// Remove existing handle if any
const existingHandle = this.bottomColumn.querySelector('swp-horizontal-scroll-handle');
if (existingHandle) {
existingHandle.remove();
}
// Create new handle
this.horizontalScrollHandle = document.createElement('swp-horizontal-scroll-handle');
this.horizontalScrollHandle.addEventListener('mousedown', this.handleHorizontalMouseDown.bind(this));
this.bottomColumn.appendChild(this.horizontalScrollHandle);
}
/**
* Calculate horizontal scroll bounds based on content and container widths
*/
private calculateHorizontalScrollBounds(): void {
if (!this.scrollableContent || !this.bottomColumn) return;
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`);
}
}
/**
* Cleanup resources
*/