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:
parent
044b547836
commit
7cb89e2ec5
2 changed files with 111 additions and 28 deletions
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue