diff --git a/src/constants/EventTypes.ts b/src/constants/EventTypes.ts index 57db036..3134579 100644 --- a/src/constants/EventTypes.ts +++ b/src/constants/EventTypes.ts @@ -81,6 +81,7 @@ export const EventTypes = { CALENDAR_INITIALIZED: 'calendar:initialized', CALENDAR_DATA_LOADED: 'calendar:calendardataloaded', GRID_RENDERED: 'calendar:gridrendered', + CONTAINER_READY_FOR_EVENTS: 'calendar:containerreadyforevents', // Management events (legacy - prefer StateEvents) REFRESH_REQUESTED: 'calendar:refreshrequested', diff --git a/src/index.ts b/src/index.ts index 5d587eb..5c57504 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,10 +36,10 @@ async function initializeCalendar(): Promise { // Create all managers console.log('📋 Creating managers...'); calendarManager = new CalendarManager(eventBus, config); - navigationManager = new NavigationManager(eventBus); + navigationManager = new NavigationManager(eventBus); // No EventRenderer dependency viewManager = new ViewManager(eventBus); eventManager = new EventManager(eventBus); - eventRenderer = new EventRenderer(eventBus); + eventRenderer = new EventRenderer(eventBus, eventManager); // Pass EventManager gridManager = new GridManager(); scrollManager = new ScrollManager(); diff --git a/src/managers/CalendarManager.ts b/src/managers/CalendarManager.ts index 4493f37..6149df8 100644 --- a/src/managers/CalendarManager.ts +++ b/src/managers/CalendarManager.ts @@ -76,14 +76,18 @@ export class CalendarManager { this.setView(this.currentView); this.setCurrentDate(this.currentDate); - // 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); + // Step 5: Event rendering will be triggered by GRID_RENDERED event + console.log('🎨 Event rendering will be triggered automatically by grid events...'); this.isInitialized = true; console.log('✅ CalendarManager: Simple initialization complete'); + // Emit initialization complete event + this.eventBus.emit(EventTypes.CALENDAR_INITIALIZED, { + currentDate: this.currentDate, + currentView: this.currentView + }); + } catch (error) { console.error('❌ CalendarManager initialization failed:', error); throw error; @@ -110,10 +114,7 @@ export class CalendarManager { date: this.currentDate }); - // Re-render events for new view if calendar is initialized - if (this.isInitialized) { - this.rerenderEventsForCurrentPeriod(); - } + // Grid re-rendering will trigger event rendering automatically via GRID_RENDERED event } /** @@ -132,10 +133,7 @@ export class CalendarManager { view: this.currentView }); - // Re-render events for new period if calendar is initialized - if (this.isInitialized) { - this.rerenderEventsForCurrentPeriod(); - } + // Grid update for new date will trigger event rendering automatically via GRID_RENDERED event } /** @@ -313,34 +311,6 @@ 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 */ @@ -399,12 +369,4 @@ export class CalendarManager { } } - /** - * Re-render events for the current period - */ - private async rerenderEventsForCurrentPeriod(): Promise { - console.log('CalendarManager: Re-rendering events for current period'); - const events = this.getEventsForCurrentPeriod(); - await this.eventRenderer.renderEvents(events); - } } \ No newline at end of file diff --git a/src/managers/EventManager.ts b/src/managers/EventManager.ts index 80633a6..95d9abc 100644 --- a/src/managers/EventManager.ts +++ b/src/managers/EventManager.ts @@ -119,6 +119,19 @@ export class EventManager { return this.events.find(event => event.id === id); } + /** + * Get events for a specific time period + */ + public getEventsForPeriod(startDate: Date, endDate: Date): CalendarEvent[] { + return this.events.filter(event => { + const eventStart = new Date(event.start); + const eventEnd = new Date(event.end); + + // Event overlaps period if it starts before period ends AND ends after period starts + return eventStart <= endDate && eventEnd >= startDate; + }); + } + public addEvent(event: Omit): CalendarEvent { const newEvent: CalendarEvent = { ...event, diff --git a/src/managers/EventRenderer.ts b/src/managers/EventRenderer.ts index e3ed664..564c73a 100644 --- a/src/managers/EventRenderer.ts +++ b/src/managers/EventRenderer.ts @@ -1,9 +1,11 @@ import { EventBus } from '../core/EventBus'; -import { IEventBus, CalendarEvent } from '../types/CalendarTypes'; +import { IEventBus, CalendarEvent, RenderContext } from '../types/CalendarTypes'; import { EventTypes } from '../constants/EventTypes'; import { StateEvents } from '../types/CalendarState'; import { calendarConfig } from '../core/CalendarConfig'; import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; +import { EventManager } from './EventManager'; +import { EventRendererStrategy } from '../renderers/EventRenderer'; /** * EventRenderer - Render events i DOM med positionering using Strategy Pattern @@ -11,73 +13,133 @@ import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; */ export class EventRenderer { private eventBus: IEventBus; + private eventManager: EventManager; + private strategy: EventRendererStrategy; - constructor(eventBus: IEventBus) { + constructor(eventBus: IEventBus, eventManager: EventManager) { this.eventBus = eventBus; + this.eventManager = eventManager; + + // Cache strategy at initialization + const calendarType = calendarConfig.getCalendarMode(); + this.strategy = CalendarTypeFactory.getEventRenderer(calendarType); + this.setupEventListeners(); } /** - * Public method to render events - called directly by CalendarManager + * Render events in a specific container for a given period */ - public async renderEvents(events: CalendarEvent[]): Promise { - console.log('EventRenderer: Direct renderEvents called with', events.length, 'events'); + public renderEvents(context: RenderContext): void { + console.log('EventRenderer: Rendering events for period', { + startDate: context.startDate, + endDate: context.endDate, + container: context.container + }); + + // Get events from EventManager for the period + const events = this.eventManager.getEventsForPeriod( + context.startDate, + context.endDate + ); + + console.log(`EventRenderer: Found ${events.length} events for period`); - // Debug: Check if we have any events if (events.length === 0) { - console.warn('EventRenderer: No events to render'); + console.log('EventRenderer: No events to render for this period'); return; } - // Debug: Log first event details - console.log('EventRenderer: First event details:', { - title: events[0].title, - start: events[0].start, - end: events[0].end, - allDay: events[0].allDay - }); - - // Get the appropriate event renderer strategy - const calendarType = calendarConfig.getCalendarMode(); - const eventRenderer = CalendarTypeFactory.getEventRenderer(calendarType); - - console.log(`EventRenderer: Using ${calendarType} event renderer strategy`); - - // Debug: Check if columns exist - const columns = document.querySelectorAll('swp-day-column'); - console.log(`EventRenderer: Found ${columns.length} day columns in DOM`); - - // Use strategy to render events - eventRenderer.renderEvents(events, calendarConfig); + // Use cached strategy to render events in the specific container + this.strategy.renderEvents(events, context.container, calendarConfig); console.log(`EventRenderer: Successfully rendered ${events.length} events`); } private setupEventListeners(): void { - // Keep only UI-related event listeners - this.eventBus.on(EventTypes.VIEW_RENDERED, () => { - // Clear existing events when view changes - this.clearEvents(); + // Event-driven rendering: React to grid and container events + this.eventBus.on(EventTypes.GRID_RENDERED, (event: Event) => { + console.log('EventRenderer: Received GRID_RENDERED event'); + this.handleGridRendered(event as CustomEvent); }); - // Handle calendar type changes + this.eventBus.on(EventTypes.CONTAINER_READY_FOR_EVENTS, (event: Event) => { + console.log('EventRenderer: Received CONTAINER_READY_FOR_EVENTS event'); + this.handleContainerReady(event as CustomEvent); + }); + + this.eventBus.on(EventTypes.VIEW_CHANGED, (event: Event) => { + console.log('EventRenderer: Received VIEW_CHANGED event'); + this.handleViewChanged(event as CustomEvent); + }); + + // Handle calendar type changes - update cached strategy this.eventBus.on(EventTypes.CALENDAR_TYPE_CHANGED, () => { - // Re-render would need to be triggered by CalendarManager now - this.clearEvents(); + const calendarType = calendarConfig.getCalendarMode(); + this.strategy = CalendarTypeFactory.getEventRenderer(calendarType); + console.log(`EventRenderer: Updated strategy to ${calendarType}`); }); } - private clearEvents(): void { - console.warn(`🗑️ EventRenderer: clearEvents() called from EventRenderer manager`); - const calendarType = calendarConfig.getCalendarMode(); - const eventRenderer = CalendarTypeFactory.getEventRenderer(calendarType); - eventRenderer.clearEvents(); + /** + * Handle GRID_RENDERED event - render events in the current grid + */ + private handleGridRendered(event: CustomEvent): void { + const { container, startDate, endDate } = event.detail; + + if (!container) { + console.error('EventRenderer: No container in GRID_RENDERED event', event.detail); + return; + } + + // Use period from event or fallback to calculated period + const periodStart = startDate; + const periodEnd = endDate; + + this.renderEvents({ + container: container, + startDate: periodStart, + endDate: periodEnd + }); } - public refresh(): void { - // Refresh would need to be coordinated by CalendarManager now + /** + * Handle CONTAINER_READY_FOR_EVENTS event - render events in pre-rendered container + */ + private handleContainerReady(event: CustomEvent): void { + const { container, startDate, endDate } = event.detail; + + if (!container || !startDate || !endDate) { + console.error('EventRenderer: Invalid CONTAINER_READY_FOR_EVENTS event data', event.detail); + return; + } + + this.renderEvents({ + container: container, + startDate: new Date(startDate), + endDate: new Date(endDate) + }); + } + + /** + * Handle VIEW_CHANGED event - clear and re-render for new view + */ + private handleViewChanged(event: CustomEvent): void { + // Clear all existing events since view structure may have changed this.clearEvents(); + + // New rendering will be triggered by subsequent GRID_RENDERED event + console.log('EventRenderer: Cleared events for view change, waiting for GRID_RENDERED'); + } + private clearEvents(container?: HTMLElement): void { + console.log(`EventRenderer: Clearing events`, container ? 'in container' : 'globally'); + this.strategy.clearEvents(container); + } + + public refresh(container?: HTMLElement): void { + // Clear events in specific container or globally + this.clearEvents(container); } public destroy(): void { diff --git a/src/managers/GridManager.ts b/src/managers/GridManager.ts index 65dcb96..40f3239 100644 --- a/src/managers/GridManager.ts +++ b/src/managers/GridManager.ts @@ -140,6 +140,16 @@ export class GridManager { const columnCount = this.getColumnCount(); console.log(`GridManager: Render complete - created ${columnCount} columns`); + + // Emit GRID_RENDERED event to trigger event rendering + const weekEnd = this.currentWeek ? new Date(this.currentWeek.getTime() + 6 * 24 * 60 * 60 * 1000) : null; + eventBus.emit(EventTypes.GRID_RENDERED, { + container: this.grid, + currentWeek: this.currentWeek, + startDate: this.currentWeek, + endDate: weekEnd, + columnCount: columnCount + }); } /** diff --git a/src/managers/NavigationManager.ts b/src/managers/NavigationManager.ts index 2bdfb5f..2d05be0 100644 --- a/src/managers/NavigationManager.ts +++ b/src/managers/NavigationManager.ts @@ -113,11 +113,11 @@ export class NavigationManager { } /** - * POC-style animation transition - creates new grid container and slides it in + * Animation transition using pre-rendered containers when available */ private animateTransition(direction: 'prev' | 'next', targetWeek: Date): void { const container = document.querySelector('swp-calendar-container'); - const currentGrid = container?.querySelector('swp-grid-container'); + const currentGrid = container?.querySelector('swp-grid-container:not([data-prerendered])'); if (!container || !currentGrid) { console.warn('NavigationManager: Required DOM elements not found'); @@ -126,31 +126,15 @@ export class NavigationManager { console.log(`NavigationManager: Starting ${direction} animation to ${targetWeek.toDateString()}`); - // Create new grid container (POC approach) - const newGrid = document.createElement('swp-grid-container'); - newGrid.innerHTML = ` - - - - - - - - `; + let newGrid: HTMLElement; - // 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%)'; + // Always create a fresh container for consistent behavior + console.log('NavigationManager: Creating new container'); + newGrid = this.renderContainer(container as HTMLElement, targetWeek); - // Add to container - container.appendChild(newGrid); - - // Render new content for target week - this.renderWeekContent(newGrid, targetWeek); + // Clear any existing transforms before animation + newGrid.style.transform = ''; + (currentGrid as HTMLElement).style.transform = ''; // Animate transition using Web Animations API const slideOutAnimation = (currentGrid as HTMLElement).animate([ @@ -181,6 +165,7 @@ export class NavigationManager { // Reset positioning newGrid.style.position = 'relative'; + newGrid.removeAttribute('data-prerendered'); // Update state this.currentWeek = new Date(targetWeek); @@ -198,6 +183,7 @@ export class NavigationManager { weekEnd: DateUtils.addDays(this.currentWeek, 6) }); + // Emit animation complete event for ScrollManager this.eventBus.emit(EventTypes.NAVIGATION_ANIMATION_COMPLETE, { direction, @@ -335,4 +321,97 @@ export class NavigationManager { weekEnd: DateUtils.addDays(this.currentWeek, 6) }); } + + /** + * Render a complete container with content and events + */ + private renderContainer(parentContainer: HTMLElement, weekStart: Date): HTMLElement { + console.log('NavigationManager: Rendering new container for week:', weekStart.toDateString()); + + // Create new grid container + const newGrid = document.createElement('swp-grid-container'); + newGrid.innerHTML = ` + + + + + + + + `; + + // Position new grid - NO transform here, let Animation API handle it + newGrid.style.position = 'absolute'; + newGrid.style.top = '0'; + newGrid.style.left = '0'; + newGrid.style.width = '100%'; + newGrid.style.height = '100%'; + + // Add to parent container + parentContainer.appendChild(newGrid); + + // Render week content (headers and columns) + this.renderWeekContentInContainer(newGrid, weekStart); + + // Emit event to trigger event rendering + const weekEnd = DateUtils.addDays(weekStart, 6); + this.eventBus.emit(EventTypes.CONTAINER_READY_FOR_EVENTS, { + container: newGrid, + startDate: weekStart, + endDate: weekEnd + }); + + console.log('NavigationManager: Container rendered with content and events triggered'); + return newGrid; + } + + + + /** + * Render week content in specific container + */ + private renderWeekContentInContainer(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 = ` + ${days[date.getDay()]} + ${date.getDate()} + `; + headerElement.dataset.date = this.formatDate(date); + + header.appendChild(headerElement); + } + + // Render day columns for target week (with hardcoded test event) + 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); + } + } } \ No newline at end of file diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index f66532b..de2f312 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -8,18 +8,15 @@ import { DateUtils } from '../utils/DateUtils'; * Interface for event rendering strategies */ export interface EventRendererStrategy { - findColumn(event: CalendarEvent): HTMLElement | null; - renderEvents(events: CalendarEvent[], config: CalendarConfig): void; - clearEvents(): void; + renderEvents(events: CalendarEvent[], container: HTMLElement, config: CalendarConfig): void; + clearEvents(container?: HTMLElement): void; } /** * Base class for event renderers with common functionality */ export abstract class BaseEventRenderer implements EventRendererStrategy { - abstract findColumn(event: CalendarEvent): HTMLElement | null; - - renderEvents(events: CalendarEvent[], config: CalendarConfig): void { + renderEvents(events: CalendarEvent[], container: HTMLElement, config: CalendarConfig): void { console.log('BaseEventRenderer: renderEvents called with', events.length, 'events'); // NOTE: Removed clearEvents() to support sliding animation @@ -30,10 +27,9 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { // Events should already be filtered by DataManager - no need to filter here console.log('BaseEventRenderer: Rendering', events.length, 'pre-filtered events'); - // OPTIMIZATION: Column-first rendering instead of event-first - // This is much more efficient when there are many events - const columns = this.getColumns(); - console.log(`BaseEventRenderer: Found ${columns.length} columns to render events in`); + // Find columns in the specific container + const columns = this.getColumns(container); + console.log(`BaseEventRenderer: Found ${columns.length} columns in container`); columns.forEach(column => { const columnEvents = this.getEventsForColumn(column, events); @@ -55,8 +51,8 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { }); } - // Abstract methods that subclasses must implement for column-first rendering - protected abstract getColumns(): HTMLElement[]; + // Abstract methods that subclasses must implement + protected abstract getColumns(container: HTMLElement): HTMLElement[]; protected abstract getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[]; protected renderEvent(event: CalendarEvent, container: Element, config: CalendarConfig): void { @@ -138,10 +134,15 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { return `${displayHour}:${minutes.toString().padStart(2, '0')} ${period}`; } - clearEvents(): void { - const existingEvents = document.querySelectorAll('swp-event'); + clearEvents(container?: HTMLElement): void { + const selector = 'swp-event'; + const existingEvents = container + ? container.querySelectorAll(selector) + : document.querySelectorAll(selector); + if (existingEvents.length > 0) { - console.warn(`🗑️ BaseEventRenderer: REMOVING ${existingEvents.length} events from DOM! Stack trace:`, new Error().stack); + console.log(`BaseEventRenderer: Clearing ${existingEvents.length} events`, + container ? 'from container' : 'globally'); } existingEvents.forEach(event => event.remove()); } @@ -151,26 +152,9 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { * Date-based event renderer */ export class DateEventRenderer extends BaseEventRenderer { - findColumn(event: CalendarEvent): HTMLElement | null { - const eventDate = new Date(event.start); - const dateStr = DateUtils.formatDate(eventDate); - const dayColumn = document.querySelector(`swp-day-column[data-date="${dateStr}"]`) as HTMLElement; - - // Debug: Check all available columns - const allColumns = document.querySelectorAll('swp-day-column'); - const availableDates = Array.from(allColumns).map(col => (col as HTMLElement).dataset.date); - - console.log('DateEventRenderer: Event', event.title, 'start:', event.start); - console.log('DateEventRenderer: Looking for date:', dateStr); - console.log('DateEventRenderer: Available columns with dates:', availableDates); - console.log('DateEventRenderer: Found column:', !!dayColumn); - - return dayColumn; - } - - protected getColumns(): HTMLElement[] { - const columns = document.querySelectorAll('swp-day-column'); - console.log('DateEventRenderer: Found', columns.length, 'day columns in DOM'); + protected getColumns(container: HTMLElement): HTMLElement[] { + const columns = container.querySelectorAll('swp-day-column'); + console.log('DateEventRenderer: Found', columns.length, 'day columns in container'); return Array.from(columns) as HTMLElement[]; } @@ -193,21 +177,9 @@ export class DateEventRenderer extends BaseEventRenderer { * Resource-based event renderer */ export class ResourceEventRenderer extends BaseEventRenderer { - findColumn(event: CalendarEvent): HTMLElement | null { - 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="${resourceName}"]`) as HTMLElement; - console.log('ResourceEventRenderer: Looking for resource column with name', resourceName, 'found:', !!resourceColumn); - return resourceColumn; - } - - protected getColumns(): HTMLElement[] { - const columns = document.querySelectorAll('swp-resource-column'); - console.log('ResourceEventRenderer: Found', columns.length, 'resource columns in DOM'); + protected getColumns(container: HTMLElement): HTMLElement[] { + const columns = container.querySelectorAll('swp-resource-column'); + console.log('ResourceEventRenderer: Found', columns.length, 'resource columns in container'); return Array.from(columns) as HTMLElement[]; } diff --git a/src/types/CalendarTypes.ts b/src/types/CalendarTypes.ts index 76805a9..97756c1 100644 --- a/src/types/CalendarTypes.ts +++ b/src/types/CalendarTypes.ts @@ -27,6 +27,12 @@ export interface ResourceCalendarData { resources: Resource[]; } +export interface RenderContext { + container: HTMLElement; + startDate: Date; + endDate: Date; +} + export interface CalendarEvent { id: string; title: string;