Calendar/src/renderers/EventRenderer.ts

314 lines
10 KiB
TypeScript
Raw Normal View History

2025-08-07 00:15:44 +02:00
// Event rendering strategy interface and implementations
import { CalendarEvent } from '../types/CalendarTypes';
import { calendarConfig } from '../core/CalendarConfig';
import { SwpEventElement } from '../elements/SwpEventElement';
import { PositionUtils } from '../utils/PositionUtils';
import { ColumnBounds } from '../utils/ColumnDetectionUtils';
import { DragColumnChangeEventPayload, DragMoveEventPayload, DragStartEventPayload } from '../types/EventTypes';
import { DateService } from '../utils/DateService';
2025-08-07 00:15:44 +02:00
/**
* Interface for event rendering strategies
*/
export interface EventRendererStrategy {
renderEvents(events: CalendarEvent[], container: HTMLElement): void;
clearEvents(container?: HTMLElement): void;
handleDragStart?(payload: DragStartEventPayload): void;
handleDragMove?(payload: DragMoveEventPayload): void;
handleDragAutoScroll?(eventId: string, snappedY: number): void;
handleDragEnd?(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: ColumnBounds, finalY: number): void;
handleEventClick?(eventId: string, originalElement: HTMLElement): void;
handleColumnChange?(payload: DragColumnChangeEventPayload): void;
handleNavigationCompleted?(): void;
2025-08-07 00:15:44 +02:00
}
/**
2025-10-02 23:11:26 +02:00
* Date-based event renderer
2025-08-07 00:15:44 +02:00
*/
2025-10-02 23:11:26 +02:00
export class DateEventRenderer implements EventRendererStrategy {
private dateService: DateService;
2025-10-04 14:50:25 +02:00
private draggedClone: HTMLElement | null = null;
private originalEvent: HTMLElement | null = null;
constructor() {
const timezone = calendarConfig.getTimezone?.();
this.dateService = new DateService(timezone);
}
2025-10-02 23:11:26 +02:00
private applyDragStyling(element: HTMLElement): void {
element.classList.add('dragging');
element.style.removeProperty("margin-left");
}
/**
* Update clone timestamp based on new position
*/
private updateCloneTimestamp(payload: DragMoveEventPayload): void {
if (payload.draggedClone.dataset.allDay === "true" || !payload.columnBounds) return;
const gridSettings = calendarConfig.getGridSettings();
const { hourHeight, dayStartHour, snapInterval } = gridSettings;
if (!payload.draggedClone.dataset.originalDuration) {
throw new DOMException("missing clone.dataset.originalDuration");
}
// Calculate snapped start minutes
const minutesFromGridStart = (payload.snappedY / hourHeight) * 60;
const snappedStartMinutes = this.calculateSnappedMinutes(
minutesFromGridStart, dayStartHour, snapInterval
);
// Calculate end minutes
const originalDuration = parseInt(payload.draggedClone.dataset.originalDuration);
const endTotalMinutes = snappedStartMinutes + originalDuration;
// Update UI
this.updateTimeDisplay(payload.draggedClone, snappedStartMinutes, endTotalMinutes);
// Update data attributes
this.updateDateTimeAttributes(
payload.draggedClone,
new Date(payload.columnBounds.date),
snappedStartMinutes,
endTotalMinutes
);
}
/**
* Calculate snapped minutes from grid start
*/
private calculateSnappedMinutes(minutesFromGridStart: number, dayStartHour: number, snapInterval: number): number {
const actualStartMinutes = (dayStartHour * 60) + minutesFromGridStart;
return Math.round(actualStartMinutes / snapInterval) * snapInterval;
}
/**
* Update time display in the UI
*/
private updateTimeDisplay(element: HTMLElement, startMinutes: number, endMinutes: number): void {
const timeElement = element.querySelector('swp-event-time');
if (!timeElement) return;
const startTime = this.formatTimeFromMinutes(startMinutes);
const endTime = this.formatTimeFromMinutes(endMinutes);
timeElement.textContent = `${startTime} - ${endTime}`;
}
2025-10-02 23:11:26 +02:00
/**
* Update data-start and data-end attributes with ISO timestamps
*/
private updateDateTimeAttributes(element: HTMLElement, columnDate: Date, startMinutes: number, endMinutes: number): void {
const startDate = this.dateService.createDateAtTime(columnDate, startMinutes);
let endDate = this.dateService.createDateAtTime(columnDate, endMinutes);
// Handle cross-midnight events
if (endMinutes >= 1440) {
const extraDays = Math.floor(endMinutes / 1440);
endDate = this.dateService.addDays(endDate, extraDays);
}
// Convert to UTC before storing as ISO string
element.dataset.start = this.dateService.toUTC(startDate);
element.dataset.end = this.dateService.toUTC(endDate);
}
/**
* Format minutes since midnight to time string
*/
private formatTimeFromMinutes(totalMinutes: number): string {
return this.dateService.minutesToTime(totalMinutes);
}
/**
* Handle drag start event
*/
public handleDragStart(payload: DragStartEventPayload): void {
this.originalEvent = payload.draggedElement;;
// Use the clone from the payload instead of creating a new one
this.draggedClone = payload.draggedClone;
if (this.draggedClone) {
2025-10-02 23:11:26 +02:00
// Apply drag styling
this.applyDragStyling(this.draggedClone);
// Add to current column's events layer (not directly to column)
const eventsLayer = payload.columnBounds?.element.querySelector('swp-events-layer');
if (eventsLayer) {
eventsLayer.appendChild(this.draggedClone);
}
}
// Make original semi-transparent
this.originalEvent.style.opacity = '0.3';
this.originalEvent.style.userSelect = 'none';
}
/**
* Handle drag move event
*/
public handleDragMove(payload: DragMoveEventPayload): void {
if (!this.draggedClone) return;
// Update position - snappedY is already the event top position
// Add +1px to match the initial positioning offset from SwpEventElement
this.draggedClone.style.top = (payload.snappedY + 1) + 'px';
// Update timestamp display
this.updateCloneTimestamp(payload);
}
/**
* Handle drag auto-scroll event
*/
public handleDragAutoScroll(eventId: string, snappedY: number): void {
if (!this.draggedClone) return;
// Update position directly using the calculated snapped position
this.draggedClone.style.top = snappedY + 'px';
// Update timestamp display
//this.updateCloneTimestamp(this.draggedClone, snappedY); //TODO: Commented as, we need to move all this scroll logic til scroll manager away from eventrenderer
}
/**
* Handle column change during drag
*/
public handleColumnChange(dragColumnChangeEvent: DragColumnChangeEventPayload): void {
if (!this.draggedClone) return;
const eventsLayer = dragColumnChangeEvent.newColumn.element.querySelector('swp-events-layer');
if (eventsLayer && this.draggedClone.parentElement !== eventsLayer) {
eventsLayer.appendChild(this.draggedClone);
// Recalculate timestamps with new column date
const currentTop = parseFloat(this.draggedClone.style.top) || 0;
const mockPayload: DragMoveEventPayload = {
draggedElement: dragColumnChangeEvent.originalElement,
draggedClone: this.draggedClone,
mousePosition: dragColumnChangeEvent.mousePosition,
mouseOffset: { x: 0, y: 0 },
columnBounds: dragColumnChangeEvent.newColumn,
snappedY: currentTop
};
this.updateCloneTimestamp(mockPayload);
}
}
/**
* Handle drag end event
*/
public handleDragEnd(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: ColumnBounds, finalY: number): void {
if (!draggedClone || !originalElement) {
console.warn('Missing draggedClone or originalElement');
return;
}
// Fade out original
2025-10-02 23:11:26 +02:00
this.fadeOutAndRemove(originalElement);
// Remove clone prefix and normalize clone to be a regular event
const cloneId = draggedClone.dataset.eventId;
if (cloneId && cloneId.startsWith('clone-')) {
draggedClone.dataset.eventId = cloneId.replace('clone-', '');
}
// Fully normalize the clone to be a regular event
draggedClone.classList.remove('dragging');
2025-10-04 14:50:25 +02:00
// Clean up instance state
this.draggedClone = null;
this.originalEvent = null;
}
/**
* Handle navigation completed event
*/
public handleNavigationCompleted(): void {
// Default implementation - can be overridden by subclasses
}
/**
* Fade out and remove element
*/
private fadeOutAndRemove(element: HTMLElement): void {
element.style.transition = 'opacity 0.3s ease-out';
element.style.opacity = '0';
setTimeout(() => {
element.remove();
}, 300);
}
renderEvents(events: CalendarEvent[], container: HTMLElement): void {
// Filter out all-day events - they should be handled by AllDayEventRenderer
const timedEvents = events.filter(event => !event.allDay);
// Find columns in the specific container for regular events
const columns = this.getColumns(container);
2025-08-13 23:05:58 +02:00
columns.forEach(column => {
const columnEvents = this.getEventsForColumn(column, timedEvents);
2025-08-13 23:05:58 +02:00
const eventsLayer = column.querySelector('swp-events-layer');
2025-10-04 14:50:25 +02:00
2025-08-13 23:05:58 +02:00
if (eventsLayer) {
2025-10-04 14:50:25 +02:00
// Simply render each event - no overlap handling
columnEvents.forEach(event => {
const element = this.renderEvent(event);
eventsLayer.appendChild(element);
});
2025-08-07 00:15:44 +02:00
}
});
}
2025-10-02 23:11:26 +02:00
private renderEvent(event: CalendarEvent): HTMLElement {
const swpEvent = SwpEventElement.fromCalendarEvent(event);
2025-10-04 14:50:25 +02:00
return swpEvent.getElement();
2025-08-07 00:15:44 +02:00
}
protected calculateEventPosition(event: CalendarEvent): { top: number; height: number } {
// Delegate to PositionUtils for centralized position calculation
return PositionUtils.calculateEventPosition(event.start, event.end);
2025-08-07 00:15:44 +02:00
}
clearEvents(container?: HTMLElement): void {
2025-10-04 14:50:25 +02:00
const selector = 'swp-event';
const existingEvents = container
? container.querySelectorAll(selector)
: document.querySelectorAll(selector);
2025-08-07 00:15:44 +02:00
existingEvents.forEach(event => event.remove());
}
2025-09-09 14:35:21 +02:00
protected getColumns(container: HTMLElement): HTMLElement[] {
const columns = container.querySelectorAll('swp-day-column');
2025-08-13 23:05:58 +02:00
return Array.from(columns) as HTMLElement[];
}
protected getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[] {
const columnDate = column.dataset.date;
if (!columnDate) {
return [];
}
2025-08-13 23:05:58 +02:00
const columnEvents = events.filter(event => {
const eventDateStr = this.dateService.formatISODate(event.start);
const matches = eventDateStr === columnDate;
return matches;
2025-08-13 23:05:58 +02:00
});
return columnEvents;
}
2025-08-07 00:15:44 +02:00
}