diff --git a/src/elements/SwpEventElement.ts b/src/elements/SwpEventElement.ts index e34a92e..772d9b9 100644 --- a/src/elements/SwpEventElement.ts +++ b/src/elements/SwpEventElement.ts @@ -101,6 +101,83 @@ export class SwpEventElement extends BaseEventElement { return new SwpEventElement(event); } + /** + * Create a clone of this SwpEventElement with "clone-" prefix + */ + public createClone(): SwpEventElement { + // Clone the underlying DOM element + const clonedElement = this.element.cloneNode(true) as HTMLElement; + + // Create new SwpEventElement instance from the cloned DOM + const clonedSwpEvent = SwpEventElement.fromExistingElement(clonedElement); + + // Apply "clone-" prefix to ID + clonedSwpEvent.updateEventId(`clone-${this.event.id}`); + + // Cache original duration for drag operations + const originalDuration = this.getOriginalEventDuration(); + clonedSwpEvent.element.dataset.originalDuration = originalDuration.toString(); + + // Set height from original element + clonedSwpEvent.element.style.height = this.element.style.height || `${this.element.getBoundingClientRect().height}px`; + + return clonedSwpEvent; + } + + /** + * Factory method to create SwpEventElement from existing DOM element + */ + public static fromExistingElement(element: HTMLElement): SwpEventElement { + // Extract CalendarEvent data from DOM element + const event = this.extractCalendarEventFromElement(element); + + // Create new instance but replace the created element with the existing one + const swpEvent = new SwpEventElement(event); + swpEvent.element = element; + + return swpEvent; + } + + /** + * Update the event ID in both the CalendarEvent and DOM element + */ + private updateEventId(newId: string): void { + this.event.id = newId; + this.element.dataset.eventId = newId; + } + + /** + * Extract original event duration from DOM element + */ + private getOriginalEventDuration(): number { + const timeElement = this.element.querySelector('swp-event-time'); + if (timeElement) { + const duration = timeElement.getAttribute('data-duration'); + if (duration) { + return parseInt(duration); + } + } + return 60; // Fallback + } + + /** + * Extract CalendarEvent from DOM element + */ + private static extractCalendarEventFromElement(element: HTMLElement): CalendarEvent { + return { + id: element.dataset.eventId || '', + title: element.dataset.title || '', + start: new Date(element.dataset.start || ''), + end: new Date(element.dataset.end || ''), + type: element.dataset.type || 'work', + allDay: false, + syncStatus: 'synced', + metadata: { + duration: element.dataset.duration + } + }; + } + /** * Factory method to convert an all-day HTML element to a timed SwpEventElement */ diff --git a/src/managers/AllDayManager.ts b/src/managers/AllDayManager.ts index 6c632f6..e4d0d78 100644 --- a/src/managers/AllDayManager.ts +++ b/src/managers/AllDayManager.ts @@ -74,14 +74,26 @@ export class AllDayManager { }); eventBus.on('drag:end', (event) => { - const { eventId, finalPosition } = (event as CustomEvent).detail; + + const { eventId, finalColumn, finalY, dropTarget } = (event as CustomEvent).detail; + + if (dropTarget != 'SWP-DAY-HEADER')//we are not inside the swp-day-header, so just ignore. + return; + + console.log('🎬 AllDayManager: Received drag:end', { + eventId: eventId, + finalColumn: finalColumn, + finalY: finalY + }); // Check if this was an all-day event const originalElement = document.querySelector(`swp-allday-container swp-allday-event[data-event-id="${eventId}"]`); const dragClone = document.querySelector(`swp-allday-container swp-allday-event[data-event-id="clone-${eventId}"]`); + + console.log('🎯 AllDayManager: Ending drag for all-day event', { eventId }); - this.handleDragEnd(originalElement as HTMLElement, dragClone as HTMLElement, finalPosition); + this.handleDragEnd(originalElement as HTMLElement, dragClone as HTMLElement, finalColumn); }); } diff --git a/src/managers/DragDropManager.ts b/src/managers/DragDropManager.ts index c9f6d62..da059b6 100644 --- a/src/managers/DragDropManager.ts +++ b/src/managers/DragDropManager.ts @@ -172,8 +172,11 @@ export class DragDropManager { const mousePosition = { x: this.lastMousePosition.x, y: this.lastMousePosition.y }; const column = this.getColumnDateFromX(mousePosition.x); + // Find the actual dragged element + const draggedElement = document.querySelector(`[data-event-id="${this.draggedEventId}"]`) as HTMLElement; + this.eventBus.emit('drag:convert-to-time_event', { - draggedEventId: this.draggedEventId, + draggedElement: draggedElement, mousePosition: mousePosition, column: column }); @@ -319,10 +322,14 @@ export class DragDropManager { // Use consolidated position calculation const positionData = this.calculateDragPosition(finalPosition); + // Detect drop target (swp-day-column or swp-day-header) + const dropTarget = this.detectDropTarget(finalPosition); + console.log('🎯 DragDropManager: Emitting drag:end', { eventId: eventId, finalColumn: positionData.column, finalY: positionData.snappedY, + dropTarget: dropTarget, isDragStarted: isDragStarted }); @@ -330,7 +337,8 @@ export class DragDropManager { eventId: eventId, finalPosition, finalColumn: positionData.column, - finalY: positionData.snappedY + finalY: positionData.snappedY, + target: dropTarget }); } else { // This was just a click - emit click event instead @@ -411,9 +419,6 @@ export class DragDropManager { // Sorter efter x-position (fra venstre til højre) this.columnBoundsCache.sort((a, b) => a.left - b.left); - console.log('📏 DragDropManager: Updated column bounds cache', { - columns: this.columnBoundsCache.length - }); } /** @@ -592,6 +597,28 @@ export class DragDropManager { return allDayElement !== null; } + /** + * Detect drop target - whether dropped in swp-day-column or swp-day-header + */ + private detectDropTarget(position: Position): 'swp-day-column' | 'swp-day-header' | null { + const elementAtPosition = document.elementFromPoint(position.x, position.y); + if (!elementAtPosition) return null; + + // Traverse up the DOM tree to find the target container + let currentElement = elementAtPosition as HTMLElement; + while (currentElement && currentElement !== document.body) { + if (currentElement.tagName === 'SWP-DAY-HEADER') { + return 'swp-day-header'; + } + if (currentElement.tagName === 'SWP-DAY-COLUMN') { + return 'swp-day-column'; + } + currentElement = currentElement.parentElement as HTMLElement; + } + + return null; + } + /** * Clean up all resources and event listeners */ diff --git a/src/managers/HeaderManager.ts b/src/managers/HeaderManager.ts index f7a7815..427b66a 100644 --- a/src/managers/HeaderManager.ts +++ b/src/managers/HeaderManager.ts @@ -44,22 +44,15 @@ export class HeaderManager { * Setup header drag event listeners - REFACTORED to use mouseenter */ public setupHeaderDragListeners(): void { - const calendarHeader = this.getCalendarHeader(); - if (!calendarHeader) return; + if (!this.getCalendarHeader()) return; console.log('🎯 HeaderManager: Setting up drag listeners with mouseenter'); - // Track last processed date to avoid duplicates - let lastProcessedDate: string | null = null; - let lastProcessedTime = 0; // Use mouseenter instead of mouseover to avoid continuous firing this.headerEventListener = (event: Event) => { - // OPTIMIZED: Check for active drag operation FIRST before doing any other work - const isDragActive = document.querySelector('.dragging') !== null; - if (!isDragActive) { - // Ingen drag operation, spring resten af funktionen over + if (!document.querySelector('.dragging') !== null) { return; } @@ -114,9 +107,8 @@ export class HeaderManager { }); }; - // Use mouseenter with capture to catch events early - calendarHeader.addEventListener('mouseenter', this.headerEventListener, true); - calendarHeader.addEventListener('mouseleave', this.headerMouseLeaveListener); + this.getCalendarHeader()?.addEventListener('mouseenter', this.headerEventListener, true); + this.getCalendarHeader()?.addEventListener('mouseleave', this.headerMouseLeaveListener); console.log('✅ HeaderManager: Event listeners attached (mouseenter + mouseleave)'); } diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index 1184c6f..ba05ba1 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -16,6 +16,13 @@ import { PositionUtils } from '../utils/PositionUtils'; export interface EventRendererStrategy { renderEvents(events: CalendarEvent[], container: HTMLElement): void; clearEvents(container?: HTMLElement): void; + handleDragStart?(originalElement: HTMLElement, eventId: string, mouseOffset: any, column: string): void; + handleDragMove?(eventId: string, snappedY: number, column: string, mouseOffset: any): void; + handleDragAutoScroll?(eventId: string, snappedY: number): void; + handleDragEnd?(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: string, finalY: number): void; + handleEventClick?(eventId: string, originalElement: HTMLElement): void; + handleColumnChange?(eventId: string, newColumn: string): void; + handleNavigationCompleted?(): void; } /** @@ -23,11 +30,11 @@ export interface EventRendererStrategy { */ export abstract class BaseEventRenderer implements EventRendererStrategy { protected dateCalculator: DateCalculator; - + // Drag and drop state private draggedClone: HTMLElement | null = null; private originalEvent: HTMLElement | null = null; - + // Resize manager constructor(dateCalculator?: DateCalculator) { @@ -70,12 +77,12 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { const remainingEvents = events.slice(index + 1); const overlappingEvents = this.overlapDetector.resolveOverlap(currentEvent, remainingEvents); - + if (overlappingEvents.length > 0) { // Der er overlaps - opret stack links const result = this.overlapDetector.decorateWithStackLinks(currentEvent, overlappingEvents); this.renderOverlappingEvents(result, container); - + // Marker alle events i overlap gruppen som processeret overlappingEvents.forEach(event => processedEvents.add(event.id)); } else { @@ -90,90 +97,14 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { /** * Setup listeners for drag events from DragDropManager + * NOTE: Event listeners moved to EventRendererManager for better separation of concerns */ protected setupDragEventListeners(): void { - // Handle drag start - eventBus.on('drag:start', (event) => { - const { eventId, mouseOffset, column } = (event as CustomEvent).detail; - // Find element dynamically - const originalElement = document.querySelector(`swp-event[data-event-id="${eventId}"]`) as HTMLElement; - if (originalElement) { - this.handleDragStart(originalElement, eventId, mouseOffset, column); - } - }); - - // Handle drag move - eventBus.on('drag:move', (event) => { - const { eventId, snappedY, column, mouseOffset } = (event as CustomEvent).detail; - this.handleDragMove(eventId, snappedY, column, mouseOffset); - }); - - // Handle drag auto-scroll (when dragging near edges triggers scroll) - eventBus.on('drag:auto-scroll', (event) => { - const { eventId, snappedY } = (event as CustomEvent).detail; - if (!this.draggedClone) return; - - // Update position directly using the calculated snapped position - this.draggedClone.style.top = snappedY + 'px'; - - // Update timestamp display - this.updateCloneTimestamp(this.draggedClone, snappedY); - }); - - // Handle drag end - eventBus.on('drag:end', (event) => { - const { eventId, finalColumn, finalY } = (event as CustomEvent).detail; - - console.log('🎬 EventRenderer: Received drag:end', { - eventId: eventId, - finalColumn: finalColumn, - finalY: finalY - }); - - // Find element dynamically - could be swp-event or swp-allday-event - let originalElement = document.querySelector(`swp-event[data-event-id="${eventId}"]`) as HTMLElement; - let elementType = 'day-event'; - if (!originalElement) { - originalElement = document.querySelector(`swp-allday-event[data-event-id="${eventId}"]`) as HTMLElement; - elementType = 'all-day-event'; - } - - console.log('🔍 EventRenderer: Found element', { - elementType: elementType, - found: !!originalElement, - tagName: originalElement?.tagName - }); - - if (originalElement) { - this.handleDragEnd(eventId, originalElement, finalColumn, finalY); - } - }); - - // Handle click (when drag threshold not reached) - eventBus.on('event:click', (event) => { - const { eventId } = (event as CustomEvent).detail; - // Find element dynamically - let originalElement = document.querySelector(`swp-event[data-event-id="${eventId}"]`) as HTMLElement; - if (!originalElement) { - originalElement = document.querySelector(`swp-allday-event[data-event-id="${eventId}"]`) as HTMLElement; - } - this.handleEventClick(eventId, originalElement); - }); - - // Handle column change - eventBus.on('drag:column-change', (event) => { - const { eventId, newColumn } = (event as CustomEvent).detail; - this.handleColumnChange(eventId, newColumn); - }); - - - // Handle navigation period change (when slide animation completes) - eventBus.on(CoreEvents.NAVIGATION_COMPLETED, () => { - // Animate all-day height after navigation completes - }); + // All event listeners now handled by EventRendererManager + // This method kept for backward compatibility but does nothing } - - + + /** * Cleanup method for proper resource management */ @@ -182,23 +113,6 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { this.originalEvent = null; } - /** - * Get original event duration from data-duration attribute - */ - private getOriginalEventDuration(originalEvent: HTMLElement): number { - // Find the swp-event-time element with data-duration attribute - const timeElement = originalEvent.querySelector('swp-event-time'); - if (timeElement) { - const duration = timeElement.getAttribute('data-duration'); - if (duration) { - const durationMinutes = parseInt(duration); - return durationMinutes; - } - } - - // Fallback to 60 minutes if attribute not found - return 60; - } /** * Apply common drag styling to an element @@ -207,89 +121,41 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { element.classList.add('dragging'); } - /** - * Create event inner structure (swp-event-time and swp-event-title) - */ - private createEventInnerStructure(event: CalendarEvent): string { - const timeRange = TimeFormatter.formatTimeRange(event.start, event.end); - const durationMinutes = (event.end.getTime() - event.start.getTime()) / (1000 * 60); - - return ` - ${timeRange} - ${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 - */ - private createEventClone(originalEvent: HTMLElement): HTMLElement { - const clone = originalEvent.cloneNode(true) as HTMLElement; - - // Prefix ID with "clone-" - const originalId = originalEvent.dataset.eventId; - if (originalId) { - clone.dataset.eventId = `clone-${originalId}`; - } - - // Get and cache original duration from data-duration attribute - const originalDurationMinutes = this.getOriginalEventDuration(originalEvent); - clone.dataset.originalDuration = originalDurationMinutes.toString(); - - // Apply common drag styling - this.applyDragStyling(clone); - - // Set height from original event - clone.style.height = originalEvent.style.height || `${originalEvent.getBoundingClientRect().height}px`; - - return clone; - } - /** * 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; + if (clone.dataset.allDay == "true") return; const gridSettings = calendarConfig.getGridSettings(); const hourHeight = gridSettings.hourHeight; const dayStartHour = gridSettings.dayStartHour; const snapInterval = gridSettings.snapInterval; - + // Calculate minutes from grid start (not from midnight) const minutesFromGridStart = (snappedY / hourHeight) * 60; - + // Add dayStartHour offset to get actual time const actualStartMinutes = (dayStartHour * 60) + minutesFromGridStart; - + // Snap to interval const snappedStartMinutes = Math.round(actualStartMinutes / snapInterval) * snapInterval; - + // Use cached original duration (no recalculation) const cachedDuration = parseInt(clone.dataset.originalDuration || '60'); const endTotalMinutes = snappedStartMinutes + cachedDuration; - + // Update dataset with reference date for performance const referenceDate = new Date('1970-01-01T00:00:00'); const startDate = new Date(referenceDate); startDate.setMinutes(startDate.getMinutes() + snappedStartMinutes); - + const endDate = new Date(referenceDate); endDate.setMinutes(endDate.getMinutes() + endTotalMinutes); - + clone.dataset.start = startDate.toISOString(); clone.dataset.end = endDate.toISOString(); // Update display @@ -300,18 +166,25 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { timeElement.textContent = `${startTime} - ${endTime}`; } } - + /** * Handle drag start event */ - private handleDragStart(originalElement: HTMLElement, eventId: string, mouseOffset: any, column: string): void { + public handleDragStart(originalElement: HTMLElement, eventId: string, mouseOffset: any, column: string): void { this.originalEvent = originalElement; - + // Remove stacking styling during drag will be handled by new system + + // Create SwpEventElement from existing DOM element and clone it + const originalSwpEvent = SwpEventElement.fromExistingElement(originalElement); + const clonedSwpEvent = originalSwpEvent.createClone(); - // Create clone - this.draggedClone = this.createEventClone(originalElement); + // Get the cloned DOM element + this.draggedClone = clonedSwpEvent.getElement(); + // Apply drag styling + this.applyDragStyling(this.draggedClone); + // Add to current column's events layer (not directly to column) const columnElement = document.querySelector(`swp-day-column[data-date="${column}"]`); if (columnElement) { @@ -323,33 +196,46 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { columnElement.appendChild(this.draggedClone); } } - + // Make original semi-transparent originalElement.style.opacity = '0.3'; originalElement.style.userSelect = 'none'; - + } - + /** * Handle drag move event */ - private handleDragMove(eventId: string, snappedY: number, column: string, mouseOffset: any): void { + public handleDragMove(eventId: string, snappedY: number, column: string, mouseOffset: any): void { if (!this.draggedClone) return; - + // Update position this.draggedClone.style.top = snappedY + 'px'; - + // Update timestamp display this.updateCloneTimestamp(this.draggedClone, snappedY); - + } - + + /** + * Handle drag auto-scroll event + */ + public handleDragAutoScroll(eventId: string, snappedY: number): void { + if (!this.draggedClone) return; + + // Update position directly using the calculated snapped position + this.draggedClone.style.top = snappedY + 'px'; + + // Update timestamp display + this.updateCloneTimestamp(this.draggedClone, snappedY); + } + /** * Handle column change during drag */ - private handleColumnChange(eventId: string, newColumn: string): void { + public handleColumnChange(eventId: string, newColumn: string): void { if (!this.draggedClone) return; - + // Move clone to new column's events layer const newColumnElement = document.querySelector(`swp-day-column[data-date="${newColumn}"]`); if (newColumnElement) { @@ -362,24 +248,24 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { } } } - + /** * Handle drag end event */ - private handleDragEnd(eventId: string, originalElement: HTMLElement, finalColumn: string, finalY: number): void { - - if (!this.draggedClone || !this.originalEvent) { - console.warn('Missing draggedClone or originalEvent'); + public handleDragEnd(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: string, finalY: number): void { + + if (!draggedClone || !originalElement) { + console.warn('Missing draggedClone or originalElement'); return; } - + // Check om original event var del af en stack - const originalStackLink = this.originalEvent.dataset.stackLink; + const originalStackLink = originalElement.dataset.stackLink; if (originalStackLink) { try { const stackData = JSON.parse(originalStackLink); - + // Saml ALLE event IDs fra hele stack chain const allStackEventIds: Set = new Set(); @@ -392,10 +278,10 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { try { const prevLinkData = JSON.parse(prevElement.dataset.stackLink); traverseStack(prevLinkData, visitedIds); - } catch (e) {} + } catch (e) { } } } - + if (linkData.next && !visitedIds.has(linkData.next)) { visitedIds.add(linkData.next); const nextElement = document.querySelector(`swp-time-grid [data-event-id="${linkData.next}"]`) as HTMLElement; @@ -403,7 +289,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { try { const nextLinkData = JSON.parse(nextElement.dataset.stackLink); traverseStack(nextLinkData, visitedIds); - } catch (e) {} + } catch (e) { } } } }; @@ -425,17 +311,17 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { if (!container) { container = element.closest('swp-events-layer') as HTMLElement; } - + const event = this.elementToCalendarEvent(element); if (event) { stackEvents.push(event); } - + // Fjern elementet element.remove(); } }); - + // Re-render stack events hvis vi fandt nogle if (stackEvents.length > 0 && container) { this.handleEventOverlaps(stackEvents, container); @@ -444,93 +330,100 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { console.warn('Failed to parse stackLink data:', e); } } - + // Remove original event from any existing groups first - this.removeEventFromExistingGroups(this.originalEvent); - + this.removeEventFromExistingGroups(originalElement); + // Fade out original - this.fadeOutAndRemove(this.originalEvent); - + this.fadeOutAndRemove(originalElement); + // Remove clone prefix and normalize clone to be a regular event - const cloneId = this.draggedClone.dataset.eventId; + const cloneId = draggedClone.dataset.eventId; if (cloneId && cloneId.startsWith('clone-')) { - this.draggedClone.dataset.eventId = cloneId.replace('clone-', ''); + draggedClone.dataset.eventId = cloneId.replace('clone-', ''); } - + // Fully normalize the clone to be a regular event - this.draggedClone.classList.remove('dragging'); + draggedClone.classList.remove('dragging'); // Behold z-index hvis det er et stacked event - + // Update dataset with new times after successful drop (only for timed events) - if (this.draggedClone.dataset.displayType !== 'allday') { - const newEvent = this.elementToCalendarEvent(this.draggedClone); + if (draggedClone.dataset.displayType !== 'allday') { + const newEvent = this.elementToCalendarEvent(draggedClone); if (newEvent) { - this.draggedClone.dataset.start = newEvent.start.toISOString(); - this.draggedClone.dataset.end = newEvent.end.toISOString(); + draggedClone.dataset.start = newEvent.start.toISOString(); + draggedClone.dataset.end = newEvent.end.toISOString(); } } - + // Detect overlaps with other events in the target column and reposition if needed - this.handleDragDropOverlaps(this.draggedClone, finalColumn); - + this.handleDragDropOverlaps(draggedClone, finalColumn); + // Fjern stackLink data fra dropped element - if (this.draggedClone.dataset.stackLink) { - delete this.draggedClone.dataset.stackLink; + if (draggedClone.dataset.stackLink) { + delete draggedClone.dataset.stackLink; } - - // Clean up + + // Clean up instance state (no longer needed since we get elements as parameters) this.draggedClone = null; this.originalEvent = null; - + } - + /** * Handle event click (when drag threshold not reached) */ - private handleEventClick(eventId: string, originalElement: HTMLElement): void { + public handleEventClick(eventId: string, originalElement: HTMLElement): void { console.log('handleEventClick:', eventId); - + // Clean up any drag artifacts from failed drag attempt if (this.draggedClone) { this.draggedClone.classList.remove('dragging'); this.draggedClone.remove(); this.draggedClone = null; } - + // Restore original element styling if it was modified if (this.originalEvent) { this.originalEvent.style.opacity = ''; this.originalEvent.style.userSelect = ''; this.originalEvent = null; } - + // Emit a clean click event for other components to handle eventBus.emit('event:clicked', { eventId: eventId, element: originalElement }); } - + + /** + * Handle navigation completed event + */ + public handleNavigationCompleted(): void { + // Default implementation - can be overridden by subclasses + } + /** * Handle overlap detection and re-rendering after drag-drop */ private handleDragDropOverlaps(droppedElement: HTMLElement, targetColumn: string): void { const targetColumnElement = document.querySelector(`swp-day-column[data-date="${targetColumn}"]`); if (!targetColumnElement) return; - + const eventsLayer = targetColumnElement.querySelector('swp-events-layer') as HTMLElement; if (!eventsLayer) return; - + // Convert dropped element to CalendarEvent with new position const droppedEvent = this.elementToCalendarEvent(droppedElement); if (!droppedEvent) return; - + // Get existing events in the column (excluding the dropped element) const existingEvents = this.getEventsInColumn(eventsLayer, droppedElement.dataset.eventId); - + // Find overlaps with the dropped event const overlappingEvents = this.overlapDetector.resolveOverlap(droppedEvent, existingEvents); - + if (overlappingEvents.length > 0) { // Remove only affected events from DOM const affectedEventIds = [droppedEvent.id, ...overlappingEvents.map(e => e.id)]; @@ -540,7 +433,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { el.remove(); } }); - + // Re-render affected events with overlap handling const affectedEvents = [droppedEvent, ...overlappingEvents]; this.handleEventOverlaps(affectedEvents, eventsLayer); @@ -556,22 +449,22 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { private getEventsInColumn(eventsLayer: HTMLElement, excludeEventId?: string): CalendarEvent[] { const eventElements = eventsLayer.querySelectorAll('swp-event'); const events: CalendarEvent[] = []; - + eventElements.forEach(el => { const element = el as HTMLElement; const eventId = element.dataset.eventId; - + // Skip the excluded event (e.g., the dropped event) if (excludeEventId && eventId === excludeEventId) { return; } - + const event = this.elementToCalendarEvent(element); if (event) { events.push(event); } }); - + return events; } @@ -584,23 +477,6 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { // No need to manually track and remove from groups } - /** - * Update element's dataset with new times after successful drop - */ - private updateElementDataset(element: HTMLElement, event: CalendarEvent): void { - element.dataset.start = event.start.toISOString(); - element.dataset.end = event.end.toISOString(); - - // Update the time display - const timeElement = element.querySelector('swp-event-time'); - if (timeElement) { - const timeRange = TimeFormatter.formatTimeRange(event.start, event.end); - timeElement.textContent = timeRange; - } - } - - - /** * Convert DOM element to CalendarEvent - handles both normal and 1970 reference dates */ @@ -610,21 +486,21 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { const type = element.dataset.type; const start = element.dataset.start; const end = element.dataset.end; - + if (!eventId || !title || !type || !start || !end) { return null; } - + let startDate = new Date(start); let endDate = new Date(end); - + // Check if we have 1970 reference date (from drag operations) if (startDate.getFullYear() === 1970) { // Find the parent column to get the actual date const columnElement = element.closest('swp-day-column') as HTMLElement; if (columnElement && columnElement.dataset.date) { const columnDate = new Date(columnElement.dataset.date); - + // Keep the time portion from the 1970 dates, but use the column's date startDate = new Date( columnDate.getFullYear(), @@ -633,7 +509,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { startDate.getHours(), startDate.getMinutes() ); - + endDate = new Date( columnDate.getFullYear(), columnDate.getMonth(), @@ -643,7 +519,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { ); } } - + return { id: eventId, title: title, @@ -657,33 +533,33 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { } }; } - + /** * Handle conversion to all-day event */ - + /** * Fade out and remove element */ private fadeOutAndRemove(element: HTMLElement): void { element.style.transition = 'opacity 0.3s ease-out'; element.style.opacity = '0'; - + setTimeout(() => { element.remove(); }, 300); } - - + + renderEvents(events: CalendarEvent[], container: HTMLElement): void { - + // NOTE: Removed clearEvents() to support sliding animation // With sliding animation, multiple grid containers exist simultaneously // clearEvents() would remove events from all containers, breaking the animation // Events are now rendered directly into the new container without clearing // Only handle regular (non-all-day) events - + // Find columns in the specific container for regular events @@ -691,7 +567,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { columns.forEach(column => { const columnEvents = this.getEventsForColumn(column, events); - + const eventsLayer = column.querySelector('swp-events-layer'); if (eventsLayer) { // NY TILGANG: Kald vores nye overlap handling @@ -708,14 +584,14 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { protected renderEvent(event: CalendarEvent): HTMLElement { const swpEvent = SwpEventElement.fromCalendarEvent(event); const eventElement = swpEvent.getElement(); - + // Setup resize handles on first mouseover only eventElement.addEventListener('mouseover', () => { if (eventElement.dataset.hasResizeHandlers !== 'true') { eventElement.dataset.hasResizeHandlers = 'true'; } }, { once: true }); - + return eventElement; } @@ -729,7 +605,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { const existingEvents = container ? container.querySelectorAll(selector) : document.querySelectorAll(selector); - + existingEvents.forEach(event => event.remove()); } @@ -743,16 +619,16 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { for (const [eventId, stackLink] of result.stackLinks.entries()) { const event = result.overlappingEvents.find(e => e.id === eventId); if (!event) continue; - + const element = this.renderEvent(event); - + // Gem stack link information på DOM elementet element.dataset.stackLink = JSON.stringify({ prev: stackLink.prev, next: stackLink.next, stackLevel: stackLink.stackLevel }); - + // Check om dette event deler kolonne med foregående (samme start tid) if (stackLink.prev) { const prevEvent = result.overlappingEvents.find(e => e.id === stackLink.prev); @@ -767,7 +643,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { // Første event i stack this.new_applyStackStyling(element, stackLink.stackLevel); } - + container.appendChild(element); } } @@ -817,8 +693,8 @@ export class DateEventRenderer extends BaseEventRenderer { const columnEvents = events.filter(event => { const eventDateStr = DateCalculator.formatISODate(event.start); const matches = eventDateStr === columnDate; - - + + return matches; }); diff --git a/src/renderers/EventRendererManager.ts b/src/renderers/EventRendererManager.ts index 2e65e8b..9fb71b8 100644 --- a/src/renderers/EventRendererManager.ts +++ b/src/renderers/EventRendererManager.ts @@ -71,25 +71,18 @@ export class EventRenderingService { this.handleViewChanged(event as CustomEvent); }); - // Simple drag:end listener to clean up day event clones - this.eventBus.on('drag:end', (event: Event) => { - const { eventId } = (event as CustomEvent).detail; - const dayEventClone = document.querySelector(`swp-event[data-event-id="clone-${eventId}"]`); - - if (dayEventClone) { - dayEventClone.remove(); - } - }); + // Handle all drag events and delegate to appropriate renderer + this.setupDragEventListeners(); // Listen for conversion from all-day event to time event this.eventBus.on('drag:convert-to-time_event', (event: Event) => { - const { draggedEventId, mousePosition, column } = (event as CustomEvent).detail; + const { draggedElement, mousePosition, column } = (event as CustomEvent).detail; console.log('🔄 EventRendererManager: Received drag:convert-to-time_event', { - draggedEventId, + draggedElement: draggedElement?.dataset.eventId, mousePosition, column }); - this.handleConvertToTimeEvent(draggedEventId, mousePosition, column); + this.handleConvertToTimeEvent(draggedElement, mousePosition, column); }); } @@ -153,18 +146,94 @@ export class EventRenderingService { } + /** + * Setup all drag event listeners - moved from EventRenderer for better separation of concerns + */ + private setupDragEventListeners(): void { + // Handle drag start + this.eventBus.on('drag:start', (event: Event) => { + const { eventId, mouseOffset, column } = (event as CustomEvent).detail; + // Find element dynamically + const originalElement = document.querySelector(`swp-event[data-event-id="${eventId}"]`) as HTMLElement; + if (originalElement && this.strategy.handleDragStart) { + this.strategy.handleDragStart(originalElement, eventId, mouseOffset, column); + } + }); + + // Handle drag move + this.eventBus.on('drag:move', (event: Event) => { + const { eventId, snappedY, column, mouseOffset } = (event as CustomEvent).detail; + if (this.strategy.handleDragMove) { + this.strategy.handleDragMove(eventId, snappedY, column, mouseOffset); + } + }); + + // Handle drag auto-scroll + this.eventBus.on('drag:auto-scroll', (event: Event) => { + const { eventId, snappedY } = (event as CustomEvent).detail; + if (this.strategy.handleDragAutoScroll) { + this.strategy.handleDragAutoScroll(eventId, snappedY); + } + }); + + // Handle drag end events and delegate to appropriate renderer + this.eventBus.on('drag:end', (event: Event) => { + const { eventId, finalColumn, finalY, target } = (event as CustomEvent).detail; + + // Only handle day column drops for EventRenderer + if (target === 'swp-day-column') { + // Find both original element and dragged clone + const originalElement = document.querySelector(`swp-day-column swp-event[data-event-id="${eventId}"]`) as HTMLElement; + const draggedClone = document.querySelector(`swp-day-column swp-event[data-event-id="clone-${eventId}"]`) as HTMLElement; + + if (originalElement && draggedClone && this.strategy.handleDragEnd) { + this.strategy.handleDragEnd(eventId, originalElement, draggedClone, finalColumn, finalY); + } + } + + // Clean up any remaining day event clones + const dayEventClone = document.querySelector(`swp-event[data-event-id="clone-${eventId}"]`); + if (dayEventClone) { + dayEventClone.remove(); + } + }); + + // Handle click (when drag threshold not reached) + this.eventBus.on('event:click', (event: Event) => { + const { eventId } = (event as CustomEvent).detail; + // Find element dynamically + const originalElement = document.querySelector(`swp-event[data-event-id="${eventId}"]`) as HTMLElement; + if (originalElement && this.strategy.handleEventClick) { + this.strategy.handleEventClick(eventId, originalElement); + } + }); + + // Handle column change + this.eventBus.on('drag:column-change', (event: Event) => { + const { eventId, newColumn } = (event as CustomEvent).detail; + if (this.strategy.handleColumnChange) { + this.strategy.handleColumnChange(eventId, newColumn); + } + }); + + // Handle navigation period change + this.eventBus.on(CoreEvents.NAVIGATION_COMPLETED, () => { + // Delegate to strategy if it handles navigation + if (this.strategy.handleNavigationCompleted) { + this.strategy.handleNavigationCompleted(); + } + }); + } + /** * Handle conversion from all-day event to time event */ - private handleConvertToTimeEvent(draggedEventId: string, mousePosition: any, column: string): void { - // Find all-day event clone - const allDayClone = document.querySelector(`swp-allday-container swp-allday-event[data-event-id="clone-${draggedEventId}"]`); - - if (!allDayClone) { - console.warn('EventRendererManager: All-day clone not found - drag may not have started properly', { draggedEventId }); - return; - } + private handleConvertToTimeEvent(draggedElement: HTMLElement, mousePosition: any, column: string): void { + // Use the provided draggedElement directly + const allDayClone = draggedElement; + const draggedEventId = draggedElement?.dataset.eventId?.replace('clone-', '') || ''; + // Use SwpEventElement factory to create day event from all-day event const dayEventElement = SwpEventElement.fromAllDayElement(allDayClone as HTMLElement); const dayElement = dayEventElement.getElement();