diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index a1e56b2..c74b0b5 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -2,16 +2,11 @@ import { CalendarEvent } from '../types/CalendarTypes'; import { calendarConfig } from '../core/CalendarConfig'; -import { eventBus } from '../core/EventBus'; -import { OverlapDetector, OverlapResult } from '../utils/OverlapDetector'; import { SwpEventElement } from '../elements/SwpEventElement'; -import { TimeFormatter } from '../utils/TimeFormatter'; import { PositionUtils } from '../utils/PositionUtils'; -import { DragOffset, StackLinkData } from '../types/DragDropTypes'; import { ColumnBounds } from '../utils/ColumnDetectionUtils'; import { DragColumnChangeEventPayload, DragMoveEventPayload, DragStartEventPayload } from '../types/EventTypes'; import { DateService } from '../utils/DateService'; -import { format } from 'date-fns'; /** * Interface for event rendering strategies @@ -27,10 +22,6 @@ export interface EventRendererStrategy { handleColumnChange?(payload: DragColumnChangeEventPayload): void; handleNavigationCompleted?(): void; } -// Abstract methods that subclasses must implement -// private getColumns(container: HTMLElement): HTMLElement[]; -// private getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[]; - /** * Date-based event renderer @@ -38,128 +29,14 @@ export interface EventRendererStrategy { export class DateEventRenderer implements EventRendererStrategy { private dateService: DateService; + private draggedClone: HTMLElement | null = null; + private originalEvent: HTMLElement | null = null; constructor() { const timezone = calendarConfig.getTimezone?.(); this.dateService = new DateService(timezone); - this.setupDragEventListeners(); } - private draggedClone: HTMLElement | null = null; - private originalEvent: HTMLElement | null = null; - - - // ============================================ - // NEW OVERLAP DETECTION SYSTEM - // All new functions prefixed with new_ - // ============================================ - - protected overlapDetector = new OverlapDetector(); - - /** - * Ny hovedfunktion til at håndtere event overlaps - * Finder transitivt overlappende events (hvis A overlapper B og B overlapper C, så er A, B, C i samme gruppe) - * @param events - Events der skal renderes i kolonnen - * @param container - Container element at rendere i - */ - protected handleEventOverlaps(events: CalendarEvent[], container: HTMLElement): void { - if (events.length === 0) return; - - if (events.length === 1) { - const element = this.renderEvent(events[0]); - container.appendChild(element); - return; - } - - // Find alle overlap grupper (transitive overlaps) - const overlapGroups = this.findTransitiveOverlapGroups(events); - - // Render hver gruppe - overlapGroups.forEach(group => { - if (group.length === 1) { - // Enkelt event uden overlaps - const element = this.renderEvent(group[0]); - container.appendChild(element); - } else { - // Gruppe med overlaps - opret stack links - // Tag første event som "current" og resten som "overlapping" - const [firstEvent, ...restEvents] = group; - const result = this.overlapDetector.decorateWithStackLinks(firstEvent, restEvents); - this.renderOverlappingEvents(result, container); - } - }); - } - - /** - * Find alle grupper af transitivt overlappende events - * Bruger Union-Find algoritme til at finde sammenhængende komponenter - */ - private findTransitiveOverlapGroups(events: CalendarEvent[]): CalendarEvent[][] { - // Byg overlap graf - const overlapMap = new Map>(); - - // Initialiser alle events - events.forEach(event => { - overlapMap.set(event.id, new Set()); - }); - - // Find alle direkte overlaps - for (let i = 0; i < events.length; i++) { - for (let j = i + 1; j < events.length; j++) { - const event1 = events[i]; - const event2 = events[j]; - - // Check om de overlapper - if (event1.start < event2.end && event1.end > event2.start) { - overlapMap.get(event1.id)!.add(event2.id); - overlapMap.get(event2.id)!.add(event1.id); - } - } - } - - // Find sammenhængende komponenter via DFS - const visited = new Set(); - const groups: CalendarEvent[][] = []; - - events.forEach(event => { - if (visited.has(event.id)) return; - - // Start ny gruppe - const group: CalendarEvent[] = []; - const stack = [event.id]; - - while (stack.length > 0) { - const currentId = stack.pop()!; - - if (visited.has(currentId)) continue; - visited.add(currentId); - - // Find event objektet - const currentEvent = events.find(e => e.id === currentId); - if (currentEvent) { - group.push(currentEvent); - } - - // Tilføj alle naboer til stack - const neighbors = overlapMap.get(currentId); - if (neighbors) { - neighbors.forEach(neighborId => { - if (!visited.has(neighborId)) { - stack.push(neighborId); - } - }); - } - } - - // Sortér gruppe efter start tid - group.sort((a, b) => a.start.getTime() - b.start.getTime()); - groups.push(group); - }); - - return groups; - } - - private applyDragStyling(element: HTMLElement): void { element.classList.add('dragging'); element.style.removeProperty("margin-left"); @@ -331,89 +208,12 @@ export class DateEventRenderer implements EventRendererStrategy { * Handle drag end event */ public handleDragEnd(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: ColumnBounds, finalY: number): void { - if (!draggedClone || !originalElement) { console.warn('Missing draggedClone or originalElement'); return; } - // Check om original event var del af en stack - 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(); - - // Recursive funktion til at traversere stack chain - const traverseStack = (linkData: StackLinkData, visitedIds: Set) => { - if (linkData.prev && !visitedIds.has(linkData.prev)) { - visitedIds.add(linkData.prev); - const prevElement = document.querySelector(`swp-time-grid [data-event-id="${linkData.prev}"]`) as HTMLElement; - if (prevElement?.dataset.stackLink) { - try { - const prevLinkData = JSON.parse(prevElement.dataset.stackLink); - traverseStack(prevLinkData, visitedIds); - } 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; - if (nextElement?.dataset.stackLink) { - try { - const nextLinkData = JSON.parse(nextElement.dataset.stackLink); - traverseStack(nextLinkData, visitedIds); - } catch (e) { } - } - } - }; - - // Start traversering fra original event's stackLink - traverseStack(stackData, allStackEventIds); - - // Fjern original eventId da det bliver flyttet - allStackEventIds.delete(eventId); - - // Find alle stack events og fjern dem - const stackEvents: CalendarEvent[] = []; - let container: HTMLElement | null = null; - - allStackEventIds.forEach(id => { - const element = document.querySelector(`swp-time-grid [data-event-id="${id}"]`) as HTMLElement; - if (element) { - // Gem container reference fra første element - if (!container) { - container = element.closest('swp-events-layer') as HTMLElement; - } - - const event = SwpEventElement.extractCalendarEventFromElement(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); - } - } catch (e) { - console.warn('Failed to parse stackLink data:', e); - } - } - - // Remove original event from any existing groups first - this.removeEventFromExistingGroups(originalElement); - // Fade out original - // TODO: this should be changed into a subscriber which only after a succesful placement is fired, not just mouseup as this can remove elements that are not placed. this.fadeOutAndRemove(originalElement); // Remove clone prefix and normalize clone to be a regular event @@ -424,23 +224,10 @@ export class DateEventRenderer implements EventRendererStrategy { // Fully normalize the clone to be a regular event draggedClone.classList.remove('dragging'); - // Behold z-index hvis det er et stacked event - // Data attributes are already updated during drag:move, so no need to update again - // The updateCloneTimestamp method keeps them synchronized throughout the drag operation - - // Detect overlaps with other events in the target column and reposition if needed - this.handleDragDropOverlaps(draggedClone, finalColumn); - - // Fjern stackLink data fra dropped element - if (draggedClone.dataset.stackLink) { - delete draggedClone.dataset.stackLink; - } - - // Clean up instance state (no longer needed since we get elements as parameters) + // Clean up instance state this.draggedClone = null; this.originalEvent = null; - } /** @@ -450,167 +237,6 @@ export class DateEventRenderer implements EventRendererStrategy { // Default implementation - can be overridden by subclasses } - /** - * Handle overlap detection and re-rendering after drag-drop - */ - private handleDragDropOverlaps(droppedElement: HTMLElement, targetColumn: ColumnBounds): void { - - const eventsLayer = targetColumn.element.querySelector('swp-events-layer') as HTMLElement; - if (!eventsLayer) return; - - // Convert dropped element to CalendarEvent with new position - const droppedEvent = SwpEventElement.extractCalendarEventFromElement(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) { - // Collect ALL events in stack chains (not just direct overlaps) - const allStackedEvents = this.collectAllStackedEvents(overlappingEvents, eventsLayer); - - // Remove all affected events from DOM - const affectedEventIds = [droppedEvent.id, ...allStackedEvents.map(e => e.id)]; - eventsLayer.querySelectorAll('swp-event').forEach(el => { - const eventId = (el as HTMLElement).dataset.eventId; - if (eventId && affectedEventIds.includes(eventId)) { - el.remove(); - } - }); - - // Re-render all affected events with overlap handling - const affectedEvents = [droppedEvent, ...allStackedEvents]; - this.handleEventOverlaps(affectedEvents, eventsLayer); - } else { - // Reset z-index for non-overlapping events - droppedElement.style.zIndex = ''; - } - } - - /** - * Collect all events in stack chains for the given overlapping events - * This ensures we re-render entire stack chains, not just direct overlaps - */ - private collectAllStackedEvents(overlappingEvents: CalendarEvent[], eventsLayer: HTMLElement): CalendarEvent[] { - const allEvents = new Map(); - const visitedIds = new Set(); - - // Add all directly overlapping events - overlappingEvents.forEach(event => { - allEvents.set(event.id, event); - visitedIds.add(event.id); - }); - - // For each overlapping event, traverse its stack chain - overlappingEvents.forEach(event => { - const element = eventsLayer.querySelector(`swp-event[data-event-id="${event.id}"]`) as HTMLElement; - if (!element?.dataset.stackLink) return; - - try { - const stackData = JSON.parse(element.dataset.stackLink); - this.traverseStackChain(stackData, eventsLayer, allEvents, visitedIds); - } catch (e) { - console.warn('Failed to parse stackLink:', e); - } - }); - - return Array.from(allEvents.values()); - } - - /** - * Recursively traverse stack chain to find all connected events - */ - private traverseStackChain( - stackLink: StackLinkData, - eventsLayer: HTMLElement, - allEvents: Map, - visitedIds: Set - ): void { - // Traverse previous events - if (stackLink.prev && !visitedIds.has(stackLink.prev)) { - visitedIds.add(stackLink.prev); - const prevElement = eventsLayer.querySelector(`swp-event[data-event-id="${stackLink.prev}"]`) as HTMLElement; - - if (prevElement) { - const prevEvent = SwpEventElement.extractCalendarEventFromElement(prevElement); - if (prevEvent) { - allEvents.set(prevEvent.id, prevEvent); - - // Continue traversing - if (prevElement.dataset.stackLink) { - try { - const prevStackData = JSON.parse(prevElement.dataset.stackLink); - this.traverseStackChain(prevStackData, eventsLayer, allEvents, visitedIds); - } catch (e) { } - } - } - } - } - - // Traverse next events - if (stackLink.next && !visitedIds.has(stackLink.next)) { - visitedIds.add(stackLink.next); - const nextElement = eventsLayer.querySelector(`swp-event[data-event-id="${stackLink.next}"]`) as HTMLElement; - - if (nextElement) { - const nextEvent = SwpEventElement.extractCalendarEventFromElement(nextElement); - if (nextEvent) { - allEvents.set(nextEvent.id, nextEvent); - - // Continue traversing - if (nextElement.dataset.stackLink) { - try { - const nextStackData = JSON.parse(nextElement.dataset.stackLink); - this.traverseStackChain(nextStackData, eventsLayer, allEvents, visitedIds); - } catch (e) { } - } - } - } - } - } - - /** - * Get all events in a column as CalendarEvent objects - */ - 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 = SwpEventElement.extractCalendarEventFromElement(element); - if (event) { - events.push(event); - } - }); - - return events; - } - - /** - * Remove event from any existing groups and cleanup empty containers - * In the new system, this is handled automatically by re-rendering overlaps - */ - private removeEventFromExistingGroups(eventElement: HTMLElement): void { - // With the new system, overlap relationships are recalculated on drop - // No need to manually track and remove from groups - } - - - /** - * Handle conversion to all-day event - */ - /** * Fade out and remove element */ @@ -625,44 +251,29 @@ export class DateEventRenderer implements EventRendererStrategy { renderEvents(events: CalendarEvent[], container: HTMLElement): void { - // Filter out all-day events - they should be handled by AllDayEventRenderer const timedEvents = events.filter(event => !event.allDay); - console.log('🎯 EventRenderer: Filtering events', { - totalEvents: events.length, - timedEvents: timedEvents.length, - filteredOutAllDay: events.length - timedEvents.length - }); - // Find columns in the specific container for regular events const columns = this.getColumns(container); columns.forEach(column => { const columnEvents = this.getEventsForColumn(column, timedEvents); - const eventsLayer = column.querySelector('swp-events-layer'); + if (eventsLayer) { - - this.handleEventOverlaps(columnEvents, eventsLayer as HTMLElement); + // Simply render each event - no overlap handling + columnEvents.forEach(event => { + const element = this.renderEvent(event); + eventsLayer.appendChild(element); + }); } }); } - - private renderEvent(event: CalendarEvent): HTMLElement { const swpEvent = SwpEventElement.fromCalendarEvent(event); - const eventElement = swpEvent.getElement(); - - // Setup resize handles on first mouseover only - eventElement.addEventListener('mouseover', () => { // TODO: This is not the correct way... we should not add eventlistener on every event - if (eventElement.dataset.hasResizeHandlers !== 'true') { - eventElement.dataset.hasResizeHandlers = 'true'; - } - }, { once: true }); - - return eventElement; + return swpEvent.getElement(); } protected calculateEventPosition(event: CalendarEvent): { top: number; height: number } { @@ -671,7 +282,7 @@ export class DateEventRenderer implements EventRendererStrategy { } clearEvents(container?: HTMLElement): void { - const selector = 'swp-event, swp-event-group'; + const selector = 'swp-event'; const existingEvents = container ? container.querySelectorAll(selector) : document.querySelectorAll(selector); @@ -679,75 +290,6 @@ export class DateEventRenderer implements EventRendererStrategy { existingEvents.forEach(event => event.remove()); } - /** - * Renderer overlappende events baseret på OverlapResult - * @param result - OverlapResult med events og stack links - * @param container - Container at rendere i - */ - protected renderOverlappingEvents(result: OverlapResult, container: HTMLElement): void { - // Iterate direkte gennem stackLinks - allerede sorteret fra decorateWithStackLinks - 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); - if (prevEvent && prevEvent.start.getTime() === event.start.getTime()) { - // Samme start tid - del kolonne (side by side) - this.new_applyColumnSharingStyling([element]); - } else { - // Forskellige start tider - stack vertikalt - this.new_applyStackStyling(element, stackLink.stackLevel); - } - } else { - // Første event i stack - this.new_applyStackStyling(element, stackLink.stackLevel); - } - - container.appendChild(element); - } - } - - /** - * Applicerer stack styling (margin-left og z-index) - * @param element - Event element - * @param stackLevel - Stack niveau - */ - protected new_applyStackStyling(element: HTMLElement, stackLevel: number): void { - element.style.marginLeft = `${stackLevel * 15}px`; - element.style.zIndex = `${100 + stackLevel}`; - } - - /** - * Applicerer column sharing styling (flexbox) - * @param elements - Event elements der skal dele plads - */ - protected new_applyColumnSharingStyling(elements: HTMLElement[]): void { - elements.forEach(element => { - element.style.flex = '1'; - element.style.minWidth = '50px'; - }); - } - - - /** - * Setup drag event listeners - placeholder method - */ - private setupDragEventListeners(): void { - // Drag event listeners are handled by EventRendererManager - // This method exists for compatibility - } - protected getColumns(container: HTMLElement): HTMLElement[] { const columns = container.querySelectorAll('swp-day-column'); return Array.from(columns) as HTMLElement[];