diff --git a/src/managers/CalendarManager.ts b/src/managers/CalendarManager.ts index baec945..0afbd87 100644 --- a/src/managers/CalendarManager.ts +++ b/src/managers/CalendarManager.ts @@ -4,6 +4,7 @@ import { calendarConfig } from '../core/CalendarConfig.js'; import { CalendarEvent, CalendarView, IEventBus } from '../types/CalendarTypes.js'; import { EventManager } from './EventManager.js'; import { GridManager } from './GridManager.js'; +import { HeaderManager } from './HeaderManager.js'; import { EventRenderingService } from '../renderers/EventRendererManager.js'; import { ScrollManager } from './ScrollManager.js'; import { DateCalculator } from '../utils/DateCalculator.js'; @@ -17,6 +18,7 @@ export class CalendarManager { private eventBus: IEventBus; private eventManager: EventManager; private gridManager: GridManager; + private headerManager: HeaderManager; private eventRenderer: EventRenderingService; private scrollManager: ScrollManager; private eventFilterManager: EventFilterManager; @@ -35,6 +37,7 @@ export class CalendarManager { this.eventBus = eventBus; this.eventManager = eventManager; this.gridManager = gridManager; + this.headerManager = new HeaderManager(); this.eventRenderer = eventRenderer; this.scrollManager = scrollManager; this.eventFilterManager = new EventFilterManager(); @@ -66,6 +69,9 @@ export class CalendarManager { } await this.gridManager.render(); + // Step 2a: Setup header drag listeners after grid render (when DOM is available) + this.headerManager.setupHeaderDragListeners(); + // Step 2b: Trigger event rendering now that data is loaded // Re-emit GRID_RENDERED to trigger EventRendererManager const gridContainer = document.querySelector('swp-calendar-container'); diff --git a/src/managers/DragDropManager.ts b/src/managers/DragDropManager.ts index a12736e..d48783d 100644 --- a/src/managers/DragDropManager.ts +++ b/src/managers/DragDropManager.ts @@ -22,7 +22,6 @@ export class DragDropManager { private eventBus: IEventBus; // Mouse tracking with optimized state - private isMouseDown = false; private lastMousePosition: Position = { x: 0, y: 0 }; private lastLoggedPosition: Position = { x: 0, y: 0 }; private currentMouseY = 0; @@ -96,7 +95,7 @@ export class DragDropManager { this.eventBus.on('header:mouseover', (event) => { const { element, targetDate, headerRenderer } = (event as CustomEvent).detail; - if (this.isMouseDown && this.draggedEventId && targetDate) { + if (this.draggedEventId && targetDate) { // Emit event to convert to all-day this.eventBus.emit('drag:convert-to-allday', { eventId: this.draggedEventId, @@ -106,10 +105,48 @@ export class DragDropManager { }); } }); + + // Listen for column mouseover events (for all-day to timed conversion) + this.eventBus.on('column:mouseover', (event) => { + const { targetColumn, targetY } = (event as CustomEvent).detail; + + if ((event as any).buttons === 1 && this.draggedEventId && this.isAllDayEventBeingDragged()) { + // Emit event to convert to timed + this.eventBus.emit('drag:convert-to-timed', { + eventId: this.draggedEventId, + targetColumn, + targetY + }); + } + }); + + // Listen for header mouseleave events (for all-day to timed conversion when leaving header) + this.eventBus.on('header:mouseleave', (event) => { + // Check if we're dragging an all-day event + if ((event as any).buttons === 1 && this.draggedEventId && this.isAllDayEventBeingDragged()) { + // Get current mouse position to determine target column and Y + const currentColumn = this.detectColumn(this.lastMousePosition.x, this.lastMousePosition.y); + + if (currentColumn) { + // Calculate Y position relative to the column + const columnElement = this.getCachedColumnElement(currentColumn); + if (columnElement) { + const columnRect = columnElement.getBoundingClientRect(); + const targetY = this.lastMousePosition.y - columnRect.top - this.mouseOffset.y; + + // Emit event to convert to timed + this.eventBus.emit('drag:convert-to-timed', { + eventId: this.draggedEventId, + targetColumn: currentColumn, + targetY: Math.max(0, targetY) + }); + } + } + } + }); } private handleMouseDown(event: MouseEvent): void { - this.isMouseDown = true; this.isDragStarted = false; this.lastMousePosition = { x: event.clientX, y: event.clientY }; this.lastLoggedPosition = { x: event.clientX, y: event.clientY }; @@ -160,7 +197,7 @@ export class DragDropManager { private handleMouseMove(event: MouseEvent): void { this.currentMouseY = event.clientY; - if (this.isMouseDown && this.draggedEventId) { + if (event.buttons === 1 && this.draggedEventId) { const currentPosition: Position = { x: event.clientX, y: event.clientY }; // Check if we need to start drag (movement threshold) @@ -230,23 +267,27 @@ export class DragDropManager { * Optimized mouse up handler with consolidated cleanup */ private handleMouseUp(event: MouseEvent): void { - if (!this.isMouseDown) return; - - this.isMouseDown = false; this.stopAutoScroll(); if (this.draggedEventId && this.originalElement) { + // Store variables locally before cleanup + const eventId = this.draggedEventId; + const originalElement = this.originalElement; + const isDragStarted = this.isDragStarted; + + // Clean up drag state first + this.cleanupDragState(); + // Only emit drag:end if drag was actually started - if (this.isDragStarted) { + if (isDragStarted) { const finalPosition: Position = { x: event.clientX, y: event.clientY }; // Use consolidated position calculation const positionData = this.calculateDragPosition(finalPosition); - // Emit drag end event this.eventBus.emit('drag:end', { - eventId: this.draggedEventId, - originalElement: this.originalElement, + eventId: eventId, + originalElement: originalElement, finalPosition, finalColumn: positionData.column, finalY: positionData.snappedY @@ -254,14 +295,11 @@ export class DragDropManager { } else { // This was just a click - emit click event instead this.eventBus.emit('event:click', { - eventId: this.draggedEventId, - originalElement: this.originalElement, + eventId: eventId, + originalElement: originalElement, mousePosition: { x: event.clientX, y: event.clientY } }); } - - // Clean up drag state - this.cleanupDragState(); } } @@ -409,7 +447,7 @@ export class DragDropManager { if (this.autoScrollAnimationId !== null) return; const scroll = () => { - if (!this.cachedElements.scrollContainer || !this.isMouseDown) { + if (!this.cachedElements.scrollContainer || !this.draggedEventId) { this.stopAutoScroll(); return; } @@ -466,6 +504,14 @@ export class DragDropManager { this.cachedElements.lastColumnDate = null; } + /** + * Check if an all-day event is currently being dragged + */ + private isAllDayEventBeingDragged(): boolean { + if (!this.originalElement) return false; + return this.originalElement.tagName === 'SWP-ALLDAY-EVENT'; + } + /** * Clean up all resources and event listeners */ diff --git a/src/managers/HeaderManager.ts b/src/managers/HeaderManager.ts new file mode 100644 index 0000000..928edd1 --- /dev/null +++ b/src/managers/HeaderManager.ts @@ -0,0 +1,139 @@ +import { eventBus } from '../core/EventBus'; +import { calendarConfig } from '../core/CalendarConfig'; +import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; + +/** + * HeaderManager - Handles all header-related event logic + * Separates event handling from rendering concerns + */ +export class HeaderManager { + private headerEventListener: ((event: Event) => void) | null = null; + private headerMouseLeaveListener: ((event: Event) => void) | null = null; + private cachedCalendarHeader: HTMLElement | null = null; + + constructor() { + // Bind methods for event listeners + this.setupHeaderDragListeners = this.setupHeaderDragListeners.bind(this); + this.destroy = this.destroy.bind(this); + } + + /** + * Get cached calendar header element + */ + private getCalendarHeader(): HTMLElement | null { + if (!this.cachedCalendarHeader) { + this.cachedCalendarHeader = document.querySelector('swp-calendar-header'); + } + return this.cachedCalendarHeader; + } + + /** + * Setup header drag event listeners + */ + public setupHeaderDragListeners(): void { + const calendarHeader = this.getCalendarHeader(); + if (!calendarHeader) return; + + // Clean up existing listeners first + this.removeEventListeners(); + + // Throttle for better performance + let lastEmitTime = 0; + const throttleDelay = 16; // ~60fps + + this.headerEventListener = (event: Event) => { + const now = Date.now(); + if (now - lastEmitTime < throttleDelay) { + return; // Throttle events for better performance + } + lastEmitTime = now; + + const target = event.target as HTMLElement; + + // Optimized element detection + const dayHeader = target.closest('swp-day-header'); + const allDayContainer = target.closest('swp-allday-container'); + + if (dayHeader || allDayContainer) { + let hoveredElement: HTMLElement; + let targetDate: string | undefined; + + if (dayHeader) { + hoveredElement = dayHeader as HTMLElement; + targetDate = hoveredElement.dataset.date; + } else if (allDayContainer) { + hoveredElement = allDayContainer as HTMLElement; + + // Optimized day calculation using cached header rect + const headerRect = calendarHeader.getBoundingClientRect(); + const dayHeaders = calendarHeader.querySelectorAll('swp-day-header'); + const mouseX = (event as MouseEvent).clientX - headerRect.left; + const dayWidth = headerRect.width / dayHeaders.length; + const dayIndex = Math.max(0, Math.min(dayHeaders.length - 1, Math.floor(mouseX / dayWidth))); + + const targetDayHeader = dayHeaders[dayIndex] as HTMLElement; + targetDate = targetDayHeader?.dataset.date; + } else { + return; + } + + // Get header renderer for coordination + const calendarType = calendarConfig.getCalendarMode(); + const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType); + + eventBus.emit('header:mouseover', { + element: hoveredElement, + targetDate, + headerRenderer + }); + } + }; + + // Header mouseleave listener + this.headerMouseLeaveListener = (event: Event) => { + eventBus.emit('header:mouseleave', { + element: event.target as HTMLElement + }); + }; + + // Add event listeners + calendarHeader.addEventListener('mouseover', this.headerEventListener); + calendarHeader.addEventListener('mouseleave', this.headerMouseLeaveListener); + } + + /** + * Remove event listeners from header + */ + private removeEventListeners(): void { + const calendarHeader = this.getCalendarHeader(); + if (!calendarHeader) return; + + if (this.headerEventListener) { + calendarHeader.removeEventListener('mouseover', this.headerEventListener); + } + + if (this.headerMouseLeaveListener) { + calendarHeader.removeEventListener('mouseleave', this.headerMouseLeaveListener); + } + } + + /** + * Clear cached header reference + */ + public clearCache(): void { + this.cachedCalendarHeader = null; + } + + /** + * Clean up resources and event listeners + */ + public destroy(): void { + this.removeEventListeners(); + + // Clear references + this.headerEventListener = null; + this.headerMouseLeaveListener = null; + + this.clearCache(); + } +} \ No newline at end of file diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index f592955..d7b2770 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -140,6 +140,12 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { this.handleConvertToAllDay(eventId, targetDate, headerRenderer); }); + // Handle convert to timed event + eventBus.on('drag:convert-to-timed', (event) => { + const { eventId, targetColumn, targetY } = (event as CustomEvent).detail; + this.handleConvertToTimed(eventId, targetColumn, targetY); + }); + // Handle navigation period change (when slide animation completes) eventBus.on(CoreEvents.NAVIGATION_COMPLETED, () => { // Animate all-day height after navigation completes @@ -183,6 +189,45 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { return 60; } + /** + * Apply common drag styling to an element + */ + private applyDragStyling(element: HTMLElement): void { + element.style.position = 'absolute'; + element.style.zIndex = '999999'; + element.style.pointerEvents = 'none'; + element.style.opacity = '0.8'; + element.style.left = '2px'; + element.style.right = '2px'; + element.style.marginLeft = '0px'; + element.style.width = ''; + } + + /** + * Create event inner structure (swp-event-time and swp-event-title) + */ + private createEventInnerStructure(event: CalendarEvent): string { + const startTime = this.formatTime(event.start); + const endTime = this.formatTime(event.end); + const durationMinutes = (event.end.getTime() - event.start.getTime()) / (1000 * 60); + + return ` + ${startTime} - ${endTime} + ${event.title} + `; + } + + /** + * Apply standard event positioning + */ + private applyEventPositioning(element: HTMLElement, top: number, height: number): void { + element.style.position = 'absolute'; + element.style.top = `${top}px`; + element.style.height = `${height}px`; + element.style.left = '2px'; + element.style.right = '2px'; + } + /** * Create a clone of an event for dragging */ @@ -199,18 +244,10 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { const originalDurationMinutes = this.getOriginalEventDuration(originalEvent); clone.dataset.originalDuration = originalDurationMinutes.toString(); + // Apply common drag styling + this.applyDragStyling(clone); - // Style for dragging - clone.style.position = 'absolute'; - clone.style.zIndex = '999999'; - clone.style.pointerEvents = 'none'; - clone.style.opacity = '0.8'; - - // Dragged event skal have fuld kolonne bredde - clone.style.left = '2px'; - clone.style.right = '2px'; - clone.style.marginLeft = '0px'; - clone.style.width = ''; + // Set height from original event clone.style.height = originalEvent.style.height || `${originalEvent.getBoundingClientRect().height}px`; return clone; @@ -220,6 +257,10 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { * Update clone timestamp based on new position */ private updateCloneTimestamp(clone: HTMLElement, snappedY: number): void { + + //important as events can pile up, so they will still fire after event has been converted to another rendered type + if(clone.dataset.allDay == "true") return; + const gridSettings = calendarConfig.getGridSettings(); const hourHeight = gridSettings.hourHeight; const dayStartHour = gridSettings.dayStartHour; @@ -248,7 +289,6 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { clone.dataset.start = startDate.toISOString(); clone.dataset.end = endDate.toISOString(); - // Update display const timeElement = clone.querySelector('swp-event-time'); if (timeElement) { @@ -283,7 +323,6 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { * Handle drag start event */ private handleDragStart(originalElement: HTMLElement, eventId: string, mouseOffset: any, column: string): void { - console.log('handleDragStart:', eventId); this.originalEvent = originalElement; // Remove stacking styling during drag will be handled by new system @@ -346,10 +385,9 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { * Handle drag end event */ private handleDragEnd(eventId: string, originalElement: HTMLElement, finalColumn: string, finalY: number): void { - console.log('handleDragEnd:', eventId); if (!this.draggedClone || !this.originalEvent) { - console.log('Missing draggedClone or originalEvent'); + console.warn('Missing draggedClone or originalEvent'); return; } @@ -443,11 +481,13 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { this.draggedClone.style.userSelect = ''; // Behold z-index hvis det er et stacked event - // Update dataset with new times after successful drop - const newEvent = this.elementToCalendarEvent(this.draggedClone); - if (newEvent) { - this.draggedClone.dataset.start = newEvent.start.toISOString(); - this.draggedClone.dataset.end = newEvent.end.toISOString(); + // Update dataset with new times after successful drop (only for timed events) + if (this.draggedClone.tagName !== 'SWP-ALLDAY-EVENT') { + const newEvent = this.elementToCalendarEvent(this.draggedClone); + if (newEvent) { + this.draggedClone.dataset.start = newEvent.start.toISOString(); + this.draggedClone.dataset.end = newEvent.end.toISOString(); + } } // Detect overlaps with other events in the target column and reposition if needed @@ -687,12 +727,15 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { const allDayEvent = document.createElement('swp-allday-event'); allDayEvent.dataset.eventId = clone.dataset.eventId || ''; allDayEvent.dataset.title = eventTitle; - allDayEvent.dataset.start = `${targetDate}T${eventTime.split(' - ')[0]}:00`; - allDayEvent.dataset.end = `${targetDate}T${eventTime.split(' - ')[1]}:00`; + allDayEvent.dataset.start = `${targetDate}T00:00:00`; + allDayEvent.dataset.end = `${targetDate}T23:59:59`; allDayEvent.dataset.type = clone.dataset.type || 'work'; allDayEvent.dataset.duration = eventDuration; + allDayEvent.dataset.allDay = "true"; + allDayEvent.textContent = eventTitle; - + + console.log("allDayEvent", allDayEvent.dataset); // Position in grid (allDayEvent as HTMLElement).style.gridColumn = columnIndex.toString(); // grid-row will be set by checkAndAnimateAllDayHeight() based on actual position @@ -711,8 +754,101 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { // Check if height animation is needed this.triggerAllDayHeightAnimation(); } - - + + /** + * Handle conversion from all-day to timed event + */ + private handleConvertToTimed(eventId: string, targetColumn: string, targetY: number): void { + if (!this.draggedClone) return; + + // Only convert if it's an all-day event + if (this.draggedClone.tagName !== 'SWP-ALLDAY-EVENT') return; + + // Transform clone to timed format + this.transformAllDayToTimed(this.draggedClone, targetColumn, targetY); + } + + /** + * Transform clone from all-day to timed event + */ + private transformAllDayToTimed(allDayClone: HTMLElement, targetColumn: string, targetY: number): void { + // Find target column element + const columnElement = document.querySelector(`swp-day-column[data-date="${targetColumn}"]`); + if (!columnElement) return; + + const eventsLayer = columnElement.querySelector('swp-events-layer'); + if (!eventsLayer) return; + + // Extract event data from all-day element + const eventId = allDayClone.dataset.eventId || ''; + const eventTitle = allDayClone.dataset.title || allDayClone.textContent || 'Untitled'; + const eventType = allDayClone.dataset.type || 'work'; + + // Calculate time from Y position + const gridSettings = calendarConfig.getGridSettings(); + const hourHeight = gridSettings.hourHeight; + const dayStartHour = gridSettings.dayStartHour; + const snapInterval = gridSettings.snapInterval; + + // Calculate start time from position + const minutesFromGridStart = (targetY / hourHeight) * 60; + const actualStartMinutes = (dayStartHour * 60) + minutesFromGridStart; + const snappedStartMinutes = Math.round(actualStartMinutes / snapInterval) * snapInterval; + + // Use default duration or extract from dataset + const duration = parseInt(allDayClone.dataset.duration || '60'); + const endMinutes = snappedStartMinutes + duration; + + // Create dates with target column date + const columnDate = new Date(targetColumn + 'T00:00:00'); + const startDate = new Date(columnDate); + startDate.setMinutes(snappedStartMinutes); + + const endDate = new Date(columnDate); + endDate.setMinutes(endMinutes); + + // Create CalendarEvent object for helper methods + const tempEvent: CalendarEvent = { + id: eventId, + title: eventTitle, + start: startDate, + end: endDate, + type: eventType, + allDay: false, + syncStatus: 'synced', + metadata: { + duration: duration + } + }; + + // Create timed event element + const timedEvent = document.createElement('swp-event'); + timedEvent.dataset.eventId = eventId; + timedEvent.dataset.title = eventTitle; + timedEvent.dataset.type = eventType; + timedEvent.dataset.start = startDate.toISOString(); + timedEvent.dataset.end = endDate.toISOString(); + timedEvent.dataset.duration = duration.toString(); + timedEvent.dataset.originalDuration = duration.toString(); + + // Create inner structure using helper method + timedEvent.innerHTML = this.createEventInnerStructure(tempEvent); + + // Apply drag styling and positioning + this.applyDragStyling(timedEvent); + const eventHeight = (duration / 60) * hourHeight - 3; + timedEvent.style.height = `${eventHeight}px`; + timedEvent.style.top = `${targetY}px`; + + // Remove all-day element + allDayClone.remove(); + + // Add timed event to events layer + eventsLayer.appendChild(timedEvent); + + // Update reference + this.draggedClone = timedEvent; + } /** * Fade out and remove element @@ -872,26 +1008,14 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { eventElement.dataset.type = event.type; eventElement.dataset.duration = event.metadata?.duration?.toString() || '60'; - // Calculate position based on time + // Calculate and apply position based on time const position = this.calculateEventPosition(event); - eventElement.style.position = 'absolute'; - eventElement.style.top = `${position.top + 1}px`; - eventElement.style.height = `${position.height - 3}px`; //adjusted so bottom does not cover horizontal time lines. + this.applyEventPositioning(eventElement, position.top + 1, position.height - 3); // Color is now handled by CSS classes based on data-type attribute - // Format time for display using unified method - const startTime = this.formatTime(event.start); - const endTime = this.formatTime(event.end); - - // Calculate duration in minutes - const durationMinutes = (event.end.getTime() - event.start.getTime()) / (1000 * 60); - - // Create event content - eventElement.innerHTML = ` - ${startTime} - ${endTime} - ${event.title} - `; + // Create event content using helper method + eventElement.innerHTML = this.createEventInnerStructure(event); // Setup resize handles on first mouseover only eventElement.addEventListener('mouseover', () => { diff --git a/src/renderers/GridRenderer.ts b/src/renderers/GridRenderer.ts index b3848b4..3a8c38a 100644 --- a/src/renderers/GridRenderer.ts +++ b/src/renderers/GridRenderer.ts @@ -11,7 +11,6 @@ import { DateCalculator } from '../utils/DateCalculator'; * Optimized to reduce redundant DOM operations and improve performance */ export class GridRenderer { - private headerEventListener: ((event: Event) => void) | null = null; private cachedGridContainer: HTMLElement | null = null; private cachedCalendarHeader: HTMLElement | null = null; private cachedTimeAxis: HTMLElement | null = null; @@ -158,8 +157,8 @@ export class GridRenderer { // Always ensure all-day containers exist for all days headerRenderer.ensureAllDayContainers(calendarHeader); - // Setup optimized event listener - this.setupOptimizedHeaderEventListener(calendarHeader); + // Setup only grid-related event listeners + this.setupGridEventListeners(); } /** @@ -209,83 +208,74 @@ export class GridRenderer { } /** - * Setup optimized event delegation listener with better performance + * Setup grid-only event listeners (column events) */ - private setupOptimizedHeaderEventListener(calendarHeader: HTMLElement): void { - // Remove existing listener if any - if (this.headerEventListener) { - calendarHeader.removeEventListener('mouseover', this.headerEventListener); - } + private setupGridEventListeners(): void { + // Setup grid body mouseover listener for all-day to timed conversion + this.setupGridBodyMouseOver(); + } - // Create optimized listener with throttling + /** + * Setup grid body mouseover listener for all-day to timed conversion + */ + private setupGridBodyMouseOver(): void { + const grid = this.cachedGridContainer; + if (!grid) return; + + const columnContainer = grid.querySelector('swp-day-columns'); + if (!columnContainer) return; + + // Throttle for better performance let lastEmitTime = 0; const throttleDelay = 16; // ~60fps - - this.headerEventListener = (event) => { + + const gridBodyEventListener = (event: Event) => { const now = Date.now(); if (now - lastEmitTime < throttleDelay) { - return; // Throttle events for better performance + return; } lastEmitTime = now; - + const target = event.target as HTMLElement; + const dayColumn = target.closest('swp-day-column'); - // Optimized element detection - const dayHeader = target.closest('swp-day-header'); - const allDayContainer = target.closest('swp-allday-container'); - - if (dayHeader || allDayContainer) { - let hoveredElement: HTMLElement; - let targetDate: string | undefined; - - if (dayHeader) { - hoveredElement = dayHeader as HTMLElement; - targetDate = hoveredElement.dataset.date; - } else if (allDayContainer) { - hoveredElement = allDayContainer as HTMLElement; + if (dayColumn) { + const targetColumn = (dayColumn as HTMLElement).dataset.date; + if (targetColumn) { + // Calculate Y position relative to the column + const columnRect = dayColumn.getBoundingClientRect(); + const mouseY = (event as MouseEvent).clientY; + const targetY = mouseY - columnRect.top; - // Optimized day calculation using cached header rect - const headerRect = calendarHeader.getBoundingClientRect(); - const dayHeaders = calendarHeader.querySelectorAll('swp-day-header'); - const mouseX = (event as MouseEvent).clientX - headerRect.left; - const dayWidth = headerRect.width / dayHeaders.length; - const dayIndex = Math.max(0, Math.min(dayHeaders.length - 1, Math.floor(mouseX / dayWidth))); - - const targetDayHeader = dayHeaders[dayIndex] as HTMLElement; - targetDate = targetDayHeader?.dataset.date; - } else { - return; + eventBus.emit('column:mouseover', { + targetColumn, + targetY + }); } - - // Get header renderer once and cache - const calendarType = calendarConfig.getCalendarMode(); - const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType); - - eventBus.emit('header:mouseover', { - element: hoveredElement, - targetDate, - headerRenderer - }); } }; - - // Add the optimized listener - calendarHeader.addEventListener('mouseover', this.headerEventListener); + + columnContainer.addEventListener('mouseover', gridBodyEventListener); + + // Store reference for cleanup + (this as any).gridBodyEventListener = gridBodyEventListener; + (this as any).cachedColumnContainer = columnContainer; } /** * 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); + // Clean up grid-only event listeners + if ((this as any).gridBodyEventListener && (this as any).cachedColumnContainer) { + (this as any).cachedColumnContainer.removeEventListener('mouseover', (this as any).gridBodyEventListener); } // Clear cached references this.cachedGridContainer = null; this.cachedCalendarHeader = null; this.cachedTimeAxis = null; - this.headerEventListener = null; + (this as any).gridBodyEventListener = null; + (this as any).cachedColumnContainer = null; } } \ No newline at end of file diff --git a/src/renderers/NavigationRenderer.ts b/src/renderers/NavigationRenderer.ts index 300de31..1af5bfe 100644 --- a/src/renderers/NavigationRenderer.ts +++ b/src/renderers/NavigationRenderer.ts @@ -4,7 +4,6 @@ import { calendarConfig } from '../core/CalendarConfig'; import { DateCalculator } from '../utils/DateCalculator'; import { EventRenderingService } from './EventRendererManager'; import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; -import { eventBus } from '../core/EventBus'; /** * NavigationRenderer - Handles DOM rendering for navigation containers @@ -12,8 +11,6 @@ import { eventBus } from '../core/EventBus'; */ export class NavigationRenderer { private eventBus: IEventBus; - private dateCalculator: DateCalculator; - private eventRenderer: EventRenderingService; // Cached DOM elements to avoid redundant queries private cachedWeekNumberElement: HTMLElement | null = null; @@ -21,9 +18,7 @@ export class NavigationRenderer { constructor(eventBus: IEventBus, eventRenderer: EventRenderingService) { this.eventBus = eventBus; - this.eventRenderer = eventRenderer; DateCalculator.initialize(calendarConfig); - this.dateCalculator = new DateCalculator(); this.setupEventListeners(); } @@ -202,9 +197,6 @@ export class NavigationRenderer { const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarConfig.getCalendarMode()); headerRenderer.ensureAllDayContainers(header as HTMLElement); - // Add event delegation listener for drag & drop functionality - this.setupHeaderEventListener(header as HTMLElement); - // Render day columns for target week dates.forEach(date => { const column = document.createElement('swp-day-column'); @@ -217,55 +209,6 @@ export class NavigationRenderer { }); } - /** - * Setup event delegation listener for header mouseover (same logic as GridRenderer) - */ - private setupHeaderEventListener(calendarHeader: HTMLElement): void { - calendarHeader.addEventListener('mouseover', (event) => { - const target = event.target as HTMLElement; - - // Check what was hovered - could be day-header OR all-day-container - const dayHeader = target.closest('swp-day-header'); - const allDayContainer = target.closest('swp-allday-container'); - - if (dayHeader || allDayContainer) { - let hoveredElement: HTMLElement; - let targetDate: string | undefined; - - if (dayHeader) { - hoveredElement = dayHeader as HTMLElement; - targetDate = hoveredElement.dataset.date; - } else if (allDayContainer) { - // For all-day areas, we need to determine which day column we're over - hoveredElement = allDayContainer as HTMLElement; - - // Calculate which day we're hovering over based on mouse position - const headerRect = calendarHeader.getBoundingClientRect(); - const dayHeaders = calendarHeader.querySelectorAll('swp-day-header'); - const mouseX = (event as MouseEvent).clientX - headerRect.left; - const dayWidth = headerRect.width / dayHeaders.length; - const dayIndex = Math.floor(mouseX / dayWidth); - - const targetDayHeader = dayHeaders[dayIndex] as HTMLElement; - targetDate = targetDayHeader?.dataset.date; - } else { - return; // No valid element found - } - - - // Get the header renderer for addToAllDay functionality - const calendarType = calendarConfig.getCalendarMode(); - const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType); - - eventBus.emit('header:mouseover', { - element: hoveredElement, - targetDate, - headerRenderer - }); - } - }); - } - /** * Public cleanup method for cached elements */