Refactors drag and drop manager for performance
Improves drag and drop performance by caching DOM elements and consolidating position calculations. This reduces redundant DOM queries and optimizes event handling for smoother user interaction. Also leverages `DateCalculator` for date/time conversions.
This commit is contained in:
parent
05bb074e9a
commit
d0936d1838
2 changed files with 193 additions and 112 deletions
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if auto-scroll should be triggered
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue