Improves the calculation of the all-day event container's height by finding the highest row number in use, ensuring the container accurately reflects the space occupied by events. Updates debug logging for clarity.
459 lines
No EOL
15 KiB
TypeScript
459 lines
No EOL
15 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 { CalendarEvent } from '../types/CalendarTypes';
|
|
import {
|
|
DragMouseEnterHeaderEventPayload,
|
|
DragStartEventPayload,
|
|
DragMoveEventPayload,
|
|
DragEndEventPayload
|
|
} from '../types/EventTypes';
|
|
import { DragOffset, MousePosition } from '../types/DragDropTypes';
|
|
|
|
/**
|
|
* AllDayManager - Handles all-day row height animations and management
|
|
* Separated from HeaderManager for clean responsibility separation
|
|
*/
|
|
export class AllDayManager {
|
|
private allDayEventRenderer: AllDayEventRenderer;
|
|
|
|
constructor() {
|
|
this.allDayEventRenderer = new AllDayEventRenderer();
|
|
this.setupEventListeners();
|
|
}
|
|
|
|
/**
|
|
* Setup event listeners for drag conversions
|
|
*/
|
|
private setupEventListeners(): void {
|
|
|
|
|
|
eventBus.on('drag:mouseenter-header', (event) => {
|
|
const { targetDate, mousePosition, originalElement, cloneElement } = (event as CustomEvent<DragMouseEnterHeaderEventPayload>).detail;
|
|
|
|
console.log('🔄 AllDayManager: Received drag:mouseenter-header', {
|
|
targetDate,
|
|
originalElementId: originalElement?.dataset?.eventId,
|
|
originalElementTag: originalElement?.tagName
|
|
});
|
|
|
|
if (targetDate && cloneElement) {
|
|
this.handleConvertToAllDay(targetDate, 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
|
|
});
|
|
|
|
if (cloneElement && cloneElement.classList.contains('all-day-style')) {
|
|
this.handleConvertFromAllDay(cloneElement);
|
|
}
|
|
|
|
this.checkAndAnimateAllDayHeight();
|
|
|
|
});
|
|
|
|
// Listen for drag operations on all-day events
|
|
eventBus.on('drag:start', (event) => {
|
|
const { draggedElement, 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:move', (event) => {
|
|
const { draggedElement, mousePosition } = (event as CustomEvent<DragMoveEventPayload>).detail;
|
|
|
|
// Only handle for all-day events - check if original element is all-day
|
|
const isAllDayEvent = draggedElement.closest('swp-allday-container');
|
|
if (!isAllDayEvent) return;
|
|
|
|
const eventId = draggedElement.dataset.eventId;
|
|
const dragClone = document.querySelector(`swp-allday-container swp-event[data-event-id="clone-${eventId}"]`);
|
|
if (dragClone) {
|
|
this.handleDragMove(dragClone as HTMLElement, mousePosition);
|
|
}
|
|
});
|
|
|
|
eventBus.on('drag:end', (event) => {
|
|
const { draggedElement, mousePosition, finalPosition, target } = (event as CustomEvent<DragEndEventPayload>).detail;
|
|
|
|
if (target != 'swp-day-header') // we are not inside the swp-day-header, so just ignore.
|
|
return;
|
|
|
|
const eventId = draggedElement.dataset.eventId;
|
|
console.log('🎬 AllDayManager: Received drag:end', {
|
|
eventId: eventId,
|
|
finalPosition
|
|
});
|
|
const dragClone = document.querySelector(`swp-allday-container swp-event[data-event-id="clone-${eventId}"]`);
|
|
|
|
|
|
console.log('🎯 AllDayManager: Ending drag for all-day event', { eventId });
|
|
this.handleDragEnd(draggedElement, dragClone as HTMLElement, { column: finalPosition.column || '', y: 0 });
|
|
});
|
|
|
|
// 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;
|
|
const currentHeight = parseInt(getComputedStyle(root).getPropertyValue('--all-day-row-height') || '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(getComputedStyle(event).gridRowStart) || 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 currentSpacerHeight = parseInt(getComputedStyle(root).getPropertyValue('--header-height')) + currentHeight;
|
|
const targetSpacerHeight = parseInt(getComputedStyle(root).getPropertyValue('--header-height')) + 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');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle conversion of timed event to all-day event using CSS styling
|
|
*/
|
|
private handleConvertToAllDay(targetDate: string, cloneElement: HTMLElement): void {
|
|
console.log('🔄 AllDayManager: Converting to all-day using CSS approach', {
|
|
eventId: cloneElement.dataset.eventId,
|
|
targetDate
|
|
});
|
|
|
|
// Get all-day container, request creation if needed
|
|
let allDayContainer = this.getAllDayContainer();
|
|
if (!allDayContainer) {
|
|
console.log('🔄 AllDayManager: All-day container not found, requesting creation...');
|
|
// Request HeaderManager to create container
|
|
eventBus.emit('header:ensure-allday-container');
|
|
|
|
// Try again after request
|
|
allDayContainer = this.getAllDayContainer();
|
|
if (!allDayContainer) {
|
|
console.error('All-day container still not found after creation request');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Calculate position BEFORE adding to container (to avoid counting clone as existing event)
|
|
const columnIndex = this.getColumnIndexForDate(targetDate);
|
|
const availableRow = this.findAvailableRow(targetDate);
|
|
|
|
// Set all properties BEFORE adding to DOM
|
|
cloneElement.classList.add('all-day-style');
|
|
cloneElement.style.gridColumn = columnIndex.toString();
|
|
cloneElement.style.gridRow = availableRow.toString();
|
|
cloneElement.dataset.allDayDate = targetDate;
|
|
cloneElement.style.display = '';
|
|
|
|
// NOW add to container (after all positioning is calculated)
|
|
allDayContainer.appendChild(cloneElement);
|
|
|
|
console.log('✅ AllDayManager: Converted to all-day style', {
|
|
eventId: cloneElement.dataset.eventId,
|
|
gridColumn: columnIndex,
|
|
gridRow: availableRow
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get column index for a specific date
|
|
*/
|
|
private getColumnIndexForDate(targetDate: string): number {
|
|
const dayHeaders = document.querySelectorAll('swp-day-header');
|
|
let columnIndex = 1;
|
|
dayHeaders.forEach((header, index) => {
|
|
if ((header as HTMLElement).dataset.date === targetDate) {
|
|
columnIndex = index + 1;
|
|
}
|
|
});
|
|
return columnIndex;
|
|
}
|
|
|
|
/**
|
|
* Find available row for all-day event in specific date column
|
|
*/
|
|
private findAvailableRow(targetDate: string): number {
|
|
const container = this.getAllDayContainer();
|
|
if (!container) return 1;
|
|
|
|
const columnIndex = this.getColumnIndexForDate(targetDate);
|
|
const existingEvents = container.querySelectorAll('swp-event');
|
|
const occupiedRows = new Set<number>();
|
|
|
|
existingEvents.forEach(event => {
|
|
const style = getComputedStyle(event);
|
|
const eventStartCol = parseInt(style.gridColumnStart);
|
|
const eventRow = parseInt(style.gridRowStart) || 1;
|
|
|
|
// Only check events in the same column
|
|
if (eventStartCol === columnIndex) {
|
|
occupiedRows.add(eventRow);
|
|
}
|
|
});
|
|
|
|
// Find first available row
|
|
let targetRow = 1;
|
|
while (occupiedRows.has(targetRow)) {
|
|
targetRow++;
|
|
}
|
|
|
|
return targetRow;
|
|
}
|
|
|
|
/**
|
|
* Handle conversion from all-day back to timed event
|
|
*/
|
|
private handleConvertFromAllDay(cloneElement: HTMLElement): void {
|
|
console.log('🔄 AllDayManager: Converting from all-day back to timed', {
|
|
eventId: cloneElement.dataset.eventId
|
|
});
|
|
|
|
// Remove all-day CSS class
|
|
cloneElement.classList.remove('all-day-style');
|
|
|
|
// Reset grid positioning
|
|
cloneElement.style.gridColumn = '';
|
|
cloneElement.style.gridRow = '';
|
|
|
|
// Remove all-day date attribute
|
|
delete cloneElement.dataset.allDayDate;
|
|
|
|
// Move back to appropriate day column (will be handled by drag logic)
|
|
// The drag system will position it correctly
|
|
|
|
console.log('✅ AllDayManager: Converted from all-day back to timed');
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
private handleDragMove(dragClone: HTMLElement, mousePosition: MousePosition): 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: { column: string; y: number }): void {
|
|
|
|
// 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 = '';
|
|
|
|
|
|
|
|
console.log('✅ AllDayManager: Completed drag operation for all-day event', {
|
|
eventId: dragClone.dataset.eventId,
|
|
finalColumn: dragClone.style.gridColumn
|
|
});
|
|
}
|
|
} |