2025-08-27 22:50:13 +02:00
/ * *
2025-09-03 19:05:03 +02:00
* DragDropManager - Optimized drag and drop with consolidated position calculations
* Reduces redundant DOM queries and improves performance through caching
2025-08-27 22:50:13 +02:00
* /
import { IEventBus } from '../types/CalendarTypes' ;
2025-09-03 20:04:47 +02:00
import { calendarConfig } from '../core/CalendarConfig' ;
2025-09-13 00:39:56 +02:00
import { PositionUtils } from '../utils/PositionUtils' ;
2025-09-26 22:11:57 +02:00
import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils' ;
2025-09-26 22:53:49 +02:00
import { SwpEventElement } from '../elements/SwpEventElement' ;
2025-09-21 15:48:13 +02:00
import {
DragStartEventPayload ,
DragMoveEventPayload ,
DragEndEventPayload ,
DragMouseEnterHeaderEventPayload ,
2025-09-26 22:11:57 +02:00
DragMouseLeaveHeaderEventPayload ,
DragColumnChangeEventPayload
2025-09-21 15:48:13 +02:00
} from '../types/EventTypes' ;
2025-09-03 19:05:03 +02:00
interface CachedElements {
scrollContainer : HTMLElement | null ;
currentColumn : HTMLElement | null ;
lastColumnDate : string | null ;
}
interface Position {
x : number ;
y : number ;
}
2025-08-27 22:50:13 +02:00
2025-09-19 00:20:30 +02:00
2025-08-27 22:50:13 +02:00
export class DragDropManager {
private eventBus : IEventBus ;
2025-09-21 15:48:13 +02:00
2025-09-03 19:05:03 +02:00
// Mouse tracking with optimized state
private lastMousePosition : Position = { x : 0 , y : 0 } ;
private lastLoggedPosition : Position = { x : 0 , y : 0 } ;
2025-08-27 22:50:13 +02:00
private currentMouseY = 0 ;
2025-09-03 19:05:03 +02:00
private mouseOffset : Position = { x : 0 , y : 0 } ;
2025-09-09 14:35:21 +02:00
private initialMousePosition : Position = { x : 0 , y : 0 } ;
2025-09-21 15:48:13 +02:00
2025-08-27 22:50:13 +02:00
// Drag state
2025-09-21 16:03:34 +02:00
private draggedElement ! : HTMLElement | null ;
2025-09-26 22:53:49 +02:00
private draggedClone ! : HTMLElement | null ;
2025-08-27 22:50:13 +02:00
private currentColumn : string | null = null ;
2025-09-09 14:35:21 +02:00
private isDragStarted = false ;
2025-09-21 15:48:13 +02:00
// Header tracking state
private isInHeader = false ;
2025-09-09 14:35:21 +02:00
// Movement threshold to distinguish click from drag
private readonly dragThreshold = 5 ; // pixels
2025-09-21 15:48:13 +02:00
2025-09-03 19:05:03 +02:00
// Cached DOM elements for performance
private cachedElements : CachedElements = {
scrollContainer : null ,
currentColumn : null ,
lastColumnDate : null
} ;
2025-09-21 15:48:13 +02:00
2025-09-22 17:51:24 +02:00
2025-08-27 22:50:13 +02:00
// Auto-scroll properties
private autoScrollAnimationId : number | null = null ;
2025-09-03 19:05:03 +02:00
private readonly scrollSpeed = 10 ; // pixels per frame
private readonly scrollThreshold = 30 ; // pixels from edge
2025-09-21 15:48:13 +02:00
2025-08-27 22:50:13 +02:00
// Snap configuration
private snapIntervalMinutes = 15 ; // Default 15 minutes
2025-09-03 20:48:23 +02:00
private hourHeightPx : number ; // Will be set from config
2025-09-21 15:48:13 +02:00
2025-09-03 19:05:03 +02:00
// Event listener references for proper cleanup
private boundHandlers = {
mouseMove : this.handleMouseMove.bind ( this ) ,
mouseDown : this.handleMouseDown.bind ( this ) ,
mouseUp : this.handleMouseUp.bind ( this )
} ;
2025-09-21 15:48:13 +02:00
2025-08-27 22:50:13 +02:00
private get snapDistancePx ( ) : number {
return ( this . snapIntervalMinutes / 60 ) * this . hourHeightPx ;
}
2025-09-21 15:48:13 +02:00
2025-09-03 20:04:47 +02:00
constructor ( eventBus : IEventBus ) {
2025-08-27 22:50:13 +02:00
this . eventBus = eventBus ;
// Get config values
2025-09-03 20:04:47 +02:00
const gridSettings = calendarConfig . getGridSettings ( ) ;
2025-08-27 22:50:13 +02:00
this . hourHeightPx = gridSettings . hourHeight ;
2025-09-03 20:48:23 +02:00
this . snapIntervalMinutes = gridSettings . snapInterval ;
2025-09-21 15:48:13 +02:00
2025-08-27 22:50:13 +02:00
this . init ( ) ;
}
2025-09-21 15:48:13 +02:00
2025-08-27 22:50:13 +02:00
/ * *
* Configure snap interval
* /
public setSnapInterval ( minutes : number ) : void {
this . snapIntervalMinutes = minutes ;
}
2025-09-21 15:48:13 +02:00
2025-09-03 19:05:03 +02:00
/ * *
* Initialize with optimized event listener setup
* /
2025-08-27 22:50:13 +02:00
private init ( ) : void {
2025-09-03 19:05:03 +02:00
// 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 ) ;
2025-09-21 15:48:13 +02:00
2025-09-22 17:51:24 +02:00
// Add mouseleave listener to calendar container for drag cancellation
const calendarContainer = document . querySelector ( 'swp-calendar-container' ) ;
if ( calendarContainer ) {
calendarContainer . addEventListener ( 'mouseleave' , ( ) = > {
if ( this . draggedElement && this . isDragStarted ) {
this . cancelDrag ( ) ;
}
} ) ;
}
2025-09-19 00:20:30 +02:00
// Initialize column bounds cache
2025-09-26 22:11:57 +02:00
ColumnDetectionUtils . updateColumnBoundsCache ( ) ;
2025-09-21 15:48:13 +02:00
2025-09-19 00:20:30 +02:00
// Listen to resize events to update cache
window . addEventListener ( 'resize' , ( ) = > {
2025-09-26 22:11:57 +02:00
ColumnDetectionUtils . updateColumnBoundsCache ( ) ;
2025-09-19 00:20:30 +02:00
} ) ;
2025-09-21 15:48:13 +02:00
2025-09-19 00:20:30 +02:00
// Listen to navigation events to update cache
this . eventBus . on ( 'navigation:completed' , ( ) = > {
2025-09-26 22:11:57 +02:00
ColumnDetectionUtils . updateColumnBoundsCache ( ) ;
2025-09-19 00:20:30 +02:00
} ) ;
2025-09-10 22:07:40 +02:00
2025-08-27 22:50:13 +02:00
}
2025-09-21 15:48:13 +02:00
2025-08-27 22:50:13 +02:00
private handleMouseDown ( event : MouseEvent ) : void {
2025-09-27 15:01:22 +02:00
// Clean up drag state first
this . cleanupDragState ( ) ;
2025-08-27 22:50:13 +02:00
this . lastMousePosition = { x : event.clientX , y : event.clientY } ;
this . lastLoggedPosition = { x : event.clientX , y : event.clientY } ;
2025-09-09 14:35:21 +02:00
this . initialMousePosition = { x : event.clientX , y : event.clientY } ;
2025-09-21 15:48:13 +02:00
2025-08-27 22:50:13 +02:00
// Check if mousedown is on an event
const target = event . target as HTMLElement ;
let eventElement = target ;
2025-09-21 15:48:13 +02:00
2025-08-27 22:50:13 +02:00
while ( eventElement && eventElement . tagName !== 'SWP-EVENTS-LAYER' ) {
2025-09-21 16:03:34 +02:00
if ( eventElement . tagName === 'SWP-EVENT' ) {
2025-08-27 22:50:13 +02:00
break ;
}
eventElement = eventElement . parentElement as HTMLElement ;
if ( ! eventElement ) return ;
}
2025-09-21 15:48:13 +02:00
2025-08-27 22:50:13 +02:00
// If we reached SWP-EVENTS-LAYER without finding an event, return
if ( ! eventElement || eventElement . tagName === 'SWP-EVENTS-LAYER' ) {
return ;
}
2025-09-21 15:48:13 +02:00
2025-09-09 14:35:21 +02:00
// Found an event - prepare for potential dragging
2025-08-27 22:50:13 +02:00
if ( eventElement ) {
2025-09-21 15:48:13 +02:00
this . draggedElement = eventElement ;
2025-08-27 22:50:13 +02:00
// Calculate mouse offset within event
const eventRect = eventElement . getBoundingClientRect ( ) ;
this . mouseOffset = {
x : event.clientX - eventRect . left ,
y : event.clientY - eventRect . top
} ;
2025-09-21 15:48:13 +02:00
2025-08-27 22:50:13 +02:00
// Detect current column
const column = this . detectColumn ( event . clientX , event . clientY ) ;
if ( column ) {
this . currentColumn = column ;
}
2025-09-21 15:48:13 +02:00
2025-09-09 14:35:21 +02:00
// Don't emit drag:start yet - wait for movement threshold
2025-08-27 22:50:13 +02:00
}
}
2025-09-21 15:48:13 +02:00
2025-09-03 19:05:03 +02:00
/ * *
* Optimized mouse move handler with consolidated position calculations
* /
2025-08-27 22:50:13 +02:00
private handleMouseMove ( event : MouseEvent ) : void {
this . currentMouseY = event . clientY ;
2025-09-21 15:48:13 +02:00
this . lastMousePosition = { x : event.clientX , y : event.clientY } ;
// Check for header enter/leave during drag
if ( this . draggedElement ) {
this . checkHeaderEnterLeave ( event ) ;
}
if ( event . buttons === 1 && this . draggedElement ) {
2025-09-03 19:05:03 +02:00
const currentPosition : Position = { x : event.clientX , y : event.clientY } ;
2025-09-21 15:48:13 +02:00
2025-09-09 14:35:21 +02:00
// Check if we need to start drag (movement threshold)
if ( ! this . isDragStarted ) {
const deltaX = Math . abs ( currentPosition . x - this . initialMousePosition . x ) ;
const deltaY = Math . abs ( currentPosition . y - this . initialMousePosition . y ) ;
const totalMovement = Math . sqrt ( deltaX * deltaX + deltaY * deltaY ) ;
2025-09-21 15:48:13 +02:00
2025-09-09 14:35:21 +02:00
if ( totalMovement >= this . dragThreshold ) {
// Start drag - emit drag:start event
this . isDragStarted = true ;
2025-09-21 15:48:13 +02:00
2025-09-26 22:53:49 +02:00
// 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 ( ) ;
2025-09-21 15:48:13 +02:00
const dragStartPayload : DragStartEventPayload = {
draggedElement : this.draggedElement ,
2025-09-26 22:53:49 +02:00
draggedClone : this.draggedClone ,
2025-09-09 14:35:21 +02:00
mousePosition : this.initialMousePosition ,
mouseOffset : this.mouseOffset ,
column : this.currentColumn
2025-09-21 15:48:13 +02:00
} ;
this . eventBus . emit ( 'drag:start' , dragStartPayload ) ;
2025-09-09 14:35:21 +02:00
} else {
// Not enough movement yet - don't start drag
return ;
}
2025-08-27 22:50:13 +02:00
}
2025-09-21 15:48:13 +02:00
2025-09-09 14:35:21 +02:00
// Continue with normal drag behavior only if drag has started
if ( this . isDragStarted ) {
const deltaY = Math . abs ( currentPosition . y - this . lastLoggedPosition . y ) ;
2025-09-21 15:48:13 +02:00
2025-09-09 14:35:21 +02:00
// Check for snap interval vertical movement (normal drag behavior)
if ( deltaY >= this . snapDistancePx ) {
this . lastLoggedPosition = currentPosition ;
2025-09-21 15:48:13 +02:00
2025-09-09 14:35:21 +02:00
// Consolidated position calculations with snapping for normal drag
const positionData = this . calculateDragPosition ( currentPosition ) ;
2025-09-21 15:48:13 +02:00
2025-09-09 14:35:21 +02:00
// Emit drag move event with snapped position (normal behavior)
2025-09-21 15:48:13 +02:00
const dragMovePayload : DragMoveEventPayload = {
draggedElement : this.draggedElement ,
2025-09-09 14:35:21 +02:00
mousePosition : currentPosition ,
snappedY : positionData.snappedY ,
column : positionData.column ,
mouseOffset : this.mouseOffset
2025-09-21 15:48:13 +02:00
} ;
this . eventBus . emit ( 'drag:move' , dragMovePayload ) ;
2025-09-09 14:35:21 +02:00
}
2025-09-21 15:48:13 +02:00
2025-09-09 14:35:21 +02:00
// Check for auto-scroll
this . checkAutoScroll ( event ) ;
2025-09-21 15:48:13 +02:00
2025-09-09 14:35:21 +02:00
// Check for column change using cached data
const newColumn = this . getColumnFromCache ( currentPosition ) ;
if ( newColumn && newColumn !== this . currentColumn ) {
const previousColumn = this . currentColumn ;
this . currentColumn = newColumn ;
2025-09-21 15:48:13 +02:00
2025-09-26 22:11:57 +02:00
const dragColumnChangePayload : DragColumnChangeEventPayload = {
2025-09-21 15:48:13 +02:00
draggedElement : this.draggedElement ,
2025-09-26 22:53:49 +02:00
draggedClone : this.draggedClone ,
2025-09-09 14:35:21 +02:00
previousColumn ,
newColumn ,
mousePosition : currentPosition
2025-09-26 22:11:57 +02:00
} ;
this . eventBus . emit ( 'drag:column-change' , dragColumnChangePayload ) ;
2025-09-09 14:35:21 +02:00
}
2025-08-27 22:50:13 +02:00
}
}
}
2025-09-21 15:48:13 +02:00
2025-09-03 19:05:03 +02:00
/ * *
* Optimized mouse up handler with consolidated cleanup
* /
2025-08-27 22:50:13 +02:00
private handleMouseUp ( event : MouseEvent ) : void {
this . stopAutoScroll ( ) ;
2025-09-21 15:48:13 +02:00
if ( this . draggedElement ) {
2025-09-10 22:07:40 +02:00
// Store variables locally before cleanup
2025-09-27 15:01:22 +02:00
//const draggedElement = this.draggedElement;
2025-09-10 22:07:40 +02:00
const isDragStarted = this . isDragStarted ;
2025-09-21 15:48:13 +02:00
2025-09-27 15:01:22 +02:00
2025-09-21 21:30:51 +02:00
2025-09-21 15:48:13 +02:00
2025-09-09 14:35:21 +02:00
// Only emit drag:end if drag was actually started
2025-09-10 22:07:40 +02:00
if ( isDragStarted ) {
2025-09-21 15:48:13 +02:00
const mousePosition : Position = { x : event.clientX , y : event.clientY } ;
2025-09-09 14:35:21 +02:00
// Use consolidated position calculation
2025-09-21 15:48:13 +02:00
const positionData = this . calculateDragPosition ( mousePosition ) ;
2025-09-20 09:40:56 +02:00
// Detect drop target (swp-day-column or swp-day-header)
2025-09-21 15:48:13 +02:00
const dropTarget = this . detectDropTarget ( mousePosition ) ;
2025-09-16 23:09:10 +02:00
console . log ( '🎯 DragDropManager: Emitting drag:end' , {
2025-09-27 15:01:22 +02:00
draggedElement : this.draggedElement.dataset.eventId ,
2025-09-16 23:09:10 +02:00
finalColumn : positionData.column ,
finalY : positionData.snappedY ,
2025-09-20 09:40:56 +02:00
dropTarget : dropTarget ,
2025-09-16 23:09:10 +02:00
isDragStarted : isDragStarted
} ) ;
2025-09-21 15:48:13 +02:00
const dragEndPayload : DragEndEventPayload = {
2025-09-27 15:01:22 +02:00
draggedElement : this.draggedElement ,
draggedClone : this.draggedClone ,
2025-09-21 15:48:13 +02:00
mousePosition ,
finalPosition : positionData ,
2025-09-20 09:40:56 +02:00
target : dropTarget
2025-09-21 15:48:13 +02:00
} ;
this . eventBus . emit ( 'drag:end' , dragEndPayload ) ;
2025-09-21 21:30:51 +02:00
2025-09-27 15:01:22 +02:00
this . draggedElement . remove ( ) ; // TODO: this should be changed into a subscriber which only after a succesful placement is fired, not just mouseup as this can remove elements that are not placed.
2025-09-21 21:30:51 +02:00
2025-09-09 14:35:21 +02:00
} else {
// This was just a click - emit click event instead
this . eventBus . emit ( 'event:click' , {
2025-09-27 15:01:22 +02:00
draggedElement : this.draggedElement ,
2025-09-09 14:35:21 +02:00
mousePosition : { x : event.clientX , y : event.clientY }
} ) ;
}
2025-08-27 22:50:13 +02:00
}
}
2025-09-21 21:30:51 +02:00
// Add a cleanup method that finds and removes ALL clones
private cleanupAllClones ( ) : void {
// Remove clones from all possible locations
const allClones = document . querySelectorAll ( '[data-event-id^="clone"]' ) ;
2025-09-22 17:51:24 +02:00
if ( allClones . length > 0 ) {
console . log ( ` 🧹 DragDropManager: Removing ${ allClones . length } clone(s) ` ) ;
allClones . forEach ( clone = > clone . remove ( ) ) ;
}
}
/ * *
* Cancel drag operation when mouse leaves grid container
* /
private cancelDrag ( ) : void {
if ( ! this . draggedElement ) return ;
console . log ( '🚫 DragDropManager: Cancelling drag - mouse left grid container' ) ;
const draggedElement = this . draggedElement ;
// 1. Remove all clones
this . cleanupAllClones ( ) ;
// 2. Restore original element
if ( draggedElement ) {
draggedElement . style . opacity = '' ;
draggedElement . style . cursor = '' ;
}
// 3. Emit cancellation event
this . eventBus . emit ( 'drag:cancelled' , {
draggedElement : draggedElement ,
reason : 'mouse-left-grid'
} ) ;
// 4. Clean up state
this . cleanupDragState ( ) ;
this . stopAutoScroll ( ) ;
2025-09-21 21:30:51 +02:00
}
2025-09-21 15:48:13 +02:00
2025-08-27 22:50:13 +02:00
/ * *
2025-09-13 00:39:56 +02:00
* Consolidated position calculation method using PositionUtils
2025-08-27 22:50:13 +02:00
* /
2025-09-03 19:05:03 +02:00
private calculateDragPosition ( mousePosition : Position ) : { column : string | null ; snappedY : number } {
const column = this . detectColumn ( mousePosition . x , mousePosition . y ) ;
const snappedY = this . calculateSnapPosition ( mousePosition . y , column ) ;
2025-09-21 15:48:13 +02:00
return { column , snappedY } ;
2025-09-03 20:48:23 +02:00
}
/ * *
2025-09-13 00:39:56 +02:00
* Optimized snap position calculation using PositionUtils
2025-09-03 19:05:03 +02:00
* /
private calculateSnapPosition ( mouseY : number , column : string | null = null ) : number {
const targetColumn = column || this . currentColumn ;
2025-09-21 15:48:13 +02:00
2025-09-03 19:05:03 +02:00
// Use cached column element if available
const columnElement = this . getCachedColumnElement ( targetColumn ) ;
2025-08-27 22:50:13 +02:00
if ( ! columnElement ) return mouseY ;
2025-09-21 15:48:13 +02:00
2025-09-13 00:39:56 +02:00
// Use PositionUtils for consistent snapping behavior
const snappedY = PositionUtils . getPositionFromCoordinate ( mouseY , columnElement ) ;
2025-09-21 15:48:13 +02:00
2025-08-27 22:50:13 +02:00
return Math . max ( 0 , snappedY ) ;
}
2025-09-21 15:48:13 +02:00
2025-09-19 00:20:30 +02:00
/ * *
* Coordinate - based column detection ( replaces DOM traversal )
* /
private detectColumn ( mouseX : number , mouseY : number ) : string | null {
// Brug den koordinatbaserede metode direkte
2025-09-26 22:11:57 +02:00
const columnDate = ColumnDetectionUtils . getColumnDateFromX ( mouseX ) ;
2025-09-21 15:48:13 +02:00
2025-09-19 00:20:30 +02:00
// Opdater stadig den eksisterende cache hvis vi finder en kolonne
2025-09-03 19:05:03 +02:00
if ( columnDate && columnDate !== this . cachedElements . lastColumnDate ) {
2025-09-19 00:20:30 +02:00
const columnElement = document . querySelector ( ` swp-day-column[data-date=" ${ columnDate } "] ` ) as HTMLElement ;
if ( columnElement ) {
this . cachedElements . currentColumn = columnElement ;
this . cachedElements . lastColumnDate = columnDate ;
}
2025-09-03 19:05:03 +02:00
}
2025-09-21 15:48:13 +02:00
2025-09-03 19:05:03 +02:00
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 ;
}
}
2025-09-21 15:48:13 +02:00
2025-09-03 19:05:03 +02:00
// 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 ;
2025-09-21 15:48:13 +02:00
2025-09-03 19:05:03 +02:00
// Return cached element if it matches
if ( this . cachedElements . lastColumnDate === columnDate && this . cachedElements . currentColumn ) {
return this . cachedElements . currentColumn ;
}
2025-09-21 15:48:13 +02:00
2025-09-03 19:05:03 +02:00
// 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 ;
}
2025-09-21 15:48:13 +02:00
2025-09-03 19:05:03 +02:00
return element ;
2025-08-27 22:50:13 +02:00
}
2025-09-21 15:48:13 +02:00
2025-08-27 22:50:13 +02:00
/ * *
2025-09-03 19:05:03 +02:00
* Optimized auto - scroll check with cached container
2025-08-27 22:50:13 +02:00
* /
private checkAutoScroll ( event : MouseEvent ) : void {
2025-09-03 19:05:03 +02:00
// Use cached scroll container
if ( ! this . cachedElements . scrollContainer ) {
this . cachedElements . scrollContainer = document . querySelector ( 'swp-scrollable-content' ) as HTMLElement ;
if ( ! this . cachedElements . scrollContainer ) {
2025-08-27 22:50:13 +02:00
return ;
}
}
2025-09-21 15:48:13 +02:00
2025-09-03 19:05:03 +02:00
const containerRect = this . cachedElements . scrollContainer . getBoundingClientRect ( ) ;
2025-08-27 22:50:13 +02:00
const mouseY = event . clientY ;
2025-09-21 15:48:13 +02:00
2025-08-27 22:50:13 +02:00
// Calculate distances from edges
const distanceFromTop = mouseY - containerRect . top ;
const distanceFromBottom = containerRect . bottom - mouseY ;
2025-09-21 15:48:13 +02:00
2025-08-27 22:50:13 +02:00
// Check if we need to scroll
if ( distanceFromTop <= this . scrollThreshold && distanceFromTop > 0 ) {
this . startAutoScroll ( 'up' ) ;
} else if ( distanceFromBottom <= this . scrollThreshold && distanceFromBottom > 0 ) {
this . startAutoScroll ( 'down' ) ;
} else {
this . stopAutoScroll ( ) ;
}
}
2025-09-21 15:48:13 +02:00
2025-08-27 22:50:13 +02:00
/ * *
2025-09-03 19:05:03 +02:00
* Optimized auto - scroll with cached container reference
2025-08-27 22:50:13 +02:00
* /
private startAutoScroll ( direction : 'up' | 'down' ) : void {
if ( this . autoScrollAnimationId !== null ) return ;
2025-09-21 15:48:13 +02:00
2025-08-27 22:50:13 +02:00
const scroll = ( ) = > {
2025-09-21 15:48:13 +02:00
if ( ! this . cachedElements . scrollContainer || ! this . draggedElement ) {
2025-08-27 22:50:13 +02:00
this . stopAutoScroll ( ) ;
return ;
}
2025-09-21 15:48:13 +02:00
2025-08-27 22:50:13 +02:00
const scrollAmount = direction === 'up' ? - this . scrollSpeed : this.scrollSpeed ;
2025-09-03 19:05:03 +02:00
this . cachedElements . scrollContainer . scrollTop += scrollAmount ;
2025-09-21 15:48:13 +02:00
2025-09-03 20:13:56 +02:00
// Emit updated position during scroll - adjust for scroll movement
2025-09-21 15:48:13 +02:00
if ( this . draggedElement ) {
2025-09-03 20:13:56 +02:00
// 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 ) ;
if ( columnElement ) {
const columnRect = columnElement . getBoundingClientRect ( ) ;
2025-09-03 20:48:23 +02:00
// Calculate free position relative to column, accounting for scroll movement (no snapping during scroll)
2025-09-03 20:13:56 +02:00
const relativeY = this . currentMouseY - columnRect . top - this . mouseOffset . y ;
2025-09-03 20:48:23 +02:00
const freeY = Math . max ( 0 , relativeY ) ;
2025-09-21 15:48:13 +02:00
2025-09-03 20:13:56 +02:00
this . eventBus . emit ( 'drag:auto-scroll' , {
2025-09-21 15:48:13 +02:00
draggedElement : this.draggedElement ,
2025-09-03 20:48:23 +02:00
snappedY : freeY , // Actually free position during scroll
2025-09-03 20:13:56 +02:00
scrollTop : this.cachedElements.scrollContainer.scrollTop
} ) ;
}
2025-08-27 22:50:13 +02:00
}
2025-09-21 15:48:13 +02:00
2025-08-27 22:50:13 +02:00
this . autoScrollAnimationId = requestAnimationFrame ( scroll ) ;
} ;
2025-09-21 15:48:13 +02:00
2025-08-27 22:50:13 +02:00
this . autoScrollAnimationId = requestAnimationFrame ( scroll ) ;
}
2025-09-21 15:48:13 +02:00
2025-08-27 22:50:13 +02:00
/ * *
* Stop auto - scroll animation
* /
private stopAutoScroll ( ) : void {
if ( this . autoScrollAnimationId !== null ) {
cancelAnimationFrame ( this . autoScrollAnimationId ) ;
this . autoScrollAnimationId = null ;
}
}
2025-09-21 15:48:13 +02:00
2025-08-27 22:50:13 +02:00
/ * *
2025-09-03 19:05:03 +02:00
* Clean up drag state
* /
private cleanupDragState ( ) : void {
2025-09-21 15:48:13 +02:00
this . draggedElement = null ;
2025-09-26 22:53:49 +02:00
this . draggedClone = null ;
2025-09-03 19:05:03 +02:00
this . currentColumn = null ;
2025-09-09 14:35:21 +02:00
this . isDragStarted = false ;
2025-09-21 15:48:13 +02:00
this . isInHeader = false ;
2025-09-03 19:05:03 +02:00
// Clear cached elements
this . cachedElements . currentColumn = null ;
this . cachedElements . lastColumnDate = null ;
}
2025-09-20 09:40:56 +02:00
/ * *
* Detect drop target - whether dropped in swp - day - column or swp - day - header
* /
private detectDropTarget ( position : Position ) : 'swp-day-column' | 'swp-day-header' | null {
2025-09-27 15:01:22 +02:00
2025-09-20 09:40:56 +02:00
// Traverse up the DOM tree to find the target container
2025-09-27 15:01:22 +02:00
let currentElement = this . draggedClone ;
2025-09-20 09:40:56 +02:00
while ( currentElement && currentElement !== document . body ) {
2025-09-27 15:01:22 +02:00
if ( currentElement . tagName === 'SWP-ALLDAY-CONTAINER' ) {
2025-09-20 09:40:56 +02:00
return 'swp-day-header' ;
}
if ( currentElement . tagName === 'SWP-DAY-COLUMN' ) {
return 'swp-day-column' ;
}
currentElement = currentElement . parentElement as HTMLElement ;
}
return null ;
}
2025-09-21 15:48:13 +02:00
/ * *
* Check for header enter / leave during drag operations
* /
private checkHeaderEnterLeave ( event : MouseEvent ) : void {
const elementAtPosition = document . elementFromPoint ( event . clientX , event . clientY ) ;
if ( ! elementAtPosition ) return ;
// Check if we're in a header area
const headerElement = elementAtPosition . closest ( 'swp-day-header, swp-calendar-header' ) ;
const isCurrentlyInHeader = ! ! headerElement ;
// Detect header enter
if ( ! this . isInHeader && isCurrentlyInHeader ) {
this . isInHeader = true ;
// Calculate target date using existing method
2025-09-26 22:11:57 +02:00
const targetDate = ColumnDetectionUtils . getColumnDateFromX ( event . clientX ) ;
2025-09-21 15:48:13 +02:00
if ( targetDate ) {
console . log ( '🎯 DragDropManager: Emitting drag:mouseenter-header' , { targetDate } ) ;
// 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 ,
mousePosition : { x : event.clientX , y : event.clientY } ,
originalElement : this.draggedElement ,
cloneElement : cloneElement
} ;
this . eventBus . emit ( 'drag:mouseenter-header' , dragMouseEnterPayload ) ;
}
}
// Detect header leave
if ( this . isInHeader && ! isCurrentlyInHeader ) {
this . isInHeader = false ;
console . log ( '🚪 DragDropManager: Emitting drag:mouseleave-header' ) ;
// Calculate target date using existing method
2025-09-26 22:11:57 +02:00
const targetDate = ColumnDetectionUtils . getColumnDateFromX ( event . clientX ) ;
2025-09-21 15:48:13 +02:00
// 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 ,
mousePosition : { x : event.clientX , y : event.clientY } ,
originalElement : this.draggedElement ,
cloneElement : cloneElement
} ;
this . eventBus . emit ( 'drag:mouseleave-header' , dragMouseLeavePayload ) ;
}
}
2025-09-03 19:05:03 +02:00
/ * *
* Clean up all resources and event listeners
2025-08-27 22:50:13 +02:00
* /
public destroy ( ) : void {
this . stopAutoScroll ( ) ;
2025-09-21 15:48:13 +02:00
2025-09-03 19:05:03 +02:00
// 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 ) ;
2025-09-21 15:48:13 +02:00
2025-09-03 19:05:03 +02:00
// Clear all cached elements
this . cachedElements . scrollContainer = null ;
this . cachedElements . currentColumn = null ;
this . cachedElements . lastColumnDate = null ;
2025-09-21 15:48:13 +02:00
2025-09-03 19:05:03 +02:00
// Clean up drag state
this . cleanupDragState ( ) ;
2025-08-27 22:50:13 +02:00
}
2025-09-21 15:48:13 +02:00
}