Introduces type-safe EventId with centralized normalization logic for clone and standard event IDs Refactors event ID management across multiple components to use consistent ID transformation methods Improves type safety and reduces potential ID-related bugs in drag-and-drop and event rendering
386 lines
14 KiB
TypeScript
386 lines
14 KiB
TypeScript
// Event rendering strategy interface and implementations
|
|
|
|
import { ICalendarEvent } from '../types/CalendarTypes';
|
|
import { IColumnInfo } from '../types/ColumnDataSource';
|
|
import { Configuration } from '../configurations/CalendarConfig';
|
|
import { SwpEventElement } from '../elements/SwpEventElement';
|
|
import { PositionUtils } from '../utils/PositionUtils';
|
|
import { IColumnBounds } from '../utils/ColumnDetectionUtils';
|
|
import { IDragColumnChangeEventPayload, IDragMoveEventPayload, IDragStartEventPayload, IDragMouseEnterColumnEventPayload } from '../types/EventTypes';
|
|
import { DateService } from '../utils/DateService';
|
|
import { EventStackManager } from '../managers/EventStackManager';
|
|
import { EventLayoutCoordinator, IGridGroupLayout, IStackedEventLayout } from '../managers/EventLayoutCoordinator';
|
|
import { EventId } from '../types/EventId';
|
|
|
|
/**
|
|
* Interface for event rendering strategies
|
|
*
|
|
* Note: renderEvents now receives columns with pre-filtered events,
|
|
* not a flat array of events. Each column contains its own events.
|
|
*/
|
|
export interface IEventRenderer {
|
|
renderEvents(columns: IColumnInfo[], container: HTMLElement): void;
|
|
clearEvents(container?: HTMLElement): void;
|
|
renderSingleColumnEvents?(column: IColumnBounds, events: ICalendarEvent[]): void;
|
|
handleDragStart?(payload: IDragStartEventPayload): void;
|
|
handleDragMove?(payload: IDragMoveEventPayload): void;
|
|
handleDragAutoScroll?(eventId: string, snappedY: number): void;
|
|
handleDragEnd?(originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: IColumnBounds, finalY: number): void;
|
|
handleEventClick?(eventId: string, originalElement: HTMLElement): void;
|
|
handleColumnChange?(payload: IDragColumnChangeEventPayload): void;
|
|
handleNavigationCompleted?(): void;
|
|
handleConvertAllDayToTimed?(payload: IDragMouseEnterColumnEventPayload): void;
|
|
}
|
|
|
|
/**
|
|
* Date-based event renderer
|
|
*/
|
|
export class DateEventRenderer implements IEventRenderer {
|
|
|
|
private dateService: DateService;
|
|
private stackManager: EventStackManager;
|
|
private layoutCoordinator: EventLayoutCoordinator;
|
|
private config: Configuration;
|
|
private positionUtils: PositionUtils;
|
|
private draggedClone: HTMLElement | null = null;
|
|
private originalEvent: HTMLElement | null = null;
|
|
|
|
constructor(
|
|
dateService: DateService,
|
|
stackManager: EventStackManager,
|
|
layoutCoordinator: EventLayoutCoordinator,
|
|
config: Configuration,
|
|
positionUtils: PositionUtils
|
|
) {
|
|
this.dateService = dateService;
|
|
this.stackManager = stackManager;
|
|
this.layoutCoordinator = layoutCoordinator;
|
|
this.config = config;
|
|
this.positionUtils = positionUtils;
|
|
}
|
|
|
|
private applyDragStyling(element: HTMLElement): void {
|
|
element.classList.add('dragging');
|
|
element.style.removeProperty("margin-left");
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Handle drag start event
|
|
*/
|
|
public handleDragStart(payload: IDragStartEventPayload): void {
|
|
|
|
this.originalEvent = payload.originalElement;;
|
|
|
|
// Use the clone from the payload instead of creating a new one
|
|
this.draggedClone = payload.draggedClone;
|
|
|
|
if (this.draggedClone && payload.columnBounds) {
|
|
// 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);
|
|
|
|
// Set initial position to prevent "jump to top" effect
|
|
// Calculate absolute Y position from original element
|
|
const originalRect = this.originalEvent.getBoundingClientRect();
|
|
const columnRect = payload.columnBounds.boundingClientRect;
|
|
const initialTop = originalRect.top - columnRect.top;
|
|
|
|
this.draggedClone.style.top = `${initialTop}px`;
|
|
}
|
|
}
|
|
|
|
// Make original semi-transparent
|
|
this.originalEvent.style.opacity = '0.3';
|
|
this.originalEvent.style.userSelect = 'none';
|
|
|
|
}
|
|
|
|
/**
|
|
* Handle drag move event
|
|
* Only updates visual position and time - date stays the same
|
|
*/
|
|
public handleDragMove(payload: IDragMoveEventPayload): void {
|
|
const swpEvent = payload.draggedClone as SwpEventElement;
|
|
swpEvent.updatePosition(payload.snappedY);
|
|
}
|
|
|
|
/**
|
|
* Handle column change during drag
|
|
* Only moves the element visually - no data updates here
|
|
* Data updates happen on drag:end in EventRenderingService
|
|
*/
|
|
public handleColumnChange(payload: IDragColumnChangeEventPayload): void {
|
|
const eventsLayer = payload.newColumn.element.querySelector('swp-events-layer');
|
|
if (eventsLayer && payload.draggedClone.parentElement !== eventsLayer) {
|
|
eventsLayer.appendChild(payload.draggedClone);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle conversion of all-day event to timed event
|
|
*/
|
|
public handleConvertAllDayToTimed(payload: IDragMouseEnterColumnEventPayload): void {
|
|
|
|
console.log('🎯 DateEventRenderer: Converting all-day to timed event', {
|
|
eventId: payload.calendarEvent.id,
|
|
targetColumn: payload.targetColumn.identifier,
|
|
snappedY: payload.snappedY
|
|
});
|
|
|
|
let timedClone = SwpEventElement.fromCalendarEvent(payload.calendarEvent);
|
|
let position = this.calculateEventPosition(payload.calendarEvent);
|
|
|
|
// Set position at snapped Y
|
|
//timedClone.style.top = `${snappedY}px`;
|
|
|
|
// Set complete styling for dragged clone (matching normal event rendering)
|
|
timedClone.style.height = `${position.height - 3}px`;
|
|
timedClone.style.left = '2px';
|
|
timedClone.style.right = '2px';
|
|
timedClone.style.width = 'auto';
|
|
timedClone.style.pointerEvents = 'none';
|
|
|
|
// Apply drag styling
|
|
this.applyDragStyling(timedClone);
|
|
|
|
// Find the events layer in the target column
|
|
let eventsLayer = payload.targetColumn.element.querySelector('swp-events-layer');
|
|
|
|
// Add "clone-" prefix to match clone ID pattern
|
|
//timedClone.dataset.eventId = `clone-${payload.calendarEvent.id}`;
|
|
|
|
// Remove old all-day clone and replace with new timed clone
|
|
payload.draggedClone.remove();
|
|
payload.replaceClone(timedClone);
|
|
eventsLayer!!.appendChild(timedClone);
|
|
|
|
}
|
|
|
|
/**
|
|
* Handle drag end event
|
|
*/
|
|
public handleDragEnd(originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: IColumnBounds, finalY: number): void {
|
|
|
|
// Only fade out and remove if it's a swp-event (not swp-allday-event)
|
|
// AllDayManager handles removal of swp-allday-event elements
|
|
if (originalElement.tagName === 'SWP-EVENT') {
|
|
this.fadeOutAndRemove(originalElement);
|
|
}
|
|
|
|
draggedClone.dataset.eventId = EventId.from(draggedClone.dataset.eventId!);
|
|
|
|
// Fully normalize the clone to be a regular event
|
|
draggedClone.classList.remove('dragging');
|
|
draggedClone.style.pointerEvents = ''; // Re-enable pointer events
|
|
|
|
// Clean up instance state
|
|
this.draggedClone = null;
|
|
this.originalEvent = null;
|
|
|
|
|
|
// Clean up any remaining day event clones
|
|
const dayEventClone = document.querySelector(`swp-event[data-event-id="${draggedClone.dataset.eventId}"]`);
|
|
if (dayEventClone) {
|
|
dayEventClone.remove();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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(columns: IColumnInfo[], container: HTMLElement): void {
|
|
// Find column DOM elements in the container
|
|
const columnElements = this.getColumns(container);
|
|
|
|
// Render events for each column using pre-filtered events from IColumnInfo
|
|
columns.forEach((columnInfo, index) => {
|
|
const columnElement = columnElements[index];
|
|
if (!columnElement) return;
|
|
|
|
// Filter out all-day events - they should be handled by AllDayEventRenderer
|
|
const timedEvents = columnInfo.events.filter(event => !event.allDay);
|
|
|
|
const eventsLayer = columnElement.querySelector('swp-events-layer') as HTMLElement;
|
|
if (eventsLayer && timedEvents.length > 0) {
|
|
this.renderColumnEvents(timedEvents, eventsLayer);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Render events for a single column
|
|
* Note: events are already filtered for this column
|
|
*/
|
|
public renderSingleColumnEvents(column: IColumnBounds, events: ICalendarEvent[]): void {
|
|
// Filter out all-day events
|
|
const timedEvents = events.filter(event => !event.allDay);
|
|
const eventsLayer = column.element.querySelector('swp-events-layer') as HTMLElement;
|
|
|
|
if (eventsLayer && timedEvents.length > 0) {
|
|
this.renderColumnEvents(timedEvents, eventsLayer);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render events in a column using combined stacking + grid algorithm
|
|
*/
|
|
private renderColumnEvents(columnEvents: ICalendarEvent[], eventsLayer: HTMLElement): void {
|
|
if (columnEvents.length === 0) return;
|
|
|
|
// Get layout from coordinator
|
|
const layout = this.layoutCoordinator.calculateColumnLayout(columnEvents);
|
|
|
|
// Render grid groups
|
|
layout.gridGroups.forEach(gridGroup => {
|
|
this.renderGridGroup(gridGroup, eventsLayer);
|
|
});
|
|
|
|
// Render stacked events
|
|
layout.stackedEvents.forEach(stackedEvent => {
|
|
const element = this.renderEvent(stackedEvent.event);
|
|
this.stackManager.applyStackLinkToElement(element, stackedEvent.stackLink);
|
|
this.stackManager.applyVisualStyling(element, stackedEvent.stackLink.stackLevel);
|
|
eventsLayer.appendChild(element);
|
|
});
|
|
}
|
|
/**
|
|
* Render events in a grid container (side-by-side with column sharing)
|
|
*/
|
|
private renderGridGroup(gridGroup: IGridGroupLayout, eventsLayer: HTMLElement): void {
|
|
const groupElement = document.createElement('swp-event-group');
|
|
|
|
// Add grid column class based on number of columns (not events)
|
|
const colCount = gridGroup.columns.length;
|
|
groupElement.classList.add(`cols-${colCount}`);
|
|
|
|
// Add stack level class for margin-left offset
|
|
groupElement.classList.add(`stack-level-${gridGroup.stackLevel}`);
|
|
|
|
// Position from layout
|
|
groupElement.style.top = `${gridGroup.position.top}px`;
|
|
|
|
// Add stack-link attribute for drag-drop (group acts as a stacked item)
|
|
const stackLink = {
|
|
stackLevel: gridGroup.stackLevel
|
|
};
|
|
this.stackManager.applyStackLinkToElement(groupElement, stackLink);
|
|
|
|
// Apply visual styling (margin-left and z-index) using StackManager
|
|
this.stackManager.applyVisualStyling(groupElement, gridGroup.stackLevel);
|
|
|
|
// Render each column
|
|
const earliestEvent = gridGroup.events[0];
|
|
gridGroup.columns.forEach((columnEvents: ICalendarEvent[]) => {
|
|
const columnContainer = this.renderGridColumn(columnEvents, earliestEvent.start);
|
|
groupElement.appendChild(columnContainer);
|
|
});
|
|
|
|
eventsLayer.appendChild(groupElement);
|
|
}
|
|
|
|
/**
|
|
* Render a single column within a grid group
|
|
* Column may contain multiple events that don't overlap
|
|
*/
|
|
private renderGridColumn(columnEvents: ICalendarEvent[], containerStart: Date): HTMLElement {
|
|
const columnContainer = document.createElement('div');
|
|
columnContainer.style.position = 'relative';
|
|
|
|
columnEvents.forEach(event => {
|
|
const element = this.renderEventInGrid(event, containerStart);
|
|
columnContainer.appendChild(element);
|
|
});
|
|
|
|
return columnContainer;
|
|
}
|
|
|
|
/**
|
|
* Render event within a grid container (absolute positioning within column)
|
|
*/
|
|
private renderEventInGrid(event: ICalendarEvent, containerStart: Date): HTMLElement {
|
|
const element = SwpEventElement.fromCalendarEvent(event);
|
|
|
|
// Calculate event height
|
|
const position = this.calculateEventPosition(event);
|
|
|
|
// Calculate relative top offset if event starts after container start
|
|
// (e.g., if container starts at 07:00 and event starts at 08:15, offset = 75 min)
|
|
const timeDiffMs = event.start.getTime() - containerStart.getTime();
|
|
const timeDiffMinutes = timeDiffMs / (1000 * 60);
|
|
const gridSettings = this.config.gridSettings;
|
|
const relativeTop = timeDiffMinutes > 0 ? (timeDiffMinutes / 60) * gridSettings.hourHeight : 0;
|
|
|
|
// Events in grid columns are positioned absolutely within their column container
|
|
element.style.position = 'absolute';
|
|
element.style.top = `${relativeTop}px`;
|
|
element.style.height = `${position.height - 3}px`;
|
|
element.style.left = '0';
|
|
element.style.right = '0';
|
|
|
|
return element;
|
|
}
|
|
|
|
|
|
private renderEvent(event: ICalendarEvent): HTMLElement {
|
|
const element = SwpEventElement.fromCalendarEvent(event);
|
|
|
|
// Apply positioning (moved from SwpEventElement.applyPositioning)
|
|
const position = this.calculateEventPosition(event);
|
|
element.style.position = 'absolute';
|
|
element.style.top = `${position.top + 1}px`;
|
|
element.style.height = `${position.height - 3}px`;
|
|
element.style.left = '2px';
|
|
element.style.right = '2px';
|
|
|
|
return element;
|
|
}
|
|
|
|
protected calculateEventPosition(event: ICalendarEvent): { top: number; height: number } {
|
|
// Delegate to PositionUtils for centralized position calculation
|
|
return this.positionUtils.calculateEventPosition(event.start, event.end);
|
|
}
|
|
|
|
clearEvents(container?: HTMLElement): void {
|
|
const eventSelector = 'swp-event';
|
|
const groupSelector = 'swp-event-group';
|
|
|
|
const existingEvents = container
|
|
? container.querySelectorAll(eventSelector)
|
|
: document.querySelectorAll(eventSelector);
|
|
|
|
const existingGroups = container
|
|
? container.querySelectorAll(groupSelector)
|
|
: document.querySelectorAll(groupSelector);
|
|
|
|
existingEvents.forEach(event => event.remove());
|
|
existingGroups.forEach(group => group.remove());
|
|
}
|
|
|
|
protected getColumns(container: HTMLElement): HTMLElement[] {
|
|
const columns = container.querySelectorAll('swp-day-column');
|
|
return Array.from(columns) as HTMLElement[];
|
|
}
|
|
}
|