From 46b8bf9fb59e32ac0baf5ad1f47b39cb5ee6f84f Mon Sep 17 00:00:00 2001 From: Janus Knudsen Date: Wed, 17 Sep 2025 23:39:29 +0200 Subject: [PATCH] Fixes drag and drop to header issues Addresses two key issues related to dragging events to the header: the premature removal of the original event element and the creation of duplicate all-day events. The original event is no longer removed when dragging to the header; it is now only removed upon a successful drop. Also, it prevents the creation of duplicate all-day events by checking for existing all-day events before creating new ones, using DOM queries to ensure accurate state. --- ...drag-drop-header-bug-analysis-corrected.md | 114 ++++++++++++++ docs/drag-drop-header-bug-analysis.md | 104 +++++++++++++ .../drag-drop-header-complete-bug-analysis.md | 123 +++++++++++++++ ...drag-drop-header-implementation-details.md | 143 ++++++++++++++++++ src/elements/SwpEventElement.ts | 109 ++++++++++--- src/managers/CalendarManager.ts | 7 + src/managers/HeaderManager.ts | 88 +++++++++++ src/managers/ScrollManager.ts | 10 ++ src/managers/ViewManager.ts | 4 +- src/renderers/GridRenderer.ts | 43 +----- 10 files changed, 684 insertions(+), 61 deletions(-) create mode 100644 docs/drag-drop-header-bug-analysis-corrected.md create mode 100644 docs/drag-drop-header-bug-analysis.md create mode 100644 docs/drag-drop-header-complete-bug-analysis.md create mode 100644 docs/drag-drop-header-implementation-details.md diff --git a/docs/drag-drop-header-bug-analysis-corrected.md b/docs/drag-drop-header-bug-analysis-corrected.md new file mode 100644 index 0000000..d0b1ceb --- /dev/null +++ b/docs/drag-drop-header-bug-analysis-corrected.md @@ -0,0 +1,114 @@ +# Korrigeret Analyse: Day Event Drag til Header og Tilbage + +## Korrekt Flow Design + +### Elementer i Spil +1. **Original Event**: Skal forblive i DOM uændret indtil drag:end +2. **Clone Event**: Det visuelle element der dragges rundt +3. **All-Day Event**: Midlertidig repræsentation i header + +## Nuværende Problem + +### AllDayManager.handleConvertToAllDay() (linje 274) +```typescript +// PROBLEM: Fjerner original for tidligt! +originalElement.remove(); // ❌ FORKERT +``` +Original element fjernes når man hover over header, men det skal først fjernes ved faktisk drop. + +### Korrekt Flow + +```mermaid +sequenceDiagram + participant User + participant Original as Original Event + participant Clone as Clone Event + participant AllDay as All-Day Event + + Note over Original: Start: Original synlig + User->>Clone: Drag start + Note over Clone: Clone oprettes og vises + Note over Original: Original bliver semi-transparent + + User->>AllDay: Enter header + Note over Clone: Clone skjules (display:none) + Note over AllDay: All-day event oprettes + Note over Original: Original forbliver (semi-transparent) + + User->>Clone: Leave header (tilbage til grid) + Note over AllDay: All-day event fjernes + Note over Clone: Clone vises igen + Note over Original: Original stadig der + + User->>Original: Drop + Note over Original: NU fjernes original + Note over Clone: Clone bliver til ny position +``` + +## Nødvendige Ændringer + +### 1. AllDayManager.handleConvertToAllDay() (linje 232-285) +```typescript +// FØR (linje 274): +originalElement.remove(); // ❌ + +// EFTER: +// originalElement.remove(); // Kommenteret ud - skal IKKE fjernes her +// Original forbliver i DOM med opacity 0.3 fra drag start +``` + +### 2. AllDayManager.handleConvertFromAllDay() (linje 290-311) +```typescript +// Nuværende kode er faktisk korrekt - den: +// 1. Fjerner all-day event +// 2. Viser clone igen +// Original er stadig der (blev aldrig fjernet) +``` + +### 3. EventRenderer - handleDragEnd() +```typescript +// Her skal original FAKTISK fjernes +// Efter successful drop: +if (this.originalEvent) { + this.originalEvent.remove(); // ✅ Korrekt tidspunkt +} +``` + +## Problem Opsummering + +**Hovedproblem**: `originalElement.remove()` på linje 274 i AllDayManager sker for tidligt. + +**Løsning**: +1. Fjern/kommenter linje 274 i AllDayManager.handleConvertToAllDay() +2. Original element skal kun fjernes i EventRenderer ved faktisk drop (drag:end) +3. Clone håndterer al visuel feedback under drag + +## Test Scenarios + +1. **Drag → Header → Grid → Drop** + - Original forbliver hele tiden + - Clone skjules/vises korrekt + - Original fjernes kun ved drop + +2. **Drag → Header → Drop i Header** + - Original forbliver indtil drop + - All-day event bliver permanent + - Original fjernes ved drop + +3. **Drag → Header → ESC** + - Original forbliver + - Drag cancelled, alt tilbage til start + +## Affected Code + +### AllDayManager.ts +- **Linje 274**: Skal fjernes/kommenteres +- Original element må IKKE fjernes her + +### EventRenderer.ts +- **handleDragEnd()**: Skal sikre original fjernes her +- Dette er det ENESTE sted original må fjernes + +### DragDropManager.ts +- Koden ser faktisk korrekt ud +- Holder styr på både original og clone \ No newline at end of file diff --git a/docs/drag-drop-header-bug-analysis.md b/docs/drag-drop-header-bug-analysis.md new file mode 100644 index 0000000..02e78b7 --- /dev/null +++ b/docs/drag-drop-header-bug-analysis.md @@ -0,0 +1,104 @@ +# Bug Analyse: Day Event Drag til Header og Tilbage + +## Problem Beskrivelse +Når en day event dragges op til headeren (for at konvertere til all-day) og derefter dragges tilbage til grid UDEN at droppe, opstår der et kritisk problem hvor original event forsvinder. + +## Flow Analyse + +### Trin 1: Drag Start (Day Event) +- **DragDropManager** (linje 139-182): `handleMouseDown()` registrerer drag start +- Original event element gemmes +- `isDragStarted = false` indtil movement threshold nås + +### Trin 2: Drag bevæger sig op mod Header +- **DragDropManager** (linje 187-253): `handleMouseMove()` tracker bevægelse +- Når threshold nås: `drag:start` event emitted +- **EventRenderer** opretter drag clone + +### Trin 3: Mouse enters Header ⚠️ PROBLEM STARTER HER +- **DragDropManager** (linje 95-112): Lytter til `header:mouseover` +- Emitter `drag:convert-to-allday` event +- **AllDayManager** (linje 232-285): `handleConvertToAllDay()`: + - Opretter all-day event i header + - **FJERNER original timed event permanent** (linje 274: `originalElement.remove()`) + - Skjuler drag clone + +### Trin 4: Mouse leaves Header (tilbage til grid) ⚠️ PROBLEM FORTSÆTTER +- **DragDropManager** (linje 128-136): Lytter til `header:mouseleave` +- Emitter `drag:convert-from-allday` event +- **AllDayManager** (linje 290-311): `handleConvertFromAllDay()`: + - Fjerner all-day event fra container + - Viser drag clone igen + - **MEN: Original event er allerede fjernet og kan ikke genskabes!** + +### Trin 5: Drop i Grid ⚠️ DATA TABT +- **DragDropManager** (linje 258-291): `handleMouseUp()` +- Emitter `drag:end` event +- Original element eksisterer ikke længere +- Kun clone eksisterer med ID "clone-{id}" + +## Root Cause +**AllDayManager.handleConvertToAllDay()** fjerner permanent det originale element (linje 274) i stedet for at skjule det midlertidigt. + +## Konsekvenser +1. Original event data går tabt +2. Event ID bliver "clone-{id}" i stedet for "{id}" +3. Event metadata kan mangle +4. Potentielle styling/positioning problemer + +## Løsningsforslag + +### Option 1: Behold Original Element (Anbefalet) +```typescript +// AllDayManager.handleConvertToAllDay() - linje 274 +// FØR: originalElement.remove(); +// EFTER: +originalElement.style.display = 'none'; +originalElement.dataset.temporarilyHidden = 'true'; +``` + +### Option 2: Gem Original Data +```typescript +// Gem original element data før fjernelse +const originalData = { + id: originalElement.dataset.eventId, + title: originalElement.dataset.title, + start: originalElement.dataset.start, + end: originalElement.dataset.end, + // ... andre properties +}; +// Gem i DragDropManager eller AllDayManager +``` + +### Option 3: Re-create Original on Leave +```typescript +// AllDayManager.handleConvertFromAllDay() +// Genskab original element fra all-day event data +const originalElement = this.recreateTimedEvent(allDayEvent); +``` + +## Implementeringsplan + +1. **Modificer AllDayManager.handleConvertToAllDay()** + - Skjul original element i stedet for at fjerne + - Marker element som midlertidigt skjult + +2. **Modificer AllDayManager.handleConvertFromAllDay()** + - Find original element (ikke kun clone) + - Vis original element igen + - Synkroniser position med clone + +3. **Opdater DragDropManager** + - Hold reference til både original og clone + - Ved drop: opdater original, fjern clone + +4. **Test Scenarios** + - Drag day event → header → tilbage → drop + - Drag day event → header → drop i header + - Drag day event → header → ESC key + - Multiple hurtige hover over header + +## Affected Files +- `src/managers/AllDayManager.ts` (linje 274, 290-311) +- `src/managers/DragDropManager.ts` (linje 95-136) +- `src/renderers/EventRenderer.ts` (potentielt) \ No newline at end of file diff --git a/docs/drag-drop-header-complete-bug-analysis.md b/docs/drag-drop-header-complete-bug-analysis.md new file mode 100644 index 0000000..54ed011 --- /dev/null +++ b/docs/drag-drop-header-complete-bug-analysis.md @@ -0,0 +1,123 @@ +# Komplet Bug Analyse: Drag-Drop Header Problemer + +## Identificerede Problemer + +### Problem 1: Original Element Fjernes For Tidligt +**Lokation**: AllDayManager.handleConvertToAllDay() linje 274 +```typescript +originalElement.remove(); // ❌ FORKERT - skal ikke fjernes her +``` + +### Problem 2: Duplicate All-Day Events ved Mouseover +**Lokation**: AllDayManager.handleConvertToAllDay() linje 232-285 + +Hver gang `header:mouseover` fyrer (hvilket kan ske mange gange under drag), oprettes et NYT all-day event uden at checke om det allerede eksisterer. + +## Event Flow Problem + +```mermaid +sequenceDiagram + participant Mouse + participant Header + participant AllDay as AllDayManager + + Note over Mouse: Dragger over header + loop Hver mouseover event + Mouse->>Header: mouseover + Header->>AllDay: drag:convert-to-allday + AllDay->>AllDay: Opretter NYT all-day event ❌ + Note over AllDay: Ingen check for eksisterende! + end + Note over AllDay: Resultat: Multiple all-day events! +``` + +## Løsning + +### Fix 1: Check for Eksisterende All-Day Event +```typescript +// AllDayManager.handleConvertToAllDay() +private handleConvertToAllDay(targetDate: string, originalElement: HTMLElement): void { + const eventId = originalElement.dataset.eventId; + + // CHECK: Eksisterer all-day event allerede? + const existingAllDay = document.querySelector( + `swp-allday-container swp-allday-event[data-event-id="${eventId}"]` + ); + + if (existingAllDay) { + console.log('All-day event already exists, skipping creation'); + return; // Exit early - don't create duplicate + } + + // Fortsæt med at oprette all-day event... +} +``` + +### Fix 2: Fjern IKKE Original Element +```typescript +// Linje 274 - skal fjernes eller kommenteres ud: +// originalElement.remove(); // <-- FJERN DENNE LINJE +``` + +### Fix 3: Track Conversion State (Optional) +For at undgå gentagne conversions, kunne vi tracke state: + +```typescript +// AllDayManager +private convertedEventIds = new Set(); + +private handleConvertToAllDay(targetDate: string, originalElement: HTMLElement): void { + const eventId = originalElement.dataset.eventId; + + // Check if already converted + if (this.convertedEventIds.has(eventId)) { + return; + } + + // Mark as converted + this.convertedEventIds.add(eventId); + + // Create all-day event... +} + +private handleConvertFromAllDay(draggedEventId: string): void { + // Clear conversion state + this.convertedEventIds.delete(draggedEventId); + + // Remove all-day event... +} +``` + +## Komplet Fix Checklist + +1. **AllDayManager.handleConvertToAllDay()** + - [ ] Tilføj check for eksisterende all-day event + - [ ] Fjern/kommenter `originalElement.remove()` linje 274 + - [ ] Log når duplicate undgås + +2. **AllDayManager.handleConvertFromAllDay()** + - [ ] Verificer at all-day event faktisk fjernes + - [ ] Clear evt. conversion state + +3. **Test Scenarios** + - [ ] Hurtig mouseover frem og tilbage over header + - [ ] Langsom drag over header + - [ ] Drag ind i header → ud → ind igen + - [ ] Multiple events dragges samtidig (hvis muligt) + +## Root Causes + +1. **Manglende idempotency**: handleConvertToAllDay() er ikke idempotent +2. **Forkert element lifecycle**: Original fjernes for tidligt +3. **Manglende state tracking**: Ingen tracking af hvilke events der er konverteret + +## Performance Overvejelser + +HeaderManager throttler mouseover events (linje 41-49), men det er ikke nok når musen bevæger sig langsomt over header. Vi skal have idempotent logik. + +## Implementeringsrækkefølge + +1. **Først**: Fix duplicate all-day events (check for existing) +2. **Derefter**: Fjern originalElement.remove() +3. **Test**: Verificer at begge problemer er løst +4. **Optional**: Implementer conversion state tracking for bedre performance \ No newline at end of file diff --git a/docs/drag-drop-header-implementation-details.md b/docs/drag-drop-header-implementation-details.md new file mode 100644 index 0000000..3da6b43 --- /dev/null +++ b/docs/drag-drop-header-implementation-details.md @@ -0,0 +1,143 @@ +# Implementeringsdetaljer: Check for Eksisterende All-Day Event + +## Hvor skal checket implementeres? + +**Fil**: `src/managers/AllDayManager.ts` +**Metode**: `handleConvertToAllDay()` +**Linje**: Lige efter linje 232 (start af metoden) + +## Præcis Implementation + +```typescript +// AllDayManager.ts - handleConvertToAllDay metode (linje 232) +private handleConvertToAllDay(targetDate: string, originalElement: HTMLElement): void { + // Extract event data from original element + const eventId = originalElement.dataset.eventId; + const title = originalElement.dataset.title || originalElement.textContent || 'Untitled'; + const type = originalElement.dataset.type || 'work'; + const startStr = originalElement.dataset.start; + const endStr = originalElement.dataset.end; + + if (!eventId || !startStr || !endStr) { + console.error('Original element missing required data (eventId, start, end)'); + return; + } + + // ===== NY KODE STARTER HER (efter linje 243) ===== + + // CHECK 1: Er dette faktisk clone elementet? + const isClone = eventId.startsWith('clone-'); + const actualEventId = isClone ? eventId.replace('clone-', '') : eventId; + + // CHECK 2: Eksisterer all-day event allerede? + const container = this.getAllDayContainer(); + if (container) { + const existingAllDay = container.querySelector( + `swp-allday-event[data-event-id="${actualEventId}"]` + ); + + if (existingAllDay) { + console.log(`All-day event for ${actualEventId} already exists, skipping creation`); + return; // Exit early - don't create duplicate + } + } + + // ===== NY KODE SLUTTER HER ===== + + // Fortsæt med normal oprettelse... + // Create CalendarEvent for all-day conversion - preserve original times + const originalStart = new Date(startStr); + // ... resten af koden +} +``` + +## Hvorfor virker dette? + +1. **Check for clone ID**: Vi checker om det draggede element er clone (starter med "clone-") +2. **Normalisér ID**: Vi får det faktiske event ID uden "clone-" prefix +3. **Query DOM**: Vi søger i all-day container efter eksisterende element med samme ID +4. **Early exit**: Hvis det findes, logger vi og returnerer uden at oprette duplicate + +## Alternative Implementeringer + +### Option A: Track med Set (Hurtigere) +```typescript +// AllDayManager class property +private activeAllDayEvents = new Set(); + +private handleConvertToAllDay(targetDate: string, originalElement: HTMLElement): void { + const eventId = originalElement.dataset.eventId; + const actualEventId = eventId?.startsWith('clone-') + ? eventId.replace('clone-', '') + : eventId; + + // Check i memory først (hurtigere) + if (this.activeAllDayEvents.has(actualEventId)) { + return; + } + + // Marker som aktiv + this.activeAllDayEvents.add(actualEventId); + + // ... opret all-day event +} + +private handleConvertFromAllDay(draggedEventId: string): void { + // Fjern fra tracking + this.activeAllDayEvents.delete(draggedEventId); + + // ... fjern all-day event +} +``` + +### Option B: Data Attribute Flag +```typescript +private handleConvertToAllDay(targetDate: string, originalElement: HTMLElement): void { + // Check flag på original element + if (originalElement.dataset.hasAllDayVersion === 'true') { + return; + } + + // Sæt flag + originalElement.dataset.hasAllDayVersion = 'true'; + + // ... opret all-day event +} + +private handleConvertFromAllDay(draggedEventId: string): void { + // Find original og fjern flag + const original = document.querySelector(`[data-event-id="${draggedEventId}"]`); + if (original) { + delete original.dataset.hasAllDayVersion; + } + + // ... fjern all-day event +} +``` + +## Anbefalet Løsning + +**DOM Query Check** (første løsning) er mest robust fordi: +1. Den checker faktisk DOM state +2. Ingen ekstra state at vedligeholde +3. Fungerer selv hvis events kommer ud af sync + +## Test Verification + +For at teste om det virker: +1. Åbn browser console +2. Drag et event langsomt over header +3. Se efter "already exists" log messages +4. Verificer kun ÉT all-day event i DOM med: + ```javascript + document.querySelectorAll('swp-allday-event').length + ``` + +## Linje 274 Fix + +Samtidig skal vi fjerne/kommentere linje 274: +```typescript +// originalElement.remove(); // <-- KOMMENTER DENNE UD +``` + +Dette sikrer original element ikke fjernes for tidligt. \ No newline at end of file diff --git a/src/elements/SwpEventElement.ts b/src/elements/SwpEventElement.ts index f8e0b88..80dcd9a 100644 --- a/src/elements/SwpEventElement.ts +++ b/src/elements/SwpEventElement.ts @@ -170,10 +170,11 @@ export class SwpAllDayEventElement extends BaseEventElement { */ private setAllDayAttributes(): void { this.element.dataset.allDay = "true"; - // Override start/end times to be full day - const dateStr = this.event.start.toISOString().split('T')[0]; - this.element.dataset.start = `${dateStr}T00:00:00`; - this.element.dataset.end = `${dateStr}T23:59:59`; + // For all-day events, preserve original start/end dates but set to full day times + const startDateStr = this.event.start.toISOString().split('T')[0]; + const endDateStr = this.event.end.toISOString().split('T')[0]; + this.element.dataset.start = `${startDateStr}T00:00:00`; + this.element.dataset.end = `${endDateStr}T23:59:59`; } /** @@ -197,28 +198,36 @@ export class SwpAllDayEventElement extends BaseEventElement { this.element.style.gridRow = row.toString(); } + /** + * Set grid column span for this all-day event + */ + public setColumnSpan(startColumn: number, endColumn: number): void { + this.element.style.gridColumn = `${startColumn} / ${endColumn + 1}`; + } + /** * Factory method to create from CalendarEvent and target date */ - public static fromCalendarEvent(event: CalendarEvent, targetDate: string): SwpAllDayEventElement { - // Calculate column index - const dayHeaders = document.querySelectorAll('swp-day-header'); - let columnIndex = 1; - dayHeaders.forEach((header, index) => { - if ((header as HTMLElement).dataset.date === targetDate) { - columnIndex = index + 1; - } - }); + public static fromCalendarEvent(event: CalendarEvent, targetDate?: string): SwpAllDayEventElement { + // Calculate column span based on event start and end dates + const { startColumn, endColumn, columnSpan } = this.calculateColumnSpan(event); + + // For backwards compatibility, use targetDate if provided, otherwise use calculated start column + const finalStartColumn = targetDate ? this.getColumnIndexForDate(targetDate) : startColumn; + const finalEndColumn = targetDate ? finalStartColumn : endColumn; + const finalColumnSpan = targetDate ? 1 : columnSpan; - // Find occupied rows in this column using computedStyle + // Find occupied rows in the spanned columns using computedStyle const existingEvents = document.querySelectorAll('swp-allday-event'); const occupiedRows = new Set(); existingEvents.forEach(existingEvent => { const style = getComputedStyle(existingEvent); - const eventCol = parseInt(style.gridColumnStart); + const eventStartCol = parseInt(style.gridColumnStart); + const eventEndCol = parseInt(style.gridColumnEnd); - if (eventCol === columnIndex) { + // Check if this existing event overlaps with our column span + if (this.columnsOverlap(eventStartCol, eventEndCol, finalStartColumn, finalEndColumn)) { const eventRow = parseInt(style.gridRowStart) || 1; occupiedRows.add(eventRow); } @@ -230,9 +239,73 @@ export class SwpAllDayEventElement extends BaseEventElement { targetRow++; } - // Create element with both column and row - const element = new SwpAllDayEventElement(event, columnIndex); + // Create element with calculated column span + const element = new SwpAllDayEventElement(event, finalStartColumn); element.setGridRow(targetRow); + element.setColumnSpan(finalStartColumn, finalEndColumn); return element; } + + /** + * Calculate column span based on event start and end dates + */ + private static calculateColumnSpan(event: CalendarEvent): { startColumn: number; endColumn: number; columnSpan: number } { + const dayHeaders = document.querySelectorAll('swp-day-header'); + + // Extract dates from headers + const headerDates: string[] = []; + dayHeaders.forEach(header => { + const date = (header as HTMLElement).dataset.date; + if (date) { + headerDates.push(date); + } + }); + + // Format event dates for comparison (YYYY-MM-DD format) + const eventStartDate = event.start.toISOString().split('T')[0]; + const eventEndDate = event.end.toISOString().split('T')[0]; + + // Find start and end column indices + let startColumn = 1; + let endColumn = headerDates.length; + + headerDates.forEach((dateStr, index) => { + if (dateStr === eventStartDate) { + startColumn = index + 1; + } + if (dateStr === eventEndDate) { + endColumn = index + 1; + } + }); + + // Ensure end column is at least start column + if (endColumn < startColumn) { + endColumn = startColumn; + } + + const columnSpan = endColumn - startColumn + 1; + + return { startColumn, endColumn, columnSpan }; + } + + /** + * Get column index for a specific date + */ + private static getColumnIndexForDate(targetDate: string): number { + const dayHeaders = document.querySelectorAll('swp-day-header'); + let columnIndex = 1; + dayHeaders.forEach((header, index) => { + if ((header as HTMLElement).dataset.date === targetDate) { + columnIndex = index + 1; + } + }); + return columnIndex; + } + + /** + * Check if two column ranges overlap + */ + private static columnsOverlap(startA: number, endA: number, startB: number, endB: number): boolean { + return !(endA < startB || endB < startA); + } } \ No newline at end of file diff --git a/src/managers/CalendarManager.ts b/src/managers/CalendarManager.ts index 0afbd87..1cae7ed 100644 --- a/src/managers/CalendarManager.ts +++ b/src/managers/CalendarManager.ts @@ -380,6 +380,13 @@ export class CalendarManager { // Re-render events in the new grid structure this.rerenderEvents(); + + // Notify HeaderManager with correct current date after grid rebuild + this.eventBus.emit('workweek:header-update', { + currentDate: this.currentDate, + currentView: this.currentView, + workweek: calendarConfig.getCurrentWorkWeek() + }); } /** diff --git a/src/managers/HeaderManager.ts b/src/managers/HeaderManager.ts index 0028b69..cd12fc0 100644 --- a/src/managers/HeaderManager.ts +++ b/src/managers/HeaderManager.ts @@ -1,6 +1,9 @@ import { eventBus } from '../core/EventBus'; import { calendarConfig } from '../core/CalendarConfig'; import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; +import { CoreEvents } from '../constants/CoreEvents'; +import { HeaderRenderContext } from '../renderers/HeaderRenderer'; +import { ResourceCalendarData } from '../types/CalendarTypes'; /** * HeaderManager - Handles all header-related event logic @@ -15,6 +18,16 @@ export class HeaderManager { // Bind methods for event listeners this.setupHeaderDragListeners = this.setupHeaderDragListeners.bind(this); this.destroy = this.destroy.bind(this); + + // Listen for navigation events to update header + this.setupNavigationListener(); + } + + /** + * Initialize header with initial date + */ + public initializeHeader(currentDate: Date, resourceData: ResourceCalendarData | null = null): void { + this.updateHeader(currentDate, resourceData); } /** @@ -98,6 +111,81 @@ export class HeaderManager { } } + /** + * Setup navigation event listener + */ + private setupNavigationListener(): void { + eventBus.on(CoreEvents.NAVIGATION_COMPLETED, (event) => { + const { currentDate, resourceData } = (event as CustomEvent).detail; + this.updateHeader(currentDate, resourceData); + }); + + // Also listen for date changes (including initial setup) + eventBus.on(CoreEvents.DATE_CHANGED, (event) => { + const { currentDate } = (event as CustomEvent).detail; + this.updateHeader(currentDate, null); + }); + + // Listen for workweek header updates after grid rebuild + eventBus.on('workweek:header-update', (event) => { + const { currentDate } = (event as CustomEvent).detail; + this.clearCache(); // Clear cache since DOM was cleared + this.updateHeader(currentDate, null); + }); + + } + + /** + * Update header content for navigation + */ + private updateHeader(currentDate: Date, resourceData: ResourceCalendarData | null = null): void { + const calendarHeader = this.getOrCreateCalendarHeader(); + if (!calendarHeader) return; + + // Clear existing content + calendarHeader.innerHTML = ''; + + // Render new header content + const calendarType = calendarConfig.getCalendarMode(); + const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType); + + const context: HeaderRenderContext = { + currentWeek: currentDate, + config: calendarConfig, + resourceData: resourceData + }; + + headerRenderer.render(calendarHeader, context); + + // Re-setup event listeners + this.setupHeaderDragListeners(); + + // Notify other managers that header was rebuilt + eventBus.emit('header:rebuilt', { + headerElement: calendarHeader + }); + } + + /** + * Get or create calendar header element + */ + private getOrCreateCalendarHeader(): HTMLElement | null { + let calendarHeader = this.getCalendarHeader(); + + if (!calendarHeader) { + // Find grid container and create header + const gridContainer = document.querySelector('swp-grid-container'); + if (gridContainer) { + 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 */ diff --git a/src/managers/ScrollManager.ts b/src/managers/ScrollManager.ts index 6a88ef9..4b77289 100644 --- a/src/managers/ScrollManager.ts +++ b/src/managers/ScrollManager.ts @@ -42,6 +42,16 @@ export class ScrollManager { this.updateScrollableHeight(); }); + // Handle header rebuild - refresh header reference and re-sync + eventBus.on('header:rebuilt', () => { + this.calendarHeader = document.querySelector('swp-calendar-header'); + if (this.scrollableContent && this.calendarHeader) { + this.setupHorizontalScrollSynchronization(); + this.syncCalendarHeaderPosition(); // Immediately sync position + } + this.updateScrollableHeight(); // Update height calculations + }); + // Handle window resize window.addEventListener('resize', () => { this.updateScrollableHeight(); diff --git a/src/managers/ViewManager.ts b/src/managers/ViewManager.ts index e242775..0b21c70 100644 --- a/src/managers/ViewManager.ts +++ b/src/managers/ViewManager.ts @@ -150,8 +150,8 @@ export class ViewManager { // Update button states using cached elements this.updateAllButtons(); - // Trigger a calendar refresh to apply the new workweek - this.eventBus.emit(CoreEvents.REFRESH_REQUESTED); + // Trigger a workweek change to apply the new workweek + this.eventBus.emit(CoreEvents.WORKWEEK_CHANGED); } /** diff --git a/src/renderers/GridRenderer.ts b/src/renderers/GridRenderer.ts index 8813bef..e14397c 100644 --- a/src/renderers/GridRenderer.ts +++ b/src/renderers/GridRenderer.ts @@ -1,7 +1,6 @@ import { calendarConfig } from '../core/CalendarConfig'; import { ResourceCalendarData, CalendarView } from '../types/CalendarTypes'; import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; -import { HeaderRenderContext } from './HeaderRenderer'; import { ColumnRenderContext } from './ColumnRenderer'; import { eventBus } from '../core/EventBus'; import { DateCalculator } from '../utils/DateCalculator'; @@ -12,7 +11,6 @@ import { DateCalculator } from '../utils/DateCalculator'; */ export class GridRenderer { private cachedGridContainer: HTMLElement | null = null; - private cachedCalendarHeader: HTMLElement | null = null; private cachedTimeAxis: HTMLElement | null = null; constructor() { @@ -38,6 +36,8 @@ export class GridRenderer { // Only clear and rebuild if grid is empty (first render) if (grid.children.length === 0) { this.createCompleteGridStructure(grid, currentDate, resourceData, view); + // Setup grid-related event listeners on first render + this.setupGridEventListeners(); } else { // Optimized update - only refresh dynamic content this.updateGridContent(grid, currentDate, resourceData, view); @@ -109,12 +109,6 @@ export class GridRenderer { ): HTMLElement { const gridContainer = document.createElement('swp-grid-container'); - // Create calendar header with caching - const calendarHeader = document.createElement('swp-calendar-header'); - this.renderCalendarHeader(calendarHeader, currentDate, resourceData, view); - this.cachedCalendarHeader = calendarHeader; - gridContainer.appendChild(calendarHeader); - // Create scrollable content structure const scrollableContent = document.createElement('swp-scrollable-content'); const timeGrid = document.createElement('swp-time-grid'); @@ -134,30 +128,6 @@ export class GridRenderer { return gridContainer; } - /** - * Render calendar header with view awareness - */ - private renderCalendarHeader( - calendarHeader: HTMLElement, - currentDate: Date, - resourceData: ResourceCalendarData | null, - view: CalendarView - ): void { - const calendarType = calendarConfig.getCalendarMode(); - const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType); - - const context: HeaderRenderContext = { - currentWeek: currentDate, // HeaderRenderer expects currentWeek property - config: calendarConfig, - resourceData: resourceData - }; - - headerRenderer.render(calendarHeader, context); - - - // Setup only grid-related event listeners - this.setupGridEventListeners(); - } /** * Render column container with view awareness @@ -189,14 +159,6 @@ export class GridRenderer { resourceData: ResourceCalendarData | null, view: CalendarView ): void { - // Use cached elements if available - const calendarHeader = this.cachedCalendarHeader || grid.querySelector('swp-calendar-header'); - if (calendarHeader) { - // Clear and re-render header content - calendarHeader.innerHTML = ''; - this.renderCalendarHeader(calendarHeader as HTMLElement, currentDate, resourceData, view); - } - // Update column container if needed const columnContainer = grid.querySelector('swp-day-columns'); if (columnContainer) { @@ -271,7 +233,6 @@ export class GridRenderer { // Clear cached references this.cachedGridContainer = null; - this.cachedCalendarHeader = null; this.cachedTimeAxis = null; (this as any).gridBodyEventListener = null; (this as any).cachedColumnContainer = null;