Refines drag and drop functionality by calculating position relative to the column during drag and snapping to the grid on mouse up, resulting in more precise placement. Addresses an issue where the dragged element's position was not correctly calculated relative to the target column.
692 lines
23 KiB
TypeScript
692 lines
23 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,
|
|
DragColumnChangeEventPayload
|
|
} from '../types/EventTypes';
|
|
import { MousePosition } from '../types/DragDropTypes';
|
|
|
|
interface CachedElements {
|
|
scrollContainer: HTMLElement | null;
|
|
}
|
|
|
|
|
|
|
|
|
|
export class DragDropManager {
|
|
private eventBus: IEventBus;
|
|
|
|
// Mouse tracking with optimized state
|
|
private lastMousePosition: MousePosition = { x: 0, y: 0 };
|
|
private lastLoggedPosition: MousePosition = { x: 0, y: 0 };
|
|
private currentMouseY = 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 currentColumnBounds: 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
|
|
|
|
private scrollContainer!: HTMLElement | null;
|
|
// Cached DOM elements for performance
|
|
|
|
|
|
|
|
|
|
// Auto-scroll properties
|
|
private autoScrollAnimationId: number | null = null;
|
|
private readonly scrollSpeed = 10; // pixels per frame
|
|
private readonly scrollThreshold = 30; // pixels from edge
|
|
|
|
// Snap configuration
|
|
private snapIntervalMinutes = 15; // Default 15 minutes
|
|
private hourHeightPx: number; // Will be set from config
|
|
|
|
// Smooth drag animation
|
|
private dragAnimationId: number | null = null;
|
|
private targetY = 0;
|
|
private currentY = 0;
|
|
private targetColumn: ColumnBounds | null = null;
|
|
|
|
private get snapDistancePx(): number {
|
|
return (this.snapIntervalMinutes / 60) * this.hourHeightPx;
|
|
}
|
|
|
|
constructor(eventBus: IEventBus) {
|
|
this.eventBus = eventBus;
|
|
// Get config values
|
|
const gridSettings = calendarConfig.getGridSettings();
|
|
this.hourHeightPx = gridSettings.hourHeight;
|
|
this.snapIntervalMinutes = gridSettings.snapInterval;
|
|
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* Configure snap interval
|
|
*/
|
|
public setSnapInterval(minutes: number): void {
|
|
this.snapIntervalMinutes = minutes;
|
|
}
|
|
|
|
/**
|
|
* 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));
|
|
|
|
this.scrollContainer = document.querySelector('swp-scrollable-content') as HTMLElement;
|
|
const calendarContainer = document.querySelector('swp-calendar-container');
|
|
|
|
if (calendarContainer) {
|
|
calendarContainer.addEventListener('mouseleave', () => {
|
|
if (this.draggedElement && 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-event')) {
|
|
// Entered an event - activate hover tracking and set color
|
|
const eventElement = target.closest<HTMLElement>('swp-event');
|
|
const mouseEvent = e as MouseEvent;
|
|
|
|
// Only handle hover if mouse button is up
|
|
if (eventElement && !this.isDragStarted && mouseEvent.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');
|
|
}
|
|
}
|
|
}, 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();
|
|
});
|
|
|
|
}
|
|
|
|
private handleMouseDown(event: MouseEvent): void {
|
|
|
|
// Clean up drag state first
|
|
this.cleanupDragState();
|
|
ColumnDetectionUtils.updateColumnBoundsCache();
|
|
this.lastMousePosition = { x: event.clientX, y: event.clientY };
|
|
this.lastLoggedPosition = { 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;
|
|
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;
|
|
}
|
|
|
|
|
|
// Found an event - check if in resize zone first
|
|
if (eventElement) {
|
|
// Check if click is in bottom resize zone
|
|
const rect = eventElement.getBoundingClientRect();
|
|
const mouseY = event.clientY;
|
|
const distanceFromBottom = rect.bottom - mouseY;
|
|
const resizeZoneHeight = 15; // Match ResizeHandleManager
|
|
|
|
// If in resize zone, don't handle this - let ResizeHandleManager take over
|
|
if (distanceFromBottom >= 0 && distanceFromBottom <= resizeZoneHeight) {
|
|
return; // Exit early - this is a resize operation
|
|
}
|
|
|
|
// Normal drag - prepare for potential dragging
|
|
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
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Optimized mouse move handler with consolidated position calculations
|
|
*/
|
|
private handleMouseMove(event: MouseEvent): void {
|
|
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 };
|
|
|
|
// Check if we need to start drag (movement threshold)
|
|
if (!this.isDragStarted && this.draggedElement) {
|
|
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);
|
|
|
|
if (totalMovement >= this.dragThreshold) {
|
|
// Start drag - emit drag:start event
|
|
this.isDragStarted = true;
|
|
|
|
// Set high z-index on event-group if exists, otherwise on event itself
|
|
const eventGroup = this.draggedElement.closest<HTMLElement>('swp-event-group');
|
|
if (eventGroup) {
|
|
eventGroup.style.zIndex = '9999';
|
|
} else {
|
|
this.draggedElement.style.zIndex = '9999';
|
|
}
|
|
|
|
// Detect current column
|
|
this.currentColumnBounds = ColumnDetectionUtils.getColumnBounds(currentPosition);
|
|
|
|
// Cast to BaseSwpEventElement and create clone (works for both SwpEventElement and SwpAllDayEventElement)
|
|
const originalElement = this.draggedElement as BaseSwpEventElement;
|
|
this.draggedClone = originalElement.createClone();
|
|
|
|
const dragStartPayload: DragStartEventPayload = {
|
|
draggedElement: this.draggedElement,
|
|
draggedClone: this.draggedClone,
|
|
mousePosition: this.initialMousePosition,
|
|
mouseOffset: this.mouseOffset,
|
|
columnBounds: this.currentColumnBounds
|
|
};
|
|
this.eventBus.emit('drag:start', dragStartPayload);
|
|
} else {
|
|
// Not enough movement yet - don't start drag
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Continue with normal drag behavior only if drag has started
|
|
if (this.isDragStarted && this.draggedElement && this.draggedClone) {
|
|
if (!this.draggedElement.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); // Store raw Y as target (no snapping)
|
|
this.targetColumn = column;
|
|
|
|
// Start animation loop if not already running
|
|
if (this.dragAnimationId === null) {
|
|
this.currentY = parseFloat(this.draggedClone.style.top) || 0;
|
|
this.animateDrag();
|
|
}
|
|
}
|
|
|
|
// Check for auto-scroll
|
|
this.checkAutoScroll(currentPosition);
|
|
}
|
|
|
|
const newColumn = ColumnDetectionUtils.getColumnBounds(currentPosition);
|
|
if (newColumn == null)
|
|
return;
|
|
|
|
if (newColumn?.index !== this.currentColumnBounds?.index) {
|
|
const previousColumn = this.currentColumnBounds;
|
|
this.currentColumnBounds = newColumn;
|
|
|
|
const dragColumnChangePayload: DragColumnChangeEventPayload = {
|
|
originalElement: this.draggedElement,
|
|
draggedClone: this.draggedClone,
|
|
previousColumn,
|
|
newColumn,
|
|
mousePosition: currentPosition
|
|
};
|
|
this.eventBus.emit('drag:column-change', dragColumnChangePayload);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Optimized mouse up handler with consolidated cleanup
|
|
*/
|
|
private handleMouseUp(event: MouseEvent): void {
|
|
this.stopAutoScroll();
|
|
this.stopDragAnimation();
|
|
|
|
if (this.draggedElement) {
|
|
|
|
// 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) {
|
|
console.warn('No column detected on mouseUp');
|
|
return;
|
|
}
|
|
|
|
// Get current position and snap it to grid
|
|
const currentY = parseFloat(this.draggedClone?.style.top || '0');
|
|
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";
|
|
|
|
console.log('🎯 DragDropManager: Emitting drag:end', {
|
|
draggedElement: this.draggedElement.dataset.eventId,
|
|
finalColumn: column,
|
|
finalY: snappedY,
|
|
dropTarget: dropTarget,
|
|
isDragStarted: this.isDragStarted
|
|
});
|
|
|
|
const dragEndPayload: DragEndEventPayload = {
|
|
originalElement: this.draggedElement,
|
|
draggedClone: this.draggedClone,
|
|
mousePosition,
|
|
finalPosition: { column, snappedY },
|
|
target: dropTarget
|
|
};
|
|
this.eventBus.emit('drag:end', dragEndPayload);
|
|
|
|
this.cleanupDragState();
|
|
|
|
} else {
|
|
// This was just a click - emit click event instead
|
|
this.eventBus.emit('event:click', {
|
|
draggedElement: this.draggedElement,
|
|
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.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();
|
|
this.stopDragAnimation();
|
|
}
|
|
|
|
/**
|
|
* Consolidated position calculation method using PositionUtils
|
|
*/
|
|
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 };
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
private animateDrag(): void {
|
|
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 interpolated position
|
|
const dragMovePayload: DragMoveEventPayload = {
|
|
draggedElement: this.draggedElement!,
|
|
draggedClone: this.draggedClone,
|
|
mousePosition: this.lastMousePosition,
|
|
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;
|
|
|
|
const dragMovePayload: DragMoveEventPayload = {
|
|
draggedElement: this.draggedElement!,
|
|
draggedClone: this.draggedClone,
|
|
mousePosition: this.lastMousePosition,
|
|
snappedY: this.currentY,
|
|
columnBounds: this.targetColumn,
|
|
mouseOffset: this.mouseOffset
|
|
};
|
|
this.eventBus.emit('drag:move', dragMovePayload);
|
|
|
|
this.dragAnimationId = null;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Optimized auto-scroll check with cached container
|
|
*/
|
|
private checkAutoScroll(mousePosition: MousePosition): void {
|
|
|
|
if (this.scrollContainer == null)
|
|
return;
|
|
|
|
const containerRect = this.scrollContainer.getBoundingClientRect();
|
|
const mouseY = mousePosition.clientY;
|
|
|
|
// Calculate distances from edges
|
|
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', mousePosition);
|
|
} else if (distanceFromBottom <= this.scrollThreshold && distanceFromBottom > 0) {
|
|
this.startAutoScroll('down', mousePosition);
|
|
} else {
|
|
this.stopAutoScroll();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Optimized auto-scroll with cached container reference
|
|
*/
|
|
private startAutoScroll(direction: 'up' | 'down', event: MousePosition): void {
|
|
if (this.autoScrollAnimationId !== null) return;
|
|
|
|
const scroll = () => {
|
|
if (!this.scrollContainer || !this.draggedElement) {
|
|
this.stopAutoScroll();
|
|
return;
|
|
}
|
|
|
|
const scrollAmount = direction === 'up' ? -this.scrollSpeed : this.scrollSpeed;
|
|
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 = ColumnDetectionUtils.getColumnBounds(event);
|
|
|
|
if (columnElement) {
|
|
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);
|
|
|
|
this.eventBus.emit('drag:auto-scroll', {
|
|
draggedElement: this.draggedElement,
|
|
snappedY: freeY, // Actually free position during scroll
|
|
scrollTop: this.scrollContainer.scrollTop
|
|
});
|
|
}
|
|
}
|
|
|
|
this.autoScrollAnimationId = requestAnimationFrame(scroll);
|
|
};
|
|
|
|
this.autoScrollAnimationId = requestAnimationFrame(scroll);
|
|
}
|
|
|
|
/**
|
|
* Stop auto-scroll animation
|
|
*/
|
|
private stopAutoScroll(): void {
|
|
if (this.autoScrollAnimationId !== null) {
|
|
cancelAnimationFrame(this.autoScrollAnimationId);
|
|
this.autoScrollAnimationId = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop drag animation
|
|
*/
|
|
private stopDragAnimation(): void {
|
|
if (this.dragAnimationId !== null) {
|
|
cancelAnimationFrame(this.dragAnimationId);
|
|
this.dragAnimationId = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean up drag state
|
|
*/
|
|
private cleanupDragState(): void {
|
|
this.draggedElement = null;
|
|
this.draggedClone = 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 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) {
|
|
console.log('🎯 DragDropManager: Mouse entered header', { targetDate: targetColumn });
|
|
|
|
// Extract CalendarEvent from the dragged clone
|
|
const calendarEvent = SwpEventElement.extractCalendarEventFromElement(this.draggedClone);
|
|
|
|
const dragMouseEnterPayload: DragMouseEnterHeaderEventPayload = {
|
|
targetColumn: targetColumn,
|
|
mousePosition: position,
|
|
originalElement: this.draggedElement,
|
|
draggedClone: this.draggedClone,
|
|
calendarEvent: calendarEvent,
|
|
// Delegate pattern - allows AllDayManager to replace the clone
|
|
replaceClone: (newClone: HTMLElement) => {
|
|
this.draggedClone = newClone;
|
|
}
|
|
};
|
|
this.eventBus.emit('drag:mouseenter-header', 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.draggedElement,
|
|
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;
|
|
}
|
|
}
|
|
}
|