Renames renderer interfaces to use 'I' prefix for clarity Updates type references across related files Improves type consistency in rendering strategies
380 lines
No EOL
14 KiB
TypeScript
380 lines
No EOL
14 KiB
TypeScript
import { EventBus } from '../core/EventBus';
|
|
import { IEventBus, CalendarEvent, RenderContext } from '../types/CalendarTypes';
|
|
import { CoreEvents } from '../constants/CoreEvents';
|
|
import { EventManager } from '../managers/EventManager';
|
|
import { IEventRenderer } 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: IEventRenderer;
|
|
private dateService: DateService;
|
|
|
|
private dragMouseLeaveHeaderListener: ((event: Event) => void) | null = null;
|
|
|
|
constructor(
|
|
eventBus: IEventBus,
|
|
eventManager: EventManager,
|
|
strategy: IEventRenderer,
|
|
dateService: DateService
|
|
) {
|
|
this.eventBus = eventBus;
|
|
this.eventManager = eventManager;
|
|
this.strategy = strategy;
|
|
this.dateService = dateService;
|
|
|
|
this.setupEventListeners();
|
|
}
|
|
|
|
/**
|
|
* Render events in a specific container for a given period
|
|
*/
|
|
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
|
|
});
|
|
}
|
|
|
|
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);
|
|
});
|
|
|
|
|
|
// Handle all drag events and delegate to appropriate renderer
|
|
this.setupDragEventListeners();
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* 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
|
|
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.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;
|
|
|
|
if (dragStartPayload.originalElement.hasAttribute('data-allday')) {
|
|
return;
|
|
}
|
|
|
|
if (dragStartPayload.originalElement && 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.draggedClone.hasAttribute('data-allday')) {
|
|
return;
|
|
}
|
|
if (this.strategy.handleDragMove) {
|
|
this.strategy.handleDragMove(dragEvent);
|
|
}
|
|
});
|
|
}
|
|
|
|
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
|
|
});
|
|
|
|
// 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);
|
|
}
|
|
} |