@rtdui/datatable
Version:
React DataTable component based on Rtdui components
712 lines (709 loc) • 27.1 kB
JavaScript
'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