2025-08-07 00:15:44 +02:00
|
|
|
// Event rendering strategy interface and implementations
|
|
|
|
|
|
|
|
|
|
import { CalendarEvent } from '../types/CalendarTypes';
|
2025-08-24 23:31:11 +02:00
|
|
|
import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig';
|
2025-09-03 20:04:47 +02:00
|
|
|
import { calendarConfig } from '../core/CalendarConfig';
|
2025-08-20 00:39:31 +02:00
|
|
|
import { DateCalculator } from '../utils/DateCalculator';
|
2025-08-27 22:50:13 +02:00
|
|
|
import { eventBus } from '../core/EventBus';
|
2025-09-01 23:37:47 +02:00
|
|
|
import { CoreEvents } from '../constants/CoreEvents';
|
2025-09-04 23:35:19 +02:00
|
|
|
import { SimpleEventOverlapManager, OverlapType } from '../managers/SimpleEventOverlapManager';
|
2025-08-07 00:15:44 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Interface for event rendering strategies
|
|
|
|
|
*/
|
|
|
|
|
export interface EventRendererStrategy {
|
2025-09-03 20:04:47 +02:00
|
|
|
renderEvents(events: CalendarEvent[], container: HTMLElement): void;
|
2025-08-16 00:51:12 +02:00
|
|
|
clearEvents(container?: HTMLElement): void;
|
2025-08-07 00:15:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Base class for event renderers with common functionality
|
|
|
|
|
*/
|
|
|
|
|
export abstract class BaseEventRenderer implements EventRendererStrategy {
|
2025-08-20 00:39:31 +02:00
|
|
|
protected dateCalculator: DateCalculator;
|
2025-09-04 23:35:19 +02:00
|
|
|
protected overlapManager: SimpleEventOverlapManager;
|
2025-08-27 22:50:13 +02:00
|
|
|
|
|
|
|
|
// Drag and drop state
|
|
|
|
|
private draggedClone: HTMLElement | null = null;
|
|
|
|
|
private originalEvent: HTMLElement | null = null;
|
2025-08-20 00:39:31 +02:00
|
|
|
|
2025-09-03 20:04:47 +02:00
|
|
|
constructor(dateCalculator?: DateCalculator) {
|
2025-09-03 18:38:52 +02:00
|
|
|
if (!dateCalculator) {
|
2025-09-03 20:04:47 +02:00
|
|
|
DateCalculator.initialize(calendarConfig);
|
2025-09-03 18:38:52 +02:00
|
|
|
}
|
|
|
|
|
this.dateCalculator = dateCalculator || new DateCalculator();
|
2025-09-04 23:35:19 +02:00
|
|
|
this.overlapManager = new SimpleEventOverlapManager();
|
2025-08-27 22:50:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Setup listeners for drag events from DragDropManager
|
|
|
|
|
*/
|
2025-08-27 23:56:38 +02:00
|
|
|
protected setupDragEventListeners(): void {
|
2025-08-27 22:50:13 +02:00
|
|
|
// Handle drag start
|
|
|
|
|
eventBus.on('drag:start', (event) => {
|
|
|
|
|
const { originalElement, eventId, mouseOffset, column } = (event as CustomEvent).detail;
|
|
|
|
|
this.handleDragStart(originalElement, eventId, mouseOffset, column);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Handle drag move
|
|
|
|
|
eventBus.on('drag:move', (event) => {
|
|
|
|
|
const { eventId, snappedY, column, mouseOffset } = (event as CustomEvent).detail;
|
|
|
|
|
this.handleDragMove(eventId, snappedY, column, mouseOffset);
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-03 20:13:56 +02:00
|
|
|
// Handle drag auto-scroll (when dragging near edges triggers scroll)
|
|
|
|
|
eventBus.on('drag:auto-scroll', (event) => {
|
|
|
|
|
const { eventId, snappedY } = (event as CustomEvent).detail;
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-27 22:50:13 +02:00
|
|
|
// Handle drag end
|
|
|
|
|
eventBus.on('drag:end', (event) => {
|
|
|
|
|
const { eventId, originalElement, finalColumn, finalY } = (event as CustomEvent).detail;
|
|
|
|
|
this.handleDragEnd(eventId, originalElement, finalColumn, finalY);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Handle column change
|
|
|
|
|
eventBus.on('drag:column-change', (event) => {
|
|
|
|
|
const { eventId, newColumn } = (event as CustomEvent).detail;
|
|
|
|
|
this.handleColumnChange(eventId, newColumn);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Handle convert to all-day
|
|
|
|
|
eventBus.on('drag:convert-to-allday', (event) => {
|
|
|
|
|
const { eventId, targetDate, headerRenderer } = (event as CustomEvent).detail;
|
|
|
|
|
this.handleConvertToAllDay(eventId, targetDate, headerRenderer);
|
|
|
|
|
});
|
2025-09-01 23:37:47 +02:00
|
|
|
|
|
|
|
|
// Handle navigation period change (when slide animation completes)
|
2025-09-03 20:04:47 +02:00
|
|
|
eventBus.on(CoreEvents.NAVIGATION_COMPLETED, () => {
|
2025-09-01 23:37:47 +02:00
|
|
|
// Animate all-day height after navigation completes
|
2025-09-03 18:15:33 +02:00
|
|
|
this.triggerAllDayHeightAnimation();
|
2025-09-01 23:37:47 +02:00
|
|
|
});
|
2025-08-27 22:50:13 +02:00
|
|
|
}
|
2025-09-03 18:15:33 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Trigger all-day height animation without creating new renderer instance
|
|
|
|
|
*/
|
|
|
|
|
private triggerAllDayHeightAnimation(): void {
|
|
|
|
|
import('./HeaderRenderer').then(({ DateHeaderRenderer }) => {
|
|
|
|
|
const headerRenderer = new DateHeaderRenderer();
|
|
|
|
|
headerRenderer.checkAndAnimateAllDayHeight();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Cleanup method for proper resource management
|
|
|
|
|
*/
|
|
|
|
|
public destroy(): void {
|
|
|
|
|
this.draggedClone = null;
|
|
|
|
|
this.originalEvent = null;
|
|
|
|
|
}
|
2025-08-31 23:48:34 +02:00
|
|
|
|
2025-08-27 22:50:13 +02:00
|
|
|
/**
|
|
|
|
|
* Get original event duration from data-duration attribute
|
|
|
|
|
*/
|
|
|
|
|
private getOriginalEventDuration(originalEvent: HTMLElement): number {
|
|
|
|
|
// Find the swp-event-time element with data-duration attribute
|
|
|
|
|
const timeElement = originalEvent.querySelector('swp-event-time');
|
|
|
|
|
if (timeElement) {
|
|
|
|
|
const duration = timeElement.getAttribute('data-duration');
|
|
|
|
|
if (duration) {
|
|
|
|
|
const durationMinutes = parseInt(duration);
|
|
|
|
|
return durationMinutes;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fallback to 60 minutes if attribute not found
|
|
|
|
|
return 60;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a clone of an event for dragging
|
|
|
|
|
*/
|
|
|
|
|
private createEventClone(originalEvent: HTMLElement): HTMLElement {
|
|
|
|
|
const clone = originalEvent.cloneNode(true) as HTMLElement;
|
|
|
|
|
|
|
|
|
|
// Prefix ID with "clone-"
|
|
|
|
|
const originalId = originalEvent.dataset.eventId;
|
|
|
|
|
if (originalId) {
|
|
|
|
|
clone.dataset.eventId = `clone-${originalId}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get and cache original duration from data-duration attribute
|
|
|
|
|
const originalDurationMinutes = this.getOriginalEventDuration(originalEvent);
|
|
|
|
|
clone.dataset.originalDuration = originalDurationMinutes.toString();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Style for dragging
|
|
|
|
|
clone.style.position = 'absolute';
|
|
|
|
|
clone.style.zIndex = '999999';
|
|
|
|
|
clone.style.pointerEvents = 'none';
|
|
|
|
|
clone.style.opacity = '0.8';
|
|
|
|
|
|
2025-09-04 19:22:26 +02:00
|
|
|
// Dragged event skal have fuld kolonne bredde
|
|
|
|
|
clone.style.left = '2px';
|
|
|
|
|
clone.style.right = '2px';
|
|
|
|
|
clone.style.width = '';
|
|
|
|
|
clone.style.height = originalEvent.style.height || `${originalEvent.getBoundingClientRect().height}px`;
|
2025-08-27 22:50:13 +02:00
|
|
|
|
|
|
|
|
return clone;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update clone timestamp based on new position
|
|
|
|
|
*/
|
|
|
|
|
private updateCloneTimestamp(clone: HTMLElement, snappedY: number): void {
|
2025-09-03 20:04:47 +02:00
|
|
|
const gridSettings = calendarConfig.getGridSettings();
|
2025-08-27 22:50:13 +02:00
|
|
|
const hourHeight = gridSettings.hourHeight;
|
|
|
|
|
const dayStartHour = gridSettings.dayStartHour;
|
2025-09-03 20:48:23 +02:00
|
|
|
const snapInterval = gridSettings.snapInterval;
|
2025-08-27 22:50:13 +02:00
|
|
|
|
2025-09-03 20:48:23 +02:00
|
|
|
// Calculate minutes from grid start (not from midnight)
|
|
|
|
|
const minutesFromGridStart = (snappedY / hourHeight) * 60;
|
|
|
|
|
|
|
|
|
|
// Add dayStartHour offset to get actual time
|
|
|
|
|
const actualStartMinutes = (dayStartHour * 60) + minutesFromGridStart;
|
|
|
|
|
|
|
|
|
|
// Snap to interval
|
|
|
|
|
const snappedStartMinutes = Math.round(actualStartMinutes / snapInterval) * snapInterval;
|
2025-08-27 22:50:13 +02:00
|
|
|
|
|
|
|
|
// Use cached original duration (no recalculation)
|
|
|
|
|
const cachedDuration = parseInt(clone.dataset.originalDuration || '60');
|
2025-09-03 20:48:23 +02:00
|
|
|
const endTotalMinutes = snappedStartMinutes + cachedDuration;
|
2025-08-27 22:50:13 +02:00
|
|
|
|
|
|
|
|
// Update display
|
|
|
|
|
const timeElement = clone.querySelector('swp-event-time');
|
|
|
|
|
if (timeElement) {
|
2025-09-03 20:48:23 +02:00
|
|
|
const newTimeText = `${this.formatTime(snappedStartMinutes)} - ${this.formatTime(endTotalMinutes)}`;
|
2025-08-27 22:50:13 +02:00
|
|
|
timeElement.textContent = newTimeText;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate event duration in minutes from element height
|
|
|
|
|
*/
|
|
|
|
|
private getEventDuration(element: HTMLElement): number {
|
2025-09-03 20:04:47 +02:00
|
|
|
const gridSettings = calendarConfig.getGridSettings();
|
2025-08-27 22:50:13 +02:00
|
|
|
const hourHeight = gridSettings.hourHeight;
|
|
|
|
|
|
|
|
|
|
// Get height from style or computed
|
|
|
|
|
let heightPx = parseFloat(element.style.height) || 0;
|
|
|
|
|
if (!heightPx) {
|
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
|
|
|
heightPx = rect.height;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Math.round((heightPx / hourHeight) * 60);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-03 18:15:33 +02:00
|
|
|
* Unified time formatting method - handles both total minutes and Date objects
|
2025-08-27 22:50:13 +02:00
|
|
|
*/
|
2025-09-03 18:15:33 +02:00
|
|
|
private formatTime(input: number | Date | string): string {
|
|
|
|
|
let hours: number, minutes: number;
|
|
|
|
|
|
|
|
|
|
if (typeof input === 'number') {
|
|
|
|
|
// Total minutes input
|
|
|
|
|
hours = Math.floor(input / 60) % 24;
|
|
|
|
|
minutes = input % 60;
|
|
|
|
|
} else {
|
|
|
|
|
// Date or ISO string input
|
|
|
|
|
const date = typeof input === 'string' ? new Date(input) : input;
|
|
|
|
|
hours = date.getHours();
|
|
|
|
|
minutes = date.getMinutes();
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-27 22:50:13 +02:00
|
|
|
const period = hours >= 12 ? 'PM' : 'AM';
|
2025-09-03 18:15:33 +02:00
|
|
|
const displayHours = hours > 12 ? hours - 12 : (hours === 0 ? 12 : hours);
|
2025-08-27 22:50:13 +02:00
|
|
|
return `${displayHours}:${minutes.toString().padStart(2, '0')} ${period}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle drag start event
|
|
|
|
|
*/
|
|
|
|
|
private handleDragStart(originalElement: HTMLElement, eventId: string, mouseOffset: any, column: string): void {
|
2025-09-04 23:35:19 +02:00
|
|
|
console.log('handleDragStart:', eventId);
|
2025-08-27 22:50:13 +02:00
|
|
|
this.originalEvent = originalElement;
|
|
|
|
|
|
2025-09-04 23:35:19 +02:00
|
|
|
// Remove stacking styling during drag
|
2025-09-04 19:22:26 +02:00
|
|
|
if (this.overlapManager.isStackedEvent(originalElement)) {
|
|
|
|
|
this.overlapManager.removeStackedStyling(originalElement);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-27 22:50:13 +02:00
|
|
|
// Create clone
|
|
|
|
|
this.draggedClone = this.createEventClone(originalElement);
|
|
|
|
|
|
2025-09-04 00:16:35 +02:00
|
|
|
// Add to current column's events layer (not directly to column)
|
2025-08-27 22:50:13 +02:00
|
|
|
const columnElement = document.querySelector(`swp-day-column[data-date="${column}"]`);
|
|
|
|
|
if (columnElement) {
|
2025-09-04 00:16:35 +02:00
|
|
|
const eventsLayer = columnElement.querySelector('swp-events-layer');
|
|
|
|
|
if (eventsLayer) {
|
|
|
|
|
eventsLayer.appendChild(this.draggedClone);
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback to column if events layer not found
|
|
|
|
|
columnElement.appendChild(this.draggedClone);
|
|
|
|
|
}
|
2025-08-27 22:50:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Make original semi-transparent
|
|
|
|
|
originalElement.style.opacity = '0.3';
|
|
|
|
|
originalElement.style.userSelect = 'none';
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle drag move event
|
|
|
|
|
*/
|
|
|
|
|
private handleDragMove(eventId: string, snappedY: number, column: string, mouseOffset: any): void {
|
|
|
|
|
if (!this.draggedClone) return;
|
|
|
|
|
|
|
|
|
|
// Update position
|
|
|
|
|
this.draggedClone.style.top = snappedY + 'px';
|
|
|
|
|
|
|
|
|
|
// Update timestamp display
|
|
|
|
|
this.updateCloneTimestamp(this.draggedClone, snappedY);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle column change during drag
|
|
|
|
|
*/
|
|
|
|
|
private handleColumnChange(eventId: string, newColumn: string): void {
|
|
|
|
|
if (!this.draggedClone) return;
|
|
|
|
|
|
2025-09-04 00:16:35 +02:00
|
|
|
// Move clone to new column's events layer
|
2025-08-27 22:50:13 +02:00
|
|
|
const newColumnElement = document.querySelector(`swp-day-column[data-date="${newColumn}"]`);
|
2025-09-04 00:16:35 +02:00
|
|
|
if (newColumnElement) {
|
|
|
|
|
const eventsLayer = newColumnElement.querySelector('swp-events-layer');
|
|
|
|
|
if (eventsLayer && this.draggedClone.parentElement !== eventsLayer) {
|
|
|
|
|
eventsLayer.appendChild(this.draggedClone);
|
|
|
|
|
} else if (!eventsLayer && this.draggedClone.parentElement !== newColumnElement) {
|
|
|
|
|
// Fallback to column if events layer not found
|
|
|
|
|
newColumnElement.appendChild(this.draggedClone);
|
|
|
|
|
}
|
2025-08-27 22:50:13 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle drag end event
|
|
|
|
|
*/
|
|
|
|
|
private handleDragEnd(eventId: string, originalElement: HTMLElement, finalColumn: string, finalY: number): void {
|
2025-09-04 23:35:19 +02:00
|
|
|
console.log('handleDragEnd:', eventId);
|
2025-08-27 23:56:38 +02:00
|
|
|
|
|
|
|
|
if (!this.draggedClone || !this.originalEvent) {
|
2025-09-04 23:35:19 +02:00
|
|
|
console.log('Missing draggedClone or originalEvent');
|
2025-08-27 23:56:38 +02:00
|
|
|
return;
|
|
|
|
|
}
|
2025-08-27 22:50:13 +02:00
|
|
|
|
2025-09-04 19:22:26 +02:00
|
|
|
// Remove original event from any existing groups first
|
|
|
|
|
this.removeEventFromExistingGroups(this.originalEvent);
|
|
|
|
|
|
2025-08-27 22:50:13 +02:00
|
|
|
// Fade out original
|
|
|
|
|
this.fadeOutAndRemove(this.originalEvent);
|
|
|
|
|
|
2025-08-27 23:56:38 +02:00
|
|
|
// Remove clone prefix and normalize clone to be a regular event
|
2025-08-27 22:50:13 +02:00
|
|
|
const cloneId = this.draggedClone.dataset.eventId;
|
|
|
|
|
if (cloneId && cloneId.startsWith('clone-')) {
|
|
|
|
|
this.draggedClone.dataset.eventId = cloneId.replace('clone-', '');
|
|
|
|
|
}
|
2025-08-27 23:56:38 +02:00
|
|
|
|
|
|
|
|
// Fully normalize the clone to be a regular event
|
2025-08-27 22:50:13 +02:00
|
|
|
this.draggedClone.style.pointerEvents = '';
|
|
|
|
|
this.draggedClone.style.opacity = '';
|
2025-08-27 23:56:38 +02:00
|
|
|
this.draggedClone.style.userSelect = '';
|
2025-09-04 19:22:26 +02:00
|
|
|
// Behold z-index hvis det er et stacked event
|
2025-08-27 23:56:38 +02:00
|
|
|
|
2025-09-04 19:22:26 +02:00
|
|
|
// Detect overlaps with other events in the target column and reposition if needed
|
|
|
|
|
this.detectAndHandleOverlaps(this.draggedClone, finalColumn);
|
2025-08-27 22:50:13 +02:00
|
|
|
|
|
|
|
|
// Clean up
|
|
|
|
|
this.draggedClone = null;
|
|
|
|
|
this.originalEvent = null;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 19:22:26 +02:00
|
|
|
/**
|
|
|
|
|
* Remove event from any existing groups and cleanup empty containers
|
|
|
|
|
*/
|
|
|
|
|
private removeEventFromExistingGroups(eventElement: HTMLElement): void {
|
|
|
|
|
const eventGroup = this.overlapManager.getEventGroup(eventElement);
|
2025-09-04 23:35:19 +02:00
|
|
|
const eventId = eventElement.dataset.eventId;
|
|
|
|
|
|
|
|
|
|
if (eventGroup && eventId) {
|
|
|
|
|
// Remove from flexbox group
|
|
|
|
|
this.overlapManager.removeFromEventGroup(eventGroup, eventId);
|
2025-09-04 19:22:26 +02:00
|
|
|
} else if (this.overlapManager.isStackedEvent(eventElement)) {
|
2025-09-04 23:35:19 +02:00
|
|
|
// Remove stacking styling and restack others
|
2025-09-04 19:22:26 +02:00
|
|
|
this.overlapManager.removeStackedStyling(eventElement);
|
2025-09-04 23:35:19 +02:00
|
|
|
const container = eventElement.closest('swp-events-layer') as HTMLElement;
|
|
|
|
|
if (container) {
|
|
|
|
|
this.overlapManager.restackEventsInContainer(container);
|
|
|
|
|
}
|
2025-09-04 19:22:26 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Restore normal event styling (full column width)
|
|
|
|
|
*/
|
|
|
|
|
private restoreNormalEventStyling(eventElement: HTMLElement): void {
|
|
|
|
|
eventElement.style.position = 'absolute';
|
|
|
|
|
eventElement.style.left = '2px';
|
|
|
|
|
eventElement.style.right = '2px';
|
|
|
|
|
eventElement.style.width = '';
|
|
|
|
|
// Behold z-index for stacked events
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 20:32:25 +02:00
|
|
|
/**
|
|
|
|
|
* Detect pixel-based overlap between two event elements
|
|
|
|
|
*/
|
|
|
|
|
private detectPixelOverlap(element1: HTMLElement, element2: HTMLElement): OverlapType {
|
|
|
|
|
const top1 = parseFloat(element1.style.top) || 0;
|
|
|
|
|
const height1 = parseFloat(element1.style.height) || 0;
|
|
|
|
|
const bottom1 = top1 + height1;
|
|
|
|
|
|
|
|
|
|
const top2 = parseFloat(element2.style.top) || 0;
|
|
|
|
|
const height2 = parseFloat(element2.style.height) || 0;
|
|
|
|
|
const bottom2 = top2 + height2;
|
|
|
|
|
|
2025-09-04 23:35:19 +02:00
|
|
|
// Check if events overlap in pixel space (with small tolerance for borders)
|
|
|
|
|
const tolerance = 2; // Account for borders and small gaps
|
|
|
|
|
if (bottom1 <= (top2 + tolerance) || bottom2 <= (top1 + tolerance)) {
|
2025-09-04 20:32:25 +02:00
|
|
|
return OverlapType.NONE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Events overlap - check start position difference for overlap type
|
|
|
|
|
const startDifference = Math.abs(top1 - top2);
|
|
|
|
|
|
|
|
|
|
// Over 40px start difference = stacking
|
|
|
|
|
if (startDifference > 40) {
|
|
|
|
|
return OverlapType.STACKING;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Within 40px start difference = column sharing
|
|
|
|
|
return OverlapType.COLUMN_SHARING;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 23:35:19 +02:00
|
|
|
/**
|
|
|
|
|
* Detect and group overlapping events during initial rendering
|
|
|
|
|
*/
|
|
|
|
|
private detectAndGroupInitialEvents(renderedElements: HTMLElement[], container: Element): void {
|
|
|
|
|
const processedElements = new Set<HTMLElement>();
|
|
|
|
|
|
|
|
|
|
for (const element of renderedElements) {
|
|
|
|
|
if (processedElements.has(element)) continue;
|
|
|
|
|
|
|
|
|
|
const overlappingElements: HTMLElement[] = [element];
|
|
|
|
|
processedElements.add(element);
|
|
|
|
|
|
|
|
|
|
// Find alle elements der overlapper med dette element
|
|
|
|
|
for (const otherElement of renderedElements) {
|
|
|
|
|
if (otherElement === element || processedElements.has(otherElement)) continue;
|
|
|
|
|
|
|
|
|
|
const overlapType = this.detectPixelOverlap(element, otherElement);
|
|
|
|
|
if (overlapType !== OverlapType.NONE) {
|
|
|
|
|
overlappingElements.push(otherElement);
|
|
|
|
|
processedElements.add(otherElement);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Hvis der er overlaps, group dem
|
|
|
|
|
if (overlappingElements.length > 1) {
|
|
|
|
|
const overlapType = this.detectPixelOverlap(overlappingElements[0], overlappingElements[1]);
|
|
|
|
|
|
|
|
|
|
// Fjern overlapping elements fra DOM
|
|
|
|
|
overlappingElements.forEach(el => el.remove());
|
|
|
|
|
|
|
|
|
|
// Konvertér til CalendarEvent objekter
|
|
|
|
|
const overlappingEvents: CalendarEvent[] = [];
|
|
|
|
|
for (const el of overlappingElements) {
|
|
|
|
|
const event = this.elementToCalendarEvent(el);
|
|
|
|
|
if (event) {
|
|
|
|
|
overlappingEvents.push(event);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (overlapType === OverlapType.COLUMN_SHARING) {
|
|
|
|
|
// Create column sharing group
|
|
|
|
|
const groupContainer = this.overlapManager.createEventGroup(overlappingEvents, { top: 0, height: 0 });
|
|
|
|
|
|
|
|
|
|
overlappingEvents.forEach(event => {
|
|
|
|
|
const eventElement = this.createEventElement(event);
|
|
|
|
|
this.positionEvent(eventElement, event);
|
|
|
|
|
this.overlapManager.addToEventGroup(groupContainer, eventElement);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
container.appendChild(groupContainer);
|
|
|
|
|
} else if (overlapType === OverlapType.STACKING) {
|
|
|
|
|
// Handle stacking
|
|
|
|
|
const sortedEvents = [...overlappingEvents].sort((a, b) => {
|
|
|
|
|
const durationA = new Date(a.end).getTime() - new Date(a.start).getTime();
|
|
|
|
|
const durationB = new Date(b.end).getTime() - new Date(b.start).getTime();
|
|
|
|
|
return durationB - durationA;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let underlyingElement: HTMLElement | null = null;
|
|
|
|
|
|
|
|
|
|
sortedEvents.forEach((event, index) => {
|
|
|
|
|
const eventElement = this.createEventElement(event);
|
|
|
|
|
this.positionEvent(eventElement, event);
|
|
|
|
|
|
|
|
|
|
if (index === 0) {
|
|
|
|
|
container.appendChild(eventElement);
|
|
|
|
|
underlyingElement = eventElement;
|
|
|
|
|
} else {
|
|
|
|
|
if (underlyingElement) {
|
|
|
|
|
this.overlapManager.createStackedEvent(eventElement, underlyingElement, index);
|
|
|
|
|
}
|
|
|
|
|
container.appendChild(eventElement);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 19:22:26 +02:00
|
|
|
/**
|
|
|
|
|
* Detect overlaps with other events in target column and handle repositioning
|
|
|
|
|
*/
|
|
|
|
|
private detectAndHandleOverlaps(droppedElement: HTMLElement, targetColumn: string): void {
|
|
|
|
|
// Find target column element
|
|
|
|
|
const columnElement = document.querySelector(`swp-day-column[data-date="${targetColumn}"]`);
|
|
|
|
|
if (!columnElement) return;
|
|
|
|
|
|
|
|
|
|
const eventsLayer = columnElement.querySelector('swp-events-layer');
|
|
|
|
|
if (!eventsLayer) return;
|
|
|
|
|
|
|
|
|
|
// Convert dropped element to CalendarEvent using its NEW position
|
|
|
|
|
const droppedEvent = this.elementToCalendarEventWithNewPosition(droppedElement, targetColumn);
|
|
|
|
|
if (!droppedEvent) return;
|
|
|
|
|
|
2025-09-04 20:06:09 +02:00
|
|
|
// Check if there's already an existing swp-event-group in the column
|
|
|
|
|
const existingGroup = eventsLayer.querySelector('swp-event-group') as HTMLElement;
|
|
|
|
|
if (existingGroup) {
|
|
|
|
|
// Check if dropped event overlaps with the group's events
|
|
|
|
|
const groupEvents = Array.from(existingGroup.querySelectorAll('swp-event')) as HTMLElement[];
|
|
|
|
|
let overlapsWithGroup = false;
|
|
|
|
|
|
|
|
|
|
for (const groupEvent of groupEvents) {
|
2025-09-04 20:32:25 +02:00
|
|
|
const overlapType = this.detectPixelOverlap(droppedElement, groupEvent);
|
2025-09-04 20:06:09 +02:00
|
|
|
if (overlapType === OverlapType.COLUMN_SHARING) {
|
|
|
|
|
overlapsWithGroup = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (overlapsWithGroup) {
|
|
|
|
|
// Simply add the dropped event to the existing group
|
|
|
|
|
this.updateElementDataset(droppedElement, droppedEvent);
|
|
|
|
|
this.overlapManager.addToEventGroup(existingGroup, droppedElement);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No existing group or no overlap with existing group - run full overlap detection
|
|
|
|
|
const existingEvents = Array.from(eventsLayer.querySelectorAll('swp-event'))
|
|
|
|
|
.filter(el => el !== droppedElement) as HTMLElement[];
|
|
|
|
|
|
2025-09-04 19:22:26 +02:00
|
|
|
// Check if dropped event overlaps with any existing events
|
|
|
|
|
let hasOverlaps = false;
|
2025-09-04 20:06:09 +02:00
|
|
|
const overlappingEvents: CalendarEvent[] = [];
|
2025-09-04 23:35:19 +02:00
|
|
|
let overlapType: OverlapType = OverlapType.NONE;
|
2025-09-04 19:22:26 +02:00
|
|
|
|
|
|
|
|
for (const existingElement of existingEvents) {
|
2025-09-04 20:06:09 +02:00
|
|
|
// Skip if it's the same event (comparing IDs)
|
2025-09-04 20:32:25 +02:00
|
|
|
if (existingElement.dataset.eventId === droppedEvent.id) continue;
|
2025-09-04 20:06:09 +02:00
|
|
|
|
2025-09-04 23:35:19 +02:00
|
|
|
const currentOverlapType = this.detectPixelOverlap(droppedElement, existingElement);
|
|
|
|
|
if (currentOverlapType !== OverlapType.NONE) {
|
2025-09-04 19:22:26 +02:00
|
|
|
hasOverlaps = true;
|
2025-09-04 23:35:19 +02:00
|
|
|
// Use the first detected overlap type for consistency
|
|
|
|
|
if (overlapType === OverlapType.NONE) {
|
|
|
|
|
overlapType = currentOverlapType;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CRITICAL FIX: Include the entire stack chain, not just the directly overlapping event
|
|
|
|
|
const stackChain = this.getFullStackChain(existingElement);
|
|
|
|
|
const alreadyIncludedIds = new Set(overlappingEvents.map(e => e.id));
|
|
|
|
|
|
|
|
|
|
for (const chainElement of stackChain) {
|
|
|
|
|
const chainEvent = this.elementToCalendarEvent(chainElement);
|
|
|
|
|
if (chainEvent && !alreadyIncludedIds.has(chainEvent.id)) {
|
|
|
|
|
overlappingEvents.push(chainEvent);
|
|
|
|
|
alreadyIncludedIds.add(chainEvent.id);
|
|
|
|
|
}
|
2025-09-04 20:32:25 +02:00
|
|
|
}
|
2025-09-04 19:22:26 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 20:06:09 +02:00
|
|
|
// Add dropped event LAST so it appears rightmost in flexbox
|
|
|
|
|
overlappingEvents.push(droppedEvent);
|
|
|
|
|
|
2025-09-04 19:22:26 +02:00
|
|
|
// Only re-render if there are actual overlaps
|
|
|
|
|
if (!hasOverlaps) {
|
|
|
|
|
// No overlaps - just update the dropped element's dataset with new times
|
|
|
|
|
this.updateElementDataset(droppedElement, droppedEvent);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 23:35:19 +02:00
|
|
|
// There are overlaps - use the detected overlap type
|
|
|
|
|
|
|
|
|
|
if (overlapType === OverlapType.COLUMN_SHARING) {
|
|
|
|
|
// Create column sharing group
|
|
|
|
|
const groupContainer = this.overlapManager.createEventGroup(overlappingEvents, { top: 0, height: 0 });
|
|
|
|
|
|
|
|
|
|
// Remove overlapping events from DOM
|
|
|
|
|
const overlappingEventIds = new Set(overlappingEvents.map(e => e.id));
|
|
|
|
|
existingEvents
|
|
|
|
|
.filter(el => overlappingEventIds.has(el.dataset.eventId || ''))
|
|
|
|
|
.forEach(el => el.remove());
|
|
|
|
|
droppedElement.remove();
|
|
|
|
|
|
|
|
|
|
// Add all events to the group
|
|
|
|
|
overlappingEvents.forEach(event => {
|
|
|
|
|
const eventElement = this.createEventElement(event);
|
|
|
|
|
this.positionEvent(eventElement, event);
|
|
|
|
|
this.overlapManager.addToEventGroup(groupContainer, eventElement);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
eventsLayer.appendChild(groupContainer);
|
|
|
|
|
} else if (overlapType === OverlapType.STACKING) {
|
|
|
|
|
// Handle stacking - sort by duration and stack shorter events on top
|
|
|
|
|
const sortedEvents = [...overlappingEvents].sort((a, b) => {
|
|
|
|
|
const durationA = new Date(a.end).getTime() - new Date(a.start).getTime();
|
|
|
|
|
const durationB = new Date(b.end).getTime() - new Date(b.start).getTime();
|
|
|
|
|
return durationB - durationA; // Longer duration first (background)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Remove overlapping events from DOM
|
|
|
|
|
const overlappingEventIds = new Set(overlappingEvents.map(e => e.id));
|
|
|
|
|
existingEvents
|
|
|
|
|
.filter(el => overlappingEventIds.has(el.dataset.eventId || ''))
|
|
|
|
|
.forEach(el => el.remove());
|
|
|
|
|
droppedElement.remove();
|
|
|
|
|
|
|
|
|
|
let underlyingElement: HTMLElement | null = null;
|
|
|
|
|
|
|
|
|
|
sortedEvents.forEach((event, index) => {
|
|
|
|
|
const eventElement = this.createEventElement(event);
|
|
|
|
|
this.positionEvent(eventElement, event);
|
|
|
|
|
|
|
|
|
|
if (index === 0) {
|
|
|
|
|
// First (longest duration) event renders normally at full width
|
2025-09-04 19:22:26 +02:00
|
|
|
eventsLayer.appendChild(eventElement);
|
2025-09-04 23:35:19 +02:00
|
|
|
underlyingElement = eventElement;
|
|
|
|
|
} else {
|
|
|
|
|
// Shorter events are stacked with margin-left offset and higher z-index
|
|
|
|
|
if (underlyingElement) {
|
|
|
|
|
this.overlapManager.createStackedEvent(eventElement, underlyingElement, index);
|
|
|
|
|
}
|
|
|
|
|
eventsLayer.appendChild(eventElement);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-09-04 19:22:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update element's dataset with new times after successful drop
|
|
|
|
|
*/
|
|
|
|
|
private updateElementDataset(element: HTMLElement, event: CalendarEvent): void {
|
|
|
|
|
element.dataset.start = event.start;
|
|
|
|
|
element.dataset.end = event.end;
|
|
|
|
|
|
|
|
|
|
// Update the time display
|
|
|
|
|
const timeElement = element.querySelector('swp-event-time');
|
|
|
|
|
if (timeElement) {
|
|
|
|
|
const startTime = this.formatTime(event.start);
|
|
|
|
|
const endTime = this.formatTime(event.end);
|
|
|
|
|
timeElement.textContent = `${startTime} - ${endTime}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Convert DOM element to CalendarEvent using its NEW position after drag
|
|
|
|
|
*/
|
|
|
|
|
private elementToCalendarEventWithNewPosition(element: HTMLElement, targetColumn: string): CalendarEvent | null {
|
|
|
|
|
const eventId = element.dataset.eventId;
|
|
|
|
|
const title = element.dataset.title;
|
|
|
|
|
const type = element.dataset.type;
|
|
|
|
|
const originalDuration = element.dataset.originalDuration;
|
|
|
|
|
|
|
|
|
|
if (!eventId || !title || !type) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate new start/end times based on current position
|
|
|
|
|
const currentTop = parseFloat(element.style.top) || 0;
|
|
|
|
|
const durationMinutes = originalDuration ? parseInt(originalDuration) : 60;
|
|
|
|
|
|
|
|
|
|
// Convert position to time
|
|
|
|
|
const gridSettings = calendarConfig.getGridSettings();
|
|
|
|
|
const hourHeight = gridSettings.hourHeight;
|
|
|
|
|
const dayStartHour = gridSettings.dayStartHour;
|
|
|
|
|
|
|
|
|
|
// Calculate minutes from grid start
|
|
|
|
|
const minutesFromGridStart = (currentTop / hourHeight) * 60;
|
|
|
|
|
const actualStartMinutes = (dayStartHour * 60) + minutesFromGridStart;
|
|
|
|
|
const actualEndMinutes = actualStartMinutes + durationMinutes;
|
|
|
|
|
|
|
|
|
|
// Create ISO date strings for the target column
|
|
|
|
|
const targetDate = new Date(targetColumn + 'T00:00:00');
|
|
|
|
|
const startDate = new Date(targetDate);
|
|
|
|
|
startDate.setMinutes(startDate.getMinutes() + actualStartMinutes);
|
|
|
|
|
|
|
|
|
|
const endDate = new Date(targetDate);
|
|
|
|
|
endDate.setMinutes(endDate.getMinutes() + actualEndMinutes);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: eventId,
|
|
|
|
|
title: title,
|
|
|
|
|
start: startDate.toISOString(),
|
|
|
|
|
end: endDate.toISOString(),
|
|
|
|
|
type: type,
|
|
|
|
|
allDay: false,
|
|
|
|
|
syncStatus: 'synced',
|
|
|
|
|
metadata: {
|
|
|
|
|
duration: durationMinutes
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 23:35:19 +02:00
|
|
|
/**
|
|
|
|
|
* Get the full stack chain for an event element
|
|
|
|
|
*/
|
|
|
|
|
private getFullStackChain(element: HTMLElement): HTMLElement[] {
|
|
|
|
|
const chain: HTMLElement[] = [];
|
|
|
|
|
|
|
|
|
|
// Find root of the stack chain (element with stackLevel 0 or no prev link)
|
|
|
|
|
let rootElement = element;
|
|
|
|
|
let rootLink = this.overlapManager.getStackLink(rootElement);
|
|
|
|
|
|
|
|
|
|
// Walk backwards to find root
|
|
|
|
|
while (rootLink?.prev) {
|
|
|
|
|
const prevElement = document.querySelector(`swp-event[data-event-id="${rootLink.prev}"]`) as HTMLElement;
|
|
|
|
|
if (!prevElement) break;
|
|
|
|
|
rootElement = prevElement;
|
|
|
|
|
rootLink = this.overlapManager.getStackLink(rootElement);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Collect entire chain from root forward
|
|
|
|
|
let currentElement = rootElement;
|
|
|
|
|
while (currentElement) {
|
|
|
|
|
chain.push(currentElement);
|
|
|
|
|
|
|
|
|
|
const currentLink = this.overlapManager.getStackLink(currentElement);
|
|
|
|
|
if (!currentLink?.next) break;
|
|
|
|
|
|
|
|
|
|
const nextElement = document.querySelector(`swp-event[data-event-id="${currentLink.next}"]`) as HTMLElement;
|
|
|
|
|
if (!nextElement) break;
|
|
|
|
|
currentElement = nextElement;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return chain;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 19:22:26 +02:00
|
|
|
/**
|
|
|
|
|
* Convert DOM element to CalendarEvent for overlap detection
|
|
|
|
|
*/
|
|
|
|
|
private elementToCalendarEvent(element: HTMLElement): CalendarEvent | null {
|
|
|
|
|
const eventId = element.dataset.eventId;
|
|
|
|
|
const title = element.dataset.title;
|
|
|
|
|
const start = element.dataset.start;
|
|
|
|
|
const end = element.dataset.end;
|
|
|
|
|
const type = element.dataset.type;
|
|
|
|
|
const duration = element.dataset.duration;
|
|
|
|
|
|
|
|
|
|
if (!eventId || !title || !start || !end || !type) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: eventId,
|
|
|
|
|
title: title,
|
|
|
|
|
start: start,
|
|
|
|
|
end: end,
|
|
|
|
|
type: type,
|
|
|
|
|
allDay: false,
|
|
|
|
|
syncStatus: 'synced', // Default to synced for existing events
|
|
|
|
|
metadata: {
|
|
|
|
|
duration: duration ? parseInt(duration) : 60
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-27 22:50:13 +02:00
|
|
|
/**
|
|
|
|
|
* Handle conversion to all-day event
|
|
|
|
|
*/
|
|
|
|
|
private handleConvertToAllDay(eventId: string, targetDate: string, headerRenderer: any): void {
|
|
|
|
|
if (!this.draggedClone) return;
|
|
|
|
|
|
|
|
|
|
// Only convert once
|
|
|
|
|
if (this.draggedClone.tagName === 'SWP-ALLDAY-EVENT') return;
|
|
|
|
|
|
|
|
|
|
// Transform clone to all-day format
|
|
|
|
|
this.transformCloneToAllDay(this.draggedClone, targetDate);
|
|
|
|
|
|
|
|
|
|
// Expand header if needed
|
|
|
|
|
headerRenderer.addToAllDay(this.draggedClone.parentElement);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Transform clone from timed to all-day event
|
|
|
|
|
*/
|
|
|
|
|
private transformCloneToAllDay(clone: HTMLElement, targetDate: string): void {
|
|
|
|
|
const calendarHeader = document.querySelector('swp-calendar-header');
|
|
|
|
|
if (!calendarHeader) return;
|
|
|
|
|
|
|
|
|
|
// Find all-day container
|
|
|
|
|
const allDayContainer = calendarHeader.querySelector('swp-allday-container');
|
|
|
|
|
if (!allDayContainer) return;
|
|
|
|
|
|
2025-09-01 23:37:47 +02:00
|
|
|
// Extract all original event data
|
2025-08-27 22:50:13 +02:00
|
|
|
const titleElement = clone.querySelector('swp-event-title');
|
|
|
|
|
const eventTitle = titleElement ? titleElement.textContent || 'Untitled' : 'Untitled';
|
|
|
|
|
|
2025-09-01 23:37:47 +02:00
|
|
|
const timeElement = clone.querySelector('swp-event-time');
|
|
|
|
|
const eventTime = timeElement ? timeElement.textContent || '' : '';
|
|
|
|
|
const eventDuration = timeElement ? timeElement.getAttribute('data-duration') || '' : '';
|
|
|
|
|
|
2025-08-27 22:50:13 +02:00
|
|
|
// Calculate column index
|
|
|
|
|
const dayHeaders = document.querySelectorAll('swp-day-header');
|
|
|
|
|
let columnIndex = 1;
|
|
|
|
|
dayHeaders.forEach((header, index) => {
|
|
|
|
|
if ((header as HTMLElement).dataset.date === targetDate) {
|
|
|
|
|
columnIndex = index + 1;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-01 23:37:47 +02:00
|
|
|
// Create all-day event with standardized data attributes
|
2025-08-27 22:50:13 +02:00
|
|
|
const allDayEvent = document.createElement('swp-allday-event');
|
|
|
|
|
allDayEvent.dataset.eventId = clone.dataset.eventId || '';
|
2025-09-01 23:37:47 +02:00
|
|
|
allDayEvent.dataset.title = eventTitle;
|
|
|
|
|
allDayEvent.dataset.start = `${targetDate}T${eventTime.split(' - ')[0]}:00`;
|
|
|
|
|
allDayEvent.dataset.end = `${targetDate}T${eventTime.split(' - ')[1]}:00`;
|
2025-08-27 22:50:13 +02:00
|
|
|
allDayEvent.dataset.type = clone.dataset.type || 'work';
|
2025-09-01 23:37:47 +02:00
|
|
|
allDayEvent.dataset.duration = eventDuration;
|
2025-08-27 22:50:13 +02:00
|
|
|
allDayEvent.textContent = eventTitle;
|
|
|
|
|
|
|
|
|
|
// Position in grid
|
|
|
|
|
(allDayEvent as HTMLElement).style.gridColumn = columnIndex.toString();
|
2025-09-01 23:37:47 +02:00
|
|
|
// grid-row will be set by checkAndAnimateAllDayHeight() based on actual position
|
2025-08-27 22:50:13 +02:00
|
|
|
|
|
|
|
|
// Remove original clone
|
|
|
|
|
if (clone.parentElement) {
|
|
|
|
|
clone.parentElement.removeChild(clone);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add to all-day container
|
|
|
|
|
allDayContainer.appendChild(allDayEvent);
|
|
|
|
|
|
|
|
|
|
// Update reference
|
|
|
|
|
this.draggedClone = allDayEvent;
|
2025-09-01 23:37:47 +02:00
|
|
|
|
|
|
|
|
// Check if height animation is needed
|
2025-09-03 18:15:33 +02:00
|
|
|
this.triggerAllDayHeightAnimation();
|
2025-08-27 22:50:13 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-01 23:37:47 +02:00
|
|
|
|
|
|
|
|
|
2025-08-27 22:50:13 +02:00
|
|
|
/**
|
|
|
|
|
* 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);
|
2025-08-20 00:39:31 +02:00
|
|
|
}
|
2025-08-29 22:49:53 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Convert dragged clone to all-day event preview
|
|
|
|
|
*/
|
|
|
|
|
private convertToAllDayPreview(targetDate: string): void {
|
|
|
|
|
if (!this.draggedClone) return;
|
|
|
|
|
|
|
|
|
|
// Only convert once
|
|
|
|
|
if (this.draggedClone.tagName === 'SWP-ALLDAY-EVENT') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Transform clone to all-day format
|
|
|
|
|
this.transformCloneToAllDay(this.draggedClone, targetDate);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Move all-day event to a new date container
|
|
|
|
|
*/
|
|
|
|
|
private moveAllDayToNewDate(targetDate: string): void {
|
|
|
|
|
if (!this.draggedClone) return;
|
|
|
|
|
|
|
|
|
|
const calendarHeader = document.querySelector('swp-calendar-header');
|
|
|
|
|
if (!calendarHeader) return;
|
|
|
|
|
|
|
|
|
|
// Find the all-day container
|
|
|
|
|
const allDayContainer = calendarHeader.querySelector('swp-allday-container');
|
|
|
|
|
if (!allDayContainer) return;
|
|
|
|
|
|
|
|
|
|
// Calculate new column index
|
|
|
|
|
const dayHeaders = document.querySelectorAll('swp-day-header');
|
|
|
|
|
let columnIndex = 1;
|
|
|
|
|
dayHeaders.forEach((header, index) => {
|
|
|
|
|
if ((header as HTMLElement).dataset.date === targetDate) {
|
|
|
|
|
columnIndex = index + 1;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Update grid column position
|
|
|
|
|
(this.draggedClone as HTMLElement).style.gridColumn = columnIndex.toString();
|
|
|
|
|
|
|
|
|
|
// Move to all-day container if not already there
|
|
|
|
|
if (this.draggedClone.parentElement !== allDayContainer) {
|
|
|
|
|
allDayContainer.appendChild(this.draggedClone);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
2025-09-03 20:04:47 +02:00
|
|
|
renderEvents(events: CalendarEvent[], container: HTMLElement): void {
|
2025-08-07 00:15:44 +02:00
|
|
|
|
2025-08-12 00:31:02 +02:00
|
|
|
// NOTE: Removed clearEvents() to support sliding animation
|
|
|
|
|
// With sliding animation, multiple grid containers exist simultaneously
|
|
|
|
|
// clearEvents() would remove events from all containers, breaking the animation
|
|
|
|
|
// Events are now rendered directly into the new container without clearing
|
2025-08-07 00:15:44 +02:00
|
|
|
|
2025-08-24 00:13:07 +02:00
|
|
|
// Separate all-day events from regular events
|
|
|
|
|
const allDayEvents = events.filter(event => event.allDay);
|
|
|
|
|
const regularEvents = events.filter(event => !event.allDay);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Always call renderAllDayEvents to ensure height is set correctly (even to 0)
|
2025-09-03 20:04:47 +02:00
|
|
|
this.renderAllDayEvents(allDayEvents, container);
|
2025-08-07 00:15:44 +02:00
|
|
|
|
2025-08-24 00:13:07 +02:00
|
|
|
// Find columns in the specific container for regular events
|
2025-08-16 00:51:12 +02:00
|
|
|
const columns = this.getColumns(container);
|
2025-08-13 23:05:58 +02:00
|
|
|
|
|
|
|
|
columns.forEach(column => {
|
2025-08-24 00:13:07 +02:00
|
|
|
const columnEvents = this.getEventsForColumn(column, regularEvents);
|
2025-08-07 00:15:44 +02:00
|
|
|
|
2025-08-13 23:05:58 +02:00
|
|
|
const eventsLayer = column.querySelector('swp-events-layer');
|
|
|
|
|
if (eventsLayer) {
|
2025-09-04 23:35:19 +02:00
|
|
|
// Render events først, så vi kan få deres pixel positioner
|
|
|
|
|
const renderedElements: HTMLElement[] = [];
|
|
|
|
|
columnEvents.forEach(event => {
|
|
|
|
|
this.renderEvent(event, eventsLayer);
|
|
|
|
|
const eventElement = eventsLayer.querySelector(`swp-event[data-event-id="${event.id}"]`) as HTMLElement;
|
|
|
|
|
if (eventElement) {
|
|
|
|
|
renderedElements.push(eventElement);
|
2025-09-04 00:16:35 +02:00
|
|
|
}
|
2025-08-13 23:05:58 +02:00
|
|
|
});
|
|
|
|
|
|
2025-09-04 23:35:19 +02:00
|
|
|
// Nu detect overlaps baseret på pixel positioner
|
|
|
|
|
this.detectAndGroupInitialEvents(renderedElements, eventsLayer);
|
|
|
|
|
|
2025-08-13 23:05:58 +02:00
|
|
|
// Debug: Verify events were actually added
|
2025-09-04 19:22:26 +02:00
|
|
|
const renderedEvents = eventsLayer.querySelectorAll('swp-event, swp-event-group');
|
2025-08-07 00:15:44 +02:00
|
|
|
} else {
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-16 00:51:12 +02:00
|
|
|
// Abstract methods that subclasses must implement
|
|
|
|
|
protected abstract getColumns(container: HTMLElement): HTMLElement[];
|
2025-08-13 23:05:58 +02:00
|
|
|
protected abstract getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[];
|
|
|
|
|
|
2025-08-24 00:13:07 +02:00
|
|
|
/**
|
|
|
|
|
* Render all-day events in the header row 2
|
|
|
|
|
*/
|
2025-09-03 20:04:47 +02:00
|
|
|
protected renderAllDayEvents(allDayEvents: CalendarEvent[], container: HTMLElement): void {
|
2025-08-24 00:13:07 +02:00
|
|
|
|
|
|
|
|
// Find the calendar header
|
|
|
|
|
const calendarHeader = container.querySelector('swp-calendar-header');
|
|
|
|
|
if (!calendarHeader) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-31 23:48:34 +02:00
|
|
|
// Find the all-day container (should always exist now)
|
2025-08-26 00:05:42 +02:00
|
|
|
const allDayContainer = calendarHeader.querySelector('swp-allday-container') as HTMLElement;
|
|
|
|
|
if (!allDayContainer) {
|
2025-08-31 23:48:34 +02:00
|
|
|
console.warn('All-day container not found - this should not happen');
|
2025-08-26 00:05:42 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear existing events
|
|
|
|
|
allDayContainer.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
if (allDayEvents.length === 0) {
|
2025-08-31 23:48:34 +02:00
|
|
|
// No events - container exists but is empty and hidden
|
2025-08-26 00:05:42 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build date to column mapping
|
2025-08-24 00:13:07 +02:00
|
|
|
const dayHeaders = calendarHeader.querySelectorAll('swp-day-header');
|
|
|
|
|
const dateToColumnMap = new Map<string, number>();
|
|
|
|
|
|
|
|
|
|
dayHeaders.forEach((header, index) => {
|
|
|
|
|
const dateStr = (header as any).dataset.date;
|
|
|
|
|
if (dateStr) {
|
|
|
|
|
dateToColumnMap.set(dateStr, index + 1); // 1-based column index
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-26 00:05:42 +02:00
|
|
|
// Calculate grid spans for all events
|
|
|
|
|
const eventSpans = allDayEvents.map(event => ({
|
|
|
|
|
event,
|
|
|
|
|
span: this.calculateEventGridSpan(event, dateToColumnMap)
|
|
|
|
|
})).filter(item => item.span.columnSpan > 0); // Remove events outside visible range
|
|
|
|
|
|
|
|
|
|
// Simple row assignment using overlap detection
|
|
|
|
|
const eventPlacements: Array<{ event: CalendarEvent, span: { startColumn: number, columnSpan: number }, row: number }> = [];
|
2025-08-24 00:13:07 +02:00
|
|
|
|
2025-08-26 00:05:42 +02:00
|
|
|
eventSpans.forEach(eventItem => {
|
|
|
|
|
let assignedRow = 1;
|
2025-08-24 00:13:07 +02:00
|
|
|
|
2025-08-26 00:05:42 +02:00
|
|
|
// Find first row where this event doesn't overlap with any existing event
|
|
|
|
|
while (true) {
|
|
|
|
|
const rowEvents = eventPlacements.filter(item => item.row === assignedRow);
|
|
|
|
|
const hasOverlap = rowEvents.some(rowEvent =>
|
|
|
|
|
this.eventsOverlap(eventItem.span, rowEvent.span)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!hasOverlap) {
|
|
|
|
|
break; // Found available row
|
|
|
|
|
}
|
|
|
|
|
assignedRow++;
|
2025-08-24 00:13:07 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-26 00:05:42 +02:00
|
|
|
eventPlacements.push({
|
|
|
|
|
event: eventItem.event,
|
|
|
|
|
span: eventItem.span,
|
|
|
|
|
row: assignedRow
|
|
|
|
|
});
|
2025-08-24 00:13:07 +02:00
|
|
|
});
|
|
|
|
|
|
2025-08-26 00:05:42 +02:00
|
|
|
// Get max row needed
|
|
|
|
|
const maxRow = Math.max(...eventPlacements.map(item => item.row), 1);
|
2025-08-25 22:05:57 +02:00
|
|
|
|
2025-08-26 00:05:42 +02:00
|
|
|
// Place events directly in the single container
|
|
|
|
|
eventPlacements.forEach(({ event, span, row }) => {
|
|
|
|
|
// Create the all-day event element
|
|
|
|
|
const allDayEvent = document.createElement('swp-allday-event');
|
|
|
|
|
allDayEvent.textContent = event.title;
|
2025-09-01 23:37:47 +02:00
|
|
|
|
|
|
|
|
// Set data attributes directly from CalendarEvent
|
|
|
|
|
allDayEvent.dataset.eventId = event.id;
|
|
|
|
|
allDayEvent.dataset.title = event.title;
|
|
|
|
|
allDayEvent.dataset.start = event.start;
|
|
|
|
|
allDayEvent.dataset.end = event.end;
|
|
|
|
|
allDayEvent.dataset.type = event.type;
|
|
|
|
|
allDayEvent.dataset.duration = event.metadata?.duration?.toString() || '60';
|
2025-08-26 00:05:42 +02:00
|
|
|
|
|
|
|
|
// Set grid position (column and row)
|
|
|
|
|
(allDayEvent as HTMLElement).style.gridColumn = span.columnSpan > 1
|
|
|
|
|
? `${span.startColumn} / span ${span.columnSpan}`
|
|
|
|
|
: `${span.startColumn}`;
|
|
|
|
|
(allDayEvent as HTMLElement).style.gridRow = row.toString();
|
|
|
|
|
|
|
|
|
|
// Use event metadata for color if available
|
|
|
|
|
if (event.metadata?.color) {
|
|
|
|
|
(allDayEvent as HTMLElement).style.backgroundColor = event.metadata.color;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
allDayContainer.appendChild(allDayEvent);
|
|
|
|
|
|
2025-08-24 00:13:07 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-03 20:04:47 +02:00
|
|
|
protected renderEvent(event: CalendarEvent, container: Element): void {
|
2025-08-07 00:15:44 +02:00
|
|
|
const eventElement = document.createElement('swp-event');
|
|
|
|
|
eventElement.dataset.eventId = event.id;
|
2025-09-01 23:37:47 +02:00
|
|
|
eventElement.dataset.title = event.title;
|
|
|
|
|
eventElement.dataset.start = event.start;
|
|
|
|
|
eventElement.dataset.end = event.end;
|
2025-08-07 00:15:44 +02:00
|
|
|
eventElement.dataset.type = event.type;
|
2025-09-01 23:37:47 +02:00
|
|
|
eventElement.dataset.duration = event.metadata?.duration?.toString() || '60';
|
2025-08-07 00:15:44 +02:00
|
|
|
|
|
|
|
|
// Calculate position based on time
|
2025-09-03 20:04:47 +02:00
|
|
|
const position = this.calculateEventPosition(event);
|
2025-08-07 00:15:44 +02:00
|
|
|
eventElement.style.position = 'absolute';
|
|
|
|
|
eventElement.style.top = `${position.top + 1}px`;
|
2025-08-22 22:57:35 +02:00
|
|
|
eventElement.style.height = `${position.height - 3}px`; //adjusted so bottom does not cover horizontal time lines.
|
2025-08-07 00:15:44 +02:00
|
|
|
|
2025-08-21 22:09:15 +02:00
|
|
|
// Color is now handled by CSS classes based on data-type attribute
|
2025-08-07 00:15:44 +02:00
|
|
|
|
2025-09-03 18:15:33 +02:00
|
|
|
// Format time for display using unified method
|
|
|
|
|
const startTime = this.formatTime(event.start);
|
|
|
|
|
const endTime = this.formatTime(event.end);
|
2025-08-27 22:50:13 +02:00
|
|
|
|
|
|
|
|
// Calculate duration in minutes
|
|
|
|
|
const startDate = new Date(event.start);
|
|
|
|
|
const endDate = new Date(event.end);
|
|
|
|
|
const durationMinutes = (endDate.getTime() - startDate.getTime()) / (1000 * 60);
|
2025-08-07 00:15:44 +02:00
|
|
|
|
|
|
|
|
// Create event content
|
|
|
|
|
eventElement.innerHTML = `
|
2025-08-27 22:50:13 +02:00
|
|
|
<swp-event-time data-duration="${durationMinutes}">${startTime} - ${endTime}</swp-event-time>
|
2025-08-07 00:15:44 +02:00
|
|
|
<swp-event-title>${event.title}</swp-event-title>
|
|
|
|
|
`;
|
2025-08-27 22:50:13 +02:00
|
|
|
|
2025-08-07 00:15:44 +02:00
|
|
|
|
|
|
|
|
container.appendChild(eventElement);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-03 20:04:47 +02:00
|
|
|
protected calculateEventPosition(event: CalendarEvent): { top: number; height: number } {
|
2025-08-07 00:15:44 +02:00
|
|
|
const startDate = new Date(event.start);
|
|
|
|
|
const endDate = new Date(event.end);
|
|
|
|
|
|
2025-09-03 20:04:47 +02:00
|
|
|
const gridSettings = calendarConfig.getGridSettings();
|
2025-08-09 01:16:04 +02:00
|
|
|
const dayStartHour = gridSettings.dayStartHour;
|
|
|
|
|
const hourHeight = gridSettings.hourHeight;
|
2025-08-07 00:15:44 +02:00
|
|
|
|
2025-09-03 20:48:23 +02:00
|
|
|
// Calculate minutes from midnight
|
2025-08-07 00:15:44 +02:00
|
|
|
const startMinutes = startDate.getHours() * 60 + startDate.getMinutes();
|
|
|
|
|
const endMinutes = endDate.getHours() * 60 + endDate.getMinutes();
|
|
|
|
|
const dayStartMinutes = dayStartHour * 60;
|
|
|
|
|
|
2025-09-03 20:48:23 +02:00
|
|
|
// Calculate top position relative to visible grid start
|
|
|
|
|
// If dayStartHour=6 and event starts at 09:00 (540 min), then:
|
|
|
|
|
// top = ((540 - 360) / 60) * hourHeight = 3 * hourHeight (3 hours from grid start)
|
2025-08-07 00:15:44 +02:00
|
|
|
const top = ((startMinutes - dayStartMinutes) / 60) * hourHeight;
|
|
|
|
|
|
2025-09-03 20:48:23 +02:00
|
|
|
// Calculate height based on event duration
|
2025-08-07 00:15:44 +02:00
|
|
|
const durationMinutes = endMinutes - startMinutes;
|
|
|
|
|
const height = (durationMinutes / 60) * hourHeight;
|
|
|
|
|
|
|
|
|
|
return { top, height };
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-26 00:05:42 +02:00
|
|
|
/**
|
|
|
|
|
* Calculate grid column span for event
|
|
|
|
|
*/
|
|
|
|
|
private calculateEventGridSpan(event: CalendarEvent, dateToColumnMap: Map<string, number>): { startColumn: number, columnSpan: number } {
|
|
|
|
|
const startDate = new Date(event.start);
|
|
|
|
|
const endDate = new Date(event.end);
|
2025-09-03 18:38:52 +02:00
|
|
|
const startDateKey = DateCalculator.formatISODate(startDate);
|
2025-08-26 00:05:42 +02:00
|
|
|
const startColumn = dateToColumnMap.get(startDateKey);
|
|
|
|
|
|
|
|
|
|
if (!startColumn) {
|
|
|
|
|
return { startColumn: 0, columnSpan: 0 }; // Event outside visible range
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate span by checking each day
|
|
|
|
|
let endColumn = startColumn;
|
|
|
|
|
const currentDate = new Date(startDate);
|
|
|
|
|
|
|
|
|
|
while (currentDate <= endDate) {
|
|
|
|
|
currentDate.setDate(currentDate.getDate() + 1);
|
2025-09-03 18:38:52 +02:00
|
|
|
const dateKey = DateCalculator.formatISODate(currentDate);
|
2025-08-26 00:05:42 +02:00
|
|
|
const col = dateToColumnMap.get(dateKey);
|
|
|
|
|
if (col) {
|
|
|
|
|
endColumn = col;
|
|
|
|
|
} else {
|
|
|
|
|
break; // Event extends beyond visible range
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const columnSpan = endColumn - startColumn + 1;
|
|
|
|
|
return { startColumn, columnSpan };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if two events overlap in columns
|
|
|
|
|
*/
|
|
|
|
|
private eventsOverlap(event1Span: { startColumn: number, columnSpan: number }, event2Span: { startColumn: number, columnSpan: number }): boolean {
|
|
|
|
|
const event1End = event1Span.startColumn + event1Span.columnSpan - 1;
|
|
|
|
|
const event2End = event2Span.startColumn + event2Span.columnSpan - 1;
|
|
|
|
|
|
|
|
|
|
return !(event1End < event2Span.startColumn || event2End < event1Span.startColumn);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 20:32:25 +02:00
|
|
|
|
2025-09-04 00:16:35 +02:00
|
|
|
/**
|
|
|
|
|
* Render column sharing group with flexbox container
|
|
|
|
|
*/
|
|
|
|
|
protected renderColumnSharingGroup(group: any, container: Element): void {
|
|
|
|
|
const groupContainer = this.overlapManager.createEventGroup(group.events, group.position);
|
|
|
|
|
|
|
|
|
|
// Render each event in the group
|
|
|
|
|
group.events.forEach((event: CalendarEvent) => {
|
|
|
|
|
const eventElement = this.createEventElement(event);
|
|
|
|
|
this.overlapManager.addToEventGroup(groupContainer, eventElement);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
container.appendChild(groupContainer);
|
|
|
|
|
|
|
|
|
|
// Emit event for debugging/logging
|
|
|
|
|
eventBus.emit('overlap:group-created', {
|
|
|
|
|
type: 'column_sharing',
|
|
|
|
|
eventCount: group.events.length,
|
|
|
|
|
events: group.events.map((e: CalendarEvent) => e.id)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-04 19:22:26 +02:00
|
|
|
* Render stacked events with margin-left offset
|
2025-09-04 00:16:35 +02:00
|
|
|
*/
|
|
|
|
|
protected renderStackedEvents(group: any, container: Element): void {
|
|
|
|
|
// Sort events by duration - longer events render first (background), shorter events on top
|
|
|
|
|
// This way shorter events are more visible and get higher z-index
|
|
|
|
|
const sortedEvents = [...group.events].sort((a, b) => {
|
|
|
|
|
const durationA = new Date(a.end).getTime() - new Date(a.start).getTime();
|
|
|
|
|
const durationB = new Date(b.end).getTime() - new Date(b.start).getTime();
|
|
|
|
|
return durationB - durationA; // Longer duration first (background)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let underlyingElement: HTMLElement | null = null;
|
|
|
|
|
|
|
|
|
|
sortedEvents.forEach((event: CalendarEvent, index: number) => {
|
|
|
|
|
const eventElement = this.createEventElement(event);
|
|
|
|
|
this.positionEvent(eventElement, event);
|
|
|
|
|
|
|
|
|
|
if (index === 0) {
|
|
|
|
|
// First (longest duration) event renders normally at full width - UNCHANGED
|
|
|
|
|
container.appendChild(eventElement);
|
|
|
|
|
underlyingElement = eventElement;
|
|
|
|
|
} else {
|
2025-09-04 19:22:26 +02:00
|
|
|
// Shorter events are stacked with margin-left offset and higher z-index
|
|
|
|
|
// Each subsequent event gets more margin: 15px, 30px, 45px, etc.
|
2025-09-04 23:35:19 +02:00
|
|
|
// Use simplified stacking - no complex chain tracking
|
|
|
|
|
this.overlapManager.createStackedEvent(eventElement, underlyingElement!, index);
|
2025-09-04 00:16:35 +02:00
|
|
|
container.appendChild(eventElement);
|
|
|
|
|
// DO NOT update underlyingElement - keep it as the longest event
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Emit event for debugging/logging
|
|
|
|
|
eventBus.emit('overlap:events-stacked', {
|
|
|
|
|
type: 'stacking',
|
|
|
|
|
eventCount: group.events.length,
|
|
|
|
|
events: group.events.map((e: CalendarEvent) => e.id)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create event element without positioning
|
|
|
|
|
*/
|
|
|
|
|
protected createEventElement(event: CalendarEvent): HTMLElement {
|
|
|
|
|
const eventElement = document.createElement('swp-event');
|
|
|
|
|
eventElement.dataset.eventId = event.id;
|
|
|
|
|
eventElement.dataset.title = event.title;
|
|
|
|
|
eventElement.dataset.start = event.start;
|
|
|
|
|
eventElement.dataset.end = event.end;
|
|
|
|
|
eventElement.dataset.type = event.type;
|
|
|
|
|
eventElement.dataset.duration = event.metadata?.duration?.toString() || '60';
|
|
|
|
|
|
|
|
|
|
// Format time for display using unified method
|
|
|
|
|
const startTime = this.formatTime(event.start);
|
|
|
|
|
const endTime = this.formatTime(event.end);
|
|
|
|
|
|
|
|
|
|
// Calculate duration in minutes
|
|
|
|
|
const startDate = new Date(event.start);
|
|
|
|
|
const endDate = new Date(event.end);
|
|
|
|
|
const durationMinutes = (endDate.getTime() - startDate.getTime()) / (1000 * 60);
|
|
|
|
|
|
|
|
|
|
// Create event content
|
|
|
|
|
eventElement.innerHTML = `
|
|
|
|
|
<swp-event-time data-duration="${durationMinutes}">${startTime} - ${endTime}</swp-event-time>
|
|
|
|
|
<swp-event-title>${event.title}</swp-event-title>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
return eventElement;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Position event element
|
|
|
|
|
*/
|
|
|
|
|
protected positionEvent(eventElement: HTMLElement, event: CalendarEvent): void {
|
|
|
|
|
const position = this.calculateEventPosition(event);
|
|
|
|
|
eventElement.style.position = 'absolute';
|
|
|
|
|
eventElement.style.top = `${position.top + 1}px`;
|
|
|
|
|
eventElement.style.height = `${position.height - 3}px`;
|
|
|
|
|
eventElement.style.left = '2px';
|
|
|
|
|
eventElement.style.right = '2px';
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-16 00:51:12 +02:00
|
|
|
clearEvents(container?: HTMLElement): void {
|
2025-09-04 19:22:26 +02:00
|
|
|
const selector = 'swp-event, swp-event-group';
|
2025-09-04 00:16:35 +02:00
|
|
|
const existingEvents = container
|
2025-08-16 00:51:12 +02:00
|
|
|
? container.querySelectorAll(selector)
|
|
|
|
|
: document.querySelectorAll(selector);
|
|
|
|
|
|
2025-08-07 00:15:44 +02:00
|
|
|
existingEvents.forEach(event => event.remove());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Date-based event renderer
|
|
|
|
|
*/
|
|
|
|
|
export class DateEventRenderer extends BaseEventRenderer {
|
2025-09-03 20:04:47 +02:00
|
|
|
constructor(dateCalculator?: DateCalculator) {
|
|
|
|
|
super(dateCalculator);
|
2025-08-27 23:56:38 +02:00
|
|
|
this.setupDragEventListeners();
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-16 00:51:12 +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;
|
2025-08-20 00:39:31 +02:00
|
|
|
if (!columnDate) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
2025-08-13 23:05:58 +02:00
|
|
|
|
|
|
|
|
const columnEvents = events.filter(event => {
|
|
|
|
|
const eventDate = new Date(event.start);
|
2025-09-03 18:38:52 +02:00
|
|
|
const eventDateStr = DateCalculator.formatISODate(eventDate);
|
2025-08-20 00:39:31 +02:00
|
|
|
const matches = eventDateStr === columnDate;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return matches;
|
2025-08-13 23:05:58 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return columnEvents;
|
|
|
|
|
}
|
2025-08-07 00:15:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Resource-based event renderer
|
|
|
|
|
*/
|
|
|
|
|
export class ResourceEventRenderer extends BaseEventRenderer {
|
2025-08-16 00:51:12 +02:00
|
|
|
protected getColumns(container: HTMLElement): HTMLElement[] {
|
|
|
|
|
const columns = container.querySelectorAll('swp-resource-column');
|
2025-08-13 23:05:58 +02:00
|
|
|
return Array.from(columns) as HTMLElement[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[] {
|
|
|
|
|
const resourceName = column.dataset.resource;
|
|
|
|
|
if (!resourceName) return [];
|
|
|
|
|
|
|
|
|
|
const columnEvents = events.filter(event => {
|
|
|
|
|
return event.resource?.name === resourceName;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return columnEvents;
|
|
|
|
|
}
|
2025-08-07 00:15:44 +02:00
|
|
|
}
|