Enhances header drawer with multi-row event layout

Improves header drawer rendering to support multi-row event stacking

Adds row-based layout algorithm for all-day events
Enables flexible height expansion based on event count
Provides more robust event placement across visible date range
This commit is contained in:
Janus C. H. Knudsen 2025-12-11 23:43:51 +01:00
parent 044b547836
commit 7cb89e2ec5
2 changed files with 111 additions and 28 deletions

View file

@ -10,6 +10,16 @@ import {
IDragLeaveHeaderPayload
} from '../../types/DragTypes';
/**
* Layout information for a header item
*/
interface IHeaderItemLayout {
event: ICalendarEvent;
row: number; // 1-indexed
colStart: number; // 1-indexed
colEnd: number; // exclusive
}
/**
* HeaderDrawerRenderer - Handles rendering of items in the header drawer
*
@ -36,7 +46,7 @@ export class HeaderDrawerRenderer {
}
/**
* Render allDay events into the header drawer
* Render allDay events into the header drawer with row stacking
*/
async render(container: HTMLElement, filter: Record<string, string[]>): Promise<void> {
const drawer = container.querySelector('swp-header-drawer');
@ -58,31 +68,27 @@ export class HeaderDrawerRenderer {
// Clear existing items
drawer.innerHTML = '';
// Render each allDay event
allDayEvents.forEach(event => {
const item = this.createHeaderItem(event, visibleDates);
if (item) drawer.appendChild(item);
if (allDayEvents.length === 0) return;
// Calculate layout with row stacking
const layouts = this.calculateLayout(allDayEvents, visibleDates);
const rowCount = Math.max(1, ...layouts.map(l => l.row));
// Render each item with layout
layouts.forEach(layout => {
const item = this.createHeaderItem(layout);
drawer.appendChild(item);
});
// Expand drawer if there are items
if (allDayEvents.length > 0) {
this.headerDrawerManager.expand();
}
// Expand drawer to fit all rows
this.headerDrawerManager.expandToRows(rowCount);
}
/**
* Create a header item element for an allDay event
* Supports multi-day events with column span
* Create a header item element from layout
*/
private createHeaderItem(event: ICalendarEvent, visibleDates: string[]): HTMLElement | null {
const startDateKey = this.dateService.getDateKey(event.start);
const endDateKey = this.dateService.getDateKey(event.end);
const startColIndex = visibleDates.indexOf(startDateKey);
const endColIndex = visibleDates.indexOf(endDateKey);
// Event skal mindst starte eller slutte inden for synlige datoer
if (startColIndex === -1 && endColIndex === -1) return null;
private createHeaderItem(layout: IHeaderItemLayout): HTMLElement {
const { event, row, colStart, colEnd } = layout;
const item = document.createElement('swp-header-item');
item.dataset.eventId = event.id;
@ -95,14 +101,68 @@ export class HeaderDrawerRenderer {
const colorClass = this.getColorClass(event);
if (colorClass) item.classList.add(colorClass);
// Grid position (1-indexed, clamp til synlige kolonner)
const col = Math.max(0, startColIndex) + 1;
const endCol = (endColIndex !== -1 ? endColIndex : visibleDates.length - 1) + 2;
item.style.gridArea = `1 / ${col} / 2 / ${endCol}`;
// Grid position from layout
item.style.gridArea = `${row} / ${colStart} / ${row + 1} / ${colEnd}`;
return item;
}
/**
* Calculate layout for all events with row stacking
* Uses track-based algorithm to find available rows for overlapping events
*/
private calculateLayout(events: ICalendarEvent[], visibleDates: string[]): IHeaderItemLayout[] {
// tracks[row][col] = occupied
const tracks: boolean[][] = [new Array(visibleDates.length).fill(false)];
const layouts: IHeaderItemLayout[] = [];
for (const event of events) {
const startCol = this.getColIndex(event.start, visibleDates);
const endCol = this.getColIndex(event.end, visibleDates);
if (startCol === -1 && endCol === -1) continue;
// Clamp til synlige kolonner
const colStart = Math.max(0, startCol);
const colEnd = (endCol !== -1 ? endCol : visibleDates.length - 1) + 1;
// Find ledig række
const row = this.findAvailableRow(tracks, colStart, colEnd);
// Marker som optaget
for (let c = colStart; c < colEnd; c++) {
tracks[row][c] = true;
}
layouts.push({ event, row: row + 1, colStart: colStart + 1, colEnd: colEnd + 1 });
}
return layouts;
}
/**
* Find available row for event spanning columns [colStart, colEnd)
*/
private findAvailableRow(tracks: boolean[][], colStart: number, colEnd: number): number {
for (let row = 0; row < tracks.length; row++) {
let available = true;
for (let c = colStart; c < colEnd; c++) {
if (tracks[row][c]) { available = false; break; }
}
if (available) return row;
}
// Ny række
tracks.push(new Array(tracks[0].length).fill(false));
return tracks.length - 1;
}
/**
* Get column index for a date (0-indexed, -1 if not found)
*/
private getColIndex(date: Date, visibleDates: string[]): number {
const dateKey = this.dateService.getDateKey(date);
return visibleDates.indexOf(dateKey);
}
/**
* Get color class based on event metadata or type
*/