Improves performance by caching DOM elements

Caches frequently accessed DOM elements in NavigationManager and
EventRenderer to reduce redundant queries, improving performance.

Updates the event renderer to trigger all-day height animations and
introduces a destroy method for resource management.

Refactors time formatting in EventRenderer to handle both total
minutes and Date objects using a unified method.
This commit is contained in:
Janus Knudsen 2025-09-03 18:15:33 +02:00
parent 77592278d3
commit 0da875a224
3 changed files with 126 additions and 37 deletions

View file

@ -17,6 +17,10 @@ export class NavigationManager {
private targetWeek: Date; private targetWeek: Date;
private animationQueue: number = 0; private animationQueue: number = 0;
// Cached DOM elements to avoid redundant queries
private cachedCalendarContainer: HTMLElement | null = null;
private cachedCurrentGrid: HTMLElement | null = null;
constructor(eventBus: IEventBus, eventRenderer: EventRenderingService) { constructor(eventBus: IEventBus, eventRenderer: EventRenderingService) {
this.eventBus = eventBus; this.eventBus = eventBus;
this.dateCalculator = new DateCalculator(calendarConfig); this.dateCalculator = new DateCalculator(calendarConfig);
@ -31,6 +35,37 @@ export class NavigationManager {
// Don't update week info immediately - wait for DOM to be ready // Don't update week info immediately - wait for DOM to be ready
} }
/**
* Get cached calendar container element
*/
private getCalendarContainer(): HTMLElement | null {
if (!this.cachedCalendarContainer) {
this.cachedCalendarContainer = document.querySelector('swp-calendar-container');
}
return this.cachedCalendarContainer;
}
/**
* Get cached current grid element
*/
private getCurrentGrid(): HTMLElement | null {
const container = this.getCalendarContainer();
if (!container) return null;
if (!this.cachedCurrentGrid) {
this.cachedCurrentGrid = container.querySelector('swp-grid-container:not([data-prerendered])');
}
return this.cachedCurrentGrid;
}
/**
* Clear cached DOM elements (call when DOM structure changes)
*/
private clearCache(): void {
this.cachedCalendarContainer = null;
this.cachedCurrentGrid = null;
}
private setupEventListeners(): void { private setupEventListeners(): void {
// Initial DOM update when calendar is initialized // Initial DOM update when calendar is initialized
this.eventBus.on(CoreEvents.INITIALIZED, () => { this.eventBus.on(CoreEvents.INITIALIZED, () => {
@ -137,8 +172,8 @@ export class NavigationManager {
* Animation transition using pre-rendered containers when available * Animation transition using pre-rendered containers when available
*/ */
private animateTransition(direction: 'prev' | 'next', targetWeek: Date): void { private animateTransition(direction: 'prev' | 'next', targetWeek: Date): void {
const container = document.querySelector('swp-calendar-container'); const container = this.getCalendarContainer();
const currentGrid = container?.querySelector('swp-grid-container:not([data-prerendered])'); const currentGrid = this.getCurrentGrid();
if (!container || !currentGrid) { if (!container || !currentGrid) {
return; return;
@ -148,7 +183,7 @@ export class NavigationManager {
let newGrid: HTMLElement; let newGrid: HTMLElement;
// Always create a fresh container for consistent behavior // Always create a fresh container for consistent behavior
newGrid = this.navigationRenderer.renderContainer(container as HTMLElement, targetWeek); newGrid = this.navigationRenderer.renderContainer(container, targetWeek);
// Clear any existing transforms before animation // Clear any existing transforms before animation
@ -187,6 +222,9 @@ export class NavigationManager {
newGrid.style.position = 'relative'; newGrid.style.position = 'relative';
newGrid.removeAttribute('data-prerendered'); newGrid.removeAttribute('data-prerendered');
// Clear cache since DOM structure changed
this.clearCache();
// Update state // Update state
this.currentWeek = new Date(targetWeek); this.currentWeek = new Date(targetWeek);
this.animationQueue--; this.animationQueue--;

View file

@ -26,9 +26,9 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
private draggedClone: HTMLElement | null = null; private draggedClone: HTMLElement | null = null;
private originalEvent: HTMLElement | null = null; private originalEvent: HTMLElement | null = null;
constructor(config: CalendarConfig) { constructor(config: CalendarConfig, dateCalculator?: DateCalculator) {
this.config = config; this.config = config;
this.dateCalculator = new DateCalculator(config); this.dateCalculator = dateCalculator || new DateCalculator(config);
} }
/** /**
@ -68,11 +68,26 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
// Handle navigation period change (when slide animation completes) // Handle navigation period change (when slide animation completes)
eventBus.on(CoreEvents.PERIOD_CHANGED, () => { eventBus.on(CoreEvents.PERIOD_CHANGED, () => {
// Animate all-day height after navigation completes // Animate all-day height after navigation completes
this.triggerAllDayHeightAnimation();
});
}
/**
* Trigger all-day height animation without creating new renderer instance
*/
private triggerAllDayHeightAnimation(): void {
import('./HeaderRenderer').then(({ DateHeaderRenderer }) => { import('./HeaderRenderer').then(({ DateHeaderRenderer }) => {
const headerRenderer = new DateHeaderRenderer(); const headerRenderer = new DateHeaderRenderer();
headerRenderer.checkAndAnimateAllDayHeight(); headerRenderer.checkAndAnimateAllDayHeight();
}); });
}); }
/**
* Cleanup method for proper resource management
*/
public destroy(): void {
this.draggedClone = null;
this.originalEvent = null;
} }
/** /**
@ -171,13 +186,24 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
} }
/** /**
* Format time from total minutes * Unified time formatting method - handles both total minutes and Date objects
*/ */
private formatTime(totalMinutes: number): string { private formatTime(input: number | Date | string): string {
const hours = Math.floor(totalMinutes / 60) % 24; let hours: number, minutes: number;
const minutes = totalMinutes % 60;
if (typeof input === 'number') {
// Total minutes input
hours = Math.floor(input / 60) % 24;
minutes = input % 60;
} else {
// Date or ISO string input
const date = typeof input === 'string' ? new Date(input) : input;
hours = date.getHours();
minutes = date.getMinutes();
}
const period = hours >= 12 ? 'PM' : 'AM'; const period = hours >= 12 ? 'PM' : 'AM';
const displayHours = hours % 12 || 12; const displayHours = hours > 12 ? hours - 12 : (hours === 0 ? 12 : hours);
return `${displayHours}:${minutes.toString().padStart(2, '0')} ${period}`; return `${displayHours}:${minutes.toString().padStart(2, '0')} ${period}`;
} }
@ -331,10 +357,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
this.draggedClone = allDayEvent; this.draggedClone = allDayEvent;
// Check if height animation is needed // Check if height animation is needed
import('./HeaderRenderer').then(({ DateHeaderRenderer }) => { this.triggerAllDayHeightAnimation();
const headerRenderer = new DateHeaderRenderer();
headerRenderer.checkAndAnimateAllDayHeight();
});
} }
@ -556,9 +579,9 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
// Color is now handled by CSS classes based on data-type attribute // Color is now handled by CSS classes based on data-type attribute
// Format time for display // Format time for display using unified method
const startTime = this.formatTimeFromISOString(event.start); const startTime = this.formatTime(event.start);
const endTime = this.formatTimeFromISOString(event.end); const endTime = this.formatTime(event.end);
// Calculate duration in minutes // Calculate duration in minutes
const startDate = new Date(event.start); const startDate = new Date(event.start);
@ -599,17 +622,6 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
return { top, height }; return { top, height };
} }
protected formatTimeFromISOString(isoString: string): string {
const date = new Date(isoString);
const hours = date.getHours();
const minutes = date.getMinutes();
const period = hours >= 12 ? 'PM' : 'AM';
const displayHour = hours > 12 ? hours - 12 : (hours === 0 ? 12 : hours);
return `${displayHour}:${minutes.toString().padStart(2, '0')} ${period}`;
}
/** /**
* Calculate grid column span for event * Calculate grid column span for event
*/ */
@ -666,8 +678,8 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
* Date-based event renderer * Date-based event renderer
*/ */
export class DateEventRenderer extends BaseEventRenderer { export class DateEventRenderer extends BaseEventRenderer {
constructor(config: CalendarConfig) { constructor(config: CalendarConfig, dateCalculator?: DateCalculator) {
super(config); super(config, dateCalculator);
this.setupDragEventListeners(); this.setupDragEventListeners();
} }

View file

@ -16,6 +16,10 @@ export class NavigationRenderer {
private dateCalculator: DateCalculator; private dateCalculator: DateCalculator;
private eventRenderer: EventRenderingService; private eventRenderer: EventRenderingService;
// Cached DOM elements to avoid redundant queries
private cachedWeekNumberElement: HTMLElement | null = null;
private cachedDateRangeElement: HTMLElement | null = null;
constructor(eventBus: IEventBus, config: CalendarConfig, eventRenderer: EventRenderingService) { constructor(eventBus: IEventBus, config: CalendarConfig, eventRenderer: EventRenderingService) {
this.eventBus = eventBus; this.eventBus = eventBus;
this.config = config; this.config = config;
@ -24,6 +28,34 @@ export class NavigationRenderer {
this.setupEventListeners(); this.setupEventListeners();
} }
/**
* Get cached week number element
*/
private getWeekNumberElement(): HTMLElement | null {
if (!this.cachedWeekNumberElement) {
this.cachedWeekNumberElement = document.querySelector('swp-week-number');
}
return this.cachedWeekNumberElement;
}
/**
* Get cached date range element
*/
private getDateRangeElement(): HTMLElement | null {
if (!this.cachedDateRangeElement) {
this.cachedDateRangeElement = document.querySelector('swp-date-range');
}
return this.cachedDateRangeElement;
}
/**
* Clear cached DOM elements (call when DOM structure changes)
*/
private clearCache(): void {
this.cachedWeekNumberElement = null;
this.cachedDateRangeElement = null;
}
/** /**
* Setup event listeners for DOM updates * Setup event listeners for DOM updates
*/ */
@ -36,11 +68,11 @@ export class NavigationRenderer {
} }
/** /**
* Update week info in DOM elements * Update week info in DOM elements using cached references
*/ */
private updateWeekInfoInDOM(weekNumber: number, dateRange: string): void { private updateWeekInfoInDOM(weekNumber: number, dateRange: string): void {
const weekNumberElement = document.querySelector('swp-week-number'); const weekNumberElement = this.getWeekNumberElement();
const dateRangeElement = document.querySelector('swp-date-range'); const dateRangeElement = this.getDateRangeElement();
if (weekNumberElement) { if (weekNumberElement) {
weekNumberElement.textContent = `Week ${weekNumber}`; weekNumberElement.textContent = `Week ${weekNumber}`;
@ -235,4 +267,11 @@ export class NavigationRenderer {
}); });
} }
/**
* Public cleanup method for cached elements
*/
public destroy(): void {
this.clearCache();
}
} }