diff --git a/src/managers/GridManager.ts b/src/managers/GridManager.ts index 69f1304..70ce268 100644 --- a/src/managers/GridManager.ts +++ b/src/managers/GridManager.ts @@ -117,12 +117,12 @@ export class GridManager { // Clear existing grid and rebuild POC structure this.grid.innerHTML = ''; - // Create POC structure: header-spacer + time-axis + week-container + right-side + // Create POC structure: header-spacer + time-axis + week-container + fixed scrollbars this.createHeaderSpacer(); - this.createRightHeaderSpacer(); this.createTimeAxis(); - this.createRightColumn(); this.createWeekContainer(); + this.createBottomRow(); + this.createFixedScrollbars(); console.log('GridManager: Grid rendered successfully with POC structure'); } @@ -138,23 +138,22 @@ export class GridManager { } /** - * Create right header spacer to align right column with week content + * Create fixed scrollbars at browser edges */ - private createRightHeaderSpacer(): void { - if (!this.grid) return; - - const rightHeaderSpacer = document.createElement('swp-right-header-spacer'); - this.grid.appendChild(rightHeaderSpacer); - } - - /** - * Create right column beside week container - */ - private createRightColumn(): void { - if (!this.grid) return; + private createFixedScrollbars(): void { + if (!document.body) return; + // Create right scrollbar at browser edge + const rightScrollbar = document.createElement('swp-right-scrollbar'); const rightColumn = document.createElement('swp-right-column'); - this.grid.appendChild(rightColumn); + rightScrollbar.appendChild(rightColumn); + document.body.appendChild(rightScrollbar); + + // Create bottom scrollbar at browser edge + const bottomScrollbar = document.createElement('swp-bottom-scrollbar'); + const bottomColumn = document.createElement('swp-bottom-column'); + bottomScrollbar.appendChild(bottomColumn); + document.body.appendChild(bottomScrollbar); } /** @@ -210,6 +209,17 @@ export class GridManager { this.grid.appendChild(weekContainer); } + /** + * Create bottom row with spacer + */ + private createBottomRow(): void { + if (!this.grid) return; + + // Bottom spacer (left) + const bottomSpacer = document.createElement('swp-bottom-spacer'); + this.grid.appendChild(bottomSpacer); + } + /** * Render week headers like in POC */ diff --git a/src/managers/ScrollManager.ts b/src/managers/ScrollManager.ts index 9600338..ceea69f 100644 --- a/src/managers/ScrollManager.ts +++ b/src/managers/ScrollManager.ts @@ -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 */ diff --git a/wwwroot/css/calendar-layout-css.css b/wwwroot/css/calendar-layout-css.css index 64d430d..4618c4c 100644 --- a/wwwroot/css/calendar-layout-css.css +++ b/wwwroot/css/calendar-layout-css.css @@ -39,8 +39,8 @@ swp-calendar-nav { swp-calendar-container { flex: 1; display: grid; - grid-template-columns: 60px 1fr 20px; - grid-template-rows: auto 1fr; + grid-template-columns: 60px 1fr; + grid-template-rows: auto 1fr 20px; overflow: hidden; position: relative; } @@ -58,14 +58,17 @@ swp-header-spacer { position: relative; } -/* Right header spacer */ -swp-right-header-spacer { - grid-column: 3; - grid-row: 1; - height: 80px; /* Same as week header height */ - background: var(--color-surface); - border-left: 1px solid var(--color-border); - border-bottom: 1px solid var(--color-border); +/* Right scrollbar - positioned at browser edge */ +swp-right-scrollbar { + position: fixed; + top: 0; + right: 0; + width: 20px; + height: 100vh; + background: #f0f0f0; + border-left: 2px solid #333; + z-index: 1000; + overflow: hidden; } /* Week container for sliding */ @@ -93,16 +96,14 @@ swp-time-axis { flex-direction: column; } -/* Right column */ +/* Right column - now part of fixed scrollbar */ swp-right-column { - grid-column: 3; - grid-row: 2; - background: #f0f0f0; - border-left: 2px solid #333; - position: relative; - z-index: 4; - width: 20px; - overflow: hidden; + position: absolute; + top: 80px; /* Below navigation */ + left: 0; + right: 0; + bottom: 20px; /* Above horizontal scrollbar */ + background: transparent; } /* Scroll handle */ @@ -127,6 +128,60 @@ 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 scrollbar - positioned at browser edge */ +swp-bottom-scrollbar { + position: fixed; + bottom: 0; + left: 0; + right: 20px; /* Leave space for vertical scrollbar */ + height: 20px; + background: #f0f0f0; + border-top: 2px solid #333; + z-index: 1000; + overflow: hidden; +} + +swp-bottom-column { + position: absolute; + top: 0; + left: 60px; /* Start after time-axis */ + right: 0; + bottom: 0; + background: transparent; +} + +/* 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); padding: 0 8px 8px 8px; @@ -151,7 +206,8 @@ swp-hour-marker::after { /* Week header */ swp-week-header { display: grid; - grid-template-columns: repeat(7, 1fr); + grid-template-columns: repeat(7, minmax(250px, 1fr)); + min-width: 1750px; /* Match day-columns width */ background: var(--color-surface); border-bottom: 1px solid var(--color-border); position: sticky; @@ -203,11 +259,21 @@ swp-day-header[data-today="true"] swp-day-date { /* Scrollable content */ swp-scrollable-content { overflow-y: auto; - overflow-x: hidden; + overflow-x: auto; scroll-behavior: smooth; position: relative; display: grid; /* Height will be set dynamically by ScrollManager via ResizeObserver */ + width: calc(100vw - 60px - 20px); /* Viewport width minus time-axis and scrollbar */ + + /* Hide native scrollbars */ + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE/Edge */ +} + +/* Chrome/Safari/Opera */ +swp-scrollable-content::-webkit-scrollbar { + display: none; } /* Time grid */ @@ -247,12 +313,14 @@ swp-day-columns { position: absolute; inset: 0; display: grid; - grid-template-columns: repeat(7, 1fr); + grid-template-columns: repeat(7, minmax(250px, 1fr)); + min-width: 1750px; /* 7 * 250px = force horizontal scroll */ } swp-day-column { position: relative; border-right: 1px solid var(--color-grid-line); + min-width: 250px; } swp-day-column:last-child {