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
This commit is contained in:
Janus Knudsen 2025-08-24 00:13:07 +02:00
parent 25522bfe17
commit 9c65143df2
11 changed files with 190 additions and 121 deletions

View file

@ -692,7 +692,7 @@
{ {
"id": "70", "id": "70",
"title": "Summer Festival", "title": "Summer Festival",
"start": "2025-08-15T00:00:00", "start": "2025-08-14T00:00:00",
"end": "2025-08-16T23:59:59", "end": "2025-08-16T23:59:59",
"type": "milestone", "type": "milestone",
"allDay": true, "allDay": true,

View file

@ -74,6 +74,21 @@ export class CalendarManager {
} }
await this.gridManager.render(); 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 // Step 3: Initialize scroll synchronization
console.log('📜 Setting up scroll synchronization...'); console.log('📜 Setting up scroll synchronization...');
this.scrollManager.initialize(); this.scrollManager.initialize();

View file

@ -122,6 +122,7 @@ export class EventManager {
* Get events for a specific time period * Get events for a specific time period
*/ */
public getEventsForPeriod(startDate: Date, endDate: Date): CalendarEvent[] { 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 => { return this.events.filter(event => {
const eventStart = new Date(event.start); const eventStart = new Date(event.start);
const eventEnd = new Date(event.end); const eventEnd = new Date(event.end);

View file

@ -7,7 +7,6 @@ import { eventBus } from '../core/EventBus';
import { calendarConfig } from '../core/CalendarConfig'; import { calendarConfig } from '../core/CalendarConfig';
import { CoreEvents } from '../constants/CoreEvents'; import { CoreEvents } from '../constants/CoreEvents';
import { ResourceCalendarData, CalendarView } from '../types/CalendarTypes'; import { ResourceCalendarData, CalendarView } from '../types/CalendarTypes';
import { AllDayEvent } from '../types/EventTypes';
import { ViewStrategy, ViewContext } from '../strategies/ViewStrategy'; import { ViewStrategy, ViewContext } from '../strategies/ViewStrategy';
import { WeekViewStrategy } from '../strategies/WeekViewStrategy'; import { WeekViewStrategy } from '../strategies/WeekViewStrategy';
import { MonthViewStrategy } from '../strategies/MonthViewStrategy'; import { MonthViewStrategy } from '../strategies/MonthViewStrategy';
@ -18,7 +17,6 @@ import { MonthViewStrategy } from '../strategies/MonthViewStrategy';
export class GridManager { export class GridManager {
private container: HTMLElement | null = null; private container: HTMLElement | null = null;
private currentDate: Date = new Date(); private currentDate: Date = new Date();
private allDayEvents: AllDayEvent[] = [];
private resourceData: ResourceCalendarData | null = null; private resourceData: ResourceCalendarData | null = null;
private currentStrategy: ViewStrategy; private currentStrategy: ViewStrategy;
private eventCleanup: (() => void)[] = []; 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 // Listen for config changes that affect rendering
this.eventCleanup.push( this.eventCleanup.push(
eventBus.on(CoreEvents.REFRESH_REQUESTED, (e: Event) => { eventBus.on(CoreEvents.REFRESH_REQUESTED, (e: Event) => {
@ -127,7 +116,6 @@ export class GridManager {
const context: ViewContext = { const context: ViewContext = {
currentDate: this.currentDate, currentDate: this.currentDate,
container: this.container, container: this.container,
allDayEvents: this.allDayEvents,
resourceData: this.resourceData resourceData: this.resourceData
}; };
@ -155,14 +143,6 @@ export class GridManager {
console.log(`✅ Grid rendered with ${layoutConfig.columnCount} columns`); 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 * Get current period label from strategy
@ -239,7 +219,6 @@ export class GridManager {
// Clear references // Clear references
this.container = null; this.container = null;
this.allDayEvents = [];
this.resourceData = null; this.resourceData = null;
} }
} }

View file

@ -29,16 +29,22 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
// clearEvents() would remove events from all containers, breaking the animation // clearEvents() would remove events from all containers, breaking the animation
// Events are now rendered directly into the new container without clearing // Events are now rendered directly into the new container without clearing
// Events should already be filtered by EventManager - no need to filter here // Separate all-day events from regular events
console.log('BaseEventRenderer: Rendering', events.length, 'pre-filtered 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); const columns = this.getColumns(container);
console.log(`BaseEventRenderer: Found ${columns.length} columns in container`); console.log(`BaseEventRenderer: Found ${columns.length} columns in container`);
columns.forEach(column => { columns.forEach(column => {
const columnEvents = this.getEventsForColumn(column, events); const columnEvents = this.getEventsForColumn(column, regularEvents);
console.log(`BaseEventRenderer: Rendering ${columnEvents.length} events in column`); console.log(`BaseEventRenderer: Rendering ${columnEvents.length} regular events in column`);
const eventsLayer = column.querySelector('swp-events-layer'); const eventsLayer = column.querySelector('swp-events-layer');
if (eventsLayer) { if (eventsLayer) {
@ -60,6 +66,143 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
protected abstract getColumns(container: HTMLElement): HTMLElement[]; protected abstract getColumns(container: HTMLElement): HTMLElement[];
protected abstract getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[]; 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<string, number>();
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<number, CalendarEvent[]>();
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 { protected renderEvent(event: CalendarEvent, container: Element, config: CalendarConfig): void {
const eventElement = document.createElement('swp-event'); const eventElement = document.createElement('swp-event');
eventElement.dataset.eventId = event.id; eventElement.dataset.eventId = event.id;

View file

@ -3,7 +3,6 @@ import { ResourceCalendarData } from '../types/CalendarTypes';
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
import { HeaderRenderContext } from './HeaderRenderer'; import { HeaderRenderContext } from './HeaderRenderer';
import { ColumnRenderContext } from './ColumnRenderer'; import { ColumnRenderContext } from './ColumnRenderer';
import { AllDayEvent } from '../types/EventTypes';
/** /**
* GridRenderer - Handles DOM rendering for the calendar grid * GridRenderer - Handles DOM rendering for the calendar grid
* Separated from GridManager to follow Single Responsibility Principle * Separated from GridManager to follow Single Responsibility Principle
@ -21,8 +20,7 @@ export class GridRenderer {
public renderGrid( public renderGrid(
grid: HTMLElement, grid: HTMLElement,
currentWeek: Date, currentWeek: Date,
resourceData: ResourceCalendarData | null, resourceData: ResourceCalendarData | null
allDayEvents: AllDayEvent[]
): void { ): void {
console.log('GridRenderer: renderGrid called', { console.log('GridRenderer: renderGrid called', {
hasGrid: !!grid, hasGrid: !!grid,
@ -41,11 +39,11 @@ export class GridRenderer {
// Create POC structure: header-spacer + time-axis + grid-container // Create POC structure: header-spacer + time-axis + grid-container
this.createHeaderSpacer(grid); this.createHeaderSpacer(grid);
this.createTimeAxis(grid); this.createTimeAxis(grid);
this.createGridContainer(grid, currentWeek, resourceData, allDayEvents); this.createGridContainer(grid, currentWeek, resourceData);
} else { } else {
console.log('GridRenderer: Re-render - updating existing structure'); console.log('GridRenderer: Re-render - updating existing structure');
// Just update the calendar header for all-day events // 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'); console.log('GridRenderer: Grid rendered successfully with POC structure');
@ -89,14 +87,13 @@ export class GridRenderer {
private createGridContainer( private createGridContainer(
grid: HTMLElement, grid: HTMLElement,
currentWeek: Date, currentWeek: Date,
resourceData: ResourceCalendarData | null, resourceData: ResourceCalendarData | null
allDayEvents: AllDayEvent[]
): void { ): void {
const gridContainer = document.createElement('swp-grid-container'); const gridContainer = document.createElement('swp-grid-container');
// Create calendar header using Strategy Pattern // Create calendar header using Strategy Pattern
const calendarHeader = document.createElement('swp-calendar-header'); const calendarHeader = document.createElement('swp-calendar-header');
this.renderCalendarHeader(calendarHeader, currentWeek, resourceData, allDayEvents); this.renderCalendarHeader(calendarHeader, currentWeek, resourceData);
gridContainer.appendChild(calendarHeader); gridContainer.appendChild(calendarHeader);
// Create scrollable content // Create scrollable content
@ -124,8 +121,7 @@ export class GridRenderer {
private renderCalendarHeader( private renderCalendarHeader(
calendarHeader: HTMLElement, calendarHeader: HTMLElement,
currentWeek: Date, currentWeek: Date,
resourceData: ResourceCalendarData | null, resourceData: ResourceCalendarData | null
allDayEvents: AllDayEvent[]
): void { ): void {
const calendarType = this.config.getCalendarMode(); const calendarType = this.config.getCalendarMode();
const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType); const headerRenderer = CalendarTypeFactory.getHeaderRenderer(calendarType);
@ -133,7 +129,6 @@ export class GridRenderer {
const context: HeaderRenderContext = { const context: HeaderRenderContext = {
currentWeek: currentWeek, currentWeek: currentWeek,
config: this.config, config: this.config,
allDayEvents: allDayEvents,
resourceData: resourceData resourceData: resourceData
}; };
@ -167,8 +162,7 @@ export class GridRenderer {
private updateCalendarHeader( private updateCalendarHeader(
grid: HTMLElement, grid: HTMLElement,
currentWeek: Date, currentWeek: Date,
resourceData: ResourceCalendarData | null, resourceData: ResourceCalendarData | null
allDayEvents: AllDayEvent[]
): void { ): void {
const calendarHeader = grid.querySelector('swp-calendar-header'); const calendarHeader = grid.querySelector('swp-calendar-header');
if (!calendarHeader) return; if (!calendarHeader) return;
@ -177,6 +171,6 @@ export class GridRenderer {
calendarHeader.innerHTML = ''; calendarHeader.innerHTML = '';
// Re-render headers using Strategy Pattern // Re-render headers using Strategy Pattern
this.renderCalendarHeader(calendarHeader as HTMLElement, currentWeek, resourceData, allDayEvents); this.renderCalendarHeader(calendarHeader as HTMLElement, currentWeek, resourceData);
} }
} }

View file

@ -17,7 +17,6 @@ export interface HeaderRenderer {
export interface HeaderRenderContext { export interface HeaderRenderContext {
currentWeek: Date; currentWeek: Date;
config: CalendarConfig; config: CalendarConfig;
allDayEvents?: any[];
resourceData?: ResourceCalendarData | null; resourceData?: ResourceCalendarData | null;
} }
@ -53,74 +52,6 @@ export class DateHeaderRenderer implements HeaderRenderer {
calendarHeader.appendChild(header); 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);
}
});
} }

View file

@ -3,7 +3,6 @@
* Allows clean separation between week view, month view, day view etc. * Allows clean separation between week view, month view, day view etc.
*/ */
import { AllDayEvent } from '../types/EventTypes';
import { ResourceCalendarData } from '../types/CalendarTypes'; import { ResourceCalendarData } from '../types/CalendarTypes';
/** /**
@ -12,7 +11,6 @@ import { ResourceCalendarData } from '../types/CalendarTypes';
export interface ViewContext { export interface ViewContext {
currentDate: Date; currentDate: Date;
container: HTMLElement; container: HTMLElement;
allDayEvents: AllDayEvent[];
resourceData: ResourceCalendarData | null; resourceData: ResourceCalendarData | null;
} }

View file

@ -39,8 +39,7 @@ export class WeekViewStrategy implements ViewStrategy {
this.gridRenderer.renderGrid( this.gridRenderer.renderGrid(
context.container, context.container,
context.currentDate, context.currentDate,
context.resourceData, context.resourceData
context.allDayEvents
); );
console.log(`Week grid rendered with ${this.getLayoutConfig().columnCount} columns`); console.log(`Week grid rendered with ${this.getLayoutConfig().columnCount} columns`);

View file

@ -16,7 +16,7 @@
--day-column-min-width: 250px; --day-column-min-width: 250px;
--week-days: 7; --week-days: 7;
--header-height: 80px; --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 */ /* Time boundaries - Default fallback values */
--day-start-hour: 0; --day-start-hour: 0;

View file

@ -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 { swp-allday-event {
grid-row: 2; /* Row 2 only */ height: 22px; /* Fixed height for consistent stacking */
height: calc(var(--all-day-row-height) - 3px); /* Dynamic height minus margin */
background: #ff9800; /* Default orange background */ background: #ff9800; /* Default orange background */
display: flex; display: flex;
align-items: center; align-items: center;
@ -277,16 +287,15 @@ swp-allday-event {
color: #fff; color: #fff;
font-size: 0.75rem; font-size: 0.75rem;
padding: 2px 4px; padding: 2px 4px;
margin: 1px;
border-radius: 3px; border-radius: 3px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; 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 { swp-allday-event:last-child {
border-right: none; /* Remove border from last all-day event */ margin-bottom: 0;
} }
/* Scrollable content */ /* Scrollable content */