From 5417a2b6b188cfda3fd3fd23ba9611298eaa6055 Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Tue, 30 Sep 2025 00:13:52 +0200 Subject: [PATCH] Improves drag and drop functionality Refactors drag and drop logic to use the dragged clone consistently, fixing issues with event handling and element manipulation during drag operations. Also includes a fix where the original element is removed after a drag is completed. Adds column bounds cache update after drag operations for improved column detection. --- src/managers/AllDayManager.ts | 27 ++++++++++------- src/managers/DragDropManager.ts | 42 +++++++++++++-------------- src/managers/HeaderManager.ts | 4 +-- src/renderers/EventRendererManager.ts | 6 ++-- src/renderers/GridStyleManager.ts | 1 + src/types/EventTypes.ts | 9 +++--- wwwroot/css/calendar-layout-css.css | 6 ++-- 7 files changed, 50 insertions(+), 45 deletions(-) diff --git a/src/managers/AllDayManager.ts b/src/managers/AllDayManager.ts index aea47d9..7ffeda8 100644 --- a/src/managers/AllDayManager.ts +++ b/src/managers/AllDayManager.ts @@ -74,7 +74,7 @@ export class AllDayManager { }); eventBus.on('drag:column-change', (event) => { - const { draggedElement, draggedClone, mousePosition } = (event as CustomEvent).detail; + const { originalElement: draggedElement, draggedClone, mousePosition } = (event as CustomEvent).detail; if (draggedClone == null) return; @@ -316,8 +316,13 @@ export class AllDayManager { * During drag: Place in row 1 only, calculate column from targetDate */ private handleConvertToAllDay(payload: DragMouseEnterHeaderEventPayload): void { + + if(payload.draggedClone?.dataset == null) + console.error("payload.cloneElement.dataset.eventId is null"); + + console.log('🔄 AllDayManager: Converting to all-day (row 1 only during drag)', { - eventId: payload.cloneElement.dataset.eventId, + eventId: payload.draggedClone.dataset.eventId, targetDate: payload.targetColumn }); @@ -325,17 +330,18 @@ export class AllDayManager { let allDayContainer = this.getAllDayContainer(); - payload.cloneElement.removeAttribute('style'); - payload.cloneElement.classList.add('all-day-style'); - payload.cloneElement.style.gridRow = '1'; - payload.cloneElement.style.gridColumn = payload.targetColumn.index.toString(); - payload.cloneElement.dataset.allday = 'true'; // Set the all-day attribute for filtering + payload.draggedClone.removeAttribute('style'); + payload.draggedClone.style.gridRow = '1'; + payload.draggedClone.style.gridColumn = payload.targetColumn.index.toString(); + payload.draggedClone.dataset.allday = 'true'; // Set the all-day attribute for filtering // Add to container - allDayContainer?.appendChild(payload.cloneElement); + allDayContainer?.appendChild(payload.draggedClone); + + ColumnDetectionUtils.updateColumnBoundsCache(); console.log('✅ AllDayManager: Converted to all-day style (simple row 1)', { - eventId: payload.cloneElement.dataset.eventId, + eventId: payload.draggedClone.dataset.eventId, gridColumn: payload.targetColumn, gridRow: 1 }); @@ -459,7 +465,7 @@ export class AllDayManager { if (oldGridArea !== newGridArea) { changedCount++; - const element = document.querySelector(`[data-event-id="${layout.calenderEvent.id}"]`) as HTMLElement; + const element = dragEndEvent.draggedClone; //:end document.querySelector(`[data-event-id="${layout.calenderEvent.id}"]`) as HTMLElement; if (element) { // Add transition class for smooth animation @@ -481,6 +487,7 @@ export class AllDayManager { dragEndEvent.draggedClone.style.opacity = ''; // 7. Restore original element opacity + dragEndEvent.originalElement.remove(); //originalElement.style.opacity = ''; // 8. Check if height adjustment is needed diff --git a/src/managers/DragDropManager.ts b/src/managers/DragDropManager.ts index 556933a..653be03 100644 --- a/src/managers/DragDropManager.ts +++ b/src/managers/DragDropManager.ts @@ -129,7 +129,7 @@ export class DragDropManager { // Clean up drag state first this.cleanupDragState(); - + ColumnDetectionUtils.updateColumnBoundsCache(); this.lastMousePosition = { x: event.clientX, y: event.clientY }; this.lastLoggedPosition = { x: event.clientX, y: event.clientY }; this.initialMousePosition = { x: event.clientX, y: event.clientY }; @@ -171,16 +171,18 @@ export class DragDropManager { this.currentMouseY = event.clientY; this.lastMousePosition = { x: event.clientX, y: event.clientY }; - // Check for header enter/leave during drag - if (this.draggedElement) { - this.checkHeaderEnterLeave(event); - } - if (event.buttons === 1 && this.draggedElement) { + + if (event.buttons === 1) { const currentPosition: MousePosition = { x: event.clientX, y: event.clientY }; //TODO: Is this really needed? why not just use event.clientX + Y directly + // Check for header enter/leave during drag + if (this.draggedClone) { + this.checkHeaderEnterLeave(event); + } + // Check if we need to start drag (movement threshold) - if (!this.isDragStarted) { + if (!this.isDragStarted && this.draggedElement) { const deltaX = Math.abs(currentPosition.x - this.initialMousePosition.x); const deltaY = Math.abs(currentPosition.y - this.initialMousePosition.y); const totalMovement = Math.sqrt(deltaX * deltaX + deltaY * deltaY); @@ -214,7 +216,7 @@ export class DragDropManager { } // Continue with normal drag behavior only if drag has started - if (this.isDragStarted) { + if (this.isDragStarted && this.draggedElement && this.draggedClone) { const deltaY = Math.abs(currentPosition.y - this.lastLoggedPosition.y); // Check for snap interval vertical movement (normal drag behavior) @@ -227,6 +229,7 @@ export class DragDropManager { // Emit drag move event with snapped position (normal behavior) const dragMovePayload: DragMoveEventPayload = { draggedElement: this.draggedElement, + draggedClone: this.draggedClone, mousePosition: currentPosition, snappedY: positionData.snappedY, columnBounds: positionData.column, @@ -246,7 +249,7 @@ export class DragDropManager { this.currentColumnBounds = newColumn; const dragColumnChangePayload: DragColumnChangeEventPayload = { - draggedElement: this.draggedElement, + originalElement: this.draggedElement, draggedClone: this.draggedClone, previousColumn, newColumn, @@ -276,6 +279,9 @@ export class DragDropManager { // Detect drop target (swp-day-column or swp-day-header) const dropTarget = this.detectDropTarget(mousePosition); + if(!dropTarget) + throw "dropTarget is null"; + console.log('🎯 DragDropManager: Emitting drag:end', { draggedElement: this.draggedElement.dataset.eventId, finalColumn: positionData.column, @@ -285,15 +291,14 @@ export class DragDropManager { }); const dragEndPayload: DragEndEventPayload = { - draggedElement: this.draggedElement, + originalElement: this.draggedElement, draggedClone: this.draggedClone, mousePosition, finalPosition: positionData, target: dropTarget }; this.eventBus.emit('drag:end', dragEndPayload); - - this.draggedElement.remove(); // TODO: this should be changed into a subscriber which only after a succesful placement is fired, not just mouseup as this can remove elements that are not placed. + } else { // This was just a click - emit click event instead @@ -489,7 +494,7 @@ export class DragDropManager { const isCurrentlyInHeader = !!headerElement; // Detect header enter - if (!this.isInHeader && isCurrentlyInHeader) { + if (!this.isInHeader && isCurrentlyInHeader && this.draggedClone) { this.isInHeader = true; // Calculate target date using existing method @@ -498,15 +503,11 @@ export class DragDropManager { if (targetColumn) { console.log('🎯 DragDropManager: Emitting drag:mouseenter-header', { targetDate: targetColumn }); - // 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 = { targetColumn: targetColumn, mousePosition: { x: event.clientX, y: event.clientY }, originalElement: this.draggedElement, - cloneElement: cloneElement + draggedClone: this.draggedClone }; this.eventBus.emit('drag:mouseenter-header', dragMouseEnterPayload); } @@ -525,15 +526,12 @@ export class DragDropManager { return; } - // 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: targetColumn.date, mousePosition: { x: event.clientX, y: event.clientY }, originalElement: this.draggedElement, - cloneElement: cloneElement + draggedClone: this.draggedClone }; this.eventBus.emit('drag:mouseleave-header', dragMouseLeavePayload); } diff --git a/src/managers/HeaderManager.ts b/src/managers/HeaderManager.ts index bc9c2a4..faa7c07 100644 --- a/src/managers/HeaderManager.ts +++ b/src/managers/HeaderManager.ts @@ -48,7 +48,7 @@ export class HeaderManager { // Create and store event listeners this.dragMouseEnterHeaderListener = (event: Event) => { - const { targetColumn: targetDate, mousePosition, originalElement, cloneElement } = (event as CustomEvent).detail; + const { targetColumn: targetDate, mousePosition, originalElement, draggedClone: cloneElement } = (event as CustomEvent).detail; console.log('🎯 HeaderManager: Received drag:mouseenter-header', { targetDate, @@ -65,7 +65,7 @@ export class HeaderManager { }; this.dragMouseLeaveHeaderListener = (event: Event) => { - const { targetDate, mousePosition, originalElement, cloneElement } = (event as CustomEvent).detail; + const { targetDate, mousePosition, originalElement, draggedClone: cloneElement } = (event as CustomEvent).detail; console.log('🚪 HeaderManager: Received drag:mouseleave-header', { targetDate, diff --git a/src/renderers/EventRendererManager.ts b/src/renderers/EventRendererManager.ts index 377d1f8..b058d74 100644 --- a/src/renderers/EventRendererManager.ts +++ b/src/renderers/EventRendererManager.ts @@ -210,7 +210,7 @@ export class EventRenderingService { // Handle drag end events and delegate to appropriate renderer this.eventBus.on('drag:end', (event: Event) => { - const { draggedElement, finalPosition, target } = (event as CustomEvent).detail; + const { originalElement: draggedElement, finalPosition, target } = (event as CustomEvent).detail; const finalColumn = finalPosition.column; const finalY = finalPosition.snappedY; const eventId = draggedElement.dataset.eventId || ''; @@ -252,14 +252,14 @@ export class EventRenderingService { } if (this.strategy.handleColumnChange) { - const eventId = columnChangeEvent.draggedElement.dataset.eventId || ''; + const eventId = columnChangeEvent.originalElement.dataset.eventId || ''; this.strategy.handleColumnChange(columnChangeEvent); } }); this.dragMouseLeaveHeaderListener = (event: Event) => { - const { targetDate, mousePosition, originalElement, cloneElement } = (event as CustomEvent).detail; + const { targetDate, mousePosition, originalElement, draggedClone: cloneElement } = (event as CustomEvent).detail; if (cloneElement) cloneElement.style.display = ''; diff --git a/src/renderers/GridStyleManager.ts b/src/renderers/GridStyleManager.ts index ec95154..c53207c 100644 --- a/src/renderers/GridStyleManager.ts +++ b/src/renderers/GridStyleManager.ts @@ -49,6 +49,7 @@ export class GridStyleManager { * Set time-related CSS variables */ private setTimeVariables(root: HTMLElement, gridSettings: GridSettings): void { + root.style.setProperty('--header-height', '80px'); // Fixed header height root.style.setProperty('--hour-height', `${gridSettings.hourHeight}px`); root.style.setProperty('--minute-height', `${gridSettings.hourHeight / 60}px`); root.style.setProperty('--snap-interval', gridSettings.snapInterval.toString()); diff --git a/src/types/EventTypes.ts b/src/types/EventTypes.ts index 0ceb180..e2b293f 100644 --- a/src/types/EventTypes.ts +++ b/src/types/EventTypes.ts @@ -57,6 +57,7 @@ export interface DragStartEventPayload { // Drag move event payload export interface DragMoveEventPayload { draggedElement: HTMLElement; + draggedClone: HTMLElement; mousePosition: MousePosition; mouseOffset: MousePosition; columnBounds: ColumnBounds | null; @@ -65,7 +66,7 @@ export interface DragMoveEventPayload { // Drag end event payload export interface DragEndEventPayload { - draggedElement: HTMLElement; + originalElement: HTMLElement; draggedClone: HTMLElement | null; mousePosition: MousePosition; finalPosition: { @@ -80,7 +81,7 @@ export interface DragMouseEnterHeaderEventPayload { targetColumn: ColumnBounds; mousePosition: MousePosition; originalElement: HTMLElement | null; - cloneElement: HTMLElement; + draggedClone: HTMLElement; } // Drag mouse leave header event payload @@ -88,12 +89,12 @@ export interface DragMouseLeaveHeaderEventPayload { targetDate: string | null; mousePosition: MousePosition; originalElement: HTMLElement| null; - cloneElement: HTMLElement| null; + draggedClone: HTMLElement| null; } // Drag column change event payload export interface DragColumnChangeEventPayload { - draggedElement: HTMLElement; + originalElement: HTMLElement; draggedClone: HTMLElement | null; previousColumn: ColumnBounds | null; newColumn: ColumnBounds; diff --git a/wwwroot/css/calendar-layout-css.css b/wwwroot/css/calendar-layout-css.css index 905790e..c06f78a 100644 --- a/wwwroot/css/calendar-layout-css.css +++ b/wwwroot/css/calendar-layout-css.css @@ -317,14 +317,12 @@ swp-allday-container swp-event { } /* Hide time element for all-day styled events */ -swp-allday-container swp-event swp-event-time, -swp-event.all-day-style swp-event-time { +swp-allday-container swp-event swp-event-time{ display: none; } /* Adjust title display for all-day styled events */ -swp-allday-container swp-event swp-event-title, -swp-event.all-day-style swp-event-title { +swp-allday-container swp-event swp-event-title { display: block; font-size: 12px; line-height: 18px;