Enables all-day event drag and drop

Implements comprehensive drag and drop for all-day events, allowing movement within the header and conversion to timed events when dragged into the calendar grid.

Optimizes column detection with a cached bounding box strategy, improving performance and accuracy. Refactors event conversion logic and renames related event bus events for clarity.
This commit is contained in:
Janus C. H. Knudsen 2025-09-19 00:20:30 +02:00
parent f1d04ae12e
commit 0b7499521e
6 changed files with 338 additions and 106 deletions

View file

@ -17,7 +17,7 @@ Når en day event dragges op til headeren (for at konvertere til all-day) og der
### Trin 3: Mouse enters Header ⚠️ PROBLEM STARTER HER ### Trin 3: Mouse enters Header ⚠️ PROBLEM STARTER HER
- **DragDropManager** (linje 95-112): Lytter til `header:mouseover` - **DragDropManager** (linje 95-112): Lytter til `header:mouseover`
- Emitter `drag:convert-to-allday` event - Emitter `drag:convert-to-allday_event` event
- **AllDayManager** (linje 232-285): `handleConvertToAllDay()`: - **AllDayManager** (linje 232-285): `handleConvertToAllDay()`:
- Opretter all-day event i header - Opretter all-day event i header
- **FJERNER original timed event permanent** (linje 274: `originalElement.remove()`) - **FJERNER original timed event permanent** (linje 274: `originalElement.remove()`)
@ -25,7 +25,7 @@ Når en day event dragges op til headeren (for at konvertere til all-day) og der
### Trin 4: Mouse leaves Header (tilbage til grid) ⚠️ PROBLEM FORTSÆTTER ### Trin 4: Mouse leaves Header (tilbage til grid) ⚠️ PROBLEM FORTSÆTTER
- **DragDropManager** (linje 128-136): Lytter til `header:mouseleave` - **DragDropManager** (linje 128-136): Lytter til `header:mouseleave`
- Emitter `drag:convert-from-allday` event - Emitter `drag:convert-to-time_event` event
- **AllDayManager** (linje 290-311): `handleConvertFromAllDay()`: - **AllDayManager** (linje 290-311): `handleConvertFromAllDay()`:
- Fjerner all-day event fra container - Fjerner all-day event fra container
- Viser drag clone igen - Viser drag clone igen

View file

@ -24,7 +24,7 @@ sequenceDiagram
Note over Mouse: Dragger over header Note over Mouse: Dragger over header
loop Hver mouseover event loop Hver mouseover event
Mouse->>Header: mouseover Mouse->>Header: mouseover
Header->>AllDay: drag:convert-to-allday Header->>AllDay: drag:convert-to-allday_event
AllDay->>AllDay: Opretter NYT all-day event ❌ AllDay->>AllDay: Opretter NYT all-day event ❌
Note over AllDay: Ingen check for eksisterende! Note over AllDay: Ingen check for eksisterende!
end end

View file

@ -133,7 +133,7 @@ eventBus.on('header:mouseover', (event) => {
if (draggedElement) { if (draggedElement) {
console.log('🎯 Converting to all-day for date:', targetDate); console.log('🎯 Converting to all-day for date:', targetDate);
this.eventBus.emit('drag:convert-to-allday', { this.eventBus.emit('drag:convert-to-allday_event', {
targetDate, targetDate,
originalElement: draggedElement, originalElement: draggedElement,
headerRenderer: (event as CustomEvent).detail.headerRenderer headerRenderer: (event as CustomEvent).detail.headerRenderer

View file

@ -19,7 +19,7 @@ export class AllDayManager {
// Bind methods for event listeners // Bind methods for event listeners
this.checkAndAnimateAllDayHeight = this.checkAndAnimateAllDayHeight.bind(this); this.checkAndAnimateAllDayHeight = this.checkAndAnimateAllDayHeight.bind(this);
this.allDayEventRenderer = new AllDayEventRenderer(); this.allDayEventRenderer = new AllDayEventRenderer();
// Listen for drag-to-allday conversions // Listen for drag-to-allday conversions
this.setupEventListeners(); this.setupEventListeners();
} }
@ -28,23 +28,16 @@ export class AllDayManager {
* Setup event listeners for drag conversions * Setup event listeners for drag conversions
*/ */
private setupEventListeners(): void { private setupEventListeners(): void {
eventBus.on('drag:convert-to-allday', (event) => { eventBus.on('drag:convert-to-allday_event', (event) => {
const { targetDate, originalElement } = (event as CustomEvent).detail; const { targetDate, originalElement } = (event as CustomEvent).detail;
console.log('🔄 AllDayManager: Received drag:convert-to-allday', { console.log('🔄 AllDayManager: Received drag:convert-to-allday_event', {
targetDate, targetDate,
originalElementId: originalElement?.dataset?.eventId, originalElementId: originalElement?.dataset?.eventId,
originalElementTag: originalElement?.tagName originalElementTag: originalElement?.tagName
}); });
this.handleConvertToAllDay(targetDate, originalElement); this.handleConvertToAllDay(targetDate, originalElement);
}); });
eventBus.on('drag:convert-from-allday', (event) => {
const { draggedEventId } = (event as CustomEvent).detail;
console.log('🔄 AllDayManager: Received drag:convert-from-allday', {
draggedEventId
});
this.handleConvertFromAllDay(draggedEventId);
});
// Listen for requests to ensure all-day container exists // Listen for requests to ensure all-day container exists
eventBus.on('allday:ensure-container', () => { eventBus.on('allday:ensure-container', () => {
@ -57,6 +50,39 @@ export class AllDayManager {
console.log('🔄 AllDayManager: Received header:mouseleave, recalculating height'); console.log('🔄 AllDayManager: Received header:mouseleave, recalculating height');
this.checkAndAnimateAllDayHeight(); this.checkAndAnimateAllDayHeight();
}); });
// Listen for drag operations on all-day events
eventBus.on('drag:start', (event) => {
const { eventId, mouseOffset } = (event as CustomEvent).detail;
// Check if this is an all-day event
const originalElement = document.querySelector(`swp-allday-container swp-allday-event[data-event-id="${eventId}"]`);
if (!originalElement) return; // Not an all-day event
console.log('🎯 AllDayManager: Starting drag for all-day event', { eventId });
this.handleDragStart(originalElement as HTMLElement, eventId, mouseOffset);
});
eventBus.on('drag:move', (event) => {
const { eventId, mousePosition } = (event as CustomEvent).detail;
// Only handle for all-day events
const dragClone = document.querySelector(`swp-allday-container swp-allday-event[data-event-id="clone-${eventId}"]`);
if (dragClone) {
this.handleDragMove(dragClone as HTMLElement, mousePosition);
}
});
eventBus.on('drag:end', (event) => {
const { eventId, finalPosition } = (event as CustomEvent).detail;
// 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);
});
} }
/** /**
@ -104,7 +130,7 @@ export class AllDayManager {
const targetHeight = targetRows * ALL_DAY_CONSTANTS.SINGLE_ROW_HEIGHT; const targetHeight = targetRows * ALL_DAY_CONSTANTS.SINGLE_ROW_HEIGHT;
const currentHeight = parseInt(getComputedStyle(root).getPropertyValue('--all-day-row-height') || '0'); const currentHeight = parseInt(getComputedStyle(root).getPropertyValue('--all-day-row-height') || '0');
const heightDifference = targetHeight - currentHeight; const heightDifference = targetHeight - currentHeight;
return { targetHeight, currentHeight, heightDifference }; return { targetHeight, currentHeight, heightDifference };
} }
@ -122,7 +148,7 @@ export class AllDayManager {
*/ */
public expandAllDayRow(): void { public expandAllDayRow(): void {
const { currentHeight } = this.calculateAllDayHeight(0); const { currentHeight } = this.calculateAllDayHeight(0);
if (currentHeight === 0) { if (currentHeight === 0) {
this.checkAndAnimateAllDayHeight(); this.checkAndAnimateAllDayHeight();
} }
@ -141,49 +167,49 @@ export class AllDayManager {
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-allday-event'); const allDayEvents = container.querySelectorAll('swp-allday-event');
// Calculate required rows - 0 if no events (will collapse) // Calculate required rows - 0 if no events (will collapse)
let maxRows = 0; let maxRows = 0;
if (allDayEvents.length > 0) { if (allDayEvents.length > 0) {
// Expand events to all dates they span and group by date // Expand events to all dates they span and group by date
const expandedEventsByDate: Record<string, string[]> = {}; const expandedEventsByDate: Record<string, string[]> = {};
(Array.from(allDayEvents) as HTMLElement[]).forEach((event: HTMLElement) => { (Array.from(allDayEvents) as HTMLElement[]).forEach((event: HTMLElement) => {
const startISO = event.dataset.start || ''; const startISO = event.dataset.start || '';
const endISO = event.dataset.end || startISO; const endISO = event.dataset.end || startISO;
const eventId = event.dataset.eventId || ''; const eventId = event.dataset.eventId || '';
// Extract dates from ISO strings // Extract dates from ISO strings
const startDate = startISO.split('T')[0]; // YYYY-MM-DD const startDate = startISO.split('T')[0]; // YYYY-MM-DD
const endDate = endISO.split('T')[0]; // YYYY-MM-DD const endDate = endISO.split('T')[0]; // YYYY-MM-DD
// Loop through all dates from start to end // Loop through all dates from start to end
let current = new Date(startDate); let current = new Date(startDate);
const end = new Date(endDate); const end = new Date(endDate);
while (current <= end) { while (current <= end) {
const dateStr = current.toISOString().split('T')[0]; // YYYY-MM-DD format const dateStr = current.toISOString().split('T')[0]; // YYYY-MM-DD format
if (!expandedEventsByDate[dateStr]) { if (!expandedEventsByDate[dateStr]) {
expandedEventsByDate[dateStr] = []; expandedEventsByDate[dateStr] = [];
} }
expandedEventsByDate[dateStr].push(eventId); expandedEventsByDate[dateStr].push(eventId);
// Move to next day // Move to next day
current.setDate(current.getDate() + 1); current.setDate(current.getDate() + 1);
} }
}); });
// Find max rows needed // Find max rows needed
maxRows = Math.max( maxRows = Math.max(
...Object.values(expandedEventsByDate).map(ids => ids?.length || 0), ...Object.values(expandedEventsByDate).map(ids => ids?.length || 0),
0 0
); );
} }
// Animate to required rows (0 = collapse, >0 = expand) // Animate to required rows (0 = collapse, >0 = expand)
this.animateToRows(maxRows); this.animateToRows(maxRows);
} }
@ -193,22 +219,22 @@ export class AllDayManager {
*/ */
public animateToRows(targetRows: number): void { public animateToRows(targetRows: number): void {
const { targetHeight, currentHeight, heightDifference } = this.calculateAllDayHeight(targetRows); const { targetHeight, currentHeight, heightDifference } = this.calculateAllDayHeight(targetRows);
if (targetHeight === currentHeight) return; // No animation needed if (targetHeight === currentHeight) return; // No animation needed
console.log(`🎬 All-day height animation: ${currentHeight}px → ${targetHeight}px (${Math.ceil(currentHeight / ALL_DAY_CONSTANTS.SINGLE_ROW_HEIGHT)}${targetRows} rows)`); console.log(`🎬 All-day height animation: ${currentHeight}px → ${targetHeight}px (${Math.ceil(currentHeight / ALL_DAY_CONSTANTS.SINGLE_ROW_HEIGHT)}${targetRows} rows)`);
// Get cached elements // Get cached elements
const calendarHeader = this.getCalendarHeader(); const calendarHeader = this.getCalendarHeader();
const headerSpacer = this.getHeaderSpacer(); const headerSpacer = this.getHeaderSpacer();
const allDayContainer = this.getAllDayContainer(); const allDayContainer = this.getAllDayContainer();
if (!calendarHeader || !allDayContainer) return; if (!calendarHeader || !allDayContainer) return;
// Get current parent height for animation // Get current parent height for animation
const currentParentHeight = parseFloat(getComputedStyle(calendarHeader).height); const currentParentHeight = parseFloat(getComputedStyle(calendarHeader).height);
const targetParentHeight = currentParentHeight + heightDifference; const targetParentHeight = currentParentHeight + heightDifference;
const animations = [ const animations = [
calendarHeader.animate([ calendarHeader.animate([
{ height: `${currentParentHeight}px` }, { height: `${currentParentHeight}px` },
@ -219,13 +245,13 @@ export class AllDayManager {
fill: 'forwards' fill: 'forwards'
}) })
]; ];
// Add spacer animation if spacer exists // Add spacer animation if spacer exists
if (headerSpacer) { if (headerSpacer) {
const root = document.documentElement; const root = document.documentElement;
const currentSpacerHeight = parseInt(getComputedStyle(root).getPropertyValue('--header-height')) + currentHeight; const currentSpacerHeight = parseInt(getComputedStyle(root).getPropertyValue('--header-height')) + currentHeight;
const targetSpacerHeight = parseInt(getComputedStyle(root).getPropertyValue('--header-height')) + targetHeight; const targetSpacerHeight = parseInt(getComputedStyle(root).getPropertyValue('--header-height')) + targetHeight;
animations.push( animations.push(
headerSpacer.animate([ headerSpacer.animate([
{ height: `${currentSpacerHeight}px` }, { height: `${currentSpacerHeight}px` },
@ -237,7 +263,7 @@ export class AllDayManager {
}) })
); );
} }
// Update CSS variable after animation // Update CSS variable after animation
Promise.all(animations.map(anim => anim.finished)).then(() => { Promise.all(animations.map(anim => anim.finished)).then(() => {
const root = document.documentElement; const root = document.documentElement;
@ -265,16 +291,16 @@ export class AllDayManager {
// Create CalendarEvent for all-day conversion - preserve original times // Create CalendarEvent for all-day conversion - preserve original times
const originalStart = new Date(startStr); const originalStart = new Date(startStr);
const originalEnd = new Date(endStr); const originalEnd = new Date(endStr);
// Set date to target date but keep original time // Set date to target date but keep original time
const targetStart = new Date(targetDate); const targetStart = new Date(targetDate);
targetStart.setHours(originalStart.getHours(), originalStart.getMinutes(), originalStart.getSeconds(), originalStart.getMilliseconds()); targetStart.setHours(originalStart.getHours(), originalStart.getMinutes(), originalStart.getSeconds(), originalStart.getMilliseconds());
const targetEnd = new Date(targetDate); const targetEnd = new Date(targetDate);
targetEnd.setHours(originalEnd.getHours(), originalEnd.getMinutes(), originalEnd.getSeconds(), originalEnd.getMilliseconds()); targetEnd.setHours(originalEnd.getHours(), originalEnd.getMinutes(), originalEnd.getSeconds(), originalEnd.getMilliseconds());
const calendarEvent: CalendarEvent = { const calendarEvent: CalendarEvent = {
id: eventId, id: `clone-${eventId}`,
title: title, title: title,
start: targetStart, start: targetStart,
end: targetEnd, end: targetEnd,
@ -286,8 +312,8 @@ export class AllDayManager {
} }
}; };
// Check if all-day event already exists for this event ID // Check if all-day clone already exists for this event ID
const existingAllDayEvent = document.querySelector(`swp-allday-container swp-allday-event[data-event-id="${eventId}"]`); const existingAllDayEvent = document.querySelector(`swp-allday-container swp-allday-event[data-event-id="clone-${eventId}"]`);
if (existingAllDayEvent) { if (existingAllDayEvent) {
// All-day event already exists, just ensure clone is hidden // All-day event already exists, just ensure clone is hidden
const dragClone = document.querySelector(`swp-event[data-event-id="clone-${eventId}"]`); const dragClone = document.querySelector(`swp-event[data-event-id="clone-${eventId}"]`);
@ -299,44 +325,19 @@ export class AllDayManager {
// Use renderer to create and add all-day event // Use renderer to create and add all-day event
const allDayElement = this.allDayEventRenderer.renderAllDayEvent(calendarEvent, targetDate); const allDayElement = this.allDayEventRenderer.renderAllDayEvent(calendarEvent, targetDate);
if (allDayElement) { if (allDayElement) {
// Hide drag clone completely // Hide drag clone completely
const dragClone = document.querySelector(`swp-event[data-event-id="clone-${eventId}"]`); const dragClone = document.querySelector(`swp-event[data-event-id="clone-${eventId}"]`);
if (dragClone) { if (dragClone) {
(dragClone as HTMLElement).style.display = 'none'; (dragClone as HTMLElement).style.display = 'none';
} }
// Animate height change // Animate height change
this.checkAndAnimateAllDayHeight(); this.checkAndAnimateAllDayHeight();
} }
} }
/**
* Handle conversion from all-day event back to day event
*/
private handleConvertFromAllDay(draggedEventId: string): void {
// Find and remove all-day event specifically in the container
const allDayEvent = document.querySelector(`swp-allday-container swp-allday-event[data-event-id="${draggedEventId}"]`);
if (allDayEvent) {
allDayEvent.remove();
}
// Show drag clone again with reset styles
const dragClone = document.querySelector(`swp-event[data-event-id="clone-${draggedEventId}"]`);
if (dragClone) {
const clone = dragClone as HTMLElement;
// Reset to standard day event styles
clone.style.display = 'block';
clone.style.zIndex = ''; // Fjern drag z-index
clone.style.cursor = ''; // Fjern drag cursor
clone.style.opacity = ''; // Fjern evt. opacity
clone.style.transform = ''; // Fjern evt. transforms
// Position styles (top, height, left, right) bevares
}
}
/** /**
* Update row height when all-day events change * Update row height when all-day events change
@ -350,36 +351,114 @@ export class AllDayManager {
*/ */
public ensureAllDayContainer(): HTMLElement | null { public ensureAllDayContainer(): HTMLElement | null {
console.log('🔍 AllDayManager: Checking if all-day container exists...'); console.log('🔍 AllDayManager: Checking if all-day container exists...');
// Try to get existing container first // Try to get existing container first
let container = this.getAllDayContainer(); let container = this.getAllDayContainer();
if (!container) { if (!container) {
console.log('🏗️ AllDayManager: Container not found, creating via AllDayEventRenderer...');
// Use the renderer to create container (which will call getContainer internally)
this.allDayEventRenderer.clearCache(); // Clear cache to force re-check this.allDayEventRenderer.clearCache(); // Clear cache to force re-check
// The renderer's getContainer method will create the container if it doesn't exist
// We can trigger this by trying to get the container
const header = this.getCalendarHeader(); const header = this.getCalendarHeader();
if (header) { container = document.createElement('swp-allday-container');
container = document.createElement('swp-allday-container'); header?.appendChild(container);
header.appendChild(container);
console.log('✅ AllDayManager: Created all-day container'); this.cachedAllDayContainer = container;
// Update our cache
this.cachedAllDayContainer = container;
} else {
console.log('❌ AllDayManager: No calendar header found, cannot create container');
}
} else {
console.log('✅ AllDayManager: All-day container already exists');
} }
return container; return container;
} }
/**
* Handle drag start for all-day events
*/
private handleDragStart(originalElement: HTMLElement, eventId: string, mouseOffset: any): void {
// Create clone
const clone = originalElement.cloneNode(true) as HTMLElement;
clone.dataset.eventId = `clone-${eventId}`;
// Get container
const container = this.getAllDayContainer();
if (!container) return;
// Add clone to container
container.appendChild(clone);
// Copy positioning from original
clone.style.gridColumn = originalElement.style.gridColumn;
clone.style.gridRow = originalElement.style.gridRow;
// Add dragging style
clone.classList.add('dragging');
clone.style.zIndex = '1000';
clone.style.cursor = 'grabbing';
// Make original semi-transparent
originalElement.style.opacity = '0.3';
console.log('✅ AllDayManager: Created drag clone for all-day event', {
eventId,
cloneId: clone.dataset.eventId,
gridColumn: clone.style.gridColumn,
gridRow: clone.style.gridRow
});
}
/**
* Handle drag move for all-day events
*/
private handleDragMove(dragClone: HTMLElement, mousePosition: any): void {
// Calculate grid column based on mouse position
const dayHeaders = document.querySelectorAll('swp-day-header');
let targetColumn = 1;
dayHeaders.forEach((header, index) => {
const rect = header.getBoundingClientRect();
if (mousePosition.x >= rect.left && mousePosition.x <= rect.right) {
targetColumn = index + 1;
}
});
// Update clone position
dragClone.style.gridColumn = targetColumn.toString();
console.log('🔄 AllDayManager: Updated drag clone position', {
eventId: dragClone.dataset.eventId,
targetColumn,
mouseX: mousePosition.x
});
}
/**
* 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-')) {
dragClone.dataset.eventId = cloneId.replace('clone-', '');
}
// Remove dragging styles
dragClone.classList.remove('dragging');
dragClone.style.zIndex = '';
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,
finalColumn: dragClone.style.gridColumn
});
}
/** /**
* Clean up cached elements and resources * Clean up cached elements and resources
*/ */

View file

@ -19,6 +19,12 @@ interface Position {
y: number; y: number;
} }
interface ColumnBounds {
date: string;
left: number;
right: number;
}
export class DragDropManager { export class DragDropManager {
private eventBus: IEventBus; private eventBus: IEventBus;
@ -45,6 +51,9 @@ export class DragDropManager {
lastColumnDate: null lastColumnDate: null
}; };
// Column bounds cache for coordinate-based column detection
private columnBoundsCache: ColumnBounds[] = [];
// Auto-scroll properties // Auto-scroll properties
private autoScrollAnimationId: number | null = null; private autoScrollAnimationId: number | null = null;
private readonly scrollSpeed = 10; // pixels per frame private readonly scrollSpeed = 10; // pixels per frame
@ -92,6 +101,19 @@ export class DragDropManager {
document.body.addEventListener('mousedown', this.boundHandlers.mouseDown); document.body.addEventListener('mousedown', this.boundHandlers.mouseDown);
document.body.addEventListener('mouseup', this.boundHandlers.mouseUp); document.body.addEventListener('mouseup', this.boundHandlers.mouseUp);
// Initialize column bounds cache
this.updateColumnBoundsCache();
// Listen to resize events to update cache
window.addEventListener('resize', () => {
this.updateColumnBoundsCache();
});
// Listen to navigation events to update cache
this.eventBus.on('navigation:completed', () => {
this.updateColumnBoundsCache();
});
// Listen for header mouseover events // Listen for header mouseover events
this.eventBus.on('header:mouseover', (event) => { this.eventBus.on('header:mouseover', (event) => {
const { targetDate, headerRenderer } = (event as CustomEvent).detail; const { targetDate, headerRenderer } = (event as CustomEvent).detail;
@ -116,7 +138,7 @@ export class DragDropManager {
console.log('✅ DragDropManager: Converting to all-day for date:', targetDate); console.log('✅ DragDropManager: Converting to all-day for date:', targetDate);
// Element findes stadig som day-event, så konverter // Element findes stadig som day-event, så konverter
this.eventBus.emit('drag:convert-to-allday', { this.eventBus.emit('drag:convert-to-allday_event', {
targetDate, targetDate,
originalElement: draggedElement, originalElement: draggedElement,
headerRenderer headerRenderer
@ -147,8 +169,13 @@ export class DragDropManager {
this.eventBus.on('header:mouseleave', (event) => { this.eventBus.on('header:mouseleave', (event) => {
// Check if we're dragging ANY event // Check if we're dragging ANY event
if (this.draggedEventId) { if (this.draggedEventId) {
this.eventBus.emit('drag:convert-from-allday', { const mousePosition = { x: this.lastMousePosition.x, y: this.lastMousePosition.y };
draggedEventId: this.draggedEventId const column = this.getColumnDateFromX(mousePosition.x);
this.eventBus.emit('drag:convert-to-time_event', {
draggedEventId: this.draggedEventId,
mousePosition: mousePosition,
column: column
}); });
} }
}); });
@ -358,25 +385,68 @@ export class DragDropManager {
} }
/** /**
* Optimized column detection with caching * Update column bounds cache for coordinate-based column detection
*/ */
private detectColumn(mouseX: number, mouseY: number): string | null { private updateColumnBoundsCache(): void {
const element = document.elementFromPoint(mouseX, mouseY); // Reset cache
if (!element) return null; this.columnBoundsCache = [];
// Walk up DOM tree to find swp-day-column // Find alle kolonner
let current = element as HTMLElement; const columns = document.querySelectorAll('swp-day-column');
while (current && current.tagName !== 'SWP-DAY-COLUMN') {
current = current.parentElement as HTMLElement; // Cache hver kolonnes x-grænser
if (!current) return null; columns.forEach(column => {
const rect = column.getBoundingClientRect();
const date = (column as HTMLElement).dataset.date;
if (date) {
this.columnBoundsCache.push({
date,
left: rect.left,
right: rect.right
});
}
});
// 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
});
}
/**
* Get column date from X coordinate using cached bounds
*/
private getColumnDateFromX(x: number): string | null {
// Opdater cache hvis tom
if (this.columnBoundsCache.length === 0) {
this.updateColumnBoundsCache();
} }
const columnDate = current.dataset.date || null; // Find den kolonne hvor x-koordinaten er indenfor grænserne
const column = this.columnBoundsCache.find(col =>
x >= col.left && x <= col.right
);
// Update cache if we found a new column return column ? column.date : null;
}
/**
* Coordinate-based column detection (replaces DOM traversal)
*/
private detectColumn(mouseX: number, mouseY: number): string | null {
// Brug den koordinatbaserede metode direkte
const columnDate = this.getColumnDateFromX(mouseX);
// Opdater stadig den eksisterende cache hvis vi finder en kolonne
if (columnDate && columnDate !== this.cachedElements.lastColumnDate) { if (columnDate && columnDate !== this.cachedElements.lastColumnDate) {
this.cachedElements.currentColumn = current; const columnElement = document.querySelector(`swp-day-column[data-date="${columnDate}"]`) as HTMLElement;
this.cachedElements.lastColumnDate = columnDate; if (columnElement) {
this.cachedElements.currentColumn = columnElement;
this.cachedElements.lastColumnDate = columnDate;
}
} }
return columnDate; return columnDate;

View file

@ -5,6 +5,7 @@ import { calendarConfig } from '../core/CalendarConfig';
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
import { EventManager } from '../managers/EventManager'; import { EventManager } from '../managers/EventManager';
import { EventRendererStrategy } from './EventRenderer'; import { EventRendererStrategy } from './EventRenderer';
import { SwpEventElement } from '../elements/SwpEventElement';
/** /**
* EventRenderingService - Render events i DOM med positionering using Strategy Pattern * EventRenderingService - Render events i DOM med positionering using Strategy Pattern
@ -69,6 +70,28 @@ export class EventRenderingService {
this.eventBus.on(CoreEvents.VIEW_CHANGED, (event: Event) => { this.eventBus.on(CoreEvents.VIEW_CHANGED, (event: Event) => {
this.handleViewChanged(event as CustomEvent); 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();
}
});
// 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;
console.log('🔄 EventRendererManager: Received drag:convert-to-time_event', {
draggedEventId,
mousePosition,
column
});
this.handleConvertToTimeEvent(draggedEventId, mousePosition, column);
});
} }
@ -128,6 +151,66 @@ export class EventRenderingService {
// New rendering will be triggered by subsequent GRID_RENDERED event // New rendering will be triggered by subsequent GRID_RENDERED event
} }
/**
* 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;
}
// Use SwpEventElement factory to create day event from all-day event
const dayEventElement = SwpEventElement.fromAllDayElement(allDayClone as HTMLElement);
const dayElement = dayEventElement.getElement();
// Remove the all-day clone - it's no longer needed since we're converting to day event
allDayClone.remove();
// Set clone ID
dayElement.dataset.eventId = `clone-${draggedEventId}`;
// Find target column
const columnElement = document.querySelector(`swp-day-column[data-date="${column}"]`);
if (!columnElement) {
console.warn('EventRendererManager: Target column not found', { column });
return;
}
// Find events layer in the column
const eventsLayer = columnElement.querySelector('swp-events-layer');
if (!eventsLayer) {
console.warn('EventRendererManager: Events layer not found in column');
return;
}
// Add to events layer
eventsLayer.appendChild(dayElement);
// Position based on mouse Y coordinate
const columnRect = columnElement.getBoundingClientRect();
const relativeY = Math.max(0, mousePosition.y - columnRect.top);
dayElement.style.top = `${relativeY}px`;
// Set drag styling
dayElement.style.zIndex = '1000';
dayElement.style.cursor = 'grabbing';
dayElement.style.opacity = '';
dayElement.style.transform = '';
console.log('✅ EventRendererManager: Converted all-day event to time event', {
draggedEventId,
column,
mousePosition,
relativeY
});
}
private clearEvents(container?: HTMLElement): void { private clearEvents(container?: HTMLElement): void {
this.strategy.clearEvents(container); this.strategy.clearEvents(container);
} }