Calendar/wwwroot/js/features/all-day/AllDayDragService.js
2026-02-03 00:02:25 +01:00

183 lines
No EOL
8.5 KiB
JavaScript

/**
* 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