Implements edge scrolling functionality

Adds edge scrolling to automatically scroll the calendar
when dragging an event near the edges of the view.

This improves the drag-and-drop experience by allowing users
to move events beyond the visible area.

Removes auto-scroll logic from the event renderer, centralizing
the scrolling behavior within the new edge scroll manager.
This commit is contained in:
Janus C. H. Knudsen 2025-10-12 09:21:32 +02:00
parent 40b19a092c
commit 8df1f6c4f1
5 changed files with 139 additions and 25 deletions

View file

@ -0,0 +1,134 @@
/**
* 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<DragMoveEventPayload>;
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();
}
}
}
}