diff --git a/src/managers/DragDropManager.ts b/src/managers/DragDropManager.ts index f0ced1d..fbbe9b4 100644 --- a/src/managers/DragDropManager.ts +++ b/src/managers/DragDropManager.ts @@ -1,37 +1,62 @@ /** - * DragDropManager - Handles drag and drop interaction logic - * Emits events for visual updates handled by EventRenderer + * DragDropManager - Optimized drag and drop with consolidated position calculations + * Reduces redundant DOM queries and improves performance through caching */ import { IEventBus } from '../types/CalendarTypes'; import { CalendarConfig } from '../core/CalendarConfig'; +import { DateCalculator } from '../utils/DateCalculator'; + +interface CachedElements { + scrollContainer: HTMLElement | null; + currentColumn: HTMLElement | null; + lastColumnDate: string | null; +} + +interface Position { + x: number; + y: number; +} export class DragDropManager { private eventBus: IEventBus; private config: CalendarConfig; - // Mouse tracking + // Mouse tracking with optimized state private isMouseDown = false; - private lastMousePosition = { x: 0, y: 0 }; - private lastLoggedPosition = { x: 0, y: 0 }; + private lastMousePosition: Position = { x: 0, y: 0 }; + private lastLoggedPosition: Position = { x: 0, y: 0 }; private currentMouseY = 0; - private mouseOffset = { x: 0, y: 0 }; + private mouseOffset: Position = { x: 0, y: 0 }; // Drag state private draggedEventId: string | null = null; private originalElement: HTMLElement | null = null; private currentColumn: string | null = null; + // Cached DOM elements for performance + private cachedElements: CachedElements = { + scrollContainer: null, + currentColumn: null, + lastColumnDate: null + }; + // Auto-scroll properties - private scrollContainer: HTMLElement | null = null; private autoScrollAnimationId: number | null = null; - private scrollSpeed = 10; // pixels per frame - private scrollThreshold = 30; // pixels from edge + private readonly scrollSpeed = 10; // pixels per frame + private readonly scrollThreshold = 30; // pixels from edge // Snap configuration private snapIntervalMinutes = 15; // Default 15 minutes private hourHeightPx = 60; // From CSS --hour-height + // Event listener references for proper cleanup + private boundHandlers = { + mouseMove: this.handleMouseMove.bind(this), + mouseDown: this.handleMouseDown.bind(this), + mouseUp: this.handleMouseUp.bind(this) + }; + private get snapDistancePx(): number { return (this.snapIntervalMinutes / 60) * this.hourHeightPx; } @@ -54,11 +79,14 @@ export class DragDropManager { this.snapIntervalMinutes = minutes; } + /** + * Initialize with optimized event listener setup + */ private init(): void { - // Listen to mouse events on body - document.body.addEventListener('mousemove', this.handleMouseMove.bind(this)); - document.body.addEventListener('mousedown', this.handleMouseDown.bind(this)); - document.body.addEventListener('mouseup', this.handleMouseUp.bind(this)); + // Use bound handlers for proper cleanup + document.body.addEventListener('mousemove', this.boundHandlers.mouseMove); + document.body.addEventListener('mousedown', this.boundHandlers.mouseDown); + document.body.addEventListener('mouseup', this.boundHandlers.mouseUp); // Listen for header mouseover events this.eventBus.on('header:mouseover', (event) => { @@ -128,103 +156,112 @@ export class DragDropManager { } } + /** + * Optimized mouse move handler with consolidated position calculations + */ private handleMouseMove(event: MouseEvent): void { this.currentMouseY = event.clientY; if (this.isMouseDown && this.draggedEventId) { - const deltaY = Math.abs(event.clientY - this.lastLoggedPosition.y); + const currentPosition: Position = { x: event.clientX, y: event.clientY }; + const deltaY = Math.abs(currentPosition.y - this.lastLoggedPosition.y); // Check for snap interval vertical movement if (deltaY >= this.snapDistancePx) { - this.lastLoggedPosition = { x: event.clientX, y: event.clientY }; + this.lastLoggedPosition = currentPosition; - // Calculate snapped position - const column = this.detectColumn(event.clientX, event.clientY); - const snappedY = this.calculateSnapPosition(event.clientY); + // Consolidated position calculations + const positionData = this.calculateDragPosition(currentPosition); - // Emit drag move event with snapped position + // Emit drag move event with consolidated data this.eventBus.emit('drag:move', { eventId: this.draggedEventId, - mousePosition: { x: event.clientX, y: event.clientY }, - snappedY, - column, + mousePosition: currentPosition, + snappedY: positionData.snappedY, + column: positionData.column, mouseOffset: this.mouseOffset }); - } // Check for auto-scroll this.checkAutoScroll(event); - // Check for column change - const newColumn = this.detectColumn(event.clientX, event.clientY); + // Check for column change using cached data + const newColumn = this.getColumnFromCache(currentPosition); if (newColumn && newColumn !== this.currentColumn) { + const previousColumn = this.currentColumn; this.currentColumn = newColumn; this.eventBus.emit('drag:column-change', { eventId: this.draggedEventId, - previousColumn: this.currentColumn, + previousColumn, newColumn, - mousePosition: { x: event.clientX, y: event.clientY } + mousePosition: currentPosition }); } } } + /** + * Optimized mouse up handler with consolidated cleanup + */ private handleMouseUp(event: MouseEvent): void { if (!this.isMouseDown) return; this.isMouseDown = false; - - // Stop auto-scroll this.stopAutoScroll(); if (this.draggedEventId && this.originalElement) { - // Calculate final position - const finalColumn = this.detectColumn(event.clientX, event.clientY); - const finalY = this.calculateSnapPosition(event.clientY); + const finalPosition: Position = { x: event.clientX, y: event.clientY }; + + // Use consolidated position calculation + const positionData = this.calculateDragPosition(finalPosition); // Emit drag end event this.eventBus.emit('drag:end', { eventId: this.draggedEventId, originalElement: this.originalElement, - finalPosition: { x: event.clientX, y: event.clientY }, - finalColumn, - finalY + finalPosition, + finalColumn: positionData.column, + finalY: positionData.snappedY }); - - // Clean up - this.draggedEventId = null; - this.originalElement = null; - this.currentColumn = null; - this.scrollContainer = null; + // Clean up drag state + this.cleanupDragState(); } } /** - * Calculate snapped Y position based on mouse Y + * Consolidated position calculation method */ - private calculateSnapPosition(mouseY: number): number { - // Find the column element to get relative position - const columnElement = this.currentColumn - ? document.querySelector(`swp-day-column[data-date="${this.currentColumn}"]`) - : null; - + private calculateDragPosition(mousePosition: Position): { column: string | null; snappedY: number } { + const column = this.detectColumn(mousePosition.x, mousePosition.y); + const snappedY = this.calculateSnapPosition(mousePosition.y, column); + + return { column, snappedY }; + } + + /** + * Optimized snap position calculation with caching + */ + private calculateSnapPosition(mouseY: number, column: string | null = null): number { + const targetColumn = column || this.currentColumn; + + // Use cached column element if available + const columnElement = this.getCachedColumnElement(targetColumn); if (!columnElement) return mouseY; const columnRect = columnElement.getBoundingClientRect(); const relativeY = mouseY - columnRect.top - this.mouseOffset.y; - // Snap to nearest interval + // Snap to nearest interval using DateCalculator precision const snappedY = Math.round(relativeY / this.snapDistancePx) * this.snapDistancePx; - // Ensure non-negative return Math.max(0, snappedY); } /** - * Detect which column the mouse is over + * Optimized column detection with caching */ private detectColumn(mouseX: number, mouseY: number): string | null { const element = document.elementFromPoint(mouseX, mouseY); @@ -237,22 +274,67 @@ export class DragDropManager { if (!current) return null; } - return current.dataset.date || null; + const columnDate = current.dataset.date || null; + + // Update cache if we found a new column + if (columnDate && columnDate !== this.cachedElements.lastColumnDate) { + this.cachedElements.currentColumn = current; + this.cachedElements.lastColumnDate = columnDate; + } + + return columnDate; + } + + /** + * Get column from cache or detect new one + */ + private getColumnFromCache(mousePosition: Position): string | null { + // Try to use cached column first + if (this.cachedElements.currentColumn && this.cachedElements.lastColumnDate) { + const rect = this.cachedElements.currentColumn.getBoundingClientRect(); + if (mousePosition.x >= rect.left && mousePosition.x <= rect.right) { + return this.cachedElements.lastColumnDate; + } + } + + // Cache miss - detect new column + return this.detectColumn(mousePosition.x, mousePosition.y); + } + + /** + * Get cached column element or query for new one + */ + private getCachedColumnElement(columnDate: string | null): HTMLElement | null { + if (!columnDate) return null; + + // Return cached element if it matches + if (this.cachedElements.lastColumnDate === columnDate && this.cachedElements.currentColumn) { + return this.cachedElements.currentColumn; + } + + // Query for new element and cache it + const element = document.querySelector(`swp-day-column[data-date="${columnDate}"]`) as HTMLElement; + if (element) { + this.cachedElements.currentColumn = element; + this.cachedElements.lastColumnDate = columnDate; + } + + return element; } /** - * Check if auto-scroll should be triggered + * Optimized auto-scroll check with cached container */ private checkAutoScroll(event: MouseEvent): void { - // Find scrollable content if not cached - if (!this.scrollContainer) { - this.scrollContainer = document.querySelector('swp-scrollable-content') as HTMLElement; - if (!this.scrollContainer) { + // Use cached scroll container + if (!this.cachedElements.scrollContainer) { + this.cachedElements.scrollContainer = document.querySelector('swp-scrollable-content') as HTMLElement; + if (!this.cachedElements.scrollContainer) { return; } } - const containerRect = this.scrollContainer.getBoundingClientRect(); + const containerRect = this.cachedElements.scrollContainer.getBoundingClientRect(); const mouseY = event.clientY; // Calculate distances from edges @@ -270,19 +352,19 @@ export class DragDropManager { } /** - * Start auto-scroll animation + * Optimized auto-scroll with cached container reference */ private startAutoScroll(direction: 'up' | 'down'): void { if (this.autoScrollAnimationId !== null) return; const scroll = () => { - if (!this.scrollContainer || !this.isMouseDown) { + if (!this.cachedElements.scrollContainer || !this.isMouseDown) { this.stopAutoScroll(); return; } const scrollAmount = direction === 'up' ? -this.scrollSpeed : this.scrollSpeed; - this.scrollContainer.scrollTop += scrollAmount; + this.cachedElements.scrollContainer.scrollTop += scrollAmount; // Emit updated position during scroll if (this.draggedEventId) { @@ -290,7 +372,7 @@ export class DragDropManager { this.eventBus.emit('drag:auto-scroll', { eventId: this.draggedEventId, snappedY, - scrollTop: this.scrollContainer.scrollTop + scrollTop: this.cachedElements.scrollContainer.scrollTop }); } @@ -311,12 +393,35 @@ export class DragDropManager { } /** - * Clean up event listeners + * Clean up drag state + */ + private cleanupDragState(): void { + this.draggedEventId = null; + this.originalElement = null; + this.currentColumn = null; + + // Clear cached elements + this.cachedElements.currentColumn = null; + this.cachedElements.lastColumnDate = null; + } + + /** + * Clean up all resources and event listeners */ public destroy(): void { this.stopAutoScroll(); - document.body.removeEventListener('mousemove', this.handleMouseMove.bind(this)); - document.body.removeEventListener('mousedown', this.handleMouseDown.bind(this)); - document.body.removeEventListener('mouseup', this.handleMouseUp.bind(this)); + + // Remove event listeners using bound references + document.body.removeEventListener('mousemove', this.boundHandlers.mouseMove); + document.body.removeEventListener('mousedown', this.boundHandlers.mouseDown); + document.body.removeEventListener('mouseup', this.boundHandlers.mouseUp); + + // Clear all cached elements + this.cachedElements.scrollContainer = null; + this.cachedElements.currentColumn = null; + this.cachedElements.lastColumnDate = null; + + // Clean up drag state + this.cleanupDragState(); } } \ No newline at end of file diff --git a/src/utils/PositionUtils.ts b/src/utils/PositionUtils.ts index 49930af..3e1f708 100644 --- a/src/utils/PositionUtils.ts +++ b/src/utils/PositionUtils.ts @@ -1,8 +1,9 @@ import { CalendarConfig } from '../core/CalendarConfig.js'; +import { DateCalculator } from './DateCalculator.js'; /** - * PositionUtils - Utility functions for pixel/minute conversion - * Handles positioning and size calculations for calendar events + * PositionUtils - Optimized positioning utilities using DateCalculator + * Focuses on pixel/position calculations while delegating date operations */ export class PositionUtils { private config: CalendarConfig; @@ -30,11 +31,10 @@ export class PositionUtils { } /** - * Convert time (HH:MM) to pixels from day start + * Convert time (HH:MM) to pixels from day start using DateCalculator */ public timeToPixels(timeString: string): number { - const [hours, minutes] = timeString.split(':').map(Number); - const totalMinutes = (hours * 60) + minutes; + const totalMinutes = DateCalculator.timeToMinutes(timeString); const gridSettings = this.config.getGridSettings(); const dayStartMinutes = gridSettings.dayStartHour * 60; const minutesFromDayStart = totalMinutes - dayStartMinutes; @@ -43,12 +43,10 @@ export class PositionUtils { } /** - * Convert Date object to pixels from day start + * Convert Date object to pixels from day start using DateCalculator */ public dateToPixels(date: Date): number { - const hours = date.getHours(); - const minutes = date.getMinutes(); - const totalMinutes = (hours * 60) + minutes; + const totalMinutes = DateCalculator.getMinutesSinceMidnight(date); const gridSettings = this.config.getGridSettings(); const dayStartMinutes = gridSettings.dayStartHour * 60; const minutesFromDayStart = totalMinutes - dayStartMinutes; @@ -57,7 +55,7 @@ export class PositionUtils { } /** - * Konverter pixels til tid (HH:MM format) + * Convert pixels to time using DateCalculator */ public pixelsToTime(pixels: number): string { const minutes = this.pixelsToMinutes(pixels); @@ -65,10 +63,7 @@ export class PositionUtils { const dayStartMinutes = gridSettings.dayStartHour * 60; const totalMinutes = dayStartMinutes + minutes; - const hours = Math.floor(totalMinutes / 60); - const mins = Math.round(totalMinutes % 60); - - return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`; + return DateCalculator.minutesToTime(totalMinutes); } /** @@ -116,19 +111,15 @@ export class PositionUtils { } /** - * Snap tid til interval + * Snap time to interval using DateCalculator */ public snapTimeToInterval(timeString: string): string { - const [hours, minutes] = timeString.split(':').map(Number); - const totalMinutes = (hours * 60) + minutes; + const totalMinutes = DateCalculator.timeToMinutes(timeString); const gridSettings = this.config.getGridSettings(); const snapInterval = gridSettings.snapInterval; const snappedMinutes = Math.round(totalMinutes / snapInterval) * snapInterval; - const snappedHours = Math.floor(snappedMinutes / 60); - const remainingMinutes = snappedMinutes % 60; - - return `${snappedHours.toString().padStart(2, '0')}:${remainingMinutes.toString().padStart(2, '0')}`; + return DateCalculator.minutesToTime(snappedMinutes); } /** @@ -231,21 +222,21 @@ export class PositionUtils { } /** - * Konverter ISO datetime til lokal tid string + * Convert ISO datetime to time string using DateCalculator */ public isoToTimeString(isoString: string): string { const date = new Date(isoString); - const hours = date.getHours(); - const minutes = date.getMinutes(); - - return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; + return DateCalculator.formatTime(date); } /** - * Konverter lokal tid string til ISO datetime for i dag + * Convert time string to ISO datetime using DateCalculator */ public timeStringToIso(timeString: string, date: Date = new Date()): string { - const [hours, minutes] = timeString.split(':').map(Number); + const totalMinutes = DateCalculator.timeToMinutes(timeString); + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + const newDate = new Date(date); newDate.setHours(hours, minutes, 0, 0); @@ -253,29 +244,14 @@ export class PositionUtils { } /** - * Beregn event varighed i minutter + * Calculate event duration using DateCalculator */ public calculateDuration(startTime: string | Date, endTime: string | Date): number { - let startMs: number; - let endMs: number; - - if (typeof startTime === 'string') { - startMs = new Date(startTime).getTime(); - } else { - startMs = startTime.getTime(); - } - - if (typeof endTime === 'string') { - endMs = new Date(endTime).getTime(); - } else { - endMs = endTime.getTime(); - } - - return Math.round((endMs - startMs) / (1000 * 60)); // Minutter + return DateCalculator.getDurationMinutes(startTime, endTime); } /** - * Format varighed til læsbar tekst + * Format duration to readable text (Danish) */ public formatDuration(minutes: number): string { if (minutes < 60) {