Refactors drag and drop column detection

Improves drag and drop functionality by refactoring column detection to use column bounds instead of dates.
This change enhances the accuracy and efficiency of determining the target column during drag operations.
It also removes redundant code and simplifies the logic in both the DragDropManager and AllDayManager.
This commit is contained in:
Janus C. H. Knudsen 2025-09-28 13:25:09 +02:00
parent 4141bffca4
commit 6ccc071587
8 changed files with 262 additions and 377 deletions

View file

@ -9,6 +9,8 @@ import { SwpEventElement } from '../elements/SwpEventElement';
import { TimeFormatter } from '../utils/TimeFormatter';
import { PositionUtils } from '../utils/PositionUtils';
import { DragOffset, StackLinkData } from '../types/DragDropTypes';
import { ColumnBounds } from '../utils/ColumnDetectionUtils';
import { DragColumnChangeEventPayload, DragMoveEventPayload, DragStartEventPayload } from '../types/EventTypes';
/**
* Interface for event rendering strategies
@ -16,12 +18,12 @@ import { DragOffset, StackLinkData } from '../types/DragDropTypes';
export interface EventRendererStrategy {
renderEvents(events: CalendarEvent[], container: HTMLElement): void;
clearEvents(container?: HTMLElement): void;
handleDragStart?(payload: import('../types/EventTypes').DragStartEventPayload): void;
handleDragMove?(eventId: string, snappedY: number, column: string, mouseOffset: DragOffset): void;
handleDragStart?(payload: DragStartEventPayload): void;
handleDragMove?(payload: DragMoveEventPayload): void;
handleDragAutoScroll?(eventId: string, snappedY: number): void;
handleDragEnd?(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: string, finalY: number): void;
handleDragEnd?(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: ColumnBounds, finalY: number): void;
handleEventClick?(eventId: string, originalElement: HTMLElement): void;
handleColumnChange?(eventId: string, newColumn: string): void;
handleColumnChange?(payload: DragColumnChangeEventPayload): void;
handleNavigationCompleted?(): void;
}
@ -160,13 +162,9 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
/**
* Handle drag start event
*/
public handleDragStart(payload: import('../types/EventTypes').DragStartEventPayload): void {
const originalElement = payload.draggedElement;
const eventId = originalElement.dataset.eventId || '';
const mouseOffset = payload.mouseOffset;
const column = payload.column || '';
this.originalEvent = originalElement;
public handleDragStart(payload: DragStartEventPayload): void {
this.originalEvent = payload.draggedElement;;
// Use the clone from the payload instead of creating a new one
this.draggedClone = payload.draggedClone;
@ -176,35 +174,29 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
this.applyDragStyling(this.draggedClone);
// Add to current column's events layer (not directly to column)
const columnElement = document.querySelector(`swp-day-column[data-date="${column}"]`);
if (columnElement) {
const eventsLayer = columnElement.querySelector('swp-events-layer');
if (eventsLayer) {
eventsLayer.appendChild(this.draggedClone);
} else {
// Fallback to column if events layer not found
columnElement.appendChild(this.draggedClone);
}
const eventsLayer = payload.columnBounds?.element.querySelector('swp-events-layer');
if (eventsLayer) {
eventsLayer.appendChild(this.draggedClone);
}
}
// Make original semi-transparent
originalElement.style.opacity = '0.3';
originalElement.style.userSelect = 'none';
this.originalEvent.style.opacity = '0.3';
this.originalEvent.style.userSelect = 'none';
}
/**
* Handle drag move event
*/
public handleDragMove(eventId: string, snappedY: number, column: string, mouseOffset: DragOffset): void {
public handleDragMove(payload: DragMoveEventPayload): void {
if (!this.draggedClone) return;
// Update position
this.draggedClone.style.top = snappedY + 'px';
this.draggedClone.style.top = payload.snappedY + 'px';
// Update timestamp display
this.updateCloneTimestamp(this.draggedClone, snappedY);
this.updateCloneTimestamp(this.draggedClone, payload.snappedY);
}
@ -224,26 +216,20 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
/**
* Handle column change during drag
*/
public handleColumnChange(eventId: string, newColumn: string): void {
public handleColumnChange(dragColumnChangeEvent: DragColumnChangeEventPayload): void {
if (!this.draggedClone) return;
// Move clone to new column's events layer
const newColumnElement = document.querySelector(`swp-day-column[data-date="${newColumn}"]`);
if (newColumnElement) {
const eventsLayer = newColumnElement.querySelector('swp-events-layer');
if (eventsLayer && this.draggedClone.parentElement !== eventsLayer) {
eventsLayer.appendChild(this.draggedClone);
} else if (!eventsLayer && this.draggedClone.parentElement !== newColumnElement) {
// Fallback to column if events layer not found
newColumnElement.appendChild(this.draggedClone);
}
const eventsLayer = dragColumnChangeEvent.newColumn.element.querySelector('swp-events-layer');
if (eventsLayer && this.draggedClone.parentElement !== eventsLayer) {
eventsLayer.appendChild(this.draggedClone);
}
}
/**
* Handle drag end event
*/
public handleDragEnd(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: string, finalY: number): void {
public handleDragEnd(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: ColumnBounds, finalY: number): void {
if (!draggedClone || !originalElement) {
console.warn('Missing draggedClone or originalElement');
@ -398,11 +384,9 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
/**
* Handle overlap detection and re-rendering after drag-drop
*/
private handleDragDropOverlaps(droppedElement: HTMLElement, targetColumn: string): void {
const targetColumnElement = document.querySelector(`swp-day-column[data-date="${targetColumn}"]`);
if (!targetColumnElement) return;
private handleDragDropOverlaps(droppedElement: HTMLElement, targetColumn: ColumnBounds): void {
const eventsLayer = targetColumnElement.querySelector('swp-events-layer') as HTMLElement;
const eventsLayer = targetColumn.element.querySelector('swp-events-layer') as HTMLElement;
if (!eventsLayer) return;
// Convert dropped element to CalendarEvent with new position
@ -543,16 +527,16 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
renderEvents(events: CalendarEvent[], container: HTMLElement): void {
// Filter out all-day events - they should be handled by AllDayEventRenderer
const timedEvents = events.filter(event => !event.allDay);
console.log('🎯 EventRenderer: Filtering events', {
totalEvents: events.length,
timedEvents: timedEvents.length,
filteredOutAllDay: events.length - timedEvents.length
});
// Find columns in the specific container for regular events
const columns = this.getColumns(container);
@ -561,7 +545,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
const eventsLayer = column.querySelector('swp-events-layer');
if (eventsLayer) {
this.handleEventOverlaps(columnEvents, eventsLayer as HTMLElement);
}
});

View file

@ -29,7 +29,7 @@ export class EventRenderingService {
// Cache strategy at initialization
const calendarType = calendarConfig.getCalendarMode();
this.strategy = CalendarTypeFactory.getEventRenderer(calendarType);
// Initialize all-day event renderer and manager
this.allDayEventRenderer = new AllDayEventRenderer();
this.allDayManager = new AllDayManager();
@ -92,7 +92,7 @@ export class EventRenderingService {
startDate: startDate.toISOString(),
endDate: endDate.toISOString()
});
// Render all-day events using period from header
this.renderAllDayEventsForPeriod(startDate, endDate);
});
@ -181,22 +181,21 @@ export class EventRenderingService {
this.eventBus.on('drag:start', (event: Event) => {
const dragStartPayload = (event as CustomEvent<DragStartEventPayload>).detail;
// Use the draggedElement directly - no need for DOM query
if (dragStartPayload.draggedElement && this.strategy.handleDragStart && dragStartPayload.column) {
if (dragStartPayload.draggedElement && this.strategy.handleDragStart && dragStartPayload.columnBounds) {
this.strategy.handleDragStart(dragStartPayload);
}
});
// Handle drag move
this.eventBus.on('drag:move', (event: Event) => {
const { draggedElement, snappedY, column, mouseOffset } = (event as CustomEvent<DragMoveEventPayload>).detail;
let dragEvent = (event as CustomEvent<DragMoveEventPayload>).detail;
// Filter: Only handle events WITHOUT data-allday attribute (normal timed events)
if (draggedElement.hasAttribute('data-allday')) {
if (dragEvent.draggedElement.hasAttribute('data-allday')) {
return; // This is an all-day event, let AllDayManager handle it
}
if (this.strategy.handleDragMove && column) {
const eventId = draggedElement.dataset.eventId || '';
this.strategy.handleDragMove(eventId, snappedY, column, mouseOffset);
if (this.strategy.handleDragMove) {
this.strategy.handleDragMove(dragEvent);
}
});
@ -239,22 +238,22 @@ export class EventRenderingService {
// Use draggedElement directly - no need for DOM query
if (draggedElement && this.strategy.handleEventClick) {
const eventId = draggedElement.dataset.eventId || '';
this.strategy.handleEventClick(eventId, draggedElement);
this.strategy.handleEventClick(eventId, draggedElement); //TODO: fix this redundant parameters
}
});
// Handle column change
this.eventBus.on('drag:column-change', (event: Event) => {
const { draggedElement, draggedClone, newColumn } = (event as CustomEvent<DragColumnChangeEventPayload>).detail;
let columnChangeEvent = (event as CustomEvent<DragColumnChangeEventPayload>).detail;
// Filter: Only handle events where clone is NOT an all-day event (normal timed events)
if (draggedClone && draggedClone.hasAttribute('data-allday')) {
return; // This is an all-day event, let AllDayManager handle it
if (columnChangeEvent.draggedClone && columnChangeEvent.draggedClone.hasAttribute('data-allday')) {
return;
}
if (this.strategy.handleColumnChange) {
const eventId = draggedElement.dataset.eventId || '';
this.strategy.handleColumnChange(eventId, newColumn); //TODO: Should be refactored to use payload, no need to lookup clone again inside
const eventId = columnChangeEvent.draggedElement.dataset.eventId || '';
this.strategy.handleColumnChange(columnChangeEvent);
}
});
@ -363,17 +362,17 @@ export class EventRenderingService {
// Get actual visible dates from DOM headers instead of generating them
const weekDates = this.getVisibleDatesFromDOM();
console.log('🔍 EventRenderingService: Using visible dates from DOM', {
weekDates,
count: weekDates.length
});
// Pass current events to AllDayManager for state tracking
this.allDayManager.setCurrentEvents(allDayEvents, weekDates);
const layouts = this.allDayManager.calculateAllDayEventsLayout(allDayEvents, weekDates);
// Render each all-day event with pre-calculated layout
layouts.forEach(layout => {
this.allDayEventRenderer.renderAllDayEventWithLayout(layout.calenderEvent, layout);
@ -395,7 +394,7 @@ export class EventRenderingService {
private clearEvents(container?: HTMLElement): void {
this.strategy.clearEvents(container);
// Also clear all-day events
this.clearAllDayEvents();
}
@ -409,18 +408,18 @@ export class EventRenderingService {
* Get visible dates from DOM headers - only the dates that are actually displayed
*/
private getVisibleDatesFromDOM(): string[] {
const dayHeaders = document.querySelectorAll('swp-calendar-header swp-day-header');
const weekDates: string[] = [];
dayHeaders.forEach(header => {
const dateAttr = header.getAttribute('data-date');
if (dateAttr) {
weekDates.push(dateAttr);
}
});
return weekDates;
}