diff --git a/src/core/CalendarConfig.ts b/src/core/CalendarConfig.ts index e63aa5c..8eb0fe4 100644 --- a/src/core/CalendarConfig.ts +++ b/src/core/CalendarConfig.ts @@ -39,6 +39,13 @@ export class CalendarConfig { showWorkHours: true, fitToWidth: false, // Fit columns to calendar width (no horizontal scroll) + // Scrollbar styling + scrollbarWidth: 16, // Width of scrollbar in pixels + scrollbarColor: '#666', // Scrollbar thumb color + scrollbarTrackColor: '#f0f0f0', // Scrollbar track color + scrollbarHoverColor: '#b53f7aff', // Scrollbar thumb hover color + scrollbarBorderRadius: 6, // Border radius for scrollbar thumb + // Interaction settings allowDrag: true, allowResize: true, diff --git a/src/managers/ScrollManager.ts b/src/managers/ScrollManager.ts index 3068035..c5664fc 100644 --- a/src/managers/ScrollManager.ts +++ b/src/managers/ScrollManager.ts @@ -5,31 +5,14 @@ import { calendarConfig } from '../core/CalendarConfig'; import { EventTypes } from '../constants/EventTypes'; /** - * Manages custom scrolling functionality for the calendar + * Manages scrolling functionality for the calendar using native scrollbars */ export class ScrollManager { - // Vertical scrolling - 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; - - // Horizontal scrolling - private bottomMiddleSpacer: 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; + private resizeObserver: ResizeObserver | null = null; constructor() { this.init(); @@ -46,14 +29,18 @@ export class ScrollManager { this.setupScrolling(); }); - // Handle mouse events for dragging (both vertical and horizontal) - document.addEventListener('mousemove', this.handleMouseMove.bind(this)); - document.addEventListener('mouseup', this.handleMouseUp.bind(this)); - // Handle window resize window.addEventListener('resize', () => { this.updateScrollableHeight(); }); + + // Handle config updates for scrollbar styling + eventBus.on(EventTypes.CONFIG_UPDATE, (event: CustomEvent) => { + const { key } = event.detail; + if (key.startsWith('scrollbar')) { + this.applyScrollbarStyling(); + } + }); } /** @@ -62,221 +49,82 @@ export class ScrollManager { private setupScrolling(): void { this.findElements(); - if (this.rightColumn && this.scrollableContent && this.calendarContainer) { + if (this.scrollableContent && this.calendarContainer) { this.setupResizeObserver(); this.updateScrollableHeight(); - this.createScrollHandle(); - this.hideNativeScrollbar(); this.setupScrollSynchronization(); - this.calculateScrollBounds(); - this.updateHandlePosition(); + this.applyScrollbarStyling(); } - // Setup horizontal scrolling - if (this.bottomMiddleSpacer && this.scrollableContent && this.weekHeader) { - this.createHorizontalScrollHandle(); + // Setup horizontal scrolling synchronization + if (this.scrollableContent && this.weekHeader) { this.setupHorizontalScrollSynchronization(); - this.calculateHorizontalScrollBounds(); - this.updateHorizontalHandlePosition(); } } + /** + * 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.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'); - - // Horizontal scrolling elements - this.bottomMiddleSpacer = document.querySelector('swp-bottom-middle-spacer'); this.weekHeader = document.querySelector('swp-week-header'); console.log('ScrollManager: Found elements:', { - rightColumn: !!this.rightColumn, - bottomMiddleSpacer: !!this.bottomMiddleSpacer, scrollableContent: !!this.scrollableContent, + calendarContainer: !!this.calendarContainer, + timeAxis: !!this.timeAxis, weekHeader: !!this.weekHeader }); } - /** - * 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 { - // 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(); - } - } - - // 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 { - // 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'); - } - } - } - - /** - * 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(); + this.scrollableContent.scrollTop = scrollTop; } /** @@ -331,8 +179,8 @@ export class ScrollManager { // Calculate available height for scrollable content const availableHeight = containerRect.height - headerHeight; - // Calculate available width (container width minus time-axis and scrollbar) - const availableWidth = containerRect.width - 60 - 20; // 60px time-axis, 20px scrollbar + // Calculate available width (container width minus time-axis) + const availableWidth = containerRect.width - 60; // 60px time-axis console.log('ScrollManager: Dynamic height calculation'); console.log('- Container height:', containerRect.height); @@ -348,23 +196,6 @@ export class ScrollManager { 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); - } - - /** - * Hide native scrollbar while keeping scroll functionality - * Note: Scrollbar hiding is now handled in CSS file - */ - private hideNativeScrollbar(): void { - // Scrollbar hiding is now handled in CSS file - // No JavaScript needed here anymore } /** @@ -385,7 +216,6 @@ export class ScrollManager { scrollTimeout = requestAnimationFrame(() => { this.syncTimeAxisPosition(); - this.updateHandlePosition(); }); }); } @@ -410,93 +240,6 @@ export class ScrollManager { } } - /** - * Create and add horizontal scroll handle to bottom middle spacer - */ - private createHorizontalScrollHandle(): void { - if (!this.bottomMiddleSpacer) return; - - // Remove existing handle if any - const existingHandle = this.bottomMiddleSpacer.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.bottomMiddleSpacer.appendChild(this.horizontalScrollHandle); - } - - /** - * Calculate horizontal scroll bounds based on content and container widths - */ - private calculateHorizontalScrollBounds(): void { - if (!this.scrollableContent || !this.bottomMiddleSpacer) 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 */ @@ -508,7 +251,6 @@ export class ScrollManager { // Listen to horizontal scroll events this.scrollableContent.addEventListener('scroll', () => { this.syncWeekHeaderPosition(); - this.updateHorizontalHandlePosition(); }); } @@ -529,7 +271,6 @@ export class ScrollManager { } } - /** * Cleanup resources */ diff --git a/src/types/CalendarTypes.ts b/src/types/CalendarTypes.ts index 17c2796..c04701c 100644 --- a/src/types/CalendarTypes.ts +++ b/src/types/CalendarTypes.ts @@ -39,6 +39,13 @@ export interface CalendarConfig { showWorkHours: boolean; fitToWidth: boolean; // Fit columns to calendar width vs horizontal scroll + // Scrollbar styling + scrollbarWidth: number; // Width of scrollbar in pixels + scrollbarColor: string; // Scrollbar thumb color + scrollbarTrackColor: string; // Scrollbar track color + scrollbarHoverColor: string; // Scrollbar thumb hover color + scrollbarBorderRadius: number; // Border radius for scrollbar thumb + // Interaction settings allowDrag: boolean; allowResize: boolean; diff --git a/wwwroot/css/calendar-layout-css.css b/wwwroot/css/calendar-layout-css.css index 6d319a0..ac7803f 100644 --- a/wwwroot/css/calendar-layout-css.css +++ b/wwwroot/css/calendar-layout-css.css @@ -38,8 +38,8 @@ swp-calendar-nav { /* Calendar container grid - POC structure */ swp-calendar-container { display: grid; - grid-template-columns: 60px 1fr 20px; - grid-template-rows: auto 1fr 20px; + grid-template-columns: 60px 1fr; + grid-template-rows: auto 1fr; height: 100%; overflow: hidden; position: relative; @@ -60,22 +60,10 @@ swp-header-spacer { } -/* Right header spacer for scrollbar alignment */ -swp-right-header-spacer { - grid-column: 3; - grid-row: 1; - height: calc(var(--header-height) + var(--all-day-row-height)); /* Dynamic height including all-day events */ - background: var(--color-surface); - border-left: 1px solid var(--color-border); - border-bottom: 1px solid var(--color-border); - z-index: 5; /* Higher than time-axis to cover it when scrolling */ - position: relative; - transition: height 300ms ease; /* Smooth height transitions */ -} /* Week container for sliding */ swp-week-container { - grid-column: 2 / 3; + grid-column: 2; grid-row: 1 / 3; display: grid; grid-template-rows: auto 1fr; @@ -85,15 +73,6 @@ swp-week-container { overflow: hidden; } -/* Right column for scrollbar */ -swp-right-column { - grid-column: 3; - grid-row: 2; - background: #f0f0f0; - border-left: 2px solid #333; - position: relative; - overflow: hidden; -} /* Time axis */ swp-time-axis { @@ -116,81 +95,8 @@ swp-time-axis-content { position: relative; } -/* Right bottom spacer */ -swp-right-bottom-spacer { - grid-column: 3; - grid-row: 3; - height: 20px; - background: var(--color-surface); - border-left: 1px solid var(--color-border); - border-top: 1px solid var(--color-border); -} - -/* Scroll handle */ -swp-scroll-handle { - position: absolute; - top: 0; - left: 2px; - width: 16px; - height: 40px; - background: #666; - border-radius: 8px; - cursor: grab; - transition: background-color 0.2s ease; - z-index: 5; -} - -swp-scroll-handle:hover { - background: #333; -} - -swp-scroll-handle.dragging { - background: #007bff; -} - -/* Bottom row for horizontal scrollbar */ -swp-bottom-spacer { - grid-column: 1; - grid-row: 3; - height: 20px; - background: var(--color-surface); - border-right: 1px solid var(--color-border); - border-top: 1px solid var(--color-border); -} - -/* Bottom middle spacer */ -swp-bottom-middle-spacer { - grid-column: 2; - grid-row: 3; - height: 20px; - background: #f0f0f0; - border-top: 2px solid #333; - overflow: hidden; - position: relative; -} -/* Horizontal scroll handle */ -swp-horizontal-scroll-handle { - position: absolute; - left: 0; - top: 2px; - width: 40px; - height: 16px; - background: #666; - border-radius: 8px; - cursor: grab; - transition: background-color 0.2s ease; - z-index: 5; -} - -swp-horizontal-scroll-handle:hover { - background: #333; -} - -swp-horizontal-scroll-handle.dragging { - background: #007bff; -} swp-hour-marker { height: var(--hour-height); @@ -321,15 +227,31 @@ swp-scrollable-content { position: relative; display: grid; /* Height and width will be set dynamically by ScrollManager via ResizeObserver */ - - /* Hide native scrollbars */ - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* IE/Edge */ } -/* Chrome/Safari/Opera */ +/* Style native scrollbars for Webkit browsers (Chrome, Safari, Edge) */ swp-scrollable-content::-webkit-scrollbar { - display: none; + width: var(--scrollbar-width, 12px); + height: var(--scrollbar-width, 12px); +} + +swp-scrollable-content::-webkit-scrollbar-track { + background: var(--scrollbar-track-color, #f0f0f0); +} + +swp-scrollable-content::-webkit-scrollbar-thumb { + background: var(--scrollbar-color, #666); + border-radius: var(--scrollbar-border-radius, 6px); +} + +swp-scrollable-content::-webkit-scrollbar-thumb:hover { + background: var(--scrollbar-hover-color, #333); +} + +/* Style native scrollbars for Firefox */ +swp-scrollable-content { + scrollbar-width: auto; /* Let it use the webkit width */ + scrollbar-color: var(--scrollbar-color, #666) var(--scrollbar-track-color, #f0f0f0); } /* Fit to width mode - disable horizontal scroll */ @@ -337,10 +259,6 @@ swp-calendar[data-fit-to-width="true"] swp-scrollable-content { overflow-x: hidden; } -/* Hide horizontal scrollbar by collapsing bottom row */ -swp-calendar[data-fit-to-width="true"] swp-calendar-container { - grid-template-rows: auto 1fr 0px; -} /* Time grid */ swp-time-grid {