Updates event and column date parsing to use identifier instead of data attribute Improves date handling consistency across multiple calendar managers and renderers Replaces direct Date casting with dateService.parseISO() for more robust date parsing
358 lines
No EOL
13 KiB
TypeScript
358 lines
No EOL
13 KiB
TypeScript
import { IEventBus, ICalendarEvent, IRenderContext } from '../types/CalendarTypes';
|
|
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, IHeaderReadyEventPayload, IResizeEndEventPayload } from '../types/EventTypes';
|
|
import { DateService } from '../utils/DateService';
|
|
import { IColumnBounds, ColumnDetectionUtils } 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 async renderEvents(context: IRenderContext): Promise<void> {
|
|
// Clear existing events in the specific container first
|
|
this.strategy.clearEvents(context.container);
|
|
|
|
// Get events from EventManager for the period
|
|
const events = await 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, columns } = event.detail;
|
|
|
|
if (!container || !columns || columns.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// Extract dates from columns
|
|
const dates = columns.map((col: any) => col.data as Date);
|
|
|
|
// Calculate startDate and endDate from dates array
|
|
const startDate = dates[0];
|
|
const endDate = dates[dates.length - 1];
|
|
|
|
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<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, originalSourceColumn, finalPosition, target } = (event as CustomEvent<IDragEndEventPayload>).detail;
|
|
const finalColumn = finalPosition.column;
|
|
const finalY = finalPosition.snappedY;
|
|
|
|
let element = draggedClone as SwpEventElement;
|
|
// Only handle day column drops for EventRenderer
|
|
if (target === 'swp-day-column' && finalColumn) {
|
|
|
|
if (originalElement && draggedClone && this.strategy.handleDragEnd) {
|
|
this.strategy.handleDragEnd(originalElement, draggedClone, finalColumn, finalY);
|
|
}
|
|
|
|
await this.eventManager.updateEvent(element.eventId, {
|
|
start: element.start,
|
|
end: element.end,
|
|
allDay: false
|
|
});
|
|
|
|
// Re-render affected columns for stacking/grouping (now with updated data)
|
|
await this.reRenderAffectedColumns(originalSourceColumn, finalColumn);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
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;
|
|
|
|
// 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;
|
|
|
|
await this.eventManager.updateEvent(eventId, {
|
|
start: newStart,
|
|
end: newEnd
|
|
});
|
|
|
|
console.log('📝 EventRendererManager: Updated event after resize', {
|
|
eventId,
|
|
newStart,
|
|
newEnd
|
|
});
|
|
|
|
const dateIdentifier = newStart.toISOString().split('T')[0];
|
|
let columnBounds = ColumnDetectionUtils.getColumnBoundsByIdentifier(dateIdentifier);
|
|
if (columnBounds)
|
|
await this.renderSingleColumn(columnBounds);
|
|
|
|
});
|
|
}
|
|
|
|
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 async reRenderAffectedColumns(originalSourceColumn: IColumnBounds | null, targetColumn: IColumnBounds | null): Promise<void> {
|
|
// Re-render original source column if exists
|
|
if (originalSourceColumn) {
|
|
await this.renderSingleColumn(originalSourceColumn);
|
|
}
|
|
|
|
// Re-render target column if exists and different from source
|
|
if (targetColumn && targetColumn.identifier !== originalSourceColumn?.identifier) {
|
|
await this.renderSingleColumn(targetColumn);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear events in a single column's events layer
|
|
*/
|
|
private clearColumnEvents(eventsLayer: HTMLElement): void {
|
|
const existingEvents = eventsLayer.querySelectorAll('swp-event');
|
|
const existingGroups = eventsLayer.querySelectorAll('swp-event-group');
|
|
|
|
existingEvents.forEach(event => event.remove());
|
|
existingGroups.forEach(group => group.remove());
|
|
}
|
|
|
|
/**
|
|
* Render events for a single column
|
|
*/
|
|
private async renderSingleColumn(column: IColumnBounds): Promise<void> {
|
|
// Get events for just this column's date
|
|
const dateString = column.identifier;
|
|
const columnStart = this.dateService.parseISO(`${dateString}T00:00:00`);
|
|
const columnEnd = this.dateService.parseISO(`${dateString}T23:59:59.999`);
|
|
|
|
// Get events from EventManager for this single date
|
|
const events = await this.eventManager.getEventsForPeriod(columnStart, columnEnd);
|
|
|
|
// Filter to timed events only
|
|
const timedEvents = events.filter(event => !event.allDay);
|
|
|
|
// Get events layer within this specific column
|
|
const eventsLayer = column.element.querySelector('swp-events-layer') as HTMLElement;
|
|
if (!eventsLayer) {
|
|
console.warn('EventRendererManager: Events layer not found in column');
|
|
return;
|
|
}
|
|
|
|
// Clear only this column's events
|
|
this.clearColumnEvents(eventsLayer);
|
|
|
|
// Render events for this column using strategy
|
|
if (this.strategy.renderSingleColumnEvents) {
|
|
this.strategy.renderSingleColumnEvents(column, timedEvents);
|
|
}
|
|
|
|
console.log('🔄 EventRendererManager: Re-rendered single column', {
|
|
columnDate: column.identifier,
|
|
eventsCount: timedEvents.length
|
|
});
|
|
}
|
|
|
|
private clearEvents(container?: HTMLElement): void {
|
|
this.strategy.clearEvents(container);
|
|
}
|
|
|
|
public refresh(container?: HTMLElement): void {
|
|
this.clearEvents(container);
|
|
}
|
|
} |