Enhances the event drag and drop functionality by setting the initial position of the dragged event to prevent it from jumping to the top of the column. Also adjust event transition for a smoother user experience. Removes unused resize logic.
353 lines
12 KiB
TypeScript
353 lines
12 KiB
TypeScript
// 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';
|
|
import { EventStackManager } from '../managers/EventStackManager';
|
|
import { EventLayoutCoordinator, GridGroupLayout, StackedEventLayout } from '../managers/EventLayoutCoordinator';
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* Date-based event renderer
|
|
*/
|
|
export class DateEventRenderer implements EventRendererStrategy {
|
|
|
|
private dateService: DateService;
|
|
private stackManager: EventStackManager;
|
|
private layoutCoordinator: EventLayoutCoordinator;
|
|
private draggedClone: HTMLElement | null = null;
|
|
private originalEvent: HTMLElement | null = null;
|
|
|
|
constructor() {
|
|
const timezone = calendarConfig.getTimezone?.();
|
|
this.dateService = new DateService(timezone);
|
|
this.stackManager = new EventStackManager();
|
|
this.layoutCoordinator = new EventLayoutCoordinator();
|
|
}
|
|
|
|
private applyDragStyling(element: HTMLElement): void {
|
|
element.classList.add('dragging');
|
|
element.style.removeProperty("margin-left");
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* 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 && 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
|
|
*/
|
|
public handleDragMove(payload: DragMoveEventPayload): void {
|
|
if (!this.draggedClone || !payload.columnBounds) return;
|
|
|
|
// Delegate to SwpEventElement to update position and timestamps
|
|
const swpEvent = this.draggedClone as SwpEventElement;
|
|
const columnDate = this.dateService.parseISO(payload.columnBounds.date);
|
|
swpEvent.updatePosition(columnDate, payload.snappedY);
|
|
}
|
|
|
|
/**
|
|
* 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 swpEvent = this.draggedClone as SwpEventElement;
|
|
const columnDate = this.dateService.parseISO(dragColumnChangeEvent.newColumn.date);
|
|
swpEvent.updatePosition(columnDate, currentTop);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
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');
|
|
draggedClone.style.pointerEvents = ''; // Re-enable pointer events
|
|
|
|
// 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);
|
|
|
|
columns.forEach(column => {
|
|
const columnEvents = this.getEventsForColumn(column, timedEvents);
|
|
const eventsLayer = column.querySelector('swp-events-layer') as HTMLElement;
|
|
|
|
if (eventsLayer) {
|
|
this.renderColumnEvents(columnEvents, eventsLayer);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Render events in a column using combined stacking + grid algorithm
|
|
*/
|
|
private renderColumnEvents(columnEvents: CalendarEvent[], 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: GridGroupLayout, 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 => {
|
|
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: CalendarEvent[], 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: CalendarEvent, 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 = calendarConfig.getGridSettings();
|
|
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: CalendarEvent): 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: CalendarEvent): { top: number; height: number } {
|
|
// Delegate to PositionUtils for centralized position calculation
|
|
return 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[];
|
|
}
|
|
|
|
protected getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[] {
|
|
const columnDate = column.dataset.date;
|
|
if (!columnDate) {
|
|
return [];
|
|
}
|
|
|
|
// Create start and end of day for interval overlap check
|
|
const columnStart = this.dateService.parseISO(`${columnDate}T00:00:00`);
|
|
const columnEnd = this.dateService.parseISO(`${columnDate}T23:59:59.999`);
|
|
|
|
const columnEvents = events.filter(event => {
|
|
// Interval overlap: event overlaps with column day if event.start < columnEnd AND event.end > columnStart
|
|
const overlaps = event.start < columnEnd && event.end > columnStart;
|
|
return overlaps;
|
|
});
|
|
|
|
return columnEvents;
|
|
}
|
|
}
|