// Event rendering strategy interface and implementations
import { CalendarEvent } from '../types/CalendarTypes';
import { CalendarConfig } from '../core/CalendarConfig';
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;
}
/**
* 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 {
console.log('BaseEventRenderer: renderEvents called with', events.length, 'events');
// NOTE: Removed clearEvents() to support sliding animation
// With sliding animation, multiple grid containers exist simultaneously
// clearEvents() would remove events from all containers, breaking the animation
// Events are now rendered directly into the new container without clearing
// 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`);
columns.forEach(column => {
const columnEvents = this.getEventsForColumn(column, events);
console.log(`BaseEventRenderer: Rendering ${columnEvents.length} events in column`);
const eventsLayer = column.querySelector('swp-events-layer');
if (eventsLayer) {
columnEvents.forEach(event => {
console.log(`BaseEventRenderer: Rendering event "${event.title}" in events layer`);
this.renderEvent(event, eventsLayer, config);
});
// Debug: Verify events were actually added
const renderedEvents = eventsLayer.querySelectorAll('swp-event');
console.log(`BaseEventRenderer: Events layer now has ${renderedEvents.length} events`);
} else {
console.warn('BaseEventRenderer: No events layer found in column');
}
});
}
// Abstract methods that subclasses must implement for column-first rendering
protected abstract getColumns(): HTMLElement[];
protected abstract getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[];
protected renderEvent(event: CalendarEvent, container: Element, config: CalendarConfig): void {
const eventElement = document.createElement('swp-event');
eventElement.dataset.eventId = event.id;
eventElement.dataset.type = event.type;
// Calculate position based on time
const position = this.calculateEventPosition(event, config);
eventElement.style.position = 'absolute';
eventElement.style.top = `${position.top + 1}px`;
eventElement.style.height = `${position.height - 1}px`;
// Set color
eventElement.style.backgroundColor = event.metadata?.color || '#3498db';
// Format time for display
const startTime = this.formatTime(event.start);
const endTime = this.formatTime(event.end);
// Create event content
eventElement.innerHTML = `
${startTime} - ${endTime}
${event.title}
`;
container.appendChild(eventElement);
console.log(`BaseEventRenderer: Created event element for "${event.title}":`, {
top: eventElement.style.top,
height: eventElement.style.height,
backgroundColor: eventElement.style.backgroundColor,
position: eventElement.style.position,
innerHTML: eventElement.innerHTML
});
}
protected calculateEventPosition(event: CalendarEvent, config: CalendarConfig): { top: number; height: number } {
const startDate = new Date(event.start);
const endDate = new Date(event.end);
const gridSettings = config.getGridSettings();
const dayStartHour = gridSettings.dayStartHour;
const hourHeight = gridSettings.hourHeight;
// Calculate minutes from visible day start
const startMinutes = startDate.getHours() * 60 + startDate.getMinutes();
const endMinutes = endDate.getHours() * 60 + endDate.getMinutes();
const dayStartMinutes = dayStartHour * 60;
// Calculate top position (subtract day start to align with time axis)
const top = ((startMinutes - dayStartMinutes) / 60) * hourHeight;
// Calculate height
const durationMinutes = endMinutes - startMinutes;
const height = (durationMinutes / 60) * hourHeight;
console.log('Event positioning calculation:', {
eventTime: `${startDate.getHours()}:${startDate.getMinutes()}`,
startMinutes,
endMinutes,
dayStartMinutes,
dayStartHour,
hourHeight,
top,
height
});
return { top, height };
}
protected formatTime(isoString: string): string {
const date = new Date(isoString);
const hours = date.getHours();
const minutes = date.getMinutes();
const period = hours >= 12 ? 'PM' : 'AM';
const displayHour = hours > 12 ? hours - 12 : (hours === 0 ? 12 : hours);
return `${displayHour}:${minutes.toString().padStart(2, '0')} ${period}`;
}
clearEvents(): void {
const existingEvents = document.querySelectorAll('swp-event');
if (existingEvents.length > 0) {
console.warn(`🗑️ BaseEventRenderer: REMOVING ${existingEvents.length} events from DOM! Stack trace:`, new Error().stack);
}
existingEvents.forEach(event => event.remove());
}
}
/**
* 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');
return Array.from(columns) as HTMLElement[];
}
protected getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[] {
const columnDate = column.dataset.date;
if (!columnDate) return [];
const columnEvents = events.filter(event => {
const eventDate = new Date(event.start);
const eventDateStr = DateUtils.formatDate(eventDate);
return eventDateStr === columnDate;
});
console.log(`DateEventRenderer: Column ${columnDate} has ${columnEvents.length} events`);
return columnEvents;
}
}
/**
* 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');
return Array.from(columns) as HTMLElement[];
}
protected getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[] {
const resourceName = column.dataset.resource;
if (!resourceName) return [];
const columnEvents = events.filter(event => {
return event.resource?.name === resourceName;
});
console.log(`ResourceEventRenderer: Resource ${resourceName} has ${columnEvents.length} events`);
return columnEvents;
}
}