diff --git a/refactored-header-manager.md b/refactored-header-manager.md new file mode 100644 index 0000000..73aebbb --- /dev/null +++ b/refactored-header-manager.md @@ -0,0 +1,184 @@ +# Refactored HeaderManager - Fjern Ghost Columns + +## 1. HeaderManager Ændringer + +```typescript +// src/managers/HeaderManager.ts + +/** + * Setup header drag event listeners - REFACTORED VERSION + */ +public setupHeaderDragListeners(): void { + const calendarHeader = this.getCalendarHeader(); + if (!calendarHeader) return; + + // Use mouseenter instead of mouseover to avoid continuous firing + this.headerEventListener = (event: Event) => { + const target = event.target as HTMLElement; + + // Check if we're entering the all-day container + const allDayContainer = target.closest('swp-allday-container'); + if (allDayContainer) { + // Calculate target date from mouse X coordinate + const targetDate = this.calculateTargetDateFromMouseX(event as MouseEvent); + + if (targetDate) { + const calendarType = calendarConfig.getCalendarMode(); + const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType); + + eventBus.emit('header:mouseover', { + element: allDayContainer, + targetDate, + headerRenderer + }); + } + } + }; + + // Header mouseleave listener - unchanged + this.headerMouseLeaveListener = (event: Event) => { + eventBus.emit('header:mouseleave', { + element: event.target as HTMLElement + }); + }; + + // Use mouseenter instead of mouseover + calendarHeader.addEventListener('mouseenter', this.headerEventListener, true); + calendarHeader.addEventListener('mouseleave', this.headerMouseLeaveListener); +} + +/** + * Calculate target date from mouse X coordinate + */ +private calculateTargetDateFromMouseX(event: MouseEvent): string | null { + const dayHeaders = document.querySelectorAll('swp-day-header'); + const mouseX = event.clientX; + + for (const header of dayHeaders) { + const headerElement = header as HTMLElement; + const rect = headerElement.getBoundingClientRect(); + + // Check if mouse X is within this header's bounds + if (mouseX >= rect.left && mouseX <= rect.right) { + return headerElement.dataset.date || null; + } + } + + return null; +} + +/** + * Remove event listeners from header - UPDATED + */ +private removeEventListeners(): void { + const calendarHeader = this.getCalendarHeader(); + if (!calendarHeader) return; + + if (this.headerEventListener) { + // Remove mouseenter listener + calendarHeader.removeEventListener('mouseenter', this.headerEventListener, true); + } + + if (this.headerMouseLeaveListener) { + calendarHeader.removeEventListener('mouseleave', this.headerMouseLeaveListener); + } +} +``` + +## 2. AllDayEventRenderer Ændringer + +```typescript +// src/renderers/AllDayEventRenderer.ts + +/** + * Get or cache all-day container, create if it doesn't exist - SIMPLIFIED + */ +private getContainer(): HTMLElement | null { + if (!this.container) { + const header = document.querySelector('swp-calendar-header'); + if (header) { + // Try to find existing container + this.container = header.querySelector('swp-allday-container'); + + // If not found, create it + if (!this.container) { + this.container = document.createElement('swp-allday-container'); + header.appendChild(this.container); + + // NO MORE GHOST COLUMNS! 🎉 + // Mouse detection handled by HeaderManager coordinate calculation + } + } + } + return this.container; +} + +// REMOVE this method entirely: +// private createGhostColumns(): void { ... } +``` + +## 3. DragDropManager Ændringer + +```typescript +// src/managers/DragDropManager.ts + +// In constructor, update the header:mouseover listener +eventBus.on('header:mouseover', (event) => { + const { targetDate, element } = (event as CustomEvent).detail; + + if (this.draggedEventId && targetDate) { + // Only proceed if we're actually dragging and have a valid target date + const draggedElement = document.querySelector(`swp-event[data-event-id="${this.draggedEventId}"]`); + + if (draggedElement) { + console.log('🎯 Converting to all-day for date:', targetDate); + + this.eventBus.emit('drag:convert-to-allday', { + targetDate, + originalElement: draggedElement, + headerRenderer: (event as CustomEvent).detail.headerRenderer + }); + } + } +}); +``` + +## 4. CSS Ændringer (hvis nødvendigt) + +```css +/* Ensure all-day container is properly positioned for mouse events */ +swp-allday-container { + position: relative; + width: 100%; + min-height: var(--all-day-row-height, 0px); + display: grid; + grid-template-columns: repeat(7, 1fr); /* Match day columns */ + pointer-events: all; /* Ensure mouse events work */ +} + +/* Remove any ghost column styles */ +/* swp-allday-column styles can be removed if they were only for ghosts */ +``` + +## 5. Fordele ved denne løsning: + +✅ **Performance**: Ingen kontinuerlige mouseover events +✅ **Simplicity**: Fjerner ghost column kompleksitet +✅ **Accuracy**: Direkte coordinate-baseret detection +✅ **Maintainability**: Mindre kode at vedligeholde +✅ **Debugging**: Lettere at følge event flow + +## 6. Potentielle udfordringer: + +⚠️ **Event Bubbling**: `mouseenter` med `capture: true` for at fange events tidligt +⚠️ **Coordinate Precision**: Skal teste at coordinate beregning er præcis +⚠️ **Multi-day Events**: Skal stadig håndteres korrekt ved drop + +## 7. Test Scenarie: + +1. Drag et day-event +2. Træk musen ind i all-day området +3. `mouseenter` fyrer én gang og beregner target date +4. Event konverteres til all-day +5. Træk musen ud af all-day området +6. `mouseleave` fyrer og konverterer tilbage diff --git a/src/managers/AllDayManager.ts b/src/managers/AllDayManager.ts index de06165..4885ae9 100644 --- a/src/managers/AllDayManager.ts +++ b/src/managers/AllDayManager.ts @@ -30,13 +30,27 @@ export class AllDayManager { private setupEventListeners(): void { eventBus.on('drag:convert-to-allday', (event) => { const { targetDate, originalElement } = (event as CustomEvent).detail; + console.log('🔄 AllDayManager: Received drag:convert-to-allday', { + targetDate, + originalElementId: originalElement?.dataset?.eventId, + originalElementTag: originalElement?.tagName + }); this.handleConvertToAllDay(targetDate, originalElement); }); eventBus.on('drag:convert-from-allday', (event) => { const { draggedEventId } = (event as CustomEvent).detail; + console.log('🔄 AllDayManager: Received drag:convert-from-allday', { + draggedEventId + }); this.handleConvertFromAllDay(draggedEventId); }); + + // Listen for requests to ensure all-day container exists + eventBus.on('allday:ensure-container', () => { + console.log('🏗️ AllDayManager: Received request to ensure all-day container exists'); + this.ensureAllDayContainer(); + }); } /** @@ -325,6 +339,41 @@ export class AllDayManager { this.checkAndAnimateAllDayHeight(); } + /** + * Ensure all-day container exists, create if needed + */ + public ensureAllDayContainer(): HTMLElement | null { + console.log('🔍 AllDayManager: Checking if all-day container exists...'); + + // Try to get existing container first + let container = this.getAllDayContainer(); + + if (!container) { + console.log('🏗️ AllDayManager: Container not found, creating via AllDayEventRenderer...'); + + // Use the renderer to create container (which will call getContainer internally) + this.allDayEventRenderer.clearCache(); // Clear cache to force re-check + + // The renderer's getContainer method will create the container if it doesn't exist + // We can trigger this by trying to get the container + const header = this.getCalendarHeader(); + if (header) { + container = document.createElement('swp-allday-container'); + header.appendChild(container); + console.log('✅ AllDayManager: Created all-day container'); + + // Update our cache + this.cachedAllDayContainer = container; + } else { + console.log('❌ AllDayManager: No calendar header found, cannot create container'); + } + } else { + console.log('✅ AllDayManager: All-day container already exists'); + } + + return container; + } + /** * Clean up cached elements and resources */ diff --git a/src/managers/DragDropManager.ts b/src/managers/DragDropManager.ts index 87ce558..d875c6d 100644 --- a/src/managers/DragDropManager.ts +++ b/src/managers/DragDropManager.ts @@ -96,18 +96,36 @@ export class DragDropManager { 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', { targetDate, originalElement: draggedElement, headerRenderer }); + } else { + console.log('❌ DragDropManager: Dragged element not found'); } + } else { + console.log('⏭️ DragDropManager: Skipping conversion - no drag or no target date'); } }); diff --git a/src/managers/HeaderManager.ts b/src/managers/HeaderManager.ts index c3c1dec..e1e5e01 100644 --- a/src/managers/HeaderManager.ts +++ b/src/managers/HeaderManager.ts @@ -41,70 +41,137 @@ export class HeaderManager { } /** - * Setup header drag event listeners + * Setup header drag event listeners - REFACTORED to use mouseenter */ public setupHeaderDragListeners(): void { const calendarHeader = this.getCalendarHeader(); if (!calendarHeader) return; - // Throttle for better performance - let lastEmitTime = 0; - const throttleDelay = 16; // ~60fps + console.log('🎯 HeaderManager: Setting up drag listeners with mouseenter'); + // Track last processed date to avoid duplicates + let lastProcessedDate: string | null = null; + let lastProcessedTime = 0; + + // Use mouseenter instead of mouseover to avoid continuous firing this.headerEventListener = (event: Event) => { - const now = Date.now(); - if (now - lastEmitTime < throttleDelay) { - return; // Throttle events for better performance - } - lastEmitTime = now; - const target = event.target as HTMLElement; - // Optimized element detection - handle day headers and all-day columns - const dayHeader = target.closest('swp-day-header'); - const allDayColumn = target.closest('swp-allday-column'); + console.log('🖱️ HeaderManager: mouseenter detected on:', target.tagName, target.className); - if (dayHeader || allDayColumn) { - const hoveredElement = (dayHeader || allDayColumn) as HTMLElement; - const targetDate = hoveredElement.dataset.date; + // 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...'); - // Get header renderer for coordination - const calendarType = calendarConfig.getCalendarMode(); - const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType); + // Emit event to AllDayManager to create container + eventBus.emit('allday:ensure-container'); - eventBus.emit('header:mouseover', { - element: hoveredElement, - targetDate, - headerRenderer - }); + // Try to find it again after creation + allDayContainer = target.closest('swp-calendar-header')?.querySelector('swp-allday-container') as HTMLElement; + } + + if (allDayContainer) { + // SMART CHECK: Only calculate target date if there's an active drag operation + const isDragActive = document.querySelector('.dragging') !== null; + + if (!isDragActive) { + console.log('⏭️ HeaderManager: No active drag operation, skipping target date calculation'); + return; + } + + console.log('📍 HeaderManager: Active drag detected, calculating target date...'); + + // Calculate target date from mouse X coordinate + const targetDate = this.calculateTargetDateFromMouseX(event as MouseEvent); + + console.log('🎯 HeaderManager: Calculated target date:', targetDate); + + if (targetDate) { + const calendarType = calendarConfig.getCalendarMode(); + const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType); + + console.log('✅ HeaderManager: Emitting header:mouseover with targetDate:', targetDate); + + eventBus.emit('header:mouseover', { + element: allDayContainer, + targetDate, + headerRenderer + }); + } else { + console.log('❌ HeaderManager: Could not calculate target date from mouse position'); + } } }; // Header mouseleave listener this.headerMouseLeaveListener = (event: Event) => { + console.log('🚪 HeaderManager: mouseleave detected'); eventBus.emit('header:mouseleave', { element: event.target as HTMLElement }); }; - // Add event listeners - calendarHeader.addEventListener('mouseover', this.headerEventListener); + // Use mouseenter with capture to catch events early + calendarHeader.addEventListener('mouseenter', this.headerEventListener, true); calendarHeader.addEventListener('mouseleave', this.headerMouseLeaveListener); + + console.log('✅ HeaderManager: Event listeners attached (mouseenter + mouseleave)'); } /** - * Remove event listeners from header + * Calculate target date from mouse X coordinate + */ + private calculateTargetDateFromMouseX(event: MouseEvent): string | null { + 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(); if (!calendarHeader) return; + console.log('🧹 HeaderManager: Removing event listeners'); + if (this.headerEventListener) { - calendarHeader.removeEventListener('mouseover', this.headerEventListener); + // Remove mouseenter listener with capture flag + calendarHeader.removeEventListener('mouseenter', this.headerEventListener, true); + console.log('✅ HeaderManager: Removed mouseenter listener'); } if (this.headerMouseLeaveListener) { calendarHeader.removeEventListener('mouseleave', this.headerMouseLeaveListener); + console.log('✅ HeaderManager: Removed mouseleave listener'); } } @@ -186,6 +253,7 @@ export class HeaderManager { return calendarHeader; } + /** * Clear cached header reference */ diff --git a/src/renderers/AllDayEventRenderer.ts b/src/renderers/AllDayEventRenderer.ts index 19c53d4..f72dcad 100644 --- a/src/renderers/AllDayEventRenderer.ts +++ b/src/renderers/AllDayEventRenderer.ts @@ -13,7 +13,7 @@ export class AllDayEventRenderer { } /** - * Get or cache all-day container, create if it doesn't exist + * Get or cache all-day container, create if it doesn't exist - SIMPLIFIED (no ghost columns) */ private getContainer(): HTMLElement | null { if (!this.container) { @@ -27,38 +27,17 @@ export class AllDayEventRenderer { this.container = document.createElement('swp-allday-container'); header.appendChild(this.container); - // Create ghost columns for mouseenter events - this.createGhostColumns(); + console.log('🏗️ AllDayEventRenderer: Created all-day container (NO ghost columns)'); + + // NO MORE GHOST COLUMNS! 🎉 + // Mouse detection handled by HeaderManager coordinate calculation } } } return this.container; } - /** - * Create ghost columns for mouseenter events - */ - private createGhostColumns(): void { - if (!this.container) return; - - // Get all day headers to create matching ghost columns - const dayHeaders = document.querySelectorAll('swp-day-header'); - dayHeaders.forEach((header, index) => { - const ghostColumn = document.createElement('swp-allday-column'); - const headerElement = header as HTMLElement; - - // Copy date from corresponding day header - if (headerElement.dataset.date) { - ghostColumn.dataset.date = headerElement.dataset.date; - } - - // Set grid column position (1-indexed) - ghostColumn.style.gridColumn = (index + 1).toString(); - ghostColumn.style.gridRow = '1 / -1'; // Span all rows - - this.container!.appendChild(ghostColumn); - }); - } + // REMOVED: createGhostColumns() method - no longer needed! /** * Render an all-day event using factory pattern