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

@ -5,6 +5,7 @@ import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig';
import { CalendarConfig } from '../core/CalendarConfig'; import { CalendarConfig } from '../core/CalendarConfig';
import { DateCalculator } from '../utils/DateCalculator'; import { DateCalculator } from '../utils/DateCalculator';
import { eventBus } from '../core/EventBus'; import { eventBus } from '../core/EventBus';
import { CoreEvents } from '../constants/CoreEvents';
/** /**
* Interface for event rendering strategies * Interface for event rendering strategies
@ -63,6 +64,15 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
const { eventId, targetDate, headerRenderer } = (event as CustomEvent).detail; const { eventId, targetDate, headerRenderer } = (event as CustomEvent).detail;
this.handleConvertToAllDay(eventId, targetDate, headerRenderer); this.handleConvertToAllDay(eventId, targetDate, headerRenderer);
}); });
// Handle navigation period change (when slide animation completes)
eventBus.on(CoreEvents.PERIOD_CHANGED, () => {
// Animate all-day height after navigation completes
import('./HeaderRenderer').then(({ DateHeaderRenderer }) => {
const headerRenderer = new DateHeaderRenderer();
headerRenderer.checkAndAnimateAllDayHeight();
});
});
} }
/** /**
@ -278,10 +288,14 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
const allDayContainer = calendarHeader.querySelector('swp-allday-container'); const allDayContainer = calendarHeader.querySelector('swp-allday-container');
if (!allDayContainer) return; if (!allDayContainer) return;
// Extract title // Extract all original event data
const titleElement = clone.querySelector('swp-event-title'); const titleElement = clone.querySelector('swp-event-title');
const eventTitle = titleElement ? titleElement.textContent || 'Untitled' : 'Untitled'; const eventTitle = titleElement ? titleElement.textContent || 'Untitled' : 'Untitled';
const timeElement = clone.querySelector('swp-event-time');
const eventTime = timeElement ? timeElement.textContent || '' : '';
const eventDuration = timeElement ? timeElement.getAttribute('data-duration') || '' : '';
// Calculate column index // Calculate column index
const dayHeaders = document.querySelectorAll('swp-day-header'); const dayHeaders = document.querySelectorAll('swp-day-header');
let columnIndex = 1; let columnIndex = 1;
@ -291,15 +305,19 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
} }
}); });
// Create all-day event // Create all-day event with standardized data attributes
const allDayEvent = document.createElement('swp-allday-event'); const allDayEvent = document.createElement('swp-allday-event');
allDayEvent.dataset.eventId = clone.dataset.eventId || ''; allDayEvent.dataset.eventId = clone.dataset.eventId || '';
allDayEvent.dataset.title = eventTitle;
allDayEvent.dataset.start = `${targetDate}T${eventTime.split(' - ')[0]}:00`;
allDayEvent.dataset.end = `${targetDate}T${eventTime.split(' - ')[1]}:00`;
allDayEvent.dataset.type = clone.dataset.type || 'work'; allDayEvent.dataset.type = clone.dataset.type || 'work';
allDayEvent.dataset.duration = eventDuration;
allDayEvent.textContent = eventTitle; allDayEvent.textContent = eventTitle;
// Position in grid // Position in grid
(allDayEvent as HTMLElement).style.gridColumn = columnIndex.toString(); (allDayEvent as HTMLElement).style.gridColumn = columnIndex.toString();
(allDayEvent as HTMLElement).style.gridRow = '1'; // grid-row will be set by checkAndAnimateAllDayHeight() based on actual position
// Remove original clone // Remove original clone
if (clone.parentElement) { if (clone.parentElement) {
@ -311,8 +329,16 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
// Update reference // Update reference
this.draggedClone = allDayEvent; this.draggedClone = allDayEvent;
// Check if height animation is needed
import('./HeaderRenderer').then(({ DateHeaderRenderer }) => {
const headerRenderer = new DateHeaderRenderer();
headerRenderer.checkAndAnimateAllDayHeight();
});
} }
/** /**
* Fade out and remove element * Fade out and remove element
*/ */
@ -487,8 +513,14 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
// Create the all-day event element // Create the all-day event element
const allDayEvent = document.createElement('swp-allday-event'); const allDayEvent = document.createElement('swp-allday-event');
allDayEvent.textContent = event.title; allDayEvent.textContent = event.title;
allDayEvent.setAttribute('data-event-id', event.id);
allDayEvent.setAttribute('data-type', event.type || 'work'); // Set data attributes directly from CalendarEvent
allDayEvent.dataset.eventId = event.id;
allDayEvent.dataset.title = event.title;
allDayEvent.dataset.start = event.start;
allDayEvent.dataset.end = event.end;
allDayEvent.dataset.type = event.type;
allDayEvent.dataset.duration = event.metadata?.duration?.toString() || '60';
// Set grid position (column and row) // Set grid position (column and row)
(allDayEvent as HTMLElement).style.gridColumn = span.columnSpan > 1 (allDayEvent as HTMLElement).style.gridColumn = span.columnSpan > 1
@ -510,7 +542,11 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
protected renderEvent(event: CalendarEvent, container: Element, config: CalendarConfig): void { protected renderEvent(event: CalendarEvent, container: Element, config: CalendarConfig): void {
const eventElement = document.createElement('swp-event'); const eventElement = document.createElement('swp-event');
eventElement.dataset.eventId = event.id; eventElement.dataset.eventId = event.id;
eventElement.dataset.title = event.title;
eventElement.dataset.start = event.start;
eventElement.dataset.end = event.end;
eventElement.dataset.type = event.type; eventElement.dataset.type = event.type;
eventElement.dataset.duration = event.metadata?.duration?.toString() || '60';
// Calculate position based on time // Calculate position based on time
const position = this.calculateEventPosition(event, config); const position = this.calculateEventPosition(event, config);

View file

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

View file

@ -12,7 +12,7 @@
"rootDir": "./src", "rootDir": "./src",
"sourceMap": true, "sourceMap": true,
"inlineSourceMap": false, "inlineSourceMap": false,
"lib": ["ES2020", "DOM", "DOM.Iterable"] "lib": ["ES2024", "DOM", "DOM.Iterable"]
}, },
"include": [ "include": [
"src/**/*" "src/**/*"