import { CalendarEvent } from '../types/CalendarTypes';
import { calendarConfig } from '../core/CalendarConfig';
import { TimeFormatter } from '../utils/TimeFormatter';
import { PositionUtils } from '../utils/PositionUtils';
/**
* 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;
}
/**
* Format time for display using TimeFormatter
*/
protected formatTime(date: Date): string {
return TimeFormatter.formatTime(date);
}
/**
* Calculate event position for timed events using PositionUtils
*/
protected calculateEventPosition(): { top: number; height: number } {
return PositionUtils.calculateEventPosition(this.event.start, this.event.end);
}
}
/**
* 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 {
const timeRange = TimeFormatter.formatTimeRange(this.event.start, this.event.end);
const durationMinutes = (this.event.end.getTime() - this.event.start.getTime()) / (1000 * 60);
this.element.innerHTML = `
${timeRange}
${this.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);
}
/**
* 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
}
};
}
/**
* 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);
}
}
/**
* All-day event element (now using unified swp-event tag)
*/
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-event');
}
/**
* Set all-day specific attributes
*/
private setAllDayAttributes(): void {
this.element.dataset.allDay = "true";
// 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`;
}
/**
* 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();
}
/**
* Set grid row for this all-day event
*/
public setGridRow(row: number): void {
this.element.style.gridRow = row.toString();
}
/**
* Set grid column span for this all-day event
*/
public setColumnSpan(startColumn: number, endColumn: number): void {
this.element.style.gridColumn = `${startColumn} / ${endColumn + 1}`;
}
/**
* Factory method to create from CalendarEvent and layout (provided by AllDayManager)
*/
public static fromCalendarEventWithLayout(
event: CalendarEvent,
layout: { startColumn: number; endColumn: number; row: number; columnSpan: number }
): SwpAllDayEventElement {
// Create element with provided layout
const element = new SwpAllDayEventElement(event, layout.startColumn);
// Set complete grid-area instead of individual properties
const gridArea = `${layout.row} / ${layout.startColumn} / ${layout.row + 1} / ${layout.endColumn + 1}`;
element.element.style.gridArea = gridArea;
console.log('✅ SwpAllDayEventElement: Created all-day event with AllDayLayoutEngine', {
eventId: event.id,
title: event.title,
gridArea: gridArea,
layout: layout
});
return element;
}
/**
* Factory method to create from CalendarEvent and target date (DEPRECATED - use AllDayManager.calculateAllDayEventLayout)
* @deprecated Use AllDayManager.calculateAllDayEventLayout() and fromCalendarEventWithLayout() instead
*/
public static fromCalendarEvent(event: CalendarEvent, targetDate?: string): SwpAllDayEventElement {
console.warn('⚠️ SwpAllDayEventElement.fromCalendarEvent is deprecated. Use AllDayManager.calculateAllDayEventLayout() instead.');
// Fallback to simple column calculation without overlap detection
const { startColumn, endColumn } = this.calculateColumnSpan(event);
const finalStartColumn = targetDate ? this.getColumnIndexForDate(targetDate) : startColumn;
const finalEndColumn = targetDate ? finalStartColumn : endColumn;
// Create element with row 1 (no overlap detection)
const element = new SwpAllDayEventElement(event, finalStartColumn);
element.setGridRow(1);
element.setColumnSpan(finalStartColumn, finalEndColumn);
return element;
}
/**
* 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);
}
}