2025-08-07 00:15:44 +02:00
|
|
|
// Event rendering strategy interface and implementations
|
|
|
|
|
|
|
|
|
|
import { CalendarEvent } from '../types/CalendarTypes';
|
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-09 14:35:21 +02:00
|
|
|
import { OverlapDetector, OverlapResult, EventId } from '../utils/OverlapDetector';
|
2025-09-10 22:36:11 +02:00
|
|
|
import { SwpEventElement, SwpAllDayEventElement } from '../elements/SwpEventElement';
|
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-08-27 22:50:13 +02:00
|
|
|
|
|
|
|
|
// Drag and drop state
|
|
|
|
|
private draggedClone: HTMLElement | null = null;
|
|
|
|
|
private originalEvent: HTMLElement | null = null;
|
2025-09-09 14:35:21 +02:00
|
|
|
|
2025-09-10 00:10:12 +02:00
|
|
|
// Resize manager
|
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-08-27 22:50:13 +02:00
|
|
|
}
|
2025-09-09 14:35:21 +02:00
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
// NEW OVERLAP DETECTION SYSTEM
|
|
|
|
|
// All new functions prefixed with new_
|
|
|
|
|
// ============================================
|
|
|
|
|
|
|
|
|
|
protected overlapDetector = new OverlapDetector();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Ny hovedfunktion til at håndtere event overlaps
|
|
|
|
|
* @param events - Events der skal renderes i kolonnen
|
|
|
|
|
* @param container - Container element at rendere i
|
|
|
|
|
*/
|
|
|
|
|
protected new_handleEventOverlaps(events: CalendarEvent[], container: HTMLElement): void {
|
|
|
|
|
if (events.length === 0) return;
|
|
|
|
|
|
|
|
|
|
if (events.length === 1) {
|
|
|
|
|
const element = this.renderEvent(events[0]);
|
|
|
|
|
container.appendChild(element);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-09 17:15:06 +02:00
|
|
|
// Track hvilke events der allerede er blevet processeret
|
|
|
|
|
const processedEvents = new Set<string>();
|
|
|
|
|
|
2025-09-09 14:35:21 +02:00
|
|
|
// Gå gennem hvert event og find overlaps
|
|
|
|
|
events.forEach((currentEvent, index) => {
|
2025-09-09 17:15:06 +02:00
|
|
|
// Skip events der allerede er processeret som del af en overlap gruppe
|
|
|
|
|
if (processedEvents.has(currentEvent.id)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-09 14:35:21 +02:00
|
|
|
const remainingEvents = events.slice(index + 1);
|
|
|
|
|
const overlappingEvents = this.overlapDetector.resolveOverlap(currentEvent, remainingEvents);
|
|
|
|
|
|
|
|
|
|
if (overlappingEvents.length > 0) {
|
|
|
|
|
// Der er overlaps - opret stack links
|
|
|
|
|
const result = this.overlapDetector.decorateWithStackLinks(currentEvent, overlappingEvents);
|
|
|
|
|
this.new_renderOverlappingEvents(result, container);
|
2025-09-09 17:15:06 +02:00
|
|
|
|
|
|
|
|
// Marker alle events i overlap gruppen som processeret
|
|
|
|
|
overlappingEvents.forEach(event => processedEvents.add(event.id));
|
2025-09-09 14:35:21 +02:00
|
|
|
} else {
|
|
|
|
|
// Intet overlap - render normalt
|
|
|
|
|
const element = this.renderEvent(currentEvent);
|
|
|
|
|
container.appendChild(element);
|
2025-09-09 17:15:06 +02:00
|
|
|
processedEvents.add(currentEvent.id);
|
2025-09-09 14:35:21 +02:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-09 14:35:21 +02:00
|
|
|
// Handle click (when drag threshold not reached)
|
|
|
|
|
eventBus.on('event:click', (event) => {
|
|
|
|
|
const { eventId, originalElement } = (event as CustomEvent).detail;
|
|
|
|
|
this.handleEventClick(eventId, originalElement);
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-27 22:50:13 +02:00
|
|
|
// Handle column change
|
|
|
|
|
eventBus.on('drag:column-change', (event) => {
|
|
|
|
|
const { eventId, newColumn } = (event as CustomEvent).detail;
|
|
|
|
|
this.handleColumnChange(eventId, newColumn);
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-10 22:36:11 +02:00
|
|
|
|
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-08-27 22:50:13 +02:00
|
|
|
}
|
2025-09-03 18:15:33 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 22:07:40 +02:00
|
|
|
/**
|
|
|
|
|
* Apply common drag styling to an element
|
|
|
|
|
*/
|
|
|
|
|
private applyDragStyling(element: HTMLElement): void {
|
|
|
|
|
element.style.position = 'absolute';
|
|
|
|
|
element.style.zIndex = '999999';
|
|
|
|
|
element.style.pointerEvents = 'none';
|
|
|
|
|
element.style.opacity = '0.8';
|
|
|
|
|
element.style.left = '2px';
|
|
|
|
|
element.style.right = '2px';
|
|
|
|
|
element.style.marginLeft = '0px';
|
|
|
|
|
element.style.width = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create event inner structure (swp-event-time and swp-event-title)
|
|
|
|
|
*/
|
|
|
|
|
private createEventInnerStructure(event: CalendarEvent): string {
|
|
|
|
|
const startTime = this.formatTime(event.start);
|
|
|
|
|
const endTime = this.formatTime(event.end);
|
|
|
|
|
const durationMinutes = (event.end.getTime() - event.start.getTime()) / (1000 * 60);
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
<swp-event-time data-duration="${durationMinutes}">${startTime} - ${endTime}</swp-event-time>
|
|
|
|
|
<swp-event-title>${event.title}</swp-event-title>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Apply standard event positioning
|
|
|
|
|
*/
|
|
|
|
|
private applyEventPositioning(element: HTMLElement, top: number, height: number): void {
|
|
|
|
|
element.style.position = 'absolute';
|
|
|
|
|
element.style.top = `${top}px`;
|
|
|
|
|
element.style.height = `${height}px`;
|
|
|
|
|
element.style.left = '2px';
|
|
|
|
|
element.style.right = '2px';
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-27 22:50:13 +02:00
|
|
|
/**
|
|
|
|
|
* 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();
|
|
|
|
|
|
2025-09-10 22:07:40 +02:00
|
|
|
// Apply common drag styling
|
|
|
|
|
this.applyDragStyling(clone);
|
2025-08-27 22:50:13 +02:00
|
|
|
|
2025-09-10 22:07:40 +02:00
|
|
|
// Set height from original event
|
2025-09-04 19:22:26 +02:00
|
|
|
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-10 22:07:40 +02:00
|
|
|
|
|
|
|
|
//important as events can pile up, so they will still fire after event has been converted to another rendered type
|
|
|
|
|
if(clone.dataset.allDay == "true") return;
|
|
|
|
|
|
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
|
|
|
|
2025-09-10 00:33:39 +02:00
|
|
|
// Update dataset with reference date for performance
|
|
|
|
|
const referenceDate = new Date('1970-01-01T00:00:00');
|
|
|
|
|
const startDate = new Date(referenceDate);
|
|
|
|
|
startDate.setMinutes(startDate.getMinutes() + snappedStartMinutes);
|
|
|
|
|
|
|
|
|
|
const endDate = new Date(referenceDate);
|
|
|
|
|
endDate.setMinutes(endDate.getMinutes() + endTotalMinutes);
|
|
|
|
|
|
|
|
|
|
clone.dataset.start = startDate.toISOString();
|
|
|
|
|
clone.dataset.end = endDate.toISOString();
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
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 {
|
|
|
|
|
this.originalEvent = originalElement;
|
|
|
|
|
|
2025-09-09 17:30:44 +02:00
|
|
|
// Remove stacking styling during drag will be handled by new system
|
2025-09-04 19:22:26 +02:00
|
|
|
|
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-08-27 23:56:38 +02:00
|
|
|
|
|
|
|
|
if (!this.draggedClone || !this.originalEvent) {
|
2025-09-10 22:07:40 +02:00
|
|
|
console.warn('Missing draggedClone or originalEvent');
|
2025-08-27 23:56:38 +02:00
|
|
|
return;
|
|
|
|
|
}
|
2025-08-27 22:50:13 +02:00
|
|
|
|
2025-09-09 22:57:26 +02:00
|
|
|
// Check om original event var del af en stack
|
|
|
|
|
const originalStackLink = this.originalEvent.dataset.stackLink;
|
|
|
|
|
|
|
|
|
|
if (originalStackLink) {
|
|
|
|
|
try {
|
|
|
|
|
const stackData = JSON.parse(originalStackLink);
|
|
|
|
|
|
|
|
|
|
// Saml ALLE event IDs fra hele stack chain
|
|
|
|
|
const allStackEventIds: Set<string> = new Set();
|
|
|
|
|
|
|
|
|
|
// Recursive funktion til at traversere stack chain
|
|
|
|
|
const traverseStack = (linkData: any, visitedIds: Set<string>) => {
|
|
|
|
|
if (linkData.prev && !visitedIds.has(linkData.prev)) {
|
|
|
|
|
visitedIds.add(linkData.prev);
|
|
|
|
|
const prevElement = document.querySelector(`swp-time-grid [data-event-id="${linkData.prev}"]`) as HTMLElement;
|
|
|
|
|
if (prevElement?.dataset.stackLink) {
|
|
|
|
|
try {
|
|
|
|
|
const prevLinkData = JSON.parse(prevElement.dataset.stackLink);
|
|
|
|
|
traverseStack(prevLinkData, visitedIds);
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (linkData.next && !visitedIds.has(linkData.next)) {
|
|
|
|
|
visitedIds.add(linkData.next);
|
|
|
|
|
const nextElement = document.querySelector(`swp-time-grid [data-event-id="${linkData.next}"]`) as HTMLElement;
|
|
|
|
|
if (nextElement?.dataset.stackLink) {
|
|
|
|
|
try {
|
|
|
|
|
const nextLinkData = JSON.parse(nextElement.dataset.stackLink);
|
|
|
|
|
traverseStack(nextLinkData, visitedIds);
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Start traversering fra original event's stackLink
|
|
|
|
|
traverseStack(stackData, allStackEventIds);
|
|
|
|
|
|
|
|
|
|
// Fjern original eventId da det bliver flyttet
|
|
|
|
|
allStackEventIds.delete(eventId);
|
|
|
|
|
|
|
|
|
|
// Find alle stack events og fjern dem
|
|
|
|
|
const stackEvents: CalendarEvent[] = [];
|
|
|
|
|
let container: HTMLElement | null = null;
|
|
|
|
|
|
|
|
|
|
allStackEventIds.forEach(id => {
|
|
|
|
|
const element = document.querySelector(`swp-time-grid [data-event-id="${id}"]`) as HTMLElement;
|
|
|
|
|
if (element) {
|
|
|
|
|
// Gem container reference fra første element
|
|
|
|
|
if (!container) {
|
|
|
|
|
container = element.closest('swp-events-layer') as HTMLElement;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const event = this.elementToCalendarEvent(element);
|
|
|
|
|
if (event) {
|
|
|
|
|
stackEvents.push(event);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fjern elementet
|
|
|
|
|
element.remove();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Re-render stack events hvis vi fandt nogle
|
|
|
|
|
if (stackEvents.length > 0 && container) {
|
|
|
|
|
this.new_handleEventOverlaps(stackEvents, container);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('Failed to parse stackLink data:', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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-10 22:07:40 +02:00
|
|
|
// Update dataset with new times after successful drop (only for timed events)
|
2025-09-11 12:10:34 +02:00
|
|
|
if (this.draggedClone.dataset.displayType !== 'allday') {
|
2025-09-10 22:07:40 +02:00
|
|
|
const newEvent = this.elementToCalendarEvent(this.draggedClone);
|
|
|
|
|
if (newEvent) {
|
|
|
|
|
this.draggedClone.dataset.start = newEvent.start.toISOString();
|
|
|
|
|
this.draggedClone.dataset.end = newEvent.end.toISOString();
|
|
|
|
|
}
|
2025-09-09 18:03:37 +02:00
|
|
|
}
|
2025-08-27 22:50:13 +02:00
|
|
|
|
2025-09-09 14:35:21 +02:00
|
|
|
// Detect overlaps with other events in the target column and reposition if needed
|
2025-09-09 17:30:44 +02:00
|
|
|
this.handleDragDropOverlaps(this.draggedClone, finalColumn);
|
2025-09-09 22:57:26 +02:00
|
|
|
|
|
|
|
|
// Fjern stackLink data fra dropped element
|
|
|
|
|
if (this.draggedClone.dataset.stackLink) {
|
|
|
|
|
delete this.draggedClone.dataset.stackLink;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-27 22:50:13 +02:00
|
|
|
// Clean up
|
|
|
|
|
this.draggedClone = null;
|
|
|
|
|
this.originalEvent = null;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 19:22:26 +02:00
|
|
|
/**
|
2025-09-09 14:35:21 +02:00
|
|
|
* Handle event click (when drag threshold not reached)
|
2025-09-04 19:22:26 +02:00
|
|
|
*/
|
2025-09-09 14:35:21 +02:00
|
|
|
private handleEventClick(eventId: string, originalElement: HTMLElement): void {
|
|
|
|
|
console.log('handleEventClick:', eventId);
|
2025-09-04 20:32:25 +02:00
|
|
|
|
2025-09-09 14:35:21 +02:00
|
|
|
// Clean up any drag artifacts from failed drag attempt
|
|
|
|
|
if (this.draggedClone) {
|
|
|
|
|
this.draggedClone.remove();
|
|
|
|
|
this.draggedClone = null;
|
|
|
|
|
}
|
2025-09-04 20:32:25 +02:00
|
|
|
|
2025-09-09 14:35:21 +02:00
|
|
|
// Restore original element styling if it was modified
|
|
|
|
|
if (this.originalEvent) {
|
|
|
|
|
this.originalEvent.style.opacity = '';
|
|
|
|
|
this.originalEvent.style.userSelect = '';
|
|
|
|
|
this.originalEvent = null;
|
2025-09-04 20:32:25 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-09 14:35:21 +02:00
|
|
|
// Emit a clean click event for other components to handle
|
|
|
|
|
eventBus.emit('event:clicked', {
|
|
|
|
|
eventId: eventId,
|
|
|
|
|
element: originalElement
|
|
|
|
|
});
|
2025-09-04 20:32:25 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-09 17:30:44 +02:00
|
|
|
/**
|
|
|
|
|
* Handle overlap detection and re-rendering after drag-drop
|
|
|
|
|
*/
|
|
|
|
|
private handleDragDropOverlaps(droppedElement: HTMLElement, targetColumn: string): void {
|
|
|
|
|
const targetColumnElement = document.querySelector(`swp-day-column[data-date="${targetColumn}"]`);
|
|
|
|
|
if (!targetColumnElement) return;
|
|
|
|
|
|
|
|
|
|
const eventsLayer = targetColumnElement.querySelector('swp-events-layer') as HTMLElement;
|
|
|
|
|
if (!eventsLayer) return;
|
|
|
|
|
|
|
|
|
|
// Convert dropped element to CalendarEvent with new position
|
2025-09-10 00:33:39 +02:00
|
|
|
const droppedEvent = this.elementToCalendarEvent(droppedElement);
|
2025-09-09 17:30:44 +02:00
|
|
|
if (!droppedEvent) return;
|
|
|
|
|
|
|
|
|
|
// Get existing events in the column (excluding the dropped element)
|
|
|
|
|
const existingEvents = this.getEventsInColumn(eventsLayer, droppedElement.dataset.eventId);
|
|
|
|
|
|
|
|
|
|
// Find overlaps with the dropped event
|
|
|
|
|
const overlappingEvents = this.overlapDetector.resolveOverlap(droppedEvent, existingEvents);
|
|
|
|
|
|
|
|
|
|
if (overlappingEvents.length > 0) {
|
|
|
|
|
// Remove only affected events from DOM
|
|
|
|
|
const affectedEventIds = [droppedEvent.id, ...overlappingEvents.map(e => e.id)];
|
|
|
|
|
eventsLayer.querySelectorAll('swp-event').forEach(el => {
|
|
|
|
|
const eventId = (el as HTMLElement).dataset.eventId;
|
|
|
|
|
if (eventId && affectedEventIds.includes(eventId)) {
|
|
|
|
|
el.remove();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Re-render affected events with overlap handling
|
|
|
|
|
const affectedEvents = [droppedEvent, ...overlappingEvents];
|
|
|
|
|
this.new_handleEventOverlaps(affectedEvents, eventsLayer);
|
2025-09-09 18:03:37 +02:00
|
|
|
} else {
|
|
|
|
|
// Reset z-index for non-overlapping events
|
|
|
|
|
droppedElement.style.zIndex = '';
|
2025-09-09 17:30:44 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all events in a column as CalendarEvent objects
|
|
|
|
|
*/
|
|
|
|
|
private getEventsInColumn(eventsLayer: HTMLElement, excludeEventId?: string): CalendarEvent[] {
|
|
|
|
|
const eventElements = eventsLayer.querySelectorAll('swp-event');
|
|
|
|
|
const events: CalendarEvent[] = [];
|
|
|
|
|
|
|
|
|
|
eventElements.forEach(el => {
|
|
|
|
|
const element = el as HTMLElement;
|
|
|
|
|
const eventId = element.dataset.eventId;
|
|
|
|
|
|
|
|
|
|
// Skip the excluded event (e.g., the dropped event)
|
|
|
|
|
if (excludeEventId && eventId === excludeEventId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const event = this.elementToCalendarEvent(element);
|
|
|
|
|
if (event) {
|
|
|
|
|
events.push(event);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return events;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 19:22:26 +02:00
|
|
|
/**
|
2025-09-09 14:35:21 +02:00
|
|
|
* Remove event from any existing groups and cleanup empty containers
|
2025-09-09 17:30:44 +02:00
|
|
|
* In the new system, this is handled automatically by re-rendering overlaps
|
2025-09-04 19:22:26 +02:00
|
|
|
*/
|
2025-09-09 14:35:21 +02:00
|
|
|
private removeEventFromExistingGroups(eventElement: HTMLElement): void {
|
2025-09-09 17:30:44 +02:00
|
|
|
// With the new system, overlap relationships are recalculated on drop
|
|
|
|
|
// No need to manually track and remove from groups
|
2025-09-04 19:22:26 +02:00
|
|
|
}
|
2025-09-09 14:35:21 +02:00
|
|
|
|
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 {
|
2025-09-09 14:35:21 +02:00
|
|
|
element.dataset.start = event.start.toISOString();
|
|
|
|
|
element.dataset.end = event.end.toISOString();
|
2025-09-04 19:22:26 +02:00
|
|
|
|
|
|
|
|
// 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}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 00:33:39 +02:00
|
|
|
|
|
|
|
|
|
2025-09-04 19:22:26 +02:00
|
|
|
/**
|
2025-09-10 00:33:39 +02:00
|
|
|
* Convert DOM element to CalendarEvent - handles both normal and 1970 reference dates
|
2025-09-04 19:22:26 +02:00
|
|
|
*/
|
2025-09-10 00:33:39 +02:00
|
|
|
private elementToCalendarEvent(element: HTMLElement): CalendarEvent | null {
|
2025-09-04 19:22:26 +02:00
|
|
|
const eventId = element.dataset.eventId;
|
|
|
|
|
const title = element.dataset.title;
|
|
|
|
|
const type = element.dataset.type;
|
2025-09-10 00:33:39 +02:00
|
|
|
const start = element.dataset.start;
|
|
|
|
|
const end = element.dataset.end;
|
2025-09-04 19:22:26 +02:00
|
|
|
|
2025-09-10 00:33:39 +02:00
|
|
|
if (!eventId || !title || !type || !start || !end) {
|
2025-09-04 19:22:26 +02:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 00:33:39 +02:00
|
|
|
let startDate = new Date(start);
|
|
|
|
|
let endDate = new Date(end);
|
2025-09-04 19:22:26 +02:00
|
|
|
|
2025-09-10 00:33:39 +02:00
|
|
|
// Check if we have 1970 reference date (from drag operations)
|
|
|
|
|
if (startDate.getFullYear() === 1970) {
|
|
|
|
|
// Find the parent column to get the actual date
|
|
|
|
|
const columnElement = element.closest('swp-day-column') as HTMLElement;
|
|
|
|
|
if (columnElement && columnElement.dataset.date) {
|
2025-09-10 16:48:38 +02:00
|
|
|
const columnDate = new Date(columnElement.dataset.date);
|
2025-09-10 00:33:39 +02:00
|
|
|
|
|
|
|
|
// Keep the time portion from the 1970 dates, but use the column's date
|
|
|
|
|
startDate = new Date(
|
|
|
|
|
columnDate.getFullYear(),
|
|
|
|
|
columnDate.getMonth(),
|
|
|
|
|
columnDate.getDate(),
|
|
|
|
|
startDate.getHours(),
|
|
|
|
|
startDate.getMinutes()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
endDate = new Date(
|
|
|
|
|
columnDate.getFullYear(),
|
|
|
|
|
columnDate.getMonth(),
|
|
|
|
|
columnDate.getDate(),
|
|
|
|
|
endDate.getHours(),
|
|
|
|
|
endDate.getMinutes()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-04 19:22:26 +02:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: eventId,
|
|
|
|
|
title: title,
|
2025-09-09 14:35:21 +02:00
|
|
|
start: startDate,
|
|
|
|
|
end: endDate,
|
2025-09-04 19:22:26 +02:00
|
|
|
type: type,
|
|
|
|
|
allDay: false,
|
|
|
|
|
syncStatus: 'synced',
|
|
|
|
|
metadata: {
|
2025-09-10 16:48:38 +02:00
|
|
|
duration: element.dataset.duration
|
2025-09-04 19:22:26 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-27 22:50:13 +02:00
|
|
|
/**
|
|
|
|
|
* Handle conversion to all-day event
|
|
|
|
|
*/
|
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
|
|
|
|
|
|
|
|
|
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-09-12 00:36:02 +02:00
|
|
|
// Only handle regular (non-all-day) events
|
2025-08-24 00:13:07 +02:00
|
|
|
|
|
|
|
|
|
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-09-12 00:36:02 +02:00
|
|
|
const columnEvents = this.getEventsForColumn(column, events);
|
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-09 14:35:21 +02:00
|
|
|
// NY TILGANG: Kald vores nye overlap handling
|
|
|
|
|
this.new_handleEventOverlaps(columnEvents, eventsLayer as HTMLElement);
|
2025-08-07 00:15:44 +02:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
2025-09-09 14:35:21 +02:00
|
|
|
protected renderEvent(event: CalendarEvent): HTMLElement {
|
2025-09-10 22:36:11 +02:00
|
|
|
const swpEvent = SwpEventElement.fromCalendarEvent(event);
|
|
|
|
|
const eventElement = swpEvent.getElement();
|
2025-08-27 22:50:13 +02:00
|
|
|
|
2025-09-09 14:35:21 +02:00
|
|
|
// Setup resize handles on first mouseover only
|
|
|
|
|
eventElement.addEventListener('mouseover', () => {
|
|
|
|
|
if (eventElement.dataset.hasResizeHandlers !== 'true') {
|
|
|
|
|
eventElement.dataset.hasResizeHandlers = 'true';
|
|
|
|
|
}
|
|
|
|
|
}, { once: true });
|
2025-09-10 00:10:12 +02:00
|
|
|
|
2025-09-09 14:35:21 +02:00
|
|
|
return eventElement;
|
2025-08-07 00:15:44 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-03 20:04:47 +02:00
|
|
|
protected calculateEventPosition(event: CalendarEvent): { top: number; height: number } {
|
2025-08-07 00:15:44 +02:00
|
|
|
|
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-09-09 14:35:21 +02:00
|
|
|
const startMinutes = event.start.getHours() * 60 + event.start.getMinutes();
|
|
|
|
|
const endMinutes = event.end.getHours() * 60 + event.end.getMinutes();
|
2025-08-07 00:15:44 +02:00
|
|
|
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-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());
|
|
|
|
|
}
|
2025-09-09 14:35:21 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Renderer overlappende events baseret på OverlapResult
|
|
|
|
|
* @param result - OverlapResult med events og stack links
|
|
|
|
|
* @param container - Container at rendere i
|
|
|
|
|
*/
|
|
|
|
|
protected new_renderOverlappingEvents(result: OverlapResult, container: HTMLElement): void {
|
|
|
|
|
// Iterate direkte gennem stackLinks - allerede sorteret fra decorateWithStackLinks
|
|
|
|
|
for (const [eventId, stackLink] of result.stackLinks.entries()) {
|
|
|
|
|
const event = result.overlappingEvents.find(e => e.id === eventId);
|
|
|
|
|
if (!event) continue;
|
|
|
|
|
|
|
|
|
|
const element = this.renderEvent(event);
|
|
|
|
|
|
2025-09-09 22:57:26 +02:00
|
|
|
// Gem stack link information på DOM elementet
|
|
|
|
|
element.dataset.stackLink = JSON.stringify({
|
|
|
|
|
prev: stackLink.prev,
|
|
|
|
|
next: stackLink.next,
|
|
|
|
|
stackLevel: stackLink.stackLevel
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-09 14:35:21 +02:00
|
|
|
// Check om dette event deler kolonne med foregående (samme start tid)
|
|
|
|
|
if (stackLink.prev) {
|
|
|
|
|
const prevEvent = result.overlappingEvents.find(e => e.id === stackLink.prev);
|
|
|
|
|
if (prevEvent && prevEvent.start.getTime() === event.start.getTime()) {
|
|
|
|
|
// Samme start tid - del kolonne (side by side)
|
|
|
|
|
this.new_applyColumnSharingStyling([element]);
|
|
|
|
|
} else {
|
|
|
|
|
// Forskellige start tider - stack vertikalt
|
|
|
|
|
this.new_applyStackStyling(element, stackLink.stackLevel);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Første event i stack
|
|
|
|
|
this.new_applyStackStyling(element, stackLink.stackLevel);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
container.appendChild(element);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Applicerer stack styling (margin-left og z-index)
|
|
|
|
|
* @param element - Event element
|
|
|
|
|
* @param stackLevel - Stack niveau
|
|
|
|
|
*/
|
|
|
|
|
protected new_applyStackStyling(element: HTMLElement, stackLevel: number): void {
|
|
|
|
|
element.style.marginLeft = `${stackLevel * 15}px`;
|
|
|
|
|
element.style.zIndex = `${100 + stackLevel}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Applicerer column sharing styling (flexbox)
|
|
|
|
|
* @param elements - Event elements der skal dele plads
|
|
|
|
|
*/
|
|
|
|
|
protected new_applyColumnSharingStyling(elements: HTMLElement[]): void {
|
|
|
|
|
elements.forEach(element => {
|
|
|
|
|
element.style.flex = '1';
|
|
|
|
|
element.style.minWidth = '50px';
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-08-07 00:15:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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 => {
|
2025-09-09 14:35:21 +02:00
|
|
|
const eventDateStr = DateCalculator.formatISODate(event.start);
|
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-09-09 14:35:21 +02:00
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
// NEW OVERLAP DETECTION SYSTEM
|
|
|
|
|
// All new functions prefixed with new_
|
|
|
|
|
// ============================================
|
|
|
|
|
|
|
|
|
|
protected overlapDetector = new OverlapDetector();
|
|
|
|
|
|
2025-08-07 00:15:44 +02:00
|
|
|
}
|