Calendar/refactored-header-manager.md
Janus Knudsen fb40279009 Refactors header drag interaction to eliminate ghost columns
Updates the `HeaderManager` to utilize `mouseenter` and `mouseleave` events on the calendar header for improved performance and accuracy.
Calculates the target date based on the mouse's X-coordinate within the header.

Removes the need for 'ghost columns' by simplifying the logic. This significantly reduces complexity.
The `AllDayEventRenderer` is modified to reflect this change, omitting ghost column creation.

Updates `DragDropManager` to accommodate the new interaction model.
Various console logs are added for debugging purposes.
2025-09-18 17:55:52 +02:00

5.4 KiB

Refactored HeaderManager - Fjern Ghost Columns

1. HeaderManager Ændringer

// 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

// 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

// 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)

/* 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