From 6498b0ba8ecaca080b58d77047ef7fbce78bdb4a Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Mon, 22 Sep 2025 23:37:43 +0200 Subject: [PATCH] Refactors all-day event rendering and DOM access Decouples all-day event rendering, making it reactive to header readiness with period data. Eliminates explicit DOM element caching, simplifying element access. Enhances the `header:ready` event payload with `startDate` and `endDate`. Improves all-day row height animation and calculation. --- src/managers/AllDayManager.ts | 73 ++++++--------------- src/managers/HeaderManager.ts | 19 ++++-- src/renderers/AllDayEventRenderer.ts | 12 +--- src/renderers/EventRendererManager.ts | 91 +++++++++++++-------------- src/renderers/NavigationRenderer.ts | 11 ++++ src/types/EventTypes.ts | 8 +++ 6 files changed, 98 insertions(+), 116 deletions(-) diff --git a/src/managers/AllDayManager.ts b/src/managers/AllDayManager.ts index 0b0a525..1083ad4 100644 --- a/src/managers/AllDayManager.ts +++ b/src/managers/AllDayManager.ts @@ -16,9 +16,6 @@ import { * Separated from HeaderManager for clean responsibility separation */ export class AllDayManager { - private cachedAllDayContainer: HTMLElement | null = null; - private cachedCalendarHeader: HTMLElement | null = null; - private cachedHeaderSpacer: HTMLElement | null = null; private allDayEventRenderer: AllDayEventRenderer; constructor() { @@ -40,12 +37,12 @@ export class AllDayManager { originalElementId: originalElement?.dataset?.eventId, originalElementTag: originalElement?.tagName }); - + if (targetDate && cloneElement) { this.handleConvertToAllDay(targetDate, cloneElement); } - this.checkAndAnimateAllDayHeight (); + this.checkAndAnimateAllDayHeight(); }); eventBus.on('drag:mouseleave-header', (event) => { @@ -59,7 +56,7 @@ export class AllDayManager { this.handleConvertFromAllDay(cloneElement); } - this.checkAndAnimateAllDayHeight (); + this.checkAndAnimateAllDayHeight(); }); @@ -111,12 +108,12 @@ export class AllDayManager { // Listen for drag cancellation to recalculate height eventBus.on('drag:cancelled', (event) => { const { draggedElement, reason } = (event as CustomEvent).detail; - + console.log('🚫 AllDayManager: Drag cancelled', { eventId: draggedElement?.dataset?.eventId, reason }); - + // Recalculate all-day height since clones may have been removed this.checkAndAnimateAllDayHeight(); }); @@ -128,37 +125,17 @@ export class AllDayManager { }); } - /** - * Get cached all-day container element - */ + private getAllDayContainer(): HTMLElement | null { - if (!this.cachedAllDayContainer) { - const calendarHeader = this.getCalendarHeader(); - if (calendarHeader) { - this.cachedAllDayContainer = calendarHeader.querySelector('swp-allday-container'); - } - } - return this.cachedAllDayContainer; + return document.querySelector('swp-calendar-header swp-allday-container'); } - /** - * Get cached calendar header element - */ private getCalendarHeader(): HTMLElement | null { - if (!this.cachedCalendarHeader) { - this.cachedCalendarHeader = document.querySelector('swp-calendar-header'); - } - return this.cachedCalendarHeader; + return document.querySelector('swp-calendar-header'); } - /** - * Get cached header spacer element - */ private getHeaderSpacer(): HTMLElement | null { - if (!this.cachedHeaderSpacer) { - this.cachedHeaderSpacer = document.querySelector('swp-header-spacer'); - } - return this.cachedHeaderSpacer; + return document.querySelector('swp-header-spacer'); } /** @@ -177,15 +154,6 @@ export class AllDayManager { return { targetHeight, currentHeight, heightDifference }; } - /** - * Clear cached DOM elements (call when DOM structure changes) - */ - private clearCache(): void { - this.cachedCalendarHeader = null; - this.cachedAllDayContainer = null; - this.cachedHeaderSpacer = null; - } - /** * Collapse all-day row when no events */ @@ -198,7 +166,10 @@ export class AllDayManager { */ public checkAndAnimateAllDayHeight(): void { const container = this.getAllDayContainer(); - if (!container) return; + if (!container) { + this.animateToRows(0); + return; + } const allDayEvents = container.querySelectorAll('swp-event'); @@ -208,15 +179,15 @@ export class AllDayManager { if (allDayEvents.length > 0) { // Track which rows are actually used by checking grid positions const usedRows = new Set(); - + (Array.from(allDayEvents) as HTMLElement[]).forEach((event: HTMLElement) => { const gridRow = parseInt(getComputedStyle(event).gridRowStart) || 1; usedRows.add(gridRow); }); - + // Max rows = highest row number in use maxRows = usedRows.size > 0 ? Math.max(...usedRows) : 0; - + console.log('πŸ” AllDayManager: Height calculation', { totalEvents: allDayEvents.length, usedRows: Array.from(usedRows).sort(), @@ -254,7 +225,7 @@ export class AllDayManager { { height: `${currentParentHeight}px` }, { height: `${targetParentHeight}px` } ], { - duration: 300, + duration: 150, easing: 'ease-out', fill: 'forwards' }) @@ -464,7 +435,7 @@ export class AllDayManager { * Handle drag end for all-day events */ private handleDragEnd(originalElement: HTMLElement, dragClone: HTMLElement, finalPosition: any): void { - + // Normalize clone const cloneId = dragClone.dataset.eventId; if (cloneId?.startsWith('clone-')) { @@ -484,12 +455,4 @@ export class AllDayManager { finalColumn: dragClone.style.gridColumn }); } - - - /** - * Clean up cached elements and resources - */ - public destroy(): void { - this.clearCache(); - } } \ No newline at end of file diff --git a/src/managers/HeaderManager.ts b/src/managers/HeaderManager.ts index 5c9d123..b30fbc0 100644 --- a/src/managers/HeaderManager.ts +++ b/src/managers/HeaderManager.ts @@ -4,7 +4,8 @@ import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; import { CoreEvents } from '../constants/CoreEvents'; import { HeaderRenderContext } from '../renderers/HeaderRenderer'; import { ResourceCalendarData } from '../types/CalendarTypes'; -import { DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload } from '../types/EventTypes'; +import { DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload, HeaderReadyEventPayload } from '../types/EventTypes'; +import { DateCalculator } from '../utils/DateCalculator'; /** * HeaderManager - Handles all header-related event logic @@ -172,10 +173,18 @@ export class HeaderManager { // Setup event listeners on the new content this.setupHeaderDragListeners(); - // Notify other managers that header is ready - eventBus.emit('header:ready', { - headerElement: calendarHeader - }); + // Calculate period from current date + const weekStart = DateCalculator.getISOWeekStart(currentDate); + const weekEnd = DateCalculator.addDays(weekStart, 6); + + // Notify other managers that header is ready with period data + const payload: HeaderReadyEventPayload = { + headerElement: calendarHeader, + startDate: weekStart, + endDate: weekEnd, + isNavigation: false + }; + eventBus.emit('header:ready', payload); } /** diff --git a/src/renderers/AllDayEventRenderer.ts b/src/renderers/AllDayEventRenderer.ts index b29ca87..0a853e4 100644 --- a/src/renderers/AllDayEventRenderer.ts +++ b/src/renderers/AllDayEventRenderer.ts @@ -16,25 +16,19 @@ export class AllDayEventRenderer { * Get or cache all-day container, create if it doesn't exist - SIMPLIFIED (no ghost columns) */ 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); - console.log('πŸ—οΈ AllDayEventRenderer: Created all-day container (NO ghost columns)'); - - // NO MORE GHOST COLUMNS! πŸŽ‰ - // Mouse detection handled by HeaderManager coordinate calculation } } - } - return this.container; + return this.container; + } // REMOVED: createGhostColumns() method - no longer needed! diff --git a/src/renderers/EventRendererManager.ts b/src/renderers/EventRendererManager.ts index 53140fa..2f3fcf2 100644 --- a/src/renderers/EventRendererManager.ts +++ b/src/renderers/EventRendererManager.ts @@ -7,7 +7,7 @@ import { EventManager } from '../managers/EventManager'; import { EventRendererStrategy } from './EventRenderer'; import { SwpEventElement } from '../elements/SwpEventElement'; import { AllDayEventRenderer } from './AllDayEventRenderer'; -import { DragStartEventPayload, DragMoveEventPayload, DragEndEventPayload, DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload } from '../types/EventTypes'; +import { DragStartEventPayload, DragMoveEventPayload, DragEndEventPayload, DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload, HeaderReadyEventPayload } from '../types/EventTypes'; /** * EventRenderingService - Render events i DOM med positionering using Strategy Pattern * HΓ₯ndterer event positioning og overlap detection @@ -18,9 +18,6 @@ export class EventRenderingService { private strategy: EventRendererStrategy; private allDayEventRenderer: AllDayEventRenderer; - // Store all-day events until header is ready with dates - private pendingAllDayEvents: CalendarEvent[] = []; - private isHeaderReady: boolean = false; private dragMouseLeaveHeaderListener: ((event: Event) => void) | null = null; @@ -42,7 +39,6 @@ export class EventRenderingService { * Render events in a specific container for a given period */ public renderEvents(context: RenderContext): void { - // Clear existing events in the specific container first this.strategy.clearEvents(context.container); @@ -56,15 +52,13 @@ export class EventRenderingService { return; } - // Filter events by type + // Filter events by type - only render timed events here const timedEvents = events.filter(event => !event.allDay); - const allDayEvents = events.filter(event => event.allDay); console.log('🎯 EventRenderingService: Event filtering', { totalEvents: events.length, timedEvents: timedEvents.length, - allDayEvents: allDayEvents.length, - allDayEventIds: allDayEvents.map(e => e.id) + allDayEvents: events.length - timedEvents.length }); // Render timed events using existing strategy @@ -72,21 +66,6 @@ export class EventRenderingService { this.strategy.renderEvents(timedEvents, context.container); } - // Render all-day events - wait for header if not ready - if (allDayEvents.length > 0) { - if (this.isHeaderReady) { - this.renderAllDayEvents(allDayEvents); - // Check and adjust all-day container height after rendering - this.eventBus.emit('allday:checkHeight'); - } else { - console.log('πŸ• EventRendererManager: Header not ready, storing all-day events for later'); - // Only store if we don't already have pending events to avoid duplicates - if (this.pendingAllDayEvents.length === 0) { - this.pendingAllDayEvents = [...allDayEvents]; - } - } - } - // Emit EVENTS_RENDERED event for filtering system this.eventBus.emit(CoreEvents.EVENTS_RENDERED, { events: events, @@ -104,18 +83,16 @@ export class EventRenderingService { this.handleViewChanged(event as CustomEvent); }); - // Listen for header ready - when dates are populated - this.eventBus.on('header:ready', () => { - console.log('🎯 EventRendererManager: Header ready, rendering pending all-day events'); - this.isHeaderReady = true; + // Listen for header ready - when dates are populated with period data + this.eventBus.on('header:ready', (event: Event) => { + const { startDate, endDate } = (event as CustomEvent).detail; + console.log('🎯 EventRendererManager: Header ready with period data', { + startDate: startDate.toISOString(), + endDate: endDate.toISOString() + }); - if (this.pendingAllDayEvents.length > 0) { - this.renderAllDayEvents(this.pendingAllDayEvents); - this.pendingAllDayEvents = []; // Clear after rendering - - // Check and adjust all-day container height after rendering - this.eventBus.emit('allday:checkHeight'); - } + // Render all-day events using period from header + this.renderAllDayEventsForPeriod(startDate, endDate); }); // Handle all drag events and delegate to appropriate renderer @@ -139,12 +116,13 @@ export class EventRenderingService { * Handle GRID_RENDERED event - render events in the current grid */ private handleGridRendered(event: CustomEvent): void { - const { container, startDate, endDate, currentDate } = event.detail; + const { container, startDate, endDate, currentDate, isNavigation } = event.detail; if (!container) { return; } + let periodStart: Date; let periodEnd: Date; @@ -350,15 +328,28 @@ export class EventRenderingService { } /** - * Render all-day events using AllDayEventRenderer + * Render all-day events for specific period using AllDayEventRenderer */ - private renderAllDayEvents(allDayEvents: CalendarEvent[]): void { + private renderAllDayEventsForPeriod(startDate: Date, endDate: Date): void { + // Get events from EventManager for the period + const events = this.eventManager.getEventsForPeriod(startDate, endDate); + + // Filter for all-day events + const allDayEvents = events.filter(event => event.allDay); + console.log('πŸ—οΈ EventRenderingService: Rendering all-day events', { + period: { + start: startDate.toISOString(), + end: endDate.toISOString() + }, count: allDayEvents.length, events: allDayEvents.map(e => ({ id: e.id, title: e.title })) }); - // Header always exists now, so we can render directly + // Clear existing all-day events first + this.clearAllDayEvents(); + + // Render each all-day event allDayEvents.forEach(event => { const renderedElement = this.allDayEventRenderer.renderAllDayEvent(event); if (renderedElement) { @@ -374,20 +365,26 @@ export class EventRenderingService { }); } }); + + // Check and adjust all-day container height after rendering + this.eventBus.emit('allday:checkHeight'); + } + + /** + * Clear only all-day events + */ + private clearAllDayEvents(): void { + const allDayContainer = document.querySelector('swp-allday-container'); + if (allDayContainer) { + allDayContainer.querySelectorAll('swp-event').forEach(event => event.remove()); + } } private clearEvents(container?: HTMLElement): void { this.strategy.clearEvents(container); // Also clear all-day events - const allDayContainer = document.querySelector('swp-allday-container'); - if (allDayContainer) { - allDayContainer.querySelectorAll('swp-event').forEach(event => event.remove()); - } - - // Clear pending all-day events - this.pendingAllDayEvents = []; - this.isHeaderReady = false; + this.clearAllDayEvents(); } public refresh(container?: HTMLElement): void { diff --git a/src/renderers/NavigationRenderer.ts b/src/renderers/NavigationRenderer.ts index 6ca0efa..396a011 100644 --- a/src/renderers/NavigationRenderer.ts +++ b/src/renderers/NavigationRenderer.ts @@ -4,6 +4,7 @@ import { calendarConfig } from '../core/CalendarConfig'; import { DateCalculator } from '../utils/DateCalculator'; import { EventRenderingService } from './EventRendererManager'; import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; +import { HeaderReadyEventPayload } from '../types/EventTypes'; /** * NavigationRenderer - Handles DOM rendering for navigation containers @@ -204,6 +205,16 @@ export class NavigationRenderer { dayColumns.appendChild(column); }); + + // Emit header:ready after header has been populated with date elements + const weekEnd = DateCalculator.addDays(weekStart, 6); + const payload: HeaderReadyEventPayload = { + headerElement: header as HTMLElement, + startDate: weekStart, + endDate: weekEnd, + isNavigation: true + }; + this.eventBus.emit('header:ready', payload); } /** diff --git a/src/types/EventTypes.ts b/src/types/EventTypes.ts index 3708def..f094b0a 100644 --- a/src/types/EventTypes.ts +++ b/src/types/EventTypes.ts @@ -85,4 +85,12 @@ export interface DragMouseLeaveHeaderEventPayload { mousePosition: MousePosition; originalElement: HTMLElement| null; cloneElement: HTMLElement| null; +} + +// Header ready event payload +export interface HeaderReadyEventPayload { + headerElement: HTMLElement; + startDate: Date; + endDate: Date; + isNavigation?: boolean; } \ No newline at end of file