2025-09-10 22:36:11 +02:00
|
|
|
import { CalendarEvent } from '../types/CalendarTypes';
|
|
|
|
|
import { calendarConfig } from '../core/CalendarConfig';
|
2025-09-12 22:21:56 +02:00
|
|
|
import { TimeFormatter } from '../utils/TimeFormatter';
|
2025-09-13 00:39:56 +02:00
|
|
|
import { PositionUtils } from '../utils/PositionUtils';
|
2025-09-10 22:36:11 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Abstract base class for event DOM elements
|
|
|
|
|
*/
|
|
|
|
|
export abstract class BaseEventElement {
|
|
|
|
|
protected element: HTMLElement;
|
|
|
|
|
protected event: CalendarEvent;
|
|
|
|
|
|
|
|
|
|
protected constructor(event: CalendarEvent) {
|
|
|
|
|
this.event = event;
|
|
|
|
|
this.element = this.createElement();
|
|
|
|
|
this.setDataAttributes();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create the underlying DOM element
|
|
|
|
|
*/
|
|
|
|
|
protected abstract createElement(): HTMLElement;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set standard data attributes on the element
|
|
|
|
|
*/
|
|
|
|
|
protected setDataAttributes(): void {
|
|
|
|
|
this.element.dataset.eventId = this.event.id;
|
|
|
|
|
this.element.dataset.title = this.event.title;
|
|
|
|
|
this.element.dataset.start = this.event.start.toISOString();
|
|
|
|
|
this.element.dataset.end = this.event.end.toISOString();
|
|
|
|
|
this.element.dataset.type = this.event.type;
|
|
|
|
|
this.element.dataset.duration = this.event.metadata?.duration?.toString() || '60';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the DOM element
|
|
|
|
|
*/
|
|
|
|
|
public getElement(): HTMLElement {
|
|
|
|
|
return this.element;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-12 22:21:56 +02:00
|
|
|
* Format time for display using TimeFormatter
|
2025-09-10 22:36:11 +02:00
|
|
|
*/
|
|
|
|
|
protected formatTime(date: Date): string {
|
2025-09-12 22:21:56 +02:00
|
|
|
return TimeFormatter.formatTime(date);
|
2025-09-10 22:36:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-13 00:39:56 +02:00
|
|
|
* Calculate event position for timed events using PositionUtils
|
2025-09-10 22:36:11 +02:00
|
|
|
*/
|
|
|
|
|
protected calculateEventPosition(): { top: number; height: number } {
|
2025-09-13 00:39:56 +02:00
|
|
|
return PositionUtils.calculateEventPosition(this.event.start, this.event.end);
|
2025-09-10 22:36:11 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Timed event element (swp-event)
|
|
|
|
|
*/
|
|
|
|
|
export class SwpEventElement extends BaseEventElement {
|
|
|
|
|
private constructor(event: CalendarEvent) {
|
|
|
|
|
super(event);
|
|
|
|
|
this.createInnerStructure();
|
|
|
|
|
this.applyPositioning();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected createElement(): HTMLElement {
|
|
|
|
|
return document.createElement('swp-event');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create inner HTML structure
|
|
|
|
|
*/
|
|
|
|
|
private createInnerStructure(): void {
|
2025-09-12 22:21:56 +02:00
|
|
|
const timeRange = TimeFormatter.formatTimeRange(this.event.start, this.event.end);
|
2025-09-10 22:36:11 +02:00
|
|
|
const durationMinutes = (this.event.end.getTime() - this.event.start.getTime()) / (1000 * 60);
|
|
|
|
|
|
|
|
|
|
this.element.innerHTML = `
|
2025-09-12 22:21:56 +02:00
|
|
|
<swp-event-time data-duration="${durationMinutes}">${timeRange}</swp-event-time>
|
2025-09-10 22:36:11 +02:00
|
|
|
<swp-event-title>${this.event.title}</swp-event-title>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Apply positioning styles
|
|
|
|
|
*/
|
|
|
|
|
private applyPositioning(): void {
|
|
|
|
|
const position = this.calculateEventPosition();
|
|
|
|
|
this.element.style.position = 'absolute';
|
|
|
|
|
this.element.style.top = `${position.top + 1}px`;
|
|
|
|
|
this.element.style.height = `${position.height - 3}px`;
|
|
|
|
|
this.element.style.left = '2px';
|
|
|
|
|
this.element.style.right = '2px';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Factory method to create a SwpEventElement from a CalendarEvent
|
|
|
|
|
*/
|
|
|
|
|
public static fromCalendarEvent(event: CalendarEvent): SwpEventElement {
|
|
|
|
|
return new SwpEventElement(event);
|
|
|
|
|
}
|
2025-09-10 23:57:48 +02:00
|
|
|
|
2025-09-20 09:40:56 +02:00
|
|
|
/**
|
|
|
|
|
* Create a clone of this SwpEventElement with "clone-" prefix
|
|
|
|
|
*/
|
|
|
|
|
public createClone(): SwpEventElement {
|
|
|
|
|
// Clone the underlying DOM element
|
|
|
|
|
const clonedElement = this.element.cloneNode(true) as HTMLElement;
|
|
|
|
|
|
|
|
|
|
// Create new SwpEventElement instance from the cloned DOM
|
|
|
|
|
const clonedSwpEvent = SwpEventElement.fromExistingElement(clonedElement);
|
|
|
|
|
|
|
|
|
|
// Apply "clone-" prefix to ID
|
|
|
|
|
clonedSwpEvent.updateEventId(`clone-${this.event.id}`);
|
|
|
|
|
|
|
|
|
|
// Cache original duration for drag operations
|
|
|
|
|
const originalDuration = this.getOriginalEventDuration();
|
|
|
|
|
clonedSwpEvent.element.dataset.originalDuration = originalDuration.toString();
|
|
|
|
|
|
|
|
|
|
// Set height from original element
|
|
|
|
|
clonedSwpEvent.element.style.height = this.element.style.height || `${this.element.getBoundingClientRect().height}px`;
|
|
|
|
|
|
|
|
|
|
return clonedSwpEvent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Factory method to create SwpEventElement from existing DOM element
|
|
|
|
|
*/
|
|
|
|
|
public static fromExistingElement(element: HTMLElement): SwpEventElement {
|
|
|
|
|
// Extract CalendarEvent data from DOM element
|
|
|
|
|
const event = this.extractCalendarEventFromElement(element);
|
|
|
|
|
|
|
|
|
|
// Create new instance but replace the created element with the existing one
|
|
|
|
|
const swpEvent = new SwpEventElement(event);
|
|
|
|
|
swpEvent.element = element;
|
|
|
|
|
|
|
|
|
|
return swpEvent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update the event ID in both the CalendarEvent and DOM element
|
|
|
|
|
*/
|
|
|
|
|
private updateEventId(newId: string): void {
|
|
|
|
|
this.event.id = newId;
|
|
|
|
|
this.element.dataset.eventId = newId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Extract original event duration from DOM element
|
|
|
|
|
*/
|
|
|
|
|
private getOriginalEventDuration(): number {
|
|
|
|
|
const timeElement = this.element.querySelector('swp-event-time');
|
|
|
|
|
if (timeElement) {
|
|
|
|
|
const duration = timeElement.getAttribute('data-duration');
|
|
|
|
|
if (duration) {
|
|
|
|
|
return parseInt(duration);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 60; // Fallback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Extract CalendarEvent from DOM element
|
|
|
|
|
*/
|
|
|
|
|
private static extractCalendarEventFromElement(element: HTMLElement): CalendarEvent {
|
|
|
|
|
return {
|
|
|
|
|
id: element.dataset.eventId || '',
|
|
|
|
|
title: element.dataset.title || '',
|
|
|
|
|
start: new Date(element.dataset.start || ''),
|
|
|
|
|
end: new Date(element.dataset.end || ''),
|
|
|
|
|
type: element.dataset.type || 'work',
|
|
|
|
|
allDay: false,
|
|
|
|
|
syncStatus: 'synced',
|
|
|
|
|
metadata: {
|
|
|
|
|
duration: element.dataset.duration
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 23:57:48 +02:00
|
|
|
/**
|
|
|
|
|
* Factory method to convert an all-day HTML element to a timed SwpEventElement
|
|
|
|
|
*/
|
|
|
|
|
public static fromAllDayElement(allDayElement: HTMLElement): SwpEventElement {
|
|
|
|
|
// Extract data from all-day element's dataset
|
|
|
|
|
const eventId = allDayElement.dataset.eventId || '';
|
|
|
|
|
const title = allDayElement.dataset.title || allDayElement.textContent || 'Untitled';
|
|
|
|
|
const type = allDayElement.dataset.type || 'work';
|
|
|
|
|
const startStr = allDayElement.dataset.start;
|
|
|
|
|
const endStr = allDayElement.dataset.end;
|
|
|
|
|
const durationStr = allDayElement.dataset.duration;
|
|
|
|
|
|
|
|
|
|
if (!startStr || !endStr) {
|
|
|
|
|
throw new Error('All-day element missing start/end dates');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse dates and set reasonable 1-hour duration for timed event
|
|
|
|
|
const originalStart = new Date(startStr);
|
|
|
|
|
const duration = durationStr ? parseInt(durationStr) : 60; // Default 1 hour
|
|
|
|
|
|
|
|
|
|
// For conversion, use current time or a reasonable default (9 AM)
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const startDate = new Date(originalStart);
|
|
|
|
|
startDate.setHours(now.getHours() || 9, now.getMinutes() || 0, 0, 0);
|
|
|
|
|
|
|
|
|
|
const endDate = new Date(startDate);
|
|
|
|
|
endDate.setMinutes(endDate.getMinutes() + duration);
|
|
|
|
|
|
|
|
|
|
// Create CalendarEvent object
|
|
|
|
|
const calendarEvent: CalendarEvent = {
|
|
|
|
|
id: eventId,
|
|
|
|
|
title: title,
|
|
|
|
|
start: startDate,
|
|
|
|
|
end: endDate,
|
|
|
|
|
type: type,
|
|
|
|
|
allDay: false,
|
|
|
|
|
syncStatus: 'synced',
|
|
|
|
|
metadata: {
|
|
|
|
|
duration: duration.toString()
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return new SwpEventElement(calendarEvent);
|
|
|
|
|
}
|
2025-09-10 22:36:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* All-day event element (swp-allday-event)
|
|
|
|
|
*/
|
|
|
|
|
export class SwpAllDayEventElement extends BaseEventElement {
|
|
|
|
|
private columnIndex: number;
|
|
|
|
|
|
|
|
|
|
private constructor(event: CalendarEvent, columnIndex: number) {
|
|
|
|
|
super(event);
|
|
|
|
|
this.columnIndex = columnIndex;
|
|
|
|
|
this.setAllDayAttributes();
|
|
|
|
|
this.createInnerStructure();
|
|
|
|
|
this.applyGridPositioning();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected createElement(): HTMLElement {
|
|
|
|
|
return document.createElement('swp-allday-event');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set all-day specific attributes
|
|
|
|
|
*/
|
|
|
|
|
private setAllDayAttributes(): void {
|
|
|
|
|
this.element.dataset.allDay = "true";
|
2025-09-17 23:39:29 +02:00
|
|
|
// For all-day events, preserve original start/end dates but set to full day times
|
|
|
|
|
const startDateStr = this.event.start.toISOString().split('T')[0];
|
|
|
|
|
const endDateStr = this.event.end.toISOString().split('T')[0];
|
|
|
|
|
this.element.dataset.start = `${startDateStr}T00:00:00`;
|
|
|
|
|
this.element.dataset.end = `${endDateStr}T23:59:59`;
|
2025-09-10 22:36:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create inner structure (just text content for all-day events)
|
|
|
|
|
*/
|
|
|
|
|
private createInnerStructure(): void {
|
|
|
|
|
this.element.textContent = this.event.title;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Apply CSS grid positioning
|
|
|
|
|
*/
|
|
|
|
|
private applyGridPositioning(): void {
|
|
|
|
|
this.element.style.gridColumn = this.columnIndex.toString();
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-13 22:21:03 +02:00
|
|
|
/**
|
|
|
|
|
* Set grid row for this all-day event
|
|
|
|
|
*/
|
|
|
|
|
public setGridRow(row: number): void {
|
|
|
|
|
this.element.style.gridRow = row.toString();
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-17 23:39:29 +02:00
|
|
|
/**
|
|
|
|
|
* Set grid column span for this all-day event
|
|
|
|
|
*/
|
|
|
|
|
public setColumnSpan(startColumn: number, endColumn: number): void {
|
|
|
|
|
this.element.style.gridColumn = `${startColumn} / ${endColumn + 1}`;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 22:36:11 +02:00
|
|
|
/**
|
|
|
|
|
* Factory method to create from CalendarEvent and target date
|
|
|
|
|
*/
|
2025-09-17 23:39:29 +02:00
|
|
|
public static fromCalendarEvent(event: CalendarEvent, targetDate?: string): SwpAllDayEventElement {
|
|
|
|
|
// Calculate column span based on event start and end dates
|
|
|
|
|
const { startColumn, endColumn, columnSpan } = this.calculateColumnSpan(event);
|
|
|
|
|
|
|
|
|
|
// For backwards compatibility, use targetDate if provided, otherwise use calculated start column
|
|
|
|
|
const finalStartColumn = targetDate ? this.getColumnIndexForDate(targetDate) : startColumn;
|
|
|
|
|
const finalEndColumn = targetDate ? finalStartColumn : endColumn;
|
|
|
|
|
const finalColumnSpan = targetDate ? 1 : columnSpan;
|
2025-09-10 22:36:11 +02:00
|
|
|
|
2025-09-17 23:39:29 +02:00
|
|
|
// Find occupied rows in the spanned columns using computedStyle
|
2025-09-13 22:21:03 +02:00
|
|
|
const existingEvents = document.querySelectorAll('swp-allday-event');
|
|
|
|
|
const occupiedRows = new Set<number>();
|
|
|
|
|
|
2025-09-18 18:00:28 +02:00
|
|
|
console.log('🔍 SwpAllDayEventElement: Checking grid row for new event', {
|
|
|
|
|
targetDate,
|
|
|
|
|
finalStartColumn,
|
|
|
|
|
finalEndColumn,
|
|
|
|
|
existingEventsCount: existingEvents.length
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-13 22:21:03 +02:00
|
|
|
existingEvents.forEach(existingEvent => {
|
|
|
|
|
const style = getComputedStyle(existingEvent);
|
2025-09-17 23:39:29 +02:00
|
|
|
const eventStartCol = parseInt(style.gridColumnStart);
|
|
|
|
|
const eventEndCol = parseInt(style.gridColumnEnd);
|
2025-09-18 18:00:28 +02:00
|
|
|
const eventRow = parseInt(style.gridRowStart) || 1;
|
|
|
|
|
const eventId = (existingEvent as HTMLElement).dataset.eventId;
|
2025-09-13 22:21:03 +02:00
|
|
|
|
2025-09-18 18:00:28 +02:00
|
|
|
console.log('📊 SwpAllDayEventElement: Checking existing event', {
|
|
|
|
|
eventId,
|
|
|
|
|
eventStartCol,
|
|
|
|
|
eventEndCol,
|
|
|
|
|
eventRow,
|
|
|
|
|
newEventColumn: finalStartColumn
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// FIXED: Only check events in the same column (not overlap detection)
|
|
|
|
|
if (eventStartCol === finalStartColumn) {
|
|
|
|
|
console.log('✅ SwpAllDayEventElement: Same column - adding occupied row', eventRow);
|
2025-09-13 22:21:03 +02:00
|
|
|
occupiedRows.add(eventRow);
|
2025-09-18 18:00:28 +02:00
|
|
|
} else {
|
|
|
|
|
console.log('⏭️ SwpAllDayEventElement: Different column - skipping');
|
2025-09-13 22:21:03 +02:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Find first available row
|
|
|
|
|
let targetRow = 1;
|
|
|
|
|
while (occupiedRows.has(targetRow)) {
|
|
|
|
|
targetRow++;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-18 18:00:28 +02:00
|
|
|
console.log('🎯 SwpAllDayEventElement: Final row assignment', {
|
|
|
|
|
targetDate,
|
|
|
|
|
finalStartColumn,
|
|
|
|
|
occupiedRows: Array.from(occupiedRows).sort(),
|
|
|
|
|
assignedRow: targetRow
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-17 23:39:29 +02:00
|
|
|
// Create element with calculated column span
|
|
|
|
|
const element = new SwpAllDayEventElement(event, finalStartColumn);
|
2025-09-13 22:21:03 +02:00
|
|
|
element.setGridRow(targetRow);
|
2025-09-17 23:39:29 +02:00
|
|
|
element.setColumnSpan(finalStartColumn, finalEndColumn);
|
2025-09-18 18:00:28 +02:00
|
|
|
|
|
|
|
|
console.log('✅ SwpAllDayEventElement: Created all-day event', {
|
|
|
|
|
eventId: event.id,
|
|
|
|
|
title: event.title,
|
|
|
|
|
column: finalStartColumn,
|
|
|
|
|
row: targetRow
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-13 22:21:03 +02:00
|
|
|
return element;
|
2025-09-10 22:36:11 +02:00
|
|
|
}
|
2025-09-17 23:39:29 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate column span based on event start and end dates
|
|
|
|
|
*/
|
|
|
|
|
private static calculateColumnSpan(event: CalendarEvent): { startColumn: number; endColumn: number; columnSpan: number } {
|
|
|
|
|
const dayHeaders = document.querySelectorAll('swp-day-header');
|
|
|
|
|
|
|
|
|
|
// Extract dates from headers
|
|
|
|
|
const headerDates: string[] = [];
|
|
|
|
|
dayHeaders.forEach(header => {
|
|
|
|
|
const date = (header as HTMLElement).dataset.date;
|
|
|
|
|
if (date) {
|
|
|
|
|
headerDates.push(date);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Format event dates for comparison (YYYY-MM-DD format)
|
|
|
|
|
const eventStartDate = event.start.toISOString().split('T')[0];
|
|
|
|
|
const eventEndDate = event.end.toISOString().split('T')[0];
|
|
|
|
|
|
|
|
|
|
// Find start and end column indices
|
|
|
|
|
let startColumn = 1;
|
|
|
|
|
let endColumn = headerDates.length;
|
|
|
|
|
|
|
|
|
|
headerDates.forEach((dateStr, index) => {
|
|
|
|
|
if (dateStr === eventStartDate) {
|
|
|
|
|
startColumn = index + 1;
|
|
|
|
|
}
|
|
|
|
|
if (dateStr === eventEndDate) {
|
|
|
|
|
endColumn = index + 1;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Ensure end column is at least start column
|
|
|
|
|
if (endColumn < startColumn) {
|
|
|
|
|
endColumn = startColumn;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const columnSpan = endColumn - startColumn + 1;
|
|
|
|
|
|
|
|
|
|
return { startColumn, endColumn, columnSpan };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get column index for a specific date
|
|
|
|
|
*/
|
|
|
|
|
private static getColumnIndexForDate(targetDate: string): number {
|
|
|
|
|
const dayHeaders = document.querySelectorAll('swp-day-header');
|
|
|
|
|
let columnIndex = 1;
|
|
|
|
|
dayHeaders.forEach((header, index) => {
|
|
|
|
|
if ((header as HTMLElement).dataset.date === targetDate) {
|
|
|
|
|
columnIndex = index + 1;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return columnIndex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if two column ranges overlap
|
|
|
|
|
*/
|
|
|
|
|
private static columnsOverlap(startA: number, endA: number, startB: number, endB: number): boolean {
|
|
|
|
|
return !(endA < startB || endB < startA);
|
|
|
|
|
}
|
2025-09-10 22:36:11 +02:00
|
|
|
}
|