Refactors drag and drop column detection

Improves drag and drop functionality by refactoring column detection to use column bounds instead of dates.
This change enhances the accuracy and efficiency of determining the target column during drag operations.
It also removes redundant code and simplifies the logic in both the DragDropManager and AllDayManager.
This commit is contained in:
Janus C. H. Knudsen 2025-09-28 13:25:09 +02:00
parent 4141bffca4
commit 6ccc071587
8 changed files with 262 additions and 377 deletions

View file

@ -6,7 +6,7 @@
import { IEventBus } from '../types/CalendarTypes';
import { calendarConfig } from '../core/CalendarConfig';
import { PositionUtils } from '../utils/PositionUtils';
import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
import { ColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
import { SwpEventElement } from '../elements/SwpEventElement';
import {
DragStartEventPayload,
@ -16,33 +16,30 @@ import {
DragMouseLeaveHeaderEventPayload,
DragColumnChangeEventPayload
} from '../types/EventTypes';
import { MousePosition } from '../types/DragDropTypes';
interface CachedElements {
scrollContainer: HTMLElement | null;
currentColumn: HTMLElement | null;
lastColumnDate: string | null;
}
interface Position {
x: number;
y: number;
}
export class DragDropManager {
private eventBus: IEventBus;
// Mouse tracking with optimized state
private lastMousePosition: Position = { x: 0, y: 0 };
private lastLoggedPosition: Position = { x: 0, y: 0 };
private lastMousePosition: MousePosition = { x: 0, y: 0 };
private lastLoggedPosition: MousePosition = { x: 0, y: 0 };
private currentMouseY = 0;
private mouseOffset: Position = { x: 0, y: 0 };
private initialMousePosition: Position = { x: 0, y: 0 };
private mouseOffset: MousePosition = { x: 0, y: 0 };
private initialMousePosition: MousePosition = { x: 0, y: 0 };
private lastColumn: ColumnBounds | null = null;
// Drag state
private draggedElement!: HTMLElement | null;
private draggedClone!: HTMLElement | null;
private currentColumn: string | null = null;
private currentColumnBounds: ColumnBounds | null = null;
private isDragStarted = false;
// Header tracking state
@ -51,12 +48,9 @@ export class DragDropManager {
// Movement threshold to distinguish click from drag
private readonly dragThreshold = 5; // pixels
private scrollContainer!: HTMLElement | null;
// Cached DOM elements for performance
private cachedElements: CachedElements = {
scrollContainer: null,
currentColumn: null,
lastColumnDate: null
};
@ -106,7 +100,7 @@ export class DragDropManager {
document.body.addEventListener('mousedown', this.boundHandlers.mouseDown);
document.body.addEventListener('mouseup', this.boundHandlers.mouseUp);
// Add mouseleave listener to calendar container for drag cancellation
this.scrollContainer = document.querySelector('swp-scrollable-content') as HTMLElement;
const calendarContainer = document.querySelector('swp-calendar-container');
if (calendarContainer) {
calendarContainer.addEventListener('mouseleave', () => {
@ -133,9 +127,9 @@ export class DragDropManager {
private handleMouseDown(event: MouseEvent): void {
// Clean up drag state first
this.cleanupDragState();
// Clean up drag state first
this.cleanupDragState();
this.lastMousePosition = { x: event.clientX, y: event.clientY };
this.lastLoggedPosition = { x: event.clientX, y: event.clientY };
this.initialMousePosition = { x: event.clientX, y: event.clientY };
@ -160,21 +154,13 @@ export class DragDropManager {
// Found an event - prepare for potential dragging
if (eventElement) {
this.draggedElement = eventElement;
this.lastColumn = ColumnDetectionUtils.getColumnBounds(this.lastMousePosition)
// Calculate mouse offset within event
const eventRect = eventElement.getBoundingClientRect();
this.mouseOffset = {
x: event.clientX - eventRect.left,
y: event.clientY - eventRect.top
};
// Detect current column
const column = this.detectColumn(event.clientX, event.clientY);
if (column) {
this.currentColumn = column;
}
// Don't emit drag:start yet - wait for movement threshold
}
}
@ -191,7 +177,7 @@ export class DragDropManager {
}
if (event.buttons === 1 && this.draggedElement) {
const currentPosition: Position = { x: event.clientX, y: event.clientY };
const currentPosition: MousePosition = { x: event.clientX, y: event.clientY }; //TODO: Is this really needed? why not just use event.clientX + Y directly
// Check if we need to start drag (movement threshold)
if (!this.isDragStarted) {
@ -203,20 +189,22 @@ export class DragDropManager {
// Start drag - emit drag:start event
this.isDragStarted = true;
// Create SwpEventElement from existing DOM element and clone it
const originalSwpEvent = SwpEventElement.fromExistingElement(this.draggedElement);
const clonedSwpEvent = originalSwpEvent.createClone();
// Get the cloned DOM element
this.draggedClone = clonedSwpEvent.getElement();
// Detect current column
this.currentColumnBounds = ColumnDetectionUtils.getColumnBounds(currentPosition);
// Create SwpEventElement from existing DOM element and clone it
const originalSwpEvent = SwpEventElement.fromExistingElement(this.draggedElement);
const clonedSwpEvent = originalSwpEvent.createClone();
// Get the cloned DOM element
this.draggedClone = clonedSwpEvent.getElement();
const dragStartPayload: DragStartEventPayload = {
draggedElement: this.draggedElement,
draggedClone: this.draggedClone,
mousePosition: this.initialMousePosition,
mouseOffset: this.mouseOffset,
column: this.currentColumn
columnBounds: this.currentColumnBounds
};
this.eventBus.emit('drag:start', dragStartPayload);
} else {
@ -241,20 +229,21 @@ export class DragDropManager {
draggedElement: this.draggedElement,
mousePosition: currentPosition,
snappedY: positionData.snappedY,
column: positionData.column,
columnBounds: positionData.column,
mouseOffset: this.mouseOffset
};
this.eventBus.emit('drag:move', dragMovePayload);
}
// Check for auto-scroll
this.checkAutoScroll(event);
this.checkAutoScroll(currentPosition);
// Check for column change using cached data
const newColumn = this.getColumnFromCache(currentPosition);
if (newColumn && newColumn !== this.currentColumn) {
const previousColumn = this.currentColumn;
this.currentColumn = newColumn;
const newColumn = ColumnDetectionUtils.getColumnBounds(currentPosition);
if (newColumn && newColumn !== this.currentColumnBounds) {
const previousColumn = this.currentColumnBounds;
this.currentColumnBounds = newColumn;
const dragColumnChangePayload: DragColumnChangeEventPayload = {
draggedElement: this.draggedElement,
@ -276,16 +265,10 @@ export class DragDropManager {
this.stopAutoScroll();
if (this.draggedElement) {
// Store variables locally before cleanup
//const draggedElement = this.draggedElement;
const isDragStarted = this.isDragStarted;
// Only emit drag:end if drag was actually started
if (isDragStarted) {
const mousePosition: Position = { x: event.clientX, y: event.clientY };
if (this.isDragStarted) {
const mousePosition: MousePosition = { x: event.clientX, y: event.clientY };
// Use consolidated position calculation
const positionData = this.calculateDragPosition(mousePosition);
@ -298,12 +281,12 @@ export class DragDropManager {
finalColumn: positionData.column,
finalY: positionData.snappedY,
dropTarget: dropTarget,
isDragStarted: isDragStarted
isDragStarted: this.isDragStarted
});
const dragEndPayload: DragEndEventPayload = {
draggedElement: this.draggedElement,
draggedClone : this.draggedClone,
draggedClone: this.draggedClone,
mousePosition,
finalPosition: positionData,
target: dropTarget
@ -325,7 +308,7 @@ export class DragDropManager {
private cleanupAllClones(): void {
// Remove clones from all possible locations
const allClones = document.querySelectorAll('[data-event-id^="clone"]');
if (allClones.length > 0) {
console.log(`🧹 DragDropManager: Removing ${allClones.length} clone(s)`);
allClones.forEach(clone => clone.remove());
@ -365,9 +348,13 @@ export class DragDropManager {
/**
* Consolidated position calculation method using PositionUtils
*/
private calculateDragPosition(mousePosition: Position): { column: string | null; snappedY: number } {
const column = this.detectColumn(mousePosition.x, mousePosition.y);
const snappedY = this.calculateSnapPosition(mousePosition.y, column);
private calculateDragPosition(mousePosition: MousePosition): { column: ColumnBounds | null; snappedY: number } {
let column = ColumnDetectionUtils.getColumnBounds(mousePosition);
let snappedY = 0;
if (column) {
snappedY = this.calculateSnapPosition(mousePosition.y, column);
return { column, snappedY };
}
return { column, snappedY };
}
@ -375,100 +362,33 @@ export class DragDropManager {
/**
* Optimized snap position calculation using PositionUtils
*/
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;
// Use PositionUtils for consistent snapping behavior
const snappedY = PositionUtils.getPositionFromCoordinate(mouseY, columnElement);
private calculateSnapPosition(mouseY: number, column: ColumnBounds): number {
const snappedY = PositionUtils.getPositionFromCoordinate(mouseY, column);
return Math.max(0, snappedY);
}
/**
* Coordinate-based column detection (replaces DOM traversal)
*/
private detectColumn(mouseX: number, mouseY: number): string | null {
// Brug den koordinatbaserede metode direkte
const columnDate = ColumnDetectionUtils.getColumnDateFromX(mouseX);
// Opdater stadig den eksisterende cache hvis vi finder en kolonne
if (columnDate && columnDate !== this.cachedElements.lastColumnDate) {
const columnElement = document.querySelector(`swp-day-column[data-date="${columnDate}"]`) as HTMLElement;
if (columnElement) {
this.cachedElements.currentColumn = columnElement;
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;
}
/**
* Optimized auto-scroll check with cached container
*/
private checkAutoScroll(event: MouseEvent): void {
// Use cached scroll container
if (!this.cachedElements.scrollContainer) {
this.cachedElements.scrollContainer = document.querySelector('swp-scrollable-content') as HTMLElement;
if (!this.cachedElements.scrollContainer) {
return;
}
}
private checkAutoScroll(mousePosition: MousePosition): void {
const containerRect = this.cachedElements.scrollContainer.getBoundingClientRect();
const mouseY = event.clientY;
if (this.scrollContainer == null)
return;
const containerRect = this.scrollContainer.getBoundingClientRect();
const mouseY = mousePosition.clientY;
// Calculate distances from edges
const distanceFromTop = mouseY - containerRect.top;
const distanceFromBottom = containerRect.bottom - mouseY;
const distanceFromTop = mousePosition.y - containerRect.top;
const distanceFromBottom = containerRect.bottom - mousePosition.y;
// Check if we need to scroll
if (distanceFromTop <= this.scrollThreshold && distanceFromTop > 0) {
this.startAutoScroll('up');
this.startAutoScroll('up', mousePosition);
} else if (distanceFromBottom <= this.scrollThreshold && distanceFromBottom > 0) {
this.startAutoScroll('down');
this.startAutoScroll('down', mousePosition);
} else {
this.stopAutoScroll();
}
@ -477,25 +397,26 @@ export class DragDropManager {
/**
* Optimized auto-scroll with cached container reference
*/
private startAutoScroll(direction: 'up' | 'down'): void {
private startAutoScroll(direction: 'up' | 'down', event: MousePosition): void {
if (this.autoScrollAnimationId !== null) return;
const scroll = () => {
if (!this.cachedElements.scrollContainer || !this.draggedElement) {
if (!this.scrollContainer || !this.draggedElement) {
this.stopAutoScroll();
return;
}
const scrollAmount = direction === 'up' ? -this.scrollSpeed : this.scrollSpeed;
this.cachedElements.scrollContainer.scrollTop += scrollAmount;
this.scrollContainer.scrollTop += scrollAmount;
// Emit updated position during scroll - adjust for scroll movement
if (this.draggedElement) {
// During autoscroll, we need to calculate position relative to the scrolled content
// The mouse hasn't moved, but the content has scrolled
const columnElement = this.getCachedColumnElement(this.currentColumn);
const columnElement = ColumnDetectionUtils.getColumnBounds(event);
if (columnElement) {
const columnRect = columnElement.getBoundingClientRect();
const columnRect = columnElement.boundingClientRect;
// Calculate free position relative to column, accounting for scroll movement (no snapping during scroll)
const relativeY = this.currentMouseY - columnRect.top - this.mouseOffset.y;
const freeY = Math.max(0, relativeY);
@ -503,7 +424,7 @@ export class DragDropManager {
this.eventBus.emit('drag:auto-scroll', {
draggedElement: this.draggedElement,
snappedY: freeY, // Actually free position during scroll
scrollTop: this.cachedElements.scrollContainer.scrollTop
scrollTop: this.scrollContainer.scrollTop
});
}
}
@ -530,20 +451,15 @@ export class DragDropManager {
private cleanupDragState(): void {
this.draggedElement = null;
this.draggedClone = null;
this.currentColumn = null;
this.isDragStarted = false;
this.isInHeader = false;
// Clear cached elements
this.cachedElements.currentColumn = null;
this.cachedElements.lastColumnDate = null;
}
/**
* Detect drop target - whether dropped in swp-day-column or swp-day-header
*/
private detectDropTarget(position: Position): 'swp-day-column' | 'swp-day-header' | null {
private detectDropTarget(position: MousePosition): 'swp-day-column' | 'swp-day-header' | null {
// Traverse up the DOM tree to find the target container
let currentElement = this.draggedClone;
while (currentElement && currentElement !== document.body) {
@ -563,6 +479,8 @@ export class DragDropManager {
* Check for header enter/leave during drag operations
*/
private checkHeaderEnterLeave(event: MouseEvent): void {
let position: MousePosition = { x: event.clientX, y: event.clientY };
const elementAtPosition = document.elementFromPoint(event.clientX, event.clientY);
if (!elementAtPosition) return;
@ -575,17 +493,17 @@ export class DragDropManager {
this.isInHeader = true;
// Calculate target date using existing method
const targetDate = ColumnDetectionUtils.getColumnDateFromX(event.clientX);
const targetColumn = ColumnDetectionUtils.getColumnBounds(position);
if (targetDate) {
console.log('🎯 DragDropManager: Emitting drag:mouseenter-header', { targetDate });
if (targetColumn) {
console.log('🎯 DragDropManager: Emitting drag:mouseenter-header', { targetDate: targetColumn });
// Find clone element (if it exists)
const eventId = this.draggedElement?.dataset.eventId;
const cloneElement = document.querySelector(`[data-event-id="clone-${eventId}"]`) as HTMLElement;
const dragMouseEnterPayload: DragMouseEnterHeaderEventPayload = {
targetDate,
targetColumn: targetColumn,
mousePosition: { x: event.clientX, y: event.clientY },
originalElement: this.draggedElement,
cloneElement: cloneElement
@ -601,14 +519,18 @@ export class DragDropManager {
console.log('🚪 DragDropManager: Emitting drag:mouseleave-header');
// Calculate target date using existing method
const targetDate = ColumnDetectionUtils.getColumnDateFromX(event.clientX);
const targetColumn = ColumnDetectionUtils.getColumnBounds(position);
if (!targetColumn) {
console.warn("No column detected, unknown reason");
return;
}
// Find clone element (if it exists)
const eventId = this.draggedElement?.dataset.eventId;
const cloneElement = document.querySelector(`[data-event-id="clone-${eventId}"]`) as HTMLElement;
const dragMouseLeavePayload: DragMouseLeaveHeaderEventPayload = {
targetDate,
targetDate: targetColumn.date,
mousePosition: { x: event.clientX, y: event.clientY },
originalElement: this.draggedElement,
cloneElement: cloneElement
@ -628,11 +550,6 @@ export class DragDropManager {
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();
}