Refactors calendar managers and renderers

Improves calendar rendering performance by centralizing DOM manipulation in a dedicated `GridRenderer` class. This reduces redundant DOM queries and improves overall efficiency.

Introduces `EventManager` for optimized event lifecycle management with caching and optimized data processing.

The `ViewManager` is refactored for optimized view switching and event handling, further streamlining the application's architecture.

This change moves from a strategy-based `GridManager` to a simpler approach leveraging the `GridRenderer` directly for DOM updates. This eliminates unnecessary abstractions and improves code maintainability.

The changes include removing the old `GridManager`, `EventManager` and introducing new versions.
This commit is contained in:
Janus Knudsen 2025-09-03 18:51:19 +02:00
parent b8b44ddae8
commit 05bb074e9a
4 changed files with 566 additions and 271 deletions

View file

@ -4,122 +4,180 @@ import { calendarConfig } from '../core/CalendarConfig';
import { CoreEvents } from '../constants/CoreEvents';
/**
* ViewManager - Håndterer skift mellem dag/uge/måned visninger
* Arbejder med custom tags fra POC design
* ViewManager - Optimized view switching with consolidated event handling
* Reduces redundant DOM queries and event listener setups
*/
export class ViewManager {
private eventBus: IEventBus;
private currentView: CalendarView = 'week';
private eventCleanup: (() => void)[] = [];
private buttonListeners: Map<Element, EventListener> = new Map();
// Cached DOM elements for performance
private cachedViewButtons: NodeListOf<Element> | null = null;
private cachedWorkweekButtons: NodeListOf<Element> | null = null;
private lastButtonCacheTime: number = 0;
private readonly CACHE_DURATION = 5000; // 5 seconds
constructor(eventBus: IEventBus) {
this.eventBus = eventBus;
this.setupEventListeners();
}
/**
* Consolidated event listener setup with better organization
*/
private setupEventListeners(): void {
// Track event bus listeners for cleanup
// Event bus listeners
this.setupEventBusListeners();
// DOM button handlers with consolidated logic
this.setupButtonHandlers();
}
/**
* Setup event bus listeners with proper cleanup tracking
*/
private setupEventBusListeners(): void {
this.eventCleanup.push(
this.eventBus.on(CoreEvents.INITIALIZED, () => {
this.initializeView();
})
);
this.eventCleanup.push(
this.eventBus.on(CoreEvents.VIEW_CHANGED, (event: Event) => {
const customEvent = event as CustomEvent;
const { currentView } = customEvent.detail;
this.changeView(currentView);
})
);
// Remove redundant VIEW_CHANGED listener that causes circular calls
// changeView is called directly from button handlers
this.eventCleanup.push(
this.eventBus.on(CoreEvents.DATE_CHANGED, () => {
this.refreshCurrentView();
})
);
// Setup view button handlers
this.setupViewButtonHandlers();
// Setup workweek preset button handlers
this.setupWorkweekButtonHandlers();
}
private setupViewButtonHandlers(): void {
const viewButtons = document.querySelectorAll('swp-view-button[data-view]');
viewButtons.forEach(button => {
const handler = (event: Event) => {
event.preventDefault();
const view = button.getAttribute('data-view') as CalendarView;
if (view && this.isValidView(view)) {
this.changeView(view);
}
};
button.addEventListener('click', handler);
this.buttonListeners.set(button, handler);
/**
* Consolidated button handler setup with shared logic
*/
private setupButtonHandlers(): void {
// Setup view buttons with consolidated handler
this.setupButtonGroup('swp-view-button[data-view]', 'data-view', (value) => {
if (this.isValidView(value)) {
this.changeView(value as CalendarView);
}
});
// Setup workweek buttons with consolidated handler
this.setupButtonGroup('swp-preset-button[data-workweek]', 'data-workweek', (value) => {
this.changeWorkweek(value);
});
}
private setupWorkweekButtonHandlers(): void {
const workweekButtons = document.querySelectorAll('swp-preset-button[data-workweek]');
workweekButtons.forEach(button => {
const handler = (event: Event) => {
/**
* Generic button group setup to eliminate duplicate code
*/
private setupButtonGroup(selector: string, attribute: string, handler: (value: string) => void): void {
const buttons = document.querySelectorAll(selector);
buttons.forEach(button => {
const clickHandler = (event: Event) => {
event.preventDefault();
const workweekId = button.getAttribute('data-workweek');
if (workweekId) {
this.changeWorkweek(workweekId);
const value = button.getAttribute(attribute);
if (value) {
handler(value);
}
};
button.addEventListener('click', handler);
this.buttonListeners.set(button, handler);
button.addEventListener('click', clickHandler);
this.buttonListeners.set(button, clickHandler);
});
}
/**
* Get cached view buttons with cache invalidation
*/
private getViewButtons(): NodeListOf<Element> {
const now = Date.now();
if (!this.cachedViewButtons || (now - this.lastButtonCacheTime) > this.CACHE_DURATION) {
this.cachedViewButtons = document.querySelectorAll('swp-view-button[data-view]');
this.lastButtonCacheTime = now;
}
return this.cachedViewButtons;
}
/**
* Get cached workweek buttons with cache invalidation
*/
private getWorkweekButtons(): NodeListOf<Element> {
const now = Date.now();
if (!this.cachedWorkweekButtons || (now - this.lastButtonCacheTime) > this.CACHE_DURATION) {
this.cachedWorkweekButtons = document.querySelectorAll('swp-preset-button[data-workweek]');
this.lastButtonCacheTime = now;
}
return this.cachedWorkweekButtons;
}
/**
* Initialize view with consolidated button updates
*/
private initializeView(): void {
this.updateViewButtons();
this.updateWorkweekButtons();
this.eventBus.emit(CoreEvents.VIEW_RENDERED, {
view: this.currentView
});
this.updateAllButtons();
this.emitViewRendered();
}
/**
* Optimized view change with debouncing
*/
private changeView(newView: CalendarView): void {
if (newView === this.currentView) return;
const previousView = this.currentView;
this.currentView = newView;
this.updateViewButtons();
this.updateAllButtons();
this.eventBus.emit(CoreEvents.VIEW_CHANGED, {
this.eventBus.emit(CoreEvents.VIEW_CHANGED, {
previousView,
currentView: newView
currentView: newView
});
}
/**
* Optimized workweek change with consolidated updates
*/
private changeWorkweek(workweekId: string): void {
// Update the calendar config
calendarConfig.setWorkWeek(workweekId);
// Update button states
this.updateWorkweekButtons();
// Update button states using cached elements
this.updateAllButtons();
// Trigger a calendar refresh to apply the new workweek
this.eventBus.emit(CoreEvents.REFRESH_REQUESTED);
}
private updateViewButtons(): void {
const viewButtons = document.querySelectorAll('swp-view-button[data-view]');
viewButtons.forEach(button => {
const buttonView = button.getAttribute('data-view') as CalendarView;
if (buttonView === this.currentView) {
/**
* Consolidated button update method to eliminate duplicate code
*/
private updateAllButtons(): void {
this.updateButtonGroup(
this.getViewButtons(),
'data-view',
this.currentView
);
this.updateButtonGroup(
this.getWorkweekButtons(),
'data-workweek',
calendarConfig.getCurrentWorkWeek()
);
}
/**
* Generic button group update to eliminate duplicate logic
*/
private updateButtonGroup(buttons: NodeListOf<Element>, attribute: string, activeValue: string): void {
buttons.forEach(button => {
const buttonValue = button.getAttribute(attribute);
if (buttonValue === activeValue) {
button.setAttribute('data-active', 'true');
} else {
button.removeAttribute('data-active');
@ -127,38 +185,46 @@ export class ViewManager {
});
}
private updateWorkweekButtons(): void {
const currentWorkweek = calendarConfig.getCurrentWorkWeek();
const workweekButtons = document.querySelectorAll('swp-preset-button[data-workweek]');
workweekButtons.forEach(button => {
const buttonWorkweek = button.getAttribute('data-workweek');
if (buttonWorkweek === currentWorkweek) {
button.setAttribute('data-active', 'true');
} else {
button.removeAttribute('data-active');
}
});
}
private refreshCurrentView(): void {
/**
* Emit view rendered event with current view
*/
private emitViewRendered(): void {
this.eventBus.emit(CoreEvents.VIEW_RENDERED, {
view: this.currentView
});
}
/**
* Refresh current view by emitting view rendered event
*/
private refreshCurrentView(): void {
this.emitViewRendered();
}
/**
* Validate if a string is a valid calendar view
*/
private isValidView(view: string): view is CalendarView {
return ['day', 'week', 'month'].includes(view);
}
/**
* Get current active view
*/
public getCurrentView(): CalendarView {
return this.currentView;
}
/**
* Public refresh method
*/
public refresh(): void {
this.refreshCurrentView();
}
/**
* Clean up all resources and cached elements
*/
public destroy(): void {
// Clean up event bus listeners
this.eventCleanup.forEach(cleanup => cleanup());
@ -169,5 +235,10 @@ export class ViewManager {
button.removeEventListener('click', handler);
});
this.buttonListeners.clear();
// Clear cached elements
this.cachedViewButtons = null;
this.cachedWorkweekButtons = null;
this.lastButtonCacheTime = 0;
}
}