diff --git a/src/v2/core/HeaderDrawerManager.ts b/src/v2/core/HeaderDrawerManager.ts index aef390a..445bb23 100644 --- a/src/v2/core/HeaderDrawerManager.ts +++ b/src/v2/core/HeaderDrawerManager.ts @@ -1,7 +1,8 @@ export class HeaderDrawerManager { private drawer!: HTMLElement; private expanded = false; - private readonly expandedHeight = 25; + private currentRows = 0; + private readonly rowHeight = 25; private readonly duration = 200; init(container: HTMLElement): void { @@ -14,16 +15,34 @@ export class HeaderDrawerManager { this.expanded ? this.collapse() : this.expand(); } + /** + * Expand drawer to single row (legacy support) + */ 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.animate(0, this.expandedHeight); + this.animate(currentHeight, targetHeight); } collapse(): void { if (!this.expanded) return; + const currentHeight = this.currentRows * this.rowHeight; this.expanded = false; - this.animate(this.expandedHeight, 0); + this.currentRows = 0; + this.animate(currentHeight, 0); } private animate(from: number, to: number): void { @@ -44,4 +63,8 @@ export class HeaderDrawerManager { isExpanded(): boolean { return this.expanded; } + + getRowCount(): number { + return this.currentRows; + } } diff --git a/src/v2/features/headerdrawer/HeaderDrawerRenderer.ts b/src/v2/features/headerdrawer/HeaderDrawerRenderer.ts index deb7740..9927f29 100644 --- a/src/v2/features/headerdrawer/HeaderDrawerRenderer.ts +++ b/src/v2/features/headerdrawer/HeaderDrawerRenderer.ts @@ -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): Promise { 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 */