UNPKG

@rtdui/datatable

Version:

React DataTable component based on Rtdui components

712 lines (709 loc) 27.1 kB
'use client'; import { jsxs, Fragment, jsx } from 'react/jsx-runtime'; import { forwardRef, useState, useEffect, useRef, useImperativeHandle, useMemo, useCallback } from 'react'; import { flattenBy, getCoreRowModel, getFilteredRowModel, getFacetedRowModel, getFacetedUniqueValues, getFacetedMinMaxValues, getGroupedRowModel, getSortedRowModel, getExpandedRowModel, getPaginationRowModel, useReactTable } from '@tanstack/react-table'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { TouchBackend } from 'react-dnd-touch-backend'; import { DndProvider } from 'react-dnd'; import { IconDirection, IconChevronDown, IconChevronRight } from '@tabler/icons-react'; import { klona } from 'klona/full'; import { useScrollTrigger } from '@rtdui/hooks'; import { isMobile, Checkbox, getType, filterProps } from '@rtdui/core'; import { useVirtualizer } from '@tanstack/react-virtual'; import clsx from 'clsx'; import { getLeafColumns } from '../utils/getAllFlatColumns.mjs'; import { getColumnDefId } from '../utils/getColumnDefId.mjs'; import { getVisibleColumns } from './utils/getVisibleColumns.mjs'; import { HeaderCell } from './HeaderCell.mjs'; import { FooterCell } from './FooterCell.mjs'; import { BodyCell } from './BodyCell.mjs'; import { TablePagination } from './TablePagination.mjs'; import { ColumnsVisibility } from './ColumnsVisibility.mjs'; import { GroupDropArea } from './GroupDropArea.mjs'; import { ExportTable } from './ExportTable.mjs'; import { IndeterminateCheckbox } from './IndeterminateCheckbox.mjs'; import { RowActive } from './features/RowActive.mjs'; let isMobileDevice = false; if (typeof document !== "undefined") { isMobileDevice = isMobile(); } const DataTable = forwardRef((props, ref) => { const { columns: columnsProp, data: dataProp, getRowId = (row) => row.id, initialState, state, onColumnFiltersChange, onColumnOrderChange, onColumnPinningChange, onColumnSizingChange, onColumnSizingInfoChange, onColumnVisibilityChange, onExpandedChange, onGlobalFilterChange, onGroupingChange, onPaginationChange, onRowPinningChange, onSortingChange, onRowSelectionChange, onStateChange, onRowActiveChange, // 自定义功能 enableColumnReorder = true, groupedColumnMode = "reorder", enableColumnResizing = true, columnResizeMode = "onChange", enableSorting = true, enableMultiSort = true, enableSortingRemoval = true, sortDescFirst = false, getSubRows, enableGrouping: enableGroupingProp = false, enableFilters = false, filterFromLeafRows = false, enableHiding = true, enablePagination = false, enableRowSelection = true, enableMultiRowSelection = false, enableSubRowSelection = false, enableClickRowSelection = true, selectAllForAllPages = true, enableStickyHeader = true, enableAutoRowNumber = false, enableExport = false, debouncedWait = 500, enableVirtualized = false, className, slots, size = "sm", showHeader = true, showToolbar = true, showBorder = true, borderWidth: borderWidthProp = 1, fixedLayout = true, onRowClick, onRowDoubleClick, validate, ...other } = props; const borderWidth = showBorder ? borderWidthProp : 0; const enableTree = !!getSubRows; const enableGrouping = enableTree ? false : enableGroupingProp; const [data, setData] = useState(dataProp); useEffect(() => { setData(dataProp); }, [dataProp]); if (enableGrouping && enableTree) { throw new Error("\u6570\u636E\u5206\u7EC4\u548C\u6811\u5F62\u8868\u683C\u4E0D\u80FD\u540C\u65F6\u542F\u7528"); } if (enableExport && enableVirtualized) { throw new Error("\u884C\u865A\u62DF\u5316\u548C\u5BFC\u51FA\u4E0D\u80FD\u540C\u65F6\u542F\u7528"); } const changesRef = useRef({ changes: { added: [], changed: {}, deleted: [] }, errors: {} }); const setErrorRow = (params) => { const { rowId, field, errorMsg } = params; if (!errorMsg) { delete changesRef.current.errors[rowId]?.[field]; if (Object.keys(changesRef.current.errors[rowId] ?? {}).length === 0) { delete changesRef.current.errors[rowId]; } return; } changesRef.current.errors[rowId] = { ...changesRef.current.errors[rowId], [field]: errorMsg }; }; const addRow = (newRow) => { Object.keys(newRow).forEach((d) => { const validateRule = validate?.[d]; const validateError = validateRule?.(newRow[d], newRow, data); if (validateError) { setErrorRow({ rowId: getRowId(newRow), field: d, errorMsg: validateError }); } }); changesRef.current.changes.added.push(newRow); setData((prev) => [...prev, newRow]); }; const changeRow = (params) => { const { row, column, value } = params; const rowId = getRowId(row); const field = column.columnDef.accessorKey; const fieldDataType = getType(row.original[field]); if (row.original[field] !== (fieldDataType === "Number" ? Number(value) : value)) { const validateRule = validate?.[field]; const validateError = validateRule?.(value, row.original, data); if (!validateError) { setErrorRow({ rowId: row.id, field, errorMsg: "" }); const addedRow = changesRef.current.changes.added.find( (d) => getRowId(d) === rowId ); if (addedRow) { addedRow[field] = fieldDataType === "Number" ? Number(value) : value; } else { changesRef.current.changes.changed[rowId] = { ...changesRef.current.changes.changed[rowId], [field]: fieldDataType === "Number" ? Number(value) : value }; } } else { setErrorRow({ rowId: row.id, field, errorMsg: validateError }); } setData( (prev) => prev.map((row2) => { if (getRowId(row2) === rowId) { return { ...row2, [field]: fieldDataType === "Number" ? Number(value) : value }; } return row2; }) ); } }; const deleteRow = () => { const { rowSelection } = table.getState(); const selectedRowId = Object.keys(rowSelection).filter( (d) => rowSelection[d] === true ); selectedRowId.forEach((d) => { const addedRowIndex = changesRef.current.changes.added.findIndex( (d2) => String(getRowId(d2)) === d ); if (addedRowIndex >= 0) { changesRef.current.changes.added.splice(addedRowIndex, 1); } else { changesRef.current.changes.deleted.push(d); } }); table.resetRowSelection(true); setData( (prev) => prev.filter((d) => !selectedRowId.includes(String(getRowId(d)))) ); }; const getChanges = () => { if (Object.keys(changesRef.current.errors).length) { throw new Error("\u6570\u636E\u8868\u683C\u4E2D\u5B58\u5728\u9519\u8BEF\u65E0\u6CD5\u4FDD\u5B58"); } return changesRef.current.changes; }; const getSelectedRowIds = () => { const { rowSelection } = table.getState(); return Object.keys(rowSelection).filter((d) => rowSelection[d] === true); }; const getSelectedRows = () => { return table.getSelectedRowModel().flatRows.map((d) => d.original); }; const getState = () => { return table.getState(); }; useImperativeHandle( ref, () => ({ addRow, changeRow, deleteRow, getChanges, getSelectedRowIds, getSelectedRows, getState }), [] ); const columns = useMemo(() => { const cloneColumns = klona(columnsProp); if (enableTree) { const expandColumnDef = flattenBy( cloneColumns, (item) => item.columns ).find((d) => d.meta?.expandable); if (!expandColumnDef) { throw new Error( "\u542F\u7528\u6811\u5F62\u8868\u683C\u65F6\u5FC5\u987B\u63D0\u4F9B\u81F3\u5C11\u4E00\u4E2A\u5B9A\u4E49\u4E86meta.expandable\u4E3Atrue\u7684column" ); } expandColumnDef.enableHiding = false; const oldHeaderDef = expandColumnDef.header; expandColumnDef.size ?? (expandColumnDef.size = 140); expandColumnDef.minSize ?? (expandColumnDef.minSize = 140); expandColumnDef.header = (cx) => { const { column, table: table2 } = cx; return /* @__PURE__ */ jsxs(Fragment, { children: [ enableRowSelection && !enableClickRowSelection && /* @__PURE__ */ jsx( IndeterminateCheckbox, { className: "absolute left-0.5 checkbox-sm", checked: selectAllForAllPages ? table2.getIsAllRowsSelected() : table2.getIsAllPageRowsSelected(), indeterminate: selectAllForAllPages ? table2.getIsSomeRowsSelected() : table2.getIsSomePageRowsSelected(), onChange: selectAllForAllPages ? table2.getToggleAllRowsSelectedHandler() : table2.getToggleAllPageRowsSelectedHandler() } ), /* @__PURE__ */ jsx( "button", { type: "button", className: "btn btn-ghost btn-circle btn-xs absolute left-5", title: "\u5C55\u5F00/\u6536\u7F29\u6240\u6709\u884C", onClick: (ev) => { ev.stopPropagation(); table2.toggleAllRowsExpanded(); }, children: /* @__PURE__ */ jsx(IconDirection, {}) } ), typeof oldHeaderDef === "function" ? oldHeaderDef(cx) : oldHeaderDef ] }); }; const oldCellDef = expandColumnDef.cell; expandColumnDef.cell = (cx) => { const { row, column, table: table2 } = cx; return /* @__PURE__ */ jsxs( "div", { style: { paddingLeft: `${row.depth * 2}em` }, className: "flex items-center gap-1", children: [ enableRowSelection && !enableClickRowSelection && /* @__PURE__ */ jsx( Checkbox, { size: "xs", checked: row.getIsSelected(), disabled: !row.getCanSelect(), onChange: row.getToggleSelectedHandler(), onClick: (ev) => ev.stopPropagation(), slots: { input: "rounded-sm" } } ), cx.row.getCanExpand() ? /* @__PURE__ */ jsx( "button", { type: "button", onClick: (e) => { e.stopPropagation(); row.getToggleExpandedHandler()(); }, className: "btn btn-ghost btn-circle btn-xs", children: row.getIsExpanded() ? /* @__PURE__ */ jsx(IconChevronDown, { size: 16 }) : /* @__PURE__ */ jsx(IconChevronRight, { size: 16 }) } ) : "\xA0", typeof oldCellDef === "function" ? oldCellDef(cx) : oldCellDef === void 0 ? cx.getValue() : oldCellDef ] } ); }; } else if (enableRowSelection && !enableClickRowSelection) { cloneColumns.unshift({ id: "\u9009\u62E9", size: 50, minSize: 50, enableHiding: false, header: ({ table: table2 }) => /* @__PURE__ */ jsx( IndeterminateCheckbox, { className: "checkbox-sm", disabled: !enableMultiRowSelection, checked: selectAllForAllPages ? table2.getIsAllRowsSelected() : table2.getIsAllPageRowsSelected(), indeterminate: selectAllForAllPages ? table2.getIsSomeRowsSelected() : table2.getIsSomePageRowsSelected(), onChange: selectAllForAllPages ? table2.getToggleAllRowsSelectedHandler() : table2.getToggleAllPageRowsSelectedHandler() } ), cell: ({ row }) => /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx( IndeterminateCheckbox, { className: "rounded-sm", checked: row.getIsSelected(), disabled: !row.getCanSelect(), indeterminate: row.getIsSomeSelected(), onChange: row.getToggleSelectedHandler(), onClick: (ev) => ev.stopPropagation() } ) }) }); } if (enableAutoRowNumber) { cloneColumns.unshift({ id: "\u884C\u53F7", size: 60, minSize: 60, header: "\u884C\u53F7", cell: (cx) => /* @__PURE__ */ jsx("span", { style: { paddingLeft: `${cx.row.depth * 1}em` }, children: cx.row.index + 1 }) }); } return cloneColumns; }, [ columnsProp, enableTree, enableRowSelection, selectAllForAllPages, enableClickRowSelection, enableAutoRowNumber, enableMultiRowSelection ]); const tableOptions = { _features: [RowActive], // 行激活的自定义功能 columns, data, getRowId, initialState, //初始状态 state, //实时状态 onStateChange, autoResetAll: false, // 覆盖所有功能提供`autoReset*`的选项, `autoReset*`表示当表格状态改变时自动重置功能的状态. 如:autoResetExpanded,autoResetPageIndex // 默认列定义, 会和columns中每个列定义进行合并,为列定义提供缺省属性 defaultColumn: { size: 140, minSize: 80, maxSize: Number.MAX_SAFE_INTEGER, cell: (cx) => { const value = cx.getValue(); const valueType = getType(value); const align = cx.column.columnDef.meta?.align; return /* @__PURE__ */ jsx( "div", { className: clsx("overflow-hidden whitespace-nowrap text-ellipsis", { "text-right": valueType === "Number" || align === "right", "text-center": valueType === "Boolean" || align === "center" }), children: cx.renderValue()?.toString() ?? "" } ); }, aggregatedCell: "" // 覆盖默认行为, 使分组行中不显示列的聚合值, 除非手动指定. }, // 列隐藏/显示功能 enableHiding, // 设置列宽调整选项 enableColumnResizing, columnResizeMode, // 行选择 enableRowSelection, enableMultiRowSelection, enableSubRowSelection, // 核心行模型, 这是必须的 getCoreRowModel: getCoreRowModel(), getSubRows: enableTree ? getSubRows : void 0, // 注意, 启用数据分组时不能设置该属性, 否则会有冲突. // 数据过滤功能 enableFilters, // filterFromLeafRows: enableTree || enableGrouping ? true : false, filterFromLeafRows, getFilteredRowModel: enableFilters ? getFilteredRowModel() : void 0, getFacetedRowModel: enableFilters ? getFacetedRowModel() : void 0, getFacetedUniqueValues: enableFilters ? getFacetedUniqueValues() : void 0, getFacetedMinMaxValues: enableFilters ? getFacetedMinMaxValues() : void 0, // 数据分组功能 enableGrouping, groupedColumnMode, getGroupedRowModel: enableGrouping ? getGroupedRowModel() : void 0, // 数据排序功能 enableSorting, enableSortingRemoval, enableMultiSort, sortDescFirst, getSortedRowModel: enableSorting ? getSortedRowModel() : void 0, // 展开功能, 树行表格和数据分组功能依赖该功能. enableExpanding: enableTree || enableGrouping, getExpandedRowModel: enableTree || enableGrouping ? getExpandedRowModel() : void 0, // 数据分页功能 getPaginationRowModel: enablePagination ? getPaginationRowModel() : void 0, // 受控状态处理 onColumnFiltersChange, onColumnOrderChange, onColumnPinningChange, onColumnSizingChange, onColumnSizingInfoChange, onColumnVisibilityChange, onExpandedChange, onGlobalFilterChange, onGroupingChange, onPaginationChange, onRowPinningChange, onSortingChange, onRowSelectionChange, onRowActiveChange, // 自定义功能 // 用于列定义中访问表的自定义元数据 meta: { addRow, changeRow, deleteRow, getChanges, getSelectedRowIds } }; const table = useReactTable(filterProps(tableOptions)); const getLeftPinning = () => { const result = []; result.push(...table.getState().grouping); if (enableAutoRowNumber) { result.push("\u884C\u53F7"); } if (enableTree) { result.push( flattenBy(columnsProp, (item) => item.columns).find((d) => d.id).id ); } else if (!enableTree && enableRowSelection && !enableClickRowSelection) { result.push("\u9009\u62E9"); } return result; }; table.setOptions((prev) => { const prevState = prev.state; if (!prevState.columnOrder?.length) { prevState.columnOrder = getLeafColumns(columnsProp).map( (d) => getColumnDefId(d) ); } prevState.columnPinning.left = getLeftPinning(); return prev; }); const tableContainerRef = useRef(null); const tableRef = useRef(null); const estimateRowHeight = useCallback( () => size === "sm" ? 36 : 56, [size] ); const { rows } = table.getRowModel(); const rowVirtualizer = useVirtualizer({ getScrollElement: () => enableVirtualized ? tableContainerRef.current : null, count: rows.length, estimateSize: estimateRowHeight, overscan: 10 }); const { getVirtualItems, getTotalSize } = rowVirtualizer; const virtualRows = getVirtualItems(); const totalSize = getTotalSize(); const paddingTop = virtualRows.length > 0 ? virtualRows[0].start || 0 : 0; const paddingBottom = virtualRows.length > 0 ? totalSize - (virtualRows[virtualRows.length - 1].end || 0) : 0; const scrollingTrigger = useScrollTrigger({ target: tableContainerRef.current, direction: "horizontal", disabled: table.getState().columnPinning.left?.length === 0 }); const handleRowClick = (e, row) => { onRowClick?.(e, row); if ((enableRowSelection === true || typeof enableRowSelection === "function" && enableRowSelection(row)) && enableClickRowSelection && !row.getIsGrouped()) { row.getToggleSelectedHandler()(e); } row.getToggleActivedHandler()(e); }; const handleRowDoubleClick = (e, row) => { onRowDoubleClick?.(e, row); row.getToggleActivedHandler()(e); }; return /* @__PURE__ */ jsx( DndProvider, { backend: isMobileDevice ? TouchBackend : HTML5Backend, context: typeof document !== "undefined" ? window : void 0, options: { delayTouchStart: 200, ignoreContextMenu: true }, children: /* @__PURE__ */ jsxs( "div", { className: clsx("data-table-root h-full flex flex-col", className), ...other, children: [ showToolbar && /* @__PURE__ */ jsxs( "div", { className: clsx( "data-table-toolbar navbar flex items-center bg-base-300 relative z-10 py-0 min-h-0", slots?.toolbar ), children: [ /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx( GroupDropArea, { table, enableGrouping: enableGrouping ?? false, className: clsx(slots?.groupDropArea) } ) }), enableExport && /* @__PURE__ */ jsx(ExportTable, { table, tableRef }), enableHiding && /* @__PURE__ */ jsx(ColumnsVisibility, { table }) ] } ), /* @__PURE__ */ jsx( "div", { ref: tableContainerRef, className: clsx( "data-table-container flex-1 overflow-auto relative z-0", slots?.container ), style: { "--borderWidth": `${borderWidth}px` }, children: /* @__PURE__ */ jsxs( "table", { ref: tableRef, className: clsx( "data-table table rounded-none", // 移除daisyUI中table的默认圆角 { "table-fixed": fixedLayout, "table-pin-rows": enableStickyHeader, "no-border": !showBorder, "table-xs": size === "xs", "table-sm": size === "sm", "table-md": size === "md", "table-lg": size === "lg", "table-xl": size === "xl" }, slots?.table ), style: { width: fixedLayout ? table.getTotalSize() : void 0 }, children: [ /* @__PURE__ */ jsx("colgroup", { children: getVisibleColumns(table).map( (column) => column.getIsVisible() ? /* @__PURE__ */ jsx( "col", { style: { width: column.getSize() } }, column.id ) : null ) }), /* @__PURE__ */ jsx( "thead", { className: enableStickyHeader ? "relative z-10" : void 0, children: table.getHeaderGroups().map((headerGroup) => /* @__PURE__ */ jsx("tr", { children: headerGroup.headers.map((header) => /* @__PURE__ */ jsx( HeaderCell, { header, table, enableColumnReorder, enableColumnResizing, showHeader, debouncedWait, scrollingTrigger }, header.id )) }, headerGroup.id)) } ), /* @__PURE__ */ jsxs( "tbody", { className: "relative z-0", children: [ table.getRowModel().rows.length === 0 && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx( "td", { colSpan: table.getVisibleLeafColumns().length, className: "text-center", children: "\u65E0\u6570\u636E" } ) }), enableVirtualized ? /* @__PURE__ */ jsxs(Fragment, { children: [ paddingTop > 0 && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", { style: { height: `${paddingTop}px` } }) }), virtualRows.map((virtualRow) => { const row = table.getRowModel().rows[virtualRow.index]; return /* @__PURE__ */ jsx( "tr", { onClick: (e) => handleRowClick(e, row), onDoubleClick: (e) => handleRowDoubleClick(e, row), className: clsx({ selected: row.getIsSelected(), actived: row.getIsActived() }), children: row.getVisibleCells().map((cell) => /* @__PURE__ */ jsx( BodyCell, { cell, enableGrouping, scrollingTrigger, changesRef }, cell.id )) }, row.id ); }), paddingBottom > 0 && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", { style: { height: `${paddingBottom}px` } }) }) ] }) : table.getRowModel().rows.map((row) => /* @__PURE__ */ jsx( "tr", { onClick: (e) => handleRowClick(e, row), onDoubleClick: (e) => handleRowDoubleClick(e, row), className: clsx({ selected: row.getIsSelected(), actived: row.getIsActived() }), children: row.getVisibleCells().map((cell) => /* @__PURE__ */ jsx( BodyCell, { cell, enableGrouping, scrollingTrigger, changesRef }, cell.id )) }, row.id )) ] } ), table.getFooterGroups()[0].headers.some((d) => d.column.columnDef.footer) && /* @__PURE__ */ jsx("tfoot", { className: "relative z-10", children: table.getFooterGroups().slice(0, 1).map((footerGroup) => /* @__PURE__ */ jsx("tr", { children: footerGroup.headers.map((header) => /* @__PURE__ */ jsx( FooterCell, { header, table, scrollingTrigger }, header.id )) }, footerGroup.id)) }) ] } ) } ), enablePagination && /* @__PURE__ */ jsx(TablePagination, { table }) ] } ) } ); }); DataTable.displayName = "@rtdui/DataTable"; export { DataTable }; //# sourceMappingURL=DataTable.mjs.map