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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue