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.
This commit is contained in:
Janus C. H. Knudsen 2025-09-21 21:30:51 +02:00
parent 2cdbc8f1a3
commit c682c30e23
6 changed files with 181 additions and 121 deletions

View file

@ -22,11 +22,7 @@ export class AllDayManager {
private allDayEventRenderer: AllDayEventRenderer; private allDayEventRenderer: AllDayEventRenderer;
constructor() { constructor() {
// Bind methods for event listeners
this.checkAndAnimateAllDayHeight = this.checkAndAnimateAllDayHeight.bind(this);
this.allDayEventRenderer = new AllDayEventRenderer(); this.allDayEventRenderer = new AllDayEventRenderer();
// Listen for drag-to-allday conversions
this.setupEventListeners(); this.setupEventListeners();
} }
@ -38,7 +34,7 @@ export class AllDayManager {
eventBus.on('drag:mouseenter-header', (event) => { eventBus.on('drag:mouseenter-header', (event) => {
const { targetDate, mousePosition, originalElement, cloneElement } = (event as CustomEvent<DragMouseEnterHeaderEventPayload>).detail; const { targetDate, mousePosition, originalElement, cloneElement } = (event as CustomEvent<DragMouseEnterHeaderEventPayload>).detail;
console.log('🔄 AllDayManager: Received drag:mouseenter-header', { console.log('🔄 AllDayManager: Received drag:mouseenter-header', {
targetDate, targetDate,
originalElementId: originalElement?.dataset?.eventId, originalElementId: originalElement?.dataset?.eventId,
@ -48,25 +44,29 @@ export class AllDayManager {
if (targetDate && cloneElement) { if (targetDate && cloneElement) {
this.handleConvertToAllDay(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 console.log('🚪 AllDayManager: Received drag:mouseleave-header', {
eventBus.on('allday:ensure-container', () => { originalElementId: originalElement?.dataset?.eventId
console.log('🏗️ AllDayManager: Received request to ensure all-day container exists'); });
this.ensureAllDayContainer();
}); 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 // Listen for drag operations on all-day events
eventBus.on('drag:start', (event) => { eventBus.on('drag:start', (event) => {
const { draggedElement, mouseOffset } = (event as CustomEvent<DragStartEventPayload>).detail; const { draggedElement, mouseOffset } = (event as CustomEvent<DragStartEventPayload>).detail;
// Check if this is an all-day event by checking if it's in all-day container // Check if this is an all-day event by checking if it's in all-day container
const isAllDayEvent = draggedElement.closest('swp-allday-container'); const isAllDayEvent = draggedElement.closest('swp-allday-container');
if (!isAllDayEvent) return; // Not an all-day event if (!isAllDayEvent) return; // Not an all-day event
@ -101,7 +101,7 @@ export class AllDayManager {
eventId: eventId, eventId: eventId,
finalPosition 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 }); 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; 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 * 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 { public checkAndAnimateAllDayHeight(): void {
const container = this.getAllDayContainer(); const container = this.getAllDayContainer();
if (!container) return; if (!container) return;
const allDayEvents = container.querySelectorAll('swp-event');
const allDayEvents = container.querySelectorAll('swp-event');
// Calculate required rows - 0 if no events (will collapse) // 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 { private handleConvertToAllDay(targetDate: string, cloneElement: HTMLElement): void {
// Extract event data from original element console.log('🔄 AllDayManager: Converting to all-day using CSS approach', {
const eventId = cloneElement.dataset.eventId; eventId: cloneElement.dataset.eventId,
const title = cloneElement.dataset.title || cloneElement.textContent || 'Untitled'; targetDate
const type = cloneElement.dataset.type || 'work'; });
const startStr = cloneElement.dataset.start;
const endStr = cloneElement.dataset.end;
if (!eventId || !startStr || !endStr) { // Get all-day container, request creation if needed
console.error('Original element missing required data (eventId, start, end)'); let allDayContainer = this.getAllDayContainer();
return; if (!allDayContainer) {
} console.log('🔄 AllDayManager: All-day container not found, requesting creation...');
//we just hide it, it will only be removed on mouse up // Request HeaderManager to create container
cloneElement.style.display = 'none'; eventBus.emit('header:ensure-allday-container');
// Create CalendarEvent for all-day conversion - preserve original times // Try again after request
const originalStart = new Date(startStr); allDayContainer = this.getAllDayContainer();
const originalEnd = new Date(endStr); if (!allDayContainer) {
console.error('All-day container still not found after creation request');
// Set date to target date but keep original time return;
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'
} }
};
// 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 // Move clone element to all-day container
const allDayElement = this.allDayEventRenderer.renderAllDayEvent(calendarEvent, targetDate); allDayContainer.appendChild(cloneElement);
if (allDayElement) { // Add CSS class for all-day styling
// Hide drag clone completely cloneElement.classList.add('all-day-style');
const dragClone = document.querySelector(`swp-event[data-event-id="clone-${eventId}"]`);
if (dragClone) {
(dragClone as HTMLElement).style.display = 'none';
}
// Animate height change // Store target date for positioning
this.checkAndAnimateAllDayHeight(); cloneElement.dataset.allDayDate = targetDate;
}
}
// Calculate and set grid column based on targetDate
const columnIndex = this.getColumnIndexForDate(targetDate);
cloneElement.style.gridColumn = columnIndex.toString();
/** // Find available row and set grid row
* Update row height when all-day events change const availableRow = this.findAvailableRow(targetDate);
*/ cloneElement.style.gridRow = availableRow.toString();
public updateRowHeight(): void {
this.checkAndAnimateAllDayHeight(); // 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 { private getColumnIndexForDate(targetDate: string): number {
console.log('🔍 AllDayManager: Checking if all-day container exists...'); 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<number>();
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(); // Only check events in the same column
container = document.createElement('swp-allday-container'); if (eventStartCol === columnIndex) {
header?.appendChild(container); occupiedRows.add(eventRow);
}
this.cachedAllDayContainer = container; });
// 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 * Handle drag end for all-day events
*/ */
private handleDragEnd(originalElement: HTMLElement, dragClone: HTMLElement, finalPosition: any): void { private handleDragEnd(originalElement: HTMLElement, dragClone: HTMLElement, finalPosition: any): void {
// Remove original element
originalElement?.remove();
// Normalize clone // Normalize clone
const cloneId = dragClone.dataset.eventId; const cloneId = dragClone.dataset.eventId;
if (cloneId?.startsWith('clone-')) { if (cloneId?.startsWith('clone-')) {
@ -475,8 +483,7 @@ const allDayEvents = container.querySelectorAll('swp-event');
dragClone.style.cursor = ''; dragClone.style.cursor = '';
dragClone.style.opacity = ''; dragClone.style.opacity = '';
// Recalculate all-day container height
this.checkAndAnimateAllDayHeight();
console.log('✅ AllDayManager: Completed drag operation for all-day event', { console.log('✅ AllDayManager: Completed drag operation for all-day event', {
eventId: dragClone.dataset.eventId, eventId: dragClone.dataset.eventId,

View file

@ -260,6 +260,7 @@ export class DragDropManager {
// Clean up drag state first // Clean up drag state first
this.cleanupDragState(); this.cleanupDragState();
// Only emit drag:end if drag was actually started // Only emit drag:end if drag was actually started
if (isDragStarted) { if (isDragStarted) {
@ -286,6 +287,9 @@ export class DragDropManager {
target: dropTarget target: dropTarget
}; };
this.eventBus.emit('drag:end', dragEndPayload); this.eventBus.emit('drag:end', dragEndPayload);
draggedElement.remove();
} else { } else {
// This was just a click - emit click event instead // This was just a click - emit click event instead
this.eventBus.emit('event:click', { 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 * Consolidated position calculation method using PositionUtils

View file

@ -24,6 +24,9 @@ export class HeaderManager {
// Listen for navigation events to update header // Listen for navigation events to update header
this.setupNavigationListener(); 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(); 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) { if (!allDayContainer) {
console.log('📍 HeaderManager: All-day container missing, requesting creation...'); console.log('📍 HeaderManager: Creating all-day container directly...');
eventBus.emit('allday:ensure-container'); 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 * Update header content for navigation
*/ */

View file

@ -4,9 +4,8 @@ import { CalendarEvent } from '../types/CalendarTypes';
import { calendarConfig } from '../core/CalendarConfig'; import { calendarConfig } from '../core/CalendarConfig';
import { DateCalculator } from '../utils/DateCalculator'; import { DateCalculator } from '../utils/DateCalculator';
import { eventBus } from '../core/EventBus'; import { eventBus } from '../core/EventBus';
import { CoreEvents } from '../constants/CoreEvents'; import { OverlapDetector, OverlapResult } from '../utils/OverlapDetector';
import { OverlapDetector, OverlapResult, EventId } from '../utils/OverlapDetector'; import { SwpEventElement } from '../elements/SwpEventElement';
import { SwpEventElement, SwpAllDayEventElement } from '../elements/SwpEventElement';
import { TimeFormatter } from '../utils/TimeFormatter'; import { TimeFormatter } from '../utils/TimeFormatter';
import { PositionUtils } from '../utils/PositionUtils'; import { PositionUtils } from '../utils/PositionUtils';

View file

@ -193,7 +193,7 @@ export class EventRenderingService {
} }
// Clean up any remaining day event clones // 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) { if (dayEventClone) {
dayEventClone.remove(); dayEventClone.remove();
} }

View file

@ -299,8 +299,19 @@ swp-allday-column {
} }
/* All-day events in containers */ /* All-day events in containers */
swp-allday-container swp-event { swp-allday-container swp-event,
height: 22px; /* Fixed height for consistent stacking */ 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 */ background: #ff9800; /* Default orange background */
display: flex; display: flex;
position: relative; position: relative;
@ -317,10 +328,25 @@ swp-allday-container swp-event {
border-left: 3px solid rgba(0, 0, 0, 0.2); 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; 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 */ /* Scrollable content */
swp-scrollable-content { swp-scrollable-content {
overflow-y: auto; overflow-y: auto;