Implements event layout coordinator
Introduces a coordinator to manage event layout calculations, separating this logic from rendering. It calculates stack levels and allocates event columns for improved visual organization of calendar events.
This commit is contained in:
parent
6b8c5d4673
commit
06356df2a3
4 changed files with 545 additions and 56 deletions
BIN
.workbench/image.png
Normal file
BIN
.workbench/image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
15
.workbench/scenarie3.html
Normal file
15
.workbench/scenarie3.html
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<swp-day-column data-date="2025-10-07" style="--before-work-height: 240px; --after-work-top: 880px;"><swp-events-layer><swp-event-group class="cols-2 stack-level-3" data-stack-link="{"stackLevel":3}" style="top: 321px; margin-left: 45px; z-index: 103;"><div style="position: relative;"><swp-event data-event-id="S3B" data-title="Scenario 3: Event B" data-start="2025-10-07T08:00:00.000Z" data-end="2025-10-07T11:00:00.000Z" data-type="work" data-duration="180" style="position: absolute; top: 0px; height: 237px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="180">10:00 - 13:00</swp-event-time>
|
||||
<swp-event-title>Scenario 3: Event B</swp-event-title>
|
||||
</swp-event></div><div style="position: relative;"><swp-event data-event-id="S3D" data-title="Scenario 3: Event D" data-start="2025-10-07T10:30:00.000Z" data-end="2025-10-07T11:30:00.000Z" data-type="work" data-duration="60" style="position: absolute; top: 200px; height: 77px; left: 0px; right: 0px;">
|
||||
<swp-event-time data-duration="60">12:30 - 13:30</swp-event-time>
|
||||
<swp-event-title>Scenario 3: Event D</swp-event-title>
|
||||
</swp-event></div></swp-event-group><swp-event data-event-id="S3A" data-title="Scenario 3: Event A" data-start="2025-10-07T07:00:00.000Z" data-end="2025-10-07T13:00:00.000Z" data-type="work" data-duration="360" data-stack-link="{"stackLevel":0,"next":"S3B"}" style="position: absolute; top: 241px; height: 477px; left: 2px; right: 2px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event-time data-duration="360">09:00 - 15:00</swp-event-time>
|
||||
<swp-event-title>Scenario 3: Event A</swp-event-title>
|
||||
</swp-event><swp-event data-event-id="S3C" data-title="Scenario 3: Event C" data-start="2025-10-07T09:00:00.000Z" data-end="2025-10-07T10:00:00.000Z" data-type="work" data-duration="60" data-stack-link="{"stackLevel":2,"prev":"S3B"}" style="position: absolute; top: 401px; height: 77px; left: 2px; right: 2px; margin-left: 30px; z-index: 102;">
|
||||
<swp-event-time data-duration="60">11:00 - 12:00</swp-event-time>
|
||||
<swp-event-title>Scenario 3: Event C</swp-event-title>
|
||||
</swp-event><swp-event data-event-id="S4A" data-title="Scenario 4: Event A" data-start="2025-10-07T14:00:00.000Z" data-end="2025-10-07T20:00:00.000Z" data-type="meeting" data-duration="360" data-stack-link="{"stackLevel":0,"next":"S4B"}" style="position: absolute; top: 801px; height: 477px; left: 2px; right: 2px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event-time data-duration="360">16:00 - 22:00</swp-event-time>
|
||||
</swp-event></swp-events-layer></swp-day-column>
|
||||
|
|
@ -2246,5 +2246,434 @@
|
|||
"duration": 240,
|
||||
"color": "#2196f3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S1A",
|
||||
"title": "Scenario 1: Event A",
|
||||
"start": "2025-10-06T05:00:00Z",
|
||||
"end": "2025-10-06T10:00:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 300,
|
||||
"color": "#ff6b6b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S1B",
|
||||
"title": "Scenario 1: Event B",
|
||||
"start": "2025-10-06T06:00:00Z",
|
||||
"end": "2025-10-06T08:00:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 120,
|
||||
"color": "#4ecdc4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S1C",
|
||||
"title": "Scenario 1: Event C",
|
||||
"start": "2025-10-06T08:30:00Z",
|
||||
"end": "2025-10-06T09:00:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 30,
|
||||
"color": "#ffe66d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S2A",
|
||||
"title": "Scenario 2: Event A",
|
||||
"start": "2025-10-06T11:00:00Z",
|
||||
"end": "2025-10-06T17:00:00Z",
|
||||
"type": "meeting",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 360,
|
||||
"color": "#ff6b6b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S2B",
|
||||
"title": "Scenario 2: Event B",
|
||||
"start": "2025-10-06T12:00:00Z",
|
||||
"end": "2025-10-06T13:00:00Z",
|
||||
"type": "meeting",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 60,
|
||||
"color": "#4ecdc4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S2C",
|
||||
"title": "Scenario 2: Event C",
|
||||
"start": "2025-10-06T13:30:00Z",
|
||||
"end": "2025-10-06T14:30:00Z",
|
||||
"type": "meeting",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 60,
|
||||
"color": "#ffe66d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S2D",
|
||||
"title": "Scenario 2: Event D",
|
||||
"start": "2025-10-06T15:00:00Z",
|
||||
"end": "2025-10-06T16:00:00Z",
|
||||
"type": "meeting",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 60,
|
||||
"color": "#a8e6cf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S3A",
|
||||
"title": "Scenario 3: Event A",
|
||||
"start": "2025-10-07T07:00:00Z",
|
||||
"end": "2025-10-07T13:00:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 360,
|
||||
"color": "#ff6b6b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S3B",
|
||||
"title": "Scenario 3: Event B",
|
||||
"start": "2025-10-07T08:00:00Z",
|
||||
"end": "2025-10-07T11:00:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 180,
|
||||
"color": "#4ecdc4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S3C",
|
||||
"title": "Scenario 3: Event C",
|
||||
"start": "2025-10-07T09:00:00Z",
|
||||
"end": "2025-10-07T10:00:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 60,
|
||||
"color": "#ffe66d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S3D",
|
||||
"title": "Scenario 3: Event D",
|
||||
"start": "2025-10-07T10:30:00Z",
|
||||
"end": "2025-10-07T11:30:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 60,
|
||||
"color": "#a8e6cf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S4A",
|
||||
"title": "Scenario 4: Event A",
|
||||
"start": "2025-10-07T14:00:00Z",
|
||||
"end": "2025-10-07T20:00:00Z",
|
||||
"type": "meeting",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 360,
|
||||
"color": "#ff6b6b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S4B",
|
||||
"title": "Scenario 4: Event B",
|
||||
"start": "2025-10-07T15:00:00Z",
|
||||
"end": "2025-10-07T19:00:00Z",
|
||||
"type": "meeting",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 240,
|
||||
"color": "#4ecdc4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S4C",
|
||||
"title": "Scenario 4: Event C",
|
||||
"start": "2025-10-07T16:00:00Z",
|
||||
"end": "2025-10-07T18:00:00Z",
|
||||
"type": "meeting",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 120,
|
||||
"color": "#ffe66d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S5A",
|
||||
"title": "Scenario 5: Event A",
|
||||
"start": "2025-10-08T05:00:00Z",
|
||||
"end": "2025-10-08T08:00:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 180,
|
||||
"color": "#ff6b6b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S5B",
|
||||
"title": "Scenario 5: Event B",
|
||||
"start": "2025-10-08T06:00:00Z",
|
||||
"end": "2025-10-08T07:30:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 90,
|
||||
"color": "#4ecdc4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S5C",
|
||||
"title": "Scenario 5: Event C",
|
||||
"start": "2025-10-08T06:00:00Z",
|
||||
"end": "2025-10-08T07:00:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 60,
|
||||
"color": "#ffe66d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S6A",
|
||||
"title": "Scenario 6: Event A",
|
||||
"start": "2025-10-08T09:00:00Z",
|
||||
"end": "2025-10-08T12:00:00Z",
|
||||
"type": "meeting",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 180,
|
||||
"color": "#ff6b6b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S6B",
|
||||
"title": "Scenario 6: Event B",
|
||||
"start": "2025-10-08T10:00:00Z",
|
||||
"end": "2025-10-08T11:30:00Z",
|
||||
"type": "meeting",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 90,
|
||||
"color": "#4ecdc4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S6C",
|
||||
"title": "Scenario 6: Event C",
|
||||
"start": "2025-10-08T10:00:00Z",
|
||||
"end": "2025-10-08T11:00:00Z",
|
||||
"type": "meeting",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 60,
|
||||
"color": "#ffe66d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S6D",
|
||||
"title": "Scenario 6: Event D",
|
||||
"start": "2025-10-08T10:30:00Z",
|
||||
"end": "2025-10-08T10:45:00Z",
|
||||
"type": "meeting",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 15,
|
||||
"color": "#a8e6cf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S7A",
|
||||
"title": "Scenario 7: Event A",
|
||||
"start": "2025-10-09T05:00:00Z",
|
||||
"end": "2025-10-09T07:30:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 150,
|
||||
"color": "#009688"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S7B",
|
||||
"title": "Scenario 7: Event B",
|
||||
"start": "2025-10-09T05:00:00Z",
|
||||
"end": "2025-10-09T07:00:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 120,
|
||||
"color": "#ff5722"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S8A",
|
||||
"title": "Scenario 8: Event A",
|
||||
"start": "2025-10-09T08:00:00Z",
|
||||
"end": "2025-10-09T09:00:00Z",
|
||||
"type": "meeting",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 60,
|
||||
"color": "#ff6b6b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S8B",
|
||||
"title": "Scenario 8: Event B",
|
||||
"start": "2025-10-09T08:15:00Z",
|
||||
"end": "2025-10-09T09:30:00Z",
|
||||
"type": "meeting",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 75,
|
||||
"color": "#4ecdc4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S9A",
|
||||
"title": "Scenario 9: Event A",
|
||||
"start": "2025-10-09T10:00:00Z",
|
||||
"end": "2025-10-09T11:00:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 60,
|
||||
"color": "#ff6b6b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S9B",
|
||||
"title": "Scenario 9: Event B",
|
||||
"start": "2025-10-09T10:30:00Z",
|
||||
"end": "2025-10-09T11:30:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 60,
|
||||
"color": "#4ecdc4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S9C",
|
||||
"title": "Scenario 9: Event C",
|
||||
"start": "2025-10-09T11:15:00Z",
|
||||
"end": "2025-10-09T13:00:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 105,
|
||||
"color": "#ffe66d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S10A",
|
||||
"title": "Scenario 10: Event A",
|
||||
"start": "2025-10-10T10:00:00Z",
|
||||
"end": "2025-10-10T13:00:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 180,
|
||||
"color": "#ff6b6b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S10B",
|
||||
"title": "Scenario 10: Event B",
|
||||
"start": "2025-10-10T10:30:00Z",
|
||||
"end": "2025-10-10T11:00:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 30,
|
||||
"color": "#4ecdc4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S10C",
|
||||
"title": "Scenario 10: Event C",
|
||||
"start": "2025-10-10T11:30:00Z",
|
||||
"end": "2025-10-10T12:30:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 60,
|
||||
"color": "#ffe66d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S10D",
|
||||
"title": "Scenario 10: Event D",
|
||||
"start": "2025-10-10T12:00:00Z",
|
||||
"end": "2025-10-10T13:00:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 60,
|
||||
"color": "#a8e6cf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "S10E",
|
||||
"title": "Scenario 10: Event E",
|
||||
"start": "2025-10-10T12:00:00Z",
|
||||
"end": "2025-10-10T13:00:00Z",
|
||||
"type": "work",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 60,
|
||||
"color": "#dda15e"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
import { CalendarEvent } from '../types/CalendarTypes';
|
||||
import { EventStackManager, EventGroup, StackLink } from './EventStackManager';
|
||||
import { PositionUtils } from '../utils/PositionUtils';
|
||||
import { calendarConfig } from '../core/CalendarConfig';
|
||||
|
||||
export interface GridGroupLayout {
|
||||
events: CalendarEvent[];
|
||||
|
|
@ -35,56 +36,91 @@ export class EventLayoutCoordinator {
|
|||
}
|
||||
|
||||
/**
|
||||
* Calculate complete layout for a column of events
|
||||
* Calculate complete layout for a column of events (recursive approach)
|
||||
*/
|
||||
public calculateColumnLayout(columnEvents: CalendarEvent[]): ColumnLayout {
|
||||
if (columnEvents.length === 0) {
|
||||
return { gridGroups: [], stackedEvents: [] };
|
||||
}
|
||||
|
||||
// Step 1: Calculate stack levels for ALL events first (to understand overlaps)
|
||||
const allStackLinks = this.stackManager.createOptimizedStackLinks(columnEvents);
|
||||
|
||||
// Step 2: Find grid candidates (start together ±15 min)
|
||||
const groups = this.stackManager.groupEventsByStartTime(columnEvents);
|
||||
const gridGroups = groups.filter(group => {
|
||||
if (group.events.length <= 1) return false;
|
||||
group.containerType = this.stackManager.decideContainerType(group);
|
||||
return group.containerType === 'GRID';
|
||||
});
|
||||
|
||||
// Step 3: Build grid group layouts
|
||||
const gridGroupLayouts: GridGroupLayout[] = [];
|
||||
const renderedEventIds = new Set<string>();
|
||||
const stackedEventLayouts: StackedEventLayout[] = [];
|
||||
const renderedEventsWithLevels: Array<{ event: CalendarEvent; level: number }> = [];
|
||||
let remaining = [...columnEvents].sort((a, b) => a.start.getTime() - b.start.getTime());
|
||||
|
||||
gridGroups.forEach(group => {
|
||||
const gridStackLevel = this.calculateGridGroupStackLevel(group, columnEvents, allStackLinks);
|
||||
const earliestEvent = group.events[0];
|
||||
const position = PositionUtils.calculateEventPosition(earliestEvent.start, earliestEvent.end);
|
||||
const columns = this.allocateColumns(group.events);
|
||||
// Process events recursively
|
||||
while (remaining.length > 0) {
|
||||
// Take first event
|
||||
const firstEvent = remaining[0];
|
||||
|
||||
gridGroupLayouts.push({
|
||||
events: group.events,
|
||||
stackLevel: gridStackLevel,
|
||||
position: { top: position.top + 1 },
|
||||
columns
|
||||
});
|
||||
// Find events that could be in GRID with first event
|
||||
// (start within threshold AND overlap)
|
||||
const gridSettings = calendarConfig.getGridSettings();
|
||||
const thresholdMinutes = gridSettings.gridStartThresholdMinutes;
|
||||
|
||||
group.events.forEach(e => renderedEventIds.add(e.id));
|
||||
});
|
||||
const gridCandidates = [firstEvent];
|
||||
for (let i = 1; i < remaining.length; i++) {
|
||||
const candidate = remaining[i];
|
||||
const startDiff = Math.abs(candidate.start.getTime() - firstEvent.start.getTime()) / (1000 * 60);
|
||||
|
||||
// Step 4: Build stacked event layouts for remaining events
|
||||
const remainingEvents = columnEvents.filter(e => !renderedEventIds.has(e.id));
|
||||
const stackedEventLayouts: StackedEventLayout[] = remainingEvents.map(event => {
|
||||
const stackLink = allStackLinks.get(event.id)!;
|
||||
const position = PositionUtils.calculateEventPosition(event.start, event.end);
|
||||
// Only add if starts within threshold AND overlaps with firstEvent
|
||||
if (startDiff <= thresholdMinutes && this.stackManager.doEventsOverlap(firstEvent, candidate)) {
|
||||
gridCandidates.push(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
event,
|
||||
stackLink,
|
||||
position: { top: position.top + 1, height: position.height - 3 }
|
||||
// Decide: should this group be GRID or STACK?
|
||||
const group: EventGroup = {
|
||||
events: gridCandidates,
|
||||
containerType: 'NONE',
|
||||
startTime: firstEvent.start
|
||||
};
|
||||
});
|
||||
const containerType = this.stackManager.decideContainerType(group);
|
||||
|
||||
if (containerType === 'GRID' && gridCandidates.length > 1) {
|
||||
// Render as GRID
|
||||
const gridStackLevel = this.calculateGridGroupStackLevelFromRendered(
|
||||
gridCandidates,
|
||||
renderedEventsWithLevels
|
||||
);
|
||||
|
||||
const earliestEvent = gridCandidates[0];
|
||||
const position = PositionUtils.calculateEventPosition(earliestEvent.start, earliestEvent.end);
|
||||
const columns = this.allocateColumns(gridCandidates);
|
||||
|
||||
gridGroupLayouts.push({
|
||||
events: gridCandidates,
|
||||
stackLevel: gridStackLevel,
|
||||
position: { top: position.top + 1 },
|
||||
columns
|
||||
});
|
||||
|
||||
// Mark all events in grid with their stack level
|
||||
gridCandidates.forEach(e => renderedEventsWithLevels.push({ event: e, level: gridStackLevel }));
|
||||
|
||||
// Remove all events in this grid from remaining
|
||||
remaining = remaining.filter(e => !gridCandidates.includes(e));
|
||||
} else {
|
||||
// Render first event as STACKED
|
||||
const stackLevel = this.calculateStackLevelFromRendered(
|
||||
firstEvent,
|
||||
renderedEventsWithLevels
|
||||
);
|
||||
|
||||
const position = PositionUtils.calculateEventPosition(firstEvent.start, firstEvent.end);
|
||||
stackedEventLayouts.push({
|
||||
event: firstEvent,
|
||||
stackLink: { stackLevel },
|
||||
position: { top: position.top + 1, height: position.height - 3 }
|
||||
});
|
||||
|
||||
// Mark this event with its stack level
|
||||
renderedEventsWithLevels.push({ event: firstEvent, level: stackLevel });
|
||||
|
||||
// Remove only first event from remaining
|
||||
remaining = remaining.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
gridGroups: gridGroupLayouts,
|
||||
|
|
@ -93,33 +129,42 @@ export class EventLayoutCoordinator {
|
|||
}
|
||||
|
||||
/**
|
||||
* Calculate stack level for a grid group based on what it overlaps OUTSIDE the group
|
||||
* Calculate stack level for a grid group based on already rendered events
|
||||
*/
|
||||
private calculateGridGroupStackLevel(
|
||||
group: EventGroup,
|
||||
allEvents: CalendarEvent[],
|
||||
stackLinks: Map<string, StackLink>
|
||||
private calculateGridGroupStackLevelFromRendered(
|
||||
gridEvents: CalendarEvent[],
|
||||
renderedEventsWithLevels: Array<{ event: CalendarEvent; level: number }>
|
||||
): number {
|
||||
const groupEventIds = new Set(group.events.map(e => e.id));
|
||||
|
||||
// Find all events OUTSIDE this group
|
||||
const outsideEvents = allEvents.filter(e => !groupEventIds.has(e.id));
|
||||
|
||||
// Find the highest stackLevel of any event that overlaps with ANY event in the grid group
|
||||
// Find highest stack level of any rendered event that overlaps with this grid
|
||||
let maxOverlappingLevel = -1;
|
||||
|
||||
for (const gridEvent of group.events) {
|
||||
for (const outsideEvent of outsideEvents) {
|
||||
if (this.stackManager.doEventsOverlap(gridEvent, outsideEvent)) {
|
||||
const outsideLink = stackLinks.get(outsideEvent.id);
|
||||
if (outsideLink) {
|
||||
maxOverlappingLevel = Math.max(maxOverlappingLevel, outsideLink.stackLevel);
|
||||
}
|
||||
for (const gridEvent of gridEvents) {
|
||||
for (const rendered of renderedEventsWithLevels) {
|
||||
if (this.stackManager.doEventsOverlap(gridEvent, rendered.event)) {
|
||||
maxOverlappingLevel = Math.max(maxOverlappingLevel, rendered.level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Grid group should be one level above the highest overlapping event
|
||||
return maxOverlappingLevel + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate stack level for a single stacked event based on already rendered events
|
||||
*/
|
||||
private calculateStackLevelFromRendered(
|
||||
event: CalendarEvent,
|
||||
renderedEventsWithLevels: Array<{ event: CalendarEvent; level: number }>
|
||||
): number {
|
||||
// Find highest stack level of any rendered event that overlaps with this event
|
||||
let maxOverlappingLevel = -1;
|
||||
|
||||
for (const rendered of renderedEventsWithLevels) {
|
||||
if (this.stackManager.doEventsOverlap(event, rendered.event)) {
|
||||
maxOverlappingLevel = Math.max(maxOverlappingLevel, rendered.level);
|
||||
}
|
||||
}
|
||||
|
||||
return maxOverlappingLevel + 1;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue