Improves all-day event rendering and animation

Refactors all-day event handling to enhance user experience.

Introduces dynamic height animation for all-day event rows, adapting to the number of overlapping events. This ensures efficient use of screen space and prevents unnecessary scrolling.

Additionally, events now store all relevant data, and the header height is checked and animated after navigation.

The previous HeaderRenderer.ts file has been refactored.
This commit is contained in:
Janus Knudsen 2025-09-01 23:37:47 +02:00
parent 58d6ad2ed2
commit 542a6874d0
3 changed files with 121 additions and 32 deletions

View file

@ -12,6 +12,7 @@ export interface HeaderRenderer {
render(calendarHeader: HTMLElement, context: HeaderRenderContext): void;
addToAllDay(dayHeader: HTMLElement): void;
ensureAllDayContainers(calendarHeader: HTMLElement): void;
checkAndAnimateAllDayHeight(): void;
}
/**
@ -33,7 +34,7 @@ export abstract class BaseHeaderRenderer implements HeaderRenderer {
if (calendarHeader) {
// Ensure container exists BEFORE animation
this.createAllDayMainStructure(calendarHeader);
this.animateHeaderExpansion(calendarHeader);
this.checkAndAnimateAllDayHeight();
}
}
}
@ -45,30 +46,82 @@ export abstract class BaseHeaderRenderer implements HeaderRenderer {
this.createAllDayMainStructure(calendarHeader);
}
private animateHeaderExpansion(calendarHeader: HTMLElement): void {
const root = document.documentElement;
const currentHeaderHeight = parseInt(getComputedStyle(root).getPropertyValue('--header-height'));
const targetHeight = currentHeaderHeight + ALL_DAY_CONSTANTS.SINGLE_ROW_HEIGHT;
checkAndAnimateAllDayHeight(): void {
const container = document.querySelector('swp-allday-container');
if (!container) return;
// Find header spacer
const headerSpacer = document.querySelector('swp-header-spacer') as HTMLElement;
const allDayEvents = container.querySelectorAll('swp-allday-event');
// Find or create all-day container (it should exist but be hidden)
let allDayContainer = calendarHeader.querySelector('swp-allday-container') as HTMLElement;
if (!allDayContainer) {
// Create container if it doesn't exist
allDayContainer = document.createElement('swp-allday-container');
calendarHeader.appendChild(allDayContainer);
// 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 container and spacer - CSS Grid auto row will handle header expansion
// Animate to required rows (0 = collapse, >0 = expand)
this.animateToRows(maxRows);
}
/**
* Animate all-day container to specific number of rows
*/
animateToRows(targetRows: number): void {
const root = document.documentElement;
const targetHeight = targetRows * ALL_DAY_CONSTANTS.SINGLE_ROW_HEIGHT;
const currentHeight = parseInt(getComputedStyle(root).getPropertyValue('--all-day-row-height') || '0');
if (targetHeight === currentHeight) return; // No animation needed
console.log(`🎬 All-day height animation starting: ${currentHeight}px → ${targetHeight}px (${Math.ceil(currentHeight / ALL_DAY_CONSTANTS.SINGLE_ROW_HEIGHT)}${targetRows} rows)`);
// Find elements to animate
const calendarHeader = document.querySelector('swp-calendar-header') as HTMLElement;
const headerSpacer = document.querySelector('swp-header-spacer') as HTMLElement;
const allDayContainer = calendarHeader?.querySelector('swp-allday-container') as HTMLElement;
if (!calendarHeader || !allDayContainer) return;
const animations = [
// Container visibility and height animation
// Container height animation
allDayContainer.animate([
{ height: '0px', opacity: '0' },
{ height: `${ALL_DAY_CONSTANTS.SINGLE_ROW_HEIGHT}px`, opacity: '1' }
{ height: `${currentHeight}px`, opacity: currentHeight > 0 ? '1' : '0' },
{ height: `${targetHeight}px`, opacity: '1' }
], {
duration: 150,
duration: 1000,
easing: 'ease-out',
fill: 'forwards'
})
@ -76,24 +129,24 @@ export abstract class BaseHeaderRenderer implements HeaderRenderer {
// Add spacer animation if spacer exists
if (headerSpacer) {
const currentSpacerHeight = parseInt(getComputedStyle(root).getPropertyValue('--header-height')) + currentHeight;
const targetSpacerHeight = parseInt(getComputedStyle(root).getPropertyValue('--header-height')) + targetHeight;
animations.push(
headerSpacer.animate([
{ height: `${currentHeaderHeight}px` },
{ height: `${targetHeight}px` }
{ height: `${currentSpacerHeight}px` },
{ height: `${targetSpacerHeight}px` }
], {
duration: 150,
duration: 1000,
easing: 'ease-out',
fill: 'forwards'
})
);
}
// Wait for all animations to finish
// Update CSS variable after animation
Promise.all(animations.map(anim => anim.finished)).then(() => {
// Set the CSS variable after animation
root.style.setProperty('--all-day-row-height', `${ALL_DAY_CONSTANTS.SINGLE_ROW_HEIGHT}px`);
// Notify ScrollManager about header height change
root.style.setProperty('--all-day-row-height', `${targetHeight}px`);
eventBus.emit('header:height-changed');
});
}