2025-10-12 09:21:32 +02:00
|
|
|
/**
|
|
|
|
|
* EdgeScrollManager - Auto-scroll when dragging near edges
|
|
|
|
|
* Uses time-based scrolling with 2-zone system for variable speed
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { IEventBus } from '../types/CalendarTypes';
|
2025-10-12 22:00:02 +02:00
|
|
|
import { DragMoveEventPayload, DragStartEventPayload } from '../types/EventTypes';
|
2025-10-12 09:21:32 +02:00
|
|
|
|
|
|
|
|
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;
|
2025-10-12 22:00:02 +02:00
|
|
|
private draggedClone: HTMLElement | null = null;
|
|
|
|
|
private initialScrollTop = 0;
|
|
|
|
|
private initialCloneTop = 0;
|
|
|
|
|
private scrollListener: ((e: Event) => void) | null = null;
|
2025-10-12 09:21:32 +02:00
|
|
|
|
|
|
|
|
// 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';
|
2025-10-12 22:00:02 +02:00
|
|
|
|
|
|
|
|
// Add scroll listener
|
|
|
|
|
this.scrollListener = this.handleScroll.bind(this);
|
|
|
|
|
this.scrollableContent.addEventListener('scroll', this.scrollListener, { passive: true });
|
2025-10-12 09:21:32 +02:00
|
|
|
}
|
|
|
|
|
}, 100);
|
|
|
|
|
|
|
|
|
|
this.subscribeToEvents();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private subscribeToEvents(): void {
|
2025-10-12 22:00:02 +02:00
|
|
|
|
2025-10-12 09:21:32 +02:00
|
|
|
// Listen to drag events from DragDropManager
|
2025-10-12 22:00:02 +02:00
|
|
|
this.eventBus.on('drag:start', (event: Event) => {
|
|
|
|
|
let customEvent = event as CustomEvent<DragStartEventPayload>;
|
|
|
|
|
this.draggedClone = customEvent.detail.draggedClone;
|
|
|
|
|
this.startDrag();
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-12 09:21:32 +02:00
|
|
|
this.eventBus.on('drag:move', (event: Event) => {
|
2025-10-12 22:00:02 +02:00
|
|
|
let customEvent = event as CustomEvent<DragMoveEventPayload>;
|
|
|
|
|
this.draggedClone = customEvent.detail.draggedClone;
|
2025-10-12 09:21:32 +02:00
|
|
|
this.updateMouseY(customEvent.detail.mousePosition.y);
|
|
|
|
|
});
|
2025-10-12 22:00:02 +02:00
|
|
|
|
2025-10-12 09:21:32 +02:00
|
|
|
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();
|
2025-10-12 22:00:02 +02:00
|
|
|
|
|
|
|
|
// Gem initial scroll position OG clone position
|
|
|
|
|
this.initialScrollTop = this.scrollableContent?.scrollTop || 0;
|
|
|
|
|
this.initialCloneTop = parseFloat(this.draggedClone?.style.top || '0');
|
|
|
|
|
|
|
|
|
|
console.log('💾 EdgeScrollManager: Saved initial state', {
|
|
|
|
|
initialScrollTop: this.initialScrollTop,
|
|
|
|
|
initialCloneTop: this.initialCloneTop
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-12 09:21:32 +02:00
|
|
|
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;
|
2025-10-12 22:00:02 +02:00
|
|
|
this.draggedClone = null;
|
|
|
|
|
this.initialScrollTop = 0;
|
|
|
|
|
this.initialCloneTop = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleScroll(): void {
|
|
|
|
|
if (!this.isDragging || !this.draggedClone || !this.scrollableContent) return;
|
|
|
|
|
|
|
|
|
|
const currentScrollTop = this.scrollableContent.scrollTop;
|
|
|
|
|
const totalScrollDelta = currentScrollTop - this.initialScrollTop;
|
|
|
|
|
|
|
|
|
|
// Beregn ny position baseret på initial position + total scroll delta
|
|
|
|
|
const newTop = this.initialCloneTop + totalScrollDelta;
|
|
|
|
|
this.draggedClone.style.top = `${newTop}px`;
|
|
|
|
|
|
|
|
|
|
console.log('📜 EdgeScrollManager: Scroll event - updated clone', {
|
|
|
|
|
initialScrollTop: this.initialScrollTop,
|
|
|
|
|
currentScrollTop,
|
|
|
|
|
totalScrollDelta,
|
|
|
|
|
initialCloneTop: this.initialCloneTop,
|
|
|
|
|
newTop
|
|
|
|
|
});
|
2025-10-12 09:21:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
vy = -this.FAST_SPEED_PXS;
|
|
|
|
|
} else if (distTop < this.OUTER_ZONE) {
|
|
|
|
|
vy = -this.SLOW_SPEED_PXS;
|
|
|
|
|
}
|
|
|
|
|
// Check bottom edge
|
|
|
|
|
else if (distBot < this.INNER_ZONE) {
|
|
|
|
|
vy = this.FAST_SPEED_PXS;
|
|
|
|
|
} else if (distBot < this.OUTER_ZONE) {
|
|
|
|
|
vy = this.SLOW_SPEED_PXS;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|