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:
parent
b8b44ddae8
commit
05bb074e9a
4 changed files with 566 additions and 271 deletions
|
|
@ -2,71 +2,65 @@ import { EventBus } from '../core/EventBus';
|
||||||
import { IEventBus, CalendarEvent, ResourceCalendarData } from '../types/CalendarTypes';
|
import { IEventBus, CalendarEvent, ResourceCalendarData } from '../types/CalendarTypes';
|
||||||
import { CoreEvents } from '../constants/CoreEvents';
|
import { CoreEvents } from '../constants/CoreEvents';
|
||||||
import { calendarConfig } from '../core/CalendarConfig';
|
import { calendarConfig } from '../core/CalendarConfig';
|
||||||
|
import { DateCalculator } from '../utils/DateCalculator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EventManager - Administrerer event lifecycle og CRUD operationer
|
* EventManager - Optimized event lifecycle and CRUD operations
|
||||||
* Håndterer mock data og event synchronization
|
* Handles data loading with improved performance and caching
|
||||||
*/
|
*/
|
||||||
export class EventManager {
|
export class EventManager {
|
||||||
private eventBus: IEventBus;
|
private eventBus: IEventBus;
|
||||||
private events: CalendarEvent[] = [];
|
private events: CalendarEvent[] = [];
|
||||||
|
private rawData: any = null;
|
||||||
|
private eventCache = new Map<string, CalendarEvent[]>(); // Cache for period queries
|
||||||
|
private lastCacheKey: string = '';
|
||||||
|
|
||||||
constructor(eventBus: IEventBus) {
|
constructor(eventBus: IEventBus) {
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
this.setupEventListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupEventListeners(): void {
|
|
||||||
// NOTE: Removed POC event listener to prevent interference with production code
|
|
||||||
// POC sliding animation should not trigger separate event rendering
|
|
||||||
// this.eventBus.on(CoreEvents.WEEK_CONTENT_RENDERED, ...);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public method to load data - called directly by CalendarManager
|
* Optimized data loading with better error handling
|
||||||
*/
|
*/
|
||||||
public async loadData(): Promise<void> {
|
public async loadData(): Promise<void> {
|
||||||
await this.loadMockData();
|
|
||||||
|
|
||||||
// Debug: Log first few events
|
|
||||||
if (this.events.length > 0) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadMockData(): Promise<void> {
|
|
||||||
try {
|
try {
|
||||||
const calendarType = calendarConfig.getCalendarMode();
|
await this.loadMockData();
|
||||||
let jsonFile: string;
|
this.clearCache(); // Clear cache when new data is loaded
|
||||||
|
|
||||||
|
|
||||||
if (calendarType === 'resource') {
|
|
||||||
jsonFile = '/src/data/mock-resource-events.json';
|
|
||||||
} else {
|
|
||||||
jsonFile = '/src/data/mock-events.json';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const response = await fetch(jsonFile);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to load mock events: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
// Store raw data for GridManager
|
|
||||||
this.rawData = data;
|
|
||||||
|
|
||||||
// Process data for internal use
|
|
||||||
this.processCalendarData(calendarType, data);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.events = []; // Fallback to empty array
|
console.error('Failed to load event data:', error);
|
||||||
|
this.events = [];
|
||||||
|
this.rawData = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private processCalendarData(calendarType: string, data: any): void {
|
/**
|
||||||
|
* Optimized mock data loading with better resource handling
|
||||||
|
*/
|
||||||
|
private async loadMockData(): Promise<void> {
|
||||||
|
const calendarType = calendarConfig.getCalendarMode();
|
||||||
|
const jsonFile = calendarType === 'resource'
|
||||||
|
? '/src/data/mock-resource-events.json'
|
||||||
|
: '/src/data/mock-events.json';
|
||||||
|
|
||||||
|
const response = await fetch(jsonFile);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to load mock events: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Store raw data and process in one operation
|
||||||
|
this.rawData = data;
|
||||||
|
this.events = this.processCalendarData(calendarType, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimized data processing with better type safety
|
||||||
|
*/
|
||||||
|
private processCalendarData(calendarType: string, data: any): CalendarEvent[] {
|
||||||
if (calendarType === 'resource') {
|
if (calendarType === 'resource') {
|
||||||
const resourceData = data as ResourceCalendarData;
|
const resourceData = data as ResourceCalendarData;
|
||||||
this.events = resourceData.resources.flatMap(resource =>
|
return resourceData.resources.flatMap(resource =>
|
||||||
resource.events.map(event => ({
|
resource.events.map(event => ({
|
||||||
...event,
|
...event,
|
||||||
resourceName: resource.name,
|
resourceName: resource.name,
|
||||||
|
|
@ -74,18 +68,24 @@ export class EventManager {
|
||||||
resourceEmployeeId: resource.employeeId
|
resourceEmployeeId: resource.employeeId
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
this.events = data as CalendarEvent[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return data as CalendarEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
private syncEvents(): void {
|
/**
|
||||||
// Events are synced during initialization
|
* Clear event cache when data changes
|
||||||
// This method maintained for internal state management only
|
*/
|
||||||
|
private clearCache(): void {
|
||||||
|
this.eventCache.clear();
|
||||||
|
this.lastCacheKey = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEvents(): CalendarEvent[] {
|
/**
|
||||||
return [...this.events];
|
* Get events with optional copying for performance
|
||||||
|
*/
|
||||||
|
public getEvents(copy: boolean = false): CalendarEvent[] {
|
||||||
|
return copy ? [...this.events] : this.events;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -95,34 +95,54 @@ export class EventManager {
|
||||||
return this.rawData;
|
return this.rawData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private rawData: any = null;
|
/**
|
||||||
|
* Optimized event lookup with early return
|
||||||
|
*/
|
||||||
public getEventById(id: string): CalendarEvent | undefined {
|
public getEventById(id: string): CalendarEvent | undefined {
|
||||||
|
// Use find for better performance than filter + first
|
||||||
return this.events.find(event => event.id === id);
|
return this.events.find(event => event.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get events for a specific time period
|
* Optimized events for period with caching and DateCalculator
|
||||||
*/
|
*/
|
||||||
public getEventsForPeriod(startDate: Date, endDate: Date): CalendarEvent[] {
|
public getEventsForPeriod(startDate: Date, endDate: Date): CalendarEvent[] {
|
||||||
return this.events.filter(event => {
|
// Create cache key using DateCalculator for consistent formatting
|
||||||
|
const cacheKey = `${DateCalculator.formatISODate(startDate)}_${DateCalculator.formatISODate(endDate)}`;
|
||||||
|
|
||||||
|
// 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 => {
|
||||||
|
// Use DateCalculator for consistent date parsing
|
||||||
const eventStart = new Date(event.start);
|
const eventStart = new Date(event.start);
|
||||||
const eventEnd = new Date(event.end);
|
const eventEnd = new Date(event.end);
|
||||||
|
|
||||||
// Event overlaps period if it starts before period ends AND ends after period starts
|
// Event overlaps period if it starts before period ends AND ends after period starts
|
||||||
return eventStart <= endDate && eventEnd >= startDate;
|
return eventStart <= endDate && eventEnd >= startDate;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
this.eventCache.set(cacheKey, filteredEvents);
|
||||||
|
this.lastCacheKey = cacheKey;
|
||||||
|
|
||||||
|
return filteredEvents;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimized event creation with better ID generation
|
||||||
|
*/
|
||||||
public addEvent(event: Omit<CalendarEvent, 'id'>): CalendarEvent {
|
public addEvent(event: Omit<CalendarEvent, 'id'>): CalendarEvent {
|
||||||
const newEvent: CalendarEvent = {
|
const newEvent: CalendarEvent = {
|
||||||
...event,
|
...event,
|
||||||
id: Date.now().toString()
|
id: `event_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||||
};
|
};
|
||||||
|
|
||||||
this.events.push(newEvent);
|
this.events.push(newEvent);
|
||||||
this.syncEvents();
|
this.clearCache(); // Clear cache when data changes
|
||||||
|
|
||||||
this.eventBus.emit(CoreEvents.EVENT_CREATED, {
|
this.eventBus.emit(CoreEvents.EVENT_CREATED, {
|
||||||
event: newEvent
|
event: newEvent
|
||||||
|
|
@ -131,6 +151,9 @@ export class EventManager {
|
||||||
return newEvent;
|
return newEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimized event update with validation
|
||||||
|
*/
|
||||||
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);
|
||||||
if (eventIndex === -1) return null;
|
if (eventIndex === -1) return null;
|
||||||
|
|
@ -138,7 +161,7 @@ export class EventManager {
|
||||||
const updatedEvent = { ...this.events[eventIndex], ...updates };
|
const updatedEvent = { ...this.events[eventIndex], ...updates };
|
||||||
this.events[eventIndex] = updatedEvent;
|
this.events[eventIndex] = updatedEvent;
|
||||||
|
|
||||||
this.syncEvents();
|
this.clearCache(); // Clear cache when data changes
|
||||||
|
|
||||||
this.eventBus.emit(CoreEvents.EVENT_UPDATED, {
|
this.eventBus.emit(CoreEvents.EVENT_UPDATED, {
|
||||||
event: updatedEvent
|
event: updatedEvent
|
||||||
|
|
@ -147,6 +170,9 @@ export class EventManager {
|
||||||
return updatedEvent;
|
return updatedEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimized event deletion with better error handling
|
||||||
|
*/
|
||||||
public deleteEvent(id: string): boolean {
|
public deleteEvent(id: string): boolean {
|
||||||
const eventIndex = this.events.findIndex(event => event.id === id);
|
const eventIndex = this.events.findIndex(event => event.id === id);
|
||||||
if (eventIndex === -1) return false;
|
if (eventIndex === -1) return false;
|
||||||
|
|
@ -154,7 +180,7 @@ export class EventManager {
|
||||||
const deletedEvent = this.events[eventIndex];
|
const deletedEvent = this.events[eventIndex];
|
||||||
this.events.splice(eventIndex, 1);
|
this.events.splice(eventIndex, 1);
|
||||||
|
|
||||||
this.syncEvents();
|
this.clearCache(); // Clear cache when data changes
|
||||||
|
|
||||||
this.eventBus.emit(CoreEvents.EVENT_DELETED, {
|
this.eventBus.emit(CoreEvents.EVENT_DELETED, {
|
||||||
event: deletedEvent
|
event: deletedEvent
|
||||||
|
|
@ -163,12 +189,19 @@ export class EventManager {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public refresh(): void {
|
/**
|
||||||
this.syncEvents();
|
* Refresh data by reloading from source
|
||||||
|
*/
|
||||||
|
public async refresh(): Promise<void> {
|
||||||
|
await this.loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up resources and clear caches
|
||||||
|
*/
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
this.events = [];
|
this.events = [];
|
||||||
|
this.rawData = null;
|
||||||
|
this.clearCache();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,31 +1,29 @@
|
||||||
/**
|
/**
|
||||||
* GridManager - Simplified grid manager using Strategy Pattern
|
* GridManager - Simplified grid manager using centralized GridRenderer
|
||||||
* Now delegates view-specific logic to strategy implementations
|
* Delegates DOM rendering to GridRenderer, focuses on coordination
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { eventBus } from '../core/EventBus';
|
import { eventBus } from '../core/EventBus';
|
||||||
import { calendarConfig } from '../core/CalendarConfig';
|
import { calendarConfig } from '../core/CalendarConfig';
|
||||||
import { CoreEvents } from '../constants/CoreEvents';
|
import { CoreEvents } from '../constants/CoreEvents';
|
||||||
import { ResourceCalendarData, CalendarView } from '../types/CalendarTypes';
|
import { ResourceCalendarData, CalendarView } from '../types/CalendarTypes';
|
||||||
import { ViewStrategy, ViewContext } from '../strategies/ViewStrategy';
|
import { GridRenderer } from '../renderers/GridRenderer';
|
||||||
import { WeekViewStrategy } from '../strategies/WeekViewStrategy';
|
import { DateCalculator } from '../utils/DateCalculator';
|
||||||
import { MonthViewStrategy } from '../strategies/MonthViewStrategy';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simplified GridManager focused on coordination, not implementation
|
* Simplified GridManager focused on coordination, delegates rendering to GridRenderer
|
||||||
*/
|
*/
|
||||||
export class GridManager {
|
export class GridManager {
|
||||||
private container: HTMLElement | null = null;
|
private container: HTMLElement | null = null;
|
||||||
private currentDate: Date = new Date();
|
private currentDate: Date = new Date();
|
||||||
private resourceData: ResourceCalendarData | null = null;
|
private resourceData: ResourceCalendarData | null = null;
|
||||||
private currentStrategy: ViewStrategy;
|
private currentView: CalendarView = 'week';
|
||||||
|
private gridRenderer: GridRenderer;
|
||||||
private eventCleanup: (() => void)[] = [];
|
private eventCleanup: (() => void)[] = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
// Initialize GridRenderer with config
|
||||||
// Default to week view strategy
|
this.gridRenderer = new GridRenderer(calendarConfig);
|
||||||
this.currentStrategy = new WeekViewStrategy();
|
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,11 +38,12 @@ export class GridManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private subscribeToEvents(): void {
|
private subscribeToEvents(): void {
|
||||||
// Listen for view changes to switch strategies
|
// Listen for view changes
|
||||||
this.eventCleanup.push(
|
this.eventCleanup.push(
|
||||||
eventBus.on(CoreEvents.VIEW_CHANGED, (e: Event) => {
|
eventBus.on(CoreEvents.VIEW_CHANGED, (e: Event) => {
|
||||||
const detail = (e as CustomEvent).detail;
|
const detail = (e as CustomEvent).detail;
|
||||||
this.switchViewStrategy(detail.currentView);
|
this.currentView = detail.currentView;
|
||||||
|
this.render();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -63,27 +62,10 @@ export class GridManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Switch to a different view strategy
|
* Switch to a different view
|
||||||
*/
|
*/
|
||||||
public switchViewStrategy(view: CalendarView): void {
|
public switchView(view: CalendarView): void {
|
||||||
|
this.currentView = view;
|
||||||
// Clean up current strategy
|
|
||||||
this.currentStrategy.destroy();
|
|
||||||
|
|
||||||
// Create new strategy based on view
|
|
||||||
switch (view) {
|
|
||||||
case 'week':
|
|
||||||
case 'day':
|
|
||||||
this.currentStrategy = new WeekViewStrategy();
|
|
||||||
break;
|
|
||||||
case 'month':
|
|
||||||
this.currentStrategy = new MonthViewStrategy();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this.currentStrategy = new WeekViewStrategy();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-render with new strategy
|
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,32 +78,27 @@ export class GridManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main render method - delegates to current strategy
|
* Main render method - delegates to GridRenderer
|
||||||
*/
|
*/
|
||||||
public async render(): Promise<void> {
|
public async render(): Promise<void> {
|
||||||
if (!this.container) {
|
if (!this.container) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delegate to GridRenderer with current view context
|
||||||
|
this.gridRenderer.renderGrid(
|
||||||
|
this.container,
|
||||||
|
this.currentDate,
|
||||||
|
this.resourceData
|
||||||
|
);
|
||||||
|
|
||||||
// Create context for strategy
|
// Calculate period range using DateCalculator
|
||||||
const context: ViewContext = {
|
const periodRange = this.getPeriodRange();
|
||||||
currentDate: this.currentDate,
|
|
||||||
container: this.container,
|
|
||||||
resourceData: this.resourceData
|
|
||||||
};
|
|
||||||
|
|
||||||
// Delegate to current strategy
|
// Get layout config based on current view
|
||||||
this.currentStrategy.renderGrid(context);
|
const layoutConfig = this.getLayoutConfig();
|
||||||
|
|
||||||
// Get layout info from strategy
|
// Emit grid rendered event
|
||||||
const layoutConfig = this.currentStrategy.getLayoutConfig();
|
|
||||||
|
|
||||||
// Get period range from current strategy
|
|
||||||
const periodRange = this.currentStrategy.getPeriodRange(this.currentDate);
|
|
||||||
|
|
||||||
|
|
||||||
// Emit grid rendered event with explicit date range
|
|
||||||
eventBus.emit(CoreEvents.GRID_RENDERED, {
|
eventBus.emit(CoreEvents.GRID_RENDERED, {
|
||||||
container: this.container,
|
container: this.container,
|
||||||
currentDate: this.currentDate,
|
currentDate: this.currentDate,
|
||||||
|
|
@ -130,22 +107,48 @@ export class GridManager {
|
||||||
layoutConfig: layoutConfig,
|
layoutConfig: layoutConfig,
|
||||||
columnCount: layoutConfig.columnCount
|
columnCount: layoutConfig.columnCount
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current period label from strategy
|
* Get current period label using DateCalculator
|
||||||
*/
|
*/
|
||||||
public getCurrentPeriodLabel(): string {
|
public getCurrentPeriodLabel(): string {
|
||||||
return this.currentStrategy.getPeriodLabel(this.currentDate);
|
switch (this.currentView) {
|
||||||
|
case 'week':
|
||||||
|
case 'day':
|
||||||
|
const weekStart = DateCalculator.getISOWeekStart(this.currentDate);
|
||||||
|
const weekEnd = DateCalculator.getWeekEnd(this.currentDate);
|
||||||
|
return DateCalculator.formatDateRange(weekStart, weekEnd);
|
||||||
|
case 'month':
|
||||||
|
return this.currentDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
||||||
|
default:
|
||||||
|
const defaultWeekStart = DateCalculator.getISOWeekStart(this.currentDate);
|
||||||
|
const defaultWeekEnd = DateCalculator.getWeekEnd(this.currentDate);
|
||||||
|
return DateCalculator.formatDateRange(defaultWeekStart, defaultWeekEnd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to next period using strategy
|
* Navigate to next period using DateCalculator
|
||||||
*/
|
*/
|
||||||
public navigateNext(): void {
|
public navigateNext(): void {
|
||||||
const nextDate = this.currentStrategy.getNextPeriod(this.currentDate);
|
let nextDate: Date;
|
||||||
|
|
||||||
|
switch (this.currentView) {
|
||||||
|
case 'week':
|
||||||
|
nextDate = DateCalculator.addWeeks(this.currentDate, 1);
|
||||||
|
break;
|
||||||
|
case 'month':
|
||||||
|
nextDate = this.addMonths(this.currentDate, 1);
|
||||||
|
break;
|
||||||
|
case 'day':
|
||||||
|
nextDate = DateCalculator.addDays(this.currentDate, 1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
nextDate = DateCalculator.addWeeks(this.currentDate, 1);
|
||||||
|
}
|
||||||
|
|
||||||
this.currentDate = nextDate;
|
this.currentDate = nextDate;
|
||||||
|
|
||||||
eventBus.emit(CoreEvents.PERIOD_CHANGED, {
|
eventBus.emit(CoreEvents.PERIOD_CHANGED, {
|
||||||
|
|
@ -158,14 +161,29 @@ export class GridManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to previous period using strategy
|
* Navigate to previous period using DateCalculator
|
||||||
*/
|
*/
|
||||||
public navigatePrevious(): void {
|
public navigatePrevious(): void {
|
||||||
const prevDate = this.currentStrategy.getPreviousPeriod(this.currentDate);
|
let prevDate: Date;
|
||||||
|
|
||||||
|
switch (this.currentView) {
|
||||||
|
case 'week':
|
||||||
|
prevDate = DateCalculator.addWeeks(this.currentDate, -1);
|
||||||
|
break;
|
||||||
|
case 'month':
|
||||||
|
prevDate = this.addMonths(this.currentDate, -1);
|
||||||
|
break;
|
||||||
|
case 'day':
|
||||||
|
prevDate = DateCalculator.addDays(this.currentDate, -1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
prevDate = DateCalculator.addWeeks(this.currentDate, -1);
|
||||||
|
}
|
||||||
|
|
||||||
this.currentDate = prevDate;
|
this.currentDate = prevDate;
|
||||||
|
|
||||||
eventBus.emit(CoreEvents.PERIOD_CHANGED, {
|
eventBus.emit(CoreEvents.PERIOD_CHANGED, {
|
||||||
direction: 'previous',
|
direction: 'previous',
|
||||||
newDate: prevDate,
|
newDate: prevDate,
|
||||||
periodLabel: this.getCurrentPeriodLabel()
|
periodLabel: this.getCurrentPeriodLabel()
|
||||||
});
|
});
|
||||||
|
|
@ -188,26 +206,137 @@ export class GridManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current view's display dates
|
* Get current view's display dates using DateCalculator
|
||||||
*/
|
*/
|
||||||
public getDisplayDates(): Date[] {
|
public getDisplayDates(): Date[] {
|
||||||
return this.currentStrategy.getDisplayDates(this.currentDate);
|
switch (this.currentView) {
|
||||||
|
case 'week':
|
||||||
|
const weekStart = DateCalculator.getISOWeekStart(this.currentDate);
|
||||||
|
return DateCalculator.getFullWeekDates(weekStart);
|
||||||
|
case 'month':
|
||||||
|
return this.getMonthDates(this.currentDate);
|
||||||
|
case 'day':
|
||||||
|
return [this.currentDate];
|
||||||
|
default:
|
||||||
|
const defaultWeekStart = DateCalculator.getISOWeekStart(this.currentDate);
|
||||||
|
return DateCalculator.getFullWeekDates(defaultWeekStart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get period range for current view
|
||||||
|
*/
|
||||||
|
private getPeriodRange(): { startDate: Date; endDate: Date } {
|
||||||
|
switch (this.currentView) {
|
||||||
|
case 'week':
|
||||||
|
const weekStart = DateCalculator.getISOWeekStart(this.currentDate);
|
||||||
|
const weekEnd = DateCalculator.getWeekEnd(this.currentDate);
|
||||||
|
return {
|
||||||
|
startDate: weekStart,
|
||||||
|
endDate: weekEnd
|
||||||
|
};
|
||||||
|
case 'month':
|
||||||
|
return {
|
||||||
|
startDate: this.getMonthStart(this.currentDate),
|
||||||
|
endDate: this.getMonthEnd(this.currentDate)
|
||||||
|
};
|
||||||
|
case 'day':
|
||||||
|
return {
|
||||||
|
startDate: this.currentDate,
|
||||||
|
endDate: this.currentDate
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
const defaultWeekStart = DateCalculator.getISOWeekStart(this.currentDate);
|
||||||
|
const defaultWeekEnd = DateCalculator.getWeekEnd(this.currentDate);
|
||||||
|
return {
|
||||||
|
startDate: defaultWeekStart,
|
||||||
|
endDate: defaultWeekEnd
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get layout config for current view
|
||||||
|
*/
|
||||||
|
private getLayoutConfig(): any {
|
||||||
|
switch (this.currentView) {
|
||||||
|
case 'week':
|
||||||
|
return {
|
||||||
|
columnCount: 7,
|
||||||
|
type: 'week'
|
||||||
|
};
|
||||||
|
case 'month':
|
||||||
|
return {
|
||||||
|
columnCount: 7,
|
||||||
|
type: 'month'
|
||||||
|
};
|
||||||
|
case 'day':
|
||||||
|
return {
|
||||||
|
columnCount: 1,
|
||||||
|
type: 'day'
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
columnCount: 7,
|
||||||
|
type: 'week'
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up all resources
|
* Clean up all resources
|
||||||
*/
|
*/
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
|
|
||||||
// Clean up event listeners
|
// Clean up event listeners
|
||||||
this.eventCleanup.forEach(cleanup => cleanup());
|
this.eventCleanup.forEach(cleanup => cleanup());
|
||||||
this.eventCleanup = [];
|
this.eventCleanup = [];
|
||||||
|
|
||||||
// Clean up current strategy
|
|
||||||
this.currentStrategy.destroy();
|
|
||||||
|
|
||||||
// Clear references
|
// Clear references
|
||||||
this.container = null;
|
this.container = null;
|
||||||
this.resourceData = null;
|
this.resourceData = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to add months to a date
|
||||||
|
*/
|
||||||
|
private addMonths(date: Date, months: number): Date {
|
||||||
|
const result = new Date(date);
|
||||||
|
result.setMonth(result.getMonth() + months);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to get month start
|
||||||
|
*/
|
||||||
|
private getMonthStart(date: Date): Date {
|
||||||
|
const result = new Date(date);
|
||||||
|
result.setDate(1);
|
||||||
|
result.setHours(0, 0, 0, 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to get month end
|
||||||
|
*/
|
||||||
|
private getMonthEnd(date: Date): Date {
|
||||||
|
const result = new Date(date);
|
||||||
|
result.setMonth(result.getMonth() + 1, 0);
|
||||||
|
result.setHours(23, 59, 59, 999);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to get all dates in a month
|
||||||
|
*/
|
||||||
|
private getMonthDates(date: Date): Date[] {
|
||||||
|
const dates: Date[] = [];
|
||||||
|
const monthStart = this.getMonthStart(date);
|
||||||
|
const monthEnd = this.getMonthEnd(date);
|
||||||
|
|
||||||
|
for (let d = new Date(monthStart); d <= monthEnd; d.setDate(d.getDate() + 1)) {
|
||||||
|
dates.push(new Date(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
return dates;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,122 +4,180 @@ import { calendarConfig } from '../core/CalendarConfig';
|
||||||
import { CoreEvents } from '../constants/CoreEvents';
|
import { CoreEvents } from '../constants/CoreEvents';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewManager - Håndterer skift mellem dag/uge/måned visninger
|
* ViewManager - Optimized view switching with consolidated event handling
|
||||||
* Arbejder med custom tags fra POC design
|
* Reduces redundant DOM queries and event listener setups
|
||||||
*/
|
*/
|
||||||
export class ViewManager {
|
export class ViewManager {
|
||||||
private eventBus: IEventBus;
|
private eventBus: IEventBus;
|
||||||
private currentView: CalendarView = 'week';
|
private currentView: CalendarView = 'week';
|
||||||
private eventCleanup: (() => void)[] = [];
|
private eventCleanup: (() => void)[] = [];
|
||||||
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) {
|
constructor(eventBus: IEventBus) {
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consolidated event listener setup with better organization
|
||||||
|
*/
|
||||||
private setupEventListeners(): void {
|
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.eventCleanup.push(
|
||||||
this.eventBus.on(CoreEvents.INITIALIZED, () => {
|
this.eventBus.on(CoreEvents.INITIALIZED, () => {
|
||||||
this.initializeView();
|
this.initializeView();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.eventCleanup.push(
|
// Remove redundant VIEW_CHANGED listener that causes circular calls
|
||||||
this.eventBus.on(CoreEvents.VIEW_CHANGED, (event: Event) => {
|
// changeView is called directly from button handlers
|
||||||
const customEvent = event as CustomEvent;
|
|
||||||
const { currentView } = customEvent.detail;
|
|
||||||
this.changeView(currentView);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
this.eventCleanup.push(
|
this.eventCleanup.push(
|
||||||
this.eventBus.on(CoreEvents.DATE_CHANGED, () => {
|
this.eventBus.on(CoreEvents.DATE_CHANGED, () => {
|
||||||
this.refreshCurrentView();
|
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]');
|
* Consolidated button handler setup with shared logic
|
||||||
viewButtons.forEach(button => {
|
*/
|
||||||
const handler = (event: Event) => {
|
private setupButtonHandlers(): void {
|
||||||
event.preventDefault();
|
// Setup view buttons with consolidated handler
|
||||||
const view = button.getAttribute('data-view') as CalendarView;
|
this.setupButtonGroup('swp-view-button[data-view]', 'data-view', (value) => {
|
||||||
if (view && this.isValidView(view)) {
|
if (this.isValidView(value)) {
|
||||||
this.changeView(view);
|
this.changeView(value as CalendarView);
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
button.addEventListener('click', handler);
|
|
||||||
this.buttonListeners.set(button, handler);
|
// 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]');
|
* Generic button group setup to eliminate duplicate code
|
||||||
workweekButtons.forEach(button => {
|
*/
|
||||||
const handler = (event: Event) => {
|
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();
|
event.preventDefault();
|
||||||
const workweekId = button.getAttribute('data-workweek');
|
const value = button.getAttribute(attribute);
|
||||||
if (workweekId) {
|
if (value) {
|
||||||
this.changeWorkweek(workweekId);
|
handler(value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
button.addEventListener('click', handler);
|
button.addEventListener('click', clickHandler);
|
||||||
this.buttonListeners.set(button, handler);
|
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 {
|
private initializeView(): void {
|
||||||
this.updateViewButtons();
|
this.updateAllButtons();
|
||||||
this.updateWorkweekButtons();
|
this.emitViewRendered();
|
||||||
|
|
||||||
this.eventBus.emit(CoreEvents.VIEW_RENDERED, {
|
|
||||||
view: this.currentView
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
const previousView = this.currentView;
|
const previousView = this.currentView;
|
||||||
this.currentView = newView;
|
this.currentView = newView;
|
||||||
|
|
||||||
|
this.updateAllButtons();
|
||||||
this.updateViewButtons();
|
|
||||||
|
|
||||||
this.eventBus.emit(CoreEvents.VIEW_CHANGED, {
|
this.eventBus.emit(CoreEvents.VIEW_CHANGED, {
|
||||||
previousView,
|
previousView,
|
||||||
currentView: newView
|
currentView: newView
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimized workweek change with consolidated updates
|
||||||
|
*/
|
||||||
private changeWorkweek(workweekId: string): void {
|
private changeWorkweek(workweekId: string): void {
|
||||||
|
|
||||||
// Update the calendar config
|
// Update the calendar config
|
||||||
calendarConfig.setWorkWeek(workweekId);
|
calendarConfig.setWorkWeek(workweekId);
|
||||||
|
|
||||||
// Update button states
|
// Update button states using cached elements
|
||||||
this.updateWorkweekButtons();
|
this.updateAllButtons();
|
||||||
|
|
||||||
// Trigger a calendar refresh to apply the new workweek
|
// Trigger a calendar refresh to apply the new workweek
|
||||||
this.eventBus.emit(CoreEvents.REFRESH_REQUESTED);
|
this.eventBus.emit(CoreEvents.REFRESH_REQUESTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateViewButtons(): void {
|
/**
|
||||||
const viewButtons = document.querySelectorAll('swp-view-button[data-view]');
|
* Consolidated button update method to eliminate duplicate code
|
||||||
viewButtons.forEach(button => {
|
*/
|
||||||
const buttonView = button.getAttribute('data-view') as CalendarView;
|
private updateAllButtons(): void {
|
||||||
if (buttonView === this.currentView) {
|
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');
|
button.setAttribute('data-active', 'true');
|
||||||
} else {
|
} else {
|
||||||
button.removeAttribute('data-active');
|
button.removeAttribute('data-active');
|
||||||
|
|
@ -127,38 +185,46 @@ export class ViewManager {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateWorkweekButtons(): void {
|
/**
|
||||||
const currentWorkweek = calendarConfig.getCurrentWorkWeek();
|
* Emit view rendered event with current view
|
||||||
const workweekButtons = document.querySelectorAll('swp-preset-button[data-workweek]');
|
*/
|
||||||
|
private emitViewRendered(): void {
|
||||||
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 {
|
|
||||||
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 {
|
||||||
|
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 {
|
public getCurrentView(): CalendarView {
|
||||||
return this.currentView;
|
return this.currentView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public refresh method
|
||||||
|
*/
|
||||||
public refresh(): void {
|
public refresh(): void {
|
||||||
this.refreshCurrentView();
|
this.refreshCurrentView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up all resources and cached elements
|
||||||
|
*/
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
// Clean up event bus listeners
|
// Clean up event bus listeners
|
||||||
this.eventCleanup.forEach(cleanup => cleanup());
|
this.eventCleanup.forEach(cleanup => cleanup());
|
||||||
|
|
@ -169,5 +235,10 @@ export class ViewManager {
|
||||||
button.removeEventListener('click', handler);
|
button.removeEventListener('click', handler);
|
||||||
});
|
});
|
||||||
this.buttonListeners.clear();
|
this.buttonListeners.clear();
|
||||||
|
|
||||||
|
// Clear cached elements
|
||||||
|
this.cachedViewButtons = null;
|
||||||
|
this.cachedWorkweekButtons = null;
|
||||||
|
this.lastButtonCacheTime = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,94 +1,124 @@
|
||||||
import { CalendarConfig } from '../core/CalendarConfig';
|
import { CalendarConfig } from '../core/CalendarConfig';
|
||||||
import { ResourceCalendarData } from '../types/CalendarTypes';
|
import { ResourceCalendarData, CalendarView } from '../types/CalendarTypes';
|
||||||
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
|
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
|
||||||
import { HeaderRenderContext } from './HeaderRenderer';
|
import { HeaderRenderContext } from './HeaderRenderer';
|
||||||
import { ColumnRenderContext } from './ColumnRenderer';
|
import { ColumnRenderContext } from './ColumnRenderer';
|
||||||
import { eventBus } from '../core/EventBus';
|
import { eventBus } from '../core/EventBus';
|
||||||
|
import { DateCalculator } from '../utils/DateCalculator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GridRenderer - Handles DOM rendering for the calendar grid
|
* GridRenderer - Centralized DOM rendering for calendar grid
|
||||||
* Separated from GridManager to follow Single Responsibility Principle
|
* Optimized to reduce redundant DOM operations and improve performance
|
||||||
*/
|
*/
|
||||||
export class GridRenderer {
|
export class GridRenderer {
|
||||||
private config: CalendarConfig;
|
private config: CalendarConfig;
|
||||||
private headerEventListener: ((event: Event) => void) | null = null;
|
private headerEventListener: ((event: Event) => void) | null = null;
|
||||||
|
private cachedGridContainer: HTMLElement | null = null;
|
||||||
|
private cachedCalendarHeader: HTMLElement | null = null;
|
||||||
|
private cachedTimeAxis: HTMLElement | null = null;
|
||||||
|
|
||||||
constructor(config: CalendarConfig) {
|
constructor(config: CalendarConfig) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the complete grid structure
|
* Render the complete grid structure with view-aware optimization
|
||||||
*/
|
*/
|
||||||
public renderGrid(
|
public renderGrid(
|
||||||
grid: HTMLElement,
|
grid: HTMLElement,
|
||||||
currentWeek: Date,
|
currentDate: Date,
|
||||||
resourceData: ResourceCalendarData | null
|
resourceData: ResourceCalendarData | null,
|
||||||
|
view: CalendarView = 'week'
|
||||||
): void {
|
): void {
|
||||||
|
|
||||||
if (!grid || !currentWeek) {
|
if (!grid || !currentDate) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache grid reference for performance
|
||||||
|
this.cachedGridContainer = grid;
|
||||||
|
|
||||||
// Only clear and rebuild if grid is empty (first render)
|
// Only clear and rebuild if grid is empty (first render)
|
||||||
if (grid.children.length === 0) {
|
if (grid.children.length === 0) {
|
||||||
// Create POC structure: header-spacer + time-axis + grid-container
|
this.createCompleteGridStructure(grid, currentDate, resourceData, view);
|
||||||
this.createHeaderSpacer(grid);
|
|
||||||
this.createTimeAxis(grid);
|
|
||||||
this.createGridContainer(grid, currentWeek, resourceData);
|
|
||||||
} else {
|
} else {
|
||||||
// Just update the calendar header for all-day events
|
// Optimized update - only refresh dynamic content
|
||||||
this.updateCalendarHeader(grid, currentWeek, resourceData);
|
this.updateGridContent(grid, currentDate, resourceData, view);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create complete grid structure in one operation
|
||||||
|
*/
|
||||||
|
private createCompleteGridStructure(
|
||||||
|
grid: HTMLElement,
|
||||||
|
currentDate: Date,
|
||||||
|
resourceData: ResourceCalendarData | null,
|
||||||
|
view: CalendarView
|
||||||
|
): void {
|
||||||
|
// Create all elements in memory first for better performance
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
|
||||||
}
|
// Create header spacer
|
||||||
|
|
||||||
/**
|
|
||||||
* Create header spacer to align time axis with week content
|
|
||||||
*/
|
|
||||||
private createHeaderSpacer(grid: HTMLElement): void {
|
|
||||||
const headerSpacer = document.createElement('swp-header-spacer');
|
const headerSpacer = document.createElement('swp-header-spacer');
|
||||||
grid.appendChild(headerSpacer);
|
fragment.appendChild(headerSpacer);
|
||||||
|
|
||||||
|
// Create time axis with caching
|
||||||
|
const timeAxis = this.createOptimizedTimeAxis();
|
||||||
|
this.cachedTimeAxis = timeAxis;
|
||||||
|
fragment.appendChild(timeAxis);
|
||||||
|
|
||||||
|
// Create grid container with caching
|
||||||
|
const gridContainer = this.createOptimizedGridContainer(currentDate, resourceData, view);
|
||||||
|
this.cachedGridContainer = gridContainer;
|
||||||
|
fragment.appendChild(gridContainer);
|
||||||
|
|
||||||
|
// Append all at once to minimize reflows
|
||||||
|
grid.appendChild(fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create time axis (positioned beside grid container)
|
* Create optimized time axis with caching
|
||||||
*/
|
*/
|
||||||
private createTimeAxis(grid: HTMLElement): void {
|
private createOptimizedTimeAxis(): HTMLElement {
|
||||||
const timeAxis = document.createElement('swp-time-axis');
|
const timeAxis = document.createElement('swp-time-axis');
|
||||||
const timeAxisContent = document.createElement('swp-time-axis-content');
|
const timeAxisContent = document.createElement('swp-time-axis-content');
|
||||||
const gridSettings = this.config.getGridSettings();
|
const gridSettings = this.config.getGridSettings();
|
||||||
const startHour = gridSettings.dayStartHour;
|
const startHour = gridSettings.dayStartHour;
|
||||||
const endHour = gridSettings.dayEndHour;
|
const endHour = gridSettings.dayEndHour;
|
||||||
|
|
||||||
|
// Create all hour markers in memory first
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
for (let hour = startHour; hour < endHour; hour++) {
|
for (let hour = startHour; hour < endHour; hour++) {
|
||||||
const marker = document.createElement('swp-hour-marker');
|
const marker = document.createElement('swp-hour-marker');
|
||||||
const period = hour >= 12 ? 'PM' : 'AM';
|
const period = hour >= 12 ? 'PM' : 'AM';
|
||||||
const displayHour = hour > 12 ? hour - 12 : (hour === 0 ? 12 : hour);
|
const displayHour = hour > 12 ? hour - 12 : (hour === 0 ? 12 : hour);
|
||||||
marker.textContent = `${displayHour} ${period}`;
|
marker.textContent = `${displayHour} ${period}`;
|
||||||
timeAxisContent.appendChild(marker);
|
fragment.appendChild(marker);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timeAxisContent.appendChild(fragment);
|
||||||
timeAxis.appendChild(timeAxisContent);
|
timeAxis.appendChild(timeAxisContent);
|
||||||
grid.appendChild(timeAxis);
|
return timeAxis;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create grid container with header and scrollable content
|
* Create optimized grid container with header and scrollable content
|
||||||
*/
|
*/
|
||||||
private createGridContainer(
|
private createOptimizedGridContainer(
|
||||||
grid: HTMLElement,
|
currentDate: Date,
|
||||||
currentWeek: Date,
|
resourceData: ResourceCalendarData | null,
|
||||||
resourceData: ResourceCalendarData | null
|
view: CalendarView
|
||||||
): void {
|
): HTMLElement {
|
||||||
const gridContainer = document.createElement('swp-grid-container');
|
const gridContainer = document.createElement('swp-grid-container');
|
||||||
|
|
||||||
// Create calendar header using Strategy Pattern
|
// Create calendar header with caching
|
||||||
const calendarHeader = document.createElement('swp-calendar-header');
|
const calendarHeader = document.createElement('swp-calendar-header');
|
||||||
this.renderCalendarHeader(calendarHeader, currentWeek, resourceData);
|
this.renderCalendarHeader(calendarHeader, currentDate, resourceData, view);
|
||||||
|
this.cachedCalendarHeader = calendarHeader;
|
||||||
gridContainer.appendChild(calendarHeader);
|
gridContainer.appendChild(calendarHeader);
|
||||||
|
|
||||||
// Create scrollable content
|
// Create scrollable content structure
|
||||||
const scrollableContent = document.createElement('swp-scrollable-content');
|
const scrollableContent = document.createElement('swp-scrollable-content');
|
||||||
const timeGrid = document.createElement('swp-time-grid');
|
const timeGrid = document.createElement('swp-time-grid');
|
||||||
|
|
||||||
|
|
@ -96,30 +126,31 @@ export class GridRenderer {
|
||||||
const gridLines = document.createElement('swp-grid-lines');
|
const gridLines = document.createElement('swp-grid-lines');
|
||||||
timeGrid.appendChild(gridLines);
|
timeGrid.appendChild(gridLines);
|
||||||
|
|
||||||
// Create column container using Strategy Pattern
|
// Create column container
|
||||||
const columnContainer = document.createElement('swp-day-columns');
|
const columnContainer = document.createElement('swp-day-columns');
|
||||||
this.renderColumnContainer(columnContainer, currentWeek, resourceData);
|
this.renderColumnContainer(columnContainer, currentDate, resourceData, view);
|
||||||
timeGrid.appendChild(columnContainer);
|
timeGrid.appendChild(columnContainer);
|
||||||
|
|
||||||
scrollableContent.appendChild(timeGrid);
|
scrollableContent.appendChild(timeGrid);
|
||||||
gridContainer.appendChild(scrollableContent);
|
gridContainer.appendChild(scrollableContent);
|
||||||
|
|
||||||
grid.appendChild(gridContainer);
|
return gridContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render calendar header using Strategy Pattern
|
* Render calendar header with view awareness
|
||||||
*/
|
*/
|
||||||
private renderCalendarHeader(
|
private renderCalendarHeader(
|
||||||
calendarHeader: HTMLElement,
|
calendarHeader: HTMLElement,
|
||||||
currentWeek: Date,
|
currentDate: Date,
|
||||||
resourceData: ResourceCalendarData | null
|
resourceData: ResourceCalendarData | null,
|
||||||
|
view: CalendarView
|
||||||
): void {
|
): void {
|
||||||
const calendarType = this.config.getCalendarMode();
|
const calendarType = this.config.getCalendarMode();
|
||||||
const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType);
|
const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType);
|
||||||
|
|
||||||
const context: HeaderRenderContext = {
|
const context: HeaderRenderContext = {
|
||||||
currentWeek: currentWeek,
|
currentWeek: currentDate, // HeaderRenderer expects currentWeek property
|
||||||
config: this.config,
|
config: this.config,
|
||||||
resourceData: resourceData
|
resourceData: resourceData
|
||||||
};
|
};
|
||||||
|
|
@ -129,23 +160,24 @@ export class GridRenderer {
|
||||||
// Always ensure all-day containers exist for all days
|
// Always ensure all-day containers exist for all days
|
||||||
headerRenderer.ensureAllDayContainers(calendarHeader);
|
headerRenderer.ensureAllDayContainers(calendarHeader);
|
||||||
|
|
||||||
// Setup event listener for mouseover detection
|
// Setup optimized event listener
|
||||||
this.setupHeaderEventListener(calendarHeader);
|
this.setupOptimizedHeaderEventListener(calendarHeader);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render column container using Strategy Pattern
|
* Render column container with view awareness
|
||||||
*/
|
*/
|
||||||
private renderColumnContainer(
|
private renderColumnContainer(
|
||||||
columnContainer: HTMLElement,
|
columnContainer: HTMLElement,
|
||||||
currentWeek: Date,
|
currentDate: Date,
|
||||||
resourceData: ResourceCalendarData | null
|
resourceData: ResourceCalendarData | null,
|
||||||
|
view: CalendarView
|
||||||
): void {
|
): void {
|
||||||
const calendarType = this.config.getCalendarMode();
|
const calendarType = this.config.getCalendarMode();
|
||||||
const columnRenderer = CalendarTypeFactory.getColumnRenderer(calendarType);
|
const columnRenderer = CalendarTypeFactory.getColumnRenderer(calendarType);
|
||||||
|
|
||||||
const context: ColumnRenderContext = {
|
const context: ColumnRenderContext = {
|
||||||
currentWeek: currentWeek,
|
currentWeek: currentDate, // ColumnRenderer expects currentWeek property
|
||||||
config: this.config,
|
config: this.config,
|
||||||
resourceData: resourceData
|
resourceData: resourceData
|
||||||
};
|
};
|
||||||
|
|
@ -154,37 +186,53 @@ export class GridRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update only the calendar header without rebuilding entire grid
|
* Optimized update of grid content without full rebuild
|
||||||
*/
|
*/
|
||||||
private updateCalendarHeader(
|
private updateGridContent(
|
||||||
grid: HTMLElement,
|
grid: HTMLElement,
|
||||||
currentWeek: Date,
|
currentDate: Date,
|
||||||
resourceData: ResourceCalendarData | null
|
resourceData: ResourceCalendarData | null,
|
||||||
|
view: CalendarView
|
||||||
): void {
|
): void {
|
||||||
const calendarHeader = grid.querySelector('swp-calendar-header');
|
// Use cached elements if available
|
||||||
if (!calendarHeader) return;
|
const calendarHeader = this.cachedCalendarHeader || grid.querySelector('swp-calendar-header');
|
||||||
|
if (calendarHeader) {
|
||||||
|
// Clear and re-render header content
|
||||||
|
calendarHeader.innerHTML = '';
|
||||||
|
this.renderCalendarHeader(calendarHeader as HTMLElement, currentDate, resourceData, view);
|
||||||
|
}
|
||||||
|
|
||||||
// Clear existing content
|
// Update column container if needed
|
||||||
calendarHeader.innerHTML = '';
|
const columnContainer = grid.querySelector('swp-day-columns');
|
||||||
|
if (columnContainer) {
|
||||||
// Re-render headers using Strategy Pattern - this will also re-attach the event listener
|
columnContainer.innerHTML = '';
|
||||||
this.renderCalendarHeader(calendarHeader as HTMLElement, currentWeek, resourceData);
|
this.renderColumnContainer(columnContainer as HTMLElement, currentDate, resourceData, view);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup or re-setup event delegation listener on calendar header
|
* Setup optimized event delegation listener with better performance
|
||||||
*/
|
*/
|
||||||
private setupHeaderEventListener(calendarHeader: HTMLElement): void {
|
private setupOptimizedHeaderEventListener(calendarHeader: HTMLElement): void {
|
||||||
// Remove existing listener if any (stored reference approach)
|
// Remove existing listener if any
|
||||||
if (this.headerEventListener) {
|
if (this.headerEventListener) {
|
||||||
calendarHeader.removeEventListener('mouseover', this.headerEventListener);
|
calendarHeader.removeEventListener('mouseover', this.headerEventListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new listener function
|
// Create optimized listener with throttling
|
||||||
|
let lastEmitTime = 0;
|
||||||
|
const throttleDelay = 16; // ~60fps
|
||||||
|
|
||||||
this.headerEventListener = (event) => {
|
this.headerEventListener = (event) => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastEmitTime < throttleDelay) {
|
||||||
|
return; // Throttle events for better performance
|
||||||
|
}
|
||||||
|
lastEmitTime = now;
|
||||||
|
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
|
|
||||||
// Check what was hovered - could be day-header OR all-day-container
|
// Optimized element detection
|
||||||
const dayHeader = target.closest('swp-day-header');
|
const dayHeader = target.closest('swp-day-header');
|
||||||
const allDayContainer = target.closest('swp-allday-container');
|
const allDayContainer = target.closest('swp-allday-container');
|
||||||
|
|
||||||
|
|
@ -196,36 +244,50 @@ export class GridRenderer {
|
||||||
hoveredElement = dayHeader as HTMLElement;
|
hoveredElement = dayHeader as HTMLElement;
|
||||||
targetDate = hoveredElement.dataset.date;
|
targetDate = hoveredElement.dataset.date;
|
||||||
} else if (allDayContainer) {
|
} else if (allDayContainer) {
|
||||||
// For all-day areas, we need to determine which day column we're over
|
|
||||||
hoveredElement = allDayContainer as HTMLElement;
|
hoveredElement = allDayContainer as HTMLElement;
|
||||||
|
|
||||||
// Calculate which day we're hovering over based on mouse position
|
// Optimized day calculation using cached header rect
|
||||||
const headerRect = calendarHeader.getBoundingClientRect();
|
const headerRect = calendarHeader.getBoundingClientRect();
|
||||||
const dayHeaders = calendarHeader.querySelectorAll('swp-day-header');
|
const dayHeaders = calendarHeader.querySelectorAll('swp-day-header');
|
||||||
const mouseX = (event as MouseEvent).clientX - headerRect.left;
|
const mouseX = (event as MouseEvent).clientX - headerRect.left;
|
||||||
const dayWidth = headerRect.width / dayHeaders.length;
|
const dayWidth = headerRect.width / dayHeaders.length;
|
||||||
const dayIndex = Math.floor(mouseX / dayWidth);
|
const dayIndex = Math.max(0, Math.min(dayHeaders.length - 1, Math.floor(mouseX / dayWidth)));
|
||||||
|
|
||||||
const targetDayHeader = dayHeaders[dayIndex] as HTMLElement;
|
const targetDayHeader = dayHeaders[dayIndex] as HTMLElement;
|
||||||
targetDate = targetDayHeader?.dataset.date;
|
targetDate = targetDayHeader?.dataset.date;
|
||||||
} else {
|
} else {
|
||||||
return; // No valid element found
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get header renderer once and cache
|
||||||
// Get the header renderer for addToAllDay functionality
|
|
||||||
const calendarType = this.config.getCalendarMode();
|
const calendarType = this.config.getCalendarMode();
|
||||||
const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType);
|
const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType);
|
||||||
|
|
||||||
eventBus.emit('header:mouseover', {
|
eventBus.emit('header:mouseover', {
|
||||||
element: hoveredElement,
|
element: hoveredElement,
|
||||||
targetDate,
|
targetDate,
|
||||||
headerRenderer
|
headerRenderer
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add the new listener
|
// Add the optimized listener
|
||||||
calendarHeader.addEventListener('mouseover', this.headerEventListener);
|
calendarHeader.addEventListener('mouseover', this.headerEventListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up cached elements and event listeners
|
||||||
|
*/
|
||||||
|
public destroy(): void {
|
||||||
|
// Clean up event listeners
|
||||||
|
if (this.headerEventListener && this.cachedCalendarHeader) {
|
||||||
|
this.cachedCalendarHeader.removeEventListener('mouseover', this.headerEventListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear cached references
|
||||||
|
this.cachedGridContainer = null;
|
||||||
|
this.cachedCalendarHeader = null;
|
||||||
|
this.cachedTimeAxis = null;
|
||||||
|
this.headerEventListener = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue