From c682c30e23c6de144ae2ebef7576ab12e4e5db0c Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Sun, 21 Sep 2025 21:30:51 +0200 Subject: [PATCH] Improves all-day drag-and-drop conversion Refactors drag-to-all-day functionality to apply CSS styling and reposition the existing drag clone within the all-day container, rather than creating a new event element. Centralizes all-day container creation in HeaderManager. Introduces `drag:mouseleave-header` to handle transitions from all-day back to timed events. Ensures consistent styling and robust cleanup of drag clones for a smoother user experience. --- src/managers/AllDayManager.ts | 223 +++++++++++++------------- src/managers/DragDropManager.ts | 10 ++ src/managers/HeaderManager.ts | 30 +++- src/renderers/EventRenderer.ts | 5 +- src/renderers/EventRendererManager.ts | 2 +- wwwroot/css/calendar-layout-css.css | 32 +++- 6 files changed, 181 insertions(+), 121 deletions(-) diff --git a/src/managers/AllDayManager.ts b/src/managers/AllDayManager.ts index fc196d4..17cc8b6 100644 --- a/src/managers/AllDayManager.ts +++ b/src/managers/AllDayManager.ts @@ -22,11 +22,7 @@ export class AllDayManager { private allDayEventRenderer: AllDayEventRenderer; constructor() { - // Bind methods for event listeners - this.checkAndAnimateAllDayHeight = this.checkAndAnimateAllDayHeight.bind(this); this.allDayEventRenderer = new AllDayEventRenderer(); - - // Listen for drag-to-allday conversions this.setupEventListeners(); } @@ -38,7 +34,7 @@ export class AllDayManager { eventBus.on('drag:mouseenter-header', (event) => { const { targetDate, mousePosition, originalElement, cloneElement } = (event as CustomEvent).detail; - + console.log('🔄 AllDayManager: Received drag:mouseenter-header', { targetDate, originalElementId: originalElement?.dataset?.eventId, @@ -48,25 +44,29 @@ export class AllDayManager { if (targetDate && cloneElement) { this.handleConvertToAllDay(targetDate, cloneElement); } + + this.checkAndAnimateAllDayHeight (); }); + eventBus.on('drag:mouseleave-header', (event) => { + const { originalElement, cloneElement } = (event as CustomEvent).detail; - // Listen for requests to ensure all-day container exists - eventBus.on('allday:ensure-container', () => { - console.log('🏗️ AllDayManager: Received request to ensure all-day container exists'); - this.ensureAllDayContainer(); - }); + console.log('🚪 AllDayManager: Received drag:mouseleave-header', { + originalElementId: originalElement?.dataset?.eventId + }); + + if (cloneElement && cloneElement.classList.contains('all-day-style')) { + this.handleConvertFromAllDay(cloneElement); + } + + this.checkAndAnimateAllDayHeight (); - // Listen for header mouseleave to recalculate all-day container height - eventBus.on('header:mouseleave', () => { - console.log('🔄 AllDayManager: Received header:mouseleave, recalculating height'); - this.checkAndAnimateAllDayHeight(); }); // Listen for drag operations on all-day events eventBus.on('drag:start', (event) => { const { draggedElement, mouseOffset } = (event as CustomEvent).detail; - + // Check if this is an all-day event by checking if it's in all-day container const isAllDayEvent = draggedElement.closest('swp-allday-container'); if (!isAllDayEvent) return; // Not an all-day event @@ -101,7 +101,7 @@ export class AllDayManager { eventId: eventId, finalPosition }); -const dragClone = document.querySelector(`swp-allday-container swp-event[data-event-id="clone-${eventId}"]`); + const dragClone = document.querySelector(`swp-allday-container swp-event[data-event-id="clone-${eventId}"]`); console.log('🎯 AllDayManager: Ending drag for all-day event', { eventId }); @@ -167,17 +167,6 @@ const dragClone = document.querySelector(`swp-allday-container swp-event[data-ev this.cachedHeaderSpacer = null; } - /** - * Expand all-day row to show events - */ - public expandAllDayRow(): void { - const { currentHeight } = this.calculateAllDayHeight(0); - - if (currentHeight === 0) { - this.checkAndAnimateAllDayHeight(); - } - } - /** * Collapse all-day row when no events */ @@ -191,7 +180,8 @@ const dragClone = document.querySelector(`swp-allday-container swp-event[data-ev public checkAndAnimateAllDayHeight(): void { const container = this.getAllDayContainer(); if (!container) return; -const allDayEvents = container.querySelectorAll('swp-event'); + + const allDayEvents = container.querySelectorAll('swp-event'); // Calculate required rows - 0 if no events (will collapse) @@ -297,103 +287,123 @@ const allDayEvents = container.querySelectorAll('swp-event'); } /** - * Handle conversion of timed event to all-day event + * Handle conversion of timed event to all-day event using CSS styling */ private handleConvertToAllDay(targetDate: string, cloneElement: HTMLElement): void { - // Extract event data from original element - const eventId = cloneElement.dataset.eventId; - const title = cloneElement.dataset.title || cloneElement.textContent || 'Untitled'; - const type = cloneElement.dataset.type || 'work'; - const startStr = cloneElement.dataset.start; - const endStr = cloneElement.dataset.end; + console.log('🔄 AllDayManager: Converting to all-day using CSS approach', { + eventId: cloneElement.dataset.eventId, + targetDate + }); - if (!eventId || !startStr || !endStr) { - console.error('Original element missing required data (eventId, start, end)'); - return; - } - //we just hide it, it will only be removed on mouse up - cloneElement.style.display = 'none'; + // Get all-day container, request creation if needed + let allDayContainer = this.getAllDayContainer(); + if (!allDayContainer) { + console.log('🔄 AllDayManager: All-day container not found, requesting creation...'); + // Request HeaderManager to create container + eventBus.emit('header:ensure-allday-container'); - // Create CalendarEvent for all-day conversion - preserve original times - const originalStart = new Date(startStr); - const originalEnd = new Date(endStr); - - // Set date to target date but keep original time - const targetStart = new Date(targetDate); - targetStart.setHours(originalStart.getHours(), originalStart.getMinutes(), originalStart.getSeconds(), originalStart.getMilliseconds()); - - const targetEnd = new Date(targetDate); - targetEnd.setHours(originalEnd.getHours(), originalEnd.getMinutes(), originalEnd.getSeconds(), originalEnd.getMilliseconds()); - - const calendarEvent: CalendarEvent = { - id: eventId, - title: title, - start: targetStart, - end: targetEnd, - type: type, - allDay: true, - syncStatus: 'synced', - metadata: { - duration: cloneElement.dataset.duration || '60' + // Try again after request + allDayContainer = this.getAllDayContainer(); + if (!allDayContainer) { + console.error('All-day container still not found after creation request'); + return; } - }; - - // Check if all-day clone already exists for this event ID - const existingAllDayEvent = document.querySelector(`swp-allday-container swp-event[data-event-id="${eventId}"]`); - if (existingAllDayEvent) { - // All-day event already exists, just ensure clone is hidden - const dragClone = document.querySelector(`swp-event[data-event-id="clone-${eventId}"]`); - if (dragClone) { - (dragClone as HTMLElement).style.display = 'none'; - } - return; } - // Use renderer to create and add all-day event - const allDayElement = this.allDayEventRenderer.renderAllDayEvent(calendarEvent, targetDate); + // Move clone element to all-day container + allDayContainer.appendChild(cloneElement); - if (allDayElement) { - // Hide drag clone completely - const dragClone = document.querySelector(`swp-event[data-event-id="clone-${eventId}"]`); - if (dragClone) { - (dragClone as HTMLElement).style.display = 'none'; - } + // Add CSS class for all-day styling + cloneElement.classList.add('all-day-style'); - // Animate height change - this.checkAndAnimateAllDayHeight(); - } - } + // Store target date for positioning + cloneElement.dataset.allDayDate = targetDate; + // Calculate and set grid column based on targetDate + const columnIndex = this.getColumnIndexForDate(targetDate); + cloneElement.style.gridColumn = columnIndex.toString(); - /** - * Update row height when all-day events change - */ - public updateRowHeight(): void { - this.checkAndAnimateAllDayHeight(); + // Find available row and set grid row + const availableRow = this.findAvailableRow(targetDate); + cloneElement.style.gridRow = availableRow.toString(); + + // Show the element (ensure it's visible) + cloneElement.style.display = ''; + + console.log('✅ AllDayManager: Converted to all-day style', { + eventId: cloneElement.dataset.eventId, + gridColumn: columnIndex, + gridRow: availableRow + }); } /** - * Ensure all-day container exists, create if needed + * Get column index for a specific date */ - public ensureAllDayContainer(): HTMLElement | null { - console.log('🔍 AllDayManager: Checking if all-day container exists...'); + private 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; + } - // Try to get existing container first - let container = this.getAllDayContainer(); + /** + * Find available row for all-day event in specific date column + */ + private findAvailableRow(targetDate: string): number { + const container = this.getAllDayContainer(); + if (!container) return 1; - if (!container) { + const columnIndex = this.getColumnIndexForDate(targetDate); + const existingEvents = container.querySelectorAll('swp-event'); + const occupiedRows = new Set(); - this.allDayEventRenderer.clearCache(); // Clear cache to force re-check + existingEvents.forEach(event => { + const style = getComputedStyle(event); + const eventStartCol = parseInt(style.gridColumnStart); + const eventRow = parseInt(style.gridRowStart) || 1; - const header = this.getCalendarHeader(); - container = document.createElement('swp-allday-container'); - header?.appendChild(container); - - this.cachedAllDayContainer = container; + // Only check events in the same column + if (eventStartCol === columnIndex) { + occupiedRows.add(eventRow); + } + }); + // Find first available row + let targetRow = 1; + while (occupiedRows.has(targetRow)) { + targetRow++; } - return container; + return targetRow; + } + + /** + * Handle conversion from all-day back to timed event + */ + private handleConvertFromAllDay(cloneElement: HTMLElement): void { + console.log('🔄 AllDayManager: Converting from all-day back to timed', { + eventId: cloneElement.dataset.eventId + }); + + // Remove all-day CSS class + cloneElement.classList.remove('all-day-style'); + + // Reset grid positioning + cloneElement.style.gridColumn = ''; + cloneElement.style.gridRow = ''; + + // Remove all-day date attribute + delete cloneElement.dataset.allDayDate; + + // Move back to appropriate day column (will be handled by drag logic) + // The drag system will position it correctly + + console.log('✅ AllDayManager: Converted from all-day back to timed'); } /** @@ -460,9 +470,7 @@ const allDayEvents = container.querySelectorAll('swp-event'); * Handle drag end for all-day events */ private handleDragEnd(originalElement: HTMLElement, dragClone: HTMLElement, finalPosition: any): void { - // Remove original element - originalElement?.remove(); - + // Normalize clone const cloneId = dragClone.dataset.eventId; if (cloneId?.startsWith('clone-')) { @@ -475,8 +483,7 @@ const allDayEvents = container.querySelectorAll('swp-event'); dragClone.style.cursor = ''; dragClone.style.opacity = ''; - // Recalculate all-day container height - this.checkAndAnimateAllDayHeight(); + console.log('✅ AllDayManager: Completed drag operation for all-day event', { eventId: dragClone.dataset.eventId, diff --git a/src/managers/DragDropManager.ts b/src/managers/DragDropManager.ts index 78a3dd5..b4004b0 100644 --- a/src/managers/DragDropManager.ts +++ b/src/managers/DragDropManager.ts @@ -260,6 +260,7 @@ export class DragDropManager { // Clean up drag state first this.cleanupDragState(); + // Only emit drag:end if drag was actually started if (isDragStarted) { @@ -286,6 +287,9 @@ export class DragDropManager { target: dropTarget }; this.eventBus.emit('drag:end', dragEndPayload); + + draggedElement.remove(); + } else { // This was just a click - emit click event instead this.eventBus.emit('event:click', { @@ -295,6 +299,12 @@ export class DragDropManager { } } } + // Add a cleanup method that finds and removes ALL clones + private cleanupAllClones(): void { + // Remove clones from all possible locations + const allClones = document.querySelectorAll('[data-event-id^="clone"]'); + allClones.forEach(clone => clone.remove()); + } /** * Consolidated position calculation method using PositionUtils diff --git a/src/managers/HeaderManager.ts b/src/managers/HeaderManager.ts index 0a4e051..1f9d1b3 100644 --- a/src/managers/HeaderManager.ts +++ b/src/managers/HeaderManager.ts @@ -24,6 +24,9 @@ export class HeaderManager { // Listen for navigation events to update header this.setupNavigationListener(); + + // Listen for requests to ensure all-day container + this.setupContainerRequestListener(); } /** @@ -95,18 +98,23 @@ export class HeaderManager { } /** - * Ensure all-day container exists in header + * Ensure all-day container exists in header - creates directly */ - private ensureAllDayContainer(): void { + private ensureAllDayContainer(): HTMLElement | null { const calendarHeader = this.getCalendarHeader(); - if (!calendarHeader) return; + if (!calendarHeader) return null; - let allDayContainer = calendarHeader.querySelector('swp-allday-container'); + let allDayContainer = calendarHeader.querySelector('swp-allday-container') as HTMLElement; if (!allDayContainer) { - console.log('📍 HeaderManager: All-day container missing, requesting creation...'); - eventBus.emit('allday:ensure-container'); + console.log('📍 HeaderManager: Creating all-day container directly...'); + allDayContainer = document.createElement('swp-allday-container'); + calendarHeader.appendChild(allDayContainer); + + console.log('✅ HeaderManager: All-day container created'); } + + return allDayContainer; } @@ -134,6 +142,16 @@ export class HeaderManager { } + /** + * Setup listener for all-day container creation requests + */ + private setupContainerRequestListener(): void { + eventBus.on('header:ensure-allday-container', () => { + console.log('📍 HeaderManager: Received request to ensure all-day container'); + this.ensureAllDayContainer(); + }); + } + /** * Update header content for navigation */ diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index ba05ba1..e2e7234 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -4,9 +4,8 @@ import { CalendarEvent } from '../types/CalendarTypes'; import { calendarConfig } from '../core/CalendarConfig'; import { DateCalculator } from '../utils/DateCalculator'; import { eventBus } from '../core/EventBus'; -import { CoreEvents } from '../constants/CoreEvents'; -import { OverlapDetector, OverlapResult, EventId } from '../utils/OverlapDetector'; -import { SwpEventElement, SwpAllDayEventElement } from '../elements/SwpEventElement'; +import { OverlapDetector, OverlapResult } from '../utils/OverlapDetector'; +import { SwpEventElement } from '../elements/SwpEventElement'; import { TimeFormatter } from '../utils/TimeFormatter'; import { PositionUtils } from '../utils/PositionUtils'; diff --git a/src/renderers/EventRendererManager.ts b/src/renderers/EventRendererManager.ts index f567b35..f8f34e8 100644 --- a/src/renderers/EventRendererManager.ts +++ b/src/renderers/EventRendererManager.ts @@ -193,7 +193,7 @@ export class EventRenderingService { } // Clean up any remaining day event clones - const dayEventClone = document.querySelector(`swp-event[data-event-id="clone-${eventId}"]`); + const dayEventClone = document.querySelector(`swp-day-column swp-event[data-event-id="clone-${eventId}"]`); if (dayEventClone) { dayEventClone.remove(); } diff --git a/wwwroot/css/calendar-layout-css.css b/wwwroot/css/calendar-layout-css.css index 2908a32..9e32e74 100644 --- a/wwwroot/css/calendar-layout-css.css +++ b/wwwroot/css/calendar-layout-css.css @@ -299,8 +299,19 @@ swp-allday-column { } /* All-day events in containers */ -swp-allday-container swp-event { - height: 22px; /* Fixed height for consistent stacking */ +swp-allday-container swp-event, +swp-event.all-day-style { + height: 22px !important; /* Fixed height for consistent stacking */ + position: relative !important; + width: auto !important; + left: auto !important; + right: auto !important; + top: auto !important; + padding: 2px 4px; + margin-bottom: 2px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; background: #ff9800; /* Default orange background */ display: flex; position: relative; @@ -317,10 +328,25 @@ swp-allday-container swp-event { border-left: 3px solid rgba(0, 0, 0, 0.2); } -swp-allday-container swp-event:last-child { +swp-allday-container swp-event:last-child, +swp-event.all-day-style:last-child { margin-bottom: 0; } +/* Hide time element for all-day styled events */ +swp-allday-container swp-event swp-event-time, +swp-event.all-day-style swp-event-time { + display: none; +} + +/* Adjust title display for all-day styled events */ +swp-allday-container swp-event swp-event-title, +swp-event.all-day-style swp-event-title { + display: block; + font-size: 12px; + line-height: 18px; +} + /* Scrollable content */ swp-scrollable-content { overflow-y: auto;