2025-09-12 00:36:02 +02:00
|
|
|
// All-day row height management and animations
|
|
|
|
|
|
|
|
|
|
import { eventBus } from '../core/EventBus';
|
|
|
|
|
import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig';
|
2025-09-13 00:39:56 +02:00
|
|
|
import { AllDayEventRenderer } from '../renderers/AllDayEventRenderer';
|
|
|
|
|
import { CalendarEvent } from '../types/CalendarTypes';
|
2025-09-12 00:36:02 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* AllDayManager - Handles all-day row height animations and management
|
|
|
|
|
* Separated from HeaderManager for clean responsibility separation
|
|
|
|
|
*/
|
|
|
|
|
export class AllDayManager {
|
|
|
|
|
private cachedAllDayContainer: HTMLElement | null = null;
|
|
|
|
|
private cachedCalendarHeader: HTMLElement | null = null;
|
|
|
|
|
private cachedHeaderSpacer: HTMLElement | null = null;
|
2025-09-13 00:39:56 +02:00
|
|
|
private allDayEventRenderer: AllDayEventRenderer;
|
2025-09-12 00:36:02 +02:00
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
// Bind methods for event listeners
|
|
|
|
|
this.checkAndAnimateAllDayHeight = this.checkAndAnimateAllDayHeight.bind(this);
|
2025-09-13 00:39:56 +02:00
|
|
|
this.allDayEventRenderer = new AllDayEventRenderer();
|
|
|
|
|
|
|
|
|
|
// Listen for drag-to-allday conversions
|
|
|
|
|
this.setupEventListeners();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Setup event listeners for drag conversions
|
|
|
|
|
*/
|
|
|
|
|
private setupEventListeners(): void {
|
|
|
|
|
eventBus.on('drag:convert-to-allday', (event) => {
|
|
|
|
|
const { targetDate, originalElement } = (event as CustomEvent).detail;
|
2025-09-18 17:55:52 +02:00
|
|
|
console.log('🔄 AllDayManager: Received drag:convert-to-allday', {
|
|
|
|
|
targetDate,
|
|
|
|
|
originalElementId: originalElement?.dataset?.eventId,
|
|
|
|
|
originalElementTag: originalElement?.tagName
|
|
|
|
|
});
|
2025-09-13 00:39:56 +02:00
|
|
|
this.handleConvertToAllDay(targetDate, originalElement);
|
|
|
|
|
});
|
2025-09-16 23:09:56 +02:00
|
|
|
|
|
|
|
|
eventBus.on('drag:convert-from-allday', (event) => {
|
|
|
|
|
const { draggedEventId } = (event as CustomEvent).detail;
|
2025-09-18 17:55:52 +02:00
|
|
|
console.log('🔄 AllDayManager: Received drag:convert-from-allday', {
|
|
|
|
|
draggedEventId
|
|
|
|
|
});
|
2025-09-16 23:09:56 +02:00
|
|
|
this.handleConvertFromAllDay(draggedEventId);
|
|
|
|
|
});
|
2025-09-18 17:55:52 +02:00
|
|
|
|
|
|
|
|
// Listen for requests to ensure all-day container exists
|
|
|
|
|
eventBus.on('allday:ensure-container', () => {
|
|
|
|
|
console.log('🏗️ AllDayManager: Received request to ensure all-day container exists');
|
|
|
|
|
this.ensureAllDayContainer();
|
|
|
|
|
});
|
2025-09-18 19:26:00 +02:00
|
|
|
|
|
|
|
|
// Listen for header mouseleave to recalculate all-day container height
|
|
|
|
|
eventBus.on('header:mouseleave', () => {
|
|
|
|
|
console.log('🔄 AllDayManager: Received header:mouseleave, recalculating height');
|
|
|
|
|
this.checkAndAnimateAllDayHeight();
|
|
|
|
|
});
|
2025-09-12 00:36:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get cached all-day container element
|
|
|
|
|
*/
|
|
|
|
|
private getAllDayContainer(): HTMLElement | null {
|
|
|
|
|
if (!this.cachedAllDayContainer) {
|
|
|
|
|
const calendarHeader = this.getCalendarHeader();
|
|
|
|
|
if (calendarHeader) {
|
|
|
|
|
this.cachedAllDayContainer = calendarHeader.querySelector('swp-allday-container');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return this.cachedAllDayContainer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get cached calendar header element
|
|
|
|
|
*/
|
|
|
|
|
private getCalendarHeader(): HTMLElement | null {
|
|
|
|
|
if (!this.cachedCalendarHeader) {
|
|
|
|
|
this.cachedCalendarHeader = document.querySelector('swp-calendar-header');
|
|
|
|
|
}
|
|
|
|
|
return this.cachedCalendarHeader;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get cached header spacer element
|
|
|
|
|
*/
|
|
|
|
|
private getHeaderSpacer(): HTMLElement | null {
|
|
|
|
|
if (!this.cachedHeaderSpacer) {
|
|
|
|
|
this.cachedHeaderSpacer = document.querySelector('swp-header-spacer');
|
|
|
|
|
}
|
|
|
|
|
return this.cachedHeaderSpacer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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 };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clear cached DOM elements (call when DOM structure changes)
|
|
|
|
|
*/
|
|
|
|
|
private clearCache(): void {
|
|
|
|
|
this.cachedCalendarHeader = null;
|
|
|
|
|
this.cachedAllDayContainer = null;
|
|
|
|
|
this.cachedHeaderSpacer = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Expand all-day row to show events
|
|
|
|
|
*/
|
|
|
|
|
public expandAllDayRow(): void {
|
|
|
|
|
const { currentHeight } = this.calculateAllDayHeight(0);
|
|
|
|
|
|
|
|
|
|
if (currentHeight === 0) {
|
|
|
|
|
this.checkAndAnimateAllDayHeight();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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) 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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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: 300,
|
|
|
|
|
easing: 'ease-out',
|
|
|
|
|
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` },
|
|
|
|
|
{ height: `${targetSpacerHeight}px` }
|
|
|
|
|
], {
|
|
|
|
|
duration: 300,
|
|
|
|
|
easing: 'ease-out',
|
|
|
|
|
fill: 'forwards'
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-13 00:39:56 +02:00
|
|
|
/**
|
|
|
|
|
* Handle conversion of timed event to all-day event
|
|
|
|
|
*/
|
|
|
|
|
private handleConvertToAllDay(targetDate: string, originalElement: HTMLElement): void {
|
|
|
|
|
// Extract event data from original element
|
|
|
|
|
const eventId = originalElement.dataset.eventId;
|
|
|
|
|
const title = originalElement.dataset.title || originalElement.textContent || 'Untitled';
|
|
|
|
|
const type = originalElement.dataset.type || 'work';
|
|
|
|
|
const startStr = originalElement.dataset.start;
|
|
|
|
|
const endStr = originalElement.dataset.end;
|
|
|
|
|
|
|
|
|
|
if (!eventId || !startStr || !endStr) {
|
|
|
|
|
console.error('Original element missing required data (eventId, start, end)');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
title: title,
|
|
|
|
|
start: targetStart,
|
|
|
|
|
end: targetEnd,
|
|
|
|
|
type: type,
|
|
|
|
|
allDay: true,
|
|
|
|
|
syncStatus: 'synced',
|
|
|
|
|
metadata: {
|
|
|
|
|
duration: originalElement.dataset.duration || '60'
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-16 23:09:56 +02:00
|
|
|
// 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}"]`);
|
|
|
|
|
if (existingAllDayEvent) {
|
|
|
|
|
// All-day event already exists, just ensure clone is hidden
|
|
|
|
|
const dragClone = document.querySelector(`swp-event[data-event-id="clone-${eventId}"]`);
|
|
|
|
|
if (dragClone) {
|
|
|
|
|
(dragClone as HTMLElement).style.display = 'none';
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-13 00:39:56 +02:00
|
|
|
// Use renderer to create and add all-day event
|
|
|
|
|
const allDayElement = this.allDayEventRenderer.renderAllDayEvent(calendarEvent, targetDate);
|
|
|
|
|
|
|
|
|
|
if (allDayElement) {
|
2025-09-16 23:09:56 +02:00
|
|
|
// Hide drag clone completely
|
|
|
|
|
const dragClone = document.querySelector(`swp-event[data-event-id="clone-${eventId}"]`);
|
|
|
|
|
if (dragClone) {
|
|
|
|
|
(dragClone as HTMLElement).style.display = 'none';
|
|
|
|
|
}
|
2025-09-13 00:39:56 +02:00
|
|
|
|
|
|
|
|
// Animate height change
|
|
|
|
|
this.checkAndAnimateAllDayHeight();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-16 23:09:56 +02:00
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-12 00:36:02 +02:00
|
|
|
/**
|
|
|
|
|
* Update row height when all-day events change
|
|
|
|
|
*/
|
|
|
|
|
public updateRowHeight(): void {
|
|
|
|
|
this.checkAndAnimateAllDayHeight();
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-18 17:55:52 +02:00
|
|
|
/**
|
|
|
|
|
* Ensure all-day container exists, create if needed
|
|
|
|
|
*/
|
|
|
|
|
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');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return container;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-12 00:36:02 +02:00
|
|
|
/**
|
|
|
|
|
* Clean up cached elements and resources
|
|
|
|
|
*/
|
|
|
|
|
public destroy(): void {
|
|
|
|
|
this.clearCache();
|
|
|
|
|
}
|
|
|
|
|
}
|