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:
parent
f1d04ae12e
commit
0b7499521e
6 changed files with 338 additions and 106 deletions
|
|
@ -19,7 +19,7 @@ export class AllDayManager {
|
|||
// Bind methods for event listeners
|
||||
this.checkAndAnimateAllDayHeight = this.checkAndAnimateAllDayHeight.bind(this);
|
||||
this.allDayEventRenderer = new AllDayEventRenderer();
|
||||
|
||||
|
||||
// Listen for drag-to-allday conversions
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
|
@ -28,23 +28,16 @@ export class AllDayManager {
|
|||
* Setup event listeners for drag conversions
|
||||
*/
|
||||
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;
|
||||
console.log('🔄 AllDayManager: Received drag:convert-to-allday', {
|
||||
console.log('🔄 AllDayManager: Received drag:convert-to-allday_event', {
|
||||
targetDate,
|
||||
originalElementId: originalElement?.dataset?.eventId,
|
||||
originalElementTag: originalElement?.tagName
|
||||
});
|
||||
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
|
||||
eventBus.on('allday:ensure-container', () => {
|
||||
|
|
@ -57,6 +50,39 @@ export class AllDayManager {
|
|||
console.log('🔄 AllDayManager: Received header:mouseleave, recalculating height');
|
||||
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 currentHeight = parseInt(getComputedStyle(root).getPropertyValue('--all-day-row-height') || '0');
|
||||
const heightDifference = targetHeight - currentHeight;
|
||||
|
||||
|
||||
return { targetHeight, currentHeight, heightDifference };
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +148,7 @@ export class AllDayManager {
|
|||
*/
|
||||
public expandAllDayRow(): void {
|
||||
const { currentHeight } = this.calculateAllDayHeight(0);
|
||||
|
||||
|
||||
if (currentHeight === 0) {
|
||||
this.checkAndAnimateAllDayHeight();
|
||||
}
|
||||
|
|
@ -141,49 +167,49 @@ export class AllDayManager {
|
|||
public checkAndAnimateAllDayHeight(): void {
|
||||
const container = this.getAllDayContainer();
|
||||
if (!container) return;
|
||||
|
||||
|
||||
const allDayEvents = container.querySelectorAll('swp-allday-event');
|
||||
|
||||
|
||||
// Calculate required rows - 0 if no events (will collapse)
|
||||
let maxRows = 0;
|
||||
|
||||
|
||||
if (allDayEvents.length > 0) {
|
||||
// Expand events to all dates they span and group by date
|
||||
const expandedEventsByDate: Record<string, string[]> = {};
|
||||
|
||||
|
||||
(Array.from(allDayEvents) as HTMLElement[]).forEach((event: HTMLElement) => {
|
||||
const startISO = event.dataset.start || '';
|
||||
const endISO = event.dataset.end || startISO;
|
||||
const eventId = event.dataset.eventId || '';
|
||||
|
||||
|
||||
// Extract dates from ISO strings
|
||||
const startDate = startISO.split('T')[0]; // YYYY-MM-DD
|
||||
const endDate = endISO.split('T')[0]; // YYYY-MM-DD
|
||||
|
||||
|
||||
// Loop through all dates from start to end
|
||||
let current = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
|
||||
|
||||
while (current <= end) {
|
||||
const dateStr = current.toISOString().split('T')[0]; // YYYY-MM-DD format
|
||||
|
||||
|
||||
if (!expandedEventsByDate[dateStr]) {
|
||||
expandedEventsByDate[dateStr] = [];
|
||||
}
|
||||
expandedEventsByDate[dateStr].push(eventId);
|
||||
|
||||
|
||||
// Move to next day
|
||||
current.setDate(current.getDate() + 1);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Find max rows needed
|
||||
maxRows = Math.max(
|
||||
...Object.values(expandedEventsByDate).map(ids => ids?.length || 0),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Animate to required rows (0 = collapse, >0 = expand)
|
||||
this.animateToRows(maxRows);
|
||||
}
|
||||
|
|
@ -193,22 +219,22 @@ export class AllDayManager {
|
|||
*/
|
||||
public animateToRows(targetRows: number): void {
|
||||
const { targetHeight, currentHeight, heightDifference } = this.calculateAllDayHeight(targetRows);
|
||||
|
||||
|
||||
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)`);
|
||||
|
||||
|
||||
// Get cached elements
|
||||
const calendarHeader = this.getCalendarHeader();
|
||||
const headerSpacer = this.getHeaderSpacer();
|
||||
const allDayContainer = this.getAllDayContainer();
|
||||
|
||||
|
||||
if (!calendarHeader || !allDayContainer) return;
|
||||
|
||||
|
||||
// Get current parent height for animation
|
||||
const currentParentHeight = parseFloat(getComputedStyle(calendarHeader).height);
|
||||
const targetParentHeight = currentParentHeight + heightDifference;
|
||||
|
||||
|
||||
const animations = [
|
||||
calendarHeader.animate([
|
||||
{ height: `${currentParentHeight}px` },
|
||||
|
|
@ -219,13 +245,13 @@ export class AllDayManager {
|
|||
fill: 'forwards'
|
||||
})
|
||||
];
|
||||
|
||||
|
||||
// Add spacer animation if spacer exists
|
||||
if (headerSpacer) {
|
||||
const root = document.documentElement;
|
||||
const currentSpacerHeight = parseInt(getComputedStyle(root).getPropertyValue('--header-height')) + currentHeight;
|
||||
const targetSpacerHeight = parseInt(getComputedStyle(root).getPropertyValue('--header-height')) + targetHeight;
|
||||
|
||||
|
||||
animations.push(
|
||||
headerSpacer.animate([
|
||||
{ height: `${currentSpacerHeight}px` },
|
||||
|
|
@ -237,7 +263,7 @@ export class AllDayManager {
|
|||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Update CSS variable after animation
|
||||
Promise.all(animations.map(anim => anim.finished)).then(() => {
|
||||
const root = document.documentElement;
|
||||
|
|
@ -265,16 +291,16 @@ export class AllDayManager {
|
|||
// 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,
|
||||
id: `clone-${eventId}`,
|
||||
title: title,
|
||||
start: targetStart,
|
||||
end: targetEnd,
|
||||
|
|
@ -286,8 +312,8 @@ export class AllDayManager {
|
|||
}
|
||||
};
|
||||
|
||||
// Check if all-day event already exists for this event ID
|
||||
const existingAllDayEvent = document.querySelector(`swp-allday-container swp-allday-event[data-event-id="${eventId}"]`);
|
||||
// Check if all-day clone already exists for this event ID
|
||||
const existingAllDayEvent = document.querySelector(`swp-allday-container swp-allday-event[data-event-id="clone-${eventId}"]`);
|
||||
if (existingAllDayEvent) {
|
||||
// All-day event already exists, just ensure clone is hidden
|
||||
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
|
||||
const allDayElement = this.allDayEventRenderer.renderAllDayEvent(calendarEvent, targetDate);
|
||||
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
|
||||
// Animate height change
|
||||
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
|
||||
|
|
@ -350,36 +351,114 @@ export class AllDayManager {
|
|||
*/
|
||||
public ensureAllDayContainer(): HTMLElement | null {
|
||||
console.log('🔍 AllDayManager: Checking if all-day container exists...');
|
||||
|
||||
|
||||
// Try to get existing container first
|
||||
let container = this.getAllDayContainer();
|
||||
|
||||
|
||||
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
|
||||
|
||||
// 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();
|
||||
if (header) {
|
||||
container = document.createElement('swp-allday-container');
|
||||
header.appendChild(container);
|
||||
console.log('✅ AllDayManager: Created all-day 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');
|
||||
container = document.createElement('swp-allday-container');
|
||||
header?.appendChild(container);
|
||||
|
||||
this.cachedAllDayContainer = 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
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue