From 82921e0643afad1e7952fe7a13f2d41f12f0443c Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Mon, 13 Oct 2025 23:05:03 +0200 Subject: [PATCH] Extracts drag hover logic into dedicated manager Moves event hover handling from DragDropManager to a new DragHoverManager. This improves separation of concerns and makes the hover logic more modular and reusable. The DragHoverManager is now responsible for tracking when the mouse hovers over events, and it emits events for other parts of the application to react to. The drag:start event is used to deactivate hover tracking when a drag operation starts. --- src/factories/ManagerFactory.ts | 5 +- src/managers/DragDropManager.ts | 100 ++++---------------------- src/managers/DragHoverManager.ts | 116 +++++++++++++++++++++++++++++++ src/types/ManagerTypes.ts | 1 + 4 files changed, 134 insertions(+), 88 deletions(-) create mode 100644 src/managers/DragHoverManager.ts diff --git a/src/factories/ManagerFactory.ts b/src/factories/ManagerFactory.ts index 8e08158..34fb15c 100644 --- a/src/factories/ManagerFactory.ts +++ b/src/factories/ManagerFactory.ts @@ -10,6 +10,7 @@ import { DragDropManager } from '../managers/DragDropManager'; import { AllDayManager } from '../managers/AllDayManager'; import { ResizeHandleManager } from '../managers/ResizeHandleManager'; import { EdgeScrollManager } from '../managers/EdgeScrollManager'; +import { DragHoverManager } from '../managers/DragHoverManager'; import { CalendarManagers } from '../types/ManagerTypes'; /** @@ -43,6 +44,7 @@ export class ManagerFactory { const allDayManager = new AllDayManager(eventManager); const resizeHandleManager = new ResizeHandleManager(); const edgeScrollManager = new EdgeScrollManager(eventBus); + const dragHoverManager = new DragHoverManager(eventBus); // CalendarManager depends on all other managers const calendarManager = new CalendarManager( @@ -65,7 +67,8 @@ export class ManagerFactory { dragDropManager, allDayManager, resizeHandleManager, - edgeScrollManager + edgeScrollManager, + dragHoverManager }; } diff --git a/src/managers/DragDropManager.ts b/src/managers/DragDropManager.ts index 2a53d34..138ad59 100644 --- a/src/managers/DragDropManager.ts +++ b/src/managers/DragDropManager.ts @@ -18,7 +18,7 @@ import { DragColumnChangeEventPayload } from '../types/EventTypes'; import { MousePosition } from '../types/DragDropTypes'; -import { CoreEvents } from '../constants/CoreEvents'; +import { CoreEvents } from '../constants/CoreEvents'; export class DragDropManager { private eventBus: IEventBus; @@ -35,10 +35,6 @@ export class DragDropManager { private previousColumn: ColumnBounds | null = null; private isDragStarted = false; - // Hover state - private isHoverTrackingActive = false; - private currentHoveredEvent: HTMLElement | null = null; - // Movement threshold to distinguish click from drag private readonly dragThreshold = 5; // pixels @@ -47,8 +43,6 @@ export class DragDropManager { private scrollDeltaY = 0; // Current scroll delta to apply in continueDrag private lastScrollTop = 0; // Last scroll position for delta calculation private isScrollCompensating = false; // Track if scroll compensation is active - private hasScrolledDuringDrag = false; // Track if we have scrolled during this drag operation - private scrollListener: ((e: Event) => void) | null = null; // Smooth drag animation private dragAnimationId: number | null = null; @@ -58,10 +52,6 @@ export class DragDropManager { constructor(eventBus: IEventBus) { this.eventBus = eventBus; - // Get config values - const gridSettings = calendarConfig.getGridSettings(); - - this.init(); } @@ -91,8 +81,6 @@ export class DragDropManager { this.handleHeaderMouseEnter(e as MouseEvent); } else if (target.closest('swp-day-column')) { this.handleColumnMouseEnter(e as MouseEvent); - } else if (target.closest('swp-event')) { - this.handleEventMouseEnter(e as MouseEvent); } }, true); // Use capture phase @@ -128,13 +116,12 @@ export class DragDropManager { // Listen to edge-scroll events to control scroll compensation this.eventBus.on('edgescroll:started', () => { this.isScrollCompensating = true; - this.hasScrolledDuringDrag = true; - + // Gem nuværende scroll position for delta beregning if (this.scrollableContent) { this.lastScrollTop = this.scrollableContent.scrollTop; } - + console.log('🎬 DragDropManager: Edge-scroll started'); }); @@ -199,18 +186,7 @@ export class DragDropManager { } } - /** - * Optimized mouse move handler with consolidated position calculations - */ private handleMouseMove(event: MouseEvent): void { - - //this.currentMouseY = event.clientY; - // this.lastMousePosition = { x: event.clientX, y: event.clientY }; - - // Check for event hover (coordinate-based) - only when mouse button is up - if (this.isHoverTrackingActive && event.buttons === 0) { - this.checkEventHover(event); - } if (event.buttons === 1) { // Always update mouse position from event @@ -283,11 +259,11 @@ export class DragDropManager { if (column) { // Calculate raw Y position relative to column (accounting for mouse offset) const columnRect = column.boundingClientRect; - + // Beregn position fra mus + scroll delta kompensation const adjustedMouseY = currentPosition.y + this.scrollDeltaY; const eventTopY = adjustedMouseY - columnRect.top - this.mouseOffset.y; - + this.targetY = Math.max(0, eventTopY); this.targetColumn = column; @@ -400,32 +376,32 @@ export class DragDropManager { // Get current clone position const cloneRect = this.draggedClone.getBoundingClientRect(); - + // Get original element position const originalRect = this.originalElement.getBoundingClientRect(); - + // Calculate distance to animate const deltaX = originalRect.left - cloneRect.left; const deltaY = originalRect.top - cloneRect.top; - + // Add transition for smooth animation this.draggedClone.style.transition = 'transform 300ms ease-out'; this.draggedClone.style.transform = `translate(${deltaX}px, ${deltaY}px)`; - + // Wait for animation to complete, then cleanup setTimeout(() => { this.cleanupAllClones(); - + if (this.originalElement) { this.originalElement.style.opacity = ''; this.originalElement.style.cursor = ''; } - + this.eventBus.emit('drag:cancelled', { originalElement: this.originalElement, reason: 'mouse-left-grid' }); - + this.cleanupDragState(); this.stopDragAnimation(); }, 300); @@ -502,7 +478,7 @@ export class DragDropManager { const currentScrollTop = this.scrollableContent.scrollTop; const scrollDelta = currentScrollTop - this.lastScrollTop; - + // Gem scroll delta for continueDrag this.scrollDeltaY += scrollDelta; this.lastScrollTop = currentScrollTop; @@ -537,7 +513,6 @@ export class DragDropManager { this.draggedClone = null; this.currentColumn = null; this.isDragStarted = false; - this.hasScrolledDuringDrag = false; this.scrollDeltaY = 0; this.lastScrollTop = 0; } @@ -562,26 +537,6 @@ export class DragDropManager { return null; } - /** - * Handle mouse enter on swp-event - activate hover tracking - */ - private handleEventMouseEnter(event: MouseEvent): void { - const target = event.target as HTMLElement; - const eventElement = target.closest('swp-event'); - - // Only handle hover if mouse button is up - if (eventElement && !this.isDragStarted && event.buttons === 0) { - // Clear any previous hover first - if (this.currentHoveredEvent && this.currentHoveredEvent !== eventElement) { - this.currentHoveredEvent.classList.remove('hover'); - } - - this.isHoverTrackingActive = true; - this.currentHoveredEvent = eventElement; - eventElement.classList.add('hover'); - } - } - /** * Handle mouse enter on calendar header - simplified using native events */ @@ -681,33 +636,4 @@ export class DragDropManager { }; this.eventBus.emit('drag:mouseleave-header', dragMouseLeavePayload); } - - private checkEventHover(event: MouseEvent): void { - // Use currentHoveredEvent to check if mouse is still within bounds - if (!this.currentHoveredEvent) return; - - const rect = this.currentHoveredEvent.getBoundingClientRect(); - const mouseX = event.clientX; - const mouseY = event.clientY; - - // Check if mouse is still within the current hovered event - const isStillInside = mouseX >= rect.left && mouseX <= rect.right && - mouseY >= rect.top && mouseY <= rect.bottom; - - // If mouse left the event - if (!isStillInside) { - // Only disable tracking and clear if mouse is NOT pressed (allow resize to work) - if (event.buttons === 0) { - this.isHoverTrackingActive = false; - this.clearEventHover(); - } - } - } - - private clearEventHover(): void { - if (this.currentHoveredEvent) { - this.currentHoveredEvent.classList.remove('hover'); - this.currentHoveredEvent = null; - } - } } diff --git a/src/managers/DragHoverManager.ts b/src/managers/DragHoverManager.ts new file mode 100644 index 0000000..65263f5 --- /dev/null +++ b/src/managers/DragHoverManager.ts @@ -0,0 +1,116 @@ +/** + * DragHoverManager - Handles event hover tracking + * Fully autonomous - listens to mouse events and manages hover state independently + */ + +import { IEventBus } from '../types/CalendarTypes'; + +export class DragHoverManager { + private isHoverTrackingActive = false; + private currentHoveredEvent: HTMLElement | null = null; + private calendarContainer: HTMLElement | null = null; + + constructor(private eventBus: IEventBus) { + this.init(); + } + + private init(): void { + // Wait for DOM to be ready + setTimeout(() => { + this.calendarContainer = document.querySelector('swp-calendar-container'); + if (this.calendarContainer) { + this.setupEventListeners(); + } + }, 100); + + // Listen to drag start to deactivate hover tracking + this.eventBus.on('drag:start', () => { + this.deactivateTracking(); + }); + } + + private setupEventListeners(): void { + if (!this.calendarContainer) return; + + // Listen to mouseenter on events (using event delegation) + this.calendarContainer.addEventListener('mouseenter', (e) => { + const target = e.target as HTMLElement; + const eventElement = target.closest('swp-event'); + + if (eventElement) { + this.handleEventMouseEnter(e as MouseEvent, eventElement); + } + }, true); // Use capture phase + + // Listen to mousemove globally to track when mouse leaves event bounds + document.body.addEventListener('mousemove', (e: MouseEvent) => { + if (this.isHoverTrackingActive && e.buttons === 0) { + this.checkEventHover(e); + } + }); + } + + /** + * Handle mouse enter on swp-event - activate hover tracking + */ + private handleEventMouseEnter(event: MouseEvent, eventElement: HTMLElement): void { + // Only handle hover if mouse button is up + if (event.buttons === 0) { + // Clear any previous hover first + if (this.currentHoveredEvent && this.currentHoveredEvent !== eventElement) { + this.currentHoveredEvent.classList.remove('hover'); + } + + this.isHoverTrackingActive = true; + this.currentHoveredEvent = eventElement; + eventElement.classList.add('hover'); + + this.eventBus.emit('event:hover:start', { element: eventElement }); + } + } + + /** + * Check if mouse is still over the currently hovered event + */ + private checkEventHover(event: MouseEvent): void { + // Only track hover when active and mouse button is up + if (!this.isHoverTrackingActive || !this.currentHoveredEvent) return; + + const rect = this.currentHoveredEvent.getBoundingClientRect(); + const mouseX = event.clientX; + const mouseY = event.clientY; + + // Check if mouse is still within the current hovered event + const isStillInside = mouseX >= rect.left && mouseX <= rect.right && + mouseY >= rect.top && mouseY <= rect.bottom; + + // If mouse left the event + if (!isStillInside) { + // Only disable tracking and clear if mouse is NOT pressed (allow resize to work) + if (event.buttons === 0) { + this.isHoverTrackingActive = false; + this.clearEventHover(); + } + } + } + + /** + * Clear hover state + */ + private clearEventHover(): void { + if (this.currentHoveredEvent) { + this.currentHoveredEvent.classList.remove('hover'); + this.eventBus.emit('event:hover:end', { element: this.currentHoveredEvent }); + this.currentHoveredEvent = null; + } + } + + /** + * Deactivate hover tracking and clear any current hover + * Called via event bus when drag starts + */ + private deactivateTracking(): void { + this.isHoverTrackingActive = false; + this.clearEventHover(); + } +} diff --git a/src/types/ManagerTypes.ts b/src/types/ManagerTypes.ts index e35768a..e58bf9f 100644 --- a/src/types/ManagerTypes.ts +++ b/src/types/ManagerTypes.ts @@ -15,6 +15,7 @@ export interface CalendarManagers { allDayManager: unknown; // Avoid interface conflicts resizeHandleManager: ResizeHandleManager; edgeScrollManager: unknown; // Avoid interface conflicts + dragHoverManager: unknown; // Avoid interface conflicts } /**