Refactors drag and drop logic

Improves drag and drop initialization and handling by extracting methods for clarity.
This enhances code readability and maintainability.
This commit is contained in:
Janus C. H. Knudsen 2025-10-08 23:29:56 +02:00
parent b6f2aba398
commit 5d406201b8
2 changed files with 125 additions and 96 deletions

View file

@ -214,88 +214,111 @@ export class DragDropManager {
if (event.buttons === 1) { if (event.buttons === 1) {
const currentPosition: MousePosition = { x: event.clientX, y: event.clientY }; const currentPosition: MousePosition = { x: event.clientX, y: event.clientY };
// Check if we need to start drag (movement threshold) // Try to initialize drag if not started
if (!this.isDragStarted && this.draggedElement) { if (!this.isDragStarted && this.draggedElement) {
const deltaX = Math.abs(currentPosition.x - this.initialMousePosition.x); if (!this.tryInitializeDrag(currentPosition)) {
const deltaY = Math.abs(currentPosition.y - this.initialMousePosition.y); return; // Not enough movement yet
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 and save as initial source column
this.currentColumnBounds = ColumnDetectionUtils.getColumnBounds(currentPosition);
this.initialColumnBounds = this.currentColumnBounds; // Save source column
// 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 // Continue drag if started
if (this.isDragStarted && this.draggedElement && this.draggedClone) { if (this.isDragStarted && this.draggedElement && this.draggedClone) {
if (!this.draggedElement.hasAttribute("data-allday")) { this.continueDrag(currentPosition);
// Calculate raw position from mouse (no snapping) this.detectAndEmitColumnChange(currentPosition);
const column = ColumnDetectionUtils.getColumnBounds(currentPosition); }
}
}
if (column) { /**
// Calculate raw Y position relative to column (accounting for mouse offset) * Try to initialize drag based on movement threshold
const columnRect = column.boundingClientRect; * Returns true if drag was initialized, false if not enough movement
const eventTopY = currentPosition.y - columnRect.top - this.mouseOffset.y; */
this.targetY = Math.max(0, eventTopY); // Store raw Y as target (no snapping) private tryInitializeDrag(currentPosition: MousePosition): boolean {
this.targetColumn = column; 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);
// Start animation loop if not already running if (totalMovement < this.dragThreshold) {
if (this.dragAnimationId === null) { return false; // Not enough movement
this.currentY = parseFloat(this.draggedClone.style.top) || 0; }
this.animateDrag();
}
}
// Check for auto-scroll // Start drag
this.checkAutoScroll(currentPosition); this.isDragStarted = true;
}
const newColumn = ColumnDetectionUtils.getColumnBounds(currentPosition); // Set high z-index on event-group if exists, otherwise on event itself
if (newColumn == null) const eventGroup = this.draggedElement!.closest<HTMLElement>('swp-event-group');
return; if (eventGroup) {
eventGroup.style.zIndex = '9999';
} else {
this.draggedElement!.style.zIndex = '9999';
}
if (newColumn?.index !== this.currentColumnBounds?.index) { // Detect current column and save as initial source column
const previousColumn = this.currentColumnBounds; this.currentColumnBounds = ColumnDetectionUtils.getColumnBounds(currentPosition);
this.currentColumnBounds = newColumn; this.initialColumnBounds = this.currentColumnBounds;
const dragColumnChangePayload: DragColumnChangeEventPayload = { // Cast to BaseSwpEventElement and create clone
originalElement: this.draggedElement, const originalElement = this.draggedElement as BaseSwpEventElement;
draggedClone: this.draggedClone, this.draggedClone = originalElement.createClone();
previousColumn,
newColumn, const dragStartPayload: DragStartEventPayload = {
mousePosition: currentPosition draggedElement: this.draggedElement!,
}; draggedClone: this.draggedClone,
this.eventBus.emit('drag:column-change', dragColumnChangePayload); mousePosition: this.initialMousePosition,
mouseOffset: this.mouseOffset,
columnBounds: this.currentColumnBounds
};
this.eventBus.emit('drag:start', dragStartPayload);
return true;
}
/**
* Continue drag movement - update position and auto-scroll
*/
private continueDrag(currentPosition: MousePosition): void {
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);
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);
}
}
/**
* Detect column change and emit event
*/
private detectAndEmitColumnChange(currentPosition: MousePosition): void {
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);
} }
} }

View file

@ -106,30 +106,16 @@ export class EventRenderingService {
* Handle GRID_RENDERED event - render events in the current grid * Handle GRID_RENDERED event - render events in the current grid
*/ */
private handleGridRendered(event: CustomEvent): void { private handleGridRendered(event: CustomEvent): void {
const { container, startDate, endDate, currentDate, isNavigation } = event.detail; const { container, startDate, endDate } = event.detail;
if (!container) { if (!container || !startDate || !endDate) {
return;
}
let periodStart: Date;
let periodEnd: Date;
if (startDate && endDate) {
// Direct date format - use as provided
periodStart = startDate;
periodEnd = endDate;
} else if (currentDate) {
return;
} else {
return; return;
} }
this.renderEvents({ this.renderEvents({
container: container, container,
startDate: periodStart, startDate,
endDate: periodEnd endDate
}); });
} }
@ -149,29 +135,44 @@ export class EventRenderingService {
* Setup all drag event listeners - moved from EventRenderer for better separation of concerns * Setup all drag event listeners - moved from EventRenderer for better separation of concerns
*/ */
private setupDragEventListeners(): void { private setupDragEventListeners(): void {
this.setupDragStartListener();
this.setupDragMoveListener();
this.setupDragAutoScrollListener();
this.setupDragEndListener();
this.setupDragColumnChangeListener();
this.setupDragMouseLeaveHeaderListener();
this.setupResizeEndListener();
this.setupNavigationCompletedListener();
}
private setupDragStartListener(): void {
this.eventBus.on('drag:start', (event: Event) => { this.eventBus.on('drag:start', (event: Event) => {
const dragStartPayload = (event as CustomEvent<DragStartEventPayload>).detail; const dragStartPayload = (event as CustomEvent<DragStartEventPayload>).detail;
if (dragStartPayload.draggedElement.hasAttribute('data-allday')) { if (dragStartPayload.draggedElement.hasAttribute('data-allday')) {
return; return;
} }
if (dragStartPayload.draggedElement && this.strategy.handleDragStart && dragStartPayload.columnBounds) { if (dragStartPayload.draggedElement && this.strategy.handleDragStart && dragStartPayload.columnBounds) {
this.strategy.handleDragStart(dragStartPayload); this.strategy.handleDragStart(dragStartPayload);
} }
}); });
}
private setupDragMoveListener(): void {
this.eventBus.on('drag:move', (event: Event) => { this.eventBus.on('drag:move', (event: Event) => {
let dragEvent = (event as CustomEvent<DragMoveEventPayload>).detail; let dragEvent = (event as CustomEvent<DragMoveEventPayload>).detail;
if (dragEvent.draggedElement.hasAttribute('data-allday')) { if (dragEvent.draggedElement.hasAttribute('data-allday')) {
return; return;
} }
if (this.strategy.handleDragMove) { if (this.strategy.handleDragMove) {
this.strategy.handleDragMove(dragEvent); this.strategy.handleDragMove(dragEvent);
} }
}); });
}
private setupDragAutoScrollListener(): void {
this.eventBus.on('drag:auto-scroll', (event: Event) => { this.eventBus.on('drag:auto-scroll', (event: Event) => {
const { draggedElement, snappedY } = (event as CustomEvent).detail; const { draggedElement, snappedY } = (event as CustomEvent).detail;
if (this.strategy.handleDragAutoScroll) { if (this.strategy.handleDragAutoScroll) {
@ -179,8 +180,9 @@ export class EventRenderingService {
this.strategy.handleDragAutoScroll(eventId, snappedY); this.strategy.handleDragAutoScroll(eventId, snappedY);
} }
}); });
}
// Handle drag end events and delegate to appropriate renderer private setupDragEndListener(): void {
this.eventBus.on('drag:end', (event: Event) => { this.eventBus.on('drag:end', (event: Event) => {
const { originalElement: draggedElement, sourceColumn, finalPosition, target } = (event as CustomEvent<DragEndEventPayload>).detail; const { originalElement: draggedElement, sourceColumn, finalPosition, target } = (event as CustomEvent<DragEndEventPayload>).detail;
const finalColumn = finalPosition.column; const finalColumn = finalPosition.column;
@ -224,8 +226,9 @@ export class EventRenderingService {
dayEventClone.remove(); dayEventClone.remove();
} }
}); });
}
// Handle column change private setupDragColumnChangeListener(): void {
this.eventBus.on('drag:column-change', (event: Event) => { this.eventBus.on('drag:column-change', (event: Event) => {
let columnChangeEvent = (event as CustomEvent<DragColumnChangeEventPayload>).detail; let columnChangeEvent = (event as CustomEvent<DragColumnChangeEventPayload>).detail;
@ -238,8 +241,9 @@ export class EventRenderingService {
this.strategy.handleColumnChange(columnChangeEvent); this.strategy.handleColumnChange(columnChangeEvent);
} }
}); });
}
private setupDragMouseLeaveHeaderListener(): void {
this.dragMouseLeaveHeaderListener = (event: Event) => { this.dragMouseLeaveHeaderListener = (event: Event) => {
const { targetDate, mousePosition, originalElement, draggedClone: cloneElement } = (event as CustomEvent<DragMouseLeaveHeaderEventPayload>).detail; const { targetDate, mousePosition, originalElement, draggedClone: cloneElement } = (event as CustomEvent<DragMouseLeaveHeaderEventPayload>).detail;
@ -255,8 +259,9 @@ export class EventRenderingService {
}; };
this.eventBus.on('drag:mouseleave-header', this.dragMouseLeaveHeaderListener); this.eventBus.on('drag:mouseleave-header', this.dragMouseLeaveHeaderListener);
}
// Handle resize end events private setupResizeEndListener(): void {
this.eventBus.on('resize:end', (event: Event) => { this.eventBus.on('resize:end', (event: Event) => {
const { eventId, element } = (event as CustomEvent<ResizeEndEventPayload>).detail; const { eventId, element } = (event as CustomEvent<ResizeEndEventPayload>).detail;
@ -286,8 +291,9 @@ export class EventRenderingService {
} }
} }
}); });
}
// Handle navigation period change private setupNavigationCompletedListener(): void {
this.eventBus.on(CoreEvents.NAVIGATION_COMPLETED, () => { this.eventBus.on(CoreEvents.NAVIGATION_COMPLETED, () => {
// Delegate to strategy if it handles navigation // Delegate to strategy if it handles navigation
if (this.strategy.handleNavigationCompleted) { if (this.strategy.handleNavigationCompleted) {