From 17b96c9027a0e88386087ce4538a2e87f23fc455 Mon Sep 17 00:00:00 2001
From: 07akioni <07akioni2@gmail.com>
Date: Tue, 24 Sep 2024 02:05:42 +0800
Subject: [PATCH] feat(data-table): supports horizontal virtual scrolling
---
package.json | 2 +-
src/data-table/demos/enUS/index.demo-entry.md | 1 +
src/data-table/demos/enUS/virtual-x.demo.vue | 82 +++
src/data-table/demos/zhCN/index.demo-entry.md | 1 +
src/data-table/demos/zhCN/virtual-x.demo.vue | 82 +++
src/data-table/src/DataTable.tsx | 5 +
src/data-table/src/MainTable.tsx | 8 +-
src/data-table/src/TableParts/Body.tsx | 555 +++++++++++-------
src/data-table/src/TableParts/Header.tsx | 404 ++++++++-----
src/data-table/src/interface.ts | 17 +
src/data-table/src/styles/index.cssr.ts | 1 +
src/data-table/src/use-group-header.ts | 17 +-
12 files changed, 821 insertions(+), 354 deletions(-)
create mode 100644 src/data-table/demos/enUS/virtual-x.demo.vue
create mode 100644 src/data-table/demos/zhCN/virtual-x.demo.vue
diff --git a/package.json b/package.json
index 4011e39217d..d98e8755db8 100644
--- a/package.json
+++ b/package.json
@@ -84,7 +84,7 @@
"treemate": "^0.3.11",
"vdirs": "^0.1.8",
"vooks": "^0.2.12",
- "vueuc": "^0.4.58"
+ "vueuc": "^0.4.63"
},
"devDependencies": {
"@antfu/eslint-config": "^2.22.0",
diff --git a/src/data-table/demos/enUS/index.demo-entry.md b/src/data-table/demos/enUS/index.demo-entry.md
index d2355beaf3a..4f108e1f62d 100644
--- a/src/data-table/demos/enUS/index.demo-entry.md
+++ b/src/data-table/demos/enUS/index.demo-entry.md
@@ -49,6 +49,7 @@ render-header
custom-style.vue
ajax-usage
virtual.vue
+virtual-x.vue
custom-filter-menu.vue
tree.vue
flex-height.vue
diff --git a/src/data-table/demos/enUS/virtual-x.demo.vue b/src/data-table/demos/enUS/virtual-x.demo.vue
new file mode 100644
index 00000000000..37bc34e4e5b
--- /dev/null
+++ b/src/data-table/demos/enUS/virtual-x.demo.vue
@@ -0,0 +1,82 @@
+
+# Large data (rows & cols)
+
+If you have a large amount of row and column data, such as thousands of rows and hundreds of columns, `naive-ui` provides horizontal + vertical virtual scrolling functionality.
+
+Due to the inherent complexity of horizontal virtual scrolling, the corresponding configuration can be quite complex, with most of the following content being necessary:
+
+1. Configure `virtual-scroll` to enable vertical virtual scrolling.
+2. Configure `virtual-scroll-x` to enable horizontal virtual scrolling:
+ - Each column needs to have a `width` property configured.
+ - Configure the `scroll-x` property, setting it to the total width of all columns.
+ - Configure the `min-row-height` property, setting it to the minimum height of each row, where all rows must be larger than this value.
+ - Configure the `height-for-row` property, which is used to set the height of each row (since only a portion of the cells in each row are always visible, this cannot be automatically calculated). If not configured, the height of each row will be set to `min-row-height`.
+3. If needed, configure `virtual-scroll-header`. By default, the header will still be fully rendered to maintain compatibility. You can enable virtual rendering for the header with this configuration:
+ - Configure the `header-height` property, setting it to the height of the header.
+
+The example below corresponds to a table with 1000 rows * 1000 columns.
+
+`naive-ui`'s table can easily support table data in the millions. You won't find this kind of functionality in many free component libraries.
+
+
+
+
+
+
+
diff --git a/src/data-table/demos/zhCN/index.demo-entry.md b/src/data-table/demos/zhCN/index.demo-entry.md
index df68978d47b..86108b145a2 100644
--- a/src/data-table/demos/zhCN/index.demo-entry.md
+++ b/src/data-table/demos/zhCN/index.demo-entry.md
@@ -51,6 +51,7 @@ render-header
custom-style.vue
ajax-usage
virtual.vue
+virtual-x.vue
custom-filter-menu.vue
tree.vue
flex-height.vue
diff --git a/src/data-table/demos/zhCN/virtual-x.demo.vue b/src/data-table/demos/zhCN/virtual-x.demo.vue
new file mode 100644
index 00000000000..392f6c5e544
--- /dev/null
+++ b/src/data-table/demos/zhCN/virtual-x.demo.vue
@@ -0,0 +1,82 @@
+
+# 大量数据(行和列)
+
+如果你有大量行数据和列数据,例如几千行 + 几百列,`naive-ui` 提供了横向 + 纵向虚拟滚动的功能。
+
+因为横向虚拟滚动的天然的复杂性,对应的配置也会较为复杂,以下多数内容都是必须的:
+
+1. 配置 `virtual-scroll` 打开纵向虚拟滚动
+2. 配置 `virtual-scroll-x` 打开横向虚拟滚动
+ - 每一个列都需要配置 `width` 属性
+ - 配置 `scroll-x` 属性,设为所有列的总宽度
+ - 配置 `min-row-height` 属性,设为每一列的最小高度,所有的列必须比这个值更大
+ - 配置 `height-for-row` 属性,用于配置每一行的高度(因为每一行永远只有一部分格子是可见的,因此无法自动求出),如果不配置,每一行的高度会被设为 `min-row-height`
+3. 如有需要,配置 `virtual-scroll-header`,默认情况下,表头依然会全量渲染以保持兼容性,你可以通过此配置来打开表头的虚拟渲染
+ - 配置 `header-height` 属性,设为表头的高度
+
+下面的例子对应了一个 1000 行 * 1000 列的表格。
+
+`naive-ui` 的表格可以轻松的支持千万级的表格数据,你在不收钱的组件库不容易找得到这样的功能。
+
+
+
+
+
+
+
diff --git a/src/data-table/src/DataTable.tsx b/src/data-table/src/DataTable.tsx
index fd9437e0081..4895b6fd080 100644
--- a/src/data-table/src/DataTable.tsx
+++ b/src/data-table/src/DataTable.tsx
@@ -244,6 +244,11 @@ export default defineComponent({
renderExpandRef,
summaryRef: toRef(props, 'summary'),
virtualScrollRef: toRef(props, 'virtualScroll'),
+ virtualScrollXRef: toRef(props, 'virtualScrollX'),
+ heightForRowRef: toRef(props, 'heightForRow'),
+ minRowHeightRef: toRef(props, 'minRowHeight'),
+ virtualScrollHeaderRef: toRef(props, 'virtualScrollHeader'),
+ headerHeightRef: toRef(props, 'headerHeight'),
rowPropsRef: toRef(props, 'rowProps'),
stripedRef: toRef(props, 'striped'),
checkOptionsRef: computed(() => {
diff --git a/src/data-table/src/MainTable.tsx b/src/data-table/src/MainTable.tsx
index 1031c893a70..cbccf6f461e 100644
--- a/src/data-table/src/MainTable.tsx
+++ b/src/data-table/src/MainTable.tsx
@@ -20,6 +20,7 @@ export default defineComponent({
maxHeightRef,
minHeightRef,
flexHeightRef,
+ virtualScrollHeaderRef,
syncScrollState
} = inject(dataTableInjectionKey)!
@@ -47,7 +48,12 @@ export default defineComponent({
function getHeaderElement(): HTMLElement | null {
const { value } = headerInstRef
if (value) {
- return value.$el
+ if (virtualScrollHeaderRef.value) {
+ return value.virtualListRef?.listElRef || null
+ }
+ else {
+ return value.$el
+ }
}
return null
}
diff --git a/src/data-table/src/TableParts/Body.tsx b/src/data-table/src/TableParts/Body.tsx
index 295c6e437f4..6b9ed62579c 100644
--- a/src/data-table/src/TableParts/Body.tsx
+++ b/src/data-table/src/TableParts/Body.tsx
@@ -1,8 +1,6 @@
+import type { CSSProperties, PropType, VNode, VNodeChild } from 'vue'
import {
- type CSSProperties,
Fragment,
- type PropType,
- type VNode,
computed,
defineComponent,
h,
@@ -12,7 +10,8 @@ import {
watchEffect
} from 'vue'
import { pxfy, repeat } from 'seemly'
-import { VResizeObserver, VirtualList, type VirtualListInst } from 'vueuc'
+import { VResizeObserver, VirtualList } from 'vueuc'
+import type { VirtualListInst } from 'vueuc'
import type { CNode } from 'css-render'
import { useMemo } from 'vooks'
import { cssrAnchorMetaName } from '../../../_mixins/common'
@@ -170,6 +169,9 @@ export default defineComponent({
summaryRef,
mergedSortStateRef,
virtualScrollRef,
+ virtualScrollXRef,
+ heightForRowRef,
+ minRowHeightRef,
componentId,
mergedTableLayoutRef,
childTriggerColIndexRef,
@@ -499,6 +501,9 @@ export default defineComponent({
hoverKey: hoverKeyRef,
mergedSortState: mergedSortStateRef,
virtualScroll: virtualScrollRef,
+ virtualScrollX: virtualScrollXRef,
+ heightForRow: heightForRowRef,
+ minRowHeight: minRowHeightRef,
mergedTableLayout: mergedTableLayoutRef,
childTriggerColIndex: childTriggerColIndexRef,
indent: indentRef,
@@ -597,7 +602,10 @@ export default defineComponent({
summary,
handleCheckboxUpdateChecked,
handleRadioUpdateChecked,
- handleUpdateExpanded
+ handleUpdateExpanded,
+ heightForRow,
+ minRowHeight,
+ virtualScrollX
} = this
const { length: colCount } = cols
@@ -683,11 +691,40 @@ export default defineComponent({
const bodyWidthPx
= bodyWidth === null ? undefined : `${bodyWidth}px`
- const renderRow = (
- rowInfo: RowRenderInfo,
- displayedRowIndex: number,
+ const CellComponent = (this.virtualScrollX ? 'div' : 'td') as 'td'
+ let leftFixedColsCount = 0
+ let rightFixedColsCount = 0
+ if (virtualScrollX) {
+ cols.forEach((col) => {
+ if (col.column.fixed === 'left') {
+ leftFixedColsCount++
+ }
+ else if (col.column.fixed === 'right') {
+ rightFixedColsCount++
+ }
+ })
+ }
+
+ const renderRow = ({
+ // Normal
+ rowInfo,
+ displayedRowIndex,
+ isVirtual,
+ // Virtual X
+ isVirtualX,
+ startColIndex,
+ endColIndex,
+ getLeft
+ }: {
+ rowInfo: RowRenderInfo
+ displayedRowIndex: number
isVirtual: boolean
- ): VNode => {
+ // for horizontal virtual list
+ isVirtualX: boolean
+ startColIndex: number
+ endColIndex: number
+ getLeft: (index: number) => number
+ }): VNode => {
const { index: actualRowIndex } = rowInfo
if ('isExpandedRow' in rowInfo) {
const {
@@ -735,6 +772,244 @@ export default defineComponent({
= typeof rowClassName === 'string'
? rowClassName
: createRowClassName(rowData, actualRowIndex, rowClassName)
+ const iteratedCols = isVirtualX
+ ? cols.filter((col, index) => {
+ if (startColIndex <= index && index <= endColIndex)
+ return true
+ if (col.column.fixed) {
+ return true
+ }
+ return false
+ })
+ : cols
+ const virtualXRowHeight = isVirtualX
+ ? pxfy(
+ heightForRow?.(rowData, actualRowIndex)
+ || minRowHeight
+ || 28
+ )
+ : undefined
+ const cells = iteratedCols.map((col) => {
+ const colIndex = col.index
+ if (displayedRowIndex in cordToPass) {
+ const cordOfRowToPass = cordToPass[displayedRowIndex]
+ const indexInCordOfRowToPass
+ = cordOfRowToPass.indexOf(colIndex)
+ if (~indexInCordOfRowToPass) {
+ cordOfRowToPass.splice(indexInCordOfRowToPass, 1)
+ return null
+ }
+ }
+ // TODO: Simplify row calculation
+ const { column } = col
+ const colKey = getColKey(col)
+ const { rowSpan, colSpan } = column
+ const mergedColSpan = isSummary
+ ? rowInfo.tmNode.rawNode[colKey]?.colSpan || 1 // optional for #1276
+ : colSpan
+ ? colSpan(rowData, actualRowIndex)
+ : 1
+ const mergedRowSpan = isSummary
+ ? rowInfo.tmNode.rawNode[colKey]?.rowSpan || 1 // optional for #1276
+ : rowSpan
+ ? rowSpan(rowData, actualRowIndex)
+ : 1
+ const isLastCol = colIndex + mergedColSpan === colCount
+ const isLastRow = displayedRowIndex + mergedRowSpan === rowCount
+ const isCrossRowTd = mergedRowSpan > 1
+ if (isCrossRowTd) {
+ cordKey[displayedRowIndex] = {
+ [colIndex]: []
+ }
+ }
+ if (mergedColSpan > 1 || isCrossRowTd) {
+ for (
+ let i = displayedRowIndex;
+ i < displayedRowIndex + mergedRowSpan;
+ ++i
+ ) {
+ if (isCrossRowTd) {
+ cordKey[displayedRowIndex][colIndex].push(
+ rowIndexToKey[i]
+ )
+ }
+ for (let j = colIndex; j < colIndex + mergedColSpan; ++j) {
+ if (i === displayedRowIndex && j === colIndex) {
+ continue
+ }
+ if (!(i in cordToPass)) {
+ cordToPass[i] = [j]
+ }
+ else {
+ cordToPass[i].push(j)
+ }
+ }
+ }
+ }
+ const hoverKey = isCrossRowTd ? this.hoverKey : null
+ const { cellProps } = column
+ const resolvedCellProps = cellProps?.(rowData, actualRowIndex)
+ const indentOffsetStyle = {
+ '--indent-offset': '' as string | number
+ }
+ const FinalCellComponent = column.fixed ? 'td' : CellComponent
+ return (
+
+ {hasChildren && colIndex === childTriggerColIndex
+ ? [
+ repeat(
+ (indentOffsetStyle['--indent-offset'] = isSummary
+ ? 0
+ : rowInfo.tmNode.level),
+
+ ),
+ isSummary || rowInfo.tmNode.isLeaf ? (
+
+ ) : (
+ {
+ handleUpdateExpanded(rowKey, rowInfo.tmNode)
+ }}
+ />
+ )
+ ]
+ : null}
+ {column.type === 'selection' ? (
+ !isSummary ? (
+ column.multiple === false ? (
+ {
+ handleRadioUpdateChecked(rowInfo.tmNode)
+ }}
+ />
+ ) : (
+ {
+ handleCheckboxUpdateChecked(
+ rowInfo.tmNode,
+ checked,
+ e.shiftKey
+ )
+ }}
+ />
+ )
+ ) : null
+ ) : column.type === 'expand' ? (
+ !isSummary ? (
+ !column.expandable || column.expandable?.(rowData) ? (
+ {
+ handleUpdateExpanded(rowKey, null)
+ }}
+ />
+ ) : null
+ ) : null
+ ) : (
+ |
+ )}
+
+ )
+ })
+
+ if (isVirtualX) {
+ if (leftFixedColsCount && rightFixedColsCount) {
+ cells.splice(
+ leftFixedColsCount,
+ 0,
+
|
+ )
+ }
+ }
+
const row = (
{
@@ -746,211 +1021,15 @@ export default defineComponent({
isSummary && `${mergedClsPrefix}-data-table-tr--summary`,
striped && `${mergedClsPrefix}-data-table-tr--striped`,
expanded && `${mergedClsPrefix}-data-table-tr--expanded`,
- mergedRowClassName
+ mergedRowClassName,
+ props?.class
]}
+ style={props?.style}
{...props}
>
- {cols.map((col, colIndex) => {
- if (displayedRowIndex in cordToPass) {
- const cordOfRowToPass = cordToPass[displayedRowIndex]
- const indexInCordOfRowToPass
- = cordOfRowToPass.indexOf(colIndex)
- if (~indexInCordOfRowToPass) {
- cordOfRowToPass.splice(indexInCordOfRowToPass, 1)
- return null
- }
- }
-
- // TODO: Simplify row calculation
- const { column } = col
- const colKey = getColKey(col)
- const { rowSpan, colSpan } = column
- const mergedColSpan = isSummary
- ? rowInfo.tmNode.rawNode[colKey]?.colSpan || 1 // optional for #1276
- : colSpan
- ? colSpan(rowData, actualRowIndex)
- : 1
- const mergedRowSpan = isSummary
- ? rowInfo.tmNode.rawNode[colKey]?.rowSpan || 1 // optional for #1276
- : rowSpan
- ? rowSpan(rowData, actualRowIndex)
- : 1
- const isLastCol = colIndex + mergedColSpan === colCount
- const isLastRow
- = displayedRowIndex + mergedRowSpan === rowCount
- const isCrossRowTd = mergedRowSpan > 1
- if (isCrossRowTd) {
- cordKey[displayedRowIndex] = {
- [colIndex]: []
- }
- }
- if (mergedColSpan > 1 || isCrossRowTd) {
- for (
- let i = displayedRowIndex;
- i < displayedRowIndex + mergedRowSpan;
- ++i
- ) {
- if (isCrossRowTd) {
- cordKey[displayedRowIndex][colIndex].push(
- rowIndexToKey[i]
- )
- }
- for (
- let j = colIndex;
- j < colIndex + mergedColSpan;
- ++j
- ) {
- if (i === displayedRowIndex && j === colIndex) {
- continue
- }
- if (!(i in cordToPass)) {
- cordToPass[i] = [j]
- }
- else {
- cordToPass[i].push(j)
- }
- }
- }
- }
- const hoverKey = isCrossRowTd ? this.hoverKey : null
- const { cellProps } = column
- const resolvedCellProps = cellProps?.(
- rowData,
- actualRowIndex
- )
- const indentOffsetStyle = {
- '--indent-offset': '' as string | number
- }
- return (
-
- {hasChildren && colIndex === childTriggerColIndex
- ? [
- repeat(
- (indentOffsetStyle['--indent-offset']
- = isSummary ? 0 : rowInfo.tmNode.level),
-
- ),
- isSummary || rowInfo.tmNode.isLeaf ? (
-
- ) : (
- {
- handleUpdateExpanded(rowKey, rowInfo.tmNode)
- }}
- />
- )
- ]
- : null}
- {column.type === 'selection' ? (
- !isSummary ? (
- column.multiple === false ? (
- {
- handleRadioUpdateChecked(rowInfo.tmNode)
- }}
- />
- ) : (
- {
- handleCheckboxUpdateChecked(
- rowInfo.tmNode,
- checked,
- e.shiftKey
- )
- }}
- />
- )
- ) : null
- ) : column.type === 'expand' ? (
- !isSummary ? (
- !column.expandable
- || column.expandable?.(rowData) ? (
- {
- handleUpdateExpanded(rowKey, null)
- }}
- />
- ) : null
- ) : null
- ) : (
- |
- )}
- |
- )
- })}
+ {cells}
)
-
return row
}
@@ -975,7 +1054,17 @@ export default defineComponent({
class={`${mergedClsPrefix}-data-table-tbody`}
>
{displayedData.map((rowInfo, displayedRowIndex) => {
- return renderRow(rowInfo, displayedRowIndex, false)
+ return renderRow({
+ rowInfo,
+ displayedRowIndex,
+ isVirtual: false,
+ isVirtualX: false,
+ startColIndex: -1,
+ endColIndex: -1,
+ getLeft(_index) {
+ return -1
+ }
+ })
})}
) : null}
@@ -987,7 +1076,7 @@ export default defineComponent({
{
+ return renderRow({
+ displayedRowIndex: itemIndex,
+ isVirtual: true,
+ isVirtualX: true,
+ rowInfo: item as RowRenderInfo,
+ startColIndex,
+ endColIndex,
+ getLeft
+ })
+ }
+ : undefined
+ }
>
{{
default: ({
item,
- index
+ index,
+ renderedItemWithCols
}: {
item: RowRenderInfo
index: number
- }) => renderRow(item, index, true)
+ renderedItemWithCols: VNodeChild
+ }) => {
+ if (renderedItemWithCols)
+ return renderedItemWithCols
+ return renderRow({
+ rowInfo: item,
+ displayedRowIndex: index,
+ isVirtual: true,
+ isVirtualX: false,
+ startColIndex: 0,
+ endColIndex: 0,
+ getLeft(_index) {
+ return 0
+ }
+ })
+ }
}}
)
diff --git a/src/data-table/src/TableParts/Header.tsx b/src/data-table/src/TableParts/Header.tsx
index 6aa71931bd0..cfcc61f9633 100644
--- a/src/data-table/src/TableParts/Header.tsx
+++ b/src/data-table/src/TableParts/Header.tsx
@@ -1,13 +1,8 @@
-import {
- Fragment,
- type VNode,
- type VNodeChild,
- defineComponent,
- h,
- inject,
- ref
-} from 'vue'
+import { Fragment, defineComponent, h, inject, ref } from 'vue'
+import type { PropType, VNode, VNodeChild } from 'vue'
import { happensIn, pxfy } from 'seemly'
+import type { VirtualListInst } from 'vueuc'
+import { VVirtualList } from 'vueuc'
import { formatLength } from '../../../_utils'
import { NCheckbox } from '../../../checkbox'
import { NEllipsis } from '../../../ellipsis'
@@ -30,6 +25,7 @@ import {
type TableExpandColumn,
dataTableInjectionKey
} from '../interface'
+import type { ColItem, RowItem } from '../use-group-header'
import SelectionMenu from './SelectionMenu'
function renderTitle(
@@ -40,6 +36,42 @@ function renderTitle(
: column.title
}
+const VirtualListItemWrapper = defineComponent({
+ props: {
+ clsPrefix: {
+ type: String,
+ required: true
+ },
+ id: {
+ type: String,
+ required: true
+ },
+ cols: {
+ type: Array as PropType,
+ required: true
+ },
+ width: String
+ },
+ render() {
+ const { clsPrefix, id, cols, width } = this
+ return (
+
+
+ {cols.map(col => (
+
+ ))}
+
+
+ {this.$slots}
+
+
+ )
+ }
+})
+
export default defineComponent({
name: 'DataTableHeader',
props: {
@@ -65,6 +97,8 @@ export default defineComponent({
componentId,
mergedTableLayoutRef,
headerCheckboxDisabledRef,
+ virtualScrollHeaderRef,
+ headerHeightRef,
onUnstableColumnResize,
doUpdateResizableWidth,
handleTableHeaderScroll,
@@ -72,6 +106,7 @@ export default defineComponent({
doUncheckAll,
doCheckAll
} = inject(dataTableInjectionKey)!
+ const virtualListRef = ref()
const cellElsRef = ref>({})
function getCellActualWidth(key: ColumnKey): number | undefined {
const element = cellElsRef.value[key]
@@ -147,6 +182,9 @@ export default defineComponent({
checkOptions: checkOptionsRef,
mergedTableLayout: mergedTableLayoutRef,
headerCheckboxDisabled: headerCheckboxDisabledRef,
+ headerHeight: headerHeightRef,
+ virtualScrollHeader: virtualScrollHeaderRef,
+ virtualListRef,
handleCheckboxUpdateChecked,
handleColHeaderClick,
handleTableHeaderScroll,
@@ -172,12 +210,233 @@ export default defineComponent({
mergedTableLayout,
headerCheckboxDisabled,
mergedSortState,
+ virtualScrollHeader,
handleColHeaderClick,
handleCheckboxUpdateChecked,
handleColumnResizeStart,
handleColumnResize
} = this
let hasEllipsis = false
+
+ const renderRow = (
+ row: RowItem[],
+ getLeft: ((index: number) => number) | null,
+ headerHeightPx: string | undefined
+ ) =>
+ row.map(({ column, colIndex, colSpan, rowSpan, isLast }) => {
+ const key = getColKey(column)
+ const { ellipsis } = column
+ if (!hasEllipsis && ellipsis)
+ hasEllipsis = true
+ const createColumnVNode = (): VNode | null => {
+ if (column.type === 'selection') {
+ return column.multiple !== false ? (
+ <>
+
+ {checkOptions ? (
+
+ ) : null}
+ >
+ ) : null
+ }
+ return (
+ <>
+
+
+ {ellipsis === true || (ellipsis && !ellipsis.tooltip) ? (
+
+ {renderTitle(column)}
+
+ ) : ellipsis && typeof ellipsis === 'object' ? (
+
+ {{
+ default: () => renderTitle(column)
+ }}
+
+ ) : (
+ renderTitle(column)
+ )}
+
+ {isColumnSortable(column) ? (
+
+ ) : null}
+
+ {isColumnFilterable(column) ? (
+
+ ) : null}
+ {isColumnResizable(column) ? (
+ {
+ handleColumnResizeStart(column as TableBaseColumn)
+ }}
+ onResize={(displacementX) => {
+ handleColumnResize(column as TableBaseColumn, displacementX)
+ }}
+ />
+ ) : null}
+ >
+ )
+ }
+ const leftFixed = key in fixedColumnLeftMap
+ const rightFixed = key in fixedColumnRightMap
+ const CellComponent = (getLeft && !column.fixed ? 'div' : 'th') as 'th'
+ return (
+ (cellElsRef[key] = el as HTMLTableCellElement)}
+ key={key}
+ style={[
+ getLeft && !column.fixed
+ ? {
+ position: 'absolute',
+ left: pxfy(getLeft(colIndex)),
+ top: 0,
+ bottom: 0
+ }
+ : {
+ left: pxfy(fixedColumnLeftMap[key]?.start),
+ right: pxfy(fixedColumnRightMap[key]?.start)
+ },
+ {
+ width: pxfy(column.width),
+ textAlign: column.titleAlign || column.align,
+ height: headerHeightPx
+ }
+ ]}
+ colspan={colSpan}
+ rowspan={rowSpan}
+ data-col-key={key}
+ class={[
+ `${mergedClsPrefix}-data-table-th`,
+ (leftFixed || rightFixed)
+ && `${mergedClsPrefix}-data-table-th--fixed-${
+ leftFixed ? 'left' : 'right'
+ }`,
+ {
+ [`${mergedClsPrefix}-data-table-th--sorting`]: isColumnSorting(
+ column,
+ mergedSortState
+ ),
+ [`${mergedClsPrefix}-data-table-th--filterable`]:
+ isColumnFilterable(column),
+ [`${mergedClsPrefix}-data-table-th--sortable`]:
+ isColumnSortable(column),
+ [`${mergedClsPrefix}-data-table-th--selection`]:
+ column.type === 'selection',
+ [`${mergedClsPrefix}-data-table-th--last`]: isLast
+ },
+ column.className
+ ]}
+ onClick={
+ column.type !== 'selection'
+ && column.type !== 'expand'
+ && !('children' in column)
+ ? (e) => {
+ handleColHeaderClick(e, column)
+ }
+ : undefined
+ }
+ >
+ {createColumnVNode()}
+
+ )
+ })
+
+ if (virtualScrollHeader) {
+ const { headerHeight } = this
+
+ let leftFixedColsCount = 0
+ let rightFixedColsCount = 0
+
+ cols.forEach((col) => {
+ if (col.column.fixed === 'left') {
+ leftFixedColsCount++
+ }
+ else if (col.column.fixed === 'right') {
+ rightFixedColsCount++
+ }
+ })
+
+ return (
+
+ )
+ }
+
const theadVNode = (
{
return (
- {row.map(({ column, colSpan, rowSpan, isLast }) => {
- const key = getColKey(column)
- const { ellipsis } = column
- if (!hasEllipsis && ellipsis)
- hasEllipsis = true
- const createColumnVNode = (): VNode | null => {
- if (column.type === 'selection') {
- return column.multiple !== false ? (
- <>
-
- {checkOptions ? (
-
- ) : null}
- >
- ) : null
- }
- return (
- <>
-
-
- {ellipsis === true
- || (ellipsis && !ellipsis.tooltip) ? (
-
- {renderTitle(column)}
-
- ) : ellipsis && typeof ellipsis === 'object' ? (
-
- {{
- default: () => renderTitle(column)
- }}
-
- ) : (
- renderTitle(column)
- )}
-
- {isColumnSortable(column) ? (
-
- ) : null}
-
- {isColumnFilterable(column) ? (
-
- ) : null}
- {isColumnResizable(column) ? (
- {
- handleColumnResizeStart(column as TableBaseColumn)
- }}
- onResize={(displacementX) => {
- handleColumnResize(
- column as TableBaseColumn,
- displacementX
- )
- }}
- />
- ) : null}
- >
- )
- }
- const leftFixed = key in fixedColumnLeftMap
- const rightFixed = key in fixedColumnRightMap
- return (
- (cellElsRef[key] = el as HTMLTableCellElement)}
- key={key}
- style={{
- textAlign: column.titleAlign || column.align,
- left: pxfy(fixedColumnLeftMap[key]?.start),
- right: pxfy(fixedColumnRightMap[key]?.start)
- }}
- colspan={colSpan}
- rowspan={rowSpan}
- data-col-key={key}
- class={[
- `${mergedClsPrefix}-data-table-th`,
- (leftFixed || rightFixed)
- && `${mergedClsPrefix}-data-table-th--fixed-${
- leftFixed ? 'left' : 'right'
- }`,
- {
- [`${mergedClsPrefix}-data-table-th--sorting`]:
- isColumnSorting(column, mergedSortState),
- [`${mergedClsPrefix}-data-table-th--filterable`]:
- isColumnFilterable(column),
- [`${mergedClsPrefix}-data-table-th--sortable`]:
- isColumnSortable(column),
- [`${mergedClsPrefix}-data-table-th--selection`]:
- column.type === 'selection',
- [`${mergedClsPrefix}-data-table-th--last`]: isLast
- },
- column.className
- ]}
- onClick={
- column.type !== 'selection'
- && column.type !== 'expand'
- && !('children' in column)
- ? (e) => {
- handleColHeaderClick(e, column)
- }
- : undefined
- }
- >
- {createColumnVNode()}
- |
- )
- })}
+ {renderRow(row, null, undefined)}
)
})}
@@ -326,7 +461,6 @@ export default defineComponent({
onScroll={handleTableHeaderScroll}
>
,
stickyExpandedRows: Boolean,
virtualScroll: Boolean,
+ virtualScrollX: Boolean,
+ virtualScrollHeader: Boolean,
+ headerHeight: Number,
+ heightForRow: Function as PropType,
+ minRowHeight: Number,
tableLayout: {
type: String as PropType<'auto' | 'fixed'>,
default: 'auto'
@@ -231,6 +237,11 @@ export interface CommonColumnInfo {
cellProps?: (rowData: T, rowIndex: number) => HTMLAttributes
}
+export type DataTableHeightForRow = (
+ rowData: T,
+ rowIndex: number
+) => number
+
export type TableColumnTitle =
| string
| ((column: TableBaseColumn) => VNodeChild)
@@ -384,6 +395,11 @@ export interface DataTableInjection {
summaryRef: Ref
rawPaginatedDataRef: Ref
virtualScrollRef: Ref
+ virtualScrollXRef: Ref
+ minRowHeightRef: Ref
+ heightForRowRef: Ref
+ virtualScrollHeaderRef: Ref
+ headerHeightRef: Ref
bodyWidthRef: Ref
mergedTableLayoutRef: Ref<'auto' | 'fixed'>
maxHeightRef: Ref
@@ -502,6 +518,7 @@ export interface MainTableBodyRef {
export interface MainTableHeaderRef {
$el: HTMLElement | null
+ virtualListRef: Ref
}
export type OnFilterMenuChange = <
diff --git a/src/data-table/src/styles/index.cssr.ts b/src/data-table/src/styles/index.cssr.ts
index 9d4ca9f3c53..ba321f978c7 100644
--- a/src/data-table/src/styles/index.cssr.ts
+++ b/src/data-table/src/styles/index.cssr.ts
@@ -184,6 +184,7 @@ export default c([
background-color: var(--n-merged-th-color);
`),
cB('data-table-tr', `
+ position: relative;
box-sizing: border-box;
background-clip: padding-box;
transition: background-color .3s var(--n-bezier);
diff --git a/src/data-table/src/use-group-header.ts b/src/data-table/src/use-group-header.ts
index 4ca77b19c4c..1b4b55244f8 100644
--- a/src/data-table/src/use-group-header.ts
+++ b/src/data-table/src/use-group-header.ts
@@ -15,12 +15,18 @@ export interface RowItem {
colSpan: number
rowSpan: number
column: TableColumn
+ colIndex: number
isLast: boolean
}
export interface ColItem {
key: string | number
style: CSSProperties
column: TableSelectionColumn | TableExpandColumn | TableBaseColumn
+ index: number
+ /**
+ * The width property is only applied to horizontally virtual scroll table
+ */
+ width: number
}
type RowItemMap = WeakMap
@@ -49,7 +55,7 @@ function getRowsAndCols(
rows[currentDepth] = []
maxDepth = currentDepth
}
- for (const column of columns) {
+ columns.forEach((column, index) => {
if ('children' in column) {
ensureMaxDepth(column.children, currentDepth + 1)
}
@@ -61,7 +67,10 @@ function getRowsAndCols(
column,
key !== undefined ? formatLength(getResizableWidth(key)) : undefined
),
- column
+ column,
+ index,
+ // The width property is only applied to horizontally virtual scroll table
+ width: column.width === undefined ? 128 : Number(column.width)
})
totalRowSpan += 1
if (!hasEllipsis) {
@@ -69,7 +78,7 @@ function getRowsAndCols(
}
dataRelatedCols.push(column)
}
- }
+ })
}
ensureMaxDepth(columns, 0)
let currentLeafIndex = 0
@@ -82,6 +91,7 @@ function getRowsAndCols(
const cachedCurrentLeafIndex = currentLeafIndex
const rowItem: RowItem = {
column,
+ colIndex: currentLeafIndex,
colSpan: 0,
rowSpan: 1,
isLast: false
@@ -112,6 +122,7 @@ function getRowsAndCols(
const rowItem: RowItem = {
column,
colSpan,
+ colIndex: currentLeafIndex,
rowSpan: maxDepth - currentDepth + 1,
isLast
}