From e9298934c67eb262e69c97466db15aaabad86564 Mon Sep 17 00:00:00 2001 From: Janus Knudsen Date: Wed, 10 Sep 2025 22:36:11 +0200 Subject: [PATCH] Introduces event element classes Creates `SwpEventElement` and `SwpAllDayEventElement` classes for handling event rendering. Refactors event creation logic in `EventRenderer` to utilize these classes, improving code organization and reusability. Adds factory methods for creating event elements from `CalendarEvent` objects, simplifying event instantiation and data management. --- src/elements/SwpEventElement.ts | 178 ++++++++++++++++++++++++++++++++ src/renderers/EventRenderer.ts | 113 +++++++++++--------- 2 files changed, 240 insertions(+), 51 deletions(-) create mode 100644 src/elements/SwpEventElement.ts 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', () => {