diff --git a/src/managers/AllDayManager.ts b/src/managers/AllDayManager.ts index eed4f7f..2c6cb0c 100644 --- a/src/managers/AllDayManager.ts +++ b/src/managers/AllDayManager.ts @@ -4,7 +4,7 @@ import { eventBus } from '../core/EventBus'; import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig'; import { AllDayEventRenderer } from '../renderers/AllDayEventRenderer'; import { AllDayLayoutEngine, EventLayout } from '../utils/AllDayLayoutEngine'; -import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; +import { ColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; import { CalendarEvent } from '../types/CalendarTypes'; import { DragMouseEnterHeaderEventPayload, @@ -22,7 +22,7 @@ import { DragOffset, MousePosition } from '../types/DragDropTypes'; export class AllDayManager { private allDayEventRenderer: AllDayEventRenderer; private layoutEngine: AllDayLayoutEngine | null = null; - + // State tracking for differential updates private currentLayouts: Map = new Map(); private currentAllDayEvents: CalendarEvent[] = []; @@ -38,16 +38,16 @@ export class AllDayManager { */ private setupEventListeners(): void { eventBus.on('drag:mouseenter-header', (event) => { - const { targetDate, mousePosition, originalElement, cloneElement } = (event as CustomEvent).detail; + const { targetColumn: targetColumnBounds, mousePosition, originalElement, cloneElement } = (event as CustomEvent).detail; console.log('🔄 AllDayManager: Received drag:mouseenter-header', { - targetDate, + targetDate: targetColumnBounds, originalElementId: originalElement?.dataset?.eventId, originalElementTag: originalElement?.tagName }); - if (targetDate && cloneElement) { - this.handleConvertToAllDay(targetDate, cloneElement); + if (targetColumnBounds && cloneElement) { + this.handleConvertToAllDay(targetColumnBounds, cloneElement); } this.checkAndAnimateAllDayHeight(); @@ -78,8 +78,8 @@ export class AllDayManager { eventBus.on('drag:column-change', (event) => { const { draggedElement, draggedClone, mousePosition } = (event as CustomEvent).detail; - - if(draggedClone == null) + + if (draggedClone == null) return; // Filter: Only handle events where clone IS an all-day event @@ -88,27 +88,20 @@ export class AllDayManager { } console.log('🔄 AllDayManager: Handling drag:column-change for all-day event', { - eventId : draggedElement.dataset.eventId, + eventId: draggedElement.dataset.eventId, cloneId: draggedClone.dataset.eventId }); - + this.handleColumnChange(draggedClone, mousePosition); }); eventBus.on('drag:end', (event) => { - const { draggedElement, mousePosition, finalPosition, target, draggedClone } = (event as CustomEvent).detail; + let draggedElement: DragEndEventPayload = (event as CustomEvent).detail; - if (target != 'swp-day-header') // we are not inside the swp-day-header, so just ignore. + if (draggedElement.target != 'swp-day-header') // we are not inside the swp-day-header, so just ignore. return; - const eventId = draggedElement.dataset.eventId; - console.log('🎬 AllDayManager: Received drag:end', { - eventId: eventId, - finalPosition - }); - - console.log('🎯 AllDayManager: Ending drag for all-day event', { eventId }); - this.handleDragEnd(draggedElement, draggedClone as HTMLElement, { column: finalPosition.column || '', y: 0 }); + this.handleDragEnd(draggedElement); }); // Listen for drag cancellation to recalculate height @@ -273,7 +266,7 @@ export class AllDayManager { this.currentLayouts.clear(); const container = this.getAllDayContainer(); if (!container) return; - + container.querySelectorAll('swp-event').forEach(element => { const htmlElement = element as HTMLElement; const eventId = htmlElement.dataset.eventId; @@ -282,7 +275,7 @@ export class AllDayManager { this.currentLayouts.set(eventId, gridArea); } }); - + console.log('📋 AllDayManager: Stored current layouts', { count: this.currentLayouts.size, layouts: Array.from(this.currentLayouts.entries()) @@ -295,7 +288,7 @@ export class AllDayManager { public setCurrentEvents(events: CalendarEvent[], weekDates: string[]): void { this.currentAllDayEvents = events; this.currentWeekDates = weekDates; - + console.log('📝 AllDayManager: Set current events', { eventCount: events.length, weekDatesCount: weekDates.length @@ -307,17 +300,17 @@ export class AllDayManager { * This is the correct method that processes all events together for proper overlap detection */ public calculateAllDayEventsLayout(events: CalendarEvent[], weekDates: string[]): EventLayout[] { - + // Store current state this.currentAllDayEvents = events; this.currentWeekDates = weekDates; - + // Initialize layout engine with provided week dates this.layoutEngine = new AllDayLayoutEngine(weekDates); - + // Calculate layout for all events together - AllDayLayoutEngine handles CalendarEvents directly return this.layoutEngine.calculateLayout(events); - + } @@ -325,22 +318,20 @@ export class AllDayManager { * Handle conversion of timed event to all-day event - SIMPLIFIED * During drag: Place in row 1 only, calculate column from targetDate */ - private handleConvertToAllDay(targetDate: string, cloneElement: HTMLElement): void { + private handleConvertToAllDay(targetColumnBounds: ColumnBounds, cloneElement: HTMLElement): void { console.log('🔄 AllDayManager: Converting to all-day (row 1 only during drag)', { eventId: cloneElement.dataset.eventId, - targetDate + targetDate: targetColumnBounds }); // Get all-day container, request creation if needed let allDayContainer = this.getAllDayContainer(); - // Calculate target column from targetDate using ColumnDetectionUtils - const targetColumn = ColumnDetectionUtils.getColumnIndexFromDate(targetDate); cloneElement.removeAttribute('style'); cloneElement.classList.add('all-day-style'); cloneElement.style.gridRow = '1'; - cloneElement.style.gridColumn = targetColumn.toString(); + cloneElement.style.gridColumn = targetColumnBounds.index.toString(); cloneElement.dataset.allday = 'true'; // Set the all-day attribute for filtering // Add to container @@ -348,7 +339,7 @@ export class AllDayManager { console.log('✅ AllDayManager: Converted to all-day style (simple row 1)', { eventId: cloneElement.dataset.eventId, - gridColumn: targetColumn, + gridColumn: targetColumnBounds, gridRow: 1 }); } @@ -398,13 +389,16 @@ export class AllDayManager { if (!allDayContainer) return; // Calculate target column using ColumnDetectionUtils - const targetColumn = ColumnDetectionUtils.getColumnIndexFromX(mousePosition.x); + const targetColumn = ColumnDetectionUtils.getColumnBounds(mousePosition); + + if (targetColumn == null) + return; // Update clone position - ALWAYS keep in row 1 during drag // Use simple grid positioning that matches all-day container structure - dragClone.style.gridColumn = targetColumn.toString(); + dragClone.style.gridColumn = targetColumn.index.toString(); dragClone.style.gridRow = '1'; // Force row 1 during drag - dragClone.style.gridArea = `1 / ${targetColumn} / 2 / ${targetColumn + 1}`; + dragClone.style.gridArea = `1 / ${targetColumn.index} / 2 / ${targetColumn.index + 1}`; console.log('🔄 AllDayManager: Updated all-day drag clone position', { eventId: dragClone.dataset.eventId, @@ -418,31 +412,38 @@ export class AllDayManager { /** * Handle drag end for all-day events - WITH DIFFERENTIAL UPDATES */ - private handleDragEnd(originalElement: HTMLElement, dragClone: HTMLElement, finalPosition: { column: string; y: number }): void { + private handleDragEnd(dragEndEvent: DragEndEventPayload): void { console.log('🎯 AllDayManager: Starting drag end with differential updates', { - eventId: dragClone.dataset.eventId, - finalColumn: finalPosition.column + dragEndEvent }); + if (dragEndEvent.draggedClone == null) + return; + // 1. Store current layouts BEFORE any changes this.storeCurrentLayouts(); // 2. Normalize clone ID - const cloneId = dragClone.dataset.eventId; + const cloneId = dragEndEvent.draggedClone?.dataset.eventId; if (cloneId?.startsWith('clone-')) { - dragClone.dataset.eventId = cloneId.replace('clone-', ''); + dragEndEvent.draggedClone.dataset.eventId = cloneId.replace('clone-', ''); } // 3. Create temporary array with existing events + the dropped event - const droppedEventId = dragClone.dataset.eventId || ''; - const droppedEventDate = dragClone.dataset.allDayDate || finalPosition.column; - + let eventId = dragEndEvent.draggedClone.dataset.eventId; + let eventDate = dragEndEvent.finalPosition.column?.date; + let eventType = dragEndEvent.draggedClone.dataset.type; + + if (eventDate == null || eventId == null || eventType == null) + return; + + const droppedEvent: CalendarEvent = { - id: droppedEventId, - title: dragClone.dataset.title || dragClone.textContent || '', - start: new Date(droppedEventDate), - end: new Date(droppedEventDate), - type: 'work', + id: eventId, + title: dragEndEvent.draggedClone.dataset.title || dragEndEvent.draggedClone.textContent || '', + start: new Date(eventDate), + end: new Date(eventDate), + type: eventType, allDay: true, syncStatus: 'synced' }; @@ -477,13 +478,13 @@ export class AllDayManager { }); // 6. Clean up drag styles from the dropped clone - dragClone.classList.remove('dragging'); - dragClone.style.zIndex = ''; - dragClone.style.cursor = ''; - dragClone.style.opacity = ''; + dragEndEvent.draggedClone.classList.remove('dragging'); + dragEndEvent.draggedClone.style.zIndex = ''; + dragEndEvent.draggedClone.style.cursor = ''; + dragEndEvent.draggedClone.style.opacity = ''; // 7. Restore original element opacity - originalElement.style.opacity = ''; + //originalElement.style.opacity = ''; // 8. Check if height adjustment is needed this.checkAndAnimateAllDayHeight(); diff --git a/src/managers/DragDropManager.ts b/src/managers/DragDropManager.ts index e521918..f694b75 100644 --- a/src/managers/DragDropManager.ts +++ b/src/managers/DragDropManager.ts @@ -6,7 +6,7 @@ import { IEventBus } from '../types/CalendarTypes'; import { calendarConfig } from '../core/CalendarConfig'; import { PositionUtils } from '../utils/PositionUtils'; -import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; +import { ColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; import { SwpEventElement } from '../elements/SwpEventElement'; import { DragStartEventPayload, @@ -16,33 +16,30 @@ import { DragMouseLeaveHeaderEventPayload, DragColumnChangeEventPayload } from '../types/EventTypes'; +import { MousePosition } from '../types/DragDropTypes'; interface CachedElements { scrollContainer: HTMLElement | null; - currentColumn: HTMLElement | null; - lastColumnDate: string | null; } -interface Position { - x: number; - y: number; -} + export class DragDropManager { private eventBus: IEventBus; // Mouse tracking with optimized state - private lastMousePosition: Position = { x: 0, y: 0 }; - private lastLoggedPosition: Position = { x: 0, y: 0 }; + private lastMousePosition: MousePosition = { x: 0, y: 0 }; + private lastLoggedPosition: MousePosition = { x: 0, y: 0 }; private currentMouseY = 0; - private mouseOffset: Position = { x: 0, y: 0 }; - private initialMousePosition: Position = { x: 0, y: 0 }; + private mouseOffset: MousePosition = { x: 0, y: 0 }; + private initialMousePosition: MousePosition = { x: 0, y: 0 }; + private lastColumn: ColumnBounds | null = null; // Drag state private draggedElement!: HTMLElement | null; private draggedClone!: HTMLElement | null; - private currentColumn: string | null = null; + private currentColumnBounds: ColumnBounds | null = null; private isDragStarted = false; // Header tracking state @@ -51,12 +48,9 @@ export class DragDropManager { // Movement threshold to distinguish click from drag private readonly dragThreshold = 5; // pixels + private scrollContainer!: HTMLElement | null; // Cached DOM elements for performance - private cachedElements: CachedElements = { - scrollContainer: null, - currentColumn: null, - lastColumnDate: null - }; + @@ -106,7 +100,7 @@ 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 + this.scrollContainer = document.querySelector('swp-scrollable-content') as HTMLElement; const calendarContainer = document.querySelector('swp-calendar-container'); if (calendarContainer) { calendarContainer.addEventListener('mouseleave', () => { @@ -133,9 +127,9 @@ export class DragDropManager { private handleMouseDown(event: MouseEvent): void { - // Clean up drag state first - this.cleanupDragState(); - + // Clean up drag state first + this.cleanupDragState(); + this.lastMousePosition = { x: event.clientX, y: event.clientY }; this.lastLoggedPosition = { x: event.clientX, y: event.clientY }; this.initialMousePosition = { x: event.clientX, y: event.clientY }; @@ -160,21 +154,13 @@ export class DragDropManager { // Found an event - prepare for potential dragging if (eventElement) { this.draggedElement = eventElement; - + this.lastColumn = ColumnDetectionUtils.getColumnBounds(this.lastMousePosition) // Calculate mouse offset within event const eventRect = eventElement.getBoundingClientRect(); this.mouseOffset = { x: event.clientX - eventRect.left, y: event.clientY - eventRect.top }; - - // Detect current column - const column = this.detectColumn(event.clientX, event.clientY); - if (column) { - this.currentColumn = column; - } - - // Don't emit drag:start yet - wait for movement threshold } } @@ -191,7 +177,7 @@ export class DragDropManager { } if (event.buttons === 1 && this.draggedElement) { - const currentPosition: Position = { x: event.clientX, y: event.clientY }; + const currentPosition: MousePosition = { x: event.clientX, y: event.clientY }; //TODO: Is this really needed? why not just use event.clientX + Y directly // Check if we need to start drag (movement threshold) if (!this.isDragStarted) { @@ -203,20 +189,22 @@ export class DragDropManager { // Start drag - emit drag:start event this.isDragStarted = true; - // Create SwpEventElement from existing DOM element and clone it - const originalSwpEvent = SwpEventElement.fromExistingElement(this.draggedElement); - const clonedSwpEvent = originalSwpEvent.createClone(); - - // Get the cloned DOM element - this.draggedClone = clonedSwpEvent.getElement(); + // Detect current column + this.currentColumnBounds = ColumnDetectionUtils.getColumnBounds(currentPosition); + + // Create SwpEventElement from existing DOM element and clone it + const originalSwpEvent = SwpEventElement.fromExistingElement(this.draggedElement); + const clonedSwpEvent = originalSwpEvent.createClone(); + + // Get the cloned DOM element + this.draggedClone = clonedSwpEvent.getElement(); - const dragStartPayload: DragStartEventPayload = { draggedElement: this.draggedElement, draggedClone: this.draggedClone, mousePosition: this.initialMousePosition, mouseOffset: this.mouseOffset, - column: this.currentColumn + columnBounds: this.currentColumnBounds }; this.eventBus.emit('drag:start', dragStartPayload); } else { @@ -241,20 +229,21 @@ export class DragDropManager { draggedElement: this.draggedElement, mousePosition: currentPosition, snappedY: positionData.snappedY, - column: positionData.column, + columnBounds: positionData.column, mouseOffset: this.mouseOffset }; this.eventBus.emit('drag:move', dragMovePayload); } // Check for auto-scroll - this.checkAutoScroll(event); + this.checkAutoScroll(currentPosition); // Check for column change using cached data - const newColumn = this.getColumnFromCache(currentPosition); - if (newColumn && newColumn !== this.currentColumn) { - const previousColumn = this.currentColumn; - this.currentColumn = newColumn; + const newColumn = ColumnDetectionUtils.getColumnBounds(currentPosition); + + if (newColumn && newColumn !== this.currentColumnBounds) { + const previousColumn = this.currentColumnBounds; + this.currentColumnBounds = newColumn; const dragColumnChangePayload: DragColumnChangeEventPayload = { draggedElement: this.draggedElement, @@ -276,16 +265,10 @@ export class DragDropManager { this.stopAutoScroll(); if (this.draggedElement) { - // Store variables locally before cleanup - //const draggedElement = this.draggedElement; - const isDragStarted = this.isDragStarted; - - - // Only emit drag:end if drag was actually started - if (isDragStarted) { - const mousePosition: Position = { x: event.clientX, y: event.clientY }; + if (this.isDragStarted) { + const mousePosition: MousePosition = { x: event.clientX, y: event.clientY }; // Use consolidated position calculation const positionData = this.calculateDragPosition(mousePosition); @@ -298,12 +281,12 @@ export class DragDropManager { finalColumn: positionData.column, finalY: positionData.snappedY, dropTarget: dropTarget, - isDragStarted: isDragStarted + isDragStarted: this.isDragStarted }); const dragEndPayload: DragEndEventPayload = { draggedElement: this.draggedElement, - draggedClone : this.draggedClone, + draggedClone: this.draggedClone, mousePosition, finalPosition: positionData, target: dropTarget @@ -325,7 +308,7 @@ export class DragDropManager { private cleanupAllClones(): void { // Remove clones from all possible locations const allClones = document.querySelectorAll('[data-event-id^="clone"]'); - + if (allClones.length > 0) { console.log(`🧹 DragDropManager: Removing ${allClones.length} clone(s)`); allClones.forEach(clone => clone.remove()); @@ -365,9 +348,13 @@ export class DragDropManager { /** * Consolidated position calculation method using PositionUtils */ - private calculateDragPosition(mousePosition: Position): { column: string | null; snappedY: number } { - const column = this.detectColumn(mousePosition.x, mousePosition.y); - const snappedY = this.calculateSnapPosition(mousePosition.y, column); + private calculateDragPosition(mousePosition: MousePosition): { column: ColumnBounds | null; snappedY: number } { + let column = ColumnDetectionUtils.getColumnBounds(mousePosition); + let snappedY = 0; + if (column) { + snappedY = this.calculateSnapPosition(mousePosition.y, column); + return { column, snappedY }; + } return { column, snappedY }; } @@ -375,100 +362,33 @@ export class DragDropManager { /** * Optimized snap position calculation using PositionUtils */ - private calculateSnapPosition(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; - - // Use PositionUtils for consistent snapping behavior - const snappedY = PositionUtils.getPositionFromCoordinate(mouseY, columnElement); + private calculateSnapPosition(mouseY: number, column: ColumnBounds): number { + const snappedY = PositionUtils.getPositionFromCoordinate(mouseY, column); return Math.max(0, snappedY); } - /** - * Coordinate-based column detection (replaces DOM traversal) - */ - private detectColumn(mouseX: number, mouseY: number): string | null { - // Brug den koordinatbaserede metode direkte - const columnDate = ColumnDetectionUtils.getColumnDateFromX(mouseX); - - // Opdater stadig den eksisterende cache hvis vi finder en kolonne - if (columnDate && columnDate !== this.cachedElements.lastColumnDate) { - const columnElement = document.querySelector(`swp-day-column[data-date="${columnDate}"]`) as HTMLElement; - if (columnElement) { - this.cachedElements.currentColumn = columnElement; - this.cachedElements.lastColumnDate = columnDate; - } - } - - return columnDate; - } - - /** - * Get column from cache or detect new one - */ - private getColumnFromCache(mousePosition: Position): string | null { - // Try to use cached column first - if (this.cachedElements.currentColumn && this.cachedElements.lastColumnDate) { - const rect = this.cachedElements.currentColumn.getBoundingClientRect(); - if (mousePosition.x >= rect.left && mousePosition.x <= rect.right) { - return this.cachedElements.lastColumnDate; - } - } - - // Cache miss - detect new column - return this.detectColumn(mousePosition.x, mousePosition.y); - } - - /** - * Get cached column element or query for new one - */ - private getCachedColumnElement(columnDate: string | null): HTMLElement | null { - if (!columnDate) return null; - - // Return cached element if it matches - if (this.cachedElements.lastColumnDate === columnDate && this.cachedElements.currentColumn) { - return this.cachedElements.currentColumn; - } - - // Query for new element and cache it - const element = document.querySelector(`swp-day-column[data-date="${columnDate}"]`) as HTMLElement; - if (element) { - this.cachedElements.currentColumn = element; - this.cachedElements.lastColumnDate = columnDate; - } - - return element; - } - /** * Optimized auto-scroll check with cached container */ - private checkAutoScroll(event: MouseEvent): void { - // Use cached scroll container - if (!this.cachedElements.scrollContainer) { - this.cachedElements.scrollContainer = document.querySelector('swp-scrollable-content') as HTMLElement; - if (!this.cachedElements.scrollContainer) { - return; - } - } + private checkAutoScroll(mousePosition: MousePosition): void { - const containerRect = this.cachedElements.scrollContainer.getBoundingClientRect(); - const mouseY = event.clientY; + if (this.scrollContainer == null) + return; + + const containerRect = this.scrollContainer.getBoundingClientRect(); + const mouseY = mousePosition.clientY; // Calculate distances from edges - const distanceFromTop = mouseY - containerRect.top; - const distanceFromBottom = containerRect.bottom - mouseY; + const distanceFromTop = mousePosition.y - containerRect.top; + const distanceFromBottom = containerRect.bottom - mousePosition.y; // Check if we need to scroll if (distanceFromTop <= this.scrollThreshold && distanceFromTop > 0) { - this.startAutoScroll('up'); + this.startAutoScroll('up', mousePosition); } else if (distanceFromBottom <= this.scrollThreshold && distanceFromBottom > 0) { - this.startAutoScroll('down'); + this.startAutoScroll('down', mousePosition); } else { this.stopAutoScroll(); } @@ -477,25 +397,26 @@ export class DragDropManager { /** * Optimized auto-scroll with cached container reference */ - private startAutoScroll(direction: 'up' | 'down'): void { + private startAutoScroll(direction: 'up' | 'down', event: MousePosition): void { if (this.autoScrollAnimationId !== null) return; const scroll = () => { - if (!this.cachedElements.scrollContainer || !this.draggedElement) { + if (!this.scrollContainer || !this.draggedElement) { this.stopAutoScroll(); return; } const scrollAmount = direction === 'up' ? -this.scrollSpeed : this.scrollSpeed; - this.cachedElements.scrollContainer.scrollTop += scrollAmount; + this.scrollContainer.scrollTop += scrollAmount; // Emit updated position during scroll - adjust for scroll movement if (this.draggedElement) { // During autoscroll, we need to calculate position relative to the scrolled content // The mouse hasn't moved, but the content has scrolled - const columnElement = this.getCachedColumnElement(this.currentColumn); + const columnElement = ColumnDetectionUtils.getColumnBounds(event); + if (columnElement) { - const columnRect = columnElement.getBoundingClientRect(); + const columnRect = columnElement.boundingClientRect; // Calculate free position relative to column, accounting for scroll movement (no snapping during scroll) const relativeY = this.currentMouseY - columnRect.top - this.mouseOffset.y; const freeY = Math.max(0, relativeY); @@ -503,7 +424,7 @@ export class DragDropManager { this.eventBus.emit('drag:auto-scroll', { draggedElement: this.draggedElement, snappedY: freeY, // Actually free position during scroll - scrollTop: this.cachedElements.scrollContainer.scrollTop + scrollTop: this.scrollContainer.scrollTop }); } } @@ -530,20 +451,15 @@ export class DragDropManager { private cleanupDragState(): void { this.draggedElement = null; this.draggedClone = null; - this.currentColumn = null; this.isDragStarted = false; this.isInHeader = false; - - // Clear cached elements - this.cachedElements.currentColumn = null; - this.cachedElements.lastColumnDate = null; } /** * Detect drop target - whether dropped in swp-day-column or swp-day-header */ - private detectDropTarget(position: Position): 'swp-day-column' | 'swp-day-header' | null { - + private detectDropTarget(position: MousePosition): 'swp-day-column' | 'swp-day-header' | null { + // Traverse up the DOM tree to find the target container let currentElement = this.draggedClone; while (currentElement && currentElement !== document.body) { @@ -563,6 +479,8 @@ export class DragDropManager { * Check for header enter/leave during drag operations */ private checkHeaderEnterLeave(event: MouseEvent): void { + + let position: MousePosition = { x: event.clientX, y: event.clientY }; const elementAtPosition = document.elementFromPoint(event.clientX, event.clientY); if (!elementAtPosition) return; @@ -575,17 +493,17 @@ export class DragDropManager { this.isInHeader = true; // Calculate target date using existing method - const targetDate = ColumnDetectionUtils.getColumnDateFromX(event.clientX); + const targetColumn = ColumnDetectionUtils.getColumnBounds(position); - if (targetDate) { - console.log('🎯 DragDropManager: Emitting drag:mouseenter-header', { targetDate }); + 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 = { - targetDate, + targetColumn: targetColumn, mousePosition: { x: event.clientX, y: event.clientY }, originalElement: this.draggedElement, cloneElement: cloneElement @@ -601,14 +519,18 @@ export class DragDropManager { console.log('🚪 DragDropManager: Emitting drag:mouseleave-header'); // Calculate target date using existing method - const targetDate = ColumnDetectionUtils.getColumnDateFromX(event.clientX); + const targetColumn = ColumnDetectionUtils.getColumnBounds(position); + if (!targetColumn) { + console.warn("No column detected, unknown reason"); + 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, + targetDate: targetColumn.date, mousePosition: { x: event.clientX, y: event.clientY }, originalElement: this.draggedElement, cloneElement: cloneElement @@ -628,11 +550,6 @@ export class DragDropManager { document.body.removeEventListener('mousedown', this.boundHandlers.mouseDown); document.body.removeEventListener('mouseup', this.boundHandlers.mouseUp); - // Clear all cached elements - this.cachedElements.scrollContainer = null; - this.cachedElements.currentColumn = null; - this.cachedElements.lastColumnDate = null; - // Clean up drag state this.cleanupDragState(); } diff --git a/src/managers/HeaderManager.ts b/src/managers/HeaderManager.ts index d31376e..bc9c2a4 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 { targetDate, mousePosition, originalElement, cloneElement } = (event as CustomEvent).detail; + const { targetColumn: targetDate, mousePosition, originalElement, cloneElement } = (event as CustomEvent).detail; console.log('🎯 HeaderManager: Received drag:mouseenter-header', { targetDate, diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index 70581f2..a313e6b 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -9,6 +9,8 @@ import { SwpEventElement } from '../elements/SwpEventElement'; import { TimeFormatter } from '../utils/TimeFormatter'; import { PositionUtils } from '../utils/PositionUtils'; import { DragOffset, StackLinkData } from '../types/DragDropTypes'; +import { ColumnBounds } from '../utils/ColumnDetectionUtils'; +import { DragColumnChangeEventPayload, DragMoveEventPayload, DragStartEventPayload } from '../types/EventTypes'; /** * Interface for event rendering strategies @@ -16,12 +18,12 @@ import { DragOffset, StackLinkData } from '../types/DragDropTypes'; export interface EventRendererStrategy { renderEvents(events: CalendarEvent[], container: HTMLElement): void; clearEvents(container?: HTMLElement): void; - handleDragStart?(payload: import('../types/EventTypes').DragStartEventPayload): void; - handleDragMove?(eventId: string, snappedY: number, column: string, mouseOffset: DragOffset): void; + handleDragStart?(payload: DragStartEventPayload): void; + handleDragMove?(payload: DragMoveEventPayload): void; handleDragAutoScroll?(eventId: string, snappedY: number): void; - handleDragEnd?(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: string, finalY: number): void; + handleDragEnd?(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: ColumnBounds, finalY: number): void; handleEventClick?(eventId: string, originalElement: HTMLElement): void; - handleColumnChange?(eventId: string, newColumn: string): void; + handleColumnChange?(payload: DragColumnChangeEventPayload): void; handleNavigationCompleted?(): void; } @@ -160,13 +162,9 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { /** * Handle drag start event */ - public handleDragStart(payload: import('../types/EventTypes').DragStartEventPayload): void { - const originalElement = payload.draggedElement; - const eventId = originalElement.dataset.eventId || ''; - const mouseOffset = payload.mouseOffset; - const column = payload.column || ''; - - this.originalEvent = originalElement; + public handleDragStart(payload: DragStartEventPayload): void { + + this.originalEvent = payload.draggedElement;; // Use the clone from the payload instead of creating a new one this.draggedClone = payload.draggedClone; @@ -176,35 +174,29 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { this.applyDragStyling(this.draggedClone); // Add to current column's events layer (not directly to column) - const columnElement = document.querySelector(`swp-day-column[data-date="${column}"]`); - if (columnElement) { - const eventsLayer = columnElement.querySelector('swp-events-layer'); - if (eventsLayer) { - eventsLayer.appendChild(this.draggedClone); - } else { - // Fallback to column if events layer not found - columnElement.appendChild(this.draggedClone); - } + const eventsLayer = payload.columnBounds?.element.querySelector('swp-events-layer'); + if (eventsLayer) { + eventsLayer.appendChild(this.draggedClone); } } // Make original semi-transparent - originalElement.style.opacity = '0.3'; - originalElement.style.userSelect = 'none'; + this.originalEvent.style.opacity = '0.3'; + this.originalEvent.style.userSelect = 'none'; } /** * Handle drag move event */ - public handleDragMove(eventId: string, snappedY: number, column: string, mouseOffset: DragOffset): void { + public handleDragMove(payload: DragMoveEventPayload): void { if (!this.draggedClone) return; // Update position - this.draggedClone.style.top = snappedY + 'px'; + this.draggedClone.style.top = payload.snappedY + 'px'; // Update timestamp display - this.updateCloneTimestamp(this.draggedClone, snappedY); + this.updateCloneTimestamp(this.draggedClone, payload.snappedY); } @@ -224,26 +216,20 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { /** * Handle column change during drag */ - public handleColumnChange(eventId: string, newColumn: string): void { + public handleColumnChange(dragColumnChangeEvent: DragColumnChangeEventPayload): void { if (!this.draggedClone) return; - // Move clone to new column's events layer - const newColumnElement = document.querySelector(`swp-day-column[data-date="${newColumn}"]`); - if (newColumnElement) { - const eventsLayer = newColumnElement.querySelector('swp-events-layer'); - if (eventsLayer && this.draggedClone.parentElement !== eventsLayer) { - eventsLayer.appendChild(this.draggedClone); - } else if (!eventsLayer && this.draggedClone.parentElement !== newColumnElement) { - // Fallback to column if events layer not found - newColumnElement.appendChild(this.draggedClone); - } + const eventsLayer = dragColumnChangeEvent.newColumn.element.querySelector('swp-events-layer'); + if (eventsLayer && this.draggedClone.parentElement !== eventsLayer) { + eventsLayer.appendChild(this.draggedClone); + } } /** * Handle drag end event */ - public handleDragEnd(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: string, finalY: number): void { + public handleDragEnd(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: ColumnBounds, finalY: number): void { if (!draggedClone || !originalElement) { console.warn('Missing draggedClone or originalElement'); @@ -398,11 +384,9 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { /** * Handle overlap detection and re-rendering after drag-drop */ - private handleDragDropOverlaps(droppedElement: HTMLElement, targetColumn: string): void { - const targetColumnElement = document.querySelector(`swp-day-column[data-date="${targetColumn}"]`); - if (!targetColumnElement) return; + private handleDragDropOverlaps(droppedElement: HTMLElement, targetColumn: ColumnBounds): void { - const eventsLayer = targetColumnElement.querySelector('swp-events-layer') as HTMLElement; + const eventsLayer = targetColumn.element.querySelector('swp-events-layer') as HTMLElement; if (!eventsLayer) return; // Convert dropped element to CalendarEvent with new position @@ -543,16 +527,16 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { renderEvents(events: CalendarEvent[], container: HTMLElement): void { - + // Filter out all-day events - they should be handled by AllDayEventRenderer const timedEvents = events.filter(event => !event.allDay); - + console.log('🎯 EventRenderer: Filtering events', { totalEvents: events.length, timedEvents: timedEvents.length, filteredOutAllDay: events.length - timedEvents.length }); - + // Find columns in the specific container for regular events const columns = this.getColumns(container); @@ -561,7 +545,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { const eventsLayer = column.querySelector('swp-events-layer'); if (eventsLayer) { - + this.handleEventOverlaps(columnEvents, eventsLayer as HTMLElement); } }); diff --git a/src/renderers/EventRendererManager.ts b/src/renderers/EventRendererManager.ts index 581b7d0..377d1f8 100644 --- a/src/renderers/EventRendererManager.ts +++ b/src/renderers/EventRendererManager.ts @@ -29,7 +29,7 @@ export class EventRenderingService { // Cache strategy at initialization const calendarType = calendarConfig.getCalendarMode(); this.strategy = CalendarTypeFactory.getEventRenderer(calendarType); - + // Initialize all-day event renderer and manager this.allDayEventRenderer = new AllDayEventRenderer(); this.allDayManager = new AllDayManager(); @@ -92,7 +92,7 @@ export class EventRenderingService { startDate: startDate.toISOString(), endDate: endDate.toISOString() }); - + // Render all-day events using period from header this.renderAllDayEventsForPeriod(startDate, endDate); }); @@ -181,22 +181,21 @@ export class EventRenderingService { this.eventBus.on('drag:start', (event: Event) => { const dragStartPayload = (event as CustomEvent).detail; // Use the draggedElement directly - no need for DOM query - if (dragStartPayload.draggedElement && this.strategy.handleDragStart && dragStartPayload.column) { + if (dragStartPayload.draggedElement && this.strategy.handleDragStart && dragStartPayload.columnBounds) { this.strategy.handleDragStart(dragStartPayload); } }); // Handle drag move this.eventBus.on('drag:move', (event: Event) => { - const { draggedElement, snappedY, column, mouseOffset } = (event as CustomEvent).detail; - + let dragEvent = (event as CustomEvent).detail; + // Filter: Only handle events WITHOUT data-allday attribute (normal timed events) - if (draggedElement.hasAttribute('data-allday')) { + if (dragEvent.draggedElement.hasAttribute('data-allday')) { return; // This is an all-day event, let AllDayManager handle it } - if (this.strategy.handleDragMove && column) { - const eventId = draggedElement.dataset.eventId || ''; - this.strategy.handleDragMove(eventId, snappedY, column, mouseOffset); + if (this.strategy.handleDragMove) { + this.strategy.handleDragMove(dragEvent); } }); @@ -239,22 +238,22 @@ export class EventRenderingService { // Use draggedElement directly - no need for DOM query if (draggedElement && this.strategy.handleEventClick) { const eventId = draggedElement.dataset.eventId || ''; - this.strategy.handleEventClick(eventId, draggedElement); + this.strategy.handleEventClick(eventId, draggedElement); //TODO: fix this redundant parameters } }); // Handle column change this.eventBus.on('drag:column-change', (event: Event) => { - const { draggedElement, draggedClone, newColumn } = (event as CustomEvent).detail; - + let columnChangeEvent = (event as CustomEvent).detail; + // Filter: Only handle events where clone is NOT an all-day event (normal timed events) - if (draggedClone && draggedClone.hasAttribute('data-allday')) { - return; // This is an all-day event, let AllDayManager handle it + if (columnChangeEvent.draggedClone && columnChangeEvent.draggedClone.hasAttribute('data-allday')) { + return; } - + if (this.strategy.handleColumnChange) { - const eventId = draggedElement.dataset.eventId || ''; - this.strategy.handleColumnChange(eventId, newColumn); //TODO: Should be refactored to use payload, no need to lookup clone again inside + const eventId = columnChangeEvent.draggedElement.dataset.eventId || ''; + this.strategy.handleColumnChange(columnChangeEvent); } }); @@ -363,17 +362,17 @@ export class EventRenderingService { // Get actual visible dates from DOM headers instead of generating them const weekDates = this.getVisibleDatesFromDOM(); - + console.log('🔍 EventRenderingService: Using visible dates from DOM', { weekDates, count: weekDates.length }); - + // Pass current events to AllDayManager for state tracking this.allDayManager.setCurrentEvents(allDayEvents, weekDates); - + const layouts = this.allDayManager.calculateAllDayEventsLayout(allDayEvents, weekDates); - + // Render each all-day event with pre-calculated layout layouts.forEach(layout => { this.allDayEventRenderer.renderAllDayEventWithLayout(layout.calenderEvent, layout); @@ -395,7 +394,7 @@ export class EventRenderingService { private clearEvents(container?: HTMLElement): void { this.strategy.clearEvents(container); - + // Also clear all-day events this.clearAllDayEvents(); } @@ -409,18 +408,18 @@ export class EventRenderingService { * Get visible dates from DOM headers - only the dates that are actually displayed */ private getVisibleDatesFromDOM(): string[] { - + const dayHeaders = document.querySelectorAll('swp-calendar-header swp-day-header'); const weekDates: string[] = []; - + dayHeaders.forEach(header => { const dateAttr = header.getAttribute('data-date'); if (dateAttr) { weekDates.push(dateAttr); } }); - - + + return weekDates; } diff --git a/src/types/EventTypes.ts b/src/types/EventTypes.ts index a649740..fe95845 100644 --- a/src/types/EventTypes.ts +++ b/src/types/EventTypes.ts @@ -2,6 +2,8 @@ * Type definitions for calendar events */ +import { ColumnBounds } from "../utils/ColumnDetectionUtils"; + export interface AllDayEvent { id: string; title: string; @@ -49,7 +51,7 @@ export interface DragStartEventPayload { draggedClone: HTMLElement | null; mousePosition: MousePosition; mouseOffset: MousePosition; - column: string | null; + columnBounds: ColumnBounds | null; } // Drag move event payload @@ -57,8 +59,8 @@ export interface DragMoveEventPayload { draggedElement: HTMLElement; mousePosition: MousePosition; mouseOffset: MousePosition; + columnBounds: ColumnBounds | null; snappedY: number; - column: string | null; } // Drag end event payload @@ -67,7 +69,7 @@ export interface DragEndEventPayload { draggedClone: HTMLElement | null; mousePosition: MousePosition; finalPosition: { - column: string | null; + column: ColumnBounds | null; snappedY: number; }; target: 'swp-day-column' | 'swp-day-header' | null; @@ -75,7 +77,7 @@ export interface DragEndEventPayload { // Drag mouse enter header event payload export interface DragMouseEnterHeaderEventPayload { - targetDate: string; + targetColumn: ColumnBounds; mousePosition: MousePosition; originalElement: HTMLElement | null; cloneElement: HTMLElement | null; @@ -93,8 +95,8 @@ export interface DragMouseLeaveHeaderEventPayload { export interface DragColumnChangeEventPayload { draggedElement: HTMLElement; draggedClone: HTMLElement | null; - previousColumn: string | null; - newColumn: string; + previousColumn: ColumnBounds | null; + newColumn: ColumnBounds; mousePosition: MousePosition; } diff --git a/src/utils/ColumnDetectionUtils.ts b/src/utils/ColumnDetectionUtils.ts index 44fd650..ec79ad6 100644 --- a/src/utils/ColumnDetectionUtils.ts +++ b/src/utils/ColumnDetectionUtils.ts @@ -3,92 +3,81 @@ * Used by both DragDropManager and AllDayManager for consistent column detection */ +import { MousePosition } from "../types/DragDropTypes"; + + export interface ColumnBounds { - date: string; - left: number; - right: number; + date: string; + left: number; + right: number; + boundingClientRect: DOMRect, + element : HTMLElement, + index: number } export class ColumnDetectionUtils { - private static columnBoundsCache: ColumnBounds[] = []; + private static columnBoundsCache: ColumnBounds[] = []; - /** - * Update column bounds cache for coordinate-based column detection - */ - public static updateColumnBoundsCache(): void { - // Reset cache - this.columnBoundsCache = []; + /** + * Update column bounds cache for coordinate-based column detection + */ + public static updateColumnBoundsCache(): void { + // Reset cache + this.columnBoundsCache = []; - // Find alle kolonner - const columns = document.querySelectorAll('swp-day-column'); + // Find alle kolonner + const columns = document.querySelectorAll('swp-day-column'); + let index = 0; + // Cache hver kolonnes x-grænser + columns.forEach(column => { + const rect = column.getBoundingClientRect(); + const date = (column as HTMLElement).dataset.date; - // Cache hver kolonnes x-grænser - columns.forEach(column => { - const rect = column.getBoundingClientRect(); - const date = (column as HTMLElement).dataset.date; - - if (date) { - this.columnBoundsCache.push({ - date, - left: rect.left, - right: rect.right + if (date) { + this.columnBoundsCache.push({ + boundingClientRect : rect, + element: column as HTMLElement, + date, + left: rect.left, + right: rect.right, + index: index++ + }); + } }); - } - }); - // Sorter efter x-position (fra venstre til højre) - this.columnBoundsCache.sort((a, b) => a.left - b.left); - } - - /** - * Get column date from X coordinate using cached bounds - */ - public static getColumnDateFromX(x: number): string | null { - // Opdater cache hvis tom - if (this.columnBoundsCache.length === 0) { - this.updateColumnBoundsCache(); + // Sorter efter x-position (fra venstre til højre) + this.columnBoundsCache.sort((a, b) => a.left - b.left); } - // Find den kolonne hvor x-koordinaten er indenfor grænserne - const column = this.columnBoundsCache.find(col => - x >= col.left && x <= col.right - ); + /** + * Get column date from X coordinate using cached bounds + */ + public static getColumnBounds(position: MousePosition): ColumnBounds | null{ + if (this.columnBoundsCache.length === 0) { + this.updateColumnBoundsCache(); + } - return column ? column.date : null; - } + // Find den kolonne hvor x-koordinaten er indenfor grænserne + let column = this.columnBoundsCache.find(col => + position.x >= col.left && position.x <= col.right + ); + if (column) + return column; - /** - * Get column index (1-based) from date - */ - public static getColumnIndexFromDate(date: string): number { - // Opdater cache hvis tom - if (this.columnBoundsCache.length === 0) { - this.updateColumnBoundsCache(); + return null; } - const columnIndex = this.columnBoundsCache.findIndex(col => col.date === date); - return columnIndex >= 0 ? columnIndex + 1 : 1; // 1-based index - } + /** + * Clear cache (useful for testing or when DOM structure changes) + */ + public static clearCache(): void { + this.columnBoundsCache = []; + } - /** - * Get column index from X coordinate - */ - public static getColumnIndexFromX(x: number): number { - const date = this.getColumnDateFromX(x); - return date ? this.getColumnIndexFromDate(date) : 1; - } - - /** - * Clear cache (useful for testing or when DOM structure changes) - */ - public static clearCache(): void { - this.columnBoundsCache = []; - } - - /** - * Get current cache for debugging - */ - public static getCache(): ColumnBounds[] { - return [...this.columnBoundsCache]; - } + /** + * Get current cache for debugging + */ + public static getCache(): ColumnBounds[] { + return [...this.columnBoundsCache]; + } } \ No newline at end of file diff --git a/src/utils/PositionUtils.ts b/src/utils/PositionUtils.ts index b8281b3..a37aa1a 100644 --- a/src/utils/PositionUtils.ts +++ b/src/utils/PositionUtils.ts @@ -1,4 +1,5 @@ import { calendarConfig } from '../core/CalendarConfig'; +import { ColumnBounds } from './ColumnDetectionUtils'; import { DateCalculator } from './DateCalculator'; /** @@ -157,22 +158,14 @@ export class PositionUtils { /** * Beregn Y position fra mouse/touch koordinat */ - public static getPositionFromCoordinate(clientY: number, containerElement: HTMLElement): number { - const rect = containerElement.getBoundingClientRect(); - const relativeY = clientY - rect.top; + public static getPositionFromCoordinate(clientY: number, column: ColumnBounds): number { + + const relativeY = clientY - column.boundingClientRect.top; // Snap til grid return PositionUtils.snapToGrid(relativeY); } - /** - * Beregn tid fra mouse/touch koordinat - */ - public static getTimeFromCoordinate(clientY: number, containerElement: HTMLElement): string { - const position = PositionUtils.getPositionFromCoordinate(clientY, containerElement); - return PositionUtils.pixelsToTime(position); - } - /** * Valider at tid er inden for arbejdstimer */