Calendar/src/renderers/EventRendererManager.ts

395 lines
14 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 { EventRendererStrategy } from './EventRenderer';
import { SwpEventElement } from '../elements/SwpEventElement';
import { DragStartEventPayload, DragMoveEventPayload, DragEndEventPayload, DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload, DragMouseEnterColumnEventPayload, DragColumnChangeEventPayload, HeaderReadyEventPayload, ResizeEndEventPayload } from '../types/EventTypes';
import { DateService } from '../utils/DateService';
import { ColumnBounds } from '../utils/ColumnDetectionUtils';
/**
* 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 dateService: DateService;
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 DateService
const timezone = calendarConfig.getTimezone?.();
this.dateService = new DateService(timezone);
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
});
// Handle all drag events and delegate to appropriate renderer
this.setupDragEventListeners();
}
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 } = event.detail;
if (!container || !startDate || !endDate) {
return;
}
this.renderEvents({
container,
startDate,
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 {
this.setupDragStartListener();
this.setupDragMoveListener();
this.setupDragAutoScrollListener();
this.setupDragEndListener();
this.setupDragColumnChangeListener();
this.setupDragMouseLeaveHeaderListener();
this.setupDragMouseEnterColumnListener();
this.setupResizeEndListener();
this.setupNavigationCompletedListener();
}
private setupDragStartListener(): void {
this.eventBus.on('drag:start', (event: Event) => {
const dragStartPayload = (event as CustomEvent<DragStartEventPayload>).detail;
2025-10-02 23:11:26 +02:00
if (dragStartPayload.draggedElement.hasAttribute('data-allday')) {
return;
2025-10-02 23:11:26 +02:00
}
if (dragStartPayload.draggedElement && this.strategy.handleDragStart && dragStartPayload.columnBounds) {
this.strategy.handleDragStart(dragStartPayload);
}
});
}
private setupDragMoveListener(): void {
this.eventBus.on('drag:move', (event: Event) => {
let dragEvent = (event as CustomEvent<DragMoveEventPayload>).detail;
if (dragEvent.draggedElement.hasAttribute('data-allday')) {
return;
}
if (this.strategy.handleDragMove) {
this.strategy.handleDragMove(dragEvent);
}
});
}
private setupDragAutoScrollListener(): void {
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);
}
});
}
private setupDragEndListener(): void {
this.eventBus.on('drag:end', (event: Event) => {
const { originalElement: draggedElement, sourceColumn, 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);
}
// Update event data in EventManager with new position from clone
if (draggedClone) {
const swpEvent = draggedClone as SwpEventElement;
const newStart = swpEvent.start;
const newEnd = swpEvent.end;
this.eventManager.updateEvent(eventId, {
start: newStart,
end: newEnd
});
console.log('📝 EventRendererManager: Updated event in EventManager', {
eventId,
newStart,
newEnd
});
}
// Re-render affected columns for stacking/grouping (now with updated data)
this.reRenderAffectedColumns(sourceColumn, finalColumn);
}
// 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();
}
});
}
private setupDragColumnChangeListener(): void {
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) {
this.strategy.handleColumnChange(columnChangeEvent);
}
});
}
private setupDragMouseLeaveHeaderListener(): void {
this.dragMouseLeaveHeaderListener = (event: Event) => {
const { targetDate, mousePosition, originalElement, draggedClone: 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);
}
private setupDragMouseEnterColumnListener(): void {
this.eventBus.on('drag:mouseenter-column', (event: Event) => {
const payload = (event as CustomEvent<DragMouseEnterColumnEventPayload>).detail;
// Only handle if clone is an all-day event
if (!payload.draggedClone.hasAttribute('data-allday')) {
return;
}
console.log('🎯 EventRendererManager: Received drag:mouseenter-column', {
targetColumn: payload.targetColumn,
snappedY: payload.snappedY,
calendarEvent: payload.calendarEvent
});
// Remove the old all-day clone from header
payload.draggedClone.remove();
// Delegate to strategy for conversion
if (this.strategy.handleConvertAllDayToTimed) {
this.strategy.handleConvertAllDayToTimed(payload);
}
});
}
private setupResizeEndListener(): void {
this.eventBus.on('resize:end', (event: Event) => {
const { eventId, element } = (event as CustomEvent<ResizeEndEventPayload>).detail;
// Update event data in EventManager with new end time from resized element
const swpEvent = element as SwpEventElement;
const newStart = swpEvent.start;
const newEnd = swpEvent.end;
this.eventManager.updateEvent(eventId, {
start: newStart,
end: newEnd
});
console.log('📝 EventRendererManager: Updated event after resize', {
eventId,
newStart,
newEnd
});
// Find the column for this event
const columnElement = element.closest('swp-day-column') as HTMLElement;
if (columnElement) {
const columnDate = columnElement.dataset.date;
if (columnDate) {
// Re-render the column to recalculate stacking/grouping
this.renderSingleColumn(columnDate);
}
}
});
}
private setupNavigationCompletedListener(): void {
this.eventBus.on(CoreEvents.NAVIGATION_COMPLETED, () => {
// Delegate to strategy if it handles navigation
if (this.strategy.handleNavigationCompleted) {
this.strategy.handleNavigationCompleted();
}
});
}
/**
* Re-render affected columns after drag to recalculate stacking/grouping
*/
private reRenderAffectedColumns(sourceColumn: ColumnBounds | null, targetColumn: ColumnBounds | null): void {
const columnsToRender = new Set<string>();
// Add source column if exists
if (sourceColumn) {
columnsToRender.add(sourceColumn.date);
}
// Add target column if exists and different from source
if (targetColumn && targetColumn.date !== sourceColumn?.date) {
columnsToRender.add(targetColumn.date);
}
// Re-render each affected column
columnsToRender.forEach(columnDate => {
this.renderSingleColumn(columnDate);
});
}
/**
* Render events for a single column by re-rendering entire container
*/
private renderSingleColumn(columnDate: string): void {
// Find the column element
const columnElement = document.querySelector(`swp-day-column[data-date="${columnDate}"]`) as HTMLElement;
if (!columnElement) {
console.warn('EventRendererManager: Column not found', { columnDate });
return;
}
// Find the parent container (swp-day-columns)
const container = columnElement.closest('swp-day-columns') as HTMLElement;
if (!container) {
console.warn('EventRendererManager: Container not found');
return;
}
// Get all columns in container to determine date range
const allColumns = Array.from(container.querySelectorAll<HTMLElement>('swp-day-column'));
if (allColumns.length === 0) return;
// Get date range from first and last column
const firstColumnDate = allColumns[0].dataset.date;
const lastColumnDate = allColumns[allColumns.length - 1].dataset.date;
if (!firstColumnDate || !lastColumnDate) return;
const startDate = this.dateService.parseISO(`${firstColumnDate}T00:00:00`);
const endDate = this.dateService.parseISO(`${lastColumnDate}T23:59:59.999`);
// Re-render entire container (this will recalculate stacking for all columns)
this.renderEvents({
container,
startDate,
endDate
});
console.log('🔄 EventRendererManager: Re-rendered container for column', {
columnDate,
startDate: firstColumnDate,
endDate: lastColumnDate
});
}
private clearEvents(container?: HTMLElement): void {
this.strategy.clearEvents(container);
}
public refresh(container?: HTMLElement): void {
this.clearEvents(container);
}
}