'use client' import { useCallback, useEffect, useRef, useState } from 'react' import type { GridEditorCell } from './types' import { MIN_COL_WIDTH, MIN_ROW_HEIGHT, ROW_NUM_WIDTH } from './gridTableConstants' import type { GridTableProps } from './gridTableConstants' import { getCellColor, getRowHeight } from './gridTableUtils' import { GridTableColumnHeader } from './GridTableColumnHeader' import { GridTableRowHeader } from './GridTableRowHeader' import { GridTableCell } from './GridTableCell' import { GridTableColorMenu } from './GridTableColorMenu' export function GridTable({ zone, layoutMetrics, selectedCell, selectedCells, onSelectCell, onToggleCellSelection, onCellTextChange, onToggleColumnBold, onToggleRowHeader, onNavigate, onDeleteColumn, onAddColumn, onDeleteRow, onAddRow, onSetCellColor, }: GridTableProps) { const containerRef = useRef(null) const [containerWidth, setContainerWidth] = useState(0) const [colorMenu, setColorMenu] = useState<{ cellId: string; x: number; y: number } | null>(null) // ---------------------------------------------------------------- // Observe container width for scaling // ---------------------------------------------------------------- useEffect(() => { const el = containerRef.current if (!el) return const ro = new ResizeObserver(([entry]) => { setContainerWidth(entry.contentRect.width) }) ro.observe(el) return () => ro.disconnect() }, []) // ---------------------------------------------------------------- // Compute column widths from OCR measurements // ---------------------------------------------------------------- const [colWidthOverrides, setColWidthOverrides] = useState(null) const columnWidthsPx = zone.columns.map((col) => col.x_max_px - col.x_min_px) const totalColWidthPx = columnWidthsPx.reduce((sum, w) => sum + w, 0) const zoneWidthPx = totalColWidthPx > 0 ? totalColWidthPx : (zone.bbox_px.w || layoutMetrics?.page_width_px || 1) const scale = containerWidth > 0 ? (containerWidth - ROW_NUM_WIDTH) / zoneWidthPx : 1 const effectiveColWidths = (colWidthOverrides ?? columnWidthsPx).map( (w) => Math.max(MIN_COL_WIDTH, w * scale), ) // ---------------------------------------------------------------- // Row height // ---------------------------------------------------------------- const avgRowHeightPx = layoutMetrics?.avg_row_height_px ?? 30 const [rowHeightOverrides, setRowHeightOverrides] = useState>(new Map()) const computeRowHeight = (rowIndex: number, isHeader: boolean): number => { if (rowHeightOverrides.has(rowIndex)) { return rowHeightOverrides.get(rowIndex)! } return getRowHeight(zone, rowIndex, isHeader, avgRowHeightPx, scale) } // ---------------------------------------------------------------- // Font size from layout metrics // ---------------------------------------------------------------- const baseFontSize = layoutMetrics?.font_size_suggestion_px ? Math.max(11, layoutMetrics.font_size_suggestion_px * scale) : 13 // ---------------------------------------------------------------- // Keyboard navigation // ---------------------------------------------------------------- const handleKeyDown = useCallback( (e: React.KeyboardEvent, cellId: string) => { if (e.key === 'Tab') { e.preventDefault() onNavigate(cellId, e.shiftKey ? 'left' : 'right') } else if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() onNavigate(cellId, 'down') } else if (e.key === 'ArrowUp') { e.preventDefault() onNavigate(cellId, 'up') } else if (e.key === 'ArrowDown') { e.preventDefault() onNavigate(cellId, 'down') } else if (e.key === 'ArrowLeft' && e.altKey) { e.preventDefault() onNavigate(cellId, 'left') } else if (e.key === 'ArrowRight' && e.altKey) { e.preventDefault() onNavigate(cellId, 'right') } else if (e.key === 'Escape') { ;(e.target as HTMLElement).blur() } }, [onNavigate], ) // ---------------------------------------------------------------- // Cell lookup // ---------------------------------------------------------------- const cellMap = new Map() for (const cell of zone.cells) { cellMap.set(`${cell.row_index}_${cell.col_index}`, cell) } // ---------------------------------------------------------------- // Column resize (drag) // ---------------------------------------------------------------- const handleColResizeStart = useCallback( (colIndex: number, startX: number) => { const baseWidths = colWidthOverrides ?? [...columnWidthsPx] const handleMouseMove = (e: MouseEvent) => { const deltaPx = (e.clientX - startX) / scale const newWidths = [...baseWidths] newWidths[colIndex] = Math.max(20, baseWidths[colIndex] + deltaPx) if (colIndex + 1 < newWidths.length) { newWidths[colIndex + 1] = Math.max(20, baseWidths[colIndex + 1] - deltaPx) } setColWidthOverrides(newWidths) } const handleMouseUp = () => { document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mouseup', handleMouseUp) document.body.style.cursor = '' document.body.style.userSelect = '' } document.body.style.cursor = 'col-resize' document.body.style.userSelect = 'none' document.addEventListener('mousemove', handleMouseMove) document.addEventListener('mouseup', handleMouseUp) }, [colWidthOverrides, columnWidthsPx, scale], ) // ---------------------------------------------------------------- // Row resize (drag) // ---------------------------------------------------------------- const handleRowResizeStart = useCallback( (rowIndex: number, startY: number, currentHeight: number) => { const handleMouseMove = (e: MouseEvent) => { const delta = e.clientY - startY const newH = Math.max(MIN_ROW_HEIGHT, currentHeight + delta) setRowHeightOverrides((prev) => { const next = new Map(prev) next.set(rowIndex, newH) return next }) } const handleMouseUp = () => { document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mouseup', handleMouseUp) document.body.style.cursor = '' document.body.style.userSelect = '' } document.body.style.cursor = 'row-resize' document.body.style.userSelect = 'none' document.addEventListener('mousemove', handleMouseMove) document.addEventListener('mouseup', handleMouseUp) }, [], ) const isBoxZone = zone.zone_type === 'box' const numCols = zone.columns.length const gridTemplateCols = `${ROW_NUM_WIDTH}px ${effectiveColWidths.map((w) => `${w.toFixed(1)}px`).join(' ')}` return (
{/* Zone label */}
{isBoxZone ? 'Box' : 'Inhalt'} Zone {zone.zone_index} {zone.columns.length} Spalten, {zone.rows.length} Zeilen, {zone.cells.length} Zellen
{/* CSS Grid */}
{/* Header: row-number corner */}
{/* Header: column labels */} {zone.columns.map((col, ci) => ( ))} {/* Data rows */} {zone.rows.map((row) => { const rowH = computeRowHeight(row.index, row.is_header) const isSpanning = zone.cells.some( (c) => c.row_index === row.index && c.col_type === 'spanning_header', ) return (
{/* Row number cell */} {/* Cells — spanning header or normal columns */} {isSpanning ? ( <> {zone.cells .filter((c) => c.row_index === row.index && c.col_type === 'spanning_header') .sort((a, b) => a.col_index - b.col_index) .map((spanCell) => { const colspan = spanCell.colspan || numCols const cellId = spanCell.cell_id const isSelected = selectedCell === cellId const cellColor = getCellColor(spanCell) const gridColStart = spanCell.col_index + 2 const gridColEnd = gridColStart + colspan return (
{cellColor && ( )} onCellTextChange(cellId, e.target.value)} onFocus={() => onSelectCell(cellId)} onKeyDown={(e) => handleKeyDown(e, cellId)} className="w-full px-3 py-1 bg-transparent border-0 outline-none text-center" style={{ color: cellColor || undefined }} spellCheck={false} />
) })} ) : ( zone.columns.map((col) => { const cell = cellMap.get(`${row.index}_${col.index}`) return ( setColorMenu({ cellId, x, y })} handleKeyDown={handleKeyDown} /> ) }) )}
) })}
{/* Color context menu (right-click) */} {colorMenu && onSetCellColor && ( setColorMenu(null)} /> )}
) }