Steps in the right direction for animated date change
This commit is contained in:
parent
5e966ddea2
commit
f50f5ad53b
7 changed files with 378 additions and 37 deletions
|
|
@ -24,6 +24,7 @@ export const EventTypes = {
|
|||
// Navigation events
|
||||
WEEK_CHANGED: 'calendar:weekchanged',
|
||||
WEEK_INFO_UPDATED: 'calendar:weekinfoupdated',
|
||||
WEEK_CONTENT_RENDERED: 'calendar:weekcontentrendered',
|
||||
NAV_PREV: 'calendar:navprev',
|
||||
NAV_NEXT: 'calendar:navnext',
|
||||
NAV_TODAY: 'calendar:navtoday',
|
||||
|
|
|
|||
|
|
@ -76,9 +76,9 @@ export class CalendarManager {
|
|||
this.setView(this.currentView);
|
||||
this.setCurrentDate(this.currentDate);
|
||||
|
||||
// Step 5: Render events (after view is set)
|
||||
console.log('🎨 Rendering events...');
|
||||
const events = this.eventManager.getEvents();
|
||||
// Step 5: Render events (after view is set) - only render events for current period
|
||||
console.log('🎨 Rendering events for current period...');
|
||||
const events = this.getEventsForCurrentPeriod();
|
||||
await this.eventRenderer.renderEvents(events);
|
||||
|
||||
this.isInitialized = true;
|
||||
|
|
@ -109,6 +109,11 @@ export class CalendarManager {
|
|||
currentView: view,
|
||||
date: this.currentDate
|
||||
});
|
||||
|
||||
// Re-render events for new view if calendar is initialized
|
||||
if (this.isInitialized) {
|
||||
this.rerenderEventsForCurrentPeriod();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -126,6 +131,11 @@ export class CalendarManager {
|
|||
currentDate: this.currentDate,
|
||||
view: this.currentView
|
||||
});
|
||||
|
||||
// Re-render events for new period if calendar is initialized
|
||||
if (this.isInitialized) {
|
||||
this.rerenderEventsForCurrentPeriod();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -302,4 +312,99 @@ export class CalendarManager {
|
|||
|
||||
return previousDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events filtered for the current period (week/month/day)
|
||||
*/
|
||||
private getEventsForCurrentPeriod(): CalendarEvent[] {
|
||||
const allEvents = this.eventManager.getEvents();
|
||||
|
||||
// Calculate current period based on view
|
||||
const period = this.calculateCurrentPeriod();
|
||||
|
||||
// Filter events to only include those in the current period
|
||||
const filteredEvents = allEvents.filter(event => {
|
||||
const eventStart = new Date(event.start);
|
||||
const eventEnd = new Date(event.end);
|
||||
const periodStart = new Date(period.start);
|
||||
const periodEnd = new Date(period.end);
|
||||
|
||||
// Include event if it overlaps with the period
|
||||
return eventStart <= periodEnd && eventEnd >= periodStart;
|
||||
});
|
||||
|
||||
// Also filter out all-day events (handled by GridManager)
|
||||
const nonAllDayEvents = filteredEvents.filter(event => !event.allDay);
|
||||
|
||||
console.log(`CalendarManager: Filtered ${allEvents.length} total events to ${nonAllDayEvents.length} non-all-day events for current period`);
|
||||
|
||||
return nonAllDayEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the current period based on view and date
|
||||
*/
|
||||
private calculateCurrentPeriod(): { start: string; end: string } {
|
||||
const current = new Date(this.currentDate);
|
||||
|
||||
switch (this.currentView) {
|
||||
case 'day':
|
||||
const dayStart = new Date(current);
|
||||
dayStart.setHours(0, 0, 0, 0);
|
||||
const dayEnd = new Date(current);
|
||||
dayEnd.setHours(23, 59, 59, 999);
|
||||
return {
|
||||
start: dayStart.toISOString(),
|
||||
end: dayEnd.toISOString()
|
||||
};
|
||||
|
||||
case 'week':
|
||||
// Find start of week (Monday)
|
||||
const weekStart = new Date(current);
|
||||
const dayOfWeek = weekStart.getDay();
|
||||
const daysToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // Sunday = 0, so 6 days back to Monday
|
||||
weekStart.setDate(weekStart.getDate() - daysToMonday);
|
||||
weekStart.setHours(0, 0, 0, 0);
|
||||
|
||||
// Find end of week (Sunday)
|
||||
const weekEnd = new Date(weekStart);
|
||||
weekEnd.setDate(weekEnd.getDate() + 6);
|
||||
weekEnd.setHours(23, 59, 59, 999);
|
||||
|
||||
return {
|
||||
start: weekStart.toISOString(),
|
||||
end: weekEnd.toISOString()
|
||||
};
|
||||
|
||||
case 'month':
|
||||
const monthStart = new Date(current.getFullYear(), current.getMonth(), 1);
|
||||
const monthEnd = new Date(current.getFullYear(), current.getMonth() + 1, 0, 23, 59, 59, 999);
|
||||
return {
|
||||
start: monthStart.toISOString(),
|
||||
end: monthEnd.toISOString()
|
||||
};
|
||||
|
||||
default:
|
||||
// Fallback to week view
|
||||
const fallbackStart = new Date(current);
|
||||
fallbackStart.setDate(fallbackStart.getDate() - 3);
|
||||
fallbackStart.setHours(0, 0, 0, 0);
|
||||
const fallbackEnd = new Date(current);
|
||||
fallbackEnd.setDate(fallbackEnd.getDate() + 3);
|
||||
fallbackEnd.setHours(23, 59, 59, 999);
|
||||
return {
|
||||
start: fallbackStart.toISOString(),
|
||||
end: fallbackEnd.toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-render events for the current period
|
||||
*/
|
||||
private async rerenderEventsForCurrentPeriod(): Promise<void> {
|
||||
console.log('CalendarManager: Re-rendering events for current period');
|
||||
const events = this.getEventsForCurrentPeriod();
|
||||
await this.eventRenderer.renderEvents(events);
|
||||
}
|
||||
}
|
||||
|
|
@ -113,6 +113,44 @@ export class DataManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter events to only include those within the specified period
|
||||
*/
|
||||
public filterEventsForPeriod(events: CalendarEvent[], period: Period): CalendarEvent[] {
|
||||
const startDate = new Date(period.start);
|
||||
const endDate = new Date(period.end);
|
||||
|
||||
return events.filter(event => {
|
||||
const eventStart = new Date(event.start);
|
||||
const eventEnd = new Date(event.end);
|
||||
|
||||
// Include event if it overlaps with the period
|
||||
return eventStart <= endDate && eventEnd >= startDate;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events filtered by period and optionally by all-day status
|
||||
*/
|
||||
public getFilteredEvents(period: Period, excludeAllDay: boolean = false): CalendarEvent[] {
|
||||
const cacheKey = `${period.start}-${period.end}`;
|
||||
const cachedData = this.cache.get(cacheKey);
|
||||
|
||||
if (!cachedData) {
|
||||
console.warn('DataManager: No cached data found for period', period);
|
||||
return [];
|
||||
}
|
||||
|
||||
let filteredEvents = this.filterEventsForPeriod(cachedData.events, period);
|
||||
|
||||
if (excludeAllDay) {
|
||||
filteredEvents = filteredEvents.filter(event => !event.allDay);
|
||||
console.log(`DataManager: Filtered out all-day events, ${filteredEvents.length} non-all-day events remaining`);
|
||||
}
|
||||
|
||||
return filteredEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new event
|
||||
*/
|
||||
|
|
@ -270,7 +308,7 @@ export class DataManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* Generate mock data for testing
|
||||
* Generate mock data for testing - only generates events within the specified period
|
||||
*/
|
||||
private getMockData(period: Period): EventData {
|
||||
const events: CalendarEvent[] = [];
|
||||
|
|
@ -282,11 +320,13 @@ export class DataManager {
|
|||
milestone: ['Project Deadline', 'Release Day', 'Demo Day']
|
||||
};
|
||||
|
||||
// Parse dates
|
||||
// Parse dates - only generate events within this exact period
|
||||
const startDate = new Date(period.start);
|
||||
const endDate = new Date(period.end);
|
||||
|
||||
// Generate some events for each day
|
||||
console.log(`DataManager: Generating mock events for period ${period.start} to ${period.end}`);
|
||||
|
||||
// Generate some events for each day within the period
|
||||
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
|
||||
// Skip weekends for most events
|
||||
const dayOfWeek = d.getDay();
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ export class EventManager {
|
|||
}
|
||||
|
||||
private setupEventListeners(): void {
|
||||
// Keep only UI-related event listeners here if needed
|
||||
// Data loading is now handled via direct method calls
|
||||
// NOTE: Removed POC event listener to prevent interference with production code
|
||||
// POC sliding animation should not trigger separate event rendering
|
||||
// this.eventBus.on(EventTypes.WEEK_CONTENT_RENDERED, ...);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -170,6 +171,82 @@ export class EventManager {
|
|||
this.syncEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load events for a specific week into a container (POC-style)
|
||||
*/
|
||||
private loadEventsForWeek(weekStart: Date, weekEnd: Date, container: HTMLElement): void {
|
||||
console.log(`EventManager: Loading events for week ${weekStart.toDateString()} - ${weekEnd.toDateString()}`);
|
||||
|
||||
// Filter events for this week
|
||||
const weekEvents = this.events.filter(event => {
|
||||
const eventDate = new Date(event.start);
|
||||
return eventDate >= weekStart && eventDate <= weekEnd;
|
||||
});
|
||||
|
||||
console.log(`EventManager: Found ${weekEvents.length} events for this week`);
|
||||
|
||||
// Render events in the container (POC approach)
|
||||
this.renderEventsInContainer(weekEvents, container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render events in a specific container (POC-style)
|
||||
*/
|
||||
private renderEventsInContainer(events: CalendarEvent[], container: HTMLElement): void {
|
||||
const dayColumns = container.querySelectorAll('swp-day-column');
|
||||
|
||||
events.forEach(event => {
|
||||
const eventDate = new Date(event.start);
|
||||
const dayOfWeek = eventDate.getDay(); // 0 = Sunday
|
||||
const column = dayColumns[dayOfWeek];
|
||||
|
||||
if (column) {
|
||||
const eventsLayer = column.querySelector('swp-events-layer');
|
||||
if (eventsLayer) {
|
||||
this.renderEventInColumn(event, eventsLayer as HTMLElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single event in a column (POC-style)
|
||||
*/
|
||||
private renderEventInColumn(event: CalendarEvent, eventsLayer: HTMLElement): void {
|
||||
const eventElement = document.createElement('swp-event');
|
||||
eventElement.dataset.type = event.type || 'meeting';
|
||||
|
||||
// Calculate position (simplified - assumes 7 AM start like POC)
|
||||
const startTime = new Date(event.start);
|
||||
const hours = startTime.getHours();
|
||||
const minutes = startTime.getMinutes();
|
||||
const startMinutes = (hours - 7) * 60 + minutes; // 7 is start hour like POC
|
||||
|
||||
// Calculate duration
|
||||
const endTime = new Date(event.end);
|
||||
const durationMs = endTime.getTime() - startTime.getTime();
|
||||
const durationMinutes = Math.floor(durationMs / (1000 * 60));
|
||||
|
||||
eventElement.style.top = `${startMinutes}px`;
|
||||
eventElement.style.height = `${durationMinutes}px`;
|
||||
|
||||
eventElement.innerHTML = `
|
||||
<swp-event-time>${this.formatTime(hours, minutes)}</swp-event-time>
|
||||
<swp-event-title>${event.title}</swp-event-title>
|
||||
`;
|
||||
|
||||
eventsLayer.appendChild(eventElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time for display (POC-style)
|
||||
*/
|
||||
private formatTime(hours: number, minutes: number): string {
|
||||
const period = hours >= 12 ? 'PM' : 'AM';
|
||||
const displayHours = hours % 12 || 12;
|
||||
return `${displayHours}:${String(minutes).padStart(2, '0')} ${period}`;
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.events = [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,28 +112,65 @@ export class NavigationManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POC-style animation transition - creates new grid container and slides it in
|
||||
*/
|
||||
private animateTransition(direction: 'prev' | 'next', targetWeek: Date): void {
|
||||
const calendarContainer = document.querySelector('swp-calendar-container');
|
||||
const container = document.querySelector('swp-calendar-container');
|
||||
const currentGrid = container?.querySelector('swp-grid-container');
|
||||
|
||||
if (!calendarContainer) {
|
||||
console.warn('NavigationManager: Calendar container not found');
|
||||
if (!container || !currentGrid) {
|
||||
console.warn('NavigationManager: Required DOM elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add transition class for visual feedback
|
||||
calendarContainer.classList.add('week-transition');
|
||||
console.log(`NavigationManager: Starting ${direction} animation to ${targetWeek.toDateString()}`);
|
||||
|
||||
// Brief fade effect
|
||||
setTimeout(() => {
|
||||
calendarContainer.classList.add('week-transition-out');
|
||||
// Create new grid container (POC approach)
|
||||
const newGrid = document.createElement('swp-grid-container');
|
||||
newGrid.innerHTML = `
|
||||
<swp-calendar-header></swp-calendar-header>
|
||||
<swp-scrollable-content>
|
||||
<swp-time-grid>
|
||||
<swp-grid-lines></swp-grid-lines>
|
||||
<swp-day-columns></swp-day-columns>
|
||||
</swp-time-grid>
|
||||
</swp-scrollable-content>
|
||||
`;
|
||||
|
||||
// Update the week after fade starts
|
||||
// Position new grid off-screen (POC positioning)
|
||||
newGrid.style.position = 'absolute';
|
||||
newGrid.style.top = '0';
|
||||
newGrid.style.left = '0';
|
||||
newGrid.style.width = '100%';
|
||||
newGrid.style.height = '100%';
|
||||
newGrid.style.transform = direction === 'next' ? 'translateX(100%)' : 'translateX(-100%)';
|
||||
|
||||
// Add to container
|
||||
container.appendChild(newGrid);
|
||||
|
||||
// Render new content for target week
|
||||
this.renderWeekContent(newGrid, targetWeek);
|
||||
|
||||
// Animate transition (POC animation)
|
||||
requestAnimationFrame(() => {
|
||||
// Slide out current grid
|
||||
(currentGrid as HTMLElement).style.transform = direction === 'next' ? 'translateX(-100%)' : 'translateX(100%)';
|
||||
(currentGrid as HTMLElement).style.opacity = '0.5';
|
||||
|
||||
// Slide in new grid
|
||||
newGrid.style.transform = 'translateX(0)';
|
||||
|
||||
// Clean up after animation (POC cleanup)
|
||||
setTimeout(() => {
|
||||
// Update currentWeek
|
||||
currentGrid.remove();
|
||||
newGrid.style.position = 'relative';
|
||||
|
||||
// Update currentWeek only after animation is complete (POC logic)
|
||||
this.currentWeek = new Date(targetWeek);
|
||||
this.animationQueue--;
|
||||
|
||||
// If this was the last queued animation, ensure we're in sync
|
||||
// If this was the last queued animation, ensure we're in sync (POC sync)
|
||||
if (this.animationQueue === 0) {
|
||||
this.currentWeek = new Date(this.targetWeek);
|
||||
}
|
||||
|
|
@ -145,13 +182,70 @@ export class NavigationManager {
|
|||
weekEnd: DateUtils.addDays(this.currentWeek, 6)
|
||||
});
|
||||
|
||||
// Remove transition classes
|
||||
setTimeout(() => {
|
||||
calendarContainer.classList.remove('week-transition', 'week-transition-out');
|
||||
}, 150);
|
||||
console.log(`NavigationManager: Completed ${direction} animation`);
|
||||
}, 400); // Match POC timing
|
||||
});
|
||||
}
|
||||
|
||||
}, 150); // Half of transition duration
|
||||
}, 50);
|
||||
/**
|
||||
* Render week content in the new grid container
|
||||
*/
|
||||
private renderWeekContent(gridContainer: HTMLElement, weekStart: Date): void {
|
||||
const header = gridContainer.querySelector('swp-calendar-header');
|
||||
const dayColumns = gridContainer.querySelector('swp-day-columns');
|
||||
|
||||
if (!header || !dayColumns) return;
|
||||
|
||||
// Clear existing content
|
||||
header.innerHTML = '';
|
||||
dayColumns.innerHTML = '';
|
||||
|
||||
// Render headers for target week
|
||||
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const date = new Date(weekStart);
|
||||
date.setDate(date.getDate() + i);
|
||||
|
||||
const headerElement = document.createElement('swp-day-header');
|
||||
if (this.isToday(date)) {
|
||||
headerElement.dataset.today = 'true';
|
||||
}
|
||||
|
||||
headerElement.innerHTML = `
|
||||
<swp-day-name>${days[date.getDay()]}</swp-day-name>
|
||||
<swp-day-date>${date.getDate()}</swp-day-date>
|
||||
`;
|
||||
headerElement.dataset.date = this.formatDate(date);
|
||||
|
||||
header.appendChild(headerElement);
|
||||
}
|
||||
|
||||
// Render day columns for target week
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const column = document.createElement('swp-day-column');
|
||||
const date = new Date(weekStart);
|
||||
date.setDate(date.getDate() + i);
|
||||
column.dataset.date = this.formatDate(date);
|
||||
|
||||
const eventsLayer = document.createElement('swp-events-layer');
|
||||
column.appendChild(eventsLayer);
|
||||
|
||||
dayColumns.appendChild(column);
|
||||
}
|
||||
|
||||
// NOTE: Removed POC event emission to prevent interference with production code
|
||||
// POC events should not trigger production event rendering
|
||||
// this.eventBus.emit(EventTypes.WEEK_CONTENT_RENDERED, { ... });
|
||||
}
|
||||
|
||||
// Utility functions (from POC)
|
||||
private formatDate(date: Date): string {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
private isToday(date: Date): boolean {
|
||||
const today = new Date();
|
||||
return date.toDateString() === today.toDateString();
|
||||
}
|
||||
|
||||
private updateWeekInfo(): void {
|
||||
|
|
|
|||
|
|
@ -25,12 +25,11 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
// Clear existing events first
|
||||
this.clearEvents();
|
||||
|
||||
// Filter out all-day events (handled by GridManager)
|
||||
const nonAllDayEvents = events.filter(event => !event.allDay);
|
||||
console.log('BaseEventRenderer: Rendering', nonAllDayEvents.length, 'non-all-day events');
|
||||
// Events should already be filtered by DataManager - no need to filter here
|
||||
console.log('BaseEventRenderer: Rendering', events.length, 'pre-filtered events');
|
||||
|
||||
// Render each event in the correct column
|
||||
nonAllDayEvents.forEach(event => {
|
||||
events.forEach(event => {
|
||||
const column = this.findColumn(event);
|
||||
|
||||
if (column) {
|
||||
|
|
@ -166,13 +165,14 @@ export class DateEventRenderer extends BaseEventRenderer {
|
|||
*/
|
||||
export class ResourceEventRenderer extends BaseEventRenderer {
|
||||
findColumn(event: CalendarEvent): HTMLElement | null {
|
||||
if (!event.resourceName) {
|
||||
console.warn('ResourceEventRenderer: Event has no resourceName', event);
|
||||
const resourceName = event.resource?.name;
|
||||
if (!resourceName) {
|
||||
console.warn('ResourceEventRenderer: Event has no resource.name', event);
|
||||
return null;
|
||||
}
|
||||
|
||||
const resourceColumn = document.querySelector(`swp-resource-column[data-resource="${event.resourceName}"]`) as HTMLElement;
|
||||
console.log('ResourceEventRenderer: Looking for resource column with name', event.resourceName, 'found:', !!resourceColumn);
|
||||
const resourceColumn = document.querySelector(`swp-resource-column[data-resource="${resourceName}"]`) as HTMLElement;
|
||||
console.log('ResourceEventRenderer: Looking for resource column with name', resourceName, 'found:', !!resourceColumn);
|
||||
return resourceColumn;
|
||||
}
|
||||
}
|
||||
24
wwwroot/css/calendar-sliding-animation.css
Normal file
24
wwwroot/css/calendar-sliding-animation.css
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/* POC-style Calendar Sliding Animation CSS */
|
||||
|
||||
/* Grid container base styles */
|
||||
swp-grid-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
transition: transform 400ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
will-change: transform;
|
||||
backface-visibility: hidden;
|
||||
transform: translateZ(0); /* GPU acceleration */
|
||||
}
|
||||
|
||||
/* Calendar container for sliding */
|
||||
swp-calendar-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Accessibility: Respect reduced motion preference */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
swp-grid-container {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue