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.
This commit is contained in:
Janus C. H. Knudsen 2025-09-22 23:37:43 +02:00
parent f5e9909935
commit 6498b0ba8e
6 changed files with 98 additions and 116 deletions

View file

@ -16,9 +16,6 @@ import {
* Separated from HeaderManager for clean responsibility separation * Separated from HeaderManager for clean responsibility separation
*/ */
export class AllDayManager { export class AllDayManager {
private cachedAllDayContainer: HTMLElement | null = null;
private cachedCalendarHeader: HTMLElement | null = null;
private cachedHeaderSpacer: HTMLElement | null = null;
private allDayEventRenderer: AllDayEventRenderer; private allDayEventRenderer: AllDayEventRenderer;
constructor() { constructor() {
@ -45,7 +42,7 @@ export class AllDayManager {
this.handleConvertToAllDay(targetDate, cloneElement); this.handleConvertToAllDay(targetDate, cloneElement);
} }
this.checkAndAnimateAllDayHeight (); this.checkAndAnimateAllDayHeight();
}); });
eventBus.on('drag:mouseleave-header', (event) => { eventBus.on('drag:mouseleave-header', (event) => {
@ -59,7 +56,7 @@ export class AllDayManager {
this.handleConvertFromAllDay(cloneElement); this.handleConvertFromAllDay(cloneElement);
} }
this.checkAndAnimateAllDayHeight (); this.checkAndAnimateAllDayHeight();
}); });
@ -128,37 +125,17 @@ export class AllDayManager {
}); });
} }
/**
* Get cached all-day container element
*/
private getAllDayContainer(): HTMLElement | null { private getAllDayContainer(): HTMLElement | null {
if (!this.cachedAllDayContainer) { return document.querySelector('swp-calendar-header swp-allday-container');
const calendarHeader = this.getCalendarHeader();
if (calendarHeader) {
this.cachedAllDayContainer = calendarHeader.querySelector('swp-allday-container');
}
}
return this.cachedAllDayContainer;
} }
/**
* Get cached calendar header element
*/
private getCalendarHeader(): HTMLElement | null { private getCalendarHeader(): HTMLElement | null {
if (!this.cachedCalendarHeader) { return document.querySelector('swp-calendar-header');
this.cachedCalendarHeader = document.querySelector('swp-calendar-header');
}
return this.cachedCalendarHeader;
} }
/**
* Get cached header spacer element
*/
private getHeaderSpacer(): HTMLElement | null { private getHeaderSpacer(): HTMLElement | null {
if (!this.cachedHeaderSpacer) { return document.querySelector('swp-header-spacer');
this.cachedHeaderSpacer = document.querySelector('swp-header-spacer');
}
return this.cachedHeaderSpacer;
} }
/** /**
@ -177,15 +154,6 @@ export class AllDayManager {
return { targetHeight, currentHeight, heightDifference }; 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 * Collapse all-day row when no events
*/ */
@ -198,7 +166,10 @@ export class AllDayManager {
*/ */
public checkAndAnimateAllDayHeight(): void { public checkAndAnimateAllDayHeight(): void {
const container = this.getAllDayContainer(); const container = this.getAllDayContainer();
if (!container) return; if (!container) {
this.animateToRows(0);
return;
}
const allDayEvents = container.querySelectorAll('swp-event'); const allDayEvents = container.querySelectorAll('swp-event');
@ -254,7 +225,7 @@ export class AllDayManager {
{ height: `${currentParentHeight}px` }, { height: `${currentParentHeight}px` },
{ height: `${targetParentHeight}px` } { height: `${targetParentHeight}px` }
], { ], {
duration: 300, duration: 150,
easing: 'ease-out', easing: 'ease-out',
fill: 'forwards' fill: 'forwards'
}) })
@ -484,12 +455,4 @@ export class AllDayManager {
finalColumn: dragClone.style.gridColumn finalColumn: dragClone.style.gridColumn
}); });
} }
/**
* Clean up cached elements and resources
*/
public destroy(): void {
this.clearCache();
}
} }

View file

@ -4,7 +4,8 @@ import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
import { CoreEvents } from '../constants/CoreEvents'; import { CoreEvents } from '../constants/CoreEvents';
import { HeaderRenderContext } from '../renderers/HeaderRenderer'; import { HeaderRenderContext } from '../renderers/HeaderRenderer';
import { ResourceCalendarData } from '../types/CalendarTypes'; 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 * HeaderManager - Handles all header-related event logic
@ -172,10 +173,18 @@ export class HeaderManager {
// Setup event listeners on the new content // Setup event listeners on the new content
this.setupHeaderDragListeners(); this.setupHeaderDragListeners();
// Notify other managers that header is ready // Calculate period from current date
eventBus.emit('header:ready', { const weekStart = DateCalculator.getISOWeekStart(currentDate);
headerElement: calendarHeader 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);
} }
/** /**

View file

@ -16,25 +16,19 @@ export class AllDayEventRenderer {
* Get or cache all-day container, create if it doesn't exist - SIMPLIFIED (no ghost columns) * Get or cache all-day container, create if it doesn't exist - SIMPLIFIED (no ghost columns)
*/ */
private getContainer(): HTMLElement | null { private getContainer(): HTMLElement | null {
if (!this.container) {
const header = document.querySelector('swp-calendar-header'); const header = document.querySelector('swp-calendar-header');
if (header) { if (header) {
// Try to find existing container
this.container = header.querySelector('swp-allday-container'); this.container = header.querySelector('swp-allday-container');
// If not found, create it
if (!this.container) { if (!this.container) {
this.container = document.createElement('swp-allday-container'); this.container = document.createElement('swp-allday-container');
header.appendChild(this.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! // REMOVED: createGhostColumns() method - no longer needed!

View file

@ -7,7 +7,7 @@ import { EventManager } from '../managers/EventManager';
import { EventRendererStrategy } from './EventRenderer'; import { EventRendererStrategy } from './EventRenderer';
import { SwpEventElement } from '../elements/SwpEventElement'; import { SwpEventElement } from '../elements/SwpEventElement';
import { AllDayEventRenderer } from './AllDayEventRenderer'; 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 * EventRenderingService - Render events i DOM med positionering using Strategy Pattern
* Håndterer event positioning og overlap detection * Håndterer event positioning og overlap detection
@ -18,9 +18,6 @@ export class EventRenderingService {
private strategy: EventRendererStrategy; private strategy: EventRendererStrategy;
private allDayEventRenderer: AllDayEventRenderer; 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; private dragMouseLeaveHeaderListener: ((event: Event) => void) | null = null;
@ -42,7 +39,6 @@ export class EventRenderingService {
* Render events in a specific container for a given period * Render events in a specific container for a given period
*/ */
public renderEvents(context: RenderContext): void { public renderEvents(context: RenderContext): void {
// Clear existing events in the specific container first // Clear existing events in the specific container first
this.strategy.clearEvents(context.container); this.strategy.clearEvents(context.container);
@ -56,15 +52,13 @@ export class EventRenderingService {
return; return;
} }
// Filter events by type // Filter events by type - only render timed events here
const timedEvents = events.filter(event => !event.allDay); const timedEvents = events.filter(event => !event.allDay);
const allDayEvents = events.filter(event => event.allDay);
console.log('🎯 EventRenderingService: Event filtering', { console.log('🎯 EventRenderingService: Event filtering', {
totalEvents: events.length, totalEvents: events.length,
timedEvents: timedEvents.length, timedEvents: timedEvents.length,
allDayEvents: allDayEvents.length, allDayEvents: events.length - timedEvents.length
allDayEventIds: allDayEvents.map(e => e.id)
}); });
// Render timed events using existing strategy // Render timed events using existing strategy
@ -72,21 +66,6 @@ export class EventRenderingService {
this.strategy.renderEvents(timedEvents, context.container); 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 // Emit EVENTS_RENDERED event for filtering system
this.eventBus.emit(CoreEvents.EVENTS_RENDERED, { this.eventBus.emit(CoreEvents.EVENTS_RENDERED, {
events: events, events: events,
@ -104,18 +83,16 @@ export class EventRenderingService {
this.handleViewChanged(event as CustomEvent); this.handleViewChanged(event as CustomEvent);
}); });
// Listen for header ready - when dates are populated // Listen for header ready - when dates are populated with period data
this.eventBus.on('header:ready', () => { this.eventBus.on('header:ready', (event: Event) => {
console.log('🎯 EventRendererManager: Header ready, rendering pending all-day events'); const { startDate, endDate } = (event as CustomEvent<HeaderReadyEventPayload>).detail;
this.isHeaderReady = true; console.log('🎯 EventRendererManager: Header ready with period data', {
startDate: startDate.toISOString(),
endDate: endDate.toISOString()
});
if (this.pendingAllDayEvents.length > 0) { // Render all-day events using period from header
this.renderAllDayEvents(this.pendingAllDayEvents); this.renderAllDayEventsForPeriod(startDate, endDate);
this.pendingAllDayEvents = []; // Clear after rendering
// Check and adjust all-day container height after rendering
this.eventBus.emit('allday:checkHeight');
}
}); });
// Handle all drag events and delegate to appropriate renderer // 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 * Handle GRID_RENDERED event - render events in the current grid
*/ */
private handleGridRendered(event: CustomEvent): void { private handleGridRendered(event: CustomEvent): void {
const { container, startDate, endDate, currentDate } = event.detail; const { container, startDate, endDate, currentDate, isNavigation } = event.detail;
if (!container) { if (!container) {
return; return;
} }
let periodStart: Date; let periodStart: Date;
let periodEnd: 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', { console.log('🏗️ EventRenderingService: Rendering all-day events', {
period: {
start: startDate.toISOString(),
end: endDate.toISOString()
},
count: allDayEvents.length, count: allDayEvents.length,
events: allDayEvents.map(e => ({ id: e.id, title: e.title })) 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 => { allDayEvents.forEach(event => {
const renderedElement = this.allDayEventRenderer.renderAllDayEvent(event); const renderedElement = this.allDayEventRenderer.renderAllDayEvent(event);
if (renderedElement) { 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 { private clearEvents(container?: HTMLElement): void {
this.strategy.clearEvents(container); this.strategy.clearEvents(container);
// Also clear all-day events // Also clear all-day events
const allDayContainer = document.querySelector('swp-allday-container'); this.clearAllDayEvents();
if (allDayContainer) {
allDayContainer.querySelectorAll('swp-event').forEach(event => event.remove());
}
// Clear pending all-day events
this.pendingAllDayEvents = [];
this.isHeaderReady = false;
} }
public refresh(container?: HTMLElement): void { public refresh(container?: HTMLElement): void {

View file

@ -4,6 +4,7 @@ import { calendarConfig } from '../core/CalendarConfig';
import { DateCalculator } from '../utils/DateCalculator'; import { DateCalculator } from '../utils/DateCalculator';
import { EventRenderingService } from './EventRendererManager'; import { EventRenderingService } from './EventRendererManager';
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
import { HeaderReadyEventPayload } from '../types/EventTypes';
/** /**
* NavigationRenderer - Handles DOM rendering for navigation containers * NavigationRenderer - Handles DOM rendering for navigation containers
@ -204,6 +205,16 @@ export class NavigationRenderer {
dayColumns.appendChild(column); 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);
} }
/** /**

View file

@ -86,3 +86,11 @@ export interface DragMouseLeaveHeaderEventPayload {
originalElement: HTMLElement| null; originalElement: HTMLElement| null;
cloneElement: HTMLElement| null; cloneElement: HTMLElement| null;
} }
// Header ready event payload
export interface HeaderReadyEventPayload {
headerElement: HTMLElement;
startDate: Date;
endDate: Date;
isNavigation?: boolean;
}