feat(grid): add transposed table view

This commit is contained in:
t8y2
2026-05-17 13:32:42 +08:00
parent 933c1fd803
commit e4bb342bf4
3 changed files with 988 additions and 560 deletions
File diff suppressed because it is too large Load Diff
+157
View File
@@ -0,0 +1,157 @@
export interface DataGridTransposeState {
showTranspose: boolean;
transposeRowIndex: number | null;
}
export interface BuildTransposeRowsOptions<T> {
columns: string[];
records: T[][];
typeByColumn?: Map<string, string>;
displayValue: (value: T, column: string, columnIndex: number, recordIndex: number) => string;
}
export interface DataGridTransposeCell<T> {
value: T;
display: string;
isNull: boolean;
}
export interface DataGridTransposeRow<T> {
id: string;
column: string;
type: string;
values: Array<DataGridTransposeCell<T>>;
}
export interface TransposeRecordWindowOptions {
totalRecords: number;
scrollLeft: number;
viewportWidth: number;
pinnedWidth: number;
recordWidth: number;
overscan?: number;
}
export interface TransposeRecordWindow {
start: number;
end: number;
beforeWidth: number;
afterWidth: number;
}
export interface TransposeSelectionRange {
startRow: number;
endRow: number;
startCol: number;
endCol: number;
}
export interface TransposeAnchorOptions {
requestedRowIndex: number;
rowIds: number[];
selectedRowIds: Set<number>;
selectedRange: TransposeSelectionRange | null;
}
export interface TransposeFieldWidthOptions {
minWidth?: number;
maxWidth?: number;
charWidth?: number;
padding?: number;
}
export interface TransposeScrollLeftOptions {
recordIndex: number;
totalRecords: number;
viewportWidth: number;
pinnedWidth: number;
recordWidth: number;
}
export function nextTransposeState(
showTranspose: boolean,
transposeRowIndex: number | null,
requestedRowIndex: number,
): DataGridTransposeState {
if (showTranspose && transposeRowIndex === requestedRowIndex) {
return { showTranspose: false, transposeRowIndex: null };
}
return { showTranspose: true, transposeRowIndex: requestedRowIndex };
}
export function buildTransposeRows<T>(options: BuildTransposeRowsOptions<T>): Array<DataGridTransposeRow<T>> {
return options.columns.map((column, columnIndex) => {
return {
id: `${columnIndex}:${column}`,
column,
type: options.typeByColumn?.get(column) || "",
values: options.records.map((record, recordIndex) => {
const value = record[columnIndex] as T;
return {
value,
display: options.displayValue(value, column, columnIndex, recordIndex),
isNull: value === null,
};
}),
};
});
}
export function visibleTransposeRecordWindow(options: TransposeRecordWindowOptions): TransposeRecordWindow {
if (options.totalRecords <= 0 || options.recordWidth <= 0) {
return { start: 0, end: 0, beforeWidth: 0, afterWidth: 0 };
}
const overscan = options.overscan ?? 2;
const recordScrollLeft = Math.max(0, options.scrollLeft - options.pinnedWidth);
const recordViewportWidth = Math.max(0, options.viewportWidth - options.pinnedWidth);
const start = Math.max(0, Math.floor(recordScrollLeft / options.recordWidth) - overscan);
const end = Math.min(
options.totalRecords,
Math.ceil((recordScrollLeft + recordViewportWidth) / options.recordWidth) + overscan + 1,
);
return {
start,
end,
beforeWidth: start * options.recordWidth,
afterWidth: Math.max(0, (options.totalRecords - end) * options.recordWidth),
};
}
export function transposeAnchorRowIndex(options: TransposeAnchorOptions): number {
const requestedRowId = options.rowIds[options.requestedRowIndex];
if (requestedRowId !== undefined && options.selectedRowIds.size > 1 && options.selectedRowIds.has(requestedRowId)) {
const firstSelectedIndex = options.rowIds.findIndex((rowId) => options.selectedRowIds.has(rowId));
if (firstSelectedIndex >= 0) return firstSelectedIndex;
}
const range = options.selectedRange;
if (
range &&
range.startRow !== range.endRow &&
options.requestedRowIndex >= range.startRow &&
options.requestedRowIndex <= range.endRow
) {
return range.startRow;
}
return options.requestedRowIndex;
}
export function transposeFieldWidth(columns: string[], options: TransposeFieldWidthOptions = {}): number {
const minWidth = options.minWidth ?? 104;
const maxWidth = options.maxWidth ?? 220;
const charWidth = options.charWidth ?? 8;
const padding = options.padding ?? 32;
const longest = columns.reduce((max, column) => Math.max(max, column.length), 0);
return Math.min(maxWidth, Math.max(minWidth, Math.ceil(longest * charWidth + padding)));
}
export function transposeScrollLeftForRecord(options: TransposeScrollLeftOptions): number {
if (options.recordWidth <= 0 || options.totalRecords <= 0) return 0;
const desired = Math.max(0, options.recordIndex) * options.recordWidth;
const totalWidth = options.pinnedWidth + options.totalRecords * options.recordWidth;
const maxScrollLeft = Math.max(0, totalWidth - options.viewportWidth);
return Math.min(desired, maxScrollLeft);
}