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:
parent
b4f5b29da3
commit
c7dcfbbaed
7 changed files with 583 additions and 410 deletions
161
docs/EventSystem-Analysis.md
Normal file
161
docs/EventSystem-Analysis.md
Normal 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*
|
||||||
|
|
@ -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}"]`);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -36,11 +42,13 @@ export class DragDropManager {
|
||||||
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
|
||||||
|
|
||||||
|
|
@ -76,7 +84,6 @@ export class DragDropManager {
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -114,74 +121,6 @@ export class DragDropManager {
|
||||||
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 {
|
||||||
|
|
@ -209,8 +148,7 @@ export class DragDropManager {
|
||||||
|
|
||||||
// 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();
|
||||||
|
|
@ -234,8 +172,14 @@ export class DragDropManager {
|
||||||
*/
|
*/
|
||||||
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)
|
||||||
|
|
@ -247,12 +191,14 @@ export class DragDropManager {
|
||||||
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;
|
||||||
|
|
@ -271,13 +217,14 @@ export class DragDropManager {
|
||||||
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
|
||||||
|
|
@ -290,7 +237,7 @@ export class DragDropManager {
|
||||||
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
|
||||||
|
|
@ -306,10 +253,9 @@ export class DragDropManager {
|
||||||
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
|
||||||
|
|
@ -317,33 +263,33 @@ export class DragDropManager {
|
||||||
|
|
||||||
// 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 }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -360,22 +306,6 @@ export class DragDropManager {
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optimized snap position calculation using PositionUtils
|
* Optimized snap position calculation using PositionUtils
|
||||||
*/
|
*/
|
||||||
|
|
@ -530,7 +460,7 @@ export class DragDropManager {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
@ -539,7 +469,7 @@ export class DragDropManager {
|
||||||
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);
|
||||||
|
|
@ -550,7 +480,7 @@ export class DragDropManager {
|
||||||
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
|
||||||
});
|
});
|
||||||
|
|
@ -577,26 +507,16 @@ export class DragDropManager {
|
||||||
* 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,6 +539,64 @@ 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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,19 @@ 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
|
||||||
this.setupHeaderDragListeners = this.setupHeaderDragListeners.bind(this);
|
this.setupHeaderDragListeners = this.setupHeaderDragListeners.bind(this);
|
||||||
|
|
@ -41,131 +44,71 @@ 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;
|
||||||
|
|
||||||
|
console.log('🎯 HeaderManager: Received drag:mouseenter-header', {
|
||||||
// Use mouseenter instead of mouseover to avoid continuous firing
|
targetDate,
|
||||||
this.headerEventListener = (event: Event) => {
|
originalElement: !!originalElement,
|
||||||
|
cloneElement: !!cloneElement
|
||||||
if (!document.querySelector('.dragging') !== null) {
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = event.target as HTMLElement;
|
|
||||||
|
|
||||||
console.log('🖱️ HeaderManager: mouseenter detected on:', target.tagName, target.className);
|
|
||||||
|
|
||||||
// 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
|
|
||||||
eventBus.emit('allday:ensure-container');
|
|
||||||
|
|
||||||
// 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) {
|
if (targetDate) {
|
||||||
|
// Ensure all-day container exists
|
||||||
|
this.ensureAllDayContainer();
|
||||||
|
|
||||||
const calendarType = calendarConfig.getCalendarMode();
|
const calendarType = calendarConfig.getCalendarMode();
|
||||||
const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType);
|
const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType);
|
||||||
|
|
||||||
console.log('✅ HeaderManager: Emitting header:mouseover with targetDate:', targetDate);
|
|
||||||
|
|
||||||
eventBus.emit('header:mouseover', {
|
}
|
||||||
element: allDayContainer,
|
};
|
||||||
|
|
||||||
|
this.dragMouseLeaveHeaderListener = (event: Event) => {
|
||||||
|
const { targetDate, mousePosition, originalElement, cloneElement } = (event as CustomEvent<DragMouseLeaveHeaderEventPayload>).detail;
|
||||||
|
|
||||||
|
console.log('🚪 HeaderManager: Received drag:mouseleave-header', {
|
||||||
targetDate,
|
targetDate,
|
||||||
headerRenderer
|
originalElement: !!originalElement,
|
||||||
|
cloneElement: !!cloneElement
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
console.log('❌ HeaderManager: Could not calculate target date from mouse position');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Header mouseleave listener
|
|
||||||
this.headerMouseLeaveListener = (event: Event) => {
|
|
||||||
console.log('🚪 HeaderManager: mouseleave detected');
|
|
||||||
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');
|
let allDayContainer = calendarHeader.querySelector('swp-allday-container');
|
||||||
|
|
||||||
if (this.headerEventListener) {
|
if (!allDayContainer) {
|
||||||
// Remove mouseenter listener with capture flag
|
console.log('📍 HeaderManager: All-day container missing, requesting creation...');
|
||||||
calendarHeader.removeEventListener('mouseenter', this.headerEventListener, true);
|
eventBus.emit('allday:ensure-container');
|
||||||
console.log('✅ HeaderManager: Removed mouseenter listener');
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.headerMouseLeaveListener) {
|
|
||||||
calendarHeader.removeEventListener('mouseleave', this.headerMouseLeaveListener);
|
|
||||||
console.log('✅ HeaderManager: Removed mouseleave listener');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup navigation event listener
|
* Setup navigation event listener
|
||||||
|
|
@ -198,9 +141,6 @@ export class HeaderManager {
|
||||||
const calendarHeader = this.getOrCreateCalendarHeader();
|
const calendarHeader = this.getOrCreateCalendarHeader();
|
||||||
if (!calendarHeader) return;
|
if (!calendarHeader) return;
|
||||||
|
|
||||||
// Remove existing event listeners BEFORE clearing content
|
|
||||||
this.removeEventListeners();
|
|
||||||
|
|
||||||
// Clear existing content
|
// Clear existing content
|
||||||
calendarHeader.innerHTML = '';
|
calendarHeader.innerHTML = '';
|
||||||
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,6 +16,8 @@ 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;
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
@ -152,42 +149,46 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -31,3 +31,58 @@ 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;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue