/** * AllDayDragService - Manages drag and drop operations for all-day events * * STATELESS SERVICE - Reads all data from DOM via AllDayDomReader * - No persistent state * - Handles timed → all-day conversion * - Handles all-day → all-day repositioning * - Handles column changes during drag * - Calculates layouts on-demand from DOM */ import { SwpAllDayEventElement } from '../../elements/SwpEventElement'; import { AllDayLayoutEngine } from '../../utils/AllDayLayoutEngine'; import { ColumnDetectionUtils } from '../../utils/ColumnDetectionUtils'; import { ALL_DAY_CONSTANTS } from '../../configurations/CalendarConfig'; import { AllDayDomReader } from './AllDayDomReader'; export class AllDayDragService { constructor(eventManager, allDayEventRenderer, dateService) { this.eventManager = eventManager; this.allDayEventRenderer = allDayEventRenderer; this.dateService = dateService; } /** * Handle conversion from timed event to all-day event * Called when dragging a timed event into the header */ handleConvertToAllDay(payload) { const allDayContainer = AllDayDomReader.getAllDayContainer(); if (!allDayContainer) return; // Create SwpAllDayEventElement from ICalendarEvent const allDayElement = SwpAllDayEventElement.fromCalendarEvent(payload.calendarEvent); // Apply grid positioning allDayElement.style.gridRow = '1'; allDayElement.style.gridColumn = payload.targetColumn.index.toString(); // Remove old swp-event clone payload.draggedClone.remove(); // Call delegate to update DragDropManager's draggedClone reference payload.replaceClone(allDayElement); // Append to container allDayContainer.appendChild(allDayElement); ColumnDetectionUtils.updateColumnBoundsCache(); } /** * Handle column change during drag of all-day event * Updates grid position while maintaining event span */ handleColumnChange(payload) { const allDayContainer = AllDayDomReader.getAllDayContainer(); if (!allDayContainer) return; const targetColumn = ColumnDetectionUtils.getColumnBounds(payload.mousePosition); if (!targetColumn || !payload.draggedClone) return; // Calculate event span from original grid positioning const { start: gridColumnStart, end: gridColumnEnd } = AllDayDomReader.getGridColumnRange(payload.draggedClone); const span = gridColumnEnd - gridColumnStart; // Update clone position maintaining the span const newStartColumn = targetColumn.index; const newEndColumn = newStartColumn + span; payload.draggedClone.style.gridColumn = `${newStartColumn} / ${newEndColumn}`; } /** * Handle drag end for all-day → all-day drops * Recalculates layouts and updates event positions */ async handleDragEnd(dragEndEvent) { if (!dragEndEvent.draggedClone) return; // Normalize clone ID dragEndEvent.draggedClone.dataset.eventId = dragEndEvent.draggedClone.dataset.eventId?.replace('clone-', ''); dragEndEvent.draggedClone.style.pointerEvents = ''; // Re-enable pointer events dragEndEvent.originalElement.dataset.eventId += '_'; const eventId = dragEndEvent.draggedClone.dataset.eventId; const eventDate = dragEndEvent.finalPosition.column?.date; const eventType = dragEndEvent.draggedClone.dataset.type; if (!eventDate || !eventId || !eventType) return; // Get original dates to preserve time const originalStartDate = new Date(dragEndEvent.draggedClone.dataset.start); const originalEndDate = new Date(dragEndEvent.draggedClone.dataset.end); // Calculate actual duration in milliseconds (preserves hours/minutes/seconds) const durationMs = originalEndDate.getTime() - originalStartDate.getTime(); // Create new start date with the new day but preserve original time const newStartDate = new Date(eventDate); newStartDate.setHours(originalStartDate.getHours(), originalStartDate.getMinutes(), originalStartDate.getSeconds(), originalStartDate.getMilliseconds()); // Create new end date by adding duration in milliseconds const newEndDate = new Date(newStartDate.getTime() + durationMs); // Update data attributes with new dates (convert to UTC) dragEndEvent.draggedClone.dataset.start = this.dateService.toUTC(newStartDate); dragEndEvent.draggedClone.dataset.end = this.dateService.toUTC(newEndDate); const droppedEvent = { id: eventId, title: dragEndEvent.draggedClone.dataset.title || '', start: newStartDate, end: newEndDate, type: eventType, allDay: true, syncStatus: 'synced' }; // Get all events from DOM and recalculate layouts const allEventsFromDOM = AllDayDomReader.getEventsAsData(); const weekDates = ColumnDetectionUtils.getColumns(); // Replace old event with dropped event const updatedEvents = [ ...allEventsFromDOM.filter(event => event.id !== eventId), droppedEvent ]; // Calculate new layouts for ALL events const newLayouts = this.calculateLayouts(updatedEvents, weekDates); // Apply layout updates to DOM this.applyLayoutUpdates(newLayouts); // Clean up drag styles from the dropped clone dragEndEvent.draggedClone.classList.remove('dragging'); dragEndEvent.draggedClone.style.zIndex = ''; dragEndEvent.draggedClone.style.cursor = ''; dragEndEvent.draggedClone.style.opacity = ''; // Apply highlight class to show the dropped event with highlight color dragEndEvent.draggedClone.classList.add('highlight'); // Update event in repository to mark as allDay=true await this.eventManager.updateEvent(eventId, { start: newStartDate, end: newEndDate, allDay: true }); this.fadeOutAndRemove(dragEndEvent.originalElement); } /** * Calculate layouts for events using AllDayLayoutEngine */ calculateLayouts(events, weekDates) { const layoutEngine = new AllDayLayoutEngine(weekDates.map(column => column.date)); return layoutEngine.calculateLayout(events); } /** * Apply layout updates to DOM elements * Only updates elements that have changed position * Public so AllDayCoordinator can use it for full recalculation */ applyLayoutUpdates(newLayouts) { const container = AllDayDomReader.getAllDayContainer(); if (!container) return; // Read current layouts from DOM const currentLayoutsMap = AllDayDomReader.getCurrentLayouts(); newLayouts.forEach((layout) => { const currentLayout = currentLayoutsMap.get(layout.calenderEvent.id); // Only update if layout changed if (currentLayout?.gridArea !== layout.gridArea) { const element = container.querySelector(`[data-event-id="${layout.calenderEvent.id}"]`); if (element) { element.classList.add('transitioning'); element.style.gridArea = layout.gridArea; element.style.gridRow = layout.row.toString(); element.style.gridColumn = `${layout.startColumn} / ${layout.endColumn + 1}`; // Update overflow classes based on row element.classList.remove('max-event-overflow-hide', 'max-event-overflow-show'); if (layout.row > ALL_DAY_CONSTANTS.MAX_COLLAPSED_ROWS) { const isExpanded = AllDayDomReader.isExpanded(); if (isExpanded) { element.classList.add('max-event-overflow-show'); } else { element.classList.add('max-event-overflow-hide'); } } // Remove transition class after animation setTimeout(() => element.classList.remove('transitioning'), 200); } } }); } /** * Fade out and remove element */ fadeOutAndRemove(element) { element.style.transition = 'opacity 0.3s ease-out'; element.style.opacity = '0'; setTimeout(() => { element.remove(); }, 300); } } //# sourceMappingURL=AllDayDragService.js.map