Enhances event layout engine with advanced rendering logic
Introduces sophisticated event layout algorithm for handling complex scheduling scenarios Adds support for: - Grid and stacked event rendering - Automatic column allocation - Nested event stacking - Threshold-based event grouping Improves visual representation of overlapping and concurrent events
This commit is contained in:
parent
4e22fbc948
commit
70172e8f10
26 changed files with 2108 additions and 44 deletions
|
|
@ -1,10 +1,12 @@
|
|||
import { ICalendarEvent, IEventBus } from '../../types/CalendarTypes';
|
||||
import { ICalendarEvent, IEventBus, IEventUpdatedPayload } from '../../types/CalendarTypes';
|
||||
import { EventService } from '../../storage/events/EventService';
|
||||
import { DateService } from '../../core/DateService';
|
||||
import { IGridConfig } from '../../core/IGridConfig';
|
||||
import { calculateEventPosition, snapToGrid, pixelsToMinutes } from '../../utils/PositionUtils';
|
||||
import { CoreEvents } from '../../constants/CoreEvents';
|
||||
import { IDragColumnChangePayload, IDragMovePayload } from '../../types/DragTypes';
|
||||
import { calculateColumnLayout } from './EventLayoutEngine';
|
||||
import { IGridGroupLayout } from './EventLayoutTypes';
|
||||
|
||||
/**
|
||||
* EventRenderer - Renders calendar events to the DOM
|
||||
|
|
@ -15,19 +17,21 @@ import { IDragColumnChangePayload, IDragMovePayload } from '../../types/DragType
|
|||
* - Event data retrieved via EventService when needed
|
||||
*/
|
||||
export class EventRenderer {
|
||||
private container: HTMLElement | null = null;
|
||||
|
||||
constructor(
|
||||
private eventService: EventService,
|
||||
private dateService: DateService,
|
||||
private gridConfig: IGridConfig,
|
||||
private eventBus: IEventBus
|
||||
) {
|
||||
this.setupDragListeners();
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup listeners for drag-drop events
|
||||
* Setup listeners for drag-drop and update events
|
||||
*/
|
||||
private setupDragListeners(): void {
|
||||
private setupListeners(): void {
|
||||
this.eventBus.on(CoreEvents.EVENT_DRAG_COLUMN_CHANGE, (e) => {
|
||||
const payload = (e as CustomEvent<IDragColumnChangePayload>).detail;
|
||||
this.handleColumnChange(payload);
|
||||
|
|
@ -37,6 +41,95 @@ export class EventRenderer {
|
|||
const payload = (e as CustomEvent<IDragMovePayload>).detail;
|
||||
this.updateDragTimestamp(payload);
|
||||
});
|
||||
|
||||
this.eventBus.on(CoreEvents.EVENT_UPDATED, (e) => {
|
||||
const payload = (e as CustomEvent<IEventUpdatedPayload>).detail;
|
||||
this.handleEventUpdated(payload);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle EVENT_UPDATED - re-render affected columns
|
||||
*/
|
||||
private async handleEventUpdated(payload: IEventUpdatedPayload): Promise<void> {
|
||||
// Re-render source column (if different from target)
|
||||
if (payload.sourceDateKey !== payload.targetDateKey ||
|
||||
payload.sourceResourceId !== payload.targetResourceId) {
|
||||
await this.rerenderColumn(payload.sourceDateKey, payload.sourceResourceId);
|
||||
}
|
||||
|
||||
// Re-render target column
|
||||
await this.rerenderColumn(payload.targetDateKey, payload.targetResourceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-render a single column with fresh data from IndexedDB
|
||||
*/
|
||||
private async rerenderColumn(dateKey: string, resourceId?: string): Promise<void> {
|
||||
const column = this.findColumn(dateKey, resourceId);
|
||||
if (!column) return;
|
||||
|
||||
// Get date range for this day
|
||||
const startDate = new Date(dateKey);
|
||||
const endDate = new Date(dateKey);
|
||||
endDate.setHours(23, 59, 59, 999);
|
||||
|
||||
// Fetch events from IndexedDB
|
||||
const events = resourceId
|
||||
? await this.eventService.getByResourceAndDateRange(resourceId, startDate, endDate)
|
||||
: await this.eventService.getByDateRange(startDate, endDate);
|
||||
|
||||
// Filter to timed events and match dateKey exactly
|
||||
const timedEvents = events.filter(event =>
|
||||
!event.allDay && this.dateService.getDateKey(event.start) === dateKey
|
||||
);
|
||||
|
||||
// Get or create events layer
|
||||
let eventsLayer = column.querySelector('swp-events-layer');
|
||||
if (!eventsLayer) {
|
||||
eventsLayer = document.createElement('swp-events-layer');
|
||||
column.appendChild(eventsLayer);
|
||||
}
|
||||
|
||||
// Clear existing events
|
||||
eventsLayer.innerHTML = '';
|
||||
|
||||
// Calculate layout with stacking/grouping
|
||||
const layout = calculateColumnLayout(timedEvents, this.gridConfig);
|
||||
|
||||
// Render GRID groups
|
||||
layout.grids.forEach(grid => {
|
||||
const groupEl = this.renderGridGroup(grid);
|
||||
eventsLayer!.appendChild(groupEl);
|
||||
});
|
||||
|
||||
// Render STACKED events
|
||||
layout.stacked.forEach(item => {
|
||||
const eventEl = this.renderStackedEvent(item.event, item.stackLevel);
|
||||
eventsLayer!.appendChild(eventEl);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a column element by dateKey and optional resourceId
|
||||
*/
|
||||
private findColumn(dateKey: string, resourceId?: string): HTMLElement | null {
|
||||
if (!this.container) return null;
|
||||
|
||||
const columns = this.container.querySelectorAll('swp-day-column');
|
||||
for (const col of columns) {
|
||||
const colEl = col as HTMLElement;
|
||||
if (colEl.dataset.date !== dateKey) continue;
|
||||
|
||||
// If resourceId specified, must match
|
||||
if (resourceId && colEl.dataset.resourceId !== resourceId) continue;
|
||||
|
||||
// If no resourceId specified but column has one, skip (simple view case)
|
||||
if (!resourceId && colEl.dataset.resourceId) continue;
|
||||
|
||||
return colEl;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -93,6 +186,9 @@ export class EventRenderer {
|
|||
* @param filter - Filter with 'date' and optionally 'resource' arrays
|
||||
*/
|
||||
async render(container: HTMLElement, filter: Record<string, string[]>): Promise<void> {
|
||||
// Store container reference for later re-renders
|
||||
this.container = container;
|
||||
|
||||
const visibleDates = filter['date'] || [];
|
||||
|
||||
if (visibleDates.length === 0) return;
|
||||
|
|
@ -142,12 +238,22 @@ export class EventRenderer {
|
|||
// Clear existing events
|
||||
eventsLayer.innerHTML = '';
|
||||
|
||||
// Render each timed event
|
||||
columnEvents.forEach(event => {
|
||||
if (!event.allDay) {
|
||||
const eventElement = this.createEventElement(event);
|
||||
eventsLayer!.appendChild(eventElement);
|
||||
}
|
||||
// Filter to timed events only
|
||||
const timedEvents = columnEvents.filter(event => !event.allDay);
|
||||
|
||||
// Calculate layout with stacking/grouping
|
||||
const layout = calculateColumnLayout(timedEvents, this.gridConfig);
|
||||
|
||||
// Render GRID groups (simultaneous events side-by-side)
|
||||
layout.grids.forEach(grid => {
|
||||
const groupEl = this.renderGridGroup(grid);
|
||||
eventsLayer!.appendChild(groupEl);
|
||||
});
|
||||
|
||||
// Render STACKED events (overlapping with margin offset)
|
||||
layout.stacked.forEach(item => {
|
||||
const eventEl = this.renderStackedEvent(item.event, item.stackLevel);
|
||||
eventsLayer!.appendChild(eventEl);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -162,8 +268,8 @@ export class EventRenderer {
|
|||
private createEventElement(event: ICalendarEvent): HTMLElement {
|
||||
const element = document.createElement('swp-event');
|
||||
|
||||
// Only essential data attribute
|
||||
element.dataset.id = event.id;
|
||||
// Only essential data attribute (eventId for DragDropManager compatibility)
|
||||
element.dataset.eventId = event.id;
|
||||
|
||||
// Calculate position
|
||||
const position = calculateEventPosition(event.start, event.end, this.gridConfig);
|
||||
|
|
@ -187,9 +293,15 @@ export class EventRenderer {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get color class based on event type
|
||||
* Get color class based on metadata.color or event type
|
||||
*/
|
||||
private getColorClass(event: ICalendarEvent): string {
|
||||
// Check metadata.color first
|
||||
if (event.metadata?.color) {
|
||||
return `is-${event.metadata.color}`;
|
||||
}
|
||||
|
||||
// Fallback to type-based color
|
||||
const typeColors: Record<string, string> = {
|
||||
'customer': 'is-blue',
|
||||
'vacation': 'is-green',
|
||||
|
|
@ -208,4 +320,70 @@ export class EventRenderer {
|
|||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a GRID group with side-by-side columns
|
||||
* Used when multiple events start at the same time
|
||||
*/
|
||||
private renderGridGroup(layout: IGridGroupLayout): HTMLElement {
|
||||
const group = document.createElement('swp-event-group');
|
||||
group.classList.add(`cols-${layout.columns.length}`);
|
||||
group.style.top = `${layout.position.top}px`;
|
||||
|
||||
// Stack level styling for entire group (if nested in another event)
|
||||
if (layout.stackLevel > 0) {
|
||||
group.style.marginLeft = `${layout.stackLevel * 15}px`;
|
||||
group.style.zIndex = `${100 + layout.stackLevel}`;
|
||||
}
|
||||
|
||||
// Calculate the height needed for the group (tallest event)
|
||||
let maxBottom = 0;
|
||||
for (const event of layout.events) {
|
||||
const pos = calculateEventPosition(event.start, event.end, this.gridConfig);
|
||||
const eventBottom = pos.top + pos.height;
|
||||
if (eventBottom > maxBottom) maxBottom = eventBottom;
|
||||
}
|
||||
const groupHeight = maxBottom - layout.position.top;
|
||||
group.style.height = `${groupHeight}px`;
|
||||
|
||||
// Create wrapper div for each column
|
||||
layout.columns.forEach(columnEvents => {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.style.position = 'relative';
|
||||
|
||||
columnEvents.forEach(event => {
|
||||
const eventEl = this.createEventElement(event);
|
||||
// Position relative to group top
|
||||
const pos = calculateEventPosition(event.start, event.end, this.gridConfig);
|
||||
eventEl.style.top = `${pos.top - layout.position.top}px`;
|
||||
eventEl.style.position = 'absolute';
|
||||
eventEl.style.left = '0';
|
||||
eventEl.style.right = '0';
|
||||
wrapper.appendChild(eventEl);
|
||||
});
|
||||
|
||||
group.appendChild(wrapper);
|
||||
});
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a STACKED event with margin-left offset
|
||||
* Used for overlapping events that don't start at the same time
|
||||
*/
|
||||
private renderStackedEvent(event: ICalendarEvent, stackLevel: number): HTMLElement {
|
||||
const element = this.createEventElement(event);
|
||||
|
||||
// Add stack metadata for drag-drop and other features
|
||||
element.dataset.stackLink = JSON.stringify({ stackLevel });
|
||||
|
||||
// Visual styling based on stack level
|
||||
if (stackLevel > 0) {
|
||||
element.style.marginLeft = `${stackLevel * 15}px`;
|
||||
element.style.zIndex = `${100 + stackLevel}`;
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue