From 8df1f6c4f1509e850b30e5b10668eb8d5b262b73 Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Sun, 12 Oct 2025 09:21:32 +0200 Subject: [PATCH] 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. --- src/factories/ManagerFactory.ts | 5 +- src/managers/EdgeScrollManager.ts | 134 ++++++++++++++++++++++++++ src/renderers/EventRenderer.ts | 13 --- src/renderers/EventRendererManager.ts | 11 --- src/types/ManagerTypes.ts | 1 + 5 files changed, 139 insertions(+), 25 deletions(-) create mode 100644 src/managers/EdgeScrollManager.ts diff --git a/src/factories/ManagerFactory.ts b/src/factories/ManagerFactory.ts index b526967..8e08158 100644 --- a/src/factories/ManagerFactory.ts +++ b/src/factories/ManagerFactory.ts @@ -9,6 +9,7 @@ import { CalendarManager } from '../managers/CalendarManager'; import { DragDropManager } from '../managers/DragDropManager'; import { AllDayManager } from '../managers/AllDayManager'; import { ResizeHandleManager } from '../managers/ResizeHandleManager'; +import { EdgeScrollManager } from '../managers/EdgeScrollManager'; import { CalendarManagers } from '../types/ManagerTypes'; /** @@ -41,6 +42,7 @@ export class ManagerFactory { const dragDropManager = new DragDropManager(eventBus); const allDayManager = new AllDayManager(eventManager); const resizeHandleManager = new ResizeHandleManager(); + const edgeScrollManager = new EdgeScrollManager(eventBus); // CalendarManager depends on all other managers const calendarManager = new CalendarManager( @@ -62,7 +64,8 @@ export class ManagerFactory { calendarManager, dragDropManager, allDayManager, - resizeHandleManager + resizeHandleManager, + edgeScrollManager }; } diff --git a/src/managers/EdgeScrollManager.ts b/src/managers/EdgeScrollManager.ts new file mode 100644 index 0000000..bf82c12 --- /dev/null +++ b/src/managers/EdgeScrollManager.ts @@ -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; + 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(); + } + } + } +} \ No newline at end of file diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index f66f7d8..42596a4 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -97,19 +97,6 @@ export class DateEventRenderer implements EventRendererStrategy { swpEvent.updatePosition(columnDate, payload.snappedY); } - /** - * Handle drag auto-scroll event - */ - public handleDragAutoScroll(eventId: string, snappedY: number): void { - if (!this.draggedClone) return; - - // Update position directly using the calculated snapped position - this.draggedClone.style.top = snappedY + 'px'; - - // Update timestamp display - //this.updateCloneTimestamp(this.draggedClone, snappedY); //TODO: Commented as, we need to move all this scroll logic til scroll manager away from eventrenderer - } - /** * Handle column change during drag */ diff --git a/src/renderers/EventRendererManager.ts b/src/renderers/EventRendererManager.ts index 7fc41d7..a37cfa1 100644 --- a/src/renderers/EventRendererManager.ts +++ b/src/renderers/EventRendererManager.ts @@ -126,7 +126,6 @@ export class EventRenderingService { private setupDragEventListeners(): void { this.setupDragStartListener(); this.setupDragMoveListener(); - this.setupDragAutoScrollListener(); this.setupDragEndListener(); this.setupDragColumnChangeListener(); this.setupDragMouseLeaveHeaderListener(); @@ -162,16 +161,6 @@ export class EventRenderingService { }); } - private setupDragAutoScrollListener(): void { - this.eventBus.on('drag:auto-scroll', (event: Event) => { - const { draggedElement, snappedY } = (event as CustomEvent).detail; - if (this.strategy.handleDragAutoScroll) { - const eventId = draggedElement.dataset.eventId || ''; - this.strategy.handleDragAutoScroll(eventId, snappedY); - } - }); - } - private setupDragEndListener(): void { this.eventBus.on('drag:end', (event: Event) => { const { originalElement: draggedElement, sourceColumn, finalPosition, target } = (event as CustomEvent).detail; diff --git a/src/types/ManagerTypes.ts b/src/types/ManagerTypes.ts index ffb4958..e35768a 100644 --- a/src/types/ManagerTypes.ts +++ b/src/types/ManagerTypes.ts @@ -14,6 +14,7 @@ export interface CalendarManagers { dragDropManager: unknown; // Avoid interface conflicts allDayManager: unknown; // Avoid interface conflicts resizeHandleManager: ResizeHandleManager; + edgeScrollManager: unknown; // Avoid interface conflicts } /**