"use client" import { CSSProperties, Fragment, memo, MouseEvent as ReactMouseEvent, ReactNode, TouchEvent as ReactTouchEvent, Ref, useCallback, useEffect, useMemo, useState, } from "react" import { useDataGrid } from "@/components/reui/data-grid/data-grid" import { Cell, Column, flexRender, Header, HeaderGroup, Row, Table, } from "@tanstack/react-table" import { cva } from "class-variance-authority" import { cn } from "@/lib/utils" import { Checkbox } from "@/components/ui/checkbox" import { Spinner } from "@/components/ui/spinner" const headerCellSpacingVariants = cva("", { variants: { size: { dense: "px-2 h-8", default: "px-3", }, }, defaultVariants: { size: "default", }, }) const bodyCellSpacingVariants = cva("", { variants: { size: { dense: "px-2 py-1.5", default: "px-3 py-2", }, }, defaultVariants: { size: "default", }, }) const footerCellSpacingVariants = cva("", { variants: { size: { dense: "px-2 py-1.5", default: "px-3 py-2", }, }, defaultVariants: { size: "default", }, }) function getPinningStyles(column: Column): CSSProperties { const isPinned = column.getIsPinned() return { left: isPinned === "left" ? `${column.getStart("left")}px` : undefined, right: isPinned === "right" ? `${column.getAfter("right")}px` : undefined, position: isPinned ? "sticky" : "relative", width: column.getSize(), zIndex: isPinned ? 1 : 0, } } function assignRef(ref: Ref | undefined, value: T | null) { if (!ref) return if (typeof ref === "function") { ref(value) return } ;(ref as { current: T | null }).current = value } type DataGridResizeStartEvent = | ReactMouseEvent | ReactTouchEvent type DataGridResizeDocumentEvent = globalThis.MouseEvent | globalThis.TouchEvent function isDataGridTouchEvent( event: DataGridResizeStartEvent | DataGridResizeDocumentEvent ): event is ReactTouchEvent | globalThis.TouchEvent { return "touches" in event } function getDataGridResizeEventClientX( event: DataGridResizeStartEvent | DataGridResizeDocumentEvent ) { if (isDataGridTouchEvent(event)) { return event.touches[0]?.clientX ?? event.changedTouches[0]?.clientX } return event.clientX } function startDataGridColumnResizeOnEnd( event: DataGridResizeStartEvent, header: Header, table: Table ) { const column = table.getColumn(header.column.id) if (!column || !column.getCanResize()) return if (isDataGridTouchEvent(event) && event.touches.length > 1) return event.persist?.() const ownerDocument = event.currentTarget.ownerDocument const previousBodyCursor = ownerDocument.body.style.cursor const previousDocumentCursor = ownerDocument.documentElement.style.cursor const startSize = header.getSize() const dragStartClientX = getDataGridResizeEventClientX(event) const headerCell = event.currentTarget.closest("th") const headerRect = headerCell?.getBoundingClientRect() const startOffset = headerRect && Number.isFinite( table.options.columnResizeDirection === "rtl" ? headerRect.left : headerRect.right ) ? table.options.columnResizeDirection === "rtl" ? headerRect.left : headerRect.right : dragStartClientX if (typeof dragStartClientX !== "number" || typeof startOffset !== "number") { return } ownerDocument.body.style.cursor = "col-resize" ownerDocument.documentElement.style.cursor = "col-resize" const columnSizingStart = header .getLeafHeaders() .map( (leafHeader) => [leafHeader.column.id, leafHeader.column.getSize()] as [string, number] ) const directionMultiplier = table.options.columnResizeDirection === "rtl" ? -1 : 1 const updateOffset = (clientXPos?: number, commit = false) => { if (typeof clientXPos !== "number") return let nextColumnSizing: Record = {} const deltaOffset = (clientXPos - dragStartClientX) * directionMultiplier const deltaPercentage = Math.max(deltaOffset / startSize, -0.999999) columnSizingStart.forEach(([columnId, headerSize]) => { nextColumnSizing[columnId] = Math.round( Math.max(headerSize + headerSize * deltaPercentage, 0) * 100 ) / 100 }) table.setColumnSizingInfo((old) => ({ ...old, startOffset, startSize, deltaOffset, deltaPercentage, columnSizingStart, isResizingColumn: column.id, })) if (commit) { table.setColumnSizing((old) => ({ ...old, ...nextColumnSizing, })) } } const endResize = (clientXPos?: number) => { updateOffset(clientXPos, true) table.setColumnSizingInfo((old) => ({ ...old, isResizingColumn: false, startOffset: null, startSize: null, deltaOffset: null, deltaPercentage: null, columnSizingStart: [], })) ownerDocument.body.style.cursor = previousBodyCursor ownerDocument.documentElement.style.cursor = previousDocumentCursor } const mouseMoveHandler = (moveEvent: globalThis.MouseEvent) => { updateOffset(moveEvent.clientX) } const mouseUpHandler = (upEvent: globalThis.MouseEvent) => { ownerDocument.removeEventListener("mousemove", mouseMoveHandler) ownerDocument.removeEventListener("mouseup", mouseUpHandler) endResize(upEvent.clientX) } const touchMoveHandler = (moveEvent: globalThis.TouchEvent) => { if (moveEvent.cancelable) { moveEvent.preventDefault() moveEvent.stopPropagation() } updateOffset(getDataGridResizeEventClientX(moveEvent)) } const touchEndHandler = (endEvent: globalThis.TouchEvent) => { ownerDocument.removeEventListener("touchmove", touchMoveHandler) ownerDocument.removeEventListener("touchend", touchEndHandler) if (endEvent.cancelable) { endEvent.preventDefault() endEvent.stopPropagation() } endResize(getDataGridResizeEventClientX(endEvent)) } const passiveIfSupported = { passive: false } as const if (isDataGridTouchEvent(event)) { ownerDocument.addEventListener( "touchmove", touchMoveHandler, passiveIfSupported ) ownerDocument.addEventListener( "touchend", touchEndHandler, passiveIfSupported ) } else { ownerDocument.addEventListener( "mousemove", mouseMoveHandler, passiveIfSupported ) ownerDocument.addEventListener( "mouseup", mouseUpHandler, passiveIfSupported ) } table.setColumnSizingInfo((old) => ({ ...old, startOffset, startSize, deltaOffset: 0, deltaPercentage: 0, columnSizingStart, isResizingColumn: column.id, })) } type DataGridTablePinnedBoundary = "top" | "bottom" function getDataGridTableRowSections( table: Table, rowsPinnable?: boolean ) { if (!rowsPinnable) { return { topRows: [] as Row[], centerRows: table.getRowModel().rows as Row[], bottomRows: [] as Row[], } } return { topRows: table.getTopRows() as Row[], centerRows: table.getCenterRows() as Row[], bottomRows: table.getBottomRows() as Row[], } } function getDataGridTableResolvedRows( table: Table, rowsPinnable?: boolean ) { const { topRows, centerRows, bottomRows } = getDataGridTableRowSections( table, rowsPinnable ) const resolvedRows: Array<{ row: Row pinnedBoundary?: DataGridTablePinnedBoundary }> = [] topRows.forEach((row, index) => { resolvedRows.push({ row, pinnedBoundary: index === topRows.length - 1 && (centerRows.length > 0 || bottomRows.length > 0) ? "top" : undefined, }) }) centerRows.forEach((row) => { resolvedRows.push({ row }) }) bottomRows.forEach((row, index) => { resolvedRows.push({ row, pinnedBoundary: index === 0 && (centerRows.length > 0 || topRows.length > 0) ? "bottom" : undefined, }) }) return resolvedRows } function DataGridTableFillCol() { const { props } = useDataGrid() if (!props.tableLayout?.columnsResizable) return null return ( ) } function DataGridTableFillHeadCell() { const { props } = useDataGrid() if (!props.tableLayout?.columnsResizable) return null return ( ) } function DataGridTableFillBodyCell() { const { props } = useDataGrid() if (!props.tableLayout?.columnsResizable) return null return ( ) } function DataGridTableFillFootCell() { const { props } = useDataGrid() if (!props.tableLayout?.columnsResizable) return null return ( ) } function DataGridTableBase({ children }: { children: ReactNode }) { const { props, table } = useDataGrid() const visibleColumns = table.getVisibleLeafColumns() /** * Compute column widths as CSS custom properties once upfront (memoized). * Cells reference these via calc(var(--col-X-size) * 1px) so the browser * handles width propagation without per-cell getSize() calls or React * re-renders of the body. */ const columnSizeVars = useMemo(() => { if (!props.tableLayout?.columnsResizable) return undefined const headers = table.getFlatHeaders() const colSizes: Record = {} for (let i = 0; i < headers.length; i++) { const header = headers[i]! colSizes[`--header-${header.id}-size`] = header.getSize() colSizes[`--col-${header.column.id}-size`] = header.column.getSize() } return colSizes // eslint-disable-next-line react-hooks/exhaustive-deps }, [ props.tableLayout?.columnsResizable, // eslint-disable-next-line react-hooks/exhaustive-deps table.getState().columnSizing, ]) return ( {visibleColumns.map((column) => ( ))} {children}
) } function DataGridTableViewport({ children, className, viewportRef, style, }: { children: ReactNode className?: string viewportRef?: Ref style?: CSSProperties }) { const { props, table } = useDataGrid() const [viewportElement, setViewportElement] = useState( null ) const [containerWidth, setContainerWidth] = useState(0) const handleViewportRef = useCallback( (node: HTMLDivElement | null) => { setViewportElement(node) assignRef(viewportRef, node) }, [viewportRef] ) const fillWidth = props.tableLayout?.columnsResizable && containerWidth > 0 ? Math.max(0, containerWidth - table.getTotalSize()) : 0 useEffect(() => { if (!viewportElement || !props.tableLayout?.columnsResizable) { setContainerWidth(0) return } const scrollViewport = (viewportElement.closest( '[data-slot="scroll-area-viewport"]' ) as HTMLElement | null) ?? viewportElement.parentElement const measurementTarget = scrollViewport ?? viewportElement const syncContainerWidth = () => { setContainerWidth(measurementTarget.clientWidth) } syncContainerWidth() if (typeof ResizeObserver === "undefined") return const observer = new ResizeObserver(syncContainerWidth) observer.observe(measurementTarget) return () => { observer.disconnect() } }, [props.tableLayout?.columnsResizable, viewportElement]) return (
{children}
) } function DataGridTableHead({ children }: { children: ReactNode }) { const { props } = useDataGrid() return ( {children} ) } function DataGridTableHeadRow({ children, headerGroup, }: { children: ReactNode headerGroup: HeaderGroup }) { const { props } = useDataGrid() return ( th]:border-b", props.tableLayout?.cellBorder && "*:last:border-e-0", props.tableLayout?.stripped && "bg-transparent", props.tableLayout?.headerBackground === false && "bg-transparent", props.tableClassNames?.headerRow )} > {children} ) } function DataGridTableHeadRowCell({ children, header, dndRef, dndStyle, }: { children: ReactNode header: Header dndRef?: React.Ref dndStyle?: CSSProperties }) { const { props } = useDataGrid() const { column } = header const isPinned = column.getIsPinned() const isLastLeftPinned = isPinned === "left" && column.getIsLastColumn("left") const isFirstRightPinned = isPinned === "right" && column.getIsFirstColumn("right") const isLastVisibleColumn = column.getIndex() === header.getContext().table.getVisibleLeafColumns().length - 1 const headerCellSpacing = headerCellSpacingVariants({ size: props.tableLayout?.dense ? "dense" : "default", }) return ( {children} ) } function DataGridTableHeadRowCellResize({ header, }: { header: Header }) { const { props, table } = useDataGrid() const { column } = header const isLastVisibleColumn = column.getIndex() === header.getContext().table.getVisibleLeafColumns().length - 1 const isResizeModeOnEnd = (props.tableLayout?.columnsResizeMode ?? table.options.columnResizeMode) === "onEnd" const handleMouseDown = (event: ReactMouseEvent) => { event.preventDefault() event.stopPropagation() if (isResizeModeOnEnd) { startDataGridColumnResizeOnEnd(event, header, table) return } header.getResizeHandler()(event) } const handleTouchStart = (event: ReactTouchEvent) => { event.preventDefault() event.stopPropagation() if (isResizeModeOnEnd) { startDataGridColumnResizeOnEnd(event, header, table) return } header.getResizeHandler()(event) } return (
column.resetSize(), onMouseDown: handleMouseDown, onTouchStart: handleTouchStart, className: cn( "absolute top-0 h-full cursor-col-resize user-select-none touch-none z-10 flex", isLastVisibleColumn ? "end-0 w-5 justify-end before:hidden" : "-end-2 w-5 justify-center before:absolute before:inset-y-0 before:w-px before:-translate-x-px before:bg-border", column.getIsResizing() && (isResizeModeOnEnd ? "opacity-100" : isLastVisibleColumn ? "before:absolute before:end-0 before:block before:inset-y-0 before:w-0.5 before:bg-primary opacity-100" : "before:block before:bg-primary before:w-0.5 opacity-100") ), }} /> ) } function DataGridTableResizeIndicator({ viewportElement, }: { viewportElement: HTMLDivElement | null }) { const { props, table } = useDataGrid() const columnSizingInfo = table.getState().columnSizingInfo const resizingColumnId = columnSizingInfo.isResizingColumn const resizeMode = props.tableLayout?.columnsResizeMode ?? table.options.columnResizeMode if ( !props.tableLayout?.columnsResizable || resizeMode !== "onEnd" || !resizingColumnId ) { return null } const resizingHeader = table .getFlatHeaders() .find( (header) => header.column.id === resizingColumnId || header.id === resizingColumnId ) if (!resizingHeader) return null const deltaOffset = columnSizingInfo.deltaOffset ?? 0 const headerHeight = viewportElement ?.querySelector('[data-slot="data-grid-table"] thead') ?.getBoundingClientRect().height ?? 0 const indicatorLeft = typeof columnSizingInfo.startOffset === "number" && viewportElement ? columnSizingInfo.startOffset - viewportElement.getBoundingClientRect().left : resizingHeader.getStart() + resizingHeader.getSize() return (