import React from 'react'
const LIMIAR_RENDERIZACAO_VIRTUAL = 1500
const ESTIMATIVA_ALTURA_LINHA_PX = 33
const OVERSCAN_LINHAS = 40
const MIN_JANELA_LINHAS = 220
const SORT_COLLATOR = new Intl.Collator('pt-BR', { numeric: true, sensitivity: 'base' })
function normalizeSortText(value) {
if (value === null || value === undefined) return ''
return String(value).trim()
}
function parseSortableNumber(value) {
if (typeof value === 'number') {
return Number.isFinite(value) ? value : null
}
const raw = normalizeSortText(value)
if (!raw) return null
const compact = raw
.replace(/\u00a0/g, '')
.replace(/\s+/g, '')
.replace('%', '')
if (!compact) return null
const candidates = [
compact,
compact.replace(/\./g, '').replace(',', '.'),
compact.replace(/,/g, ''),
]
for (let index = 0; index < candidates.length; index += 1) {
const parsed = Number(candidates[index])
if (Number.isFinite(parsed)) return parsed
}
return null
}
function DataTable({
table,
maxHeight = 320,
highlightedRowIndices = null,
highlightIndexColumn = 'Índice',
highlightClassName = 'table-row-highlight',
}) {
if (!table || !table.columns || !table.rows) {
return
Sem dados.
}
const columns = Array.isArray(table.columns) ? table.columns : []
const sourceRows = Array.isArray(table.rows) ? table.rows : []
const [sortConfig, setSortConfig] = React.useState(null)
const sortedRows = React.useMemo(() => {
if (!sortConfig || !columns.includes(sortConfig.column)) return sourceRows
const direction = sortConfig.direction === 'desc' ? 'desc' : 'asc'
const column = sortConfig.column
const indexedRows = sourceRows.map((row, index) => ({ row, index }))
indexedRows.sort((leftItem, rightItem) => {
const leftValue = leftItem.row?.[column]
const rightValue = rightItem.row?.[column]
const leftText = normalizeSortText(leftValue)
const rightText = normalizeSortText(rightValue)
const leftEmpty = leftText === ''
const rightEmpty = rightText === ''
if (leftEmpty || rightEmpty) {
if (leftEmpty && rightEmpty) return leftItem.index - rightItem.index
return leftEmpty ? 1 : -1
}
const leftNumber = parseSortableNumber(leftValue)
const rightNumber = parseSortableNumber(rightValue)
let comparison = 0
if (leftNumber !== null && rightNumber !== null) {
if (leftNumber < rightNumber) comparison = -1
else if (leftNumber > rightNumber) comparison = 1
} else {
comparison = SORT_COLLATOR.compare(leftText, rightText)
}
if (comparison === 0) return leftItem.index - rightItem.index
return direction === 'asc' ? comparison : -comparison
})
return indexedRows.map((item) => item.row)
}, [columns, sourceRows, sortConfig])
const totalRows = sortedRows.length
const colSpan = Math.max(1, columns.length)
const virtualizacaoAtiva = totalRows > LIMIAR_RENDERIZACAO_VIRTUAL
const wrapperRef = React.useRef(null)
const [scrollTop, setScrollTop] = React.useState(0)
const [viewportHeight, setViewportHeight] = React.useState(Number(maxHeight) || 320)
const tableIdentity = React.useMemo(() => {
const colunas = columns.join("|")
return `${colunas}::${sourceRows.length}`
}, [columns, sourceRows.length])
React.useEffect(() => {
setSortConfig((prev) => {
if (!prev) return prev
if (!columns.includes(prev.column)) return null
return prev
})
}, [columns])
React.useEffect(() => {
setScrollTop(0)
if (wrapperRef.current) {
wrapperRef.current.scrollTop = 0
setViewportHeight(wrapperRef.current.clientHeight || Number(maxHeight) || 320)
}
}, [tableIdentity, maxHeight])
React.useEffect(() => {
if (!wrapperRef.current) return undefined
const target = wrapperRef.current
if (typeof ResizeObserver === 'undefined') return undefined
const observer = new ResizeObserver(() => {
setViewportHeight(target.clientHeight || Number(maxHeight) || 320)
})
observer.observe(target)
return () => observer.disconnect()
}, [maxHeight])
const onWrapperScroll = React.useCallback((event) => {
if (!virtualizacaoAtiva) return
setScrollTop(event.currentTarget.scrollTop || 0)
}, [virtualizacaoAtiva])
const onToggleSort = React.useCallback((column) => {
setSortConfig((prev) => {
if (!prev || prev.column !== column) {
return { column, direction: 'asc' }
}
if (prev.direction === 'asc') {
return { column, direction: 'desc' }
}
return null
})
}, [])
const alturaViewport = Math.max(1, Number(viewportHeight) || Number(maxHeight) || 320)
const linhasVisiveis = Math.max(1, Math.ceil(alturaViewport / ESTIMATIVA_ALTURA_LINHA_PX))
const janelaLinhas = Math.max(MIN_JANELA_LINHAS, linhasVisiveis + (OVERSCAN_LINHAS * 2))
let startIndex = 0
let endIndex = totalRows
if (virtualizacaoAtiva) {
const primeiraLinhaVisivel = Math.max(0, Math.floor(scrollTop / ESTIMATIVA_ALTURA_LINHA_PX))
startIndex = Math.max(0, primeiraLinhaVisivel - OVERSCAN_LINHAS)
endIndex = Math.min(totalRows, startIndex + janelaLinhas)
if (endIndex - startIndex < janelaLinhas && startIndex > 0) {
startIndex = Math.max(0, endIndex - janelaLinhas)
}
}
const rowsToRender = virtualizacaoAtiva
? sortedRows.slice(startIndex, endIndex)
: sortedRows
const topSpacerHeight = virtualizacaoAtiva ? startIndex * ESTIMATIVA_ALTURA_LINHA_PX : 0
const bottomSpacerHeight = virtualizacaoAtiva ? Math.max(0, (totalRows - endIndex) * ESTIMATIVA_ALTURA_LINHA_PX) : 0
const highlightedSet = Array.isArray(highlightedRowIndices) && highlightedRowIndices.length > 0
? new Set(highlightedRowIndices.map((item) => String(item)))
: null
return (
{columns.map((col) => {
const isActiveSort = sortConfig?.column === col
const direction = isActiveSort ? sortConfig.direction : null
const sortIndicator = !direction ? '-' : (direction === 'asc' ? '^' : 'v')
const ariaSort = direction === 'asc' ? 'ascending' : direction === 'desc' ? 'descending' : 'none'
return (
onToggleSort(col)}
title={isActiveSort ? `Ordenado por ${col} (${direction === 'asc' ? 'crescente' : 'decrescente'})` : `Ordenar por ${col}`}
>
{col}
{sortIndicator}
)
})}
{virtualizacaoAtiva && topSpacerHeight > 0 ? (
) : null}
{rowsToRender.map((row, i) => {
const absoluteIndex = virtualizacaoAtiva ? startIndex + i : i
const rowIndex = row?.[highlightIndexColumn]
const rowClassName = highlightedSet && rowIndex != null && highlightedSet.has(String(rowIndex))
? highlightClassName
: ''
return (
{columns.map((col) => (
{String(row[col] ?? '')}
))}
)
})}
{virtualizacaoAtiva && bottomSpacerHeight > 0 ? (
) : null}
{virtualizacaoAtiva ? (
Exibindo janela virtual com {rowsToRender.length} linhas de {totalRows} (ativada acima de {LIMIAR_RENDERIZACAO_VIRTUAL} linhas, sem supressão de dados).
) : null}
{table.truncated ? (
Mostrando {table.returned_rows} de {table.total_rows} linhas.
) : null}
)
}
export default React.memo(DataTable)