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

@ -6,7 +6,7 @@ import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
import { EventManager } from '../managers/EventManager';
import { EventRendererStrategy } from './EventRenderer';
import { SwpEventElement } from '../elements/SwpEventElement';
import { DragStartEventPayload, DragMoveEventPayload, DragEndEventPayload, DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload } from '../types/EventTypes';
/**
* EventRenderingService - Render events i DOM med positionering using Strategy Pattern
* Håndterer event positioning og overlap detection
@ -16,14 +16,16 @@ export class EventRenderingService {
private eventManager: EventManager;
private strategy: EventRendererStrategy;
private dragMouseLeaveHeaderListener: ((event: Event) => void) | null = null;
constructor(eventBus: IEventBus, eventManager: EventManager) {
this.eventBus = eventBus;
this.eventManager = eventManager;
// Cache strategy at initialization
const calendarType = calendarConfig.getCalendarMode();
this.strategy = CalendarTypeFactory.getEventRenderer(calendarType);
this.setupEventListeners();
}
@ -31,24 +33,24 @@ export class EventRenderingService {
* Render events in a specific container for a given period
*/
public renderEvents(context: RenderContext): void {
// Clear existing events in the specific container first
this.strategy.clearEvents(context.container);
// Get events from EventManager for the period
const events = this.eventManager.getEventsForPeriod(
context.startDate,
context.endDate
);
if (events.length === 0) {
return;
}
// Use cached strategy to render events in the specific container
this.strategy.renderEvents(events, context.container);
// Emit EVENTS_RENDERED event for filtering system
this.eventBus.emit(CoreEvents.EVENTS_RENDERED, {
events: events,
@ -57,16 +59,11 @@ export class EventRenderingService {
}
private setupEventListeners(): void {
// Event-driven rendering: React to grid and container events
this.eventBus.on(CoreEvents.GRID_RENDERED, (event: Event) => {
this.handleGridRendered(event as CustomEvent);
});
// CONTAINER_READY_FOR_EVENTS removed - events are now pre-rendered synchronously
// this.eventBus.on(EventTypes.CONTAINER_READY_FOR_EVENTS, (event: Event) => {
// this.handleContainerReady(event as CustomEvent);
// });
this.eventBus.on(CoreEvents.VIEW_CHANGED, (event: Event) => {
this.handleViewChanged(event as CustomEvent);
});
@ -93,7 +90,7 @@ export class EventRenderingService {
*/
private handleGridRendered(event: CustomEvent): void {
const { container, startDate, endDate, currentDate } = event.detail;
if (!container) {
return;
}
@ -110,7 +107,7 @@ export class EventRenderingService {
} else {
return;
}
this.renderEvents({
container: container,
startDate: periodStart,
@ -123,7 +120,7 @@ export class EventRenderingService {
*/
private handleContainerReady(event: CustomEvent): void {
const { container, startDate, endDate } = event.detail;
if (!container || !startDate || !endDate) {
return;
}
@ -141,7 +138,7 @@ export class EventRenderingService {
private handleViewChanged(event: CustomEvent): void {
// Clear all existing events since view structure may have changed
this.clearEvents();
// New rendering will be triggered by subsequent GRID_RENDERED event
}
@ -152,45 +149,49 @@ export class EventRenderingService {
private setupDragEventListeners(): void {
// Handle drag start
this.eventBus.on('drag:start', (event: Event) => {
const { eventId, mouseOffset, column } = (event as CustomEvent).detail;
// Find element dynamically
const originalElement = document.querySelector(`swp-event[data-event-id="${eventId}"]`) as HTMLElement;
if (originalElement && this.strategy.handleDragStart) {
this.strategy.handleDragStart(originalElement, eventId, mouseOffset, column);
const { draggedElement, mouseOffset, column } = (event as CustomEvent<DragStartEventPayload>).detail;
// Use the draggedElement directly - no need for DOM query
if (draggedElement && this.strategy.handleDragStart && column) {
const eventId = draggedElement.dataset.eventId || '';
this.strategy.handleDragStart(draggedElement, eventId, mouseOffset, column);
}
});
// Handle drag move
this.eventBus.on('drag:move', (event: Event) => {
const { eventId, snappedY, column, mouseOffset } = (event as CustomEvent).detail;
if (this.strategy.handleDragMove) {
const { draggedElement, snappedY, column, mouseOffset } = (event as CustomEvent<DragMoveEventPayload>).detail;
if (this.strategy.handleDragMove && column) {
const eventId = draggedElement.dataset.eventId || '';
this.strategy.handleDragMove(eventId, snappedY, column, mouseOffset);
}
});
// Handle drag auto-scroll
this.eventBus.on('drag:auto-scroll', (event: Event) => {
const { eventId, snappedY } = (event as CustomEvent).detail;
const { draggedElement, snappedY } = (event as CustomEvent).detail;
if (this.strategy.handleDragAutoScroll) {
const eventId = draggedElement.dataset.eventId || '';
this.strategy.handleDragAutoScroll(eventId, snappedY);
}
});
// Handle drag end events and delegate to appropriate renderer
this.eventBus.on('drag:end', (event: Event) => {
const { eventId, finalColumn, finalY, target } = (event as CustomEvent).detail;
const { draggedElement, finalPosition, target } = (event as CustomEvent<DragEndEventPayload>).detail;
const finalColumn = finalPosition.column;
const finalY = finalPosition.snappedY;
const eventId = draggedElement.dataset.eventId || '';
// Only handle day column drops for EventRenderer
if (target === 'swp-day-column') {
// Find both original element and dragged clone
const originalElement = document.querySelector(`swp-day-column swp-event[data-event-id="${eventId}"]`) as HTMLElement;
if (target === 'swp-day-column' && finalColumn) {
// Find dragged clone - use draggedElement as original
const draggedClone = document.querySelector(`swp-day-column swp-event[data-event-id="clone-${eventId}"]`) as HTMLElement;
if (originalElement && draggedClone && this.strategy.handleDragEnd) {
this.strategy.handleDragEnd(eventId, originalElement, draggedClone, finalColumn, finalY);
if (draggedElement && draggedClone && this.strategy.handleDragEnd) {
this.strategy.handleDragEnd(eventId, draggedElement, draggedClone, finalColumn, finalY);
}
}
// Clean up any remaining day event clones
const dayEventClone = document.querySelector(`swp-event[data-event-id="clone-${eventId}"]`);
if (dayEventClone) {
@ -200,22 +201,40 @@ export class EventRenderingService {
// Handle click (when drag threshold not reached)
this.eventBus.on('event:click', (event: Event) => {
const { eventId } = (event as CustomEvent).detail;
// Find element dynamically
const originalElement = document.querySelector(`swp-event[data-event-id="${eventId}"]`) as HTMLElement;
if (originalElement && this.strategy.handleEventClick) {
this.strategy.handleEventClick(eventId, originalElement);
const { draggedElement } = (event as CustomEvent).detail;
// Use draggedElement directly - no need for DOM query
if (draggedElement && this.strategy.handleEventClick) {
const eventId = draggedElement.dataset.eventId || '';
this.strategy.handleEventClick(eventId, draggedElement);
}
});
// Handle column change
this.eventBus.on('drag:column-change', (event: Event) => {
const { eventId, newColumn } = (event as CustomEvent).detail;
const { draggedElement, newColumn } = (event as CustomEvent).detail;
if (this.strategy.handleColumnChange) {
const eventId = draggedElement.dataset.eventId || '';
this.strategy.handleColumnChange(eventId, newColumn);
}
});
this.dragMouseLeaveHeaderListener = (event: Event) => {
const { targetDate, mousePosition, originalElement, cloneElement } = (event as CustomEvent<DragMouseLeaveHeaderEventPayload>).detail;
if (cloneElement)
cloneElement.style.display = '';
console.log('🚪 EventRendererManager: Received drag:mouseleave-header', {
targetDate,
originalElement: originalElement,
cloneElement: cloneElement
});
};
this.eventBus.on('drag:mouseleave-header', this.dragMouseLeaveHeaderListener);
// Handle navigation period change
this.eventBus.on(CoreEvents.NAVIGATION_COMPLETED, () => {
// Delegate to strategy if it handles navigation
@ -232,46 +251,46 @@ export class EventRenderingService {
// Use the provided draggedElement directly
const allDayClone = draggedElement;
const draggedEventId = draggedElement?.dataset.eventId?.replace('clone-', '') || '';
// Use SwpEventElement factory to create day event from all-day event
const dayEventElement = SwpEventElement.fromAllDayElement(allDayClone as HTMLElement);
const dayElement = dayEventElement.getElement();
// Remove the all-day clone - it's no longer needed since we're converting to day event
allDayClone.remove();
// Set clone ID
dayElement.dataset.eventId = `clone-${draggedEventId}`;
// Find target column
const columnElement = document.querySelector(`swp-day-column[data-date="${column}"]`);
if (!columnElement) {
console.warn('EventRendererManager: Target column not found', { column });
return;
}
// Find events layer in the column
const eventsLayer = columnElement.querySelector('swp-events-layer');
if (!eventsLayer) {
console.warn('EventRendererManager: Events layer not found in column');
return;
}
// Add to events layer
eventsLayer.appendChild(dayElement);
// Position based on mouse Y coordinate
const columnRect = columnElement.getBoundingClientRect();
const relativeY = Math.max(0, mousePosition.y - columnRect.top);
dayElement.style.top = `${relativeY}px`;
// Set drag styling
dayElement.style.zIndex = '1000';
dayElement.style.cursor = 'grabbing';
dayElement.style.opacity = '';
dayElement.style.transform = '';
console.log('✅ EventRendererManager: Converted all-day event to time event', {
draggedEventId,
column,