From 9f6d4333cb83a9077c174f676b5f04535f574f72 Mon Sep 17 00:00:00 2001 From: Janus Knudsen Date: Tue, 29 Jul 2025 00:52:01 +0200 Subject: [PATCH] Implements custom scroll and event logging Adds custom scroll management for the calendar week view, replacing native scrollbars with a custom handle. Introduces categorized event logging with console grouping and styling, enhancing debug output. It also allows configuring logging for specific event categories. --- src/core/EventBus.ts | 86 ++++++- src/index.ts | 5 +- src/managers/EventRenderer.ts | 8 +- src/managers/GridManager.ts | 42 +++- src/managers/ScrollManager.ts | 356 ++++++++++++++++++++++++++++ wwwroot/css/calendar-layout-css.css | 84 ++++++- wwwroot/index.html | 88 +++---- 7 files changed, 606 insertions(+), 63 deletions(-) create mode 100644 src/managers/ScrollManager.ts diff --git a/src/core/EventBus.ts b/src/core/EventBus.ts index aa81bed..9991ab4 100644 --- a/src/core/EventBus.ts +++ b/src/core/EventBus.ts @@ -9,6 +9,17 @@ export class EventBus implements IEventBus { private eventLog: EventLogEntry[] = []; private debug: boolean = false; private listeners: Set = new Set(); + + // Log configuration for different categories + private logConfig: { [key: string]: boolean } = { + calendar: true, + grid: true, + event: true, + scroll: true, + navigation: true, + view: true, + default: true + }; /** * Subscribe to an event via DOM addEventListener @@ -55,9 +66,9 @@ export class EventBus implements IEventBus { cancelable: true }); - // Log event + // Log event with grouping if (this.debug) { - console.log(`📢 Event: ${eventType}`, detail); + this.logEventWithGrouping(eventType, detail); } this.eventLog.push({ @@ -70,6 +81,77 @@ export class EventBus implements IEventBus { return !document.dispatchEvent(event); } + /** + * Log event with console grouping + */ + private logEventWithGrouping(eventType: string, detail: any): void { + // Extract category from event type (e.g., 'calendar:datechanged' → 'calendar') + const category = this.extractCategory(eventType); + + // Only log if category is enabled + if (!this.logConfig[category]) { + return; + } + + // Get category emoji and color + const { emoji, color } = this.getCategoryStyle(category); + + // Use collapsed group to reduce visual noise + console.groupCollapsed(`%c${emoji} ${category.toUpperCase()}`, `color: ${color}; font-weight: bold`); + console.log(`Event: ${eventType}`, detail); + console.groupEnd(); + } + + /** + * Extract category from event type + */ + private extractCategory(eventType: string): string { + if (eventType.includes(':')) { + return eventType.split(':')[0]; + } + + // Fallback: try to detect category from event name patterns + const lowerType = eventType.toLowerCase(); + if (lowerType.includes('grid') || lowerType.includes('rendered')) return 'grid'; + if (lowerType.includes('event') || lowerType.includes('sync')) return 'event'; + if (lowerType.includes('scroll')) return 'scroll'; + if (lowerType.includes('nav') || lowerType.includes('date')) return 'navigation'; + if (lowerType.includes('view')) return 'view'; + + return 'default'; + } + + /** + * Get styling for different categories + */ + private getCategoryStyle(category: string): { emoji: string; color: string } { + const styles: { [key: string]: { emoji: string; color: string } } = { + calendar: { emoji: '🗓️', color: '#2196F3' }, + grid: { emoji: '📊', color: '#4CAF50' }, + event: { emoji: '📅', color: '#FF9800' }, + scroll: { emoji: '📜', color: '#9C27B0' }, + navigation: { emoji: '🧭', color: '#F44336' }, + view: { emoji: '👁️', color: '#00BCD4' }, + default: { emoji: '📢', color: '#607D8B' } + }; + + return styles[category] || styles.default; + } + + /** + * Configure logging for specific categories + */ + setLogConfig(config: { [key: string]: boolean }): void { + this.logConfig = { ...this.logConfig, ...config }; + } + + /** + * Get current log configuration + */ + getLogConfig(): { [key: string]: boolean } { + return { ...this.logConfig }; + } + /** * Get event history */ diff --git a/src/index.ts b/src/index.ts index d1caca2..ef04ae4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { ViewManager } from './managers/ViewManager.js'; import { EventManager } from './managers/EventManager.js'; import { EventRenderer } from './managers/EventRenderer.js'; import { GridManager } from './managers/GridManager.js'; +import { ScrollManager } from './managers/ScrollManager.js'; import { CalendarConfig } from './core/CalendarConfig.js'; /** @@ -23,6 +24,7 @@ function initializeCalendar(): void { const viewManager = new ViewManager(eventBus); const eventManager = new EventManager(eventBus); const eventRenderer = new EventRenderer(eventBus); + const scrollManager = new ScrollManager(); // Initialize BEFORE GridManager const gridManager = new GridManager(); // Enable debug mode for development @@ -41,7 +43,8 @@ function initializeCalendar(): void { viewManager, eventManager, eventRenderer, - gridManager + gridManager, + scrollManager }; } diff --git a/src/managers/EventRenderer.ts b/src/managers/EventRenderer.ts index 3c54df9..7fa4a45 100644 --- a/src/managers/EventRenderer.ts +++ b/src/managers/EventRenderer.ts @@ -49,16 +49,10 @@ export class EventRenderer { } private renderEvents(events: CalendarEvent[]): void { - console.log(`EventRenderer: Rendering ${events.length} events`); - // Clear existing events first this.clearEvents(); - // For now, just log events - proper rendering will be implemented later - events.forEach(event => { - console.log(`EventRenderer: Event "${event.title}" from ${event.start} to ${event.end}`); - }); - + // For now, just emit event rendered - proper rendering will be implemented later this.eventBus.emit(EventTypes.EVENT_RENDERED, { count: events.length }); diff --git a/src/managers/GridManager.ts b/src/managers/GridManager.ts index d537706..69f1304 100644 --- a/src/managers/GridManager.ts +++ b/src/managers/GridManager.ts @@ -94,7 +94,9 @@ export class GridManager { this.renderGrid(); // Emit grid rendered event + console.log('GridManager: Emitting GRID_RENDERED event'); eventBus.emit(EventTypes.GRID_RENDERED); + console.log('GridManager: GRID_RENDERED event emitted'); } /** @@ -115,15 +117,48 @@ export class GridManager { // Clear existing grid and rebuild POC structure this.grid.innerHTML = ''; - // Create POC structure: time-axis + week-container + // Create POC structure: header-spacer + time-axis + week-container + right-side + this.createHeaderSpacer(); + this.createRightHeaderSpacer(); this.createTimeAxis(); + this.createRightColumn(); this.createWeekContainer(); console.log('GridManager: Grid rendered successfully with POC structure'); } /** - * Create time axis (left column) like in POC + * Create header spacer to align time axis with week content + */ + private createHeaderSpacer(): void { + if (!this.grid) return; + + const headerSpacer = document.createElement('swp-header-spacer'); + this.grid.appendChild(headerSpacer); + } + + /** + * Create right header spacer to align right column with week content + */ + 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; + + const rightColumn = document.createElement('swp-right-column'); + this.grid.appendChild(rightColumn); + } + + /** + * Create time axis (positioned beside week container) like in POC */ private createTimeAxis(): void { if (!this.grid) return; @@ -132,7 +167,7 @@ export class GridManager { const startHour = calendarConfig.get('dayStartHour'); const endHour = calendarConfig.get('dayEndHour'); - for (let hour = startHour; hour <= endHour; hour++) { + for (let hour = startHour; hour < endHour; hour++) { const marker = document.createElement('swp-hour-marker'); const period = hour >= 12 ? 'PM' : 'AM'; const displayHour = hour > 12 ? hour - 12 : (hour === 0 ? 12 : hour); @@ -221,7 +256,6 @@ export class GridManager { const column = document.createElement('swp-day-column'); (column as any).dataset.date = this.formatDate(date); - console.log(`GridManager: Creating day column ${dayIndex} for date ${this.formatDate(date)}`); // Add dummy content to force column width (temporary test) const dummyContent = document.createElement('div'); diff --git a/src/managers/ScrollManager.ts b/src/managers/ScrollManager.ts new file mode 100644 index 0000000..9600338 --- /dev/null +++ b/src/managers/ScrollManager.ts @@ -0,0 +1,356 @@ +// 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 { + 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; + + 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(); + }); + + // Handle mouse events for dragging + 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(); + } + } + + /** + * 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'); + } + + /** + * 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 { + if (!this.isDragging || !this.scrollHandle || !this.scrollableContent) return; + + 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 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'); + } + } + + /** + * 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; + + 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); + + // Set the height on scrollable content + if (availableHeight > 0) { + this.scrollableContent.style.height = `${availableHeight}px`; + + // Recalculate scroll bounds after height change + setTimeout(() => { + this.calculateScrollBounds(); + this.updateHandlePosition(); + }, 0); + } + } + + /** + * Hide native scrollbar while keeping scroll functionality + */ + 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); + } + + /** + * 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`); + } + } + + /** + * Cleanup resources + */ + destroy(): void { + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + this.resizeObserver = null; + } + } +} \ No newline at end of file diff --git a/wwwroot/css/calendar-layout-css.css b/wwwroot/css/calendar-layout-css.css index 77b4e7a..201b7f9 100644 --- a/wwwroot/css/calendar-layout-css.css +++ b/wwwroot/css/calendar-layout-css.css @@ -1,12 +1,26 @@ /* styles/layout.css - POC Structure Implementation */ -/* Main calendar container */ +/* Calendar wrapper container - full viewport */ +.calendar-wrapper { + width: 100vw; + height: 100vh; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + box-sizing: border-box; + overflow: hidden; +} + +/* Main calendar container - full height */ swp-calendar { display: flex; flex-direction: column; height: 100vh; + width: 100%; background: var(--color-background); position: relative; + overflow: hidden; } /* Navigation bar layout */ @@ -25,16 +39,37 @@ swp-calendar-nav { swp-calendar-container { flex: 1; display: grid; - grid-template-columns: 60px 1fr; - grid-template-rows: 1fr; + grid-template-columns: 60px 1fr 20px; + grid-template-rows: auto 1fr; overflow: hidden; position: relative; } +/* Header spacer for time axis alignment */ +swp-header-spacer { + grid-column: 1; + grid-row: 1; + height: 80px; /* Same as week header height */ + background: var(--color-surface); + border-right: 1px solid var(--color-border); + border-bottom: 1px solid var(--color-border); +} + +/* 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); +} + /* Week container for sliding */ swp-week-container { grid-column: 2; + grid-row: 1 / 3; display: grid; grid-template-rows: auto 1fr; position: relative; @@ -45,13 +80,49 @@ swp-week-container { /* Time axis */ swp-time-axis { grid-column: 1; - grid-row: 1; + grid-row: 2; background: var(--color-surface); border-right: 1px solid var(--color-border); position: sticky; left: 0; z-index: 4; - padding-top: 80px; /* Match header height */ + width: 60px; + display: flex; + flex-direction: column; +} + +/* Right column */ +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; +} + +/* 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; } swp-hour-marker { @@ -134,12 +205,13 @@ swp-scrollable-content { scroll-behavior: smooth; position: relative; display: grid; + /* Height will be set dynamically by ScrollManager via ResizeObserver */ } /* Time grid */ swp-time-grid { position: relative; - height: calc(12 * var(--hour-height)); + height: calc((var(--day-end-hour) - var(--day-start-hour)) * var(--hour-height)); } swp-time-grid::before { diff --git a/wwwroot/index.html b/wwwroot/index.html index 6918d5a..587a50f 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -13,51 +13,53 @@ - - - - - - - Today - +
+ + + + + + + Today + + + + Week 3 + Jan 15 - Jan 21, 2024 + + + + + + + + + + + + + + + Day + Week + Month + + - - Week 3 - Jan 15 - Jan 21, 2024 - + + + + - - - - - - - - - - - - - Day - Week - Month - - - - - - - - - - + + +