Improves drag and drop functionality by refactoring column detection to use column bounds instead of dates. This change enhances the accuracy and efficiency of determining the target column during drag operations. It also removes redundant code and simplifies the logic in both the DragDropManager and AllDayManager.
494 lines
No EOL
17 KiB
TypeScript
494 lines
No EOL
17 KiB
TypeScript
// All-day row height management and animations
|
|
|
|
import { eventBus } from '../core/EventBus';
|
|
import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig';
|
|
import { AllDayEventRenderer } from '../renderers/AllDayEventRenderer';
|
|
import { AllDayLayoutEngine, EventLayout } from '../utils/AllDayLayoutEngine';
|
|
import { ColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
|
|
import { CalendarEvent } from '../types/CalendarTypes';
|
|
import {
|
|
DragMouseEnterHeaderEventPayload,
|
|
DragStartEventPayload,
|
|
DragMoveEventPayload,
|
|
DragEndEventPayload,
|
|
DragColumnChangeEventPayload
|
|
} from '../types/EventTypes';
|
|
import { DragOffset, MousePosition } from '../types/DragDropTypes';
|
|
|
|
/**
|
|
* AllDayManager - Handles all-day row height animations and management
|
|
* Uses AllDayLayoutEngine for all overlap detection and layout calculation
|
|
*/
|
|
export class AllDayManager {
|
|
private allDayEventRenderer: AllDayEventRenderer;
|
|
private layoutEngine: AllDayLayoutEngine | null = null;
|
|
|
|
// State tracking for differential updates
|
|
private currentLayouts: Map<string, string> = new Map();
|
|
private currentAllDayEvents: CalendarEvent[] = [];
|
|
private currentWeekDates: string[] = [];
|
|
|
|
constructor() {
|
|
this.allDayEventRenderer = new AllDayEventRenderer();
|
|
this.setupEventListeners();
|
|
}
|
|
|
|
/**
|
|
* Setup event listeners for drag conversions
|
|
*/
|
|
private setupEventListeners(): void {
|
|
eventBus.on('drag:mouseenter-header', (event) => {
|
|
const { targetColumn: targetColumnBounds, mousePosition, originalElement, cloneElement } = (event as CustomEvent<DragMouseEnterHeaderEventPayload>).detail;
|
|
|
|
console.log('🔄 AllDayManager: Received drag:mouseenter-header', {
|
|
targetDate: targetColumnBounds,
|
|
originalElementId: originalElement?.dataset?.eventId,
|
|
originalElementTag: originalElement?.tagName
|
|
});
|
|
|
|
if (targetColumnBounds && cloneElement) {
|
|
this.handleConvertToAllDay(targetColumnBounds, cloneElement);
|
|
}
|
|
|
|
this.checkAndAnimateAllDayHeight();
|
|
});
|
|
|
|
eventBus.on('drag:mouseleave-header', (event) => {
|
|
const { originalElement, cloneElement } = (event as CustomEvent).detail;
|
|
|
|
console.log('🚪 AllDayManager: Received drag:mouseleave-header', {
|
|
originalElementId: originalElement?.dataset?.eventId
|
|
});
|
|
|
|
this.checkAndAnimateAllDayHeight();
|
|
});
|
|
|
|
// Listen for drag operations on all-day events
|
|
eventBus.on('drag:start', (event) => {
|
|
const { draggedElement, draggedClone, mouseOffset } = (event as CustomEvent<DragStartEventPayload>).detail;
|
|
|
|
// Check if this is an all-day event by checking if it's in all-day container
|
|
const isAllDayEvent = draggedElement.closest('swp-allday-container');
|
|
if (!isAllDayEvent) return; // Not an all-day event
|
|
|
|
const eventId = draggedElement.dataset.eventId;
|
|
console.log('🎯 AllDayManager: Starting drag for all-day event', { eventId });
|
|
this.handleDragStart(draggedElement, eventId || '', mouseOffset);
|
|
});
|
|
|
|
eventBus.on('drag:column-change', (event) => {
|
|
const { draggedElement, draggedClone, mousePosition } = (event as CustomEvent<DragColumnChangeEventPayload>).detail;
|
|
|
|
if (draggedClone == null)
|
|
return;
|
|
|
|
// Filter: Only handle events where clone IS an all-day event
|
|
if (!draggedClone.hasAttribute('data-allday')) {
|
|
return; // This is not an all-day event, let EventRendererManager handle it
|
|
}
|
|
|
|
console.log('🔄 AllDayManager: Handling drag:column-change for all-day event', {
|
|
eventId: draggedElement.dataset.eventId,
|
|
cloneId: draggedClone.dataset.eventId
|
|
});
|
|
|
|
this.handleColumnChange(draggedClone, mousePosition);
|
|
});
|
|
|
|
eventBus.on('drag:end', (event) => {
|
|
let draggedElement: DragEndEventPayload = (event as CustomEvent<DragEndEventPayload>).detail;
|
|
|
|
if (draggedElement.target != 'swp-day-header') // we are not inside the swp-day-header, so just ignore.
|
|
return;
|
|
|
|
this.handleDragEnd(draggedElement);
|
|
});
|
|
|
|
// Listen for drag cancellation to recalculate height
|
|
eventBus.on('drag:cancelled', (event) => {
|
|
const { draggedElement, reason } = (event as CustomEvent).detail;
|
|
|
|
console.log('🚫 AllDayManager: Drag cancelled', {
|
|
eventId: draggedElement?.dataset?.eventId,
|
|
reason
|
|
});
|
|
|
|
// Recalculate all-day height since clones may have been removed
|
|
this.checkAndAnimateAllDayHeight();
|
|
});
|
|
|
|
// Listen for height check requests from EventRendererManager
|
|
eventBus.on('allday:checkHeight', () => {
|
|
console.log('📏 AllDayManager: Received allday:checkHeight request');
|
|
this.checkAndAnimateAllDayHeight();
|
|
});
|
|
}
|
|
|
|
private getAllDayContainer(): HTMLElement | null {
|
|
return document.querySelector('swp-calendar-header swp-allday-container');
|
|
}
|
|
|
|
private getCalendarHeader(): HTMLElement | null {
|
|
return document.querySelector('swp-calendar-header');
|
|
}
|
|
|
|
private getHeaderSpacer(): HTMLElement | null {
|
|
return document.querySelector('swp-header-spacer');
|
|
}
|
|
|
|
/**
|
|
* Calculate all-day height based on number of rows
|
|
*/
|
|
private calculateAllDayHeight(targetRows: number): {
|
|
targetHeight: number;
|
|
currentHeight: number;
|
|
heightDifference: number;
|
|
} {
|
|
const root = document.documentElement;
|
|
const targetHeight = targetRows * ALL_DAY_CONSTANTS.SINGLE_ROW_HEIGHT;
|
|
// Read CSS variable directly from style property or default to 0
|
|
const currentHeightStr = root.style.getPropertyValue('--all-day-row-height') || '0px';
|
|
const currentHeight = parseInt(currentHeightStr) || 0;
|
|
const heightDifference = targetHeight - currentHeight;
|
|
|
|
return { targetHeight, currentHeight, heightDifference };
|
|
}
|
|
|
|
/**
|
|
* Collapse all-day row when no events
|
|
*/
|
|
public collapseAllDayRow(): void {
|
|
this.animateToRows(0);
|
|
}
|
|
|
|
/**
|
|
* Check current all-day events and animate to correct height
|
|
*/
|
|
public checkAndAnimateAllDayHeight(): void {
|
|
const container = this.getAllDayContainer();
|
|
if (!container) {
|
|
this.animateToRows(0);
|
|
return;
|
|
}
|
|
|
|
const allDayEvents = container.querySelectorAll('swp-event');
|
|
|
|
// Calculate required rows - 0 if no events (will collapse)
|
|
let maxRows = 0;
|
|
|
|
if (allDayEvents.length > 0) {
|
|
// Find the HIGHEST row number in use (not count of unique rows)
|
|
let highestRow = 0;
|
|
|
|
(Array.from(allDayEvents) as HTMLElement[]).forEach((event: HTMLElement) => {
|
|
const gridRow = parseInt(event.style.gridRow) || 1;
|
|
highestRow = Math.max(highestRow, gridRow);
|
|
});
|
|
|
|
// Max rows = highest row number (e.g. if row 3 is used, height = 3 rows)
|
|
maxRows = highestRow;
|
|
|
|
console.log('🔍 AllDayManager: Height calculation FIXED', {
|
|
totalEvents: allDayEvents.length,
|
|
highestRowFound: highestRow,
|
|
maxRows
|
|
});
|
|
}
|
|
|
|
// Animate to required rows (0 = collapse, >0 = expand)
|
|
this.animateToRows(maxRows);
|
|
}
|
|
|
|
/**
|
|
* Animate all-day container to specific number of rows
|
|
*/
|
|
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` },
|
|
{ height: `${targetParentHeight}px` }
|
|
], {
|
|
duration: 150,
|
|
easing: 'ease-out',
|
|
fill: 'forwards'
|
|
})
|
|
];
|
|
|
|
// Add spacer animation if spacer exists, but don't use fill: 'forwards'
|
|
if (headerSpacer) {
|
|
const root = document.documentElement;
|
|
const headerHeightStr = root.style.getPropertyValue('--header-height');
|
|
const headerHeight = parseInt(headerHeightStr);
|
|
const currentSpacerHeight = headerHeight + currentHeight;
|
|
const targetSpacerHeight = headerHeight + targetHeight;
|
|
|
|
animations.push(
|
|
headerSpacer.animate([
|
|
{ height: `${currentSpacerHeight}px` },
|
|
{ height: `${targetSpacerHeight}px` }
|
|
], {
|
|
duration: 150,
|
|
easing: 'ease-out'
|
|
// No fill: 'forwards' - let CSS calc() take over after animation
|
|
})
|
|
);
|
|
}
|
|
|
|
// Update CSS variable after animation
|
|
Promise.all(animations.map(anim => anim.finished)).then(() => {
|
|
const root = document.documentElement;
|
|
root.style.setProperty('--all-day-row-height', `${targetHeight}px`);
|
|
eventBus.emit('header:height-changed');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Store current layouts from DOM for comparison
|
|
*/
|
|
private storeCurrentLayouts(): void {
|
|
this.currentLayouts.clear();
|
|
const container = this.getAllDayContainer();
|
|
if (!container) return;
|
|
|
|
container.querySelectorAll('swp-event').forEach(element => {
|
|
const htmlElement = element as HTMLElement;
|
|
const eventId = htmlElement.dataset.eventId;
|
|
const gridArea = htmlElement.style.gridArea;
|
|
if (eventId && gridArea) {
|
|
this.currentLayouts.set(eventId, gridArea);
|
|
}
|
|
});
|
|
|
|
console.log('📋 AllDayManager: Stored current layouts', {
|
|
count: this.currentLayouts.size,
|
|
layouts: Array.from(this.currentLayouts.entries())
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Set current events and week dates (called by EventRendererManager)
|
|
*/
|
|
public setCurrentEvents(events: CalendarEvent[], weekDates: string[]): void {
|
|
this.currentAllDayEvents = events;
|
|
this.currentWeekDates = weekDates;
|
|
|
|
console.log('📝 AllDayManager: Set current events', {
|
|
eventCount: events.length,
|
|
weekDatesCount: weekDates.length
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Calculate layout for ALL all-day events using AllDayLayoutEngine
|
|
* This is the correct method that processes all events together for proper overlap detection
|
|
*/
|
|
public calculateAllDayEventsLayout(events: CalendarEvent[], weekDates: string[]): EventLayout[] {
|
|
|
|
// Store current state
|
|
this.currentAllDayEvents = events;
|
|
this.currentWeekDates = weekDates;
|
|
|
|
// Initialize layout engine with provided week dates
|
|
this.layoutEngine = new AllDayLayoutEngine(weekDates);
|
|
|
|
// Calculate layout for all events together - AllDayLayoutEngine handles CalendarEvents directly
|
|
return this.layoutEngine.calculateLayout(events);
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Handle conversion of timed event to all-day event - SIMPLIFIED
|
|
* During drag: Place in row 1 only, calculate column from targetDate
|
|
*/
|
|
private handleConvertToAllDay(targetColumnBounds: ColumnBounds, cloneElement: HTMLElement): void {
|
|
console.log('🔄 AllDayManager: Converting to all-day (row 1 only during drag)', {
|
|
eventId: cloneElement.dataset.eventId,
|
|
targetDate: targetColumnBounds
|
|
});
|
|
|
|
// Get all-day container, request creation if needed
|
|
let allDayContainer = this.getAllDayContainer();
|
|
|
|
|
|
cloneElement.removeAttribute('style');
|
|
cloneElement.classList.add('all-day-style');
|
|
cloneElement.style.gridRow = '1';
|
|
cloneElement.style.gridColumn = targetColumnBounds.index.toString();
|
|
cloneElement.dataset.allday = 'true'; // Set the all-day attribute for filtering
|
|
|
|
// Add to container
|
|
allDayContainer?.appendChild(cloneElement);
|
|
|
|
console.log('✅ AllDayManager: Converted to all-day style (simple row 1)', {
|
|
eventId: cloneElement.dataset.eventId,
|
|
gridColumn: targetColumnBounds,
|
|
gridRow: 1
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Handle drag start for all-day events
|
|
*/
|
|
private handleDragStart(originalElement: HTMLElement, eventId: string, mouseOffset: DragOffset): 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 - SPECIALIZED FOR ALL-DAY CONTAINER
|
|
*/
|
|
private handleColumnChange(dragClone: HTMLElement, mousePosition: MousePosition): void {
|
|
// Get the all-day container to understand its grid structure
|
|
const allDayContainer = this.getAllDayContainer();
|
|
if (!allDayContainer) return;
|
|
|
|
// Calculate target column using ColumnDetectionUtils
|
|
const targetColumn = ColumnDetectionUtils.getColumnBounds(mousePosition);
|
|
|
|
if (targetColumn == null)
|
|
return;
|
|
|
|
// Update clone position - ALWAYS keep in row 1 during drag
|
|
// Use simple grid positioning that matches all-day container structure
|
|
dragClone.style.gridColumn = targetColumn.index.toString();
|
|
dragClone.style.gridRow = '1'; // Force row 1 during drag
|
|
dragClone.style.gridArea = `1 / ${targetColumn.index} / 2 / ${targetColumn.index + 1}`;
|
|
|
|
console.log('🔄 AllDayManager: Updated all-day drag clone position', {
|
|
eventId: dragClone.dataset.eventId,
|
|
targetColumn,
|
|
gridRow: 1,
|
|
gridArea: dragClone.style.gridArea,
|
|
mouseX: mousePosition.x
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle drag end for all-day events - WITH DIFFERENTIAL UPDATES
|
|
*/
|
|
private handleDragEnd(dragEndEvent: DragEndEventPayload): void {
|
|
console.log('🎯 AllDayManager: Starting drag end with differential updates', {
|
|
dragEndEvent
|
|
});
|
|
|
|
if (dragEndEvent.draggedClone == null)
|
|
return;
|
|
|
|
// 1. Store current layouts BEFORE any changes
|
|
this.storeCurrentLayouts();
|
|
|
|
// 2. Normalize clone ID
|
|
const cloneId = dragEndEvent.draggedClone?.dataset.eventId;
|
|
if (cloneId?.startsWith('clone-')) {
|
|
dragEndEvent.draggedClone.dataset.eventId = cloneId.replace('clone-', '');
|
|
}
|
|
|
|
// 3. Create temporary array with existing events + the dropped event
|
|
let eventId = dragEndEvent.draggedClone.dataset.eventId;
|
|
let eventDate = dragEndEvent.finalPosition.column?.date;
|
|
let eventType = dragEndEvent.draggedClone.dataset.type;
|
|
|
|
if (eventDate == null || eventId == null || eventType == null)
|
|
return;
|
|
|
|
|
|
const droppedEvent: CalendarEvent = {
|
|
id: eventId,
|
|
title: dragEndEvent.draggedClone.dataset.title || dragEndEvent.draggedClone.textContent || '',
|
|
start: new Date(eventDate),
|
|
end: new Date(eventDate),
|
|
type: eventType,
|
|
allDay: true,
|
|
syncStatus: 'synced'
|
|
};
|
|
|
|
// Use current events + dropped event for calculation
|
|
const tempEvents = [...this.currentAllDayEvents, droppedEvent];
|
|
|
|
// 4. Calculate new layouts for ALL events
|
|
const newLayouts = this.calculateAllDayEventsLayout(tempEvents, this.currentWeekDates);
|
|
|
|
// 5. Apply differential updates - only update events that changed
|
|
let changedCount = 0;
|
|
newLayouts.forEach((layout) => {
|
|
const oldGridArea = this.currentLayouts.get(layout.calenderEvent.id);
|
|
const newGridArea = layout.gridArea;
|
|
|
|
if (oldGridArea !== newGridArea) {
|
|
changedCount++;
|
|
const element = document.querySelector(`[data-event-id="${layout.calenderEvent.id}"]`) as HTMLElement;
|
|
if (element) {
|
|
|
|
// Add transition class for smooth animation
|
|
element.classList.add('transitioning');
|
|
element.style.gridArea = newGridArea;
|
|
element.style.gridRow = layout.row.toString();
|
|
element.style.gridColumn = `${layout.startColumn} / ${layout.endColumn + 1}`;
|
|
|
|
// Remove transition class after animation
|
|
setTimeout(() => element.classList.remove('transitioning'), 200);
|
|
}
|
|
}
|
|
});
|
|
|
|
// 6. 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 = '';
|
|
|
|
// 7. Restore original element opacity
|
|
//originalElement.style.opacity = '';
|
|
|
|
// 8. Check if height adjustment is needed
|
|
this.checkAndAnimateAllDayHeight();
|
|
|
|
}
|
|
|
|
} |