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

@ -0,0 +1,161 @@
# Calendar Event System Analysis
## Overview
Analysis of all events used in the Calendar Plantempus system, categorized by type and usage.
## Core Events (25 events)
*Defined in `src/constants/CoreEvents.ts`*
### Lifecycle Events (3)
- `core:initialized` - Calendar initialization complete
- `core:ready` - Calendar ready for use
- `core:destroyed` - Calendar cleanup complete
### View Events (3)
- `view:changed` - Calendar view changed (day/week/month)
- `view:rendered` - View rendering complete
- `workweek:changed` - Work week configuration changed
### Navigation Events (4)
- `nav:date-changed` - Current date changed
- `nav:navigation-completed` - Navigation animation/transition complete
- `nav:period-info-update` - Week/period information updated
- `nav:navigate-to-event` - Request to navigate to specific event
### Data Events (4)
- `data:loading` - Data fetch started
- `data:loaded` - Data fetch completed
- `data:error` - Data fetch error
- `data:events-filtered` - Events filtered
### Grid Events (3)
- `grid:rendered` - Grid rendering complete
- `grid:clicked` - Grid cell clicked
- `grid:cell-selected` - Grid cell selected
### Event Management (4)
- `event:created` - New event created
- `event:updated` - Event updated
- `event:deleted` - Event deleted
- `event:selected` - Event selected
### System Events (2)
- `system:error` - System error occurred
- `system:refresh` - Refresh requested
### Filter Events (1)
- `filter:changed` - Event filter changed
### Rendering Events (1)
- `events:rendered` - Events rendering complete
## Custom Events (22 events)
*Used throughout the system for specific functionality*
### Drag & Drop Events (12)
- `drag:start` - Drag operation started
- `drag:move` - Drag operation in progress
- `drag:end` - Drag operation ended
- `drag:auto-scroll` - Auto-scroll during drag
- `drag:column-change` - Dragged to different column
- `drag:mouseenter-header` - Mouse entered header during drag
- `drag:mouseleave-header` - Mouse left header during drag
- `drag:convert-to-time_event` - Convert all-day to timed event
### Event Interaction (2)
- `event:click` - Event clicked (no drag)
- `event:clicked` - Event clicked (legacy)
### Header Events (3)
- `header:mouseleave` - Mouse left header area
- `header:height-changed` - Header height changed
- `header:rebuilt` - Header DOM rebuilt
### All-Day Events (1)
- `allday:ensure-container` - Ensure all-day container exists
### Column Events (1)
- `column:mouseover` - Mouse over column
### Scroll Events (1)
- `scroll:to-event-time` - Scroll to specific event time
### Workweek Events (1)
- `workweek:header-update` - Update header after workweek change
### Navigation Events (1)
- `navigation:completed` - Navigation completed (different from core event)
## Event Payload Analysis
### Type Safety Issues Found
#### 1. AllDayManager Event Mismatch
**File:** `src/managers/AllDayManager.ts:33-34`
```typescript
// Expected payload:
const { targetDate, originalElement } = (event as CustomEvent).detail;
// Actual payload from DragDropManager:
{
targetDate: string,
mousePosition: { x: number, y: number },
originalElement: HTMLElement,
cloneElement: HTMLElement | null
}
```
#### 2. Inconsistent Event Signatures
Multiple events have different payload structures across different emitters/listeners.
#### 3. No Type Safety
All events use `(event as CustomEvent).detail` without proper TypeScript interfaces.
## Event Usage Statistics
### Most Used Events
1. **Drag Events** - 12 different types, used heavily in drag-drop system
2. **Core Navigation** - 4 types, used across all managers
3. **Grid Events** - 3 types, fundamental to calendar rendering
4. **Header Events** - 3 types, critical for all-day functionality
### Critical Events (High Impact)
- `drag:mouseenter-header` / `drag:mouseleave-header` - Core drag functionality
- `nav:navigation-completed` - Synchronizes multiple managers
- `grid:rendered` - Triggers event rendering
- `events:rendered` - Triggers filtering system
### Simple Events (Low Impact)
- `header:height-changed` - Simple notification
- `allday:ensure-container` - Simple request
- `system:refresh` - Simple trigger
## Recommendations
### Priority 1: Fix Critical Issues
1. Fix AllDayManager event signature mismatch
2. Standardize drag event payloads
3. Document current event contracts
### Priority 2: Type Safety Implementation
1. Create TypeScript interfaces for all event payloads
2. Implement type-safe EventBus
3. Migrate drag events first (highest complexity)
### Priority 3: System Cleanup
1. Consolidate duplicate events (`event:click` vs `event:clicked`)
2. Standardize event naming conventions
3. Remove unused events
## Total Event Count
- **Core Events:** 25
- **Custom Events:** 22
- **Total:** 47 unique event types
## Files Analyzed
- `src/constants/CoreEvents.ts`
- `src/managers/*.ts` (8 files)
- `src/renderers/*.ts` (4 files)
- `src/core/CalendarConfig.ts`
*Analysis completed: 2025-09-20*

View file

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

View file

@ -5,8 +5,14 @@
import { IEventBus } from '../types/CalendarTypes'; import { IEventBus } from '../types/CalendarTypes';
import { calendarConfig } from '../core/CalendarConfig'; import { calendarConfig } from '../core/CalendarConfig';
import { DateCalculator } from '../utils/DateCalculator';
import { PositionUtils } from '../utils/PositionUtils'; import { PositionUtils } from '../utils/PositionUtils';
import {
DragStartEventPayload,
DragMoveEventPayload,
DragEndEventPayload,
DragMouseEnterHeaderEventPayload,
DragMouseLeaveHeaderEventPayload
} from '../types/EventTypes';
interface CachedElements { interface CachedElements {
scrollContainer: HTMLElement | null; scrollContainer: HTMLElement | null;
@ -27,71 +33,72 @@ interface ColumnBounds {
export class DragDropManager { export class DragDropManager {
private eventBus: IEventBus; private eventBus: IEventBus;
// Mouse tracking with optimized state // Mouse tracking with optimized state
private lastMousePosition: Position = { x: 0, y: 0 }; private lastMousePosition: Position = { x: 0, y: 0 };
private lastLoggedPosition: Position = { x: 0, y: 0 }; private lastLoggedPosition: Position = { x: 0, y: 0 };
private currentMouseY = 0; private currentMouseY = 0;
private mouseOffset: Position = { x: 0, y: 0 }; private mouseOffset: Position = { x: 0, y: 0 };
private initialMousePosition: Position = { x: 0, y: 0 }; private initialMousePosition: Position = { x: 0, y: 0 };
// Drag state // Drag state
private draggedEventId: string | null = null; private draggedElement: HTMLElement | null = null ;
private originalElement: HTMLElement | null = null;
private currentColumn: string | null = null; private currentColumn: string | null = null;
private isDragStarted = false; private isDragStarted = false;
// Header tracking state
private isInHeader = false;
// Movement threshold to distinguish click from drag // Movement threshold to distinguish click from drag
private readonly dragThreshold = 5; // pixels private readonly dragThreshold = 5; // pixels
// Cached DOM elements for performance // Cached DOM elements for performance
private cachedElements: CachedElements = { private cachedElements: CachedElements = {
scrollContainer: null, scrollContainer: null,
currentColumn: null, currentColumn: null,
lastColumnDate: null lastColumnDate: null
}; };
// Column bounds cache for coordinate-based column detection // Column bounds cache for coordinate-based column detection
private columnBoundsCache: ColumnBounds[] = []; private columnBoundsCache: ColumnBounds[] = [];
// Auto-scroll properties // Auto-scroll properties
private autoScrollAnimationId: number | null = null; private autoScrollAnimationId: number | null = null;
private readonly scrollSpeed = 10; // pixels per frame private readonly scrollSpeed = 10; // pixels per frame
private readonly scrollThreshold = 30; // pixels from edge private readonly scrollThreshold = 30; // pixels from edge
// Snap configuration // Snap configuration
private snapIntervalMinutes = 15; // Default 15 minutes private snapIntervalMinutes = 15; // Default 15 minutes
private hourHeightPx: number; // Will be set from config private hourHeightPx: number; // Will be set from config
// Event listener references for proper cleanup // Event listener references for proper cleanup
private boundHandlers = { private boundHandlers = {
mouseMove: this.handleMouseMove.bind(this), mouseMove: this.handleMouseMove.bind(this),
mouseDown: this.handleMouseDown.bind(this), mouseDown: this.handleMouseDown.bind(this),
mouseUp: this.handleMouseUp.bind(this) mouseUp: this.handleMouseUp.bind(this)
}; };
private get snapDistancePx(): number { private get snapDistancePx(): number {
return (this.snapIntervalMinutes / 60) * this.hourHeightPx; return (this.snapIntervalMinutes / 60) * this.hourHeightPx;
} }
constructor(eventBus: IEventBus) { constructor(eventBus: IEventBus) {
this.eventBus = eventBus; this.eventBus = eventBus;
// Get config values // Get config values
const gridSettings = calendarConfig.getGridSettings(); const gridSettings = calendarConfig.getGridSettings();
this.hourHeightPx = gridSettings.hourHeight; this.hourHeightPx = gridSettings.hourHeight;
this.snapIntervalMinutes = gridSettings.snapInterval; this.snapIntervalMinutes = gridSettings.snapInterval;
this.init(); this.init();
} }
/** /**
* Configure snap interval * Configure snap interval
*/ */
public setSnapInterval(minutes: number): void { public setSnapInterval(minutes: number): void {
this.snapIntervalMinutes = minutes; this.snapIntervalMinutes = minutes;
} }
/** /**
* Initialize with optimized event listener setup * Initialize with optimized event listener setup
*/ */
@ -100,100 +107,32 @@ export class DragDropManager {
document.body.addEventListener('mousemove', this.boundHandlers.mouseMove); document.body.addEventListener('mousemove', this.boundHandlers.mouseMove);
document.body.addEventListener('mousedown', this.boundHandlers.mouseDown); document.body.addEventListener('mousedown', this.boundHandlers.mouseDown);
document.body.addEventListener('mouseup', this.boundHandlers.mouseUp); document.body.addEventListener('mouseup', this.boundHandlers.mouseUp);
// Initialize column bounds cache // Initialize column bounds cache
this.updateColumnBoundsCache(); this.updateColumnBoundsCache();
// Listen to resize events to update cache // Listen to resize events to update cache
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
this.updateColumnBoundsCache(); this.updateColumnBoundsCache();
}); });
// Listen to navigation events to update cache // Listen to navigation events to update cache
this.eventBus.on('navigation:completed', () => { this.eventBus.on('navigation:completed', () => {
this.updateColumnBoundsCache(); this.updateColumnBoundsCache();
}); });
// Listen for header mouseover events
this.eventBus.on('header:mouseover', (event) => {
const { targetDate, headerRenderer } = (event as CustomEvent).detail;
console.log('🎯 DragDropManager: Received header:mouseover', {
targetDate,
draggedEventId: this.draggedEventId,
isDragging: !!this.draggedEventId
});
if (this.draggedEventId && targetDate) {
// Find dragget element dynamisk
const draggedElement = document.querySelector(`swp-event[data-event-id="${this.draggedEventId}"]`);
console.log('🔍 DragDropManager: Looking for dragged element', {
eventId: this.draggedEventId,
found: !!draggedElement,
tagName: draggedElement?.tagName
});
if (draggedElement) {
console.log('✅ DragDropManager: Converting to all-day for date:', targetDate);
// Element findes stadig som day-event, så konverter
this.eventBus.emit('drag:convert-to-allday_event', {
targetDate,
originalElement: draggedElement,
headerRenderer
});
} else {
console.log('❌ DragDropManager: Dragged element not found');
}
} else {
console.log('⏭️ DragDropManager: Skipping conversion - no drag or no target date');
}
});
// Listen for column mouseover events (for all-day to timed conversion)
this.eventBus.on('column:mouseover', (event) => {
const { targetColumn, targetY } = (event as CustomEvent).detail;
if (this.draggedEventId && this.isAllDayEventBeingDragged()) {
// Emit event to convert to timed
this.eventBus.emit('drag:convert-to-timed', {
eventId: this.draggedEventId,
targetColumn,
targetY
});
}
});
// Listen for header mouseleave events (convert from all-day back to day)
this.eventBus.on('header:mouseleave', (event) => {
// Check if we're dragging ANY event
if (this.draggedEventId) {
const mousePosition = { x: this.lastMousePosition.x, y: this.lastMousePosition.y };
const column = this.getColumnDateFromX(mousePosition.x);
// Find the actual dragged element
const draggedElement = document.querySelector(`[data-event-id="${this.draggedEventId}"]`) as HTMLElement;
this.eventBus.emit('drag:convert-to-time_event', {
draggedElement: draggedElement,
mousePosition: mousePosition,
column: column
});
}
});
} }
private handleMouseDown(event: MouseEvent): void { private handleMouseDown(event: MouseEvent): void {
this.isDragStarted = false; this.isDragStarted = false;
this.lastMousePosition = { x: event.clientX, y: event.clientY }; this.lastMousePosition = { x: event.clientX, y: event.clientY };
this.lastLoggedPosition = { x: event.clientX, y: event.clientY }; this.lastLoggedPosition = { x: event.clientX, y: event.clientY };
this.initialMousePosition = { x: event.clientX, y: event.clientY }; this.initialMousePosition = { x: event.clientX, y: event.clientY };
// Check if mousedown is on an event // Check if mousedown is on an event
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
let eventElement = target; let eventElement = target;
while (eventElement && eventElement.tagName !== 'SWP-EVENTS-LAYER') { while (eventElement && eventElement.tagName !== 'SWP-EVENTS-LAYER') {
if (eventElement.tagName === 'SWP-EVENT' || eventElement.tagName === 'SWP-ALLDAY-EVENT') { if (eventElement.tagName === 'SWP-EVENT' || eventElement.tagName === 'SWP-ALLDAY-EVENT') {
break; break;
@ -201,96 +140,104 @@ export class DragDropManager {
eventElement = eventElement.parentElement as HTMLElement; eventElement = eventElement.parentElement as HTMLElement;
if (!eventElement) return; if (!eventElement) return;
} }
// If we reached SWP-EVENTS-LAYER without finding an event, return // If we reached SWP-EVENTS-LAYER without finding an event, return
if (!eventElement || eventElement.tagName === 'SWP-EVENTS-LAYER') { if (!eventElement || eventElement.tagName === 'SWP-EVENTS-LAYER') {
return; return;
} }
// Found an event - prepare for potential dragging // Found an event - prepare for potential dragging
if (eventElement) { if (eventElement) {
this.originalElement = eventElement; this.draggedElement = eventElement;
this.draggedEventId = eventElement.dataset.eventId || null;
// Calculate mouse offset within event // Calculate mouse offset within event
const eventRect = eventElement.getBoundingClientRect(); const eventRect = eventElement.getBoundingClientRect();
this.mouseOffset = { this.mouseOffset = {
x: event.clientX - eventRect.left, x: event.clientX - eventRect.left,
y: event.clientY - eventRect.top y: event.clientY - eventRect.top
}; };
// Detect current column // Detect current column
const column = this.detectColumn(event.clientX, event.clientY); const column = this.detectColumn(event.clientX, event.clientY);
if (column) { if (column) {
this.currentColumn = column; this.currentColumn = column;
} }
// Don't emit drag:start yet - wait for movement threshold // Don't emit drag:start yet - wait for movement threshold
} }
} }
/** /**
* Optimized mouse move handler with consolidated position calculations * Optimized mouse move handler with consolidated position calculations
*/ */
private handleMouseMove(event: MouseEvent): void { private handleMouseMove(event: MouseEvent): void {
this.currentMouseY = event.clientY; this.currentMouseY = event.clientY;
this.lastMousePosition = { x: event.clientX, y: event.clientY };
if (event.buttons === 1 && this.draggedEventId) {
// Check for header enter/leave during drag
if (this.draggedElement) {
this.checkHeaderEnterLeave(event);
}
if (event.buttons === 1 && this.draggedElement) {
const currentPosition: Position = { x: event.clientX, y: event.clientY }; const currentPosition: Position = { x: event.clientX, y: event.clientY };
// Check if we need to start drag (movement threshold) // Check if we need to start drag (movement threshold)
if (!this.isDragStarted) { if (!this.isDragStarted) {
const deltaX = Math.abs(currentPosition.x - this.initialMousePosition.x); const deltaX = Math.abs(currentPosition.x - this.initialMousePosition.x);
const deltaY = Math.abs(currentPosition.y - this.initialMousePosition.y); const deltaY = Math.abs(currentPosition.y - this.initialMousePosition.y);
const totalMovement = Math.sqrt(deltaX * deltaX + deltaY * deltaY); const totalMovement = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (totalMovement >= this.dragThreshold) { if (totalMovement >= this.dragThreshold) {
// Start drag - emit drag:start event // Start drag - emit drag:start event
this.isDragStarted = true; this.isDragStarted = true;
this.eventBus.emit('drag:start', {
eventId: this.draggedEventId, const dragStartPayload: DragStartEventPayload = {
draggedElement: this.draggedElement,
mousePosition: this.initialMousePosition, mousePosition: this.initialMousePosition,
mouseOffset: this.mouseOffset, mouseOffset: this.mouseOffset,
column: this.currentColumn column: this.currentColumn
}); };
this.eventBus.emit('drag:start', dragStartPayload);
} else { } else {
// Not enough movement yet - don't start drag // Not enough movement yet - don't start drag
return; return;
} }
} }
// Continue with normal drag behavior only if drag has started // Continue with normal drag behavior only if drag has started
if (this.isDragStarted) { if (this.isDragStarted) {
const deltaY = Math.abs(currentPosition.y - this.lastLoggedPosition.y); const deltaY = Math.abs(currentPosition.y - this.lastLoggedPosition.y);
// Check for snap interval vertical movement (normal drag behavior) // Check for snap interval vertical movement (normal drag behavior)
if (deltaY >= this.snapDistancePx) { if (deltaY >= this.snapDistancePx) {
this.lastLoggedPosition = currentPosition; this.lastLoggedPosition = currentPosition;
// Consolidated position calculations with snapping for normal drag // Consolidated position calculations with snapping for normal drag
const positionData = this.calculateDragPosition(currentPosition); const positionData = this.calculateDragPosition(currentPosition);
// Emit drag move event with snapped position (normal behavior) // Emit drag move event with snapped position (normal behavior)
this.eventBus.emit('drag:move', { const dragMovePayload: DragMoveEventPayload = {
eventId: this.draggedEventId, draggedElement: this.draggedElement,
mousePosition: currentPosition, mousePosition: currentPosition,
snappedY: positionData.snappedY, snappedY: positionData.snappedY,
column: positionData.column, column: positionData.column,
mouseOffset: this.mouseOffset mouseOffset: this.mouseOffset
}); };
this.eventBus.emit('drag:move', dragMovePayload);
} }
// Check for auto-scroll // Check for auto-scroll
this.checkAutoScroll(event); this.checkAutoScroll(event);
// Check for column change using cached data // Check for column change using cached data
const newColumn = this.getColumnFromCache(currentPosition); const newColumn = this.getColumnFromCache(currentPosition);
if (newColumn && newColumn !== this.currentColumn) { if (newColumn && newColumn !== this.currentColumn) {
const previousColumn = this.currentColumn; const previousColumn = this.currentColumn;
this.currentColumn = newColumn; this.currentColumn = newColumn;
this.eventBus.emit('drag:column-change', { this.eventBus.emit('drag:column-change', {
eventId: this.draggedEventId, draggedElement: this.draggedElement,
previousColumn, previousColumn,
newColumn, newColumn,
mousePosition: currentPosition mousePosition: currentPosition
@ -299,81 +246,64 @@ export class DragDropManager {
} }
} }
} }
/** /**
* Optimized mouse up handler with consolidated cleanup * Optimized mouse up handler with consolidated cleanup
*/ */
private handleMouseUp(event: MouseEvent): void { private handleMouseUp(event: MouseEvent): void {
this.stopAutoScroll(); this.stopAutoScroll();
if (this.draggedEventId && this.originalElement) { if (this.draggedElement) {
// Store variables locally before cleanup // Store variables locally before cleanup
const eventId = this.draggedEventId; const draggedElement = this.draggedElement;
const originalElement = this.originalElement;
const isDragStarted = this.isDragStarted; const isDragStarted = this.isDragStarted;
// Clean up drag state first // Clean up drag state first
this.cleanupDragState(); this.cleanupDragState();
// Only emit drag:end if drag was actually started // Only emit drag:end if drag was actually started
if (isDragStarted) { if (isDragStarted) {
const finalPosition: Position = { x: event.clientX, y: event.clientY }; const mousePosition: Position = { x: event.clientX, y: event.clientY };
// Use consolidated position calculation // Use consolidated position calculation
const positionData = this.calculateDragPosition(finalPosition); const positionData = this.calculateDragPosition(mousePosition);
// Detect drop target (swp-day-column or swp-day-header) // Detect drop target (swp-day-column or swp-day-header)
const dropTarget = this.detectDropTarget(finalPosition); const dropTarget = this.detectDropTarget(mousePosition);
console.log('🎯 DragDropManager: Emitting drag:end', { console.log('🎯 DragDropManager: Emitting drag:end', {
eventId: eventId, draggedElement: draggedElement.dataset.eventId,
finalColumn: positionData.column, finalColumn: positionData.column,
finalY: positionData.snappedY, finalY: positionData.snappedY,
dropTarget: dropTarget, dropTarget: dropTarget,
isDragStarted: isDragStarted isDragStarted: isDragStarted
}); });
this.eventBus.emit('drag:end', { const dragEndPayload: DragEndEventPayload = {
eventId: eventId, draggedElement: draggedElement,
finalPosition, mousePosition,
finalColumn: positionData.column, finalPosition: positionData,
finalY: positionData.snappedY,
target: dropTarget target: dropTarget
}); };
this.eventBus.emit('drag:end', dragEndPayload);
} else { } else {
// This was just a click - emit click event instead // This was just a click - emit click event instead
this.eventBus.emit('event:click', { this.eventBus.emit('event:click', {
eventId: eventId, draggedElement: draggedElement,
mousePosition: { x: event.clientX, y: event.clientY } mousePosition: { x: event.clientX, y: event.clientY }
}); });
} }
} }
} }
/** /**
* Consolidated position calculation method using PositionUtils * Consolidated position calculation method using PositionUtils
*/ */
private calculateDragPosition(mousePosition: Position): { column: string | null; snappedY: number } { private calculateDragPosition(mousePosition: Position): { column: string | null; snappedY: number } {
const column = this.detectColumn(mousePosition.x, mousePosition.y); const column = this.detectColumn(mousePosition.x, mousePosition.y);
const snappedY = this.calculateSnapPosition(mousePosition.y, column); const snappedY = this.calculateSnapPosition(mousePosition.y, column);
return { column, snappedY };
}
/** return { column, snappedY };
* Calculate free position (follows mouse exactly)
*/
private calculateFreePosition(mouseY: number, column: string | null = null): number {
const targetColumn = column || this.currentColumn;
// Use cached column element if available
const columnElement = this.getCachedColumnElement(targetColumn);
if (!columnElement) return mouseY;
const relativeY = PositionUtils.getPositionFromCoordinate(mouseY, columnElement);
// Return free position (no snapping)
return Math.max(0, relativeY);
} }
/** /**
@ -381,32 +311,32 @@ export class DragDropManager {
*/ */
private calculateSnapPosition(mouseY: number, column: string | null = null): number { private calculateSnapPosition(mouseY: number, column: string | null = null): number {
const targetColumn = column || this.currentColumn; const targetColumn = column || this.currentColumn;
// Use cached column element if available // Use cached column element if available
const columnElement = this.getCachedColumnElement(targetColumn); const columnElement = this.getCachedColumnElement(targetColumn);
if (!columnElement) return mouseY; if (!columnElement) return mouseY;
// Use PositionUtils for consistent snapping behavior // Use PositionUtils for consistent snapping behavior
const snappedY = PositionUtils.getPositionFromCoordinate(mouseY, columnElement); const snappedY = PositionUtils.getPositionFromCoordinate(mouseY, columnElement);
return Math.max(0, snappedY); return Math.max(0, snappedY);
} }
/** /**
* Update column bounds cache for coordinate-based column detection * Update column bounds cache for coordinate-based column detection
*/ */
private updateColumnBoundsCache(): void { private updateColumnBoundsCache(): void {
// Reset cache // Reset cache
this.columnBoundsCache = []; this.columnBoundsCache = [];
// Find alle kolonner // Find alle kolonner
const columns = document.querySelectorAll('swp-day-column'); const columns = document.querySelectorAll('swp-day-column');
// Cache hver kolonnes x-grænser // Cache hver kolonnes x-grænser
columns.forEach(column => { columns.forEach(column => {
const rect = column.getBoundingClientRect(); const rect = column.getBoundingClientRect();
const date = (column as HTMLElement).dataset.date; const date = (column as HTMLElement).dataset.date;
if (date) { if (date) {
this.columnBoundsCache.push({ this.columnBoundsCache.push({
date, date,
@ -415,10 +345,10 @@ export class DragDropManager {
}); });
} }
}); });
// Sorter efter x-position (fra venstre til højre) // Sorter efter x-position (fra venstre til højre)
this.columnBoundsCache.sort((a, b) => a.left - b.left); this.columnBoundsCache.sort((a, b) => a.left - b.left);
} }
/** /**
@ -429,12 +359,12 @@ export class DragDropManager {
if (this.columnBoundsCache.length === 0) { if (this.columnBoundsCache.length === 0) {
this.updateColumnBoundsCache(); this.updateColumnBoundsCache();
} }
// Find den kolonne hvor x-koordinaten er indenfor grænserne // Find den kolonne hvor x-koordinaten er indenfor grænserne
const column = this.columnBoundsCache.find(col => const column = this.columnBoundsCache.find(col =>
x >= col.left && x <= col.right x >= col.left && x <= col.right
); );
return column ? column.date : null; return column ? column.date : null;
} }
@ -444,7 +374,7 @@ export class DragDropManager {
private detectColumn(mouseX: number, mouseY: number): string | null { private detectColumn(mouseX: number, mouseY: number): string | null {
// Brug den koordinatbaserede metode direkte // Brug den koordinatbaserede metode direkte
const columnDate = this.getColumnDateFromX(mouseX); const columnDate = this.getColumnDateFromX(mouseX);
// Opdater stadig den eksisterende cache hvis vi finder en kolonne // Opdater stadig den eksisterende cache hvis vi finder en kolonne
if (columnDate && columnDate !== this.cachedElements.lastColumnDate) { if (columnDate && columnDate !== this.cachedElements.lastColumnDate) {
const columnElement = document.querySelector(`swp-day-column[data-date="${columnDate}"]`) as HTMLElement; const columnElement = document.querySelector(`swp-day-column[data-date="${columnDate}"]`) as HTMLElement;
@ -453,7 +383,7 @@ export class DragDropManager {
this.cachedElements.lastColumnDate = columnDate; this.cachedElements.lastColumnDate = columnDate;
} }
} }
return columnDate; return columnDate;
} }
@ -468,7 +398,7 @@ export class DragDropManager {
return this.cachedElements.lastColumnDate; return this.cachedElements.lastColumnDate;
} }
} }
// Cache miss - detect new column // Cache miss - detect new column
return this.detectColumn(mousePosition.x, mousePosition.y); return this.detectColumn(mousePosition.x, mousePosition.y);
} }
@ -478,22 +408,22 @@ export class DragDropManager {
*/ */
private getCachedColumnElement(columnDate: string | null): HTMLElement | null { private getCachedColumnElement(columnDate: string | null): HTMLElement | null {
if (!columnDate) return null; if (!columnDate) return null;
// Return cached element if it matches // Return cached element if it matches
if (this.cachedElements.lastColumnDate === columnDate && this.cachedElements.currentColumn) { if (this.cachedElements.lastColumnDate === columnDate && this.cachedElements.currentColumn) {
return this.cachedElements.currentColumn; return this.cachedElements.currentColumn;
} }
// Query for new element and cache it // Query for new element and cache it
const element = document.querySelector(`swp-day-column[data-date="${columnDate}"]`) as HTMLElement; const element = document.querySelector(`swp-day-column[data-date="${columnDate}"]`) as HTMLElement;
if (element) { if (element) {
this.cachedElements.currentColumn = element; this.cachedElements.currentColumn = element;
this.cachedElements.lastColumnDate = columnDate; this.cachedElements.lastColumnDate = columnDate;
} }
return element; return element;
} }
/** /**
* Optimized auto-scroll check with cached container * Optimized auto-scroll check with cached container
*/ */
@ -505,14 +435,14 @@ export class DragDropManager {
return; return;
} }
} }
const containerRect = this.cachedElements.scrollContainer.getBoundingClientRect(); const containerRect = this.cachedElements.scrollContainer.getBoundingClientRect();
const mouseY = event.clientY; const mouseY = event.clientY;
// Calculate distances from edges // Calculate distances from edges
const distanceFromTop = mouseY - containerRect.top; const distanceFromTop = mouseY - containerRect.top;
const distanceFromBottom = containerRect.bottom - mouseY; const distanceFromBottom = containerRect.bottom - mouseY;
// Check if we need to scroll // Check if we need to scroll
if (distanceFromTop <= this.scrollThreshold && distanceFromTop > 0) { if (distanceFromTop <= this.scrollThreshold && distanceFromTop > 0) {
this.startAutoScroll('up'); this.startAutoScroll('up');
@ -522,24 +452,24 @@ export class DragDropManager {
this.stopAutoScroll(); this.stopAutoScroll();
} }
} }
/** /**
* Optimized auto-scroll with cached container reference * Optimized auto-scroll with cached container reference
*/ */
private startAutoScroll(direction: 'up' | 'down'): void { private startAutoScroll(direction: 'up' | 'down'): void {
if (this.autoScrollAnimationId !== null) return; if (this.autoScrollAnimationId !== null) return;
const scroll = () => { const scroll = () => {
if (!this.cachedElements.scrollContainer || !this.draggedEventId) { if (!this.cachedElements.scrollContainer || !this.draggedElement) {
this.stopAutoScroll(); this.stopAutoScroll();
return; return;
} }
const scrollAmount = direction === 'up' ? -this.scrollSpeed : this.scrollSpeed; const scrollAmount = direction === 'up' ? -this.scrollSpeed : this.scrollSpeed;
this.cachedElements.scrollContainer.scrollTop += scrollAmount; this.cachedElements.scrollContainer.scrollTop += scrollAmount;
// Emit updated position during scroll - adjust for scroll movement // Emit updated position during scroll - adjust for scroll movement
if (this.draggedEventId) { if (this.draggedElement) {
// During autoscroll, we need to calculate position relative to the scrolled content // During autoscroll, we need to calculate position relative to the scrolled content
// The mouse hasn't moved, but the content has scrolled // The mouse hasn't moved, but the content has scrolled
const columnElement = this.getCachedColumnElement(this.currentColumn); const columnElement = this.getCachedColumnElement(this.currentColumn);
@ -548,21 +478,21 @@ export class DragDropManager {
// Calculate free position relative to column, accounting for scroll movement (no snapping during scroll) // Calculate free position relative to column, accounting for scroll movement (no snapping during scroll)
const relativeY = this.currentMouseY - columnRect.top - this.mouseOffset.y; const relativeY = this.currentMouseY - columnRect.top - this.mouseOffset.y;
const freeY = Math.max(0, relativeY); const freeY = Math.max(0, relativeY);
this.eventBus.emit('drag:auto-scroll', { this.eventBus.emit('drag:auto-scroll', {
eventId: this.draggedEventId, draggedElement: this.draggedElement,
snappedY: freeY, // Actually free position during scroll snappedY: freeY, // Actually free position during scroll
scrollTop: this.cachedElements.scrollContainer.scrollTop scrollTop: this.cachedElements.scrollContainer.scrollTop
}); });
} }
} }
this.autoScrollAnimationId = requestAnimationFrame(scroll); this.autoScrollAnimationId = requestAnimationFrame(scroll);
}; };
this.autoScrollAnimationId = requestAnimationFrame(scroll); this.autoScrollAnimationId = requestAnimationFrame(scroll);
} }
/** /**
* Stop auto-scroll animation * Stop auto-scroll animation
*/ */
@ -572,31 +502,21 @@ export class DragDropManager {
this.autoScrollAnimationId = null; this.autoScrollAnimationId = null;
} }
} }
/** /**
* Clean up drag state * Clean up drag state
*/ */
private cleanupDragState(): void { private cleanupDragState(): void {
this.draggedEventId = null; this.draggedElement = null;
this.originalElement = null;
this.currentColumn = null; this.currentColumn = null;
this.isDragStarted = false; this.isDragStarted = false;
this.isInHeader = false;
// Clear cached elements // Clear cached elements
this.cachedElements.currentColumn = null; this.cachedElements.currentColumn = null;
this.cachedElements.lastColumnDate = null; this.cachedElements.lastColumnDate = null;
} }
/**
* Check if an all-day event is currently being dragged
*/
private isAllDayEventBeingDragged(): boolean {
if (!this.draggedEventId) return false;
// Check if element exists as all-day event
const allDayElement = document.querySelector(`swp-allday-event[data-event-id="${this.draggedEventId}"]`);
return allDayElement !== null;
}
/** /**
* Detect drop target - whether dropped in swp-day-column or swp-day-header * Detect drop target - whether dropped in swp-day-column or swp-day-header
*/ */
@ -619,23 +539,81 @@ export class DragDropManager {
return null; return null;
} }
/**
* Check for header enter/leave during drag operations
*/
private checkHeaderEnterLeave(event: MouseEvent): void {
const elementAtPosition = document.elementFromPoint(event.clientX, event.clientY);
if (!elementAtPosition) return;
// Check if we're in a header area
const headerElement = elementAtPosition.closest('swp-day-header, swp-calendar-header');
const isCurrentlyInHeader = !!headerElement;
// Detect header enter
if (!this.isInHeader && isCurrentlyInHeader) {
this.isInHeader = true;
// Calculate target date using existing method
const targetDate = this.getColumnDateFromX(event.clientX);
if (targetDate) {
console.log('🎯 DragDropManager: Emitting drag:mouseenter-header', { targetDate });
// Find clone element (if it exists)
const eventId = this.draggedElement?.dataset.eventId;
const cloneElement = document.querySelector(`[data-event-id="clone-${eventId}"]`) as HTMLElement;
const dragMouseEnterPayload: DragMouseEnterHeaderEventPayload = {
targetDate,
mousePosition: { x: event.clientX, y: event.clientY },
originalElement: this.draggedElement,
cloneElement: cloneElement
};
this.eventBus.emit('drag:mouseenter-header', dragMouseEnterPayload);
}
}
// Detect header leave
if (this.isInHeader && !isCurrentlyInHeader) {
this.isInHeader = false;
console.log('🚪 DragDropManager: Emitting drag:mouseleave-header');
// Calculate target date using existing method
const targetDate = this.getColumnDateFromX(event.clientX);
// Find clone element (if it exists)
const eventId = this.draggedElement?.dataset.eventId;
const cloneElement = document.querySelector(`[data-event-id="clone-${eventId}"]`) as HTMLElement;
const dragMouseLeavePayload: DragMouseLeaveHeaderEventPayload = {
targetDate,
mousePosition: { x: event.clientX, y: event.clientY },
originalElement: this.draggedElement,
cloneElement: cloneElement
};
this.eventBus.emit('drag:mouseleave-header', dragMouseLeavePayload);
}
}
/** /**
* Clean up all resources and event listeners * Clean up all resources and event listeners
*/ */
public destroy(): void { public destroy(): void {
this.stopAutoScroll(); this.stopAutoScroll();
// Remove event listeners using bound references // Remove event listeners using bound references
document.body.removeEventListener('mousemove', this.boundHandlers.mouseMove); document.body.removeEventListener('mousemove', this.boundHandlers.mouseMove);
document.body.removeEventListener('mousedown', this.boundHandlers.mouseDown); document.body.removeEventListener('mousedown', this.boundHandlers.mouseDown);
document.body.removeEventListener('mouseup', this.boundHandlers.mouseUp); document.body.removeEventListener('mouseup', this.boundHandlers.mouseUp);
// Clear all cached elements // Clear all cached elements
this.cachedElements.scrollContainer = null; this.cachedElements.scrollContainer = null;
this.cachedElements.currentColumn = null; this.cachedElements.currentColumn = null;
this.cachedElements.lastColumnDate = null; this.cachedElements.lastColumnDate = null;
// Clean up drag state // Clean up drag state
this.cleanupDragState(); this.cleanupDragState();
} }
} }

View file

@ -4,15 +4,18 @@ import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
import { CoreEvents } from '../constants/CoreEvents'; import { CoreEvents } from '../constants/CoreEvents';
import { HeaderRenderContext } from '../renderers/HeaderRenderer'; import { HeaderRenderContext } from '../renderers/HeaderRenderer';
import { ResourceCalendarData } from '../types/CalendarTypes'; import { ResourceCalendarData } from '../types/CalendarTypes';
import { DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload } from '../types/EventTypes';
/** /**
* HeaderManager - Handles all header-related event logic * HeaderManager - Handles all header-related event logic
* Separates event handling from rendering concerns * Separates event handling from rendering concerns
*/ */
export class HeaderManager { export class HeaderManager {
private headerEventListener: ((event: Event) => void) | null = null;
private headerMouseLeaveListener: ((event: Event) => void) | null = null;
private cachedCalendarHeader: HTMLElement | null = null; private cachedCalendarHeader: HTMLElement | null = null;
// Event listeners for drag events
private dragMouseEnterHeaderListener: ((event: Event) => void) | null = null;
private dragMouseLeaveHeaderListener: ((event: Event) => void) | null = null;
constructor() { constructor() {
// Bind methods for event listeners // Bind methods for event listeners
@ -41,132 +44,72 @@ export class HeaderManager {
} }
/** /**
* Setup header drag event listeners - REFACTORED to use mouseenter * Setup header drag event listeners - REFACTORED to listen to DragDropManager events
*/ */
public setupHeaderDragListeners(): void { public setupHeaderDragListeners(): void {
if (!this.getCalendarHeader()) return; console.log('🎯 HeaderManager: Setting up drag event listeners');
console.log('🎯 HeaderManager: Setting up drag listeners with mouseenter'); // Create and store event listeners
this.dragMouseEnterHeaderListener = (event: Event) => {
const { targetDate, mousePosition, originalElement, cloneElement } = (event as CustomEvent<DragMouseEnterHeaderEventPayload>).detail;
// Use mouseenter instead of mouseover to avoid continuous firing
this.headerEventListener = (event: Event) => {
if (!document.querySelector('.dragging') !== null) { console.log('🎯 HeaderManager: Received drag:mouseenter-header', {
return; targetDate,
} originalElement: !!originalElement,
cloneElement: !!cloneElement
});
const target = event.target as HTMLElement; if (targetDate) {
// Ensure all-day container exists
console.log('🖱️ HeaderManager: mouseenter detected on:', target.tagName, target.className); this.ensureAllDayContainer();
// Check if we're entering the all-day container OR the header area where container should be
let allDayContainer = target.closest('swp-allday-container');
// If no container exists, check if we're in the header and should create one via AllDayManager
if (!allDayContainer && target.closest('swp-calendar-header')) {
console.log('📍 HeaderManager: In header area but no all-day container exists, requesting creation...');
// Emit event to AllDayManager to create container const calendarType = calendarConfig.getCalendarMode();
eventBus.emit('allday:ensure-container'); const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType);
// Try to find it again after creation
allDayContainer = target.closest('swp-calendar-header')?.querySelector('swp-allday-container') as HTMLElement;
}
if (allDayContainer) {
console.log('📍 HeaderManager: Active drag detected, calculating target date...');
// Calculate target date from mouse X coordinate
const targetDate = this.calculateTargetDateFromMouseX(event as MouseEvent);
console.log('🎯 HeaderManager: Calculated target date:', targetDate);
if (targetDate) {
const calendarType = calendarConfig.getCalendarMode();
const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType);
console.log('✅ HeaderManager: Emitting header:mouseover with targetDate:', targetDate);
eventBus.emit('header:mouseover', {
element: allDayContainer,
targetDate,
headerRenderer
});
} else {
console.log('❌ HeaderManager: Could not calculate target date from mouse position');
}
} }
}; };
// Header mouseleave listener this.dragMouseLeaveHeaderListener = (event: Event) => {
this.headerMouseLeaveListener = (event: Event) => { const { targetDate, mousePosition, originalElement, cloneElement } = (event as CustomEvent<DragMouseLeaveHeaderEventPayload>).detail;
console.log('🚪 HeaderManager: mouseleave detected');
console.log('🚪 HeaderManager: Received drag:mouseleave-header', {
targetDate,
originalElement: !!originalElement,
cloneElement: !!cloneElement
});
eventBus.emit('header:mouseleave', { eventBus.emit('header:mouseleave', {
element: event.target as HTMLElement element: this.getCalendarHeader(),
targetDate,
originalElement,
cloneElement
}); });
}; };
this.getCalendarHeader()?.addEventListener('mouseenter', this.headerEventListener, true); // Listen for drag events from DragDropManager
this.getCalendarHeader()?.addEventListener('mouseleave', this.headerMouseLeaveListener); eventBus.on('drag:mouseenter-header', this.dragMouseEnterHeaderListener);
eventBus.on('drag:mouseleave-header', this.dragMouseLeaveHeaderListener);
console.log('✅ HeaderManager: Event listeners attached (mouseenter + mouseleave)'); console.log('✅ HeaderManager: Drag event listeners attached');
} }
/** /**
* Calculate target date from mouse X coordinate * Ensure all-day container exists in header
*/ */
private calculateTargetDateFromMouseX(event: MouseEvent): string | null { private ensureAllDayContainer(): void {
const dayHeaders = document.querySelectorAll('swp-day-header');
const mouseX = event.clientX;
console.log('🧮 HeaderManager: Calculating target date from mouseX:', mouseX);
console.log('📊 HeaderManager: Found', dayHeaders.length, 'day headers');
for (const header of dayHeaders) {
const headerElement = header as HTMLElement;
const rect = headerElement.getBoundingClientRect();
const headerDate = headerElement.dataset.date;
console.log('📏 HeaderManager: Checking header', headerDate, 'bounds:', {
left: rect.left,
right: rect.right,
mouseX: mouseX,
isWithin: mouseX >= rect.left && mouseX <= rect.right
});
// Check if mouse X is within this header's bounds
if (mouseX >= rect.left && mouseX <= rect.right) {
console.log('🎯 HeaderManager: Found matching header for date:', headerDate);
return headerDate || null;
}
}
console.log('❌ HeaderManager: No matching header found for mouseX:', mouseX);
return null;
}
/**
* Remove event listeners from header - UPDATED for mouseenter
*/
private removeEventListeners(): void {
const calendarHeader = this.getCalendarHeader(); const calendarHeader = this.getCalendarHeader();
if (!calendarHeader) return; if (!calendarHeader) return;
console.log('🧹 HeaderManager: Removing event listeners');
if (this.headerEventListener) {
// Remove mouseenter listener with capture flag
calendarHeader.removeEventListener('mouseenter', this.headerEventListener, true);
console.log('✅ HeaderManager: Removed mouseenter listener');
}
if (this.headerMouseLeaveListener) { let allDayContainer = calendarHeader.querySelector('swp-allday-container');
calendarHeader.removeEventListener('mouseleave', this.headerMouseLeaveListener);
console.log('✅ HeaderManager: Removed mouseleave listener'); if (!allDayContainer) {
console.log('📍 HeaderManager: All-day container missing, requesting creation...');
eventBus.emit('allday:ensure-container');
} }
} }
/** /**
* Setup navigation event listener * Setup navigation event listener
*/ */
@ -198,10 +141,7 @@ export class HeaderManager {
const calendarHeader = this.getOrCreateCalendarHeader(); const calendarHeader = this.getOrCreateCalendarHeader();
if (!calendarHeader) return; if (!calendarHeader) return;
// Remove existing event listeners BEFORE clearing content // Clear existing content
this.removeEventListeners();
// Clear existing content
calendarHeader.innerHTML = ''; calendarHeader.innerHTML = '';
// Render new header content // Render new header content
@ -257,11 +197,18 @@ export class HeaderManager {
* Clean up resources and event listeners * Clean up resources and event listeners
*/ */
public destroy(): void { public destroy(): void {
this.removeEventListeners();
// Remove eventBus listeners
if (this.dragMouseEnterHeaderListener) {
eventBus.off('drag:mouseenter-header', this.dragMouseEnterHeaderListener);
}
if (this.dragMouseLeaveHeaderListener) {
eventBus.off('drag:mouseleave-header', this.dragMouseLeaveHeaderListener);
}
// Clear references // Clear references
this.headerEventListener = null; this.dragMouseEnterHeaderListener = null;
this.headerMouseLeaveListener = null; this.dragMouseLeaveHeaderListener = null;
this.clearCache(); this.clearCache();
} }

View file

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

View file

@ -3,7 +3,6 @@ import { ResourceCalendarData, CalendarView } from '../types/CalendarTypes';
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
import { ColumnRenderContext } from './ColumnRenderer'; import { ColumnRenderContext } from './ColumnRenderer';
import { eventBus } from '../core/EventBus'; import { eventBus } from '../core/EventBus';
import { DateCalculator } from '../utils/DateCalculator';
/** /**
* GridRenderer - Centralized DOM rendering for calendar grid * GridRenderer - Centralized DOM rendering for calendar grid
@ -37,7 +36,7 @@ export class GridRenderer {
if (grid.children.length === 0) { if (grid.children.length === 0) {
this.createCompleteGridStructure(grid, currentDate, resourceData, view); this.createCompleteGridStructure(grid, currentDate, resourceData, view);
// Setup grid-related event listeners on first render // Setup grid-related event listeners on first render
this.setupGridEventListeners(); // this.setupGridEventListeners();
} else { } else {
// Optimized update - only refresh dynamic content // Optimized update - only refresh dynamic content
this.updateGridContent(grid, currentDate, resourceData, view); this.updateGridContent(grid, currentDate, resourceData, view);
@ -169,15 +168,15 @@ export class GridRenderer {
/** /**
* Setup grid-only event listeners (column events) * Setup grid-only event listeners (column events)
*/
private setupGridEventListeners(): void { private setupGridEventListeners(): void {
// Setup grid body mouseover listener for all-day to timed conversion // Setup grid body mouseover listener for all-day to timed conversion
this.setupGridBodyMouseOver(); this.setupGridBodyMouseOver();
} }
*/
/** /**
* Setup grid body mouseover listener for all-day to timed conversion * Setup grid body mouseover listener for all-day to timed conversion
*/
private setupGridBodyMouseOver(): void { private setupGridBodyMouseOver(): void {
const grid = this.cachedGridContainer; const grid = this.cachedGridContainer;
if (!grid) return; if (!grid) return;
@ -221,15 +220,15 @@ export class GridRenderer {
(this as any).gridBodyEventListener = gridBodyEventListener; (this as any).gridBodyEventListener = gridBodyEventListener;
(this as any).cachedColumnContainer = columnContainer; (this as any).cachedColumnContainer = columnContainer;
} }
*/
/** /**
* Clean up cached elements and event listeners * Clean up cached elements and event listeners
*/ */
public destroy(): void { public destroy(): void {
// Clean up grid-only event listeners // Clean up grid-only event listeners
if ((this as any).gridBodyEventListener && (this as any).cachedColumnContainer) { // if ((this as any).gridBodyEventListener && (this as any).cachedColumnContainer) {
(this as any).cachedColumnContainer.removeEventListener('mouseover', (this as any).gridBodyEventListener); // (this as any).cachedColumnContainer.removeEventListener('mouseover', (this as any).gridBodyEventListener);
} //}
// Clear cached references // Clear cached references
this.cachedGridContainer = null; this.cachedGridContainer = null;

View file

@ -30,4 +30,59 @@ export interface TimeEvent {
}; };
} }
export type CalendarEventData = AllDayEvent | TimeEvent; export type CalendarEventData = AllDayEvent | TimeEvent;
/**
* Drag Event Payload Interfaces
* Type-safe interfaces for drag and drop events
*/
// Common position interface
export interface MousePosition {
x: number;
y: number;
}
// Drag start event payload
export interface DragStartEventPayload {
draggedElement: HTMLElement;
mousePosition: MousePosition;
mouseOffset: MousePosition;
column: string | null;
}
// Drag move event payload
export interface DragMoveEventPayload {
draggedElement: HTMLElement;
mousePosition: MousePosition;
mouseOffset: MousePosition;
snappedY: number;
column: string | null;
}
// Drag end event payload
export interface DragEndEventPayload {
draggedElement: HTMLElement;
mousePosition: MousePosition;
finalPosition: {
column: string | null;
snappedY: number;
};
target: 'swp-day-column' | 'swp-day-header' | null;
}
// Drag mouse enter header event payload
export interface DragMouseEnterHeaderEventPayload {
targetDate: string;
mousePosition: MousePosition;
originalElement: HTMLElement | null;
cloneElement: HTMLElement | null;
}
// Drag mouse leave header event payload
export interface DragMouseLeaveHeaderEventPayload {
targetDate: string | null;
mousePosition: MousePosition;
originalElement: HTMLElement| null;
cloneElement: HTMLElement| null;
}