/** * usePretextRowHeights * * Pre-computes row heights using pretext text measurement. * For each row, measures each cell's height at the given column width, * then takes the maximum + padding as the row height. */ import { computed, type Ref } from "vue"; import { measureTextHeight } from "./measureText"; import type { ListTableColumn } from "./types"; import { toRefs } from "@vueuse/core"; const DEFAULT_FONT = "14px Inter, sans-serif"; const DEFAULT_LINE_HEIGHT = 20; const DEFAULT_ROW_PADDING = 5; const CELL_VERTICAL_PADDING = 12; // top + bottom per cell export interface RowHeightEntry { height: number; isCustomRenderer: boolean; } export function resolveRowHeights( data: Ref, columns: Ref[]>, columnWidths: Ref, lineMaxWidth: number, formatCellValue: (row: T, col: ListTableColumn) => string, options?: { font?: string; lineHeight?: number; rowPadding?: number; fixedRowHeight?: number; debug?: boolean; } ) { const font = options?.font ?? DEFAULT_FONT; const lineHeight = options?.lineHeight ?? DEFAULT_LINE_HEIGHT; const rowPadding = options?.rowPadding ?? DEFAULT_ROW_PADDING; const { rowHeights, cellHeights } = toRefs( computed(() => { if (!data.value.length || !columns.value.length || !columnWidths.value.length) { return { rowHeights: [], cellHeights: [] }; } const rowHeights: number[] = []; const cellHeights: Array[] | undefined = options?.debug ? [] : undefined; for (const row of data.value) { let maxCellHeight = lineHeight; // minimum 1 line let rowCellHeights: Array = []; for (let i = 0; i < columns.value.length; i++) { const col = columns.value[i]; // Use flex basis width for height calculation as an approximation // of actual column width (CSS flex layout determines actual width) const colWidth = columnWidths.value[i] ?? 120; // Check if custom renderer exists (we can't measure these with pretext) const isCustomRenderer = !!(col.cellRenderer || col.slot); let rowHeightEntry: RowHeightEntry; if (options?.fixedRowHeight) { rowHeightEntry = { height: options.fixedRowHeight, isCustomRenderer, }; } else if (isCustomRenderer) { rowHeightEntry = { height: 42, isCustomRenderer, }; } else { try { const cellText = formatCellValue(row, col); if (!cellText) throw "Invalid Cell Text"; // Calculate available width for text (excluding cell padding) const availableWidth = Math.min(colWidth - CELL_VERTICAL_PADDING * 2, col.maxTextWidth ?? lineMaxWidth); if (availableWidth <= 0) throw "Invalid Column Width"; rowHeightEntry = { height: measureTextHeight(cellText, font, availableWidth, lineHeight), isCustomRenderer, }; } catch { // Fallback: assume single line rowHeightEntry = { height: 42, isCustomRenderer, }; } } maxCellHeight = Math.max(maxCellHeight, rowHeightEntry?.height ?? 0); if (options?.debug) { rowCellHeights.push(rowHeightEntry); } } const rowHeight = maxCellHeight + rowPadding * 2; rowHeights.push(rowHeight); cellHeights?.push(rowCellHeights); } return { rowHeights, cellHeights, }; }) ); // Total height (sum of all row heights) - useful for virtualizer const totalHeight = computed(() => rowHeights.value.reduce((sum, height) => sum + height, 0)); return { rowHeights, totalHeight, cellHeights, }; }