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:
Janus C. H. Knudsen 2025-09-21 15:48:13 +02:00
parent b4f5b29da3
commit c7dcfbbaed
7 changed files with 583 additions and 410 deletions

View file

@ -4,6 +4,12 @@ import { eventBus } from '../core/EventBus';
import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig';
import { AllDayEventRenderer } from '../renderers/AllDayEventRenderer';
import { CalendarEvent } from '../types/CalendarTypes';
import {
DragMouseEnterHeaderEventPayload,
DragStartEventPayload,
DragMoveEventPayload,
DragEndEventPayload
} from '../types/EventTypes';
/**
* AllDayManager - Handles all-day row height animations and management
@ -28,14 +34,20 @@ export class AllDayManager {
* Setup event listeners for drag conversions
*/
private setupEventListeners(): void {
eventBus.on('drag:convert-to-allday_event', (event) => {
const { targetDate, originalElement } = (event as CustomEvent).detail;
console.log('🔄 AllDayManager: Received drag:convert-to-allday_event', {
eventBus.on('drag:mouseenter-header', (event) => {
const { targetDate, mousePosition, originalElement, cloneElement } = (event as CustomEvent<DragMouseEnterHeaderEventPayload>).detail;
console.log('🔄 AllDayManager: Received drag:mouseenter-header', {
targetDate,
originalElementId: originalElement?.dataset?.eventId,
originalElementTag: originalElement?.tagName
});
this.handleConvertToAllDay(targetDate, originalElement);
if (targetDate && cloneElement) {
this.handleConvertToAllDay(targetDate, cloneElement);
}
});
@ -53,20 +65,25 @@ export class AllDayManager {
// Listen for drag operations on all-day events
eventBus.on('drag:start', (event) => {
const { eventId, mouseOffset } = (event as CustomEvent).detail;
// Check if this is an all-day event
const originalElement = document.querySelector(`swp-allday-container swp-allday-event[data-event-id="${eventId}"]`);
if (!originalElement) return; // Not an all-day event
const { draggedElement, mouseOffset } = (event as CustomEvent<DragStartEventPayload>).detail;
// Check if this is an all-day event by checking if it's in all-day container
const isAllDayEvent = draggedElement.closest('swp-allday-container');
if (!isAllDayEvent) return; // Not an all-day event
const eventId = draggedElement.dataset.eventId;
console.log('🎯 AllDayManager: Starting drag for all-day event', { eventId });
this.handleDragStart(originalElement as HTMLElement, eventId, mouseOffset);
this.handleDragStart(draggedElement, eventId || '', mouseOffset);
});
eventBus.on('drag:move', (event) => {
const { eventId, mousePosition } = (event as CustomEvent).detail;
const { draggedElement, mousePosition } = (event as CustomEvent<DragMoveEventPayload>).detail;
// Only handle for all-day events
// Only handle for all-day events - check if original element is all-day
const isAllDayEvent = draggedElement.closest('swp-allday-container');
if (!isAllDayEvent) return;
const eventId = draggedElement.dataset.eventId;
const dragClone = document.querySelector(`swp-allday-container swp-allday-event[data-event-id="clone-${eventId}"]`);
if (dragClone) {
this.handleDragMove(dragClone as HTMLElement, mousePosition);
@ -74,26 +91,21 @@ export class AllDayManager {
});
eventBus.on('drag:end', (event) => {
const { draggedElement, mousePosition, finalPosition, target } = (event as CustomEvent<DragEndEventPayload>).detail;
const { eventId, finalColumn, finalY, dropTarget } = (event as CustomEvent).detail;
if (dropTarget != 'SWP-DAY-HEADER')//we are not inside the swp-day-header, so just ignore.
if (target != 'swp-day-header') // we are not inside the swp-day-header, so just ignore.
return;
const eventId = draggedElement.dataset.eventId;
console.log('🎬 AllDayManager: Received drag:end', {
eventId: eventId,
finalColumn: finalColumn,
finalY: finalY
finalPosition
});
// Check if this was an all-day event
const originalElement = document.querySelector(`swp-allday-container swp-allday-event[data-event-id="${eventId}"]`);
const dragClone = document.querySelector(`swp-allday-container swp-allday-event[data-event-id="clone-${eventId}"]`);
console.log('🎯 AllDayManager: Ending drag for all-day event', { eventId });
this.handleDragEnd(originalElement as HTMLElement, dragClone as HTMLElement, finalColumn);
this.handleDragEnd(draggedElement, dragClone as HTMLElement, finalPosition.column);
});
}
@ -287,18 +299,20 @@ export class AllDayManager {
/**
* Handle conversion of timed event to all-day event
*/
private handleConvertToAllDay(targetDate: string, originalElement: HTMLElement): void {
private handleConvertToAllDay(targetDate: string, cloneElement: HTMLElement): void {
// Extract event data from original element
const eventId = originalElement.dataset.eventId;
const title = originalElement.dataset.title || originalElement.textContent || 'Untitled';
const type = originalElement.dataset.type || 'work';
const startStr = originalElement.dataset.start;
const endStr = originalElement.dataset.end;
const eventId = cloneElement.dataset.eventId;
const title = cloneElement.dataset.title || cloneElement.textContent || 'Untitled';
const type = cloneElement.dataset.type || 'work';
const startStr = cloneElement.dataset.start;
const endStr = cloneElement.dataset.end;
if (!eventId || !startStr || !endStr) {
console.error('Original element missing required data (eventId, start, end)');
return;
}
//we just hide it, it will only be removed on mouse up
cloneElement.style.display = 'none';
// Create CalendarEvent for all-day conversion - preserve original times
const originalStart = new Date(startStr);
@ -312,7 +326,7 @@ export class AllDayManager {
targetEnd.setHours(originalEnd.getHours(), originalEnd.getMinutes(), originalEnd.getSeconds(), originalEnd.getMilliseconds());
const calendarEvent: CalendarEvent = {
id: `clone-${eventId}`,
id: eventId,
title: title,
start: targetStart,
end: targetEnd,
@ -320,12 +334,12 @@ export class AllDayManager {
allDay: true,
syncStatus: 'synced',
metadata: {
duration: originalElement.dataset.duration || '60'
duration: cloneElement.dataset.duration || '60'
}
};
// Check if all-day clone already exists for this event ID
const existingAllDayEvent = document.querySelector(`swp-allday-container swp-allday-event[data-event-id="clone-${eventId}"]`);
const existingAllDayEvent = document.querySelector(`swp-allday-container swp-allday-event[data-event-id="${eventId}"]`);
if (existingAllDayEvent) {
// All-day event already exists, just ensure clone is hidden
const dragClone = document.querySelector(`swp-event[data-event-id="clone-${eventId}"]`);