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-05 23:54:50 +02:00
import { EventStackManager , EventGroup , StackLink } from '../managers/EventStackManager' ;
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-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-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 ;
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-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 ;
const columnDate = new Date ( payload . columnBounds . date ) ;
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-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 ;
const columnDate = new Date ( dragColumnChangeEvent . newColumn . date ) ;
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-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 ;
console . log ( '[EventRenderer] Rendering column with' , columnEvents . length , 'events' ) ;
// Step 1: Calculate stack levels for ALL events first (to understand overlaps)
const allStackLinks = this . stackManager . createOptimizedStackLinks ( columnEvents ) ;
console . log ( '[EventRenderer] All stack links:' ) ;
columnEvents . forEach ( event = > {
const link = allStackLinks . get ( event . id ) ;
console . log ( ` Event ${ event . id } ( ${ event . title } ): stackLevel= ${ link ? . stackLevel ? ? 'none' } ` ) ;
} ) ;
// Step 2: Find grid candidates (start together ±15 min)
const groups = this . stackManager . groupEventsByStartTime ( columnEvents ) ;
const gridGroups = groups . filter ( group = > {
if ( group . events . length <= 1 ) return false ;
group . containerType = this . stackManager . decideContainerType ( group ) ;
return group . containerType === 'GRID' ;
} ) ;
console . log ( '[EventRenderer] Grid groups:' , gridGroups . length ) ;
gridGroups . forEach ( ( g , i ) = > {
console . log ( ` Grid group ${ i } : ` , g . events . map ( e = > e . id ) ) ;
} ) ;
// Step 3: Render grid groups and track which events have been rendered
const renderedIds = new Set < string > ( ) ;
gridGroups . forEach ( ( group , index ) = > {
console . log ( ` [EventRenderer] Rendering grid group ${ index } with ${ group . events . length } events: ` , group . events . map ( e = > e . id ) ) ;
// Calculate grid group stack level by finding what it overlaps OUTSIDE the group
const gridStackLevel = this . calculateGridGroupStackLevel ( group , columnEvents , allStackLinks ) ;
console . log ( ` Grid group stack level: ${ gridStackLevel } ` ) ;
this . renderGridGroup ( group , eventsLayer , gridStackLevel ) ;
group . events . forEach ( e = > renderedIds . add ( e . id ) ) ;
} ) ;
// Step 4: Get remaining events (not in grid)
const remainingEvents = columnEvents . filter ( e = > ! renderedIds . has ( e . id ) ) ;
console . log ( '[EventRenderer] Remaining events for stacking:' ) ;
remainingEvents . forEach ( event = > {
const link = allStackLinks . get ( event . id ) ;
console . log ( ` Event ${ event . id } ( ${ event . title } ): stackLevel= ${ link ? . stackLevel ? ? 'none' } ` ) ;
} ) ;
// Step 5: Render remaining stacked/single events
remainingEvents . forEach ( event = > {
const element = this . renderEvent ( event ) ;
const stackLink = allStackLinks . get ( event . id ) ;
console . log ( ` [EventRenderer] Rendering stacked event ${ event . id } , stackLink: ` , stackLink ) ;
if ( stackLink ) {
// Apply stack link to element (for drag-drop)
this . stackManager . applyStackLinkToElement ( element , stackLink ) ;
// Apply visual styling
this . stackManager . applyVisualStyling ( element , stackLink . stackLevel ) ;
console . log ( ` Applied margin-left: ${ stackLink . stackLevel * 15 } px, stack-link: ` , stackLink ) ;
2025-08-07 00:15:44 +02:00
}
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
/ * *
* Calculate stack level for a grid group based on what it overlaps OUTSIDE the group
* /
private calculateGridGroupStackLevel (
group : EventGroup ,
allEvents : CalendarEvent [ ] ,
stackLinks : Map < string , StackLink >
) : number {
const groupEventIds = new Set ( group . events . map ( e = > e . id ) ) ;
// Find all events OUTSIDE this group
const outsideEvents = allEvents . filter ( e = > ! groupEventIds . has ( e . id ) ) ;
// Find the highest stackLevel of any event that overlaps with ANY event in the grid group
let maxOverlappingLevel = - 1 ;
for ( const gridEvent of group . events ) {
for ( const outsideEvent of outsideEvents ) {
if ( this . stackManager . doEventsOverlap ( gridEvent , outsideEvent ) ) {
const outsideLink = stackLinks . get ( outsideEvent . id ) ;
if ( outsideLink ) {
maxOverlappingLevel = Math . max ( maxOverlappingLevel , outsideLink . stackLevel ) ;
}
}
}
}
// Grid group should be one level above the highest overlapping event
return maxOverlappingLevel + 1 ;
}
/ * *
* Render events in a grid container ( side - by - side )
* /
private renderGridGroup ( group : EventGroup , eventsLayer : HTMLElement , stackLevel : number ) : void {
const groupElement = document . createElement ( 'swp-event-group' ) ;
// Add grid column class based on event count
const colCount = group . events . length ;
groupElement . classList . add ( ` cols- ${ colCount } ` ) ;
// Add stack level class for margin-left offset
groupElement . classList . add ( ` stack-level- ${ stackLevel } ` ) ;
// Position based on earliest event
const earliestEvent = group . events [ 0 ] ;
const position = this . calculateEventPosition ( earliestEvent ) ;
groupElement . style . top = ` ${ position . top + 1 } px ` ;
// Add z-index based on stack level
groupElement . style . zIndex = ` ${ this . stackManager . calculateZIndex ( stackLevel ) } ` ;
// Add stack-link attribute for drag-drop (group acts as a stacked item)
const stackLink : StackLink = {
stackLevel : stackLevel
// prev/next will be handled by drag-drop manager if needed
} ;
this . stackManager . applyStackLinkToElement ( groupElement , stackLink ) ;
// NO height on the group - it should auto-size based on children
// Render each event within the grid
group . events . forEach ( event = > {
const element = this . renderEventInGrid ( event , earliestEvent . start ) ;
groupElement . appendChild ( element ) ;
} ) ;
eventsLayer . appendChild ( groupElement ) ;
}
/ * *
* Render event within a grid container ( relative positioning )
* /
private renderEventInGrid ( event : CalendarEvent , containerStart : Date ) : HTMLElement {
const element = SwpEventElement . fromCalendarEvent ( event ) ;
// Calculate event height
const position = this . calculateEventPosition ( event ) ;
// Events in grid are positioned relatively - NO top offset needed
// The grid container itself is positioned absolutely with the correct top
element . style . position = 'relative' ;
element . style . height = ` ${ position . height - 3 } px ` ;
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-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
}