2025-08-07 00:15:44 +02:00
// Event rendering strategy interface and implementations
import { CalendarEvent } from '../types/CalendarTypes' ;
2025-09-03 20:04:47 +02:00
import { calendarConfig } from '../core/CalendarConfig' ;
2025-09-21 21:30:51 +02:00
import { SwpEventElement } from '../elements/SwpEventElement' ;
2025-09-13 00:39:56 +02:00
import { PositionUtils } from '../utils/PositionUtils' ;
2025-09-28 13:25:09 +02:00
import { ColumnBounds } from '../utils/ColumnDetectionUtils' ;
import { DragColumnChangeEventPayload , DragMoveEventPayload , DragStartEventPayload } from '../types/EventTypes' ;
2025-10-03 16:47:42 +02:00
import { DateService } from '../utils/DateService' ;
2025-08-07 00:15:44 +02:00
/ * *
* Interface for event rendering strategies
* /
export interface EventRendererStrategy {
2025-09-03 20:04:47 +02:00
renderEvents ( events : CalendarEvent [ ] , container : HTMLElement ) : void ;
2025-08-16 00:51:12 +02:00
clearEvents ( container? : HTMLElement ) : void ;
2025-09-28 13:25:09 +02:00
handleDragStart ? ( payload : DragStartEventPayload ) : void ;
handleDragMove ? ( payload : DragMoveEventPayload ) : void ;
2025-09-20 09:40:56 +02:00
handleDragAutoScroll ? ( eventId : string , snappedY : number ) : void ;
2025-09-28 13:25:09 +02:00
handleDragEnd ? ( eventId : string , originalElement : HTMLElement , draggedClone : HTMLElement , finalColumn : ColumnBounds , finalY : number ) : void ;
2025-09-20 09:40:56 +02:00
handleEventClick ? ( eventId : string , originalElement : HTMLElement ) : void ;
2025-09-28 13:25:09 +02:00
handleColumnChange ? ( payload : DragColumnChangeEventPayload ) : void ;
2025-09-20 09:40:56 +02:00
handleNavigationCompleted ? ( ) : void ;
2025-08-07 00:15:44 +02:00
}
/ * *
2025-10-02 23:11:26 +02:00
* Date - based event renderer
2025-08-07 00:15:44 +02:00
* /
2025-10-02 23:11:26 +02:00
export class DateEventRenderer implements EventRendererStrategy {
2025-09-20 09:40:56 +02:00
2025-10-03 20:50:40 +02:00
private dateService : DateService ;
2025-10-04 14:50:25 +02:00
private draggedClone : HTMLElement | null = null ;
private originalEvent : HTMLElement | null = null ;
2025-08-20 00:39:31 +02:00
2025-10-03 20:50:40 +02:00
constructor ( ) {
2025-10-04 00:32:26 +02:00
const timezone = calendarConfig . getTimezone ? . ( ) ;
2025-10-03 20:50:40 +02:00
this . dateService = new DateService ( timezone ) ;
2025-08-27 22:50:13 +02:00
}
2025-10-02 23:11:26 +02:00
2025-09-10 22:07:40 +02:00
private applyDragStyling ( element : HTMLElement ) : void {
2025-09-16 23:09:10 +02:00
element . classList . add ( 'dragging' ) ;
2025-10-04 00:32:26 +02:00
element . style . removeProperty ( "margin-left" ) ;
2025-09-10 22:07:40 +02:00
}
2025-08-27 22:50:13 +02:00
/ * *
* Update clone timestamp based on new position
* /
2025-10-03 16:05:22 +02:00
private updateCloneTimestamp ( payload : DragMoveEventPayload ) : void {
2025-10-03 16:47:42 +02:00
if ( payload . draggedClone . dataset . allDay === "true" || ! payload . columnBounds ) return ;
2025-09-10 22:07:40 +02:00
2025-09-03 20:04:47 +02:00
const gridSettings = calendarConfig . getGridSettings ( ) ;
2025-10-03 16:47:42 +02:00
const { hourHeight , dayStartHour , snapInterval } = gridSettings ;
if ( ! payload . draggedClone . dataset . originalDuration ) {
throw new DOMException ( "missing clone.dataset.originalDuration" ) ;
}
2025-09-20 09:40:56 +02:00
2025-10-03 16:47:42 +02:00
// Calculate snapped start minutes
2025-10-03 16:05:22 +02:00
const minutesFromGridStart = ( payload . snappedY / hourHeight ) * 60 ;
2025-10-03 16:47:42 +02:00
const snappedStartMinutes = this . calculateSnappedMinutes (
minutesFromGridStart , dayStartHour , snapInterval
) ;
// Calculate end minutes
const originalDuration = parseInt ( payload . draggedClone . dataset . originalDuration ) ;
const endTotalMinutes = snappedStartMinutes + originalDuration ;
// Update UI
this . updateTimeDisplay ( payload . draggedClone , snappedStartMinutes , endTotalMinutes ) ;
// Update data attributes
this . updateDateTimeAttributes (
payload . draggedClone ,
new Date ( payload . columnBounds . date ) ,
snappedStartMinutes ,
endTotalMinutes
) ;
}
2025-09-20 09:40:56 +02:00
2025-10-03 16:47:42 +02:00
/ * *
* Calculate snapped minutes from grid start
* /
private calculateSnappedMinutes ( minutesFromGridStart : number , dayStartHour : number , snapInterval : number ) : number {
2025-09-03 20:48:23 +02:00
const actualStartMinutes = ( dayStartHour * 60 ) + minutesFromGridStart ;
2025-10-03 16:47:42 +02:00
return Math . round ( actualStartMinutes / snapInterval ) * snapInterval ;
}
2025-09-20 09:40:56 +02:00
2025-10-03 16:47:42 +02:00
/ * *
* Update time display in the UI
* /
private updateTimeDisplay ( element : HTMLElement , startMinutes : number , endMinutes : number ) : void {
const timeElement = element . querySelector ( 'swp-event-time' ) ;
if ( ! timeElement ) return ;
2025-09-20 09:40:56 +02:00
2025-10-03 16:47:42 +02:00
const startTime = this . formatTimeFromMinutes ( startMinutes ) ;
const endTime = this . formatTimeFromMinutes ( endMinutes ) ;
timeElement . textContent = ` ${ startTime } - ${ endTime } ` ;
}
2025-10-02 23:11:26 +02:00
2025-10-03 16:47:42 +02:00
/ * *
* Update data - start and data - end attributes with ISO timestamps
* /
private updateDateTimeAttributes ( element : HTMLElement , columnDate : Date , startMinutes : number , endMinutes : number ) : void {
2025-10-03 20:59:52 +02:00
const startDate = this . dateService . createDateAtTime ( columnDate , startMinutes ) ;
let endDate = this . dateService . createDateAtTime ( columnDate , endMinutes ) ;
2025-10-03 16:47:42 +02:00
// Handle cross-midnight events
if ( endMinutes >= 1440 ) {
const extraDays = Math . floor ( endMinutes / 1440 ) ;
2025-10-03 20:59:52 +02:00
endDate = this . dateService . addDays ( endDate , extraDays ) ;
2025-10-03 16:47:42 +02:00
}
2025-10-03 20:59:52 +02:00
2025-10-04 00:32:26 +02:00
// Convert to UTC before storing as ISO string
element . dataset . start = this . dateService . toUTC ( startDate ) ;
element . dataset . end = this . dateService . toUTC ( endDate ) ;
2025-10-03 16:47:42 +02:00
}
2025-09-20 09:40:56 +02:00
2025-10-03 16:47:42 +02:00
/ * *
* Format minutes since midnight to time string
* /
private formatTimeFromMinutes ( totalMinutes : number ) : string {
2025-10-03 20:59:52 +02:00
return this . dateService . minutesToTime ( totalMinutes ) ;
2025-08-27 22:50:13 +02:00
}
2025-09-20 09:40:56 +02:00
2025-08-27 22:50:13 +02:00
/ * *
* Handle drag start event
* /
2025-09-28 13:25:09 +02:00
public handleDragStart ( payload : DragStartEventPayload ) : void {
this . originalEvent = payload . draggedElement ; ;
2025-09-20 09:40:56 +02:00
2025-09-26 22:53:49 +02:00
// Use the clone from the payload instead of creating a new one
this . draggedClone = payload . draggedClone ;
if ( this . draggedClone ) {
2025-10-02 23:11:26 +02:00
// Apply drag styling
2025-09-26 22:53:49 +02:00
this . applyDragStyling ( this . draggedClone ) ;
// Add to current column's events layer (not directly to column)
2025-09-28 13:25:09 +02:00
const eventsLayer = payload . columnBounds ? . element . querySelector ( 'swp-events-layer' ) ;
if ( eventsLayer ) {
eventsLayer . appendChild ( this . draggedClone ) ;
2025-09-04 00:16:35 +02:00
}
2025-08-27 22:50:13 +02:00
}
2025-09-20 09:40:56 +02:00
2025-08-27 22:50:13 +02:00
// Make original semi-transparent
2025-09-28 13:25:09 +02:00
this . originalEvent . style . opacity = '0.3' ;
this . originalEvent . style . userSelect = 'none' ;
2025-09-20 09:40:56 +02:00
2025-08-27 22:50:13 +02:00
}
2025-09-20 09:40:56 +02:00
2025-08-27 22:50:13 +02:00
/ * *
* Handle drag move event
* /
2025-09-28 13:25:09 +02:00
public handleDragMove ( payload : DragMoveEventPayload ) : void {
2025-08-27 22:50:13 +02:00
if ( ! this . draggedClone ) return ;
2025-09-20 09:40:56 +02:00
2025-10-03 19:09:44 +02:00
// Update position - snappedY is already the event top position
// Add +1px to match the initial positioning offset from SwpEventElement
this . draggedClone . style . top = ( payload . snappedY + 1 ) + 'px' ;
2025-09-20 09:40:56 +02:00
2025-08-27 22:50:13 +02:00
// Update timestamp display
2025-10-03 16:05:22 +02:00
this . updateCloneTimestamp ( payload ) ;
2025-09-20 09:40:56 +02:00
2025-08-27 22:50:13 +02:00
}
2025-09-20 09:40:56 +02:00
/ * *
* Handle drag auto - scroll event
* /
public handleDragAutoScroll ( eventId : string , snappedY : number ) : void {
if ( ! this . draggedClone ) return ;
// Update position directly using the calculated snapped position
this . draggedClone . style . top = snappedY + 'px' ;
// Update timestamp display
2025-10-03 16:33:26 +02:00
//this.updateCloneTimestamp(this.draggedClone, snappedY); //TODO: Commented as, we need to move all this scroll logic til scroll manager away from eventrenderer
2025-09-20 09:40:56 +02:00
}
2025-08-27 22:50:13 +02:00
/ * *
* Handle column change during drag
* /
2025-09-28 13:25:09 +02:00
public handleColumnChange ( dragColumnChangeEvent : DragColumnChangeEventPayload ) : void {
2025-08-27 22:50:13 +02:00
if ( ! this . draggedClone ) return ;
2025-09-20 09:40:56 +02:00
2025-09-28 13:25:09 +02:00
const eventsLayer = dragColumnChangeEvent . newColumn . element . querySelector ( 'swp-events-layer' ) ;
if ( eventsLayer && this . draggedClone . parentElement !== eventsLayer ) {
eventsLayer . appendChild ( this . draggedClone ) ;
2025-10-03 16:47:42 +02:00
// Recalculate timestamps with new column date
const currentTop = parseFloat ( this . draggedClone . style . top ) || 0 ;
const mockPayload : DragMoveEventPayload = {
draggedElement : dragColumnChangeEvent.originalElement ,
draggedClone : this.draggedClone ,
mousePosition : dragColumnChangeEvent.mousePosition ,
mouseOffset : { x : 0 , y : 0 } ,
columnBounds : dragColumnChangeEvent.newColumn ,
snappedY : currentTop
} ;
this . updateCloneTimestamp ( mockPayload ) ;
2025-08-27 22:50:13 +02:00
}
}
2025-09-20 09:40:56 +02:00
2025-08-27 22:50:13 +02:00
/ * *
* Handle drag end event
* /
2025-09-28 13:25:09 +02:00
public handleDragEnd ( eventId : string , originalElement : HTMLElement , draggedClone : HTMLElement , finalColumn : ColumnBounds , finalY : number ) : void {
2025-09-20 09:40:56 +02:00
if ( ! draggedClone || ! originalElement ) {
console . warn ( 'Missing draggedClone or originalElement' ) ;
2025-08-27 23:56:38 +02:00
return ;
}
2025-09-20 09:40:56 +02:00
2025-08-27 22:50:13 +02:00
// Fade out original
2025-10-02 23:11:26 +02:00
this . fadeOutAndRemove ( originalElement ) ;
2025-09-20 09:40:56 +02:00
2025-08-27 23:56:38 +02:00
// Remove clone prefix and normalize clone to be a regular event
2025-09-20 09:40:56 +02:00
const cloneId = draggedClone . dataset . eventId ;
2025-08-27 22:50:13 +02:00
if ( cloneId && cloneId . startsWith ( 'clone-' ) ) {
2025-09-20 09:40:56 +02:00
draggedClone . dataset . eventId = cloneId . replace ( 'clone-' , '' ) ;
2025-08-27 22:50:13 +02:00
}
2025-09-20 09:40:56 +02:00
2025-08-27 23:56:38 +02:00
// Fully normalize the clone to be a regular event
2025-09-20 09:40:56 +02:00
draggedClone . classList . remove ( 'dragging' ) ;
2025-10-04 14:50:25 +02:00
// Clean up instance state
2025-08-27 22:50:13 +02:00
this . draggedClone = null ;
this . originalEvent = null ;
}
2025-09-20 09:40:56 +02:00
/ * *
* Handle navigation completed event
* /
public handleNavigationCompleted ( ) : void {
// Default implementation - can be overridden by subclasses
}
2025-08-27 22:50:13 +02:00
/ * *
2025-09-29 20:50:52 +02:00
* Fade out and remove element
2025-08-27 22:50:13 +02:00
* /
private fadeOutAndRemove ( element : HTMLElement ) : void {
element . style . transition = 'opacity 0.3s ease-out' ;
element . style . opacity = '0' ;
2025-09-20 09:40:56 +02:00
2025-08-27 22:50:13 +02:00
setTimeout ( ( ) = > {
element . remove ( ) ;
} , 300 ) ;
2025-08-20 00:39:31 +02:00
}
2025-09-20 09:40:56 +02:00
2025-09-03 20:04:47 +02:00
renderEvents ( events : CalendarEvent [ ] , container : HTMLElement ) : void {
2025-09-22 21:53:18 +02:00
// Filter out all-day events - they should be handled by AllDayEventRenderer
const timedEvents = events . filter ( event = > ! event . allDay ) ;
2025-09-28 13:25:09 +02:00
2025-08-24 00:13:07 +02:00
// Find columns in the specific container for regular events
2025-08-16 00:51:12 +02:00
const columns = this . getColumns ( container ) ;
2025-08-13 23:05:58 +02:00
columns . forEach ( column = > {
2025-09-22 21:53:18 +02:00
const columnEvents = this . getEventsForColumn ( column , timedEvents ) ;
2025-08-13 23:05:58 +02:00
const eventsLayer = column . querySelector ( 'swp-events-layer' ) ;
2025-10-04 14:50:25 +02:00
2025-08-13 23:05:58 +02:00
if ( eventsLayer ) {
2025-10-04 14:50:25 +02:00
// Simply render each event - no overlap handling
columnEvents . forEach ( event = > {
const element = this . renderEvent ( event ) ;
eventsLayer . appendChild ( element ) ;
} ) ;
2025-08-07 00:15:44 +02:00
}
} ) ;
}
2025-10-02 23:11:26 +02:00
private renderEvent ( event : CalendarEvent ) : HTMLElement {
2025-09-10 22:36:11 +02:00
const swpEvent = SwpEventElement . fromCalendarEvent ( event ) ;
2025-10-04 14:50:25 +02:00
return swpEvent . getElement ( ) ;
2025-08-07 00:15:44 +02:00
}
2025-09-03 20:04:47 +02:00
protected calculateEventPosition ( event : CalendarEvent ) : { top : number ; height : number } {
2025-09-13 00:39:56 +02:00
// Delegate to PositionUtils for centralized position calculation
return PositionUtils . calculateEventPosition ( event . start , event . end ) ;
2025-08-07 00:15:44 +02:00
}
2025-08-16 00:51:12 +02:00
clearEvents ( container? : HTMLElement ) : void {
2025-10-04 14:50:25 +02:00
const selector = 'swp-event' ;
2025-09-04 00:16:35 +02:00
const existingEvents = container
2025-08-16 00:51:12 +02:00
? container . querySelectorAll ( selector )
: document . querySelectorAll ( selector ) ;
2025-09-20 09:40:56 +02:00
2025-08-07 00:15:44 +02:00
existingEvents . forEach ( event = > event . remove ( ) ) ;
}
2025-09-09 14:35:21 +02:00
2025-08-16 00:51:12 +02:00
protected getColumns ( container : HTMLElement ) : HTMLElement [ ] {
const columns = container . querySelectorAll ( 'swp-day-column' ) ;
2025-08-13 23:05:58 +02:00
return Array . from ( columns ) as HTMLElement [ ] ;
}
protected getEventsForColumn ( column : HTMLElement , events : CalendarEvent [ ] ) : CalendarEvent [ ] {
const columnDate = column . dataset . date ;
2025-08-20 00:39:31 +02:00
if ( ! columnDate ) {
return [ ] ;
}
2025-08-13 23:05:58 +02:00
const columnEvents = events . filter ( event = > {
2025-10-03 20:50:40 +02:00
const eventDateStr = this . dateService . formatISODate ( event . start ) ;
2025-08-20 00:39:31 +02:00
const matches = eventDateStr === columnDate ;
2025-09-20 09:40:56 +02:00
2025-08-20 00:39:31 +02:00
return matches ;
2025-08-13 23:05:58 +02:00
} ) ;
return columnEvents ;
}
2025-08-07 00:15:44 +02:00
}