Add a custom download virtualizer with fixed 40px package rows, 38px file rows, overscan, and pinned rename rows. Keep the downloads model logically complete so header selection, shift ranges, and actionable selections operate on every filtered expanded row instead of the rendered DOM window. Render the virtualized body inside the existing downloads table scrollport without adding a second scroll context, while preserving column drag data attributes, row click behavior, context menus, and disabled native package dragging. Cover bounded rendering, full logical row selection, scroll window geometry, pinned inline rename rendering, and responsive scrollport CSS with focused tests.
89 lines
2.9 KiB
TypeScript
89 lines
2.9 KiB
TypeScript
export const DOWNLOAD_PACKAGE_ROW_HEIGHT = 40;
|
|
export const DOWNLOAD_FILE_ROW_HEIGHT = 38;
|
|
export const DOWNLOAD_VIRTUAL_OVERSCAN_ROWS = 8;
|
|
export const DOWNLOAD_VIRTUAL_DEFAULT_VIEWPORT_HEIGHT = 720;
|
|
|
|
export interface DownloadVirtualRowInput {
|
|
id: string;
|
|
height: number;
|
|
}
|
|
|
|
export interface DownloadVirtualPositionedRow<T extends DownloadVirtualRowInput> {
|
|
id: string;
|
|
index: number;
|
|
top: number;
|
|
height: number;
|
|
pinned: boolean;
|
|
source: T;
|
|
}
|
|
|
|
export interface DownloadVirtualWindow<T extends DownloadVirtualRowInput> {
|
|
rows: DownloadVirtualPositionedRow<T>[];
|
|
totalHeight: number;
|
|
startIndex: number;
|
|
endIndex: number;
|
|
}
|
|
|
|
export interface DownloadVirtualWindowOptions {
|
|
scrollTop: number;
|
|
viewportHeight: number;
|
|
overscan?: number;
|
|
pinnedIds?: Iterable<string | null | undefined>;
|
|
}
|
|
|
|
function finiteDimension(value: number, fallback = 0): number {
|
|
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
}
|
|
|
|
export function calculateDownloadVirtualWindow<T extends DownloadVirtualRowInput>(rows: readonly T[], options: DownloadVirtualWindowOptions): DownloadVirtualWindow<T> {
|
|
if (rows.length === 0) return { rows: [], totalHeight: 0, startIndex: 0, endIndex: -1 };
|
|
|
|
const overscan = Math.max(0, Math.floor(finiteDimension(options.overscan ?? DOWNLOAD_VIRTUAL_OVERSCAN_ROWS)));
|
|
const scrollTop = finiteDimension(options.scrollTop);
|
|
const viewportHeight = finiteDimension(options.viewportHeight, DOWNLOAD_VIRTUAL_DEFAULT_VIEWPORT_HEIGHT);
|
|
const viewportBottom = scrollTop + viewportHeight;
|
|
const pinnedIds = new Set(Array.from(options.pinnedIds ?? []).filter((id): id is string => Boolean(id)));
|
|
const tops: number[] = [];
|
|
let totalHeight = 0;
|
|
let firstVisible = -1;
|
|
let lastVisible = -1;
|
|
|
|
for (let index = 0; index < rows.length; index += 1) {
|
|
const row = rows[index];
|
|
const height = finiteDimension(row.height);
|
|
const top = totalHeight;
|
|
const bottom = top + height;
|
|
tops.push(top);
|
|
totalHeight = bottom;
|
|
if (firstVisible === -1 && bottom > scrollTop) firstVisible = index;
|
|
if (top < viewportBottom) lastVisible = index;
|
|
}
|
|
|
|
if (firstVisible === -1) firstVisible = rows.length - 1;
|
|
if (lastVisible < firstVisible) lastVisible = firstVisible;
|
|
|
|
const startIndex = Math.max(0, firstVisible - overscan);
|
|
const endIndex = Math.min(rows.length - 1, lastVisible + overscan);
|
|
const selected = new Set<number>();
|
|
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
selected.add(index);
|
|
}
|
|
for (let index = 0; index < rows.length; index += 1) {
|
|
if (pinnedIds.has(rows[index].id)) selected.add(index);
|
|
}
|
|
|
|
const positioned = [...selected].sort((left, right) => left - right).map((index) => {
|
|
const source = rows[index];
|
|
return {
|
|
id: source.id,
|
|
index,
|
|
top: tops[index],
|
|
height: finiteDimension(source.height),
|
|
pinned: pinnedIds.has(source.id),
|
|
source
|
|
};
|
|
});
|
|
|
|
return { rows: positioned, totalHeight, startIndex, endIndex };
|
|
}
|