Introduces the ability to convert all-day events to timed events by dragging them out of the header. Leverages a factory method to create timed events from all-day elements, ensuring proper data conversion and styling. Improves user experience by allowing more flexible event scheduling.
223 lines
No EOL
6.7 KiB
TypeScript
223 lines
No EOL
6.7 KiB
TypeScript
import { CalendarEvent } from '../types/CalendarTypes';
|
|
import { calendarConfig } from '../core/CalendarConfig';
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
protected formatTime(date: Date): string {
|
|
const hours = date.getHours();
|
|
const minutes = date.getMinutes();
|
|
const period = hours >= 12 ? 'PM' : 'AM';
|
|
const displayHours = hours > 12 ? hours - 12 : (hours === 0 ? 12 : hours);
|
|
return `${displayHours}:${minutes.toString().padStart(2, '0')} ${period}`;
|
|
}
|
|
|
|
/**
|
|
* Calculate event position for timed events
|
|
*/
|
|
protected calculateEventPosition(): { top: number; height: number } {
|
|
const gridSettings = calendarConfig.getGridSettings();
|
|
const dayStartHour = gridSettings.dayStartHour;
|
|
const hourHeight = gridSettings.hourHeight;
|
|
|
|
const startMinutes = this.event.start.getHours() * 60 + this.event.start.getMinutes();
|
|
const endMinutes = this.event.end.getHours() * 60 + this.event.end.getMinutes();
|
|
const dayStartMinutes = dayStartHour * 60;
|
|
|
|
const top = ((startMinutes - dayStartMinutes) / 60) * hourHeight;
|
|
const durationMinutes = endMinutes - startMinutes;
|
|
const height = (durationMinutes / 60) * hourHeight;
|
|
|
|
return { top, height };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 startTime = this.formatTime(this.event.start);
|
|
const endTime = this.formatTime(this.event.end);
|
|
const durationMinutes = (this.event.end.getTime() - this.event.start.getTime()) / (1000 * 60);
|
|
|
|
this.element.innerHTML = `
|
|
<swp-event-time data-duration="${durationMinutes}">${startTime} - ${endTime}</swp-event-time>
|
|
<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);
|
|
}
|
|
|
|
/**
|
|
* 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 (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";
|
|
// Override start/end times to be full day
|
|
const dateStr = this.event.start.toISOString().split('T')[0];
|
|
this.element.dataset.start = `${dateStr}T00:00:00`;
|
|
this.element.dataset.end = `${dateStr}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();
|
|
}
|
|
|
|
/**
|
|
* Factory method to create from CalendarEvent and target date
|
|
*/
|
|
public static fromCalendarEvent(event: CalendarEvent, targetDate: string): SwpAllDayEventElement {
|
|
// 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;
|
|
}
|
|
});
|
|
|
|
return new SwpAllDayEventElement(event, columnIndex);
|
|
}
|
|
} |