From 4141bffca436b13986c77eab3077a9a500c0026b Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Sat, 27 Sep 2025 15:01:22 +0200 Subject: [PATCH] Refactors all-day event layout calculation Simplifies all-day event rendering by streamlining the layout calculation and event placement process, using the AllDayLayoutEngine to determine the grid positions. This removes deprecated methods and improves overall code clarity. --- src/elements/SwpEventElement.ts | 148 +++----------------------- src/managers/AllDayManager.ts | 115 ++------------------ src/managers/DragDropManager.ts | 27 ++--- src/renderers/AllDayEventRenderer.ts | 38 +++---- src/renderers/EventRendererManager.ts | 28 +---- src/types/EventTypes.ts | 1 + src/utils/AllDayLayoutEngine.ts | 40 ++++--- 7 files changed, 76 insertions(+), 321 deletions(-) diff --git a/src/elements/SwpEventElement.ts b/src/elements/SwpEventElement.ts index 87be4cd..a883fe1 100644 --- a/src/elements/SwpEventElement.ts +++ b/src/elements/SwpEventElement.ts @@ -2,6 +2,7 @@ import { CalendarEvent } from '../types/CalendarTypes'; import { calendarConfig } from '../core/CalendarConfig'; import { TimeFormatter } from '../utils/TimeFormatter'; import { PositionUtils } from '../utils/PositionUtils'; +import { EventLayout } from '../utils/AllDayLayoutEngine'; /** * Abstract base class for event DOM elements @@ -75,7 +76,7 @@ export class SwpEventElement extends BaseEventElement { private createInnerStructure(): void { const timeRange = TimeFormatter.formatTimeRange(this.event.start, this.event.end); const durationMinutes = (this.event.end.getTime() - this.event.start.getTime()) / (1000 * 60); - + this.element.innerHTML = ` ${timeRange} ${this.event.title} @@ -107,20 +108,20 @@ export class SwpEventElement extends BaseEventElement { 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; } @@ -130,11 +131,11 @@ export class SwpEventElement extends BaseEventElement { 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; } @@ -202,7 +203,7 @@ export class SwpEventElement extends BaseEventElement { const now = new Date(); const startDate = new Date(originalStart); startDate.setHours(now.getHours() || 9, now.getMinutes() || 0, 0, 0); - + const endDate = new Date(startDate); endDate.setMinutes(endDate.getMinutes() + duration); @@ -228,14 +229,12 @@ export class SwpEventElement extends BaseEventElement { * All-day event element (now using unified swp-event tag) */ export class SwpAllDayEventElement extends BaseEventElement { - private columnIndex: number; - private constructor(event: CalendarEvent, columnIndex: number) { + constructor(event: CalendarEvent) { super(event); - this.columnIndex = columnIndex; this.setAllDayAttributes(); this.createInnerStructure(); - this.applyGridPositioning(); + // this.applyGridPositioning(); } protected createElement(): HTMLElement { @@ -264,128 +263,9 @@ export class SwpAllDayEventElement extends BaseEventElement { /** * Apply CSS grid positioning */ - private applyGridPositioning(): void { - this.element.style.gridColumn = this.columnIndex.toString(); - } - - /** - * Set grid row for this all-day event - */ - public setGridRow(row: number): void { - this.element.style.gridRow = row.toString(); - } - - /** - * Set grid column span for this all-day event - */ - public setColumnSpan(startColumn: number, endColumn: number): void { - this.element.style.gridColumn = `${startColumn} / ${endColumn + 1}`; - } - - /** - * Factory method to create from CalendarEvent and layout (provided by AllDayManager) - */ - public static fromCalendarEventWithLayout( - event: CalendarEvent, - layout: { startColumn: number; endColumn: number; row: number; columnSpan: number } - ): SwpAllDayEventElement { - // Create element with provided layout - const element = new SwpAllDayEventElement(event, layout.startColumn); - - // Set complete grid-area instead of individual properties + public applyGridPositioning(layout: EventLayout): void { const gridArea = `${layout.row} / ${layout.startColumn} / ${layout.row + 1} / ${layout.endColumn + 1}`; - element.element.style.gridArea = gridArea; - - console.log('✅ SwpAllDayEventElement: Created all-day event with AllDayLayoutEngine', { - eventId: event.id, - title: event.title, - gridArea: gridArea, - layout: layout - }); - - return element; + this.element.style.gridArea = gridArea; } - /** - * Factory method to create from CalendarEvent and target date (DEPRECATED - use AllDayManager.calculateAllDayEventLayout) - * @deprecated Use AllDayManager.calculateAllDayEventLayout() and fromCalendarEventWithLayout() instead - */ - public static fromCalendarEvent(event: CalendarEvent, targetDate?: string): SwpAllDayEventElement { - console.warn('⚠️ SwpAllDayEventElement.fromCalendarEvent is deprecated. Use AllDayManager.calculateAllDayEventLayout() instead.'); - - // Fallback to simple column calculation without overlap detection - const { startColumn, endColumn } = this.calculateColumnSpan(event); - const finalStartColumn = targetDate ? this.getColumnIndexForDate(targetDate) : startColumn; - const finalEndColumn = targetDate ? finalStartColumn : endColumn; - - // Create element with row 1 (no overlap detection) - const element = new SwpAllDayEventElement(event, finalStartColumn); - element.setGridRow(1); - element.setColumnSpan(finalStartColumn, finalEndColumn); - - return element; - } - - /** - * Calculate column span based on event start and end dates - */ - private static calculateColumnSpan(event: CalendarEvent): { startColumn: number; endColumn: number; columnSpan: number } { - const dayHeaders = document.querySelectorAll('swp-day-header'); - - // Extract dates from headers - const headerDates: string[] = []; - dayHeaders.forEach(header => { - const date = (header as HTMLElement).dataset.date; - if (date) { - headerDates.push(date); - } - }); - - // Format event dates for comparison (YYYY-MM-DD format) - const eventStartDate = event.start.toISOString().split('T')[0]; - const eventEndDate = event.end.toISOString().split('T')[0]; - - // Find start and end column indices - let startColumn = 1; - let endColumn = headerDates.length; - - headerDates.forEach((dateStr, index) => { - if (dateStr === eventStartDate) { - startColumn = index + 1; - } - if (dateStr === eventEndDate) { - endColumn = index + 1; - } - }); - - // Ensure end column is at least start column - if (endColumn < startColumn) { - endColumn = startColumn; - } - - const columnSpan = endColumn - startColumn + 1; - - return { startColumn, endColumn, columnSpan }; - } - - /** - * Get column index for a specific date - */ - private static getColumnIndexForDate(targetDate: string): number { - const dayHeaders = document.querySelectorAll('swp-day-header'); - let columnIndex = 1; - dayHeaders.forEach((header, index) => { - if ((header as HTMLElement).dataset.date === targetDate) { - columnIndex = index + 1; - } - }); - return columnIndex; - } - - /** - * Check if two column ranges overlap - */ - private static columnsOverlap(startA: number, endA: number, startB: number, endB: number): boolean { - return !(endA < startB || endB < startA); - } } \ No newline at end of file diff --git a/src/managers/AllDayManager.ts b/src/managers/AllDayManager.ts index d4c4011..eed4f7f 100644 --- a/src/managers/AllDayManager.ts +++ b/src/managers/AllDayManager.ts @@ -3,7 +3,7 @@ import { eventBus } from '../core/EventBus'; import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig'; import { AllDayEventRenderer } from '../renderers/AllDayEventRenderer'; -import { AllDayLayoutEngine } from '../utils/AllDayLayoutEngine'; +import { AllDayLayoutEngine, EventLayout } from '../utils/AllDayLayoutEngine'; import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; import { CalendarEvent } from '../types/CalendarTypes'; import { @@ -96,7 +96,7 @@ export class AllDayManager { }); eventBus.on('drag:end', (event) => { - const { draggedElement, mousePosition, finalPosition, target } = (event as CustomEvent).detail; + const { draggedElement, mousePosition, finalPosition, target, draggedClone } = (event as CustomEvent).detail; if (target != 'swp-day-header') // we are not inside the swp-day-header, so just ignore. return; @@ -106,10 +106,9 @@ export class AllDayManager { eventId: eventId, finalPosition }); - const dragClone = document.querySelector(`swp-allday-container swp-event[data-event-id="clone-${eventId}"]`); console.log('🎯 AllDayManager: Ending drag for all-day event', { eventId }); - this.handleDragEnd(draggedElement, dragClone as HTMLElement, { column: finalPosition.column || '', y: 0 }); + this.handleDragEnd(draggedElement, draggedClone as HTMLElement, { column: finalPosition.column || '', y: 0 }); }); // Listen for drag cancellation to recalculate height @@ -307,18 +306,7 @@ export class AllDayManager { * Calculate layout for ALL all-day events using AllDayLayoutEngine * This is the correct method that processes all events together for proper overlap detection */ - public calculateAllDayEventsLayout(events: CalendarEvent[], weekDates: string[]): Map { - console.log('🔍 AllDayManager: calculateAllDayEventsLayout - Processing all events together', { - eventCount: events.length, - events: events.map(e => ({ id: e.id, title: e.title, start: e.start.toISOString().split('T')[0], end: e.end.toISOString().split('T')[0] })), - weekDates - }); + public calculateAllDayEventsLayout(events: CalendarEvent[], weekDates: string[]): EventLayout[] { // Store current state this.currentAllDayEvents = events; @@ -328,35 +316,8 @@ export class AllDayManager { this.layoutEngine = new AllDayLayoutEngine(weekDates); // Calculate layout for all events together - AllDayLayoutEngine handles CalendarEvents directly - const layouts = this.layoutEngine.calculateLayout(events); + return this.layoutEngine.calculateLayout(events); - // Convert to expected return format - const result = new Map(); - - layouts.forEach((layout, eventId) => { - result.set(eventId, { - startColumn: layout.startColumn, - endColumn: layout.endColumn, - row: layout.row, - columnSpan: layout.columnSpan, - gridArea: layout.gridArea - }); - - console.log('✅ AllDayManager: Calculated layout for event', { - eventId, - title: events.find(e => e.id === eventId)?.title, - gridArea: layout.gridArea, - layout: layout - }); - }); - - return result; } @@ -494,19 +455,14 @@ export class AllDayManager { // 5. Apply differential updates - only update events that changed let changedCount = 0; - newLayouts.forEach((layout, eventId) => { - const oldGridArea = this.currentLayouts.get(eventId); + newLayouts.forEach((layout) => { + const oldGridArea = this.currentLayouts.get(layout.calenderEvent.id); const newGridArea = layout.gridArea; if (oldGridArea !== newGridArea) { changedCount++; - const element = document.querySelector(`[data-event-id="${eventId}"]`) as HTMLElement; + const element = document.querySelector(`[data-event-id="${layout.calenderEvent.id}"]`) as HTMLElement; if (element) { - console.log('🔄 AllDayManager: Updating event position', { - eventId, - oldGridArea, - newGridArea - }); // Add transition class for smooth animation element.classList.add('transitioning'); @@ -532,61 +488,6 @@ export class AllDayManager { // 8. Check if height adjustment is needed this.checkAndAnimateAllDayHeight(); - console.log('✅ AllDayManager: Completed differential drag end', { - eventId: droppedEventId, - totalEvents: newLayouts.size, - changedEvents: changedCount, - finalGridArea: newLayouts.get(droppedEventId)?.gridArea - }); - } - - /** - * Get existing all-day events from DOM - * Since we don't have direct access to EventManager, we'll get events from the current DOM - */ - private getExistingAllDayEvents(): CalendarEvent[] { - const allDayContainer = this.getAllDayContainer(); - if (!allDayContainer) { - return []; - } - - const existingElements = allDayContainer.querySelectorAll('swp-event'); - const events: CalendarEvent[] = []; - - existingElements.forEach(element => { - const htmlElement = element as HTMLElement; - const eventId = htmlElement.dataset.eventId; - const title = htmlElement.dataset.title || htmlElement.textContent || ''; - const allDayDate = htmlElement.dataset.allDayDate; - - if (eventId && allDayDate) { - events.push({ - id: eventId, - title: title, - start: new Date(allDayDate), - end: new Date(allDayDate), - type: 'work', - allDay: true, - syncStatus: 'synced' - }); - } - }); - - return events; - } - - private getVisibleDatesFromDOM(): string[] { - const dayHeaders = document.querySelectorAll('swp-calendar-header swp-day-header'); - const weekDates: string[] = []; - - dayHeaders.forEach(header => { - const dateAttr = header.getAttribute('data-date'); - if (dateAttr) { - weekDates.push(dateAttr); - } - }); - - return weekDates; } } \ No newline at end of file diff --git a/src/managers/DragDropManager.ts b/src/managers/DragDropManager.ts index b6b2933..e521918 100644 --- a/src/managers/DragDropManager.ts +++ b/src/managers/DragDropManager.ts @@ -132,7 +132,10 @@ export class DragDropManager { } private handleMouseDown(event: MouseEvent): void { - this.isDragStarted = false; + + // Clean up drag state first + this.cleanupDragState(); + this.lastMousePosition = { x: event.clientX, y: event.clientY }; this.lastLoggedPosition = { x: event.clientX, y: event.clientY }; this.initialMousePosition = { x: event.clientX, y: event.clientY }; @@ -274,11 +277,10 @@ export class DragDropManager { if (this.draggedElement) { // Store variables locally before cleanup - const draggedElement = this.draggedElement; + //const draggedElement = this.draggedElement; const isDragStarted = this.isDragStarted; - // Clean up drag state first - this.cleanupDragState(); + // Only emit drag:end if drag was actually started @@ -292,7 +294,7 @@ export class DragDropManager { const dropTarget = this.detectDropTarget(mousePosition); console.log('🎯 DragDropManager: Emitting drag:end', { - draggedElement: draggedElement.dataset.eventId, + draggedElement: this.draggedElement.dataset.eventId, finalColumn: positionData.column, finalY: positionData.snappedY, dropTarget: dropTarget, @@ -300,19 +302,20 @@ export class DragDropManager { }); const dragEndPayload: DragEndEventPayload = { - draggedElement: draggedElement, + draggedElement: this.draggedElement, + draggedClone : this.draggedClone, mousePosition, finalPosition: positionData, target: dropTarget }; this.eventBus.emit('drag:end', dragEndPayload); - draggedElement.remove(); + this.draggedElement.remove(); // 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. } else { // This was just a click - emit click event instead this.eventBus.emit('event:click', { - draggedElement: draggedElement, + draggedElement: this.draggedElement, mousePosition: { x: event.clientX, y: event.clientY } }); } @@ -540,13 +543,11 @@ export class DragDropManager { * 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; + let currentElement = this.draggedClone; while (currentElement && currentElement !== document.body) { - if (currentElement.tagName === 'SWP-DAY-HEADER') { + if (currentElement.tagName === 'SWP-ALLDAY-CONTAINER') { return 'swp-day-header'; } if (currentElement.tagName === 'SWP-DAY-COLUMN') { diff --git a/src/renderers/AllDayEventRenderer.ts b/src/renderers/AllDayEventRenderer.ts index 4f95a26..acdce87 100644 --- a/src/renderers/AllDayEventRenderer.ts +++ b/src/renderers/AllDayEventRenderer.ts @@ -1,5 +1,6 @@ import { CalendarEvent } from '../types/CalendarTypes'; import { SwpAllDayEventElement } from '../elements/SwpEventElement'; +import { EventLayout } from '../utils/AllDayLayoutEngine'; /** * AllDayEventRenderer - Simple rendering of all-day events @@ -17,19 +18,19 @@ export class AllDayEventRenderer { * Get or cache all-day container, create if it doesn't exist - SIMPLIFIED (no ghost columns) */ private getContainer(): HTMLElement | null { - - const header = document.querySelector('swp-calendar-header'); - if (header) { - this.container = header.querySelector('swp-allday-container'); - - if (!this.container) { - this.container = document.createElement('swp-allday-container'); - header.appendChild(this.container); - - } + + const header = document.querySelector('swp-calendar-header'); + if (header) { + this.container = header.querySelector('swp-allday-container'); + + if (!this.container) { + this.container = document.createElement('swp-allday-container'); + header.appendChild(this.container); + } - return this.container; - + } + return this.container; + } // REMOVED: createGhostColumns() method - no longer needed! @@ -39,16 +40,15 @@ export class AllDayEventRenderer { */ public renderAllDayEventWithLayout( event: CalendarEvent, - layout: { startColumn: number; endColumn: number; row: number; columnSpan: number } - ): HTMLElement | null { + layout: EventLayout + ) { const container = this.getContainer(); if (!container) return null; - const allDayElement = SwpAllDayEventElement.fromCalendarEventWithLayout(event, layout); - const element = allDayElement.getElement(); - - container.appendChild(element); - return element; + let dayEvent = new SwpAllDayEventElement(event); + dayEvent.applyGridPositioning(layout); + + container.appendChild(dayEvent.getElement()); } diff --git a/src/renderers/EventRendererManager.ts b/src/renderers/EventRendererManager.ts index 53e9b02..581b7d0 100644 --- a/src/renderers/EventRendererManager.ts +++ b/src/renderers/EventRendererManager.ts @@ -372,35 +372,11 @@ export class EventRenderingService { // Pass current events to AllDayManager for state tracking this.allDayManager.setCurrentEvents(allDayEvents, weekDates); - // Calculate layout for ALL all-day events together using AllDayLayoutEngine const layouts = this.allDayManager.calculateAllDayEventsLayout(allDayEvents, weekDates); // Render each all-day event with pre-calculated layout - allDayEvents.forEach(event => { - const layout = layouts.get(event.id); - if (!layout) { - console.warn('❌ EventRenderingService: No layout found for all-day event', { - id: event.id, - title: event.title - }); - return; - } - - // Render with pre-calculated layout - const renderedElement = this.allDayEventRenderer.renderAllDayEventWithLayout(event, layout); - if (renderedElement) { - console.log('✅ EventRenderingService: Rendered all-day event with AllDayLayoutEngine', { - id: event.id, - title: event.title, - gridArea: layout.gridArea, - element: renderedElement.tagName - }); - } else { - console.warn('❌ EventRenderingService: Failed to render all-day event', { - id: event.id, - title: event.title - }); - } + layouts.forEach(layout => { + this.allDayEventRenderer.renderAllDayEventWithLayout(layout.calenderEvent, layout); }); // Check and adjust all-day container height after rendering diff --git a/src/types/EventTypes.ts b/src/types/EventTypes.ts index d9ff01a..a649740 100644 --- a/src/types/EventTypes.ts +++ b/src/types/EventTypes.ts @@ -64,6 +64,7 @@ export interface DragMoveEventPayload { // Drag end event payload export interface DragEndEventPayload { draggedElement: HTMLElement; + draggedClone: HTMLElement | null; mousePosition: MousePosition; finalPosition: { column: string | null; diff --git a/src/utils/AllDayLayoutEngine.ts b/src/utils/AllDayLayoutEngine.ts index 6503901..ac8bad8 100644 --- a/src/utils/AllDayLayoutEngine.ts +++ b/src/utils/AllDayLayoutEngine.ts @@ -1,7 +1,7 @@ import { CalendarEvent } from '../types/CalendarTypes'; export interface EventLayout { - id: string; + calenderEvent: CalendarEvent; gridArea: string; // "row-start / col-start / row-end / col-end" startColumn: number; endColumn: number; @@ -21,42 +21,38 @@ export class AllDayLayoutEngine { /** * Calculate layout for all events using clean day-based logic */ - public calculateLayout(events: CalendarEvent[]): Map { - const layouts = new Map(); - - if (this.weekDates.length === 0) { - return layouts; - } + public calculateLayout(events: CalendarEvent[]): EventLayout[] { + let layouts: EventLayout[] = []; // Reset tracks for new calculation this.tracks = [new Array(this.weekDates.length).fill(false)]; - + // Filter to only visible events const visibleEvents = events.filter(event => this.isEventVisible(event)); - + // Process events in input order (no sorting) for (const event of visibleEvents) { const startDay = this.getEventStartDay(event); const endDay = this.getEventEndDay(event); - + if (startDay > 0 && endDay > 0) { const track = this.findAvailableTrack(startDay - 1, endDay - 1); // Convert to 0-based for tracks - + // Mark days as occupied for (let day = startDay - 1; day <= endDay - 1; day++) { this.tracks[track][day] = true; } - + const layout: EventLayout = { - id: event.id, + calenderEvent: event, gridArea: `${track + 1} / ${startDay} / ${track + 2} / ${endDay + 1}`, startColumn: startDay, endColumn: endDay, row: track + 1, columnSpan: endDay - startDay + 1 }; - - layouts.set(event.id, layout); + layouts.push(layout); + } } @@ -72,7 +68,7 @@ export class AllDayLayoutEngine { return trackIndex; } } - + // Create new track if none available this.tracks.push(new Array(this.weekDates.length).fill(false)); return this.tracks.length - 1; @@ -96,10 +92,10 @@ export class AllDayLayoutEngine { private getEventStartDay(event: CalendarEvent): number { const eventStartDate = this.formatDate(event.start); const firstVisibleDate = this.weekDates[0]; - + // If event starts before visible range, clip to first visible day const clippedStartDate = eventStartDate < firstVisibleDate ? firstVisibleDate : eventStartDate; - + const dayIndex = this.weekDates.indexOf(clippedStartDate); return dayIndex >= 0 ? dayIndex + 1 : 0; } @@ -110,10 +106,10 @@ export class AllDayLayoutEngine { private getEventEndDay(event: CalendarEvent): number { const eventEndDate = this.formatDate(event.end); const lastVisibleDate = this.weekDates[this.weekDates.length - 1]; - + // If event ends after visible range, clip to last visible day const clippedEndDate = eventEndDate > lastVisibleDate ? lastVisibleDate : eventEndDate; - + const dayIndex = this.weekDates.indexOf(clippedEndDate); return dayIndex >= 0 ? dayIndex + 1 : 0; } @@ -123,12 +119,12 @@ export class AllDayLayoutEngine { */ private isEventVisible(event: CalendarEvent): boolean { if (this.weekDates.length === 0) return false; - + const eventStartDate = this.formatDate(event.start); const eventEndDate = this.formatDate(event.end); const firstVisibleDate = this.weekDates[0]; const lastVisibleDate = this.weekDates[this.weekDates.length - 1]; - + // Event overlaps if it doesn't end before visible range starts // AND doesn't start after visible range ends return !(eventEndDate < firstVisibleDate || eventStartDate > lastVisibleDate);