/* eslint-disable unicorn/prefer-logical-operator-over-ternary */ /** @jsxImportSource @emotion/react */ import type { CSSObject, SerializedStyles } from '@emotion/react'; import { css } from '@emotion/react'; import type { CSSProperties, ReactElement, Ref, WheelEvent } from 'react'; import { forwardRef, memo, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react'; import { useResizeObserver } from 'react-d3-utils'; import type { CellProps, Column as ReactColumn, TableInstance, UseSortByColumnOptions, UseSortByInstanceProps, UseSortByOptions, UseTableOptions, } from 'react-table'; import { useSortBy, useTable } from 'react-table'; import type { HighlightEventSource } from '../../highlight/index.js'; import type { BaseContextMenuProps } from '../ContextMenuBluePrint.js'; import { BaseReactTable } from './BaseReactTable.js'; import { EmptyDataRow } from './Elements/EmptyDataRow.js'; import ReactTableHeader from './Elements/ReactTableHeader.js'; import type { ClickEvent } from './Elements/ReactTableRow.js'; import ReactTableRow from './Elements/ReactTableRow.js'; import { ReactTableProvider, useReactTableContext, } from './utility/ReactTableContext.js'; import type { RowSpanHeaders } from './utility/useRowSpan.js'; import useRowSpan, { prepareRowSpan } from './utility/useRowSpan.js'; interface ExtraColumn { enableRowSpan?: boolean; style?: CSSProperties; Cell?: (cell: CellProps) => ReactElement | string; } export type Column = ReactColumn & ExtraColumn & UseSortByColumnOptions; type TableInstanceWithHooks = TableInstance & { rowSpanHeaders: RowSpanHeaders; } & UseSortByInstanceProps; type TableOptions = UseTableOptions & UseSortByOptions; interface SortEvent { onSortEnd?: (data: any, isTableSorted?: boolean) => void; } export interface BaseRowStyle { active?: CSSProperties; activated?: CSSProperties; hover?: CSSProperties; base?: CSSProperties; } export interface TableContextMenuProps { onContextMenuSelect?: ( selected: Parameters[0], data: any, ) => void; contextMenu?: BaseContextMenuProps['options']; } interface ReactTableProps extends TableContextMenuProps, ClickEvent, SortEvent { data: T[]; columns: Array>; highlightedSource?: HighlightEventSource; approxItemHeight?: number; approxColumnWidth?: number; groupKey?: keyof T; indexKey?: string; enableVirtualScroll?: boolean; enableColumnsVirtualScroll?: boolean; activeRow?: (data: any) => boolean; enableDefaultActiveRow?: boolean; totalCount?: number; emptyDataRowText?: string; rowStyle?: BaseRowStyle | ((data: T) => BaseRowStyle | undefined); style?: CSSObject | SerializedStyles; disableDefaultRowStyle?: boolean; } interface ReactTableInnerProps extends ReactTableProps { onScroll?: (event: WheelEvent) => void; } const styles = { table: ( enableVirtualScroll: boolean, enableColumnsVirtualScroll: any, ): CSSProperties => { const style: CSSProperties = { tableLayout: 'auto' }; if (enableVirtualScroll) { style.position = 'sticky'; style.top = 0; } if (enableColumnsVirtualScroll) { style.position = 'sticky'; style.left = 0; } return style; }, }; const counterStyle: CSSProperties = { position: 'absolute', bottom: 0, width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.3)', pointerEvents: 'none', textAlign: 'center', fontWeight: 'bolder', color: 'white', fontSize: '1.4em', }; const ReactTableInner = forwardRef(TableInner) as ( props: ReactTableInnerProps & { ref?: Ref }, ) => ReactElement; function TableInner( props: ReactTableInnerProps, ref: Ref, ) { const { data, columns, highlightedSource, contextMenu = [], onContextMenuSelect, onScroll, approxItemHeight = 40, enableVirtualScroll = false, enableColumnsVirtualScroll = false, approxColumnWidth = 40, groupKey, onClick, activeRow, totalCount, indexKey = 'index', onSortEnd, rowStyle, disableDefaultRowStyle = false, enableDefaultActiveRow = false, emptyDataRowText = 'No Data', } = props; const isSortedEventTriggered = useRef(false); const virtualBoundary = useReactTableContext(); const [rowIndex, setRowIndex] = useState(); const timeoutIdRef = useRef(); const [isCounterVisible, setCounterVisibility] = useState(false); const memoColumns = useMemo(() => { const end = virtualBoundary.columns.end === columns.length - 1 ? virtualBoundary.columns.end + 1 : virtualBoundary.columns.end; return enableColumnsVirtualScroll ? columns.slice(virtualBoundary.columns.start, end) : columns; }, [enableColumnsVirtualScroll, virtualBoundary.columns, columns]); const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow, rowSpanHeaders, state, // Access the sort state here } = useTable( { columns: memoColumns, data, autoResetSortBy: false, } as TableOptions, useSortBy, useRowSpan, ) as TableInstanceWithHooks; const { sortBy } = state as any; function clickHandler(event: any, row: any) { setRowIndex(row.index); onClick?.(event, row); } function scrollHandler(e: any) { if (enableVirtualScroll) { onScroll?.(e); } if (timeoutIdRef.current) { clearTimeout(timeoutIdRef.current); setCounterVisibility(true); } timeoutIdRef.current = setTimeout(() => { setCounterVisibility(false); }, 1000); } useEffect(() => { if (isSortedEventTriggered.current) { const isTableSorted = sortBy.length > 0; const data = rows.map((row) => row.original); onSortEnd?.(data, isTableSorted); isSortedEventTriggered.current = false; } }, [onSortEnd, rows, sortBy?.length]); function headerClickHandler() { isSortedEventTriggered.current = true; } const end = virtualBoundary.rows.end === rows.length - 1 ? virtualBoundary.rows.end + 1 : virtualBoundary.rows.end; const rowsData = enableVirtualScroll ? rows.slice(virtualBoundary.rows.start, end) : rows; const lastRow = rowsData.at(-1); const index = (lastRow?.original as any)?.[indexKey] || lastRow?.index; const total = totalCount ? totalCount : data.length; const startColumn = columns[virtualBoundary.columns.start]?.Header; return ( <>
{!data || (data?.length === 0 && ( ))} {rowsData.map((row, index) => { prepareRow(row); prepareRowSpan( rows, enableVirtualScroll ? index + virtualBoundary.rows.start : index, rowSpanHeaders, groupKey, ); const { key, ...restRowProps } = row.getRowProps(); return ( ); })}
{(enableVirtualScroll || enableColumnsVirtualScroll) && (

{enableColumnsVirtualScroll && typeof startColumn === 'string' && ( {`Column ${startColumn}`} )} {typeof index === 'number' ? index + 1 : total} / {total}

)} ); } interface VirtualBoundary { start: number; end: number; } export interface TableVirtualBoundary { rows: VirtualBoundary; columns: VirtualBoundary; } function ReactTable(props: ReactTableProps) { const { data, approxItemHeight = 40, approxColumnWidth = 40, groupKey, onSortEnd, columns, style = {}, } = props; const containerRef = useRef(null); const visibleRowsCountRef = useRef(0); const visibleColumnsCountRef = useRef(0); const [mRef, { width, height } = { width: 0, height: 0 }] = useResizeObserver(); const [tableVirtualBoundary, setTableVirtualBoundary] = useState({ rows: { start: 1, end: 0, }, columns: { start: 1, end: 0, }, }); useLayoutEffect(() => { if (containerRef.current && height && width) { const header = containerRef.current.querySelectorAll('thead'); const rowsCount = Math.ceil( (Math.ceil(height) - Math.ceil(header[0].clientHeight)) / approxItemHeight, ); const columnsCount = Math.ceil(Math.ceil(width) / approxColumnWidth); if ( (rowsCount > 0 && rowsCount !== visibleRowsCountRef.current) || (columnsCount > 0 && columnsCount !== visibleColumnsCountRef.current) ) { visibleRowsCountRef.current = rowsCount; visibleColumnsCountRef.current = columnsCount; setTableVirtualBoundary({ rows: { start: 0, end: rowsCount }, columns: { start: 0, end: columnsCount }, }); } } }, [approxColumnWidth, approxItemHeight, height, width]); function lookForGroupIndex(currentIndex: number, side: 1 | -1) { const currentItem = data[currentIndex]; if ((currentItem as { index: number })?.index && groupKey) { switch (side) { case -1: { let index = currentIndex - 1; while (index > 0) { if (data[index][groupKey] !== currentItem[groupKey]) { return index + 1; } index--; } return currentIndex; } case 1: { let index = currentIndex + 1; while (index < data.length) { if (data[index][groupKey] !== currentItem[groupKey]) { return index - 1; } index++; } return currentIndex; } default: return currentIndex; } } return currentIndex; } function findColumnStartIndex(index: number, numberOfVisibleColumns: number) { const newIndex = index - numberOfVisibleColumns; return newIndex >= columns.length ? newIndex : index; } function findColumnEndIndex(index: number, numberOfVisibleColumns: number) { const newIndex = index + numberOfVisibleColumns; return newIndex >= columns.length ? columns.length - 1 : newIndex; } function findStartIndex(index: number, numberOfVisibleRows: number) { const newIndex = index - numberOfVisibleRows; const currentIndx = newIndex >= data.length ? newIndex : index; // return currentIndx; // Look for the first index of the group return lookForGroupIndex(currentIndx, -1); } function findEndIndex(index: number, numberOfVisibleRows: number) { const newIndex = index + numberOfVisibleRows; const currentIndx = newIndex >= data.length ? data.length - 1 : newIndex; // return currentIndx; // Look for the last index of the group return lookForGroupIndex(currentIndx, 1); } function scrollHandler() { if (containerRef.current) { const { scrollTop, scrollLeft } = containerRef.current; const rowCurrentIndx = Math.ceil(scrollTop / approxItemHeight); const rowStart = findStartIndex( rowCurrentIndx, visibleRowsCountRef.current, ); const rowEnd = findEndIndex(rowCurrentIndx, visibleRowsCountRef.current); const columnCurrentIndx = Math.ceil(scrollLeft / approxColumnWidth); const columnStart = findColumnStartIndex( columnCurrentIndx, visibleColumnsCountRef.current, ); const columnEnd = findColumnEndIndex( columnCurrentIndx, visibleColumnsCountRef.current, ); setTableVirtualBoundary({ rows: { start: rowStart, end: rowEnd }, columns: { start: columnStart, end: columnEnd }, }); } } return (
onScroll={scrollHandler} onSortEnd={onSortEnd} ref={containerRef} {...props} />
); } export default memo(ReactTable) as ( props: ReactTableInnerProps, ) => ReactElement;