Improves event rendering by integrating event filtering directly into column data sources Key changes: - Moves event filtering responsibility to IColumnDataSource - Simplifies event rendering pipeline by pre-filtering events per column - Supports both date and resource-based calendar modes - Enhances drag and drop event update mechanism Optimizes calendar rendering performance and flexibility
269 lines
No EOL
10 KiB
TypeScript
269 lines
No EOL
10 KiB
TypeScript
import { IEventBus } from '../types/CalendarTypes';
|
|
import { IColumnInfo, IColumnDataSource } from '../types/ColumnDataSource';
|
|
import { CoreEvents } from '../constants/CoreEvents';
|
|
import { EventManager } from '../managers/EventManager';
|
|
import { IEventRenderer } from './EventRenderer';
|
|
import { SwpEventElement } from '../elements/SwpEventElement';
|
|
import { IDragStartEventPayload, IDragMoveEventPayload, IDragEndEventPayload, IDragMouseEnterHeaderEventPayload, IDragMouseLeaveHeaderEventPayload, IDragMouseEnterColumnEventPayload, IDragColumnChangeEventPayload, IResizeEndEventPayload } from '../types/EventTypes';
|
|
import { DateService } from '../utils/DateService';
|
|
|
|
/**
|
|
* 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 dataSource: IColumnDataSource;
|
|
private dateService: DateService;
|
|
|
|
private dragMouseLeaveHeaderListener: ((event: Event) => void) | null = null;
|
|
|
|
constructor(
|
|
eventBus: IEventBus,
|
|
eventManager: EventManager,
|
|
strategy: IEventRenderer,
|
|
dataSource: IColumnDataSource,
|
|
dateService: DateService
|
|
) {
|
|
this.eventBus = eventBus;
|
|
this.eventManager = eventManager;
|
|
this.strategy = strategy;
|
|
this.dataSource = dataSource;
|
|
this.dateService = dateService;
|
|
|
|
this.setupEventListeners();
|
|
}
|
|
|
|
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
|
|
* Events are now pre-filtered per column by IColumnDataSource
|
|
*/
|
|
private handleGridRendered(event: CustomEvent): void {
|
|
const { container, columns } = event.detail;
|
|
|
|
if (!container || !columns || columns.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// Render events directly from columns (pre-filtered by IColumnDataSource)
|
|
this.renderEventsFromColumns(container, columns);
|
|
}
|
|
|
|
/**
|
|
* Render events from pre-filtered columns
|
|
* Each column already contains its events (filtered by IColumnDataSource)
|
|
*/
|
|
private renderEventsFromColumns(container: HTMLElement, columns: IColumnInfo[]): void {
|
|
this.strategy.clearEvents(container);
|
|
this.strategy.renderEvents(columns, container);
|
|
|
|
// Emit EVENTS_RENDERED for filtering system
|
|
const allEvents = columns.flatMap(col => col.events);
|
|
this.eventBus.emit(CoreEvents.EVENTS_RENDERED, {
|
|
events: allEvents,
|
|
container: container
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* 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<IDragStartEventPayload>).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<IDragMoveEventPayload>).detail;
|
|
|
|
if (dragEvent.draggedClone.hasAttribute('data-allday')) {
|
|
return;
|
|
}
|
|
if (this.strategy.handleDragMove) {
|
|
this.strategy.handleDragMove(dragEvent);
|
|
}
|
|
});
|
|
}
|
|
|
|
private setupDragEndListener(): void {
|
|
this.eventBus.on('drag:end', async (event: Event) => {
|
|
const { originalElement, draggedClone, finalPosition, target } = (event as CustomEvent<IDragEndEventPayload>).detail;
|
|
const finalColumn = finalPosition.column;
|
|
const finalY = finalPosition.snappedY;
|
|
|
|
// Only handle day column drops
|
|
if (target === 'swp-day-column' && finalColumn) {
|
|
const element = draggedClone as SwpEventElement;
|
|
|
|
if (originalElement && draggedClone && this.strategy.handleDragEnd) {
|
|
this.strategy.handleDragEnd(originalElement, draggedClone, finalColumn, finalY);
|
|
}
|
|
|
|
// Build update payload based on mode
|
|
const updatePayload: { start: Date; end: Date; allDay: boolean; resourceId?: string } = {
|
|
start: element.start,
|
|
end: element.end,
|
|
allDay: false
|
|
};
|
|
|
|
if (this.dataSource.isResource()) {
|
|
// Resource mode: update resourceId, keep existing date
|
|
updatePayload.resourceId = finalColumn.identifier;
|
|
} else {
|
|
// Date mode: update date from column, keep existing time
|
|
const newDate = this.dateService.parseISO(finalColumn.identifier);
|
|
const startTimeMinutes = this.dateService.getMinutesSinceMidnight(element.start);
|
|
const endTimeMinutes = this.dateService.getMinutesSinceMidnight(element.end);
|
|
updatePayload.start = this.dateService.createDateAtTime(newDate, startTimeMinutes);
|
|
updatePayload.end = this.dateService.createDateAtTime(newDate, endTimeMinutes);
|
|
}
|
|
|
|
await this.eventManager.updateEvent(element.eventId, updatePayload);
|
|
|
|
// Trigger full refresh to re-render with updated data
|
|
this.eventBus.emit(CoreEvents.REFRESH_REQUESTED, {});
|
|
}
|
|
});
|
|
}
|
|
|
|
private setupDragColumnChangeListener(): void {
|
|
this.eventBus.on('drag:column-change', (event: Event) => {
|
|
let columnChangeEvent = (event as CustomEvent<IDragColumnChangeEventPayload>).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 { targetColumn, mousePosition, originalElement, draggedClone: cloneElement } = (event as CustomEvent<IDragMouseLeaveHeaderEventPayload>).detail;
|
|
|
|
if (cloneElement)
|
|
cloneElement.style.display = '';
|
|
|
|
console.log('🚪 EventRendererManager: Received drag:mouseleave-header', {
|
|
targetColumn: targetColumn?.identifier,
|
|
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<IDragMouseEnterColumnEventPayload>).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', async (event: Event) => {
|
|
const { eventId, element } = (event as CustomEvent<IResizeEndEventPayload>).detail;
|
|
|
|
const swpEvent = element as SwpEventElement;
|
|
await this.eventManager.updateEvent(eventId, {
|
|
start: swpEvent.start,
|
|
end: swpEvent.end
|
|
});
|
|
|
|
// Trigger full refresh to re-render with updated data
|
|
this.eventBus.emit(CoreEvents.REFRESH_REQUESTED, {});
|
|
});
|
|
}
|
|
|
|
private setupNavigationCompletedListener(): void {
|
|
this.eventBus.on(CoreEvents.NAVIGATION_COMPLETED, () => {
|
|
// Delegate to strategy if it handles navigation
|
|
if (this.strategy.handleNavigationCompleted) {
|
|
this.strategy.handleNavigationCompleted();
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
private clearEvents(container?: HTMLElement): void {
|
|
this.strategy.clearEvents(container);
|
|
}
|
|
|
|
public refresh(container?: HTMLElement): void {
|
|
this.clearEvents(container);
|
|
}
|
|
} |