import React, { useMemo, forwardRef, useRef, useCallback } from "react"; import { FixedSizeList, VariableSizeList, ListChildComponentProps, ListItemKeySelector, ListOnScrollProps, } from "react-window"; import { useConfig } from "../util"; import { VirtualizedOptions } from "./addons/scrollable"; import { mergeRefs } from "../_util/merge-refs"; export interface TableVirtualizedBodyProps extends VirtualizedOptions { itemKey?: ListItemKeySelector; tableBoxStyle: React.CSSProperties; onScrollCapture?: React.DOMAttributes["onScrollCapture"]; records?: Record[]; renderRecord?: (record: any, recordIndex: number) => JSX.Element[]; getRowKey?: (record: any, recordIndex: number) => string; topTip?: React.ReactNode; bottomTip?: React.ReactNode; /** * 是否使用了行展开填充 records * * 此时实际行数大于 records 数量 */ expandable?: boolean; } const defaultItemHeight = 45; export const TableVirtualizedBody = forwardRef(function TableVirtualizedBody( { tableBoxStyle, onScrollCapture, onScrollBottom, height, itemHeight = defaultItemHeight, records, renderRecord, getRowKey, topTip, bottomTip, expandable, }: TableVirtualizedBodyProps, ref: React.Ref ) { const { classPrefix } = useConfig(); const bodyRef = useRef(null); const OuterElement = useMemo( () => forwardRef((props, ref: React.Ref) => { return (
); }), // eslint-disable-next-line react-hooks/exhaustive-deps [] ); const InnerElement = useMemo( () => forwardRef( ( { style = {}, children }: React.HTMLAttributes, ref: React.Ref ) => ( {children}
) ), // eslint-disable-next-line react-hooks/exhaustive-deps [tableBoxStyle] ); const getItemKey = useCallback( (index: number, data) => { if (data[index].type === "top-tip") { return "__TABLE_TOP_TIP__"; } if (data[index].type === "bottom-tip") { return "__TABLE_BOTTOM_TIP__"; } if (expandable) { return data[index].row.key; } return getRowKey(data[index].row, index); }, [expandable, getRowKey] ); const items = useMemo(() => { // 此时要预先渲染行计算总数 if (expandable) { const recordRows = []; records.map(renderRecord).forEach(rows => { rows.forEach(r => recordRows.push(r)); }); return [ { type: "top-tip", row: topTip }, ...recordRows.map(r => ({ type: "record", row: r })), { type: "bottom-tip", row: bottomTip }, ].filter(i => !!i.row); } return [ { type: "top-tip", row: topTip }, ...records.map(r => ({ type: "record", row: r })), { type: "bottom-tip", row: bottomTip }, ].filter(i => !!i.row); }, [bottomTip, expandable, records, renderRecord, topTip]); const Row = useCallback( ({ data, index, style }: ListChildComponentProps) => { if (data[index].type === "top-tip" || data[index].type === "bottom-tip") { return data[index].row; } // 此时行已预先渲染计算 if (expandable) { return React.cloneElement(data[index].row, { style: { ...style, display: "flex" }, }); } return renderRecord(data[index].row, index).map(row => React.cloneElement(row, { style: { ...style, display: "flex" }, }) ); }, [expandable, renderRecord] ); const getItemHeight = useCallback( (index: number) => { if (typeof itemHeight !== "function") { return itemHeight; } const offset = items[0].type === "top-tip" ? 1 : 0; if ( items[index].type === "top-tip" || items[index].type === "bottom-tip" ) { return defaultItemHeight; } return itemHeight(items[index].row, index - offset); }, [itemHeight, items] ); function handleScroll(props: ListOnScrollProps) { const itemsSize = items.reduce( (p, _, index) => p + (typeof itemHeight === "function" ? getItemHeight(index) : itemHeight), 0 ); if (onScrollBottom) { if (bodyRef.current && itemsSize > height) { const { scrollHeight, scrollTop, clientHeight } = bodyRef.current; if (scrollHeight <= Math.ceil(clientHeight + scrollTop + 1)) { onScrollBottom(props); } } } } if (typeof itemHeight === "function") { return ( getItemHeight(index)} itemCount={items.length} itemData={items} onScroll={handleScroll} itemKey={getItemKey} > {Row} ); } return ( {Row} ); }); TableVirtualizedBody.displayName = "TableVirtualizedBody";