Refactor event rendering with column-based event management

Improves event rendering by integrating event filtering directly into column data sources

Key changes:
- Moves event filtering responsibility to IColumnDataSource
- Simplifies event rendering pipeline by pre-filtering events per column
- Supports both date and resource-based calendar modes
- Enhances drag and drop event update mechanism

Optimizes calendar rendering performance and flexibility
This commit is contained in:
Janus C. H. Knudsen 2025-11-22 23:38:52 +01:00
parent eeaeddeef8
commit 17909696ed
9 changed files with 179 additions and 250 deletions

View file

@ -2,6 +2,7 @@ import { IColumnDataSource, IColumnInfo } from '../types/ColumnDataSource';
import { DateService } from '../utils/DateService'; import { DateService } from '../utils/DateService';
import { Configuration } from '../configurations/CalendarConfig'; import { Configuration } from '../configurations/CalendarConfig';
import { CalendarView } from '../types/CalendarTypes'; import { CalendarView } from '../types/CalendarTypes';
import { EventService } from '../storage/events/EventService';
/** /**
* DateColumnDataSource - Provides date-based columns * DateColumnDataSource - Provides date-based columns
@ -10,25 +11,31 @@ import { CalendarView } from '../types/CalendarTypes';
* - Current date * - Current date
* - Current view (day/week/month) * - Current view (day/week/month)
* - Workweek settings * - Workweek settings
*
* Also fetches and filters events per column using EventService.
*/ */
export class DateColumnDataSource implements IColumnDataSource { export class DateColumnDataSource implements IColumnDataSource {
private dateService: DateService; private dateService: DateService;
private config: Configuration; private config: Configuration;
private eventService: EventService;
private currentDate: Date; private currentDate: Date;
private currentView: CalendarView; private currentView: CalendarView;
constructor( constructor(
dateService: DateService, dateService: DateService,
config: Configuration config: Configuration,
eventService: EventService
) { ) {
this.dateService = dateService; this.dateService = dateService;
this.config = config; this.config = config;
this.eventService = eventService;
this.currentDate = new Date(); this.currentDate = new Date();
this.currentView = this.config.currentView; this.currentView = this.config.currentView;
} }
/** /**
* Get columns (dates) to display * Get columns (dates) to display with their events
* Each column fetches its own events directly from EventService
*/ */
public async getColumns(): Promise<IColumnInfo[]> { public async getColumns(): Promise<IColumnInfo[]> {
let dates: Date[]; let dates: Date[];
@ -47,11 +54,19 @@ export class DateColumnDataSource implements IColumnDataSource {
dates = this.getWeekDates(); dates = this.getWeekDates();
} }
// Convert Date[] to IColumnInfo[] // Fetch events for each column directly from EventService
return dates.map(date => ({ const columnsWithEvents = await Promise.all(
dates.map(async date => ({
identifier: this.dateService.formatISODate(date), identifier: this.dateService.formatISODate(date),
data: date data: date,
})); events: await this.eventService.getByDateRange(
this.dateService.startOfDay(date),
this.dateService.endOfDay(date)
)
}))
);
return columnsWithEvents;
} }
/** /**
@ -61,6 +76,13 @@ export class DateColumnDataSource implements IColumnDataSource {
return 'date'; return 'date';
} }
/**
* Check if this datasource is in resource mode
*/
public isResource(): boolean {
return false;
}
/** /**
* Update current date * Update current date
*/ */

View file

@ -1,34 +1,52 @@
import { IColumnDataSource, IColumnInfo } from '../types/ColumnDataSource'; import { IColumnDataSource, IColumnInfo } from '../types/ColumnDataSource';
import { CalendarView } from '../types/CalendarTypes'; import { CalendarView } from '../types/CalendarTypes';
import { ResourceService } from '../storage/resources/ResourceService'; import { ResourceService } from '../storage/resources/ResourceService';
import { EventService } from '../storage/events/EventService';
import { DateService } from '../utils/DateService';
/** /**
* ResourceColumnDataSource - Provides resource-based columns * ResourceColumnDataSource - Provides resource-based columns
* *
* In resource mode, columns represent resources (people, rooms, etc.) * In resource mode, columns represent resources (people, rooms, etc.)
* instead of dates. Events are still filtered by current date, * instead of dates. Events are filtered by current date AND resourceId.
* but grouped by resourceId.
*/ */
export class ResourceColumnDataSource implements IColumnDataSource { export class ResourceColumnDataSource implements IColumnDataSource {
private resourceService: ResourceService; private resourceService: ResourceService;
private eventService: EventService;
private dateService: DateService;
private currentDate: Date; private currentDate: Date;
private currentView: CalendarView; private currentView: CalendarView;
constructor(resourceService: ResourceService) { constructor(
resourceService: ResourceService,
eventService: EventService,
dateService: DateService
) {
this.resourceService = resourceService; this.resourceService = resourceService;
this.eventService = eventService;
this.dateService = dateService;
this.currentDate = new Date(); this.currentDate = new Date();
this.currentView = 'day'; this.currentView = 'day';
} }
/** /**
* Get columns (resources) to display * Get columns (resources) to display with their events
*/ */
public async getColumns(): Promise<IColumnInfo[]> { public async getColumns(): Promise<IColumnInfo[]> {
const resources = await this.resourceService.getActive(); const resources = await this.resourceService.getActive();
return resources.map(resource => ({ const startDate = this.dateService.startOfDay(this.currentDate);
const endDate = this.dateService.endOfDay(this.currentDate);
// Fetch events for each resource in parallel
const columnsWithEvents = await Promise.all(
resources.map(async resource => ({
identifier: resource.id, identifier: resource.id,
data: resource data: resource,
})); events: await this.eventService.getByResourceAndDateRange(resource.id, startDate, endDate)
}))
);
return columnsWithEvents;
} }
/** /**
@ -38,6 +56,13 @@ export class ResourceColumnDataSource implements IColumnDataSource {
return 'resource'; return 'resource';
} }
/**
* Check if this datasource is in resource mode
*/
public isResource(): boolean {
return true;
}
/** /**
* Update current date (for event filtering) * Update current date (for event filtering)
*/ */

View file

@ -112,19 +112,20 @@ export class SwpEventElement extends BaseSwpEventElement {
/** /**
* Update event position during drag * Update event position during drag
* @param columnDate - The date of the column * Uses the event's existing date, only updates the time based on Y position
* @param snappedY - The Y position in pixels * @param snappedY - The Y position in pixels
*/ */
public updatePosition(columnDate: Date, snappedY: number): void { public updatePosition(snappedY: number): void {
// 1. Update visual position // 1. Update visual position
this.style.top = `${snappedY + 1}px`; this.style.top = `${snappedY + 1}px`;
// 2. Calculate new timestamps // 2. Calculate new timestamps (keep existing date, only change time)
const existingDate = this.start;
const { startMinutes, endMinutes } = this.calculateTimesFromPosition(snappedY); const { startMinutes, endMinutes } = this.calculateTimesFromPosition(snappedY);
// 3. Update data attributes (triggers attributeChangedCallback) // 3. Update data attributes (triggers attributeChangedCallback)
const startDate = this.dateService.createDateAtTime(columnDate, startMinutes); const startDate = this.dateService.createDateAtTime(existingDate, startMinutes);
let endDate = this.dateService.createDateAtTime(columnDate, endMinutes); let endDate = this.dateService.createDateAtTime(existingDate, endMinutes);
// Handle cross-midnight events // Handle cross-midnight events
if (endMinutes >= 1440) { if (endMinutes >= 1440) {

View file

@ -140,9 +140,8 @@ async function initializeCalendar(): Promise<void> {
builder.registerType(MockResourceRepository).as<IApiRepository<IResource>>(); builder.registerType(MockResourceRepository).as<IApiRepository<IResource>>();
builder.registerType(MockAuditRepository).as<IApiRepository<IAuditEntry>>(); builder.registerType(MockAuditRepository).as<IApiRepository<IAuditEntry>>();
// Calendar mode: 'date' or 'resource' (default to resource)
const calendarMode: 'date' | 'resource' = 'resource';
let calendarMode = 'resource' ;
// Register DataSource and HeaderRenderer based on mode // Register DataSource and HeaderRenderer based on mode
if (calendarMode === 'resource') { if (calendarMode === 'resource') {
builder.registerType(ResourceColumnDataSource).as<IColumnDataSource>(); builder.registerType(ResourceColumnDataSource).as<IColumnDataSource>();
@ -155,6 +154,7 @@ async function initializeCalendar(): Promise<void> {
// Register entity services (sync status management) // Register entity services (sync status management)
// Open/Closed Principle: Adding new entity only requires adding one line here // Open/Closed Principle: Adding new entity only requires adding one line here
builder.registerType(EventService).as<IEntityService<ICalendarEvent>>(); builder.registerType(EventService).as<IEntityService<ICalendarEvent>>();
builder.registerType(EventService).as<EventService>();
builder.registerType(BookingService).as<IEntityService<IBooking>>(); builder.registerType(BookingService).as<IEntityService<IBooking>>();
builder.registerType(CustomerService).as<IEntityService<ICustomer>>(); builder.registerType(CustomerService).as<IEntityService<ICustomer>>();
builder.registerType(ResourceService).as<IEntityService<IResource>>(); builder.registerType(ResourceService).as<IEntityService<IResource>>();

View file

@ -1,6 +1,8 @@
/** /**
* GridManager - Simplified grid manager using centralized GridRenderer * GridManager - Simplified grid manager using centralized GridRenderer
* Delegates DOM rendering to GridRenderer, focuses on coordination * Delegates DOM rendering to GridRenderer, focuses on coordination
*
* Note: Events are now provided by IColumnDataSource (each column has its own events)
*/ */
import { eventBus } from '../core/EventBus'; import { eventBus } from '../core/EventBus';
@ -10,7 +12,6 @@ import { GridRenderer } from '../renderers/GridRenderer';
import { DateService } from '../utils/DateService'; import { DateService } from '../utils/DateService';
import { IColumnDataSource } from '../types/ColumnDataSource'; import { IColumnDataSource } from '../types/ColumnDataSource';
import { Configuration } from '../configurations/CalendarConfig'; import { Configuration } from '../configurations/CalendarConfig';
import { EventManager } from './EventManager';
/** /**
* Simplified GridManager focused on coordination, delegates rendering to GridRenderer * Simplified GridManager focused on coordination, delegates rendering to GridRenderer
@ -23,19 +24,16 @@ export class GridManager {
private dateService: DateService; private dateService: DateService;
private config: Configuration; private config: Configuration;
private dataSource: IColumnDataSource; private dataSource: IColumnDataSource;
private eventManager: EventManager;
constructor( constructor(
gridRenderer: GridRenderer, gridRenderer: GridRenderer,
dateService: DateService, dateService: DateService,
config: Configuration, config: Configuration,
eventManager: EventManager,
dataSource: IColumnDataSource dataSource: IColumnDataSource
) { ) {
this.gridRenderer = gridRenderer; this.gridRenderer = gridRenderer;
this.dateService = dateService; this.dateService = dateService;
this.config = config; this.config = config;
this.eventManager = eventManager;
this.dataSource = dataSource; this.dataSource = dataSource;
this.init(); this.init();
} }
@ -82,31 +80,25 @@ export class GridManager {
/** /**
* Main render method - delegates to GridRenderer * Main render method - delegates to GridRenderer
* Note: CSS variables are automatically updated by ConfigManager when config changes * Note: CSS variables are automatically updated by ConfigManager when config changes
* Note: Events are included in columns from IColumnDataSource
*/ */
public async render(): Promise<void> { public async render(): Promise<void> {
if (!this.container) { if (!this.container) {
return; return;
} }
// Get columns from datasource - single source of truth // Get columns from datasource - single source of truth (includes events per column)
const columns = await this.dataSource.getColumns(); const columns = await this.dataSource.getColumns();
// Set grid columns CSS variable based on actual column count // Set grid columns CSS variable based on actual column count
document.documentElement.style.setProperty('--grid-columns', columns.length.toString()); document.documentElement.style.setProperty('--grid-columns', columns.length.toString());
// Extract dates for EventManager query // Delegate to GridRenderer with columns (events are inside each column)
const dates = columns.map(col => col.data as Date);
const startDate = dates[0];
const endDate = dates[dates.length - 1];
const events = await this.eventManager.getEventsForPeriod(startDate, endDate);
// Delegate to GridRenderer with columns and events
this.gridRenderer.renderGrid( this.gridRenderer.renderGrid(
this.container, this.container,
this.currentDate, this.currentDate,
this.currentView, this.currentView,
columns, columns
events
); );
// Emit grid rendered event // Emit grid rendered event

View file

@ -1,6 +1,7 @@
// Event rendering strategy interface and implementations // Event rendering strategy interface and implementations
import { ICalendarEvent } from '../types/CalendarTypes'; import { ICalendarEvent } from '../types/CalendarTypes';
import { IColumnInfo } from '../types/ColumnDataSource';
import { Configuration } from '../configurations/CalendarConfig'; import { Configuration } from '../configurations/CalendarConfig';
import { SwpEventElement } from '../elements/SwpEventElement'; import { SwpEventElement } from '../elements/SwpEventElement';
import { PositionUtils } from '../utils/PositionUtils'; import { PositionUtils } from '../utils/PositionUtils';
@ -12,9 +13,12 @@ import { EventLayoutCoordinator, IGridGroupLayout, IStackedEventLayout } from '.
/** /**
* Interface for event rendering strategies * Interface for event rendering strategies
*
* Note: renderEvents now receives columns with pre-filtered events,
* not a flat array of events. Each column contains its own events.
*/ */
export interface IEventRenderer { export interface IEventRenderer {
renderEvents(events: ICalendarEvent[], container: HTMLElement): void; renderEvents(columns: IColumnInfo[], container: HTMLElement): void;
clearEvents(container?: HTMLElement): void; clearEvents(container?: HTMLElement): void;
renderSingleColumnEvents?(column: IColumnBounds, events: ICalendarEvent[]): void; renderSingleColumnEvents?(column: IColumnBounds, events: ICalendarEvent[]): void;
handleDragStart?(payload: IDragStartEventPayload): void; handleDragStart?(payload: IDragStartEventPayload): void;
@ -98,28 +102,22 @@ export class DateEventRenderer implements IEventRenderer {
/** /**
* Handle drag move event * Handle drag move event
* Only updates visual position and time - date stays the same
*/ */
public handleDragMove(payload: IDragMoveEventPayload): void { public handleDragMove(payload: IDragMoveEventPayload): void {
const swpEvent = payload.draggedClone as SwpEventElement; const swpEvent = payload.draggedClone as SwpEventElement;
const columnDate = this.dateService.parseISO(payload.columnBounds!!.identifier); swpEvent.updatePosition(payload.snappedY);
swpEvent.updatePosition(columnDate, payload.snappedY);
} }
/** /**
* Handle column change during drag * Handle column change during drag
* Only moves the element visually - no data updates here
* Data updates happen on drag:end in EventRenderingService
*/ */
public handleColumnChange(payload: IDragColumnChangeEventPayload): void { public handleColumnChange(payload: IDragColumnChangeEventPayload): void {
const eventsLayer = payload.newColumn.element.querySelector('swp-events-layer'); const eventsLayer = payload.newColumn.element.querySelector('swp-events-layer');
if (eventsLayer && payload.draggedClone.parentElement !== eventsLayer) { if (eventsLayer && payload.draggedClone.parentElement !== eventsLayer) {
eventsLayer.appendChild(payload.draggedClone); eventsLayer.appendChild(payload.draggedClone);
// Recalculate timestamps with new column date
const currentTop = parseFloat(payload.draggedClone.style.top) || 0;
const swpEvent = payload.draggedClone as SwpEventElement;
const columnDate = this.dateService.parseISO(payload.newColumn.identifier);
swpEvent.updatePosition(columnDate, currentTop);
} }
} }
@ -220,32 +218,36 @@ export class DateEventRenderer implements IEventRenderer {
} }
renderEvents(events: ICalendarEvent[], container: HTMLElement): void { renderEvents(columns: IColumnInfo[], container: HTMLElement): void {
// Find column DOM elements in the container
const columnElements = this.getColumns(container);
// Render events for each column using pre-filtered events from IColumnInfo
columns.forEach((columnInfo, index) => {
const columnElement = columnElements[index];
if (!columnElement) return;
// Filter out all-day events - they should be handled by AllDayEventRenderer // Filter out all-day events - they should be handled by AllDayEventRenderer
const timedEvents = events.filter(event => !event.allDay); const timedEvents = columnInfo.events.filter(event => !event.allDay);
// Find columns in the specific container for regular events const eventsLayer = columnElement.querySelector('swp-events-layer') as HTMLElement;
const columns = this.getColumns(container); if (eventsLayer && timedEvents.length > 0) {
this.renderColumnEvents(timedEvents, eventsLayer);
columns.forEach(column => {
const columnEvents = this.getEventsForColumn(column, timedEvents);
const eventsLayer = column.querySelector('swp-events-layer') as HTMLElement;
if (eventsLayer) {
this.renderColumnEvents(columnEvents, eventsLayer);
} }
}); });
} }
/** /**
* Render events for a single column * Render events for a single column
* Note: events are already filtered for this column
*/ */
public renderSingleColumnEvents(column: IColumnBounds, events: ICalendarEvent[]): void { public renderSingleColumnEvents(column: IColumnBounds, events: ICalendarEvent[]): void {
const columnEvents = this.getEventsForColumn(column.element, events); // Filter out all-day events
const timedEvents = events.filter(event => !event.allDay);
const eventsLayer = column.element.querySelector('swp-events-layer') as HTMLElement; const eventsLayer = column.element.querySelector('swp-events-layer') as HTMLElement;
if (eventsLayer) { if (eventsLayer && timedEvents.length > 0) {
this.renderColumnEvents(columnEvents, eventsLayer); this.renderColumnEvents(timedEvents, eventsLayer);
} }
} }
@ -388,24 +390,4 @@ export class DateEventRenderer implements IEventRenderer {
const columns = container.querySelectorAll('swp-day-column'); const columns = container.querySelectorAll('swp-day-column');
return Array.from(columns) as HTMLElement[]; return Array.from(columns) as HTMLElement[];
} }
protected getEventsForColumn(column: HTMLElement, events: ICalendarEvent[]): ICalendarEvent[] {
const columnId = column.dataset.columnId;
if (!columnId) {
return [];
}
// Create start and end of day for interval overlap check
// In date-mode, columnId is ISO date string like "2024-11-13"
const columnStart = this.dateService.parseISO(`${columnId}T00:00:00`);
const columnEnd = this.dateService.parseISO(`${columnId}T23:59:59.999`);
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;
});
return columnEvents;
}
} }

View file

@ -1,11 +1,12 @@
import { IEventBus, ICalendarEvent, IRenderContext } from '../types/CalendarTypes'; import { IEventBus } from '../types/CalendarTypes';
import { IColumnInfo, IColumnDataSource } from '../types/ColumnDataSource';
import { CoreEvents } from '../constants/CoreEvents'; import { CoreEvents } from '../constants/CoreEvents';
import { EventManager } from '../managers/EventManager'; import { EventManager } from '../managers/EventManager';
import { IEventRenderer } from './EventRenderer'; import { IEventRenderer } from './EventRenderer';
import { SwpEventElement } from '../elements/SwpEventElement'; import { SwpEventElement } from '../elements/SwpEventElement';
import { IDragStartEventPayload, IDragMoveEventPayload, IDragEndEventPayload, IDragMouseEnterHeaderEventPayload, IDragMouseLeaveHeaderEventPayload, IDragMouseEnterColumnEventPayload, IDragColumnChangeEventPayload, IHeaderReadyEventPayload, IResizeEndEventPayload } from '../types/EventTypes'; import { IDragStartEventPayload, IDragMoveEventPayload, IDragEndEventPayload, IDragMouseEnterHeaderEventPayload, IDragMouseLeaveHeaderEventPayload, IDragMouseEnterColumnEventPayload, IDragColumnChangeEventPayload, IResizeEndEventPayload } from '../types/EventTypes';
import { DateService } from '../utils/DateService'; import { DateService } from '../utils/DateService';
import { IColumnBounds, ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
/** /**
* EventRenderingService - Render events i DOM med positionering using Strategy Pattern * EventRenderingService - Render events i DOM med positionering using Strategy Pattern
* Håndterer event positioning og overlap detection * Håndterer event positioning og overlap detection
@ -14,6 +15,7 @@ export class EventRenderingService {
private eventBus: IEventBus; private eventBus: IEventBus;
private eventManager: EventManager; private eventManager: EventManager;
private strategy: IEventRenderer; private strategy: IEventRenderer;
private dataSource: IColumnDataSource;
private dateService: DateService; private dateService: DateService;
private dragMouseLeaveHeaderListener: ((event: Event) => void) | null = null; private dragMouseLeaveHeaderListener: ((event: Event) => void) | null = null;
@ -22,54 +24,18 @@ export class EventRenderingService {
eventBus: IEventBus, eventBus: IEventBus,
eventManager: EventManager, eventManager: EventManager,
strategy: IEventRenderer, strategy: IEventRenderer,
dataSource: IColumnDataSource,
dateService: DateService dateService: DateService
) { ) {
this.eventBus = eventBus; this.eventBus = eventBus;
this.eventManager = eventManager; this.eventManager = eventManager;
this.strategy = strategy; this.strategy = strategy;
this.dataSource = dataSource;
this.dateService = dateService; this.dateService = dateService;
this.setupEventListeners(); this.setupEventListeners();
} }
/**
* Render events in a specific container for a given period
*/
public async renderEvents(context: IRenderContext): Promise<void> {
// Clear existing events in the specific container first
this.strategy.clearEvents(context.container);
// Get events from EventManager for the period
const events = await this.eventManager.getEventsForPeriod(
context.startDate,
context.endDate
);
if (events.length === 0) {
return;
}
// Filter events by type - only render timed events here
const timedEvents = events.filter(event => !event.allDay);
console.log('🎯 EventRenderingService: Event filtering', {
totalEvents: events.length,
timedEvents: timedEvents.length,
allDayEvents: events.length - timedEvents.length
});
// Render timed events using existing strategy
if (timedEvents.length > 0) {
this.strategy.renderEvents(timedEvents, context.container);
}
// Emit EVENTS_RENDERED event for filtering system
this.eventBus.emit(CoreEvents.EVENTS_RENDERED, {
events: events,
container: context.container
});
}
private setupEventListeners(): void { private setupEventListeners(): void {
this.eventBus.on(CoreEvents.GRID_RENDERED, (event: Event) => { this.eventBus.on(CoreEvents.GRID_RENDERED, (event: Event) => {
@ -89,6 +55,7 @@ export class EventRenderingService {
/** /**
* Handle GRID_RENDERED event - render events in the current grid * Handle GRID_RENDERED event - render events in the current grid
* Events are now pre-filtered per column by IColumnDataSource
*/ */
private handleGridRendered(event: CustomEvent): void { private handleGridRendered(event: CustomEvent): void {
const { container, columns } = event.detail; const { container, columns } = event.detail;
@ -97,17 +64,23 @@ export class EventRenderingService {
return; return;
} }
// Extract dates from columns // Render events directly from columns (pre-filtered by IColumnDataSource)
const dates = columns.map((col: any) => col.data as Date); this.renderEventsFromColumns(container, columns);
}
// Calculate startDate and endDate from dates array /**
const startDate = dates[0]; * Render events from pre-filtered columns
const endDate = dates[dates.length - 1]; * Each column already contains its events (filtered by IColumnDataSource)
*/
private renderEventsFromColumns(container: HTMLElement, columns: IColumnInfo[]): void {
this.strategy.clearEvents(container);
this.strategy.renderEvents(columns, container);
this.renderEvents({ // Emit EVENTS_RENDERED for filtering system
container, const allEvents = columns.flatMap(col => col.events);
startDate, this.eventBus.emit(CoreEvents.EVENTS_RENDERED, {
endDate events: allEvents,
container: container
}); });
} }
@ -166,29 +139,42 @@ export class EventRenderingService {
private setupDragEndListener(): void { private setupDragEndListener(): void {
this.eventBus.on('drag:end', async (event: Event) => { this.eventBus.on('drag:end', async (event: Event) => {
const { originalElement, draggedClone, finalPosition, target } = (event as CustomEvent<IDragEndEventPayload>).detail;
const { originalElement, draggedClone, originalSourceColumn, finalPosition, target } = (event as CustomEvent<IDragEndEventPayload>).detail;
const finalColumn = finalPosition.column; const finalColumn = finalPosition.column;
const finalY = finalPosition.snappedY; const finalY = finalPosition.snappedY;
let element = draggedClone as SwpEventElement; // Only handle day column drops
// Only handle day column drops for EventRenderer
if (target === 'swp-day-column' && finalColumn) { if (target === 'swp-day-column' && finalColumn) {
const element = draggedClone as SwpEventElement;
if (originalElement && draggedClone && this.strategy.handleDragEnd) { if (originalElement && draggedClone && this.strategy.handleDragEnd) {
this.strategy.handleDragEnd(originalElement, draggedClone, finalColumn, finalY); this.strategy.handleDragEnd(originalElement, draggedClone, finalColumn, finalY);
} }
await this.eventManager.updateEvent(element.eventId, { // Build update payload based on mode
const updatePayload: { start: Date; end: Date; allDay: boolean; resourceId?: string } = {
start: element.start, start: element.start,
end: element.end, end: element.end,
allDay: false allDay: false
}); };
// Re-render affected columns for stacking/grouping (now with updated data) if (this.dataSource.isResource()) {
await this.reRenderAffectedColumns(originalSourceColumn, finalColumn); // Resource mode: update resourceId, keep existing date
updatePayload.resourceId = finalColumn.identifier;
} else {
// Date mode: update date from column, keep existing time
const newDate = this.dateService.parseISO(finalColumn.identifier);
const startTimeMinutes = this.dateService.getMinutesSinceMidnight(element.start);
const endTimeMinutes = this.dateService.getMinutesSinceMidnight(element.end);
updatePayload.start = this.dateService.createDateAtTime(newDate, startTimeMinutes);
updatePayload.end = this.dateService.createDateAtTime(newDate, endTimeMinutes);
} }
await this.eventManager.updateEvent(element.eventId, updatePayload);
// Trigger full refresh to re-render with updated data
this.eventBus.emit(CoreEvents.REFRESH_REQUESTED, {});
}
}); });
} }
@ -252,27 +238,14 @@ export class EventRenderingService {
this.eventBus.on('resize:end', async (event: Event) => { this.eventBus.on('resize:end', async (event: Event) => {
const { eventId, element } = (event as CustomEvent<IResizeEndEventPayload>).detail; const { eventId, element } = (event as CustomEvent<IResizeEndEventPayload>).detail;
// Update event data in EventManager with new end time from resized element
const swpEvent = element as SwpEventElement; const swpEvent = element as SwpEventElement;
const newStart = swpEvent.start;
const newEnd = swpEvent.end;
await this.eventManager.updateEvent(eventId, { await this.eventManager.updateEvent(eventId, {
start: newStart, start: swpEvent.start,
end: newEnd end: swpEvent.end
}); });
console.log('📝 EventRendererManager: Updated event after resize', { // Trigger full refresh to re-render with updated data
eventId, this.eventBus.emit(CoreEvents.REFRESH_REQUESTED, {});
newStart,
newEnd
});
const dateIdentifier = newStart.toISOString().split('T')[0];
let columnBounds = ColumnDetectionUtils.getColumnBoundsByIdentifier(dateIdentifier);
if (columnBounds)
await this.renderSingleColumn(columnBounds);
}); });
} }
@ -286,68 +259,6 @@ export class EventRenderingService {
} }
/**
* Re-render affected columns after drag to recalculate stacking/grouping
*/
private async reRenderAffectedColumns(originalSourceColumn: IColumnBounds | null, targetColumn: IColumnBounds | null): Promise<void> {
// Re-render original source column if exists
if (originalSourceColumn) {
await this.renderSingleColumn(originalSourceColumn);
}
// Re-render target column if exists and different from source
if (targetColumn && targetColumn.identifier !== originalSourceColumn?.identifier) {
await this.renderSingleColumn(targetColumn);
}
}
/**
* Clear events in a single column's events layer
*/
private clearColumnEvents(eventsLayer: HTMLElement): void {
const existingEvents = eventsLayer.querySelectorAll('swp-event');
const existingGroups = eventsLayer.querySelectorAll('swp-event-group');
existingEvents.forEach(event => event.remove());
existingGroups.forEach(group => group.remove());
}
/**
* Render events for a single column
*/
private async renderSingleColumn(column: IColumnBounds): Promise<void> {
// Get events for just this column's date
const dateString = column.identifier;
const columnStart = this.dateService.parseISO(`${dateString}T00:00:00`);
const columnEnd = this.dateService.parseISO(`${dateString}T23:59:59.999`);
// Get events from EventManager for this single date
const events = await this.eventManager.getEventsForPeriod(columnStart, columnEnd);
// Filter to timed events only
const timedEvents = events.filter(event => !event.allDay);
// Get events layer within this specific column
const eventsLayer = column.element.querySelector('swp-events-layer') as HTMLElement;
if (!eventsLayer) {
console.warn('EventRendererManager: Events layer not found in column');
return;
}
// Clear only this column's events
this.clearColumnEvents(eventsLayer);
// Render events for this column using strategy
if (this.strategy.renderSingleColumnEvents) {
this.strategy.renderSingleColumnEvents(column, timedEvents);
}
console.log('🔄 EventRendererManager: Re-rendered single column', {
columnDate: column.identifier,
eventsCount: timedEvents.length
});
}
private clearEvents(container?: HTMLElement): void { private clearEvents(container?: HTMLElement): void {
this.strategy.clearEvents(container); this.strategy.clearEvents(container);
} }

View file

@ -1,5 +1,5 @@
import { Configuration } from '../configurations/CalendarConfig'; import { Configuration } from '../configurations/CalendarConfig';
import { CalendarView, ICalendarEvent } from '../types/CalendarTypes'; import { CalendarView } from '../types/CalendarTypes';
import { IColumnRenderer, IColumnRenderContext } from './ColumnRenderer'; import { IColumnRenderer, IColumnRenderContext } from './ColumnRenderer';
import { eventBus } from '../core/EventBus'; import { eventBus } from '../core/EventBus';
import { DateService } from '../utils/DateService'; import { DateService } from '../utils/DateService';
@ -105,15 +105,13 @@ export class GridRenderer {
* @param grid - Container element where grid will be rendered * @param grid - Container element where grid will be rendered
* @param currentDate - Base date for the current view (e.g., any date in the week) * @param currentDate - Base date for the current view (e.g., any date in the week)
* @param view - Calendar view type (day/week/month) * @param view - Calendar view type (day/week/month)
* @param dates - Array of dates to render as columns * @param columns - Array of columns to render (each column contains its events)
* @param events - All events for the period
*/ */
public renderGrid( public renderGrid(
grid: HTMLElement, grid: HTMLElement,
currentDate: Date, currentDate: Date,
view: CalendarView = 'week', view: CalendarView = 'week',
columns: IColumnInfo[] = [], columns: IColumnInfo[] = []
events: ICalendarEvent[] = []
): void { ): void {
if (!grid || !currentDate) { if (!grid || !currentDate) {
@ -125,10 +123,10 @@ export class GridRenderer {
// Only clear and rebuild if grid is empty (first render) // Only clear and rebuild if grid is empty (first render)
if (grid.children.length === 0) { if (grid.children.length === 0) {
this.createCompleteGridStructure(grid, currentDate, view, columns, events); this.createCompleteGridStructure(grid, currentDate, view, columns);
} else { } else {
// Optimized update - only refresh dynamic content // Optimized update - only refresh dynamic content
this.updateGridContent(grid, currentDate, view, columns, events); this.updateGridContent(grid, currentDate, view, columns);
} }
} }
@ -146,14 +144,13 @@ export class GridRenderer {
* @param grid - Parent container * @param grid - Parent container
* @param currentDate - Current view date * @param currentDate - Current view date
* @param view - View type * @param view - View type
* @param dates - Array of dates to render * @param columns - Array of columns to render (each column contains its events)
*/ */
private createCompleteGridStructure( private createCompleteGridStructure(
grid: HTMLElement, grid: HTMLElement,
currentDate: Date, currentDate: Date,
view: CalendarView, view: CalendarView,
columns: IColumnInfo[], columns: IColumnInfo[]
events: ICalendarEvent[]
): void { ): void {
// Create all elements in memory first for better performance // Create all elements in memory first for better performance
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
@ -168,7 +165,7 @@ export class GridRenderer {
fragment.appendChild(timeAxis); fragment.appendChild(timeAxis);
// Create grid container with caching // Create grid container with caching
const gridContainer = this.createOptimizedGridContainer(columns, events); const gridContainer = this.createOptimizedGridContainer(columns);
this.cachedGridContainer = gridContainer; this.cachedGridContainer = gridContainer;
fragment.appendChild(gridContainer); fragment.appendChild(gridContainer);
@ -213,14 +210,11 @@ export class GridRenderer {
* - Time grid (grid lines + day columns) - structure created here * - Time grid (grid lines + day columns) - structure created here
* - Column container - created here, populated by ColumnRenderer * - Column container - created here, populated by ColumnRenderer
* *
* @param currentDate - Current view date * @param columns - Array of columns to render (each column contains its events)
* @param view - View type
* @param dates - Array of dates to render
* @returns Complete grid container element * @returns Complete grid container element
*/ */
private createOptimizedGridContainer( private createOptimizedGridContainer(
columns: IColumnInfo[], columns: IColumnInfo[]
events: ICalendarEvent[]
): HTMLElement { ): HTMLElement {
const gridContainer = document.createElement('swp-grid-container'); const gridContainer = document.createElement('swp-grid-container');
@ -238,7 +232,7 @@ export class GridRenderer {
// Create column container // Create column container
const columnContainer = document.createElement('swp-day-columns'); const columnContainer = document.createElement('swp-day-columns');
this.renderColumnContainer(columnContainer, columns, events); this.renderColumnContainer(columnContainer, columns);
timeGrid.appendChild(columnContainer); timeGrid.appendChild(columnContainer);
scrollableContent.appendChild(timeGrid); scrollableContent.appendChild(timeGrid);
@ -255,13 +249,11 @@ export class GridRenderer {
* Event rendering is handled by EventRenderingService listening to GRID_RENDERED. * Event rendering is handled by EventRenderingService listening to GRID_RENDERED.
* *
* @param columnContainer - Empty container to populate * @param columnContainer - Empty container to populate
* @param dates - Array of dates to render * @param columns - Array of columns to render (each column contains its events)
* @param events - All events for the period (passed through, not used here)
*/ */
private renderColumnContainer( private renderColumnContainer(
columnContainer: HTMLElement, columnContainer: HTMLElement,
columns: IColumnInfo[], columns: IColumnInfo[]
events: ICalendarEvent[]
): void { ): void {
// Delegate to ColumnRenderer // Delegate to ColumnRenderer
this.columnRenderer.render(columnContainer, { this.columnRenderer.render(columnContainer, {
@ -279,21 +271,19 @@ export class GridRenderer {
* @param grid - Existing grid element * @param grid - Existing grid element
* @param currentDate - New view date * @param currentDate - New view date
* @param view - View type * @param view - View type
* @param dates - Array of dates to render * @param columns - Array of columns to render (each column contains its events)
* @param events - All events for the period
*/ */
private updateGridContent( private updateGridContent(
grid: HTMLElement, grid: HTMLElement,
currentDate: Date, currentDate: Date,
view: CalendarView, view: CalendarView,
columns: IColumnInfo[], columns: IColumnInfo[]
events: ICalendarEvent[]
): void { ): void {
// Update column container if needed // Update column container if needed
const columnContainer = grid.querySelector('swp-day-columns'); const columnContainer = grid.querySelector('swp-day-columns');
if (columnContainer) { if (columnContainer) {
columnContainer.innerHTML = ''; columnContainer.innerHTML = '';
this.renderColumnContainer(columnContainer as HTMLElement, columns, events); this.renderColumnContainer(columnContainer as HTMLElement, columns);
} }
} }
/** /**
@ -310,8 +300,8 @@ export class GridRenderer {
* @returns New grid element ready for animation * @returns New grid element ready for animation
*/ */
public createNavigationGrid(parentContainer: HTMLElement, columns: IColumnInfo[]): HTMLElement { public createNavigationGrid(parentContainer: HTMLElement, columns: IColumnInfo[]): HTMLElement {
// Create grid structure without events (events rendered by EventRenderingService) // Create grid structure (events are in columns, rendered by EventRenderingService)
const newGrid = this.createOptimizedGridContainer(columns, []); const newGrid = this.createOptimizedGridContainer(columns);
// Position new grid for animation - NO transform here, let Animation API handle it // Position new grid for animation - NO transform here, let Animation API handle it
newGrid.style.position = 'absolute'; newGrid.style.position = 'absolute';

View file

@ -1,5 +1,5 @@
import { IResource } from './ResourceTypes'; import { IResource } from './ResourceTypes';
import { CalendarView } from './CalendarTypes'; import { CalendarView, ICalendarEvent } from './CalendarTypes';
/** /**
* Column information container * Column information container
@ -8,6 +8,7 @@ import { CalendarView } from './CalendarTypes';
export interface IColumnInfo { export interface IColumnInfo {
identifier: string; // "2024-11-13" (date mode) or "person-1" (resource mode) identifier: string; // "2024-11-13" (date mode) or "person-1" (resource mode)
data: Date | IResource; // Date for date-mode, IResource for resource-mode data: Date | IResource; // Date for date-mode, IResource for resource-mode
events: ICalendarEvent[]; // Events for this column (pre-filtered by datasource)
} }
/** /**
@ -28,6 +29,11 @@ export interface IColumnDataSource {
*/ */
getType(): 'date' | 'resource'; getType(): 'date' | 'resource';
/**
* Check if this datasource is in resource mode
*/
isResource(): boolean;
/** /**
* Update the current date for column calculations * Update the current date for column calculations
* @param date - The new current date * @param date - The new current date