diff --git a/src/elements/SwpEventElement.ts b/src/elements/SwpEventElement.ts
new file mode 100644
index 0000000..ce9dcc5
--- /dev/null
+++ b/src/elements/SwpEventElement.ts
@@ -0,0 +1,178 @@
+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 = `
+ ${startTime} - ${endTime}
+ ${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);
+ }
+}
+
+/**
+ * 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);
+ }
+}
\ No newline at end of file
diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts
index d7b2770..d7624b6 100644
--- a/src/renderers/EventRenderer.ts
+++ b/src/renderers/EventRenderer.ts
@@ -7,6 +7,7 @@ import { eventBus } from '../core/EventBus';
import { CoreEvents } from '../constants/CoreEvents';
import { OverlapDetector, OverlapResult, EventId } from '../utils/OverlapDetector';
import { ResizeManager } from '../managers/ResizeManager';
+import { SwpEventElement, SwpAllDayEventElement } from '../elements/SwpEventElement';
/**
* Interface for event rendering strategies
@@ -146,6 +147,12 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
this.handleConvertToTimed(eventId, targetColumn, targetY);
});
+ // Handle simple all-day duration adjustment (when leaving header)
+ eventBus.on('drag:adjust-allday-duration', (event) => {
+ const { eventId, durationMinutes } = (event as CustomEvent).detail;
+ this.handleAdjustAllDayDuration(eventId, durationMinutes);
+ });
+
// Handle navigation period change (when slide animation completes)
eventBus.on(CoreEvents.NAVIGATION_COMPLETED, () => {
// Animate all-day height after navigation completes
@@ -723,21 +730,25 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
}
});
- // Create all-day event with standardized data attributes
- const allDayEvent = document.createElement('swp-allday-event');
- allDayEvent.dataset.eventId = clone.dataset.eventId || '';
- allDayEvent.dataset.title = eventTitle;
- allDayEvent.dataset.start = `${targetDate}T00:00:00`;
- allDayEvent.dataset.end = `${targetDate}T23:59:59`;
- allDayEvent.dataset.type = clone.dataset.type || 'work';
- allDayEvent.dataset.duration = eventDuration;
- allDayEvent.dataset.allDay = "true";
+ // Create CalendarEvent object for the factory
+ const tempEvent: CalendarEvent = {
+ id: clone.dataset.eventId || '',
+ title: eventTitle,
+ start: new Date(`${targetDate}T00:00:00`),
+ end: new Date(`${targetDate}T23:59:59`),
+ type: clone.dataset.type || 'work',
+ allDay: true,
+ syncStatus: 'synced',
+ metadata: {
+ duration: eventDuration
+ }
+ };
- allDayEvent.textContent = eventTitle;
+ // Create all-day event using factory
+ const swpAllDayEvent = SwpAllDayEventElement.fromCalendarEvent(tempEvent, targetDate);
+ const allDayEvent = swpAllDayEvent.getElement();
console.log("allDayEvent", allDayEvent.dataset);
- // Position in grid
- (allDayEvent as HTMLElement).style.gridColumn = columnIndex.toString();
// grid-row will be set by checkAndAnimateAllDayHeight() based on actual position
// Remove original clone
@@ -768,6 +779,33 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
this.transformAllDayToTimed(this.draggedClone, targetColumn, targetY);
}
+ /**
+ * Handle simple all-day duration adjustment (when leaving header)
+ */
+ private handleAdjustAllDayDuration(eventId: string, durationMinutes: number): void {
+ if (!this.draggedClone) return;
+
+ // Only adjust if it's an all-day event
+ if (this.draggedClone.tagName !== 'SWP-ALLDAY-EVENT') return;
+
+ // Simply adjust the duration and height - keep all other data intact
+ this.draggedClone.dataset.duration = durationMinutes.toString();
+
+ // Calculate new height based on duration
+ const gridSettings = calendarConfig.getGridSettings();
+ const hourHeight = gridSettings.hourHeight;
+ const newHeight = (durationMinutes / 60) * hourHeight;
+ this.draggedClone.style.height = `${newHeight}px`;
+
+ // Remove all-day specific styling to make it behave like a timed event
+ this.draggedClone.style.gridColumn = '';
+ this.draggedClone.style.gridRow = '';
+ this.draggedClone.dataset.allDay = "false";
+
+ // Apply basic timed event positioning
+ this.applyDragStyling(this.draggedClone);
+ }
+
/**
* Transform clone from all-day to timed event
*/
@@ -821,18 +859,12 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
}
};
- // Create timed event element
- const timedEvent = document.createElement('swp-event');
- timedEvent.dataset.eventId = eventId;
- timedEvent.dataset.title = eventTitle;
- timedEvent.dataset.type = eventType;
- timedEvent.dataset.start = startDate.toISOString();
- timedEvent.dataset.end = endDate.toISOString();
- timedEvent.dataset.duration = duration.toString();
- timedEvent.dataset.originalDuration = duration.toString();
+ // Create timed event using factory
+ const swpTimedEvent = SwpEventElement.fromCalendarEvent(tempEvent);
+ const timedEvent = swpTimedEvent.getElement();
- // Create inner structure using helper method
- timedEvent.innerHTML = this.createEventInnerStructure(tempEvent);
+ // Set additional drag-specific attributes
+ timedEvent.dataset.originalDuration = duration.toString();
// Apply drag styling and positioning
this.applyDragStyling(timedEvent);
@@ -970,19 +1002,12 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
// 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;
+ // Create all-day event using factory
+ const eventDateStr = DateCalculator.formatISODate(event.start);
+ const swpAllDayEvent = SwpAllDayEventElement.fromCalendarEvent(event, eventDateStr);
+ const allDayEvent = swpAllDayEvent.getElement();
- // Set data attributes directly from CalendarEvent
- allDayEvent.dataset.eventId = event.id;
- allDayEvent.dataset.title = event.title;
- allDayEvent.dataset.start = event.start.toISOString();
- allDayEvent.dataset.end = event.end.toISOString();
- allDayEvent.dataset.type = event.type;
- allDayEvent.dataset.duration = event.metadata?.duration?.toString() || '60';
-
- // Set grid position (column and row)
+ // Override grid position for spanning events
(allDayEvent as HTMLElement).style.gridColumn = span.columnSpan > 1
? `${span.startColumn} / span ${span.columnSpan}`
: `${span.startColumn}`;
@@ -1000,22 +1025,8 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
}
protected renderEvent(event: CalendarEvent): HTMLElement {
- const eventElement = document.createElement('swp-event');
- eventElement.dataset.eventId = event.id;
- eventElement.dataset.title = event.title;
- eventElement.dataset.start = event.start.toISOString();
- eventElement.dataset.end = event.end.toISOString();
- eventElement.dataset.type = event.type;
- eventElement.dataset.duration = event.metadata?.duration?.toString() || '60';
-
- // Calculate and apply position based on time
- const position = this.calculateEventPosition(event);
- this.applyEventPositioning(eventElement, position.top + 1, position.height - 3);
-
- // Color is now handled by CSS classes based on data-type attribute
-
- // Create event content using helper method
- eventElement.innerHTML = this.createEventInnerStructure(event);
+ const swpEvent = SwpEventElement.fromCalendarEvent(event);
+ const eventElement = swpEvent.getElement();
// Setup resize handles on first mouseover only
eventElement.addEventListener('mouseover', () => {