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.
This commit is contained in:
Janus C. H. Knudsen 2025-10-13 23:05:03 +02:00
parent 78ad5d3bc0
commit 82921e0643
4 changed files with 134 additions and 88 deletions

View file

@ -10,6 +10,7 @@ import { DragDropManager } from '../managers/DragDropManager';
import { AllDayManager } from '../managers/AllDayManager'; import { AllDayManager } from '../managers/AllDayManager';
import { ResizeHandleManager } from '../managers/ResizeHandleManager'; import { ResizeHandleManager } from '../managers/ResizeHandleManager';
import { EdgeScrollManager } from '../managers/EdgeScrollManager'; import { EdgeScrollManager } from '../managers/EdgeScrollManager';
import { DragHoverManager } from '../managers/DragHoverManager';
import { CalendarManagers } from '../types/ManagerTypes'; import { CalendarManagers } from '../types/ManagerTypes';
/** /**
@ -43,6 +44,7 @@ export class ManagerFactory {
const allDayManager = new AllDayManager(eventManager); const allDayManager = new AllDayManager(eventManager);
const resizeHandleManager = new ResizeHandleManager(); const resizeHandleManager = new ResizeHandleManager();
const edgeScrollManager = new EdgeScrollManager(eventBus); const edgeScrollManager = new EdgeScrollManager(eventBus);
const dragHoverManager = new DragHoverManager(eventBus);
// CalendarManager depends on all other managers // CalendarManager depends on all other managers
const calendarManager = new CalendarManager( const calendarManager = new CalendarManager(
@ -65,7 +67,8 @@ export class ManagerFactory {
dragDropManager, dragDropManager,
allDayManager, allDayManager,
resizeHandleManager, resizeHandleManager,
edgeScrollManager edgeScrollManager,
dragHoverManager
}; };
} }

View file

@ -18,7 +18,7 @@ import {
DragColumnChangeEventPayload DragColumnChangeEventPayload
} from '../types/EventTypes'; } from '../types/EventTypes';
import { MousePosition } from '../types/DragDropTypes'; import { MousePosition } from '../types/DragDropTypes';
import { CoreEvents } from '../constants/CoreEvents'; import { CoreEvents } from '../constants/CoreEvents';
export class DragDropManager { export class DragDropManager {
private eventBus: IEventBus; private eventBus: IEventBus;
@ -35,10 +35,6 @@ export class DragDropManager {
private previousColumn: ColumnBounds | null = null; private previousColumn: ColumnBounds | null = null;
private isDragStarted = false; private isDragStarted = false;
// Hover state
private isHoverTrackingActive = false;
private currentHoveredEvent: HTMLElement | null = null;
// Movement threshold to distinguish click from drag // Movement threshold to distinguish click from drag
private readonly dragThreshold = 5; // pixels private readonly dragThreshold = 5; // pixels
@ -47,8 +43,6 @@ export class DragDropManager {
private scrollDeltaY = 0; // Current scroll delta to apply in continueDrag private scrollDeltaY = 0; // Current scroll delta to apply in continueDrag
private lastScrollTop = 0; // Last scroll position for delta calculation private lastScrollTop = 0; // Last scroll position for delta calculation
private isScrollCompensating = false; // Track if scroll compensation is active 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 // Smooth drag animation
private dragAnimationId: number | null = null; private dragAnimationId: number | null = null;
@ -58,10 +52,6 @@ export class DragDropManager {
constructor(eventBus: IEventBus) { constructor(eventBus: IEventBus) {
this.eventBus = eventBus; this.eventBus = eventBus;
// Get config values
const gridSettings = calendarConfig.getGridSettings();
this.init(); this.init();
} }
@ -91,8 +81,6 @@ export class DragDropManager {
this.handleHeaderMouseEnter(e as MouseEvent); this.handleHeaderMouseEnter(e as MouseEvent);
} else if (target.closest('swp-day-column')) { } else if (target.closest('swp-day-column')) {
this.handleColumnMouseEnter(e as MouseEvent); this.handleColumnMouseEnter(e as MouseEvent);
} else if (target.closest('swp-event')) {
this.handleEventMouseEnter(e as MouseEvent);
} }
}, true); // Use capture phase }, true); // Use capture phase
@ -128,13 +116,12 @@ export class DragDropManager {
// Listen to edge-scroll events to control scroll compensation // Listen to edge-scroll events to control scroll compensation
this.eventBus.on('edgescroll:started', () => { this.eventBus.on('edgescroll:started', () => {
this.isScrollCompensating = true; this.isScrollCompensating = true;
this.hasScrolledDuringDrag = true;
// Gem nuværende scroll position for delta beregning // Gem nuværende scroll position for delta beregning
if (this.scrollableContent) { if (this.scrollableContent) {
this.lastScrollTop = this.scrollableContent.scrollTop; this.lastScrollTop = this.scrollableContent.scrollTop;
} }
console.log('🎬 DragDropManager: Edge-scroll started'); 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 { 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) { if (event.buttons === 1) {
// Always update mouse position from event // Always update mouse position from event
@ -283,11 +259,11 @@ export class DragDropManager {
if (column) { if (column) {
// Calculate raw Y position relative to column (accounting for mouse offset) // Calculate raw Y position relative to column (accounting for mouse offset)
const columnRect = column.boundingClientRect; const columnRect = column.boundingClientRect;
// Beregn position fra mus + scroll delta kompensation // Beregn position fra mus + scroll delta kompensation
const adjustedMouseY = currentPosition.y + this.scrollDeltaY; const adjustedMouseY = currentPosition.y + this.scrollDeltaY;
const eventTopY = adjustedMouseY - columnRect.top - this.mouseOffset.y; const eventTopY = adjustedMouseY - columnRect.top - this.mouseOffset.y;
this.targetY = Math.max(0, eventTopY); this.targetY = Math.max(0, eventTopY);
this.targetColumn = column; this.targetColumn = column;
@ -400,32 +376,32 @@ export class DragDropManager {
// Get current clone position // Get current clone position
const cloneRect = this.draggedClone.getBoundingClientRect(); const cloneRect = this.draggedClone.getBoundingClientRect();
// Get original element position // Get original element position
const originalRect = this.originalElement.getBoundingClientRect(); const originalRect = this.originalElement.getBoundingClientRect();
// Calculate distance to animate // Calculate distance to animate
const deltaX = originalRect.left - cloneRect.left; const deltaX = originalRect.left - cloneRect.left;
const deltaY = originalRect.top - cloneRect.top; const deltaY = originalRect.top - cloneRect.top;
// Add transition for smooth animation // Add transition for smooth animation
this.draggedClone.style.transition = 'transform 300ms ease-out'; this.draggedClone.style.transition = 'transform 300ms ease-out';
this.draggedClone.style.transform = `translate(${deltaX}px, ${deltaY}px)`; this.draggedClone.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
// Wait for animation to complete, then cleanup // Wait for animation to complete, then cleanup
setTimeout(() => { setTimeout(() => {
this.cleanupAllClones(); this.cleanupAllClones();
if (this.originalElement) { if (this.originalElement) {
this.originalElement.style.opacity = ''; this.originalElement.style.opacity = '';
this.originalElement.style.cursor = ''; this.originalElement.style.cursor = '';
} }
this.eventBus.emit('drag:cancelled', { this.eventBus.emit('drag:cancelled', {
originalElement: this.originalElement, originalElement: this.originalElement,
reason: 'mouse-left-grid' reason: 'mouse-left-grid'
}); });
this.cleanupDragState(); this.cleanupDragState();
this.stopDragAnimation(); this.stopDragAnimation();
}, 300); }, 300);
@ -502,7 +478,7 @@ export class DragDropManager {
const currentScrollTop = this.scrollableContent.scrollTop; const currentScrollTop = this.scrollableContent.scrollTop;
const scrollDelta = currentScrollTop - this.lastScrollTop; const scrollDelta = currentScrollTop - this.lastScrollTop;
// Gem scroll delta for continueDrag // Gem scroll delta for continueDrag
this.scrollDeltaY += scrollDelta; this.scrollDeltaY += scrollDelta;
this.lastScrollTop = currentScrollTop; this.lastScrollTop = currentScrollTop;
@ -537,7 +513,6 @@ export class DragDropManager {
this.draggedClone = null; this.draggedClone = null;
this.currentColumn = null; this.currentColumn = null;
this.isDragStarted = false; this.isDragStarted = false;
this.hasScrolledDuringDrag = false;
this.scrollDeltaY = 0; this.scrollDeltaY = 0;
this.lastScrollTop = 0; this.lastScrollTop = 0;
} }
@ -562,26 +537,6 @@ export class DragDropManager {
return null; 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<HTMLElement>('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 * Handle mouse enter on calendar header - simplified using native events
*/ */
@ -681,33 +636,4 @@ export class DragDropManager {
}; };
this.eventBus.emit('drag:mouseleave-header', dragMouseLeavePayload); 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;
}
}
} }

View file

@ -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<HTMLElement>('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();
}
}

View file

@ -15,6 +15,7 @@ export interface CalendarManagers {
allDayManager: unknown; // Avoid interface conflicts allDayManager: unknown; // Avoid interface conflicts
resizeHandleManager: ResizeHandleManager; resizeHandleManager: ResizeHandleManager;
edgeScrollManager: unknown; // Avoid interface conflicts edgeScrollManager: unknown; // Avoid interface conflicts
dragHoverManager: unknown; // Avoid interface conflicts
} }
/** /**