/** * EdgeScrollManager - Auto-scroll when dragging near edges * Uses time-based scrolling with 2-zone system for variable speed */ import { IEventBus } from '../types/CalendarTypes'; import { DragMoveEventPayload } from '../types/EventTypes'; export class EdgeScrollManager { private scrollableContent: HTMLElement | null = null; private scrollRAF: number | null = null; private mouseY = 0; private isDragging = false; private lastTs = 0; private rect: DOMRect | null = null; // Constants - fixed values as per requirements private readonly OUTER_ZONE = 100; // px from edge (slow zone) private readonly INNER_ZONE = 50; // px from edge (fast zone) private readonly SLOW_SPEED_PXS = 800; // px/sec in outer zone private readonly FAST_SPEED_PXS = 2400; // px/sec in inner zone constructor(private eventBus: IEventBus) { this.init(); } private init(): void { // Wait for DOM to be ready setTimeout(() => { this.scrollableContent = document.querySelector('swp-scrollable-content'); if (this.scrollableContent) { // Disable smooth scroll for instant auto-scroll this.scrollableContent.style.scrollBehavior = 'auto'; } }, 100); this.subscribeToEvents(); } private subscribeToEvents(): void { // Listen to drag events from DragDropManager this.eventBus.on('drag:start', () => this.startDrag()); this.eventBus.on('drag:move', (event: Event) => { const customEvent = event as CustomEvent; this.updateMouseY(customEvent.detail.mousePosition.y); }); this.eventBus.on('drag:end', () => this.stopDrag()); this.eventBus.on('drag:cancelled', () => this.stopDrag()); } private startDrag(): void { console.log('🎬 EdgeScrollManager: Starting drag'); this.isDragging = true; this.lastTs = performance.now(); if (this.scrollRAF === null) { this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts)); } } private updateMouseY(y: number): void { this.mouseY = y; // Ensure RAF loop is running during drag if (this.isDragging && this.scrollRAF === null) { this.lastTs = performance.now(); this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts)); } } private stopDrag(): void { this.isDragging = false; if (this.scrollRAF !== null) { cancelAnimationFrame(this.scrollRAF); this.scrollRAF = null; } this.rect = null; this.lastTs = 0; } private scrollTick(ts: number): void { const dt = this.lastTs ? (ts - this.lastTs) / 1000 : 0; this.lastTs = ts; if (!this.scrollableContent) { this.stopDrag(); return; } // Cache rect for performance (only measure once per frame) if (!this.rect) { this.rect = this.scrollableContent.getBoundingClientRect(); } let vy = 0; if (this.isDragging) { const distTop = this.mouseY - this.rect.top; const distBot = this.rect.bottom - this.mouseY; // Check top edge if (distTop < this.INNER_ZONE) { // Inner zone (0-50px) - fast speed vy = -this.FAST_SPEED_PXS; console.log('⬆️ EdgeScrollManager: Top FAST', { distTop, vy }); } else if (distTop < this.OUTER_ZONE) { // Outer zone (50-100px) - slow speed vy = -this.SLOW_SPEED_PXS; console.log('⬆️ EdgeScrollManager: Top SLOW', { distTop, vy }); } // Check bottom edge else if (distBot < this.INNER_ZONE) { // Inner zone (0-50px) - fast speed vy = this.FAST_SPEED_PXS; console.log('⬇️ EdgeScrollManager: Bottom FAST', { distBot, vy }); } else if (distBot < this.OUTER_ZONE) { // Outer zone (50-100px) - slow speed vy = this.SLOW_SPEED_PXS; console.log('⬇️ EdgeScrollManager: Bottom SLOW', { distBot, vy }); } } if (vy !== 0 && this.isDragging) { // Time-based scrolling for frame-rate independence this.scrollableContent.scrollTop += vy * dt; this.rect = null; // Invalidate cache for next frame this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts)); } else { // Continue RAF loop even if not scrolling, to detect edge entry if (this.isDragging) { this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts)); } else { this.stopDrag(); } } } }