Improves edge scroll detection by listening for actual scroll events instead of relying on mouse position. This change ensures that the 'edgescroll:started' event is only emitted when scrolling has actually begun, preventing false positives and improving the accuracy of scroll compensation. It also removes the unnecessary scroll listener from the DragDropManager, consolidating scroll handling in the EdgeScrollManager.
676 lines
22 KiB
TypeScript
676 lines
22 KiB
TypeScript
/**
|
|
* 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 { PositionUtils } from '../utils/PositionUtils';
|
|
import { ColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
|
|
import { SwpEventElement, BaseSwpEventElement } from '../elements/SwpEventElement';
|
|
import {
|
|
DragStartEventPayload,
|
|
DragMoveEventPayload,
|
|
DragEndEventPayload,
|
|
DragMouseEnterHeaderEventPayload,
|
|
DragMouseLeaveHeaderEventPayload,
|
|
DragMouseEnterColumnEventPayload,
|
|
DragColumnChangeEventPayload
|
|
} from '../types/EventTypes';
|
|
import { MousePosition } from '../types/DragDropTypes';
|
|
import { CoreEvents } from '../constants/CoreEvents';
|
|
|
|
export class DragDropManager {
|
|
private eventBus: IEventBus;
|
|
|
|
// Mouse tracking with optimized state
|
|
private mouseDownPosition: MousePosition = { x: 0, y: 0 };
|
|
private currentMousePosition: MousePosition = { x: 0, y: 0 };
|
|
private mouseOffset: MousePosition = { x: 0, y: 0 };
|
|
|
|
// Drag state
|
|
private originalElement!: HTMLElement | null;
|
|
private draggedClone!: HTMLElement | null;
|
|
private currentColumn: ColumnBounds | null = null;
|
|
private previousColumn: ColumnBounds | null = null;
|
|
private isDragStarted = false;
|
|
|
|
// Hover state
|
|
private isHoverTrackingActive = false;
|
|
private currentHoveredEvent: HTMLElement | null = null;
|
|
|
|
// Movement threshold to distinguish click from drag
|
|
private readonly dragThreshold = 5; // pixels
|
|
|
|
// Scroll compensation
|
|
private scrollableContent: HTMLElement | null = null;
|
|
private initialScrollTop = 0;
|
|
private initialCloneTop = 0;
|
|
private isScrollCompensating = false; // Track if scroll compensation is active
|
|
private scrollListener: ((e: Event) => void) | null = null;
|
|
|
|
// Smooth drag animation
|
|
private dragAnimationId: number | null = null;
|
|
private targetY = 0;
|
|
private currentY = 0;
|
|
private targetColumn: ColumnBounds | null = null;
|
|
|
|
constructor(eventBus: IEventBus) {
|
|
this.eventBus = eventBus;
|
|
// Get config values
|
|
const gridSettings = calendarConfig.getGridSettings();
|
|
|
|
|
|
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* Initialize with optimized event listener setup
|
|
*/
|
|
private init(): void {
|
|
// Add event listeners
|
|
document.body.addEventListener('mousemove', this.handleMouseMove.bind(this));
|
|
document.body.addEventListener('mousedown', this.handleMouseDown.bind(this));
|
|
document.body.addEventListener('mouseup', this.handleMouseUp.bind(this));
|
|
|
|
const calendarContainer = document.querySelector('swp-calendar-container');
|
|
|
|
if (calendarContainer) {
|
|
calendarContainer.addEventListener('mouseleave', () => {
|
|
if (this.originalElement && this.isDragStarted) {
|
|
this.cancelDrag();
|
|
}
|
|
});
|
|
|
|
// Event delegation for header enter/leave
|
|
calendarContainer.addEventListener('mouseenter', (e) => {
|
|
const target = e.target as HTMLElement;
|
|
if (target.closest('swp-calendar-header')) {
|
|
this.handleHeaderMouseEnter(e as MouseEvent);
|
|
} else if (target.closest('swp-day-column')) {
|
|
this.handleColumnMouseEnter(e as MouseEvent);
|
|
} else if (target.closest('swp-event')) {
|
|
this.handleEventMouseEnter(e as MouseEvent);
|
|
}
|
|
}, true); // Use capture phase
|
|
|
|
calendarContainer.addEventListener('mouseleave', (e) => {
|
|
const target = e.target as HTMLElement;
|
|
if (target.closest('swp-calendar-header')) {
|
|
this.handleHeaderMouseLeave(e as MouseEvent);
|
|
}
|
|
// Don't handle swp-event mouseleave here - let mousemove handle it
|
|
}, true); // Use capture phase
|
|
}
|
|
|
|
// Initialize column bounds cache
|
|
ColumnDetectionUtils.updateColumnBoundsCache();
|
|
|
|
|
|
|
|
|
|
// Listen to resize events to update cache
|
|
window.addEventListener('resize', () => {
|
|
ColumnDetectionUtils.updateColumnBoundsCache();
|
|
});
|
|
|
|
// Listen to navigation events to update cache
|
|
this.eventBus.on('navigation:completed', () => {
|
|
ColumnDetectionUtils.updateColumnBoundsCache();
|
|
});
|
|
|
|
this.eventBus.on(CoreEvents.GRID_RENDERED, (event: Event) => {
|
|
this.handleGridRendered(event as CustomEvent);
|
|
});
|
|
|
|
// Listen to edge-scroll events to control scroll compensation
|
|
this.eventBus.on('edgescroll:started', () => {
|
|
this.isScrollCompensating = true;
|
|
console.log('🎬 DragDropManager: Edge-scroll started - disabling continueDrag()');
|
|
});
|
|
|
|
this.eventBus.on('edgescroll:stopped', () => {
|
|
this.isScrollCompensating = false;
|
|
console.log('🛑 DragDropManager: Edge-scroll stopped - enabling continueDrag()');
|
|
});
|
|
|
|
}
|
|
private handleGridRendered(event: CustomEvent) {
|
|
this.scrollableContent = document.querySelector('swp-scrollable-content');
|
|
this.scrollableContent!.addEventListener('scroll', this.handleScroll.bind(this), { passive: true });
|
|
}
|
|
|
|
private handleMouseDown(event: MouseEvent): void {
|
|
|
|
// Clean up drag state first
|
|
this.cleanupDragState();
|
|
ColumnDetectionUtils.updateColumnBoundsCache();
|
|
//this.lastMousePosition = { x: event.clientX, y: event.clientY };
|
|
//this.initialMousePosition = { x: event.clientX, y: event.clientY };
|
|
|
|
// Check if mousedown is on an event
|
|
const target = event.target as HTMLElement;
|
|
if (target.closest('swp-resize-handle')) return;
|
|
|
|
let eventElement = target;
|
|
|
|
while (eventElement && eventElement.tagName !== 'SWP-GRID-CONTAINER') {
|
|
if (eventElement.tagName === 'SWP-EVENT' || eventElement.tagName === 'SWP-ALLDAY-EVENT') {
|
|
break;
|
|
}
|
|
eventElement = eventElement.parentElement as HTMLElement;
|
|
if (!eventElement) return;
|
|
}
|
|
|
|
if (eventElement) {
|
|
|
|
// Normal drag - prepare for potential dragging
|
|
this.originalElement = eventElement;
|
|
// Calculate mouse offset within event
|
|
const eventRect = eventElement.getBoundingClientRect();
|
|
this.mouseOffset = {
|
|
x: event.clientX - eventRect.left,
|
|
y: event.clientY - eventRect.top
|
|
};
|
|
this.mouseDownPosition = { x: event.clientX, y: event.clientY };
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Optimized mouse move handler with consolidated position calculations
|
|
*/
|
|
private handleMouseMove(event: MouseEvent): void {
|
|
console.log('handleMouseMove', event)
|
|
if (this.isScrollCompensating) return;
|
|
//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) {
|
|
const currentPosition: MousePosition = { x: event.clientX, y: event.clientY };
|
|
this.currentMousePosition = currentPosition; // Track current mouse position
|
|
|
|
// Try to initialize drag if not started
|
|
if (!this.isDragStarted && this.originalElement) {
|
|
if (!this.initializeDrag(currentPosition)) {
|
|
return; // Not enough movement yet
|
|
}
|
|
}
|
|
|
|
// Continue drag if started //TODO: This has to be fixed... it fires way too many events, we can do better
|
|
if (this.isDragStarted && this.originalElement && this.draggedClone) {
|
|
//console.log("Continue drag if started", this.draggedClone);
|
|
this.continueDrag(currentPosition);
|
|
this.detectColumnChange(currentPosition);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Try to initialize drag based on movement threshold
|
|
* Returns true if drag was initialized, false if not enough movement
|
|
*/
|
|
private initializeDrag(currentPosition: MousePosition): boolean {
|
|
const deltaX = Math.abs(currentPosition.x - this.mouseDownPosition.x);
|
|
const deltaY = Math.abs(currentPosition.y - this.mouseDownPosition.y);
|
|
const totalMovement = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
|
|
if (totalMovement < this.dragThreshold) {
|
|
return false; // Not enough movement
|
|
}
|
|
|
|
// Start drag
|
|
this.isDragStarted = true;
|
|
|
|
|
|
|
|
// Set high z-index on event-group if exists, otherwise on event itself
|
|
const eventGroup = this.originalElement!.closest<HTMLElement>('swp-event-group');
|
|
if (eventGroup) {
|
|
eventGroup.style.zIndex = '9999';
|
|
} else {
|
|
this.originalElement!.style.zIndex = '9999';
|
|
}
|
|
|
|
const originalElement = this.originalElement as BaseSwpEventElement;
|
|
this.currentColumn = ColumnDetectionUtils.getColumnBounds(currentPosition);
|
|
this.draggedClone = originalElement.createClone();
|
|
|
|
const dragStartPayload: DragStartEventPayload = {
|
|
originalElement: this.originalElement!,
|
|
draggedClone: this.draggedClone,
|
|
mousePosition: this.mouseDownPosition,
|
|
mouseOffset: this.mouseOffset,
|
|
columnBounds: this.currentColumn
|
|
};
|
|
this.eventBus.emit('drag:start', dragStartPayload);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
private continueDrag(currentPosition: MousePosition): void {
|
|
|
|
if (!this.draggedClone!.hasAttribute("data-allday")) {
|
|
// Calculate raw position from mouse (no snapping)
|
|
const column = ColumnDetectionUtils.getColumnBounds(currentPosition);
|
|
|
|
if (column) {
|
|
// Calculate raw Y position relative to column (accounting for mouse offset)
|
|
const columnRect = column.boundingClientRect;
|
|
const eventTopY = currentPosition.y - columnRect.top - this.mouseOffset.y;
|
|
this.targetY = Math.max(0, eventTopY);
|
|
this.targetColumn = column;
|
|
|
|
// Start animation loop if not already running
|
|
if (this.dragAnimationId === null) {
|
|
this.currentY = parseFloat(this.draggedClone!.style.top) || 0;
|
|
this.animateDrag();
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Detect column change and emit event
|
|
*/
|
|
private detectColumnChange(currentPosition: MousePosition): void {
|
|
const newColumn = ColumnDetectionUtils.getColumnBounds(currentPosition);
|
|
if (newColumn == null) return;
|
|
|
|
if (newColumn.index !== this.currentColumn?.index) {
|
|
this.previousColumn = this.currentColumn;
|
|
this.currentColumn = newColumn;
|
|
|
|
const dragColumnChangePayload: DragColumnChangeEventPayload = {
|
|
originalElement: this.originalElement!,
|
|
draggedClone: this.draggedClone!,
|
|
previousColumn: this.previousColumn,
|
|
newColumn,
|
|
mousePosition: currentPosition
|
|
};
|
|
this.eventBus.emit('drag:column-change', dragColumnChangePayload);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Optimized mouse up handler with consolidated cleanup
|
|
*/
|
|
private handleMouseUp(event: MouseEvent): void {
|
|
this.stopDragAnimation();
|
|
|
|
if (this.originalElement) {
|
|
|
|
// Only emit drag:end if drag was actually started
|
|
if (this.isDragStarted) {
|
|
const mousePosition: MousePosition = { x: event.clientX, y: event.clientY };
|
|
|
|
// Snap to grid on mouse up (like ResizeHandleManager)
|
|
const column = ColumnDetectionUtils.getColumnBounds(mousePosition);
|
|
|
|
if (!column) return;
|
|
|
|
// Get current position and snap it to grid
|
|
const snappedY = this.calculateSnapPosition(mousePosition.y, column);
|
|
|
|
// Update clone to snapped position immediately
|
|
if (this.draggedClone) {
|
|
this.draggedClone.style.top = `${snappedY}px`;
|
|
}
|
|
|
|
// Detect drop target (swp-day-column or swp-day-header)
|
|
const dropTarget = this.detectDropTarget(mousePosition);
|
|
|
|
if (!dropTarget)
|
|
throw "dropTarget is null";
|
|
|
|
const dragEndPayload: DragEndEventPayload = {
|
|
originalElement: this.originalElement,
|
|
draggedClone: this.draggedClone,
|
|
mousePosition,
|
|
sourceColumn: this.previousColumn!!,
|
|
finalPosition: { column, snappedY }, // Where drag ended
|
|
target: dropTarget
|
|
};
|
|
|
|
console.log('DragEndEventPayload', dragEndPayload);
|
|
|
|
this.eventBus.emit('drag:end', dragEndPayload);
|
|
|
|
this.cleanupDragState();
|
|
|
|
} else {
|
|
// This was just a click - emit click event instead
|
|
this.eventBus.emit('event:click', {
|
|
clickedElement: this.originalElement,
|
|
mousePosition: { x: event.clientX, y: event.clientY }
|
|
});
|
|
}
|
|
}
|
|
}
|
|
// 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"]');
|
|
|
|
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.originalElement) return;
|
|
|
|
console.log('🚫 DragDropManager: Cancelling drag - mouse left grid container');
|
|
|
|
this.cleanupAllClones();
|
|
|
|
this.originalElement.style.opacity = '';
|
|
this.originalElement.style.cursor = '';
|
|
|
|
this.eventBus.emit('drag:cancelled', {
|
|
originalElement: this.originalElement,
|
|
reason: 'mouse-left-grid'
|
|
});
|
|
|
|
this.cleanupDragState();
|
|
this.stopDragAnimation();
|
|
}
|
|
|
|
/**
|
|
* Optimized snap position calculation using PositionUtils
|
|
*/
|
|
private calculateSnapPosition(mouseY: number, column: ColumnBounds): number {
|
|
// Calculate where the event top would be (accounting for mouse offset)
|
|
const eventTopY = mouseY - this.mouseOffset.y;
|
|
|
|
// Snap the event top position, not the mouse position
|
|
const snappedY = PositionUtils.getPositionFromCoordinate(eventTopY, column);
|
|
|
|
return Math.max(0, snappedY);
|
|
}
|
|
|
|
/**
|
|
* Smooth drag animation using requestAnimationFrame
|
|
* Emits drag:move events with current draggedClone reference on each frame
|
|
*/
|
|
private animateDrag(): void { //TODO: this can be optimized... WAIT !!!
|
|
|
|
if (!this.isDragStarted || !this.draggedClone || !this.targetColumn) {
|
|
this.dragAnimationId = null;
|
|
return;
|
|
}
|
|
|
|
// Smooth interpolation towards target
|
|
const diff = this.targetY - this.currentY;
|
|
const step = diff * 0.3; // 30% of distance per frame
|
|
|
|
// Update if difference is significant
|
|
if (Math.abs(diff) > 0.5) {
|
|
this.currentY += step;
|
|
|
|
// Emit drag:move event with current draggedClone reference
|
|
const dragMovePayload: DragMoveEventPayload = {
|
|
originalElement: this.originalElement!,
|
|
draggedClone: this.draggedClone, // Always uses current reference
|
|
mousePosition: this.currentMousePosition, // Use current mouse position!
|
|
snappedY: this.currentY,
|
|
columnBounds: this.targetColumn,
|
|
mouseOffset: this.mouseOffset
|
|
};
|
|
this.eventBus.emit('drag:move', dragMovePayload);
|
|
|
|
this.dragAnimationId = requestAnimationFrame(() => this.animateDrag());
|
|
} else {
|
|
// Close enough - snap to target
|
|
this.currentY = this.targetY;
|
|
|
|
// Emit final position
|
|
const dragMovePayload: DragMoveEventPayload = {
|
|
originalElement: this.originalElement!,
|
|
draggedClone: this.draggedClone,
|
|
mousePosition: this.currentMousePosition, // Use current mouse position!
|
|
snappedY: this.currentY,
|
|
columnBounds: this.targetColumn,
|
|
mouseOffset: this.mouseOffset
|
|
};
|
|
this.eventBus.emit('drag:move', dragMovePayload);
|
|
|
|
this.dragAnimationId = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle scroll during drag - compensate clone position
|
|
*/
|
|
private handleScroll(): void {
|
|
if (!this.isDragStarted || !this.draggedClone || !this.scrollableContent || !this.isScrollCompensating) return;
|
|
|
|
// First time scrolling - save initial positions NOW!
|
|
|
|
this.initialScrollTop = this.scrollableContent.scrollTop;
|
|
this.initialCloneTop = parseFloat(this.draggedClone.style.top || '0');
|
|
|
|
console.log('💾 DragDropManager: Scroll compensation started', {
|
|
initialScrollTop: this.initialScrollTop,
|
|
initialCloneTop: this.initialCloneTop
|
|
});
|
|
|
|
|
|
const currentScrollTop = this.scrollableContent.scrollTop;
|
|
const totalScrollDelta = currentScrollTop - this.initialScrollTop;
|
|
|
|
// Beregn ny position baseret på initial position + total scroll delta
|
|
const newTop = this.initialCloneTop + totalScrollDelta;
|
|
this.draggedClone.style.top = `${newTop}px`;
|
|
|
|
console.log('📜 DragDropManager: Scroll compensation', {
|
|
initialScrollTop: this.initialScrollTop,
|
|
currentScrollTop,
|
|
totalScrollDelta,
|
|
initialCloneTop: this.initialCloneTop,
|
|
newTop
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Stop drag animation
|
|
*/
|
|
private stopDragAnimation(): void {
|
|
if (this.dragAnimationId !== null) {
|
|
cancelAnimationFrame(this.dragAnimationId);
|
|
this.dragAnimationId = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean up drag state
|
|
*/
|
|
private cleanupDragState(): void {
|
|
this.previousColumn = null;
|
|
this.originalElement = null;
|
|
this.draggedClone = null;
|
|
this.currentColumn = null;
|
|
this.isDragStarted = false;
|
|
}
|
|
|
|
/**
|
|
* Detect drop target - whether dropped in swp-day-column or swp-day-header
|
|
*/
|
|
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) {
|
|
if (currentElement.tagName === 'SWP-ALLDAY-CONTAINER') {
|
|
return 'swp-day-header';
|
|
}
|
|
if (currentElement.tagName === 'SWP-DAY-COLUMN') {
|
|
return 'swp-day-column';
|
|
}
|
|
currentElement = currentElement.parentElement as HTMLElement;
|
|
}
|
|
|
|
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
|
|
*/
|
|
private handleHeaderMouseEnter(event: MouseEvent): void {
|
|
// Only handle if we're dragging a timed event (not all-day)
|
|
if (!this.isDragStarted || !this.draggedClone) {
|
|
return;
|
|
}
|
|
|
|
const position: MousePosition = { x: event.clientX, y: event.clientY };
|
|
const targetColumn = ColumnDetectionUtils.getColumnBounds(position);
|
|
|
|
if (targetColumn) {
|
|
const calendarEvent = SwpEventElement.extractCalendarEventFromElement(this.draggedClone);
|
|
|
|
const dragMouseEnterPayload: DragMouseEnterHeaderEventPayload = {
|
|
targetColumn: targetColumn,
|
|
mousePosition: position,
|
|
originalElement: this.originalElement,
|
|
draggedClone: this.draggedClone,
|
|
calendarEvent: calendarEvent,
|
|
replaceClone: (newClone: HTMLElement) => {
|
|
this.draggedClone = newClone;
|
|
this.dragAnimationId === null;
|
|
}
|
|
};
|
|
console.log('DragMouseEnterHeaderEventPayload', dragMouseEnterPayload);
|
|
this.eventBus.emit('drag:mouseenter-header', dragMouseEnterPayload);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle mouse enter on day column - for converting all-day to timed events
|
|
*/
|
|
private handleColumnMouseEnter(event: MouseEvent): void {
|
|
// Only handle if we're dragging an all-day event
|
|
if (!this.isDragStarted || !this.draggedClone || !this.draggedClone.hasAttribute('data-allday')) {
|
|
return;
|
|
}
|
|
|
|
console.log('🎯 DragDropManager: Mouse entered day column');
|
|
|
|
const position: MousePosition = { x: event.clientX, y: event.clientY };
|
|
const targetColumn = ColumnDetectionUtils.getColumnBounds(position);
|
|
|
|
if (!targetColumn) {
|
|
console.warn("No column detected when entering day column");
|
|
return;
|
|
}
|
|
|
|
// Calculate snapped Y position
|
|
const snappedY = this.calculateSnapPosition(position.y, targetColumn);
|
|
|
|
// Extract CalendarEvent from the dragged clone
|
|
const calendarEvent = SwpEventElement.extractCalendarEventFromElement(this.draggedClone);
|
|
|
|
const dragMouseEnterPayload: DragMouseEnterColumnEventPayload = {
|
|
targetColumn: targetColumn,
|
|
mousePosition: position,
|
|
snappedY: snappedY,
|
|
originalElement: this.originalElement,
|
|
draggedClone: this.draggedClone,
|
|
calendarEvent: calendarEvent,
|
|
replaceClone: (newClone: HTMLElement) => {
|
|
this.draggedClone = newClone;
|
|
this.dragAnimationId === null;
|
|
this.stopDragAnimation();
|
|
}
|
|
};
|
|
this.eventBus.emit('drag:mouseenter-column', dragMouseEnterPayload);
|
|
}
|
|
|
|
/**
|
|
* Handle mouse leave from calendar header - simplified using native events
|
|
*/
|
|
private handleHeaderMouseLeave(event: MouseEvent): void {
|
|
// Only handle if we're dragging an all-day event
|
|
if (!this.isDragStarted || !this.draggedClone || !this.draggedClone.hasAttribute("data-allday")) {
|
|
return;
|
|
}
|
|
|
|
console.log('🚪 DragDropManager: Mouse left header');
|
|
|
|
const position: MousePosition = { x: event.clientX, y: event.clientY };
|
|
const targetColumn = ColumnDetectionUtils.getColumnBounds(position);
|
|
|
|
if (!targetColumn) {
|
|
console.warn("No column detected when leaving header");
|
|
return;
|
|
}
|
|
|
|
const dragMouseLeavePayload: DragMouseLeaveHeaderEventPayload = {
|
|
targetDate: targetColumn.date,
|
|
mousePosition: position,
|
|
originalElement: this.originalElement,
|
|
draggedClone: this.draggedClone
|
|
};
|
|
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;
|
|
}
|
|
}
|
|
}
|