Calendar/src/renderers/EventRendererManager.ts

430 lines
16 KiB
TypeScript
Raw Normal View History

import { EventBus } from '../core/EventBus';
import { IEventBus, CalendarEvent, RenderContext } from '../types/CalendarTypes';
import { CoreEvents } from '../constants/CoreEvents';
import { calendarConfig } from '../core/CalendarConfig';
2025-08-07 00:15:44 +02:00
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
import { EventManager } from '../managers/EventManager';
import { AllDayManager } from '../managers/AllDayManager';
import { EventRendererStrategy } from './EventRenderer';
import { SwpEventElement } from '../elements/SwpEventElement';
import { AllDayEventRenderer } from './AllDayEventRenderer';
import { DragStartEventPayload, DragMoveEventPayload, DragEndEventPayload, DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload, DragColumnChangeEventPayload, HeaderReadyEventPayload } from '../types/EventTypes';
/**
* EventRenderingService - Render events i DOM med positionering using Strategy Pattern
* Håndterer event positioning og overlap detection
*/
export class EventRenderingService {
private eventBus: IEventBus;
private eventManager: EventManager;
private strategy: EventRendererStrategy;
private allDayEventRenderer: AllDayEventRenderer;
private allDayManager: AllDayManager;
private dragMouseLeaveHeaderListener: ((event: Event) => void) | null = null;
constructor(eventBus: IEventBus, eventManager: EventManager) {
this.eventBus = eventBus;
this.eventManager = eventManager;
// Cache strategy at initialization
const calendarType = calendarConfig.getCalendarMode();
this.strategy = CalendarTypeFactory.getEventRenderer(calendarType);
// Initialize all-day event renderer and manager
this.allDayEventRenderer = new AllDayEventRenderer();
this.allDayManager = new AllDayManager();
this.setupEventListeners();
}
2025-08-09 01:16:04 +02:00
/**
* Render events in a specific container for a given period
2025-08-09 01:16:04 +02:00
*/
public renderEvents(context: RenderContext): void {
// Clear existing events in the specific container first
this.strategy.clearEvents(context.container);
// Get events from EventManager for the period
const events = this.eventManager.getEventsForPeriod(
context.startDate,
context.endDate
);
if (events.length === 0) {
return;
}
// Filter events by type - only render timed events here
const timedEvents = events.filter(event => !event.allDay);
console.log('🎯 EventRenderingService: Event filtering', {
totalEvents: events.length,
timedEvents: timedEvents.length,
allDayEvents: events.length - timedEvents.length
});
// Render timed events using existing strategy
if (timedEvents.length > 0) {
this.strategy.renderEvents(timedEvents, context.container);
}
// Emit EVENTS_RENDERED event for filtering system
this.eventBus.emit(CoreEvents.EVENTS_RENDERED, {
events: events,
container: context.container
});
2025-08-09 01:16:04 +02:00
}
2025-08-02 23:59:52 +02:00
2025-08-09 01:16:04 +02:00
private setupEventListeners(): void {
this.eventBus.on(CoreEvents.GRID_RENDERED, (event: Event) => {
this.handleGridRendered(event as CustomEvent);
});
this.eventBus.on(CoreEvents.VIEW_CHANGED, (event: Event) => {
this.handleViewChanged(event as CustomEvent);
2025-08-09 01:16:04 +02:00
});
// Listen for header ready - when dates are populated with period data
this.eventBus.on('header:ready', (event: Event) => {
const { startDate, endDate } = (event as CustomEvent<HeaderReadyEventPayload>).detail;
console.log('🎯 EventRendererManager: Header ready with period data', {
startDate: startDate.toISOString(),
endDate: endDate.toISOString()
});
// Render all-day events using period from header
this.renderAllDayEventsForPeriod(startDate, endDate);
});
// Handle all drag events and delegate to appropriate renderer
this.setupDragEventListeners();
// Listen for conversion from all-day event to time event
this.eventBus.on('drag:convert-to-time_event', (event: Event) => {
const { draggedElement, mousePosition, column } = (event as CustomEvent).detail;
console.log('🔄 EventRendererManager: Received drag:convert-to-time_event', {
draggedElement: draggedElement?.dataset.eventId,
mousePosition,
column
});
this.handleConvertToTimeEvent(draggedElement, mousePosition, column);
});
}
2025-08-09 01:16:04 +02:00
/**
* Handle GRID_RENDERED event - render events in the current grid
*/
private handleGridRendered(event: CustomEvent): void {
const { container, startDate, endDate, currentDate, isNavigation } = event.detail;
if (!container) {
return;
}
let periodStart: Date;
let periodEnd: Date;
if (startDate && endDate) {
// Direct date format - use as provided
periodStart = startDate;
periodEnd = endDate;
} else if (currentDate) {
return;
} else {
return;
}
this.renderEvents({
container: container,
startDate: periodStart,
endDate: periodEnd
});
}
/**
* 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) {
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
2025-08-09 01:16:04 +02:00
this.clearEvents();
// New rendering will be triggered by subsequent GRID_RENDERED event
}
/**
* Setup all drag event listeners - moved from EventRenderer for better separation of concerns
*/
private setupDragEventListeners(): void {
// Handle drag start
this.eventBus.on('drag:start', (event: Event) => {
const dragStartPayload = (event as CustomEvent<DragStartEventPayload>).detail;
// Use the draggedElement directly - no need for DOM query
if (dragStartPayload.draggedElement && this.strategy.handleDragStart && dragStartPayload.columnBounds) {
this.strategy.handleDragStart(dragStartPayload);
}
});
// Handle drag move
this.eventBus.on('drag:move', (event: Event) => {
let dragEvent = (event as CustomEvent<DragMoveEventPayload>).detail;
// Filter: Only handle events WITHOUT data-allday attribute (normal timed events)
if (dragEvent.draggedElement.hasAttribute('data-allday')) {
return; // This is an all-day event, let AllDayManager handle it
}
if (this.strategy.handleDragMove) {
this.strategy.handleDragMove(dragEvent);
}
});
// Handle drag auto-scroll
this.eventBus.on('drag:auto-scroll', (event: Event) => {
const { draggedElement, snappedY } = (event as CustomEvent).detail;
if (this.strategy.handleDragAutoScroll) {
const eventId = draggedElement.dataset.eventId || '';
this.strategy.handleDragAutoScroll(eventId, snappedY);
}
});
// Handle drag end events and delegate to appropriate renderer
this.eventBus.on('drag:end', (event: Event) => {
const { draggedElement, finalPosition, target } = (event as CustomEvent<DragEndEventPayload>).detail;
const finalColumn = finalPosition.column;
const finalY = finalPosition.snappedY;
const eventId = draggedElement.dataset.eventId || '';
// Only handle day column drops for EventRenderer
if (target === 'swp-day-column' && finalColumn) {
// Find dragged clone - use draggedElement as original
const draggedClone = document.querySelector(`swp-day-column swp-event[data-event-id="clone-${eventId}"]`) as HTMLElement;
if (draggedElement && draggedClone && this.strategy.handleDragEnd) {
this.strategy.handleDragEnd(eventId, draggedElement, draggedClone, finalColumn, finalY);
}
}
// Clean up any remaining day event clones
const dayEventClone = document.querySelector(`swp-day-column swp-event[data-event-id="clone-${eventId}"]`);
if (dayEventClone) {
dayEventClone.remove();
}
});
// Handle click (when drag threshold not reached)
this.eventBus.on('event:click', (event: Event) => {
const { draggedElement } = (event as CustomEvent).detail;
// Use draggedElement directly - no need for DOM query
if (draggedElement && this.strategy.handleEventClick) {
const eventId = draggedElement.dataset.eventId || '';
this.strategy.handleEventClick(eventId, draggedElement); //TODO: fix this redundant parameters
}
});
// Handle column change
this.eventBus.on('drag:column-change', (event: Event) => {
let columnChangeEvent = (event as CustomEvent<DragColumnChangeEventPayload>).detail;
// Filter: Only handle events where clone is NOT an all-day event (normal timed events)
if (columnChangeEvent.draggedClone && columnChangeEvent.draggedClone.hasAttribute('data-allday')) {
return;
}
if (this.strategy.handleColumnChange) {
const eventId = columnChangeEvent.draggedElement.dataset.eventId || '';
this.strategy.handleColumnChange(columnChangeEvent);
}
});
this.dragMouseLeaveHeaderListener = (event: Event) => {
const { targetDate, mousePosition, originalElement, cloneElement } = (event as CustomEvent<DragMouseLeaveHeaderEventPayload>).detail;
if (cloneElement)
cloneElement.style.display = '';
console.log('🚪 EventRendererManager: Received drag:mouseleave-header', {
targetDate,
originalElement: originalElement,
cloneElement: cloneElement
});
};
this.eventBus.on('drag:mouseleave-header', this.dragMouseLeaveHeaderListener);
// Handle navigation period change
this.eventBus.on(CoreEvents.NAVIGATION_COMPLETED, () => {
// Delegate to strategy if it handles navigation
if (this.strategy.handleNavigationCompleted) {
this.strategy.handleNavigationCompleted();
}
});
}
/**
* Handle conversion from all-day event to time event
*/
private handleConvertToTimeEvent(draggedElement: HTMLElement, mousePosition: { x: number; y: number }, column: string): void {
// Use the provided draggedElement directly
const allDayClone = draggedElement;
const draggedEventId = draggedElement?.dataset.eventId?.replace('clone-', '') || '';
// Use SwpEventElement factory to create day event from all-day event
const dayEventElement = SwpEventElement.fromAllDayElement(allDayClone as HTMLElement);
const dayElement = dayEventElement.getElement();
// Remove the all-day clone - it's no longer needed since we're converting to day event
allDayClone.remove();
// Set clone ID
dayElement.dataset.eventId = `clone-${draggedEventId}`;
// Find target column
const columnElement = document.querySelector(`swp-day-column[data-date="${column}"]`);
if (!columnElement) {
console.warn('EventRendererManager: Target column not found', { column });
return;
}
// Find events layer in the column
const eventsLayer = columnElement.querySelector('swp-events-layer');
if (!eventsLayer) {
console.warn('EventRendererManager: Events layer not found in column');
return;
}
// Add to events layer
eventsLayer.appendChild(dayElement);
// Position based on mouse Y coordinate
const columnRect = columnElement.getBoundingClientRect();
const relativeY = Math.max(0, mousePosition.y - columnRect.top);
dayElement.style.top = `${relativeY}px`;
// Set drag styling
dayElement.style.zIndex = '1000';
dayElement.style.cursor = 'grabbing';
dayElement.style.opacity = '';
dayElement.style.transform = '';
console.log('✅ EventRendererManager: Converted all-day event to time event', {
draggedEventId,
column,
mousePosition,
relativeY
});
}
/**
* Render all-day events for specific period using AllDayEventRenderer
*/
private renderAllDayEventsForPeriod(startDate: Date, endDate: Date): void {
// Get events from EventManager for the period
const events = this.eventManager.getEventsForPeriod(startDate, endDate);
// Filter for all-day events
const allDayEvents = events.filter(event => event.allDay);
console.log('🏗️ EventRenderingService: Rendering all-day events', {
period: {
start: startDate.toISOString(),
end: endDate.toISOString()
},
count: allDayEvents.length,
events: allDayEvents.map(e => ({ id: e.id, title: e.title }))
});
// Clear existing all-day events first
this.clearAllDayEvents();
// Get actual visible dates from DOM headers instead of generating them
const weekDates = this.getVisibleDatesFromDOM();
console.log('🔍 EventRenderingService: Using visible dates from DOM', {
weekDates,
count: weekDates.length
});
// Pass current events to AllDayManager for state tracking
this.allDayManager.setCurrentEvents(allDayEvents, weekDates);
const layouts = this.allDayManager.calculateAllDayEventsLayout(allDayEvents, weekDates);
// Render each all-day event with pre-calculated layout
layouts.forEach(layout => {
this.allDayEventRenderer.renderAllDayEventWithLayout(layout.calenderEvent, layout);
});
// Check and adjust all-day container height after rendering
this.eventBus.emit('allday:checkHeight');
}
/**
* Clear only all-day events
*/
private clearAllDayEvents(): void {
const allDayContainer = document.querySelector('swp-allday-container');
if (allDayContainer) {
allDayContainer.querySelectorAll('swp-event').forEach(event => event.remove());
}
}
private clearEvents(container?: HTMLElement): void {
this.strategy.clearEvents(container);
// Also clear all-day events
this.clearAllDayEvents();
}
public refresh(container?: HTMLElement): void {
// Clear events in specific container or globally
this.clearEvents(container);
}
/**
* Get visible dates from DOM headers - only the dates that are actually displayed
*/
private getVisibleDatesFromDOM(): string[] {
const dayHeaders = document.querySelectorAll('swp-calendar-header swp-day-header');
const weekDates: string[] = [];
dayHeaders.forEach(header => {
const dateAttr = header.getAttribute('data-date');
if (dateAttr) {
weekDates.push(dateAttr);
}
});
return weekDates;
}
public destroy(): void {
this.clearEvents();
}
}