From 9c65143df272c8cce5afe5ea28b2a6a939bd17c2 Mon Sep 17 00:00:00 2001 From: Janus Knudsen Date: Sun, 24 Aug 2025 00:13:07 +0200 Subject: [PATCH] Refactors event rendering and display Improves event rendering by introducing dedicated event renderers and streamlining event display logic. - Adds a base event renderer and specialized date and resource-based renderers to handle event display logic. - Renders all-day events within a dedicated container in the calendar header. - Removes the direct filtering of all-day events from the `GridManager`. - Fixes an issue where the 'Summer Festival' event started on the wrong date. The changes enhance the flexibility and maintainability of the calendar, provide dedicated containers and styling for allday events and fix date issues related to certain events --- src/data/mock-events.json | 2 +- src/managers/CalendarManager.ts | 15 +++ src/managers/EventManager.ts | 1 + src/managers/GridManager.ts | 21 ---- src/renderers/EventRenderer.ts | 153 +++++++++++++++++++++++++++- src/renderers/GridRenderer.ts | 22 ++-- src/renderers/HeaderRenderer.ts | 69 ------------- src/strategies/ViewStrategy.ts | 2 - src/strategies/WeekViewStrategy.ts | 3 +- wwwroot/css/calendar-base-css.css | 2 +- wwwroot/css/calendar-layout-css.css | 21 ++-- 11 files changed, 190 insertions(+), 121 deletions(-) diff --git a/src/data/mock-events.json b/src/data/mock-events.json index 192190f..ff80c44 100644 --- a/src/data/mock-events.json +++ b/src/data/mock-events.json @@ -692,7 +692,7 @@ { "id": "70", "title": "Summer Festival", - "start": "2025-08-15T00:00:00", + "start": "2025-08-14T00:00:00", "end": "2025-08-16T23:59:59", "type": "milestone", "allDay": true, diff --git a/src/managers/CalendarManager.ts b/src/managers/CalendarManager.ts index 173e3df..551fe54 100644 --- a/src/managers/CalendarManager.ts +++ b/src/managers/CalendarManager.ts @@ -74,6 +74,21 @@ export class CalendarManager { } await this.gridManager.render(); + // Step 2b: Trigger event rendering now that data is loaded + // Re-emit GRID_RENDERED to trigger EventRendererManager + console.log('๐ŸŽจ Triggering event rendering...'); + const gridContainer = document.querySelector('swp-calendar-container'); + if (gridContainer) { + const periodRange = this.gridManager.getDisplayDates(); + this.eventBus.emit(CoreEvents.GRID_RENDERED, { + container: gridContainer, + currentDate: new Date(), + startDate: periodRange[0], + endDate: periodRange[periodRange.length - 1], + columnCount: periodRange.length + }); + } + // Step 3: Initialize scroll synchronization console.log('๐Ÿ“œ Setting up scroll synchronization...'); this.scrollManager.initialize(); diff --git a/src/managers/EventManager.ts b/src/managers/EventManager.ts index 2f7e79b..8e2770f 100644 --- a/src/managers/EventManager.ts +++ b/src/managers/EventManager.ts @@ -122,6 +122,7 @@ export class EventManager { * Get events for a specific time period */ public getEventsForPeriod(startDate: Date, endDate: Date): CalendarEvent[] { + console.log(`EventManager.getEventsForPeriod: Checking ${this.events.length} events for period ${startDate.toDateString()} - ${endDate.toDateString()}`); return this.events.filter(event => { const eventStart = new Date(event.start); const eventEnd = new Date(event.end); diff --git a/src/managers/GridManager.ts b/src/managers/GridManager.ts index f751751..4d82bbc 100644 --- a/src/managers/GridManager.ts +++ b/src/managers/GridManager.ts @@ -7,7 +7,6 @@ import { eventBus } from '../core/EventBus'; import { calendarConfig } from '../core/CalendarConfig'; import { CoreEvents } from '../constants/CoreEvents'; import { ResourceCalendarData, CalendarView } from '../types/CalendarTypes'; -import { AllDayEvent } from '../types/EventTypes'; import { ViewStrategy, ViewContext } from '../strategies/ViewStrategy'; import { WeekViewStrategy } from '../strategies/WeekViewStrategy'; import { MonthViewStrategy } from '../strategies/MonthViewStrategy'; @@ -18,7 +17,6 @@ import { MonthViewStrategy } from '../strategies/MonthViewStrategy'; export class GridManager { private container: HTMLElement | null = null; private currentDate: Date = new Date(); - private allDayEvents: AllDayEvent[] = []; private resourceData: ResourceCalendarData | null = null; private currentStrategy: ViewStrategy; private eventCleanup: (() => void)[] = []; @@ -53,15 +51,6 @@ export class GridManager { }) ); - // Listen for data changes - - this.eventCleanup.push( - eventBus.on(CoreEvents.DATA_LOADED, (e: Event) => { - const detail = (e as CustomEvent).detail; - this.updateAllDayEvents(detail.events); - }) - ); - // Listen for config changes that affect rendering this.eventCleanup.push( eventBus.on(CoreEvents.REFRESH_REQUESTED, (e: Event) => { @@ -127,7 +116,6 @@ export class GridManager { const context: ViewContext = { currentDate: this.currentDate, container: this.container, - allDayEvents: this.allDayEvents, resourceData: this.resourceData }; @@ -155,14 +143,6 @@ export class GridManager { console.log(`โœ… Grid rendered with ${layoutConfig.columnCount} columns`); } - /** - * Update all-day events and re-render - */ - private updateAllDayEvents(events: AllDayEvent[]): void { - console.log(`GridManager: Updating ${events.length} all-day events`); - this.allDayEvents = events.filter(event => event.allDay); - this.render(); - } /** * Get current period label from strategy @@ -239,7 +219,6 @@ export class GridManager { // Clear references this.container = null; - this.allDayEvents = []; this.resourceData = null; } } \ No newline at end of file diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index e0d229b..1fc2b6a 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -29,16 +29,22 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { // clearEvents() would remove events from all containers, breaking the animation // Events are now rendered directly into the new container without clearing - // Events should already be filtered by EventManager - no need to filter here - console.log('BaseEventRenderer: Rendering', events.length, 'pre-filtered events'); + // Separate all-day events from regular events + const allDayEvents = events.filter(event => event.allDay); + const regularEvents = events.filter(event => !event.allDay); + + console.log(`BaseEventRenderer: Rendering ${allDayEvents.length} all-day events and ${regularEvents.length} regular events`); - // Find columns in the specific container + // Always call renderAllDayEvents to ensure height is set correctly (even to 0) + this.renderAllDayEvents(allDayEvents, container, config); + + // Find columns in the specific container for regular events const columns = this.getColumns(container); console.log(`BaseEventRenderer: Found ${columns.length} columns in container`); columns.forEach(column => { - const columnEvents = this.getEventsForColumn(column, events); - console.log(`BaseEventRenderer: Rendering ${columnEvents.length} events in column`); + const columnEvents = this.getEventsForColumn(column, regularEvents); + console.log(`BaseEventRenderer: Rendering ${columnEvents.length} regular events in column`); const eventsLayer = column.querySelector('swp-events-layer'); if (eventsLayer) { @@ -60,6 +66,143 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { protected abstract getColumns(container: HTMLElement): HTMLElement[]; protected abstract getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[]; + /** + * Render all-day events in the header row 2 + */ + protected renderAllDayEvents(allDayEvents: CalendarEvent[], container: HTMLElement, config: CalendarConfig): void { + console.log(`BaseEventRenderer: Rendering ${allDayEvents.length} all-day events`); + + // Find the calendar header + const calendarHeader = container.querySelector('swp-calendar-header'); + if (!calendarHeader) { + console.warn('BaseEventRenderer: No calendar header found for all-day events'); + return; + } + + // Clear any existing all-day containers first + const existingContainers = calendarHeader.querySelectorAll('swp-allday-container'); + existingContainers.forEach(container => container.remove()); + + // Track maximum number of stacked events to calculate row height + let maxStackHeight = 0; + + // Get day headers to build date map + const dayHeaders = calendarHeader.querySelectorAll('swp-day-header'); + const dateToColumnMap = new Map(); + const visibleDates: string[] = []; + + dayHeaders.forEach((header, index) => { + const dateStr = (header as any).dataset.date; + if (dateStr) { + dateToColumnMap.set(dateStr, index + 1); // 1-based column index + visibleDates.push(dateStr); + } + }); + + // Group events by their start column for container creation + const eventsByStartColumn = new Map(); + + allDayEvents.forEach(event => { + const startDate = new Date(event.start); + const startDateKey = this.dateCalculator.formatISODate(startDate); + const startColumn = dateToColumnMap.get(startDateKey); + + if (!startColumn) { + console.log(`BaseEventRenderer: Event "${event.title}" starts outside visible week`); + return; + } + + // Store event with its start column + if (!eventsByStartColumn.has(startColumn)) { + eventsByStartColumn.set(startColumn, []); + } + eventsByStartColumn.get(startColumn)!.push(event); + }); + + // Create containers and render events + eventsByStartColumn.forEach((events, startColumn) => { + events.forEach(event => { + const startDate = new Date(event.start); + const endDate = new Date(event.end); + + // Calculate span + let endColumn = startColumn; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + currentDate.setDate(currentDate.getDate() + 1); + const dateKey = this.dateCalculator.formatISODate(currentDate); + const col = dateToColumnMap.get(dateKey); + if (col) { + endColumn = col; + } else { + break; + } + } + + const columnSpan = endColumn - startColumn + 1; + + // Create or find container for this column span + const containerKey = `${startColumn}-${columnSpan}`; + let allDayContainer = calendarHeader.querySelector(`swp-allday-container[data-container-key="${containerKey}"]`); + + if (!allDayContainer) { + // Create container that spans the appropriate columns + allDayContainer = document.createElement('swp-allday-container'); + allDayContainer.setAttribute('data-container-key', containerKey); + (allDayContainer as HTMLElement).style.gridColumn = columnSpan > 1 + ? `${startColumn} / span ${columnSpan}` + : `${startColumn}`; + (allDayContainer as HTMLElement).style.gridRow = '2'; + calendarHeader.appendChild(allDayContainer); + } + + // Create the all-day event element inside container + const allDayEvent = document.createElement('swp-allday-event'); + allDayEvent.textContent = event.title; + allDayEvent.setAttribute('data-event-id', event.id); + allDayEvent.setAttribute('data-type', event.type || 'work'); + + // Use event metadata for color if available + if (event.metadata?.color) { + (allDayEvent as HTMLElement).style.backgroundColor = event.metadata.color; + } + + console.log(`BaseEventRenderer: All-day event "${event.title}" in container spanning columns ${startColumn} to ${endColumn}`); + + allDayContainer.appendChild(allDayEvent); + + // Track max stack height + const containerEventCount = allDayContainer.querySelectorAll('swp-allday-event').length; + if (containerEventCount > maxStackHeight) { + maxStackHeight = containerEventCount; + } + }); + }); + + // Calculate and set the all-day row height based on max stack + // Each event is 22px height + 2px gap + const eventHeight = 22; + const gap = 2; + const padding = 4; // Container padding (2px top + 2px bottom) + const calculatedHeight = maxStackHeight > 0 + ? (maxStackHeight * eventHeight) + ((maxStackHeight - 1) * gap) + padding + : 0; // No height if no events + + // Set CSS variable for row height + const root = document.documentElement; + root.style.setProperty('--all-day-row-height', `${calculatedHeight}px`); + + // Also update header-spacer height + const headerSpacer = container.querySelector('swp-header-spacer'); + if (headerSpacer) { + const headerHeight = parseInt(getComputedStyle(root).getPropertyValue('--header-height') || '80'); + (headerSpacer as HTMLElement).style.height = `${headerHeight + calculatedHeight}px`; + } + + console.log(`BaseEventRenderer: Set all-day row height to ${calculatedHeight}px (max stack: ${maxStackHeight})`); + } + protected renderEvent(event: CalendarEvent, container: Element, config: CalendarConfig): void { const eventElement = document.createElement('swp-event'); eventElement.dataset.eventId = event.id; diff --git a/src/renderers/GridRenderer.ts b/src/renderers/GridRenderer.ts index 877a540..656bc79 100644 --- a/src/renderers/GridRenderer.ts +++ b/src/renderers/GridRenderer.ts @@ -3,7 +3,6 @@ import { ResourceCalendarData } from '../types/CalendarTypes'; import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; import { HeaderRenderContext } from './HeaderRenderer'; import { ColumnRenderContext } from './ColumnRenderer'; -import { AllDayEvent } from '../types/EventTypes'; /** * GridRenderer - Handles DOM rendering for the calendar grid * Separated from GridManager to follow Single Responsibility Principle @@ -21,8 +20,7 @@ export class GridRenderer { public renderGrid( grid: HTMLElement, currentWeek: Date, - resourceData: ResourceCalendarData | null, - allDayEvents: AllDayEvent[] + resourceData: ResourceCalendarData | null ): void { console.log('GridRenderer: renderGrid called', { hasGrid: !!grid, @@ -41,11 +39,11 @@ export class GridRenderer { // Create POC structure: header-spacer + time-axis + grid-container this.createHeaderSpacer(grid); this.createTimeAxis(grid); - this.createGridContainer(grid, currentWeek, resourceData, allDayEvents); + this.createGridContainer(grid, currentWeek, resourceData); } else { console.log('GridRenderer: Re-render - updating existing structure'); // Just update the calendar header for all-day events - this.updateCalendarHeader(grid, currentWeek, resourceData, allDayEvents); + this.updateCalendarHeader(grid, currentWeek, resourceData); } console.log('GridRenderer: Grid rendered successfully with POC structure'); @@ -89,14 +87,13 @@ export class GridRenderer { private createGridContainer( grid: HTMLElement, currentWeek: Date, - resourceData: ResourceCalendarData | null, - allDayEvents: AllDayEvent[] + resourceData: ResourceCalendarData | null ): void { const gridContainer = document.createElement('swp-grid-container'); // Create calendar header using Strategy Pattern const calendarHeader = document.createElement('swp-calendar-header'); - this.renderCalendarHeader(calendarHeader, currentWeek, resourceData, allDayEvents); + this.renderCalendarHeader(calendarHeader, currentWeek, resourceData); gridContainer.appendChild(calendarHeader); // Create scrollable content @@ -124,8 +121,7 @@ export class GridRenderer { private renderCalendarHeader( calendarHeader: HTMLElement, currentWeek: Date, - resourceData: ResourceCalendarData | null, - allDayEvents: AllDayEvent[] + resourceData: ResourceCalendarData | null ): void { const calendarType = this.config.getCalendarMode(); const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType); @@ -133,7 +129,6 @@ export class GridRenderer { const context: HeaderRenderContext = { currentWeek: currentWeek, config: this.config, - allDayEvents: allDayEvents, resourceData: resourceData }; @@ -167,8 +162,7 @@ export class GridRenderer { private updateCalendarHeader( grid: HTMLElement, currentWeek: Date, - resourceData: ResourceCalendarData | null, - allDayEvents: AllDayEvent[] + resourceData: ResourceCalendarData | null ): void { const calendarHeader = grid.querySelector('swp-calendar-header'); if (!calendarHeader) return; @@ -177,6 +171,6 @@ export class GridRenderer { calendarHeader.innerHTML = ''; // Re-render headers using Strategy Pattern - this.renderCalendarHeader(calendarHeader as HTMLElement, currentWeek, resourceData, allDayEvents); + this.renderCalendarHeader(calendarHeader as HTMLElement, currentWeek, resourceData); } } \ No newline at end of file diff --git a/src/renderers/HeaderRenderer.ts b/src/renderers/HeaderRenderer.ts index da383ae..d6bc4f3 100644 --- a/src/renderers/HeaderRenderer.ts +++ b/src/renderers/HeaderRenderer.ts @@ -17,7 +17,6 @@ export interface HeaderRenderer { export interface HeaderRenderContext { currentWeek: Date; config: CalendarConfig; - allDayEvents?: any[]; resourceData?: ResourceCalendarData | null; } @@ -53,74 +52,6 @@ export class DateHeaderRenderer implements HeaderRenderer { calendarHeader.appendChild(header); }); - - // Render all-day events in row 2 - this.renderAllDayEvents(calendarHeader, context); - } - - private renderAllDayEvents(calendarHeader: HTMLElement, context: HeaderRenderContext): void { - const { currentWeek, config, allDayEvents = [] } = context; - - const dates = this.dateCalculator.getWorkWeekDates(currentWeek); - const weekDays = config.getDateViewSettings().weekDays; - const daysToShow = dates.slice(0, weekDays); - - // TEST: Add a simple test event for Monday (column 1) - const testEvent = document.createElement('swp-allday-event'); - testEvent.textContent = 'TEST ALL-DAY EVENT'; - testEvent.style.gridColumn = '1'; - testEvent.style.gridRow = '2'; - testEvent.style.backgroundColor = 'orange'; - testEvent.style.color = 'white'; - testEvent.style.padding = '4px'; - testEvent.style.fontSize = '12px'; - testEvent.style.fontWeight = 'bold'; - calendarHeader.appendChild(testEvent); - console.log('๐Ÿงช Added test all-day event to row 2, column 1'); - - // Process each all-day event to calculate its span - allDayEvents.forEach(event => { - const startDate = new Date(event.start); - const endDate = new Date(event.end); - - // Find start and end column indices - let startColumnIndex = -1; - let endColumnIndex = -1; - - daysToShow.forEach((date, index) => { - const dateStr = this.dateCalculator.formatISODate(date); - const startDateStr = this.dateCalculator.formatISODate(startDate); - - if (dateStr === startDateStr) { - startColumnIndex = index; - } - - // For end date, we need to check if the event spans to this day - if (date <= endDate) { - endColumnIndex = index; - } - }); - - // Only render if the event starts within the visible week - if (startColumnIndex >= 0) { - // If end column is not found or is before start, default to single day - if (endColumnIndex < startColumnIndex) { - endColumnIndex = startColumnIndex; - } - - const allDayEvent = document.createElement('swp-allday-event'); - allDayEvent.textContent = event.title; - - // Set grid column span: start column (1-based) to end column + 1 (1-based) - const gridColumnStart = startColumnIndex + 1; - const gridColumnEnd = endColumnIndex + 2; - allDayEvent.style.gridColumn = `${gridColumnStart} / ${gridColumnEnd}`; - // Color is now handled by CSS classes based on event type - allDayEvent.dataset.type = event.type || 'work'; - - calendarHeader.appendChild(allDayEvent); - } - }); } diff --git a/src/strategies/ViewStrategy.ts b/src/strategies/ViewStrategy.ts index 7594ac9..578364c 100644 --- a/src/strategies/ViewStrategy.ts +++ b/src/strategies/ViewStrategy.ts @@ -3,7 +3,6 @@ * Allows clean separation between week view, month view, day view etc. */ -import { AllDayEvent } from '../types/EventTypes'; import { ResourceCalendarData } from '../types/CalendarTypes'; /** @@ -12,7 +11,6 @@ import { ResourceCalendarData } from '../types/CalendarTypes'; export interface ViewContext { currentDate: Date; container: HTMLElement; - allDayEvents: AllDayEvent[]; resourceData: ResourceCalendarData | null; } diff --git a/src/strategies/WeekViewStrategy.ts b/src/strategies/WeekViewStrategy.ts index 83dca7f..21e307f 100644 --- a/src/strategies/WeekViewStrategy.ts +++ b/src/strategies/WeekViewStrategy.ts @@ -39,8 +39,7 @@ export class WeekViewStrategy implements ViewStrategy { this.gridRenderer.renderGrid( context.container, context.currentDate, - context.resourceData, - context.allDayEvents + context.resourceData ); console.log(`Week grid rendered with ${this.getLayoutConfig().columnCount} columns`); diff --git a/wwwroot/css/calendar-base-css.css b/wwwroot/css/calendar-base-css.css index 44c0e05..b033710 100644 --- a/wwwroot/css/calendar-base-css.css +++ b/wwwroot/css/calendar-base-css.css @@ -16,7 +16,7 @@ --day-column-min-width: 250px; --week-days: 7; --header-height: 80px; - --all-day-row-height: 40px; /* Default height for all-day events row */ + --all-day-row-height: 0px; /* Default height for all-day events row */ /* Time boundaries - Default fallback values */ --day-start-hour: 0; diff --git a/wwwroot/css/calendar-layout-css.css b/wwwroot/css/calendar-layout-css.css index bce3227..8eadd27 100644 --- a/wwwroot/css/calendar-layout-css.css +++ b/wwwroot/css/calendar-layout-css.css @@ -266,10 +266,20 @@ swp-day-header[data-today="true"] swp-day-date { } -/* All-day events in row 2 */ +/* All-day event container - spans columns as needed */ +swp-allday-container { + display: grid; + grid-template-columns: 1fr; /* Single column for now, can expand later */ + grid-auto-rows: min-content; + gap: 2px; + padding: 2px; + height: 100%; + overflow: hidden; +} + +/* All-day events in containers */ swp-allday-event { - grid-row: 2; /* Row 2 only */ - height: calc(var(--all-day-row-height) - 3px); /* Dynamic height minus margin */ + height: 22px; /* Fixed height for consistent stacking */ background: #ff9800; /* Default orange background */ display: flex; align-items: center; @@ -277,16 +287,15 @@ swp-allday-event { color: #fff; font-size: 0.75rem; padding: 2px 4px; - margin: 1px; border-radius: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - border-right: 1px solid rgba(0, 0, 0, 0.1); + border-left: 3px solid rgba(0, 0, 0, 0.2); } swp-allday-event:last-child { - border-right: none; /* Remove border from last all-day event */ + margin-bottom: 0; } /* Scrollable content */