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-10-06 00:24:13 +02:00
import { EventStackManager } from '../managers/EventStackManager' ;
import { EventLayoutCoordinator , GridGroupLayout , StackedEventLayout } from '../managers/EventLayoutCoordinator' ;
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-05 23:54:50 +02:00
private stackManager : EventStackManager ;
2025-10-06 00:24:13 +02:00
private layoutCoordinator : EventLayoutCoordinator ;
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-10-05 23:54:50 +02:00
this . stackManager = new EventStackManager ( ) ;
2025-10-06 00:24:13 +02:00
this . layoutCoordinator = new EventLayoutCoordinator ( ) ;
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-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 ;
2025-10-08 21:50:41 +02:00
if ( this . draggedClone && payload . columnBounds ) {
// 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-10-08 21:50:41 +02:00
const eventsLayer = payload . columnBounds . element . querySelector ( 'swp-events-layer' ) ;
2025-09-28 13:25:09 +02:00
if ( eventsLayer ) {
eventsLayer . appendChild ( this . draggedClone ) ;
2025-10-08 21:50:41 +02:00
// Set initial position to prevent "jump to top" effect
// Calculate absolute Y position from original element
const originalRect = this . originalEvent . getBoundingClientRect ( ) ;
const columnRect = payload . columnBounds . boundingClientRect ;
const initialTop = originalRect . top - columnRect . top ;
this . draggedClone . style . top = ` ${ initialTop } px ` ;
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-10-04 15:35:09 +02:00
if ( ! this . draggedClone || ! payload . columnBounds ) return ;
2025-09-20 09:40:56 +02:00
2025-10-04 15:35:09 +02:00
// Delegate to SwpEventElement to update position and timestamps
const swpEvent = this . draggedClone as SwpEventElement ;
2025-10-06 21:16:29 +02:00
const columnDate = this . dateService . parseISO ( payload . columnBounds . date ) ;
2025-10-04 15:35:09 +02:00
swpEvent . updatePosition ( columnDate , payload . snappedY ) ;
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-06 21:16:29 +02:00
2025-10-03 16:47:42 +02:00
// Recalculate timestamps with new column date
const currentTop = parseFloat ( this . draggedClone . style . top ) || 0 ;
2025-10-04 15:35:09 +02:00
const swpEvent = this . draggedClone as SwpEventElement ;
2025-10-06 21:16:29 +02:00
const columnDate = this . dateService . parseISO ( dragColumnChangeEvent . newColumn . date ) ;
2025-10-04 15:35:09 +02:00
swpEvent . updatePosition ( columnDate , currentTop ) ;
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-08 00:58:38 +02:00
draggedClone . style . pointerEvents = '' ; // Re-enable pointer events
2025-09-20 09:40:56 +02:00
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-10-05 23:54:50 +02:00
const eventsLayer = column . querySelector ( 'swp-events-layer' ) as HTMLElement ;
2025-08-13 23:05:58 +02:00
if ( eventsLayer ) {
2025-10-05 23:54:50 +02:00
this . renderColumnEvents ( columnEvents , eventsLayer ) ;
}
} ) ;
}
/ * *
* Render events in a column using combined stacking + grid algorithm
* /
private renderColumnEvents ( columnEvents : CalendarEvent [ ] , eventsLayer : HTMLElement ) : void {
if ( columnEvents . length === 0 ) return ;
2025-10-06 00:24:13 +02:00
// Get layout from coordinator
const layout = this . layoutCoordinator . calculateColumnLayout ( columnEvents ) ;
2025-10-05 23:54:50 +02:00
2025-10-06 00:24:13 +02:00
// Render grid groups
layout . gridGroups . forEach ( gridGroup = > {
this . renderGridGroup ( gridGroup , eventsLayer ) ;
2025-10-05 23:54:50 +02:00
} ) ;
2025-10-06 00:24:13 +02:00
// Render stacked events
layout . stackedEvents . forEach ( stackedEvent = > {
const element = this . renderEvent ( stackedEvent . event ) ;
this . stackManager . applyStackLinkToElement ( element , stackedEvent . stackLink ) ;
this . stackManager . applyVisualStyling ( element , stackedEvent . stackLink . stackLevel ) ;
2025-10-05 23:54:50 +02:00
eventsLayer . appendChild ( element ) ;
2025-08-07 00:15:44 +02:00
} ) ;
}
2025-10-05 23:54:50 +02:00
/ * *
2025-10-06 17:05:18 +02:00
* Render events in a grid container ( side - by - side with column sharing )
2025-10-05 23:54:50 +02:00
* /
2025-10-06 00:24:13 +02:00
private renderGridGroup ( gridGroup : GridGroupLayout , eventsLayer : HTMLElement ) : void {
2025-10-05 23:54:50 +02:00
const groupElement = document . createElement ( 'swp-event-group' ) ;
2025-10-06 17:05:18 +02:00
// Add grid column class based on number of columns (not events)
const colCount = gridGroup . columns . length ;
2025-10-05 23:54:50 +02:00
groupElement . classList . add ( ` cols- ${ colCount } ` ) ;
// Add stack level class for margin-left offset
2025-10-06 00:24:13 +02:00
groupElement . classList . add ( ` stack-level- ${ gridGroup . stackLevel } ` ) ;
2025-10-05 23:54:50 +02:00
2025-10-06 00:24:13 +02:00
// Position from layout
groupElement . style . top = ` ${ gridGroup . position . top } px ` ;
2025-10-05 23:54:50 +02:00
// Add stack-link attribute for drag-drop (group acts as a stacked item)
2025-10-06 00:24:13 +02:00
const stackLink = {
stackLevel : gridGroup.stackLevel
2025-10-05 23:54:50 +02:00
} ;
this . stackManager . applyStackLinkToElement ( groupElement , stackLink ) ;
2025-10-06 21:16:29 +02:00
// Apply visual styling (margin-left and z-index) using StackManager
this . stackManager . applyVisualStyling ( groupElement , gridGroup . stackLevel ) ;
2025-10-06 17:05:18 +02:00
// Render each column
2025-10-06 00:24:13 +02:00
const earliestEvent = gridGroup . events [ 0 ] ;
2025-10-06 17:05:18 +02:00
gridGroup . columns . forEach ( columnEvents = > {
const columnContainer = this . renderGridColumn ( columnEvents , earliestEvent . start ) ;
groupElement . appendChild ( columnContainer ) ;
2025-10-05 23:54:50 +02:00
} ) ;
eventsLayer . appendChild ( groupElement ) ;
}
/ * *
2025-10-06 17:05:18 +02:00
* Render a single column within a grid group
* Column may contain multiple events that don ' t overlap
* /
private renderGridColumn ( columnEvents : CalendarEvent [ ] , containerStart : Date ) : HTMLElement {
const columnContainer = document . createElement ( 'div' ) ;
columnContainer . style . position = 'relative' ;
columnEvents . forEach ( event = > {
const element = this . renderEventInGrid ( event , containerStart ) ;
columnContainer . appendChild ( element ) ;
} ) ;
return columnContainer ;
}
/ * *
* Render event within a grid container ( absolute positioning within column )
2025-10-05 23:54:50 +02:00
* /
private renderEventInGrid ( event : CalendarEvent , containerStart : Date ) : HTMLElement {
const element = SwpEventElement . fromCalendarEvent ( event ) ;
// Calculate event height
const position = this . calculateEventPosition ( event ) ;
2025-10-06 17:05:18 +02:00
// Calculate relative top offset if event starts after container start
// (e.g., if container starts at 07:00 and event starts at 08:15, offset = 75 min)
const timeDiffMs = event . start . getTime ( ) - containerStart . getTime ( ) ;
const timeDiffMinutes = timeDiffMs / ( 1000 * 60 ) ;
const gridSettings = calendarConfig . getGridSettings ( ) ;
const relativeTop = timeDiffMinutes > 0 ? ( timeDiffMinutes / 60 ) * gridSettings.hourHeight : 0 ;
// Events in grid columns are positioned absolutely within their column container
element . style . position = 'absolute' ;
element . style . top = ` ${ relativeTop } px ` ;
2025-10-05 23:54:50 +02:00
element . style . height = ` ${ position . height - 3 } px ` ;
2025-10-06 17:05:18 +02:00
element . style . left = '0' ;
element . style . right = '0' ;
2025-10-05 23:54:50 +02:00
return element ;
}
2025-10-02 23:11:26 +02:00
private renderEvent ( event : CalendarEvent ) : HTMLElement {
2025-10-05 21:53:25 +02:00
const element = SwpEventElement . fromCalendarEvent ( event ) ;
// Apply positioning (moved from SwpEventElement.applyPositioning)
const position = this . calculateEventPosition ( event ) ;
element . style . position = 'absolute' ;
element . style . top = ` ${ position . top + 1 } px ` ;
element . style . height = ` ${ position . height - 3 } px ` ;
element . style . left = '2px' ;
element . style . right = '2px' ;
return element ;
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-06 00:24:13 +02:00
const eventSelector = 'swp-event' ;
const groupSelector = 'swp-event-group' ;
2025-09-04 00:16:35 +02:00
const existingEvents = container
2025-10-06 00:24:13 +02:00
? container . querySelectorAll ( eventSelector )
: document . querySelectorAll ( eventSelector ) ;
const existingGroups = container
? container . querySelectorAll ( groupSelector )
: document . querySelectorAll ( groupSelector ) ;
2025-09-20 09:40:56 +02:00
2025-08-07 00:15:44 +02:00
existingEvents . forEach ( event = > event . remove ( ) ) ;
2025-10-06 00:24:13 +02:00
existingGroups . forEach ( group = > group . remove ( ) ) ;
2025-08-07 00:15:44 +02:00
}
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
2025-10-06 21:16:29 +02:00
// Create start and end of day for interval overlap check
const columnStart = this . dateService . parseISO ( ` ${ columnDate } T00:00:00 ` ) ;
const columnEnd = this . dateService . parseISO ( ` ${ columnDate } T23:59:59.999 ` ) ;
2025-09-20 09:40:56 +02:00
2025-10-06 21:16:29 +02:00
const columnEvents = events . filter ( event = > {
// Interval overlap: event overlaps with column day if event.start < columnEnd AND event.end > columnStart
const overlaps = event . start < columnEnd && event . end > columnStart ;
return overlaps ;
2025-08-13 23:05:58 +02:00
} ) ;
return columnEvents ;
}
2025-08-07 00:15:44 +02:00
}