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:
parent
1ae4f00f2b
commit
b6ab1ff50e
6 changed files with 193 additions and 327 deletions
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
const cacheKey = `${this.dateService.formatISODate(startDate)}_${this.dateService.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 => {
|
|
||||||
// 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 this.events.filter(event => {
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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}`;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue