From 134ee29cb199d5fe1ceb318fb21778f431ce1d30 Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Mon, 22 Sep 2025 17:51:24 +0200 Subject: [PATCH] Improves drag cancellation behavior Implements drag cancellation when the mouse leaves the calendar container during a drag operation. This prevents orphaned drag clones and restores the original event's state, enhancing user experience. All-day events now correctly recalculate their height upon drag cancellation, ensuring accurate rendering after clone removal. Refactors HeaderManager by removing redundant caching of the calendar header element. Adds new mock event data for September and October 2025 to expand testing and demonstration scenarios. --- src/data/mock-events.json | 470 ++++++++++++++++++++++++++++++++ src/managers/AllDayManager.ts | 13 + src/managers/DragDropManager.ts | 47 +++- src/managers/HeaderManager.ts | 17 +- 4 files changed, 531 insertions(+), 16 deletions(-) diff --git a/src/data/mock-events.json b/src/data/mock-events.json index ff7bb6f..904cf74 100644 --- a/src/data/mock-events.json +++ b/src/data/mock-events.json @@ -1208,5 +1208,475 @@ "allDay": false, "syncStatus": "synced", "metadata": { "duration": 120, "color": "#2196f3" } + }, + { + "id": "122", + "title": "Multi-Day Conference", + "start": "2025-09-22T00:00:00", + "end": "2025-09-24T23:59:59", + "type": "meeting", + "allDay": true, + "syncStatus": "synced", + "metadata": { "duration": 4320, "color": "#4caf50" } + }, + { + "id": "123", + "title": "Project Sprint", + "start": "2025-09-23T00:00:00", + "end": "2025-09-25T23:59:59", + "type": "work", + "allDay": true, + "syncStatus": "synced", + "metadata": { "duration": 4320, "color": "#2196f3" } + }, + { + "id": "124", + "title": "Training Week", + "start": "2025-09-29T00:00:00", + "end": "2025-10-03T23:59:59", + "type": "meeting", + "allDay": true, + "syncStatus": "synced", + "metadata": { "duration": 7200, "color": "#9c27b0" } + }, + { + "id": "125", + "title": "Holiday Weekend", + "start": "2025-10-04T00:00:00", + "end": "2025-10-06T23:59:59", + "type": "milestone", + "allDay": true, + "syncStatus": "synced", + "metadata": { "duration": 4320, "color": "#ff6f00" } + }, + { + "id": "126", + "title": "Client Visit", + "start": "2025-10-07T00:00:00", + "end": "2025-10-09T23:59:59", + "type": "meeting", + "allDay": true, + "syncStatus": "synced", + "metadata": { "duration": 4320, "color": "#e91e63" } + }, + { + "id": "127", + "title": "Development Marathon", + "start": "2025-10-13T00:00:00", + "end": "2025-10-15T23:59:59", + "type": "work", + "allDay": true, + "syncStatus": "synced", + "metadata": { "duration": 4320, "color": "#3f51b5" } + }, + { + "id": "128", + "title": "Morgen Standup", + "start": "2025-09-22T09:00:00", + "end": "2025-09-22T09:30:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 30, "color": "#ff5722" } + }, + { + "id": "129", + "title": "Klient Præsentation", + "start": "2025-09-22T14:00:00", + "end": "2025-09-22T15:30:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 90, "color": "#e91e63" } + }, + { + "id": "130", + "title": "Eftermiddags Kodning", + "start": "2025-09-22T16:00:00", + "end": "2025-09-22T18:00:00", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 120, "color": "#2196f3" } + }, + { + "id": "131", + "title": "Team Standup", + "start": "2025-09-23T09:00:00", + "end": "2025-09-23T09:30:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 30, "color": "#ff5722" } + }, + { + "id": "132", + "title": "Arkitektur Review", + "start": "2025-09-23T11:00:00", + "end": "2025-09-23T12:30:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 90, "color": "#009688" } + }, + { + "id": "133", + "title": "Frokost & Læring", + "start": "2025-09-23T12:30:00", + "end": "2025-09-23T13:30:00", + "type": "meal", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 60, "color": "#ff9800" } + }, + { + "id": "134", + "title": "Team Standup", + "start": "2025-09-24T09:00:00", + "end": "2025-09-24T09:30:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 30, "color": "#ff5722" } + }, + { + "id": "135", + "title": "Database Optimering", + "start": "2025-09-24T10:00:00", + "end": "2025-09-24T12:00:00", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 120, "color": "#3f51b5" } + }, + { + "id": "136", + "title": "Klient Opkald", + "start": "2025-09-24T15:00:00", + "end": "2025-09-24T16:00:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 60, "color": "#795548" } + }, + { + "id": "137", + "title": "Team Standup", + "start": "2025-09-25T09:00:00", + "end": "2025-09-25T09:30:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 30, "color": "#ff5722" } + }, + { + "id": "138", + "title": "Sprint Review", + "start": "2025-09-25T14:00:00", + "end": "2025-09-25T15:00:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 60, "color": "#607d8b" } + }, + { + "id": "139", + "title": "Retrospektiv", + "start": "2025-09-25T15:30:00", + "end": "2025-09-25T16:30:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 60, "color": "#9c27b0" } + }, + { + "id": "140", + "title": "Team Standup", + "start": "2025-09-26T09:00:00", + "end": "2025-09-26T09:30:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 30, "color": "#ff5722" } + }, + { + "id": "141", + "title": "Ny Feature Udvikling", + "start": "2025-09-26T10:00:00", + "end": "2025-09-26T12:00:00", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 120, "color": "#4caf50" } + }, + { + "id": "142", + "title": "Sikkerhedsgennemgang", + "start": "2025-09-26T14:00:00", + "end": "2025-09-26T15:30:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 90, "color": "#f44336" } + }, + { + "id": "143", + "title": "Weekend Hackathon", + "start": "2025-09-27T00:00:00", + "end": "2025-09-28T23:59:59", + "type": "work", + "allDay": true, + "syncStatus": "synced", + "metadata": { "duration": 2880, "color": "#673ab7" } + }, + { + "id": "144", + "title": "Team Standup", + "start": "2025-09-29T09:00:00", + "end": "2025-09-29T09:30:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 30, "color": "#ff5722" } + }, + { + "id": "145", + "title": "Månedlig Planlægning", + "start": "2025-09-29T10:00:00", + "end": "2025-09-29T12:00:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 120, "color": "#9c27b0" } + }, + { + "id": "146", + "title": "Performance Test", + "start": "2025-09-29T14:00:00", + "end": "2025-09-29T16:00:00", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 120, "color": "#00bcd4" } + }, + { + "id": "147", + "title": "Team Standup", + "start": "2025-09-30T09:00:00", + "end": "2025-09-30T09:30:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 30, "color": "#ff5722" } + }, + { + "id": "148", + "title": "Kvartal Afslutning", + "start": "2025-09-30T15:00:00", + "end": "2025-09-30T17:00:00", + "type": "milestone", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 120, "color": "#f44336" } + }, + { + "id": "149", + "title": "Oktober Kickoff", + "start": "2025-10-01T09:00:00", + "end": "2025-10-01T10:00:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 60, "color": "#4caf50" } + }, + { + "id": "150", + "title": "Sprint Planlægning", + "start": "2025-10-01T10:30:00", + "end": "2025-10-01T12:00:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 90, "color": "#673ab7" } + }, + { + "id": "151", + "title": "Eftermiddags Kodning", + "start": "2025-10-01T14:00:00", + "end": "2025-10-01T17:00:00", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 180, "color": "#2196f3" } + }, + { + "id": "152", + "title": "Team Standup", + "start": "2025-10-02T09:00:00", + "end": "2025-10-02T09:30:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 30, "color": "#ff5722" } + }, + { + "id": "153", + "title": "API Design Workshop", + "start": "2025-10-02T11:00:00", + "end": "2025-10-02T12:30:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 90, "color": "#009688" } + }, + { + "id": "154", + "title": "Bug Fixing Session", + "start": "2025-10-02T15:00:00", + "end": "2025-10-02T17:00:00", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 120, "color": "#ff5722" } + }, + { + "id": "155", + "title": "Team Standup", + "start": "2025-10-03T09:00:00", + "end": "2025-10-03T09:30:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 30, "color": "#ff5722" } + }, + { + "id": "156", + "title": "Klient Demo", + "start": "2025-10-03T14:00:00", + "end": "2025-10-03T15:00:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 60, "color": "#e91e63" } + }, + { + "id": "157", + "title": "Code Review Session", + "start": "2025-10-03T16:00:00", + "end": "2025-10-03T17:00:00", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 60, "color": "#009688" } + }, + { + "id": "158", + "title": "Fredag Standup", + "start": "2025-10-04T09:00:00", + "end": "2025-10-04T09:30:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 30, "color": "#ff5722" } + }, + { + "id": "159", + "title": "Uge Retrospektiv", + "start": "2025-10-04T15:00:00", + "end": "2025-10-04T16:00:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 60, "color": "#9c27b0" } + }, + { + "id": "160", + "title": "Weekend Projekt", + "start": "2025-10-05T10:00:00", + "end": "2025-10-05T14:00:00", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 240, "color": "#3f51b5" } + }, + { + "id": "161", + "title": "Teknisk Workshop", + "start": "2025-09-24T00:00:00", + "end": "2025-09-26T23:59:59", + "type": "meeting", + "allDay": true, + "syncStatus": "synced", + "metadata": { "duration": 4320, "color": "#795548" } + }, + { + "id": "162", + "title": "Produktudvikling Sprint", + "start": "2025-10-01T00:00:00", + "end": "2025-10-03T23:59:59", + "type": "work", + "allDay": true, + "syncStatus": "synced", + "metadata": { "duration": 4320, "color": "#cddc39" } + }, + { + "id": "163", + "title": "Tidlig Morgen Træning", + "start": "2025-09-23T06:30:00", + "end": "2025-09-23T07:30:00", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 60, "color": "#00bcd4" } + }, + { + "id": "164", + "title": "Sen Aften Deploy", + "start": "2025-09-25T22:00:00", + "end": "2025-09-26T00:30:00", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 150, "color": "#ffc107" } + }, + { + "id": "165", + "title": "Overlappende Møde A", + "start": "2025-09-30T10:00:00", + "end": "2025-09-30T11:30:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 90, "color": "#8bc34a" } + }, + { + "id": "166", + "title": "Overlappende Møde B", + "start": "2025-09-30T10:30:00", + "end": "2025-09-30T12:00:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 90, "color": "#ff6f00" } + }, + { + "id": "167", + "title": "Kort Check-in", + "start": "2025-10-02T09:45:00", + "end": "2025-10-02T10:00:00", + "type": "meeting", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 15, "color": "#607d8b" } + }, + { + "id": "168", + "title": "Lang Udviklingssession", + "start": "2025-10-04T09:00:00", + "end": "2025-10-04T13:00:00", + "type": "work", + "allDay": false, + "syncStatus": "synced", + "metadata": { "duration": 240, "color": "#2196f3" } } ] \ No newline at end of file diff --git a/src/managers/AllDayManager.ts b/src/managers/AllDayManager.ts index e0887cc..c88f775 100644 --- a/src/managers/AllDayManager.ts +++ b/src/managers/AllDayManager.ts @@ -107,6 +107,19 @@ export class AllDayManager { console.log('🎯 AllDayManager: Ending drag for all-day event', { eventId }); this.handleDragEnd(draggedElement, dragClone as HTMLElement, finalPosition.column); }); + + // Listen for drag cancellation to recalculate height + eventBus.on('drag:cancelled', (event) => { + const { draggedElement, reason } = (event as CustomEvent).detail; + + console.log('🚫 AllDayManager: Drag cancelled', { + eventId: draggedElement?.dataset?.eventId, + reason + }); + + // Recalculate all-day height since clones may have been removed + this.checkAndAnimateAllDayHeight(); + }); } /** diff --git a/src/managers/DragDropManager.ts b/src/managers/DragDropManager.ts index b4004b0..8db9840 100644 --- a/src/managers/DragDropManager.ts +++ b/src/managers/DragDropManager.ts @@ -62,6 +62,7 @@ export class DragDropManager { // Column bounds cache for coordinate-based column detection private columnBoundsCache: ColumnBounds[] = []; + // Auto-scroll properties private autoScrollAnimationId: number | null = null; private readonly scrollSpeed = 10; // pixels per frame @@ -108,6 +109,16 @@ export class DragDropManager { document.body.addEventListener('mousedown', this.boundHandlers.mouseDown); document.body.addEventListener('mouseup', this.boundHandlers.mouseUp); + // Add mouseleave listener to calendar container for drag cancellation + const calendarContainer = document.querySelector('swp-calendar-container'); + if (calendarContainer) { + calendarContainer.addEventListener('mouseleave', () => { + if (this.draggedElement && this.isDragStarted) { + this.cancelDrag(); + } + }); + } + // Initialize column bounds cache this.updateColumnBoundsCache(); @@ -303,7 +314,41 @@ export class DragDropManager { private cleanupAllClones(): void { // Remove clones from all possible locations const allClones = document.querySelectorAll('[data-event-id^="clone"]'); - allClones.forEach(clone => clone.remove()); + + if (allClones.length > 0) { + console.log(`🧹 DragDropManager: Removing ${allClones.length} clone(s)`); + allClones.forEach(clone => clone.remove()); + } + } + + /** + * Cancel drag operation when mouse leaves grid container + */ + private cancelDrag(): void { + if (!this.draggedElement) return; + + console.log('🚫 DragDropManager: Cancelling drag - mouse left grid container'); + + const draggedElement = this.draggedElement; + + // 1. Remove all clones + this.cleanupAllClones(); + + // 2. Restore original element + if (draggedElement) { + draggedElement.style.opacity = ''; + draggedElement.style.cursor = ''; + } + + // 3. Emit cancellation event + this.eventBus.emit('drag:cancelled', { + draggedElement: draggedElement, + reason: 'mouse-left-grid' + }); + + // 4. Clean up state + this.cleanupDragState(); + this.stopAutoScroll(); } /** diff --git a/src/managers/HeaderManager.ts b/src/managers/HeaderManager.ts index 1f9d1b3..8ac9204 100644 --- a/src/managers/HeaderManager.ts +++ b/src/managers/HeaderManager.ts @@ -11,7 +11,6 @@ import { DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload } fr * Separates event handling from rendering concerns */ export class HeaderManager { - private cachedCalendarHeader: HTMLElement | null = null; // Event listeners for drag events private dragMouseEnterHeaderListener: ((event: Event) => void) | null = null; @@ -40,10 +39,7 @@ export class HeaderManager { * Get cached calendar header element */ private getCalendarHeader(): HTMLElement | null { - if (!this.cachedCalendarHeader) { - this.cachedCalendarHeader = document.querySelector('swp-calendar-header'); - } - return this.cachedCalendarHeader; + return document.querySelector('swp-calendar-header'); } /** @@ -196,21 +192,13 @@ export class HeaderManager { calendarHeader = document.createElement('swp-calendar-header'); // Insert header as first child gridContainer.insertBefore(calendarHeader, gridContainer.firstChild); - this.cachedCalendarHeader = calendarHeader; + } } return calendarHeader; } - - /** - * Clear cached header reference - */ - public clearCache(): void { - this.cachedCalendarHeader = null; - } - /** * Clean up resources and event listeners */ @@ -228,6 +216,5 @@ export class HeaderManager { this.dragMouseEnterHeaderListener = null; this.dragMouseLeaveHeaderListener = null; - this.clearCache(); } } \ No newline at end of file