import { useId, useRef, useState, type CSSProperties, type PointerEvent as ReactPointerEvent, type ReactNode } from 'react'; import { useResultTableSelection } from './ResultTableSelection'; const MIN_COLUMN_WIDTH = 72; const MAX_COLUMN_WIDTH = 720; interface ColumnDrag { pointerId: number; columnIndex: number; startX: number; startWidths: number[]; } interface Props { headers: ReactNode[]; children?: ReactNode; enabled?: boolean; className?: string; sourceSheetId?: string; } function clampColumnWidth(width: number): number { return Math.max(MIN_COLUMN_WIDTH, Math.min(MAX_COLUMN_WIDTH, Math.round(width))); } function columnWidthStyle(width: number): CSSProperties { return { width: `${width}px` }; } function measuredColumnWidths(table: HTMLTableElement, columnCount: number): number[] { const headers = Array.from(table.tHead?.rows[0]?.cells ?? []); return Array.from({ length: columnCount }, (_, index) => clampColumnWidth(headers[index]?.getBoundingClientRect().width || MIN_COLUMN_WIDTH)); } /** * Misst den ungebrochenen Inhalt der Zielspalte. `white-space: nowrap` wird nur * fuer den Messaugenblick gesetzt und sofort wieder entfernt; importierter * Inhalt wird dabei weder kopiert noch protokolliert. */ function fittedColumnWidth(table: HTMLTableElement, columnIndex: number): number { const cells = Array.from(table.rows) .map(row => row.cells[columnIndex]) .filter((cell): cell is HTMLTableCellElement => Boolean(cell)); let preferred = MIN_COLUMN_WIDTH; for (const cell of cells) { const previousWhiteSpace = cell.style.whiteSpace; const previousWidth = cell.style.width; cell.style.whiteSpace = 'nowrap'; cell.style.width = 'auto'; preferred = Math.max(preferred, cell.scrollWidth + 2); cell.style.whiteSpace = previousWhiteSpace; cell.style.width = previousWidth; } return clampColumnWidth(preferred); } export function ResizableResultTable({ headers, children, enabled = false, className = 'rich-text-table', sourceSheetId }: Props) { const tableId = useId(); const selection = useResultTableSelection(); const columnCount = headers.length; const tableRef = useRef(null); const dragRef = useRef(null); const [columnWidths, setColumnWidths] = useState([]); const appliedWidths = enabled && columnWidths.length === columnCount ? columnWidths : []; const tableStyle: CSSProperties | undefined = appliedWidths.length ? { tableLayout: 'fixed', width: `${appliedWidths.reduce((sum, width) => sum + width, 0)}px` } : undefined; const startResize = (columnIndex: number) => (event: ReactPointerEvent) => { if (!enabled || !tableRef.current) return; const startWidths = measuredColumnWidths(tableRef.current, columnCount); dragRef.current = { pointerId: event.pointerId, columnIndex, startX: event.clientX, startWidths }; setColumnWidths(startWidths); event.currentTarget.setPointerCapture?.(event.pointerId); event.preventDefault(); event.stopPropagation(); }; const moveResize = (event: ReactPointerEvent) => { const drag = dragRef.current; if (!drag || drag.pointerId !== event.pointerId) return; const next = [...drag.startWidths]; next[drag.columnIndex] = clampColumnWidth(drag.startWidths[drag.columnIndex] + event.clientX - drag.startX); setColumnWidths(next); }; const stopResize = (event: ReactPointerEvent) => { if (dragRef.current?.pointerId !== event.pointerId) return; dragRef.current = null; if (event.currentTarget.hasPointerCapture?.(event.pointerId)) event.currentTarget.releasePointerCapture?.(event.pointerId); }; const autoFit = (columnIndex: number) => { const table = tableRef.current; if (!enabled || !table) return; const current = measuredColumnWidths(table, columnCount); current[columnIndex] = fittedColumnWidth(table, columnIndex); setColumnWidths(current); }; const resizeByKeyboard = (columnIndex: number, delta: number) => { const table = tableRef.current; if (!enabled || !table) return; const current = appliedWidths.length ? [...appliedWidths] : measuredColumnWidths(table, columnCount); current[columnIndex] = clampColumnWidth(current[columnIndex] + delta); setColumnWidths(current); }; return ( {(selection || enabled) && } {appliedWidths.length > 0 && {appliedWidths.map((width, index) => )}} {headers.map((header, index) => )}{children}
{selection && } {enabled && Column widths can be changed by dragging a header separator or fitted by double-clicking it.}
{header}{enabled && index < columnCount - 1 && { event.preventDefault(); event.stopPropagation(); autoFit(index); }} onKeyDown={event => { if (event.key === 'Enter') { event.preventDefault(); autoFit(index); } if (event.key === 'ArrowLeft') { event.preventDefault(); resizeByKeyboard(index, -16); } if (event.key === 'ArrowRight') { event.preventDefault(); resizeByKeyboard(index, 16); } }} />}
); } export { fittedColumnWidth, measuredColumnWidths };