136 lines
3.7 KiB
TypeScript
136 lines
3.7 KiB
TypeScript
|
|
/**
|
||
|
|
* HeaderDrawerLayoutEngine - Calculates row placement for header items
|
||
|
|
*
|
||
|
|
* Prevents visual overlap by assigning items to different rows when
|
||
|
|
* they occupy the same columns. Uses a track-based algorithm similar
|
||
|
|
* to V1's AllDayLayoutEngine.
|
||
|
|
*
|
||
|
|
* Each row can hold multiple items as long as they don't overlap in columns.
|
||
|
|
* When an item spans columns that are already occupied, it's placed in the
|
||
|
|
* next available row.
|
||
|
|
*/
|
||
|
|
|
||
|
|
export interface IHeaderItemLayout {
|
||
|
|
itemId: string;
|
||
|
|
gridArea: string; // "row / col-start / row+1 / col-end"
|
||
|
|
startColumn: number;
|
||
|
|
endColumn: number;
|
||
|
|
row: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface IHeaderItemInput {
|
||
|
|
id: string;
|
||
|
|
columnStart: number; // 0-based column index
|
||
|
|
columnEnd: number; // 0-based end column (inclusive)
|
||
|
|
}
|
||
|
|
|
||
|
|
export class HeaderDrawerLayoutEngine {
|
||
|
|
private tracks: boolean[][] = [];
|
||
|
|
private columnCount: number;
|
||
|
|
|
||
|
|
constructor(columnCount: number) {
|
||
|
|
this.columnCount = columnCount;
|
||
|
|
this.reset();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Reset tracks for new layout calculation
|
||
|
|
*/
|
||
|
|
reset(): void {
|
||
|
|
this.tracks = [new Array(this.columnCount).fill(false)];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Calculate layout for all items
|
||
|
|
* Items should be sorted by start column for optimal packing
|
||
|
|
*/
|
||
|
|
calculateLayout(items: IHeaderItemInput[]): IHeaderItemLayout[] {
|
||
|
|
this.reset();
|
||
|
|
const layouts: IHeaderItemLayout[] = [];
|
||
|
|
|
||
|
|
for (const item of items) {
|
||
|
|
const row = this.findAvailableRow(item.columnStart, item.columnEnd);
|
||
|
|
|
||
|
|
// Mark columns as occupied in this row
|
||
|
|
for (let col = item.columnStart; col <= item.columnEnd; col++) {
|
||
|
|
this.tracks[row][col] = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// gridArea format: "row / col-start / row+1 / col-end"
|
||
|
|
// CSS grid uses 1-based indices
|
||
|
|
layouts.push({
|
||
|
|
itemId: item.id,
|
||
|
|
gridArea: `${row + 1} / ${item.columnStart + 1} / ${row + 2} / ${item.columnEnd + 2}`,
|
||
|
|
startColumn: item.columnStart,
|
||
|
|
endColumn: item.columnEnd,
|
||
|
|
row: row + 1 // 1-based for CSS
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return layouts;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Calculate layout for a single new item
|
||
|
|
* Useful for real-time drag operations
|
||
|
|
*/
|
||
|
|
calculateSingleLayout(item: IHeaderItemInput): IHeaderItemLayout {
|
||
|
|
const row = this.findAvailableRow(item.columnStart, item.columnEnd);
|
||
|
|
|
||
|
|
// Mark columns as occupied
|
||
|
|
for (let col = item.columnStart; col <= item.columnEnd; col++) {
|
||
|
|
this.tracks[row][col] = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
itemId: item.id,
|
||
|
|
gridArea: `${row + 1} / ${item.columnStart + 1} / ${row + 2} / ${item.columnEnd + 2}`,
|
||
|
|
startColumn: item.columnStart,
|
||
|
|
endColumn: item.columnEnd,
|
||
|
|
row: row + 1
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Find the first row where all columns in range are available
|
||
|
|
*/
|
||
|
|
private findAvailableRow(startCol: number, endCol: number): number {
|
||
|
|
for (let row = 0; row < this.tracks.length; row++) {
|
||
|
|
if (this.isRowAvailable(row, startCol, endCol)) {
|
||
|
|
return row;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add new row if all existing rows are occupied
|
||
|
|
this.tracks.push(new Array(this.columnCount).fill(false));
|
||
|
|
return this.tracks.length - 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if columns in range are all available in given row
|
||
|
|
*/
|
||
|
|
private isRowAvailable(row: number, startCol: number, endCol: number): boolean {
|
||
|
|
for (let col = startCol; col <= endCol; col++) {
|
||
|
|
if (this.tracks[row][col]) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the number of rows currently in use
|
||
|
|
*/
|
||
|
|
getRowCount(): number {
|
||
|
|
return this.tracks.length;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Update column count (e.g., when view changes)
|
||
|
|
*/
|
||
|
|
setColumnCount(count: number): void {
|
||
|
|
this.columnCount = count;
|
||
|
|
this.reset();
|
||
|
|
}
|
||
|
|
}
|