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

@ -1,7 +1,8 @@
export class HeaderDrawerManager { export class HeaderDrawerManager {
private drawer!: HTMLElement; private drawer!: HTMLElement;
private expanded = false; private expanded = false;
private readonly expandedHeight = 25; private currentRows = 0;
private readonly rowHeight = 25;
private readonly duration = 200; private readonly duration = 200;
init(container: HTMLElement): void { init(container: HTMLElement): void {
@ -14,16 +15,34 @@ export class HeaderDrawerManager {
this.expanded ? this.collapse() : this.expand(); this.expanded ? this.collapse() : this.expand();
} }
/**
* Expand drawer to single row (legacy support)
*/
expand(): void { expand(): void {
if (this.expanded) return; this.expandToRows(1);
}
/**
* Expand drawer to fit specified number of rows
*/
expandToRows(rowCount: number): void {
const targetHeight = rowCount * this.rowHeight;
const currentHeight = this.expanded ? this.currentRows * this.rowHeight : 0;
// Skip if already at target
if (this.expanded && this.currentRows === rowCount) return;
this.currentRows = rowCount;
this.expanded = true; this.expanded = true;
this.animate(0, this.expandedHeight); this.animate(currentHeight, targetHeight);
} }
collapse(): void { collapse(): void {
if (!this.expanded) return; if (!this.expanded) return;
const currentHeight = this.currentRows * this.rowHeight;
this.expanded = false; this.expanded = false;
this.animate(this.expandedHeight, 0); this.currentRows = 0;
this.animate(currentHeight, 0);
} }
private animate(from: number, to: number): void { private animate(from: number, to: number): void {
@ -44,4 +63,8 @@ export class HeaderDrawerManager {
isExpanded(): boolean { isExpanded(): boolean {
return this.expanded; return this.expanded;
} }
getRowCount(): number {
return this.currentRows;
}
} }

View file

@ -10,6 +10,16 @@ import {
IDragLeaveHeaderPayload IDragLeaveHeaderPayload
} from '../../types/DragTypes'; } 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 * 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> { async render(container: HTMLElement, filter: Record<string, string[]>): Promise<void> {
const drawer = container.querySelector('swp-header-drawer'); const drawer = container.querySelector('swp-header-drawer');
@ -58,31 +68,27 @@ export class HeaderDrawerRenderer {
// Clear existing items // Clear existing items
drawer.innerHTML = ''; drawer.innerHTML = '';
// Render each allDay event if (allDayEvents.length === 0) return;
allDayEvents.forEach(event => {
const item = this.createHeaderItem(event, visibleDates); // Calculate layout with row stacking
if (item) drawer.appendChild(item); 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 // Expand drawer to fit all rows
if (allDayEvents.length > 0) { this.headerDrawerManager.expandToRows(rowCount);
this.headerDrawerManager.expand();
}
} }
/** /**
* Create a header item element for an allDay event * Create a header item element from layout
* Supports multi-day events with column span
*/ */
private createHeaderItem(event: ICalendarEvent, visibleDates: string[]): HTMLElement | null { private createHeaderItem(layout: IHeaderItemLayout): HTMLElement {
const startDateKey = this.dateService.getDateKey(event.start); const { event, row, colStart, colEnd } = layout;
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;
const item = document.createElement('swp-header-item'); const item = document.createElement('swp-header-item');
item.dataset.eventId = event.id; item.dataset.eventId = event.id;
@ -95,14 +101,68 @@ export class HeaderDrawerRenderer {
const colorClass = this.getColorClass(event); const colorClass = this.getColorClass(event);
if (colorClass) item.classList.add(colorClass); if (colorClass) item.classList.add(colorClass);
// Grid position (1-indexed, clamp til synlige kolonner) // Grid position from layout
const col = Math.max(0, startColIndex) + 1; item.style.gridArea = `${row} / ${colStart} / ${row + 1} / ${colEnd}`;
const endCol = (endColIndex !== -1 ? endColIndex : visibleDates.length - 1) + 2;
item.style.gridArea = `1 / ${col} / 2 / ${endCol}`;
return item; 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 * Get color class based on event metadata or type
*/ */