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:
parent
58d6ad2ed2
commit
542a6874d0
3 changed files with 121 additions and 32 deletions
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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/**/*"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue