Implements drag and drop functionality
Introduces a DragDropManager to handle event dragging and dropping, replacing the ColumnDetector. This change centralizes drag and drop logic, improving code organization and maintainability. The EventRenderer now uses the DragDropManager's events to visually update the calendar during drag operations. Removes ColumnDetector which is now replaced by the drag and drop manager.
This commit is contained in:
parent
be4a8af7c4
commit
f697944d75
4 changed files with 658 additions and 676 deletions
338
src/managers/DragDropManager.ts
Normal file
338
src/managers/DragDropManager.ts
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
/**
|
||||
* DragDropManager - Handles drag and drop interaction logic
|
||||
* Emits events for visual updates handled by EventRenderer
|
||||
*/
|
||||
|
||||
import { IEventBus } from '../types/CalendarTypes';
|
||||
import { CalendarConfig } from '../core/CalendarConfig';
|
||||
|
||||
export class DragDropManager {
|
||||
private eventBus: IEventBus;
|
||||
private config: CalendarConfig;
|
||||
|
||||
// Mouse tracking
|
||||
private isMouseDown = false;
|
||||
private lastMousePosition = { x: 0, y: 0 };
|
||||
private lastLoggedPosition = { x: 0, y: 0 };
|
||||
private currentMouseY = 0;
|
||||
private mouseOffset = { x: 0, y: 0 };
|
||||
|
||||
// Drag state
|
||||
private draggedEventId: string | null = null;
|
||||
private originalElement: HTMLElement | null = null;
|
||||
private currentColumn: string | null = null;
|
||||
|
||||
// Auto-scroll properties
|
||||
private scrollContainer: HTMLElement | null = null;
|
||||
private autoScrollAnimationId: number | null = null;
|
||||
private scrollSpeed = 10; // pixels per frame
|
||||
private scrollThreshold = 30; // pixels from edge
|
||||
|
||||
// Snap configuration
|
||||
private snapIntervalMinutes = 15; // Default 15 minutes
|
||||
private hourHeightPx = 60; // From CSS --hour-height
|
||||
|
||||
private get snapDistancePx(): number {
|
||||
return (this.snapIntervalMinutes / 60) * this.hourHeightPx;
|
||||
}
|
||||
|
||||
constructor(eventBus: IEventBus, config: CalendarConfig) {
|
||||
this.eventBus = eventBus;
|
||||
this.config = config;
|
||||
|
||||
// Get config values
|
||||
const gridSettings = config.getGridSettings();
|
||||
this.hourHeightPx = gridSettings.hourHeight;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure snap interval
|
||||
*/
|
||||
public setSnapInterval(minutes: number): void {
|
||||
this.snapIntervalMinutes = minutes;
|
||||
console.log(`DragDropManager: Snap interval set to ${minutes} minutes (${this.snapDistancePx}px)`);
|
||||
}
|
||||
|
||||
private init(): void {
|
||||
// Listen to mouse events on body
|
||||
document.body.addEventListener('mousemove', this.handleMouseMove.bind(this));
|
||||
document.body.addEventListener('mousedown', this.handleMouseDown.bind(this));
|
||||
document.body.addEventListener('mouseup', this.handleMouseUp.bind(this));
|
||||
|
||||
// Listen for header mouseover events
|
||||
this.eventBus.on('header:mouseover', (event) => {
|
||||
const { element, targetDate, headerRenderer } = (event as CustomEvent).detail;
|
||||
|
||||
if (this.isMouseDown && this.draggedEventId && targetDate) {
|
||||
// Emit event to convert to all-day
|
||||
this.eventBus.emit('drag:convert-to-allday', {
|
||||
eventId: this.draggedEventId,
|
||||
targetDate,
|
||||
element,
|
||||
headerRenderer
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private handleMouseDown(event: MouseEvent): void {
|
||||
this.isMouseDown = true;
|
||||
this.lastMousePosition = { x: event.clientX, y: event.clientY };
|
||||
this.lastLoggedPosition = { 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;
|
||||
}
|
||||
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 - start dragging
|
||||
if (eventElement) {
|
||||
this.originalElement = eventElement;
|
||||
this.draggedEventId = eventElement.dataset.eventId || null;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Emit drag start event
|
||||
this.eventBus.emit('drag:start', {
|
||||
originalElement: eventElement,
|
||||
eventId: this.draggedEventId,
|
||||
mousePosition: { x: event.clientX, y: event.clientY },
|
||||
mouseOffset: this.mouseOffset,
|
||||
column: this.currentColumn
|
||||
});
|
||||
|
||||
console.log('DragDropManager: Drag started', {
|
||||
eventId: this.draggedEventId,
|
||||
column: this.currentColumn
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private handleMouseMove(event: MouseEvent): void {
|
||||
this.currentMouseY = event.clientY;
|
||||
|
||||
if (this.isMouseDown && this.draggedEventId) {
|
||||
const deltaY = Math.abs(event.clientY - this.lastLoggedPosition.y);
|
||||
|
||||
// Check for snap interval vertical movement
|
||||
if (deltaY >= this.snapDistancePx) {
|
||||
this.lastLoggedPosition = { x: event.clientX, y: event.clientY };
|
||||
|
||||
// Calculate snapped position
|
||||
const column = this.detectColumn(event.clientX, event.clientY);
|
||||
const snappedY = this.calculateSnapPosition(event.clientY);
|
||||
|
||||
// Emit drag move event with snapped position
|
||||
this.eventBus.emit('drag:move', {
|
||||
eventId: this.draggedEventId,
|
||||
mousePosition: { x: event.clientX, y: event.clientY },
|
||||
snappedY,
|
||||
column,
|
||||
mouseOffset: this.mouseOffset
|
||||
});
|
||||
|
||||
console.log(`DragDropManager: Drag moved ${this.snapIntervalMinutes} minutes`, {
|
||||
snappedY,
|
||||
column
|
||||
});
|
||||
}
|
||||
|
||||
// Check for auto-scroll
|
||||
this.checkAutoScroll(event);
|
||||
|
||||
// Check for column change
|
||||
const newColumn = this.detectColumn(event.clientX, event.clientY);
|
||||
if (newColumn && newColumn !== this.currentColumn) {
|
||||
console.log(`DragDropManager: Column changed from ${this.currentColumn} to ${newColumn}`);
|
||||
this.currentColumn = newColumn;
|
||||
|
||||
this.eventBus.emit('drag:column-change', {
|
||||
eventId: this.draggedEventId,
|
||||
previousColumn: this.currentColumn,
|
||||
newColumn,
|
||||
mousePosition: { x: event.clientX, y: event.clientY }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleMouseUp(event: MouseEvent): void {
|
||||
if (!this.isMouseDown) return;
|
||||
|
||||
this.isMouseDown = false;
|
||||
|
||||
// Stop auto-scroll
|
||||
this.stopAutoScroll();
|
||||
|
||||
if (this.draggedEventId && this.originalElement) {
|
||||
// Calculate final position
|
||||
const finalColumn = this.detectColumn(event.clientX, event.clientY);
|
||||
const finalY = this.calculateSnapPosition(event.clientY);
|
||||
|
||||
// Emit drag end event
|
||||
this.eventBus.emit('drag:end', {
|
||||
eventId: this.draggedEventId,
|
||||
originalElement: this.originalElement,
|
||||
finalPosition: { x: event.clientX, y: event.clientY },
|
||||
finalColumn,
|
||||
finalY
|
||||
});
|
||||
|
||||
console.log('DragDropManager: Drag ended', {
|
||||
eventId: this.draggedEventId,
|
||||
finalColumn,
|
||||
finalY
|
||||
});
|
||||
|
||||
// Clean up
|
||||
this.draggedEventId = null;
|
||||
this.originalElement = null;
|
||||
this.currentColumn = null;
|
||||
this.scrollContainer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate snapped Y position based on mouse Y
|
||||
*/
|
||||
private calculateSnapPosition(mouseY: number): number {
|
||||
// Find the column element to get relative position
|
||||
const columnElement = this.currentColumn
|
||||
? document.querySelector(`swp-day-column[data-date="${this.currentColumn}"]`)
|
||||
: null;
|
||||
|
||||
if (!columnElement) return mouseY;
|
||||
|
||||
const columnRect = columnElement.getBoundingClientRect();
|
||||
const relativeY = mouseY - columnRect.top - this.mouseOffset.y;
|
||||
|
||||
// Snap to nearest interval
|
||||
const snappedY = Math.round(relativeY / this.snapDistancePx) * this.snapDistancePx;
|
||||
|
||||
// Ensure non-negative
|
||||
return Math.max(0, snappedY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect which column the mouse is over
|
||||
*/
|
||||
private detectColumn(mouseX: number, mouseY: number): string | null {
|
||||
const element = document.elementFromPoint(mouseX, mouseY);
|
||||
if (!element) return null;
|
||||
|
||||
// Walk up DOM tree to find swp-day-column
|
||||
let current = element as HTMLElement;
|
||||
while (current && current.tagName !== 'SWP-DAY-COLUMN') {
|
||||
current = current.parentElement as HTMLElement;
|
||||
if (!current) return null;
|
||||
}
|
||||
|
||||
return current.dataset.date || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if auto-scroll should be triggered
|
||||
*/
|
||||
private checkAutoScroll(event: MouseEvent): void {
|
||||
// Find scrollable content if not cached
|
||||
if (!this.scrollContainer) {
|
||||
this.scrollContainer = document.querySelector('swp-scrollable-content') as HTMLElement;
|
||||
if (!this.scrollContainer) {
|
||||
console.warn('DragDropManager: Could not find swp-scrollable-content for auto-scroll');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const containerRect = this.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');
|
||||
} else if (distanceFromBottom <= this.scrollThreshold && distanceFromBottom > 0) {
|
||||
this.startAutoScroll('down');
|
||||
} else {
|
||||
this.stopAutoScroll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start auto-scroll animation
|
||||
*/
|
||||
private startAutoScroll(direction: 'up' | 'down'): void {
|
||||
if (this.autoScrollAnimationId !== null) return;
|
||||
|
||||
const scroll = () => {
|
||||
if (!this.scrollContainer || !this.isMouseDown) {
|
||||
this.stopAutoScroll();
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollAmount = direction === 'up' ? -this.scrollSpeed : this.scrollSpeed;
|
||||
this.scrollContainer.scrollTop += scrollAmount;
|
||||
|
||||
// Emit updated position during scroll
|
||||
if (this.draggedEventId) {
|
||||
const snappedY = this.calculateSnapPosition(this.currentMouseY);
|
||||
this.eventBus.emit('drag:auto-scroll', {
|
||||
eventId: this.draggedEventId,
|
||||
snappedY,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up event listeners
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.stopAutoScroll();
|
||||
document.body.removeEventListener('mousemove', this.handleMouseMove.bind(this));
|
||||
document.body.removeEventListener('mousedown', this.handleMouseDown.bind(this));
|
||||
document.body.removeEventListener('mouseup', this.handleMouseUp.bind(this));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue