Improves drag-drop event system with type safety
Introduces dedicated TypeScript interfaces for all drag-and-drop event payloads, enhancing type safety and developer experience. Centralizes drag event detection and emission within `DragDropManager`. Refactors `AllDayManager`, `HeaderManager`, and `EventRendererManager` to subscribe to these typed events, improving decoupling and clarifying responsibilities. Resolves known inconsistencies in drag event payloads, especially for all-day event conversions. Adds a comprehensive analysis document (`docs/EventSystem-Analysis.md`) detailing the event system and planned improvements.
This commit is contained in:
parent
b4f5b29da3
commit
c7dcfbbaed
7 changed files with 583 additions and 410 deletions
|
|
@ -5,8 +5,14 @@
|
|||
|
||||
import { IEventBus } from '../types/CalendarTypes';
|
||||
import { calendarConfig } from '../core/CalendarConfig';
|
||||
import { DateCalculator } from '../utils/DateCalculator';
|
||||
import { PositionUtils } from '../utils/PositionUtils';
|
||||
import {
|
||||
DragStartEventPayload,
|
||||
DragMoveEventPayload,
|
||||
DragEndEventPayload,
|
||||
DragMouseEnterHeaderEventPayload,
|
||||
DragMouseLeaveHeaderEventPayload
|
||||
} from '../types/EventTypes';
|
||||
|
||||
interface CachedElements {
|
||||
scrollContainer: HTMLElement | null;
|
||||
|
|
@ -27,71 +33,72 @@ interface ColumnBounds {
|
|||
|
||||
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 currentMouseY = 0;
|
||||
private mouseOffset: Position = { x: 0, y: 0 };
|
||||
private initialMousePosition: Position = { x: 0, y: 0 };
|
||||
|
||||
|
||||
// Drag state
|
||||
private draggedEventId: string | null = null;
|
||||
private originalElement: HTMLElement | null = null;
|
||||
private draggedElement: HTMLElement | null = null ;
|
||||
private currentColumn: string | null = null;
|
||||
private isDragStarted = false;
|
||||
|
||||
|
||||
// Header tracking state
|
||||
private isInHeader = false;
|
||||
|
||||
// Movement threshold to distinguish click from drag
|
||||
private readonly dragThreshold = 5; // pixels
|
||||
|
||||
|
||||
// Cached DOM elements for performance
|
||||
private cachedElements: CachedElements = {
|
||||
scrollContainer: null,
|
||||
currentColumn: null,
|
||||
lastColumnDate: null
|
||||
};
|
||||
|
||||
|
||||
// Column bounds cache for coordinate-based column detection
|
||||
private columnBoundsCache: ColumnBounds[] = [];
|
||||
|
||||
|
||||
// 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
|
||||
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
*/
|
||||
|
|
@ -100,100 +107,32 @@ export class DragDropManager {
|
|||
document.body.addEventListener('mousemove', this.boundHandlers.mouseMove);
|
||||
document.body.addEventListener('mousedown', this.boundHandlers.mouseDown);
|
||||
document.body.addEventListener('mouseup', this.boundHandlers.mouseUp);
|
||||
|
||||
|
||||
// Initialize column bounds cache
|
||||
this.updateColumnBoundsCache();
|
||||
|
||||
|
||||
// Listen to resize events to update cache
|
||||
window.addEventListener('resize', () => {
|
||||
this.updateColumnBoundsCache();
|
||||
});
|
||||
|
||||
|
||||
// Listen to navigation events to update cache
|
||||
this.eventBus.on('navigation:completed', () => {
|
||||
this.updateColumnBoundsCache();
|
||||
});
|
||||
|
||||
// Listen for header mouseover events
|
||||
this.eventBus.on('header:mouseover', (event) => {
|
||||
const { targetDate, headerRenderer } = (event as CustomEvent).detail;
|
||||
|
||||
console.log('🎯 DragDropManager: Received header:mouseover', {
|
||||
targetDate,
|
||||
draggedEventId: this.draggedEventId,
|
||||
isDragging: !!this.draggedEventId
|
||||
});
|
||||
|
||||
if (this.draggedEventId && targetDate) {
|
||||
// Find dragget element dynamisk
|
||||
const draggedElement = document.querySelector(`swp-event[data-event-id="${this.draggedEventId}"]`);
|
||||
|
||||
console.log('🔍 DragDropManager: Looking for dragged element', {
|
||||
eventId: this.draggedEventId,
|
||||
found: !!draggedElement,
|
||||
tagName: draggedElement?.tagName
|
||||
});
|
||||
|
||||
if (draggedElement) {
|
||||
console.log('✅ DragDropManager: Converting to all-day for date:', targetDate);
|
||||
|
||||
// Element findes stadig som day-event, så konverter
|
||||
this.eventBus.emit('drag:convert-to-allday_event', {
|
||||
targetDate,
|
||||
originalElement: draggedElement,
|
||||
headerRenderer
|
||||
});
|
||||
} else {
|
||||
console.log('❌ DragDropManager: Dragged element not found');
|
||||
}
|
||||
} else {
|
||||
console.log('⏭️ DragDropManager: Skipping conversion - no drag or no target date');
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for column mouseover events (for all-day to timed conversion)
|
||||
this.eventBus.on('column:mouseover', (event) => {
|
||||
const { targetColumn, targetY } = (event as CustomEvent).detail;
|
||||
|
||||
if (this.draggedEventId && this.isAllDayEventBeingDragged()) {
|
||||
// Emit event to convert to timed
|
||||
this.eventBus.emit('drag:convert-to-timed', {
|
||||
eventId: this.draggedEventId,
|
||||
targetColumn,
|
||||
targetY
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for header mouseleave events (convert from all-day back to day)
|
||||
this.eventBus.on('header:mouseleave', (event) => {
|
||||
// Check if we're dragging ANY event
|
||||
if (this.draggedEventId) {
|
||||
const mousePosition = { x: this.lastMousePosition.x, y: this.lastMousePosition.y };
|
||||
const column = this.getColumnDateFromX(mousePosition.x);
|
||||
|
||||
// Find the actual dragged element
|
||||
const draggedElement = document.querySelector(`[data-event-id="${this.draggedEventId}"]`) as HTMLElement;
|
||||
|
||||
this.eventBus.emit('drag:convert-to-time_event', {
|
||||
draggedElement: draggedElement,
|
||||
mousePosition: mousePosition,
|
||||
column: column
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private handleMouseDown(event: MouseEvent): void {
|
||||
this.isDragStarted = false;
|
||||
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-EVENTS-LAYER') {
|
||||
if (eventElement.tagName === 'SWP-EVENT' || eventElement.tagName === 'SWP-ALLDAY-EVENT') {
|
||||
break;
|
||||
|
|
@ -201,96 +140,104 @@ export class DragDropManager {
|
|||
eventElement = eventElement.parentElement as HTMLElement;
|
||||
if (!eventElement) return;
|
||||
}
|
||||
|
||||
|
||||
// If we reached SWP-EVENTS-LAYER without finding an event, return
|
||||
if (!eventElement || eventElement.tagName === 'SWP-EVENTS-LAYER') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Found an event - prepare for potential dragging
|
||||
if (eventElement) {
|
||||
this.originalElement = eventElement;
|
||||
this.draggedEventId = eventElement.dataset.eventId || null;
|
||||
|
||||
this.draggedElement = eventElement;
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Optimized mouse move handler with consolidated position calculations
|
||||
*/
|
||||
private handleMouseMove(event: MouseEvent): void {
|
||||
this.currentMouseY = event.clientY;
|
||||
|
||||
if (event.buttons === 1 && this.draggedEventId) {
|
||||
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) {
|
||||
const currentPosition: Position = { x: event.clientX, y: event.clientY };
|
||||
|
||||
|
||||
// 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);
|
||||
|
||||
|
||||
if (totalMovement >= this.dragThreshold) {
|
||||
// Start drag - emit drag:start event
|
||||
this.isDragStarted = true;
|
||||
this.eventBus.emit('drag:start', {
|
||||
eventId: this.draggedEventId,
|
||||
|
||||
const dragStartPayload: DragStartEventPayload = {
|
||||
draggedElement: this.draggedElement,
|
||||
mousePosition: this.initialMousePosition,
|
||||
mouseOffset: this.mouseOffset,
|
||||
column: this.currentColumn
|
||||
});
|
||||
};
|
||||
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) {
|
||||
const deltaY = Math.abs(currentPosition.y - this.lastLoggedPosition.y);
|
||||
|
||||
|
||||
// Check for snap interval vertical movement (normal drag behavior)
|
||||
if (deltaY >= this.snapDistancePx) {
|
||||
this.lastLoggedPosition = currentPosition;
|
||||
|
||||
|
||||
// Consolidated position calculations with snapping for normal drag
|
||||
const positionData = this.calculateDragPosition(currentPosition);
|
||||
|
||||
|
||||
// Emit drag move event with snapped position (normal behavior)
|
||||
this.eventBus.emit('drag:move', {
|
||||
eventId: this.draggedEventId,
|
||||
const dragMovePayload: DragMoveEventPayload = {
|
||||
draggedElement: this.draggedElement,
|
||||
mousePosition: currentPosition,
|
||||
snappedY: positionData.snappedY,
|
||||
column: positionData.column,
|
||||
mouseOffset: this.mouseOffset
|
||||
});
|
||||
};
|
||||
this.eventBus.emit('drag:move', dragMovePayload);
|
||||
}
|
||||
|
||||
|
||||
// Check for auto-scroll
|
||||
this.checkAutoScroll(event);
|
||||
|
||||
|
||||
// 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,
|
||||
draggedElement: this.draggedElement,
|
||||
previousColumn,
|
||||
newColumn,
|
||||
mousePosition: currentPosition
|
||||
|
|
@ -299,81 +246,64 @@ export class DragDropManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Optimized mouse up handler with consolidated cleanup
|
||||
*/
|
||||
private handleMouseUp(event: MouseEvent): void {
|
||||
this.stopAutoScroll();
|
||||
|
||||
if (this.draggedEventId && this.originalElement) {
|
||||
|
||||
if (this.draggedElement) {
|
||||
// Store variables locally before cleanup
|
||||
const eventId = this.draggedEventId;
|
||||
const originalElement = this.originalElement;
|
||||
const draggedElement = this.draggedElement;
|
||||
const isDragStarted = this.isDragStarted;
|
||||
|
||||
|
||||
// Clean up drag state first
|
||||
this.cleanupDragState();
|
||||
|
||||
|
||||
// Only emit drag:end if drag was actually started
|
||||
if (isDragStarted) {
|
||||
const finalPosition: Position = { x: event.clientX, y: event.clientY };
|
||||
|
||||
const mousePosition: Position = { x: event.clientX, y: event.clientY };
|
||||
|
||||
// Use consolidated position calculation
|
||||
const positionData = this.calculateDragPosition(finalPosition);
|
||||
|
||||
const positionData = this.calculateDragPosition(mousePosition);
|
||||
|
||||
// Detect drop target (swp-day-column or swp-day-header)
|
||||
const dropTarget = this.detectDropTarget(finalPosition);
|
||||
|
||||
const dropTarget = this.detectDropTarget(mousePosition);
|
||||
|
||||
console.log('🎯 DragDropManager: Emitting drag:end', {
|
||||
eventId: eventId,
|
||||
draggedElement: draggedElement.dataset.eventId,
|
||||
finalColumn: positionData.column,
|
||||
finalY: positionData.snappedY,
|
||||
dropTarget: dropTarget,
|
||||
isDragStarted: isDragStarted
|
||||
});
|
||||
|
||||
this.eventBus.emit('drag:end', {
|
||||
eventId: eventId,
|
||||
finalPosition,
|
||||
finalColumn: positionData.column,
|
||||
finalY: positionData.snappedY,
|
||||
|
||||
const dragEndPayload: DragEndEventPayload = {
|
||||
draggedElement: draggedElement,
|
||||
mousePosition,
|
||||
finalPosition: positionData,
|
||||
target: dropTarget
|
||||
});
|
||||
};
|
||||
this.eventBus.emit('drag:end', dragEndPayload);
|
||||
} else {
|
||||
// This was just a click - emit click event instead
|
||||
this.eventBus.emit('event:click', {
|
||||
eventId: eventId,
|
||||
draggedElement: draggedElement,
|
||||
mousePosition: { x: event.clientX, y: event.clientY }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
return { column, snappedY };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate free position (follows mouse exactly)
|
||||
*/
|
||||
private calculateFreePosition(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 relativeY = PositionUtils.getPositionFromCoordinate(mouseY, columnElement);
|
||||
|
||||
// Return free position (no snapping)
|
||||
return Math.max(0, relativeY);
|
||||
return { column, snappedY };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -381,32 +311,32 @@ export class DragDropManager {
|
|||
*/
|
||||
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);
|
||||
|
||||
|
||||
return Math.max(0, snappedY);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update column bounds cache for coordinate-based column detection
|
||||
*/
|
||||
private updateColumnBoundsCache(): void {
|
||||
// Reset cache
|
||||
this.columnBoundsCache = [];
|
||||
|
||||
|
||||
// Find alle kolonner
|
||||
const columns = document.querySelectorAll('swp-day-column');
|
||||
|
||||
|
||||
// Cache hver kolonnes x-grænser
|
||||
columns.forEach(column => {
|
||||
const rect = column.getBoundingClientRect();
|
||||
const date = (column as HTMLElement).dataset.date;
|
||||
|
||||
|
||||
if (date) {
|
||||
this.columnBoundsCache.push({
|
||||
date,
|
||||
|
|
@ -415,10 +345,10 @@ export class DragDropManager {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Sorter efter x-position (fra venstre til højre)
|
||||
this.columnBoundsCache.sort((a, b) => a.left - b.left);
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -429,12 +359,12 @@ export class DragDropManager {
|
|||
if (this.columnBoundsCache.length === 0) {
|
||||
this.updateColumnBoundsCache();
|
||||
}
|
||||
|
||||
|
||||
// Find den kolonne hvor x-koordinaten er indenfor grænserne
|
||||
const column = this.columnBoundsCache.find(col =>
|
||||
x >= col.left && x <= col.right
|
||||
);
|
||||
|
||||
|
||||
return column ? column.date : null;
|
||||
}
|
||||
|
||||
|
|
@ -444,7 +374,7 @@ export class DragDropManager {
|
|||
private detectColumn(mouseX: number, mouseY: number): string | null {
|
||||
// Brug den koordinatbaserede metode direkte
|
||||
const columnDate = this.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;
|
||||
|
|
@ -453,7 +383,7 @@ export class DragDropManager {
|
|||
this.cachedElements.lastColumnDate = columnDate;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return columnDate;
|
||||
}
|
||||
|
||||
|
|
@ -468,7 +398,7 @@ export class DragDropManager {
|
|||
return this.cachedElements.lastColumnDate;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Cache miss - detect new column
|
||||
return this.detectColumn(mousePosition.x, mousePosition.y);
|
||||
}
|
||||
|
|
@ -478,22 +408,22 @@ export class DragDropManager {
|
|||
*/
|
||||
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
|
||||
*/
|
||||
|
|
@ -505,14 +435,14 @@ export class DragDropManager {
|
|||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const containerRect = this.cachedElements.scrollContainer.getBoundingClientRect();
|
||||
const mouseY = event.clientY;
|
||||
|
||||
|
||||
// Calculate distances from edges
|
||||
const distanceFromTop = mouseY - containerRect.top;
|
||||
const distanceFromBottom = containerRect.bottom - mouseY;
|
||||
|
||||
|
||||
// Check if we need to scroll
|
||||
if (distanceFromTop <= this.scrollThreshold && distanceFromTop > 0) {
|
||||
this.startAutoScroll('up');
|
||||
|
|
@ -522,24 +452,24 @@ export class DragDropManager {
|
|||
this.stopAutoScroll();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Optimized auto-scroll with cached container reference
|
||||
*/
|
||||
private startAutoScroll(direction: 'up' | 'down'): void {
|
||||
if (this.autoScrollAnimationId !== null) return;
|
||||
|
||||
|
||||
const scroll = () => {
|
||||
if (!this.cachedElements.scrollContainer || !this.draggedEventId) {
|
||||
if (!this.cachedElements.scrollContainer || !this.draggedElement) {
|
||||
this.stopAutoScroll();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const scrollAmount = direction === 'up' ? -this.scrollSpeed : this.scrollSpeed;
|
||||
this.cachedElements.scrollContainer.scrollTop += scrollAmount;
|
||||
|
||||
|
||||
// Emit updated position during scroll - adjust for scroll movement
|
||||
if (this.draggedEventId) {
|
||||
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);
|
||||
|
|
@ -548,21 +478,21 @@ export class DragDropManager {
|
|||
// 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', {
|
||||
eventId: this.draggedEventId,
|
||||
draggedElement: this.draggedElement,
|
||||
snappedY: freeY, // Actually free position during scroll
|
||||
scrollTop: this.cachedElements.scrollContainer.scrollTop
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.autoScrollAnimationId = requestAnimationFrame(scroll);
|
||||
};
|
||||
|
||||
|
||||
this.autoScrollAnimationId = requestAnimationFrame(scroll);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Stop auto-scroll animation
|
||||
*/
|
||||
|
|
@ -572,31 +502,21 @@ export class DragDropManager {
|
|||
this.autoScrollAnimationId = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clean up drag state
|
||||
*/
|
||||
private cleanupDragState(): void {
|
||||
this.draggedEventId = null;
|
||||
this.originalElement = null;
|
||||
this.draggedElement = null;
|
||||
this.currentColumn = null;
|
||||
this.isDragStarted = false;
|
||||
|
||||
this.isInHeader = false;
|
||||
|
||||
// Clear cached elements
|
||||
this.cachedElements.currentColumn = null;
|
||||
this.cachedElements.lastColumnDate = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an all-day event is currently being dragged
|
||||
*/
|
||||
private isAllDayEventBeingDragged(): boolean {
|
||||
if (!this.draggedEventId) return false;
|
||||
// Check if element exists as all-day event
|
||||
const allDayElement = document.querySelector(`swp-allday-event[data-event-id="${this.draggedEventId}"]`);
|
||||
return allDayElement !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect drop target - whether dropped in swp-day-column or swp-day-header
|
||||
*/
|
||||
|
|
@ -619,23 +539,81 @@ export class DragDropManager {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
const targetDate = this.getColumnDateFromX(event.clientX);
|
||||
|
||||
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
|
||||
const targetDate = this.getColumnDateFromX(event.clientX);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all resources and event listeners
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.stopAutoScroll();
|
||||
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue