Refactors and optimizes core calendar managers

Streamlines several core managers by removing unnecessary complexity, caching, and redundant methods

Key improvements:
- Simplified event and view management logic
- Removed unnecessary caching mechanisms
- Reduced method complexity in managers
- Improved code readability and performance
This commit is contained in:
Janus C. H. Knudsen 2025-11-01 21:07:07 +01:00
parent 1ae4f00f2b
commit b6ab1ff50e
6 changed files with 193 additions and 327 deletions

View file

@ -1,6 +1,135 @@
/** /**
* DragDropManager - Optimized drag and drop with consolidated position calculations * DragDropManager - Advanced drag-and-drop system with smooth animations and event type conversion
* Reduces redundant DOM queries and improves performance through caching *
* ARCHITECTURE OVERVIEW:
* =====================
* DragDropManager provides a sophisticated drag-and-drop system for calendar events that supports:
* - Smooth animated dragging with requestAnimationFrame
* - Automatic event type conversion (timed events all-day events)
* - Scroll compensation during edge scrolling
* - Grid snapping for precise event placement
* - Column detection and change tracking
*
* KEY FEATURES:
* =============
* 1. DRAG DETECTION
* - Movement threshold (5px) to distinguish clicks from drags
* - Immediate visual feedback with cloned element
* - Mouse offset tracking for natural drag feel
*
* 2. SMOOTH ANIMATION
* - Uses requestAnimationFrame for 60fps animations
* - Interpolated movement (30% per frame) for smooth transitions
* - Continuous drag:move events for real-time updates
*
* 3. EVENT TYPE CONVERSION
* - Timed All-day: When dragging into calendar header
* - All-day Timed: When dragging into day columns
* - Automatic clone replacement with appropriate element type
*
* 4. SCROLL COMPENSATION
* - Tracks scroll delta during edge-scrolling
* - Compensates dragged element position during scroll
* - Prevents visual "jumping" when scrolling while dragging
*
* 5. GRID SNAPPING
* - Snaps to time grid on mouse up
* - Uses PositionUtils for consistent positioning
* - Accounts for mouse offset within event
*
* STATE MANAGEMENT:
* =================
* Mouse Tracking:
* - mouseDownPosition: Initial click position
* - currentMousePosition: Latest mouse position
* - mouseOffset: Click offset within event (for natural dragging)
*
* Drag State:
* - originalElement: Source event being dragged
* - draggedClone: Animated clone following mouse
* - currentColumn: Column mouse is currently over
* - previousColumn: Last column (for detecting changes)
* - isDragStarted: Whether drag threshold exceeded
*
* Scroll State:
* - scrollDeltaY: Accumulated scroll offset during drag
* - lastScrollTop: Previous scroll position
* - isScrollCompensating: Whether edge-scroll is active
*
* Animation State:
* - dragAnimationId: requestAnimationFrame ID
* - targetY: Desired position for smooth interpolation
* - currentY: Current interpolated position
*
* EVENT FLOW:
* ===========
* 1. Mouse Down (handleMouseDown)
* Store originalElement and mouse offset
* Wait for movement
*
* 2. Mouse Move (handleMouseMove)
* Check movement threshold
* Initialize drag if threshold exceeded (initializeDrag)
* Create clone
* Emit drag:start
* Start animation loop
* Continue drag (continueDrag)
* Calculate target position with scroll compensation
* Update animation target
* Detect column changes (detectColumnChange)
* Emit drag:column-change
*
* 3. Animation Loop (animateDrag)
* Interpolate currentY toward targetY
* Emit drag:move on each frame
* Schedule next frame until target reached
*
* 4. Event Type Conversion
* Entering header (handleHeaderMouseEnter)
* Emit drag:mouseenter-header
* AllDayManager creates all-day clone
* Entering column (handleColumnMouseEnter)
* Emit drag:mouseenter-column
* EventRenderingService creates timed clone
*
* 5. Mouse Up (handleMouseUp)
* Stop animation
* Snap to grid
* Detect drop target (header or column)
* Emit drag:end with final position
* Cleanup drag state
*
* SCROLL COMPENSATION SYSTEM:
* ===========================
* Problem: When EdgeScrollManager scrolls the grid during drag, the dragged element
* can appear to "jump" because the mouse position stays the same but the
* coordinate system (scrollable content) has moved.
*
* Solution: Track cumulative scroll delta and add it to mouse position calculations
*
* Flow:
* 1. EdgeScrollManager starts scrolling emit edgescroll:started
* 2. DragDropManager sets isScrollCompensating = true
* 3. On each scroll event:
* Calculate scrollDelta = currentScrollTop - lastScrollTop
* Accumulate into scrollDeltaY
* Call continueDrag with adjusted position
* 4. continueDrag adds scrollDeltaY to mouse Y coordinate
* 5. On event conversion, reset scrollDeltaY (new clone, new coordinate system)
*
* PERFORMANCE OPTIMIZATIONS:
* ==========================
* - Uses ColumnDetectionUtils cache for fast column lookups
* - Single requestAnimationFrame loop (not per-mousemove)
* - Interpolated animation reduces update frequency
* - Passive scroll listeners
* - Event delegation for header/column detection
*
* USAGE:
* ======
* const dragDropManager = new DragDropManager(eventBus, positionUtils);
* // Automatically attaches event listeners and manages drag lifecycle
* // Other managers listen to drag:start, drag:move, drag:end, etc.
*/ */
import { IEventBus } from '../types/CalendarTypes'; import { IEventBus } from '../types/CalendarTypes';
@ -122,24 +251,19 @@ export class DragDropManager {
if (this.scrollableContent) { if (this.scrollableContent) {
this.lastScrollTop = this.scrollableContent.scrollTop; this.lastScrollTop = this.scrollableContent.scrollTop;
} }
console.log('🎬 DragDropManager: Edge-scroll started');
}); });
this.eventBus.on('edgescroll:stopped', () => { this.eventBus.on('edgescroll:stopped', () => {
this.isScrollCompensating = false; this.isScrollCompensating = false;
console.log('🛑 DragDropManager: Edge-scroll stopped');
}); });
// Reset scrollDeltaY when event converts (new clone created) // Reset scrollDeltaY when event converts (new clone created)
this.eventBus.on('drag:mouseenter-header', () => { this.eventBus.on('drag:mouseenter-header', () => {
console.log('🔄 DragDropManager: Event converting to all-day - resetting scrollDeltaY');
this.scrollDeltaY = 0; this.scrollDeltaY = 0;
this.lastScrollTop = 0; this.lastScrollTop = 0;
}); });
this.eventBus.on('drag:mouseenter-column', () => { this.eventBus.on('drag:mouseenter-column', () => {
console.log('🔄 DragDropManager: Event converting to timed - resetting scrollDeltaY');
this.scrollDeltaY = 0; this.scrollDeltaY = 0;
this.lastScrollTop = 0; this.lastScrollTop = 0;
}); });
@ -340,8 +464,6 @@ export class DragDropManager {
target: dropTarget target: dropTarget
}; };
console.log('DragEndEventPayload', dragEndPayload);
this.eventBus.emit('drag:end', dragEndPayload); this.eventBus.emit('drag:end', dragEndPayload);
this.cleanupDragState(); this.cleanupDragState();
@ -361,7 +483,6 @@ export class DragDropManager {
const allClones = document.querySelectorAll('[data-event-id^="clone"]'); const allClones = document.querySelectorAll('[data-event-id^="clone"]');
if (allClones.length > 0) { if (allClones.length > 0) {
console.log(`🧹 DragDropManager: Removing ${allClones.length} clone(s)`);
allClones.forEach(clone => clone.remove()); allClones.forEach(clone => clone.remove());
} }
} }
@ -373,8 +494,6 @@ export class DragDropManager {
private cancelDrag(): void { private cancelDrag(): void {
if (!this.originalElement || !this.draggedClone) return; if (!this.originalElement || !this.draggedClone) return;
console.log('🚫 DragDropManager: Cancelling drag - mouse left grid container');
// Get current clone position // Get current clone position
const cloneRect = this.draggedClone.getBoundingClientRect(); const cloneRect = this.draggedClone.getBoundingClientRect();
@ -486,13 +605,6 @@ export class DragDropManager {
// Kald continueDrag med nuværende mus position // Kald continueDrag med nuværende mus position
this.continueDrag(this.currentMousePosition); this.continueDrag(this.currentMousePosition);
console.log('📜 DragDropManager: Scroll compensation', {
currentScrollTop,
lastScrollTop: this.lastScrollTop - scrollDelta,
scrollDelta,
scrollDeltaY: this.scrollDeltaY
});
} }
/** /**
@ -564,7 +676,6 @@ export class DragDropManager {
this.dragAnimationId === null; this.dragAnimationId === null;
} }
}; };
console.log('DragMouseEnterHeaderEventPayload', dragMouseEnterPayload);
this.eventBus.emit('drag:mouseenter-header', dragMouseEnterPayload); this.eventBus.emit('drag:mouseenter-header', dragMouseEnterPayload);
} }
} }
@ -578,13 +689,10 @@ export class DragDropManager {
return; return;
} }
console.log('🎯 DragDropManager: Mouse entered day column');
const position: MousePosition = { x: event.clientX, y: event.clientY }; const position: MousePosition = { x: event.clientX, y: event.clientY };
const targetColumn = ColumnDetectionUtils.getColumnBounds(position); const targetColumn = ColumnDetectionUtils.getColumnBounds(position);
if (!targetColumn) { if (!targetColumn) {
console.warn("No column detected when entering day column");
return; return;
} }
@ -619,13 +727,10 @@ export class DragDropManager {
return; return;
} }
console.log('🚪 DragDropManager: Mouse left header');
const position: MousePosition = { x: event.clientX, y: event.clientY }; const position: MousePosition = { x: event.clientX, y: event.clientY };
const targetColumn = ColumnDetectionUtils.getColumnBounds(position); const targetColumn = ColumnDetectionUtils.getColumnBounds(position);
if (!targetColumn) { if (!targetColumn) {
console.warn("No column detected when leaving header");
return; return;
} }

View file

@ -15,15 +15,13 @@ interface RawEventData {
} }
/** /**
* EventManager - Optimized event lifecycle and CRUD operations * EventManager - Event lifecycle and CRUD operations
* Handles data loading with improved performance and caching * Handles data loading and event management
*/ */
export class EventManager { export class EventManager {
private events: CalendarEvent[] = []; private events: CalendarEvent[] = [];
private rawData: RawEventData[] | null = null; private rawData: RawEventData[] | null = null;
private eventCache = new Map<string, CalendarEvent[]>(); // Cache for period queries
private lastCacheKey: string = '';
private dateService: DateService; private dateService: DateService;
private config: CalendarConfig; private config: CalendarConfig;
@ -37,12 +35,11 @@ export class EventManager {
} }
/** /**
* Optimized data loading with better error handling * Load event data from JSON file
*/ */
public async loadData(): Promise<void> { public async loadData(): Promise<void> {
try { try {
await this.loadMockData(); await this.loadMockData();
this.clearCache(); // Clear cache when new data is loaded
} catch (error) { } catch (error) {
console.error('Failed to load event data:', error); console.error('Failed to load event data:', error);
this.events = []; this.events = [];
@ -69,7 +66,7 @@ export class EventManager {
} }
/** /**
* Optimized data processing with better type safety * Process raw event data and convert to CalendarEvent objects
*/ */
private processCalendarData(data: RawEventData[]): CalendarEvent[] { private processCalendarData(data: RawEventData[]): CalendarEvent[] {
return data.map((event): CalendarEvent => ({ return data.map((event): CalendarEvent => ({
@ -82,14 +79,6 @@ export class EventManager {
})); }));
} }
/**
* Clear event cache when data changes
*/
private clearCache(): void {
this.eventCache.clear();
this.lastCacheKey = '';
}
/** /**
* Get events with optional copying for performance * Get events with optional copying for performance
*/ */
@ -162,32 +151,17 @@ export class EventManager {
} }
/** /**
* Optimized events for period with caching and DateService * Get events that overlap with a given time period
*/ */
public getEventsForPeriod(startDate: Date, endDate: Date): CalendarEvent[] { public getEventsForPeriod(startDate: Date, endDate: Date): CalendarEvent[] {
// Create cache key using DateService for consistent formatting // Event overlaps period if it starts before period ends AND ends after period starts
const cacheKey = `${this.dateService.formatISODate(startDate)}_${this.dateService.formatISODate(endDate)}`; return this.events.filter(event => {
// Return cached result if available
if (this.lastCacheKey === cacheKey && this.eventCache.has(cacheKey)) {
return this.eventCache.get(cacheKey)!;
}
// Filter events using optimized date operations
const filteredEvents = this.events.filter(event => {
// Event overlaps period if it starts before period ends AND ends after period starts
return event.start <= endDate && event.end >= startDate; return event.start <= endDate && event.end >= startDate;
}); });
// Cache the result
this.eventCache.set(cacheKey, filteredEvents);
this.lastCacheKey = cacheKey;
return filteredEvents;
} }
/** /**
* Optimized event creation with better ID generation * Create a new event and add it to the calendar
*/ */
public addEvent(event: Omit<CalendarEvent, 'id'>): CalendarEvent { public addEvent(event: Omit<CalendarEvent, 'id'>): CalendarEvent {
const newEvent: CalendarEvent = { const newEvent: CalendarEvent = {
@ -196,7 +170,6 @@ export class EventManager {
}; };
this.events.push(newEvent); this.events.push(newEvent);
this.clearCache(); // Clear cache when data changes
this.eventBus.emit(CoreEvents.EVENT_CREATED, { this.eventBus.emit(CoreEvents.EVENT_CREATED, {
event: newEvent event: newEvent
@ -206,7 +179,7 @@ export class EventManager {
} }
/** /**
* Optimized event update with validation * Update an existing event
*/ */
public updateEvent(id: string, updates: Partial<CalendarEvent>): CalendarEvent | null { public updateEvent(id: string, updates: Partial<CalendarEvent>): CalendarEvent | null {
const eventIndex = this.events.findIndex(event => event.id === id); const eventIndex = this.events.findIndex(event => event.id === id);
@ -215,38 +188,10 @@ export class EventManager {
const updatedEvent = { ...this.events[eventIndex], ...updates }; const updatedEvent = { ...this.events[eventIndex], ...updates };
this.events[eventIndex] = updatedEvent; this.events[eventIndex] = updatedEvent;
this.clearCache(); // Clear cache when data changes
this.eventBus.emit(CoreEvents.EVENT_UPDATED, { this.eventBus.emit(CoreEvents.EVENT_UPDATED, {
event: updatedEvent event: updatedEvent
}); });
return updatedEvent; return updatedEvent;
} }
/**
* Optimized event deletion with better error handling
*/
public deleteEvent(id: string): boolean {
const eventIndex = this.events.findIndex(event => event.id === id);
if (eventIndex === -1) return false;
const deletedEvent = this.events[eventIndex];
this.events.splice(eventIndex, 1);
this.clearCache(); // Clear cache when data changes
this.eventBus.emit(CoreEvents.EVENT_DELETED, {
event: deletedEvent
});
return true;
}
/**
* Refresh data by reloading from source
*/
public async refresh(): Promise<void> {
await this.loadData();
}
} }

View file

@ -26,20 +26,6 @@ export class HeaderManager {
this.setupNavigationListener(); this.setupNavigationListener();
} }
/**
* Initialize header with initial date
*/
public initializeHeader(currentDate: Date): void {
this.updateHeader(currentDate);
}
/**
* Get cached calendar header element
*/
private getCalendarHeader(): HTMLElement | null {
return document.querySelector('swp-calendar-header');
}
/** /**
* Setup header drag event listeners - Listen to DragDropManager events * Setup header drag event listeners - Listen to DragDropManager events
*/ */
@ -65,8 +51,6 @@ export class HeaderManager {
originalElement: !!originalElement, originalElement: !!originalElement,
cloneElement: !!cloneElement cloneElement: !!cloneElement
}); });
// Header renderer already injected - ready to use if needed
} }
/** /**
@ -115,7 +99,7 @@ export class HeaderManager {
rendererType: this.headerRenderer.constructor.name rendererType: this.headerRenderer.constructor.name
}); });
const calendarHeader = this.getOrCreateCalendarHeader(); const calendarHeader = document.querySelector('swp-calendar-header') as HTMLElement;
if (!calendarHeader) { if (!calendarHeader) {
console.warn('❌ HeaderManager: No calendar header found!'); console.warn('❌ HeaderManager: No calendar header found!');
return; return;
@ -130,9 +114,7 @@ export class HeaderManager {
config: this.config config: this.config
}; };
console.log('🎨 HeaderManager: Calling renderer.render()', context);
this.headerRenderer.render(calendarHeader, context); this.headerRenderer.render(calendarHeader, context);
console.log('✅ HeaderManager: Renderer completed');
// Setup event listeners on the new content // Setup event listeners on the new content
this.setupHeaderDragListeners(); this.setupHeaderDragListeners();
@ -143,18 +125,4 @@ export class HeaderManager {
}; };
eventBus.emit('header:ready', payload); eventBus.emit('header:ready', payload);
} }
/**
* Get calendar header element - header always exists now
*/
private getOrCreateCalendarHeader(): HTMLElement | null {
const calendarHeader = this.getCalendarHeader();
if (!calendarHeader) {
console.warn('HeaderManager: Calendar header not found - should always exist now!');
return null;
}
return calendarHeader;
}
} }

View file

@ -5,10 +5,6 @@ import { CoreEvents } from '../constants/CoreEvents';
import { NavigationRenderer } from '../renderers/NavigationRenderer'; import { NavigationRenderer } from '../renderers/NavigationRenderer';
import { GridRenderer } from '../renderers/GridRenderer'; import { GridRenderer } from '../renderers/GridRenderer';
/**
* NavigationManager handles calendar navigation (prev/next/today buttons)
* with simplified CSS Grid approach
*/
export class NavigationManager { export class NavigationManager {
private eventBus: IEventBus; private eventBus: IEventBus;
private navigationRenderer: NavigationRenderer; private navigationRenderer: NavigationRenderer;
@ -49,14 +45,6 @@ export class NavigationManager {
} }
private getCalendarContainer(): HTMLElement | null {
return document.querySelector('swp-calendar-container');
}
private getCurrentGrid(): HTMLElement | null {
return document.querySelector('swp-calendar-container swp-grid-container:not([data-prerendered])');
}
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, () => {
@ -215,8 +203,9 @@ 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 = this.getCalendarContainer();
const currentGrid = this.getCurrentGrid(); const container = document.querySelector('swp-calendar-container') as HTMLElement;
const currentGrid = document.querySelector('swp-calendar-container swp-grid-container:not([data-prerendered])') as HTMLElement;
if (!container || !currentGrid) { if (!container || !currentGrid) {
return; return;
@ -240,10 +229,10 @@ export class NavigationManager {
// Clear any existing transforms before animation // Clear any existing transforms before animation
newGrid.style.transform = ''; newGrid.style.transform = '';
(currentGrid as HTMLElement).style.transform = ''; currentGrid.style.transform = '';
// Animate transition using Web Animations API // Animate transition using Web Animations API
const slideOutAnimation = (currentGrid as HTMLElement).animate([ const slideOutAnimation = currentGrid.animate([
{ transform: 'translateX(0)', opacity: '1' }, { transform: 'translateX(0)', opacity: '1' },
{ transform: direction === 'next' ? 'translateX(-100%)' : 'translateX(100%)', opacity: '0.5' } { transform: direction === 'next' ? 'translateX(-100%)' : 'translateX(100%)', opacity: '0.5' }
], { ], {
@ -308,35 +297,4 @@ export class NavigationManager {
weekEnd weekEnd
}); });
} }
/**
* Get current week start date
*/
getCurrentWeek(): Date {
return new Date(this.currentWeek);
}
/**
* Get target week (where navigation is heading)
*/
getTargetWeek(): Date {
return new Date(this.targetWeek);
}
/**
* Check if navigation animation is in progress
*/
isAnimating(): boolean {
return this.animationQueue > 0;
}
/**
* Force navigation to specific week without animation
*/
setWeek(weekStart: Date): void {
this.currentWeek = new Date(weekStart);
this.targetWeek = new Date(weekStart);
this.updateWeekInfo();
}
} }

View file

@ -1,77 +1,49 @@
import { EventBus } from '../core/EventBus';
import { CalendarView, IEventBus } from '../types/CalendarTypes'; import { CalendarView, IEventBus } from '../types/CalendarTypes';
import { CalendarConfig } from '../core/CalendarConfig'; import { CalendarConfig } from '../core/CalendarConfig';
import { CoreEvents } from '../constants/CoreEvents'; import { CoreEvents } from '../constants/CoreEvents';
/**
* ViewManager - Optimized view switching with consolidated event handling
* Reduces redundant DOM queries and event listener setups
*/
export class ViewManager { export class ViewManager {
private eventBus: IEventBus; private eventBus: IEventBus;
private config: CalendarConfig; private config: CalendarConfig;
private currentView: CalendarView = 'week'; private currentView: CalendarView = 'week';
private buttonListeners: Map<Element, EventListener> = new Map(); 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, config: CalendarConfig) { constructor(eventBus: IEventBus, config: CalendarConfig) {
this.eventBus = eventBus; this.eventBus = eventBus;
this.config = config; this.config = config;
this.setupEventListeners(); this.setupEventListeners();
} }
/**
* Consolidated event listener setup with better organization
*/
private setupEventListeners(): void { private setupEventListeners(): void {
// Event bus listeners
this.setupEventBusListeners(); this.setupEventBusListeners();
// DOM button handlers with consolidated logic
this.setupButtonHandlers(); this.setupButtonHandlers();
} }
/**
* Setup event bus listeners with proper cleanup tracking
*/
private setupEventBusListeners(): void { private setupEventBusListeners(): void {
this.eventBus.on(CoreEvents.INITIALIZED, () => { this.eventBus.on(CoreEvents.INITIALIZED, () => {
this.initializeView(); this.initializeView();
}); });
// Remove redundant VIEW_CHANGED listener that causes circular calls
// changeView is called directly from button handlers
this.eventBus.on(CoreEvents.DATE_CHANGED, () => { this.eventBus.on(CoreEvents.DATE_CHANGED, () => {
this.refreshCurrentView(); this.refreshCurrentView();
}); });
} }
/**
* Consolidated button handler setup with shared logic
*/
private setupButtonHandlers(): void { private setupButtonHandlers(): void {
// Setup view buttons with consolidated handler
this.setupButtonGroup('swp-view-button[data-view]', 'data-view', (value) => { this.setupButtonGroup('swp-view-button[data-view]', 'data-view', (value) => {
if (this.isValidView(value)) { if (this.isValidView(value)) {
this.changeView(value as CalendarView); this.changeView(value as CalendarView);
} }
}); });
// Setup workweek buttons with consolidated handler
this.setupButtonGroup('swp-preset-button[data-workweek]', 'data-workweek', (value) => { this.setupButtonGroup('swp-preset-button[data-workweek]', 'data-workweek', (value) => {
this.changeWorkweek(value); this.changeWorkweek(value);
}); });
} }
/**
* Generic button group setup to eliminate duplicate code
*/
private setupButtonGroup(selector: string, attribute: string, handler: (value: string) => void): void { private setupButtonGroup(selector: string, attribute: string, handler: (value: string) => void): void {
const buttons = document.querySelectorAll(selector); const buttons = document.querySelectorAll(selector);
buttons.forEach(button => { buttons.forEach(button => {
@ -87,42 +59,24 @@ export class ViewManager {
}); });
} }
/**
* Get cached view buttons with cache invalidation
*/
private getViewButtons(): NodeListOf<Element> { private getViewButtons(): NodeListOf<Element> {
const now = Date.now();
if (!this.cachedViewButtons || (now - this.lastButtonCacheTime) > this.CACHE_DURATION) { return document.querySelectorAll('swp-view-button[data-view]');
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> { private getWorkweekButtons(): NodeListOf<Element> {
const now = Date.now();
if (!this.cachedWorkweekButtons || (now - this.lastButtonCacheTime) > this.CACHE_DURATION) { return document.querySelectorAll('swp-preset-button[data-workweek]');
this.cachedWorkweekButtons = document.querySelectorAll('swp-preset-button[data-workweek]');
this.lastButtonCacheTime = now;
}
return this.cachedWorkweekButtons;
} }
/**
* Initialize view with consolidated button updates
*/
private initializeView(): void { private initializeView(): void {
this.updateAllButtons(); this.updateAllButtons();
this.emitViewRendered(); this.emitViewRendered();
} }
/**
* Optimized view change with debouncing
*/
private changeView(newView: CalendarView): void { private changeView(newView: CalendarView): void {
if (newView === this.currentView) return; if (newView === this.currentView) return;
@ -137,27 +91,18 @@ export class ViewManager {
}); });
} }
/**
* Optimized workweek change with consolidated updates
*/
private changeWorkweek(workweekId: string): void { private changeWorkweek(workweekId: string): void {
// Update the calendar config (does not emit events)
this.config.setWorkWeek(workweekId); this.config.setWorkWeek(workweekId);
// Update button states using cached elements
this.updateAllButtons(); this.updateAllButtons();
// Emit workweek change event with full payload
const settings = this.config.getWorkWeekSettings(); const settings = this.config.getWorkWeekSettings();
this.eventBus.emit(CoreEvents.WORKWEEK_CHANGED, { this.eventBus.emit(CoreEvents.WORKWEEK_CHANGED, {
workWeekId: workweekId, workWeekId: workweekId,
settings: settings settings: settings
}); });
} }
/**
* Consolidated button update method to eliminate duplicate code
*/
private updateAllButtons(): void { private updateAllButtons(): void {
this.updateButtonGroup( this.updateButtonGroup(
this.getViewButtons(), this.getViewButtons(),
@ -172,9 +117,6 @@ export class ViewManager {
); );
} }
/**
* Generic button group update to eliminate duplicate logic
*/
private updateButtonGroup(buttons: NodeListOf<Element>, attribute: string, activeValue: string): void { private updateButtonGroup(buttons: NodeListOf<Element>, attribute: string, activeValue: string): void {
buttons.forEach(button => { buttons.forEach(button => {
const buttonValue = button.getAttribute(attribute); const buttonValue = button.getAttribute(attribute);
@ -186,41 +128,19 @@ export class ViewManager {
}); });
} }
/**
* Emit view rendered event with current view
*/
private emitViewRendered(): void { private emitViewRendered(): void {
this.eventBus.emit(CoreEvents.VIEW_RENDERED, { this.eventBus.emit(CoreEvents.VIEW_RENDERED, {
view: this.currentView view: this.currentView
}); });
} }
/**
* Refresh current view by emitting view rendered event
*/
private refreshCurrentView(): void { private refreshCurrentView(): void {
this.emitViewRendered(); this.emitViewRendered();
} }
/**
* Validate if a string is a valid calendar view
*/
private isValidView(view: string): view is CalendarView { private isValidView(view: string): view is CalendarView {
return ['day', 'week', 'month'].includes(view); return ['day', 'week', 'month'].includes(view);
} }
/**
* Get current active view
*/
public getCurrentView(): CalendarView {
return this.currentView;
}
/**
* Public refresh method
*/
public refresh(): void {
this.refreshCurrentView();
}
} }

View file

@ -6,45 +6,16 @@ import { EventRenderingService } from './EventRendererManager';
* NavigationRenderer - Handles DOM rendering for navigation containers * NavigationRenderer - Handles DOM rendering for navigation containers
* Separated from NavigationManager to follow Single Responsibility Principle * Separated from NavigationManager to follow Single Responsibility Principle
*/ */
export class NavigationRenderer { export class NavigationRenderer {
private eventBus: IEventBus; private eventBus: IEventBus;
// Cached DOM elements to avoid redundant queries
private cachedWeekNumberElement: HTMLElement | null = null;
private cachedDateRangeElement: HTMLElement | null = null;
constructor(eventBus: IEventBus, eventRenderer: EventRenderingService) { constructor(eventBus: IEventBus, eventRenderer: EventRenderingService) {
this.eventBus = eventBus; this.eventBus = eventBus;
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
@ -57,12 +28,11 @@ export class NavigationRenderer {
}); });
} }
/**
* 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 = this.getWeekNumberElement();
const dateRangeElement = this.getDateRangeElement(); const weekNumberElement = document.querySelector('swp-week-number');
const dateRangeElement = document.querySelector('swp-date-range');
if (weekNumberElement) { if (weekNumberElement) {
weekNumberElement.textContent = `Week ${weekNumber}`; weekNumberElement.textContent = `Week ${weekNumber}`;