mirror of
https://github.com/tusen-ai/naive-ui.git
synced 2024-11-21 01:13:16 +08:00
feat(data-table): supports horizontal virtual scrolling
This commit is contained in:
parent
2e453472d8
commit
17b96c9027
@ -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",
|
||||
|
@ -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
|
||||
|
82
src/data-table/demos/enUS/virtual-x.demo.vue
Normal file
82
src/data-table/demos/enUS/virtual-x.demo.vue
Normal file
@ -0,0 +1,82 @@
|
||||
<markdown>
|
||||
# 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.
|
||||
</markdown>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
|
||||
interface RowData {
|
||||
key: number
|
||||
name: string
|
||||
age: number
|
||||
address: string
|
||||
}
|
||||
|
||||
const columns: DataTableColumns<RowData> = []
|
||||
|
||||
let scrollX = 0
|
||||
|
||||
for (let i = 0; i < 1000; ++i) {
|
||||
scrollX += 100
|
||||
columns.push({
|
||||
title: `Col ${i}`,
|
||||
width: 100,
|
||||
key: i,
|
||||
fixed: i <= 1 ? 'left' : i > 997 ? 'right' : undefined,
|
||||
render(_, rowIndex) {
|
||||
return `${i}-${rowIndex}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const data: RowData[] = Array.from({ length: 1000 }).map((_, index) => ({
|
||||
key: index,
|
||||
name: `Edward King ${index}`,
|
||||
age: 32,
|
||||
address: `London, Park Lane no. ${index}`
|
||||
}))
|
||||
return {
|
||||
data,
|
||||
columns,
|
||||
scrollX,
|
||||
minRowHeight: 48,
|
||||
heightForRow: () => 48
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:max-height="250"
|
||||
virtual-scroll
|
||||
virtual-scroll-x
|
||||
:scroll-x="scrollX"
|
||||
:min-row-height="48"
|
||||
:height-for-row="heightForRow"
|
||||
virtual-scroll-header
|
||||
:header-height="48"
|
||||
/>
|
||||
</template>
|
@ -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
|
||||
|
82
src/data-table/demos/zhCN/virtual-x.demo.vue
Normal file
82
src/data-table/demos/zhCN/virtual-x.demo.vue
Normal file
@ -0,0 +1,82 @@
|
||||
<markdown>
|
||||
# 大量数据(行和列)
|
||||
|
||||
如果你有大量行数据和列数据,例如几千行 + 几百列,`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` 的表格可以轻松的支持千万级的表格数据,你在不收钱的组件库不容易找得到这样的功能。
|
||||
</markdown>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
|
||||
interface RowData {
|
||||
key: number
|
||||
name: string
|
||||
age: number
|
||||
address: string
|
||||
}
|
||||
|
||||
const columns: DataTableColumns<RowData> = []
|
||||
|
||||
let scrollX = 0
|
||||
|
||||
for (let i = 0; i < 1000; ++i) {
|
||||
scrollX += 100
|
||||
columns.push({
|
||||
title: `Col ${i}`,
|
||||
width: 100,
|
||||
key: i,
|
||||
fixed: i <= 1 ? 'left' : i > 997 ? 'right' : undefined,
|
||||
render(_, rowIndex) {
|
||||
return `${i}-${rowIndex}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const data: RowData[] = Array.from({ length: 1000 }).map((_, index) => ({
|
||||
key: index,
|
||||
name: `Edward King ${index}`,
|
||||
age: 32,
|
||||
address: `London, Park Lane no. ${index}`
|
||||
}))
|
||||
return {
|
||||
data,
|
||||
columns,
|
||||
scrollX,
|
||||
minRowHeight: 48,
|
||||
heightForRow: () => 48
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:max-height="250"
|
||||
virtual-scroll
|
||||
virtual-scroll-x
|
||||
:scroll-x="scrollX"
|
||||
:min-row-height="48"
|
||||
:height-for-row="heightForRow"
|
||||
virtual-scroll-header
|
||||
:header-height="48"
|
||||
/>
|
||||
</template>
|
@ -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(() => {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 (
|
||||
<FinalCellComponent
|
||||
{...resolvedCellProps}
|
||||
key={colKey}
|
||||
style={[
|
||||
{
|
||||
textAlign: column.align || undefined,
|
||||
width: pxfy(column.width)
|
||||
},
|
||||
isVirtualX && {
|
||||
height: virtualXRowHeight
|
||||
},
|
||||
isVirtualX && !column.fixed
|
||||
? {
|
||||
position: 'absolute',
|
||||
left: pxfy(getLeft(colIndex)),
|
||||
top: 0,
|
||||
bottom: 0
|
||||
}
|
||||
: {
|
||||
left: pxfy(fixedColumnLeftMap[colKey]?.start),
|
||||
right: pxfy(fixedColumnRightMap[colKey]?.start)
|
||||
},
|
||||
indentOffsetStyle as CSSProperties,
|
||||
resolvedCellProps?.style || ''
|
||||
]}
|
||||
colspan={mergedColSpan}
|
||||
rowspan={isVirtual ? undefined : mergedRowSpan}
|
||||
data-col-key={colKey}
|
||||
class={[
|
||||
`${mergedClsPrefix}-data-table-td`,
|
||||
column.className,
|
||||
resolvedCellProps?.class,
|
||||
isSummary && `${mergedClsPrefix}-data-table-td--summary`,
|
||||
hoverKey !== null
|
||||
&& cordKey[displayedRowIndex][colIndex].includes(
|
||||
hoverKey
|
||||
)
|
||||
&& `${mergedClsPrefix}-data-table-td--hover`,
|
||||
isColumnSorting(column, mergedSortState)
|
||||
&& `${mergedClsPrefix}-data-table-td--sorting`,
|
||||
column.fixed
|
||||
&& `${mergedClsPrefix}-data-table-td--fixed-${column.fixed}`,
|
||||
column.align
|
||||
&& `${mergedClsPrefix}-data-table-td--${column.align}-align`,
|
||||
column.type === 'selection'
|
||||
&& `${mergedClsPrefix}-data-table-td--selection`,
|
||||
column.type === 'expand'
|
||||
&& `${mergedClsPrefix}-data-table-td--expand`,
|
||||
isLastCol && `${mergedClsPrefix}-data-table-td--last-col`,
|
||||
isLastRow && `${mergedClsPrefix}-data-table-td--last-row`
|
||||
]}
|
||||
>
|
||||
{hasChildren && colIndex === childTriggerColIndex
|
||||
? [
|
||||
repeat(
|
||||
(indentOffsetStyle['--indent-offset'] = isSummary
|
||||
? 0
|
||||
: rowInfo.tmNode.level),
|
||||
<div
|
||||
class={`${mergedClsPrefix}-data-table-indent`}
|
||||
style={indentStyle}
|
||||
/>
|
||||
),
|
||||
isSummary || rowInfo.tmNode.isLeaf ? (
|
||||
<div
|
||||
class={`${mergedClsPrefix}-data-table-expand-placeholder`}
|
||||
/>
|
||||
) : (
|
||||
<ExpandTrigger
|
||||
class={`${mergedClsPrefix}-data-table-expand-trigger`}
|
||||
clsPrefix={mergedClsPrefix}
|
||||
expanded={expanded}
|
||||
rowData={rowData}
|
||||
renderExpandIcon={this.renderExpandIcon}
|
||||
loading={loadingKeySet.has(rowInfo.key)}
|
||||
onClick={() => {
|
||||
handleUpdateExpanded(rowKey, rowInfo.tmNode)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
]
|
||||
: null}
|
||||
{column.type === 'selection' ? (
|
||||
!isSummary ? (
|
||||
column.multiple === false ? (
|
||||
<RenderSafeRadio
|
||||
key={currentPage}
|
||||
rowKey={rowKey}
|
||||
disabled={rowInfo.tmNode.disabled}
|
||||
onUpdateChecked={() => {
|
||||
handleRadioUpdateChecked(rowInfo.tmNode)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<RenderSafeCheckbox
|
||||
key={currentPage}
|
||||
rowKey={rowKey}
|
||||
disabled={rowInfo.tmNode.disabled}
|
||||
onUpdateChecked={(checked: boolean, e) => {
|
||||
handleCheckboxUpdateChecked(
|
||||
rowInfo.tmNode,
|
||||
checked,
|
||||
e.shiftKey
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
) : null
|
||||
) : column.type === 'expand' ? (
|
||||
!isSummary ? (
|
||||
!column.expandable || column.expandable?.(rowData) ? (
|
||||
<ExpandTrigger
|
||||
clsPrefix={mergedClsPrefix}
|
||||
rowData={rowData}
|
||||
expanded={expanded}
|
||||
renderExpandIcon={this.renderExpandIcon}
|
||||
onClick={() => {
|
||||
handleUpdateExpanded(rowKey, null)
|
||||
}}
|
||||
/>
|
||||
) : null
|
||||
) : null
|
||||
) : (
|
||||
<Cell
|
||||
clsPrefix={mergedClsPrefix}
|
||||
index={actualRowIndex}
|
||||
row={rowData}
|
||||
column={column}
|
||||
isSummary={isSummary}
|
||||
mergedTheme={mergedTheme}
|
||||
renderCell={this.renderCell}
|
||||
/>
|
||||
)}
|
||||
</FinalCellComponent>
|
||||
)
|
||||
})
|
||||
|
||||
if (isVirtualX) {
|
||||
if (leftFixedColsCount && rightFixedColsCount) {
|
||||
cells.splice(
|
||||
leftFixedColsCount,
|
||||
0,
|
||||
<td
|
||||
colspan={
|
||||
cols.length - leftFixedColsCount - rightFixedColsCount
|
||||
}
|
||||
style={{
|
||||
pointerEvents: 'none',
|
||||
visibility: 'hidden',
|
||||
height: 0
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const row = (
|
||||
<tr
|
||||
onMouseenter={() => {
|
||||
@ -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 (
|
||||
<td
|
||||
{...resolvedCellProps}
|
||||
key={colKey}
|
||||
style={[
|
||||
{
|
||||
textAlign: column.align || undefined,
|
||||
left: pxfy(fixedColumnLeftMap[colKey]?.start),
|
||||
right: pxfy(fixedColumnRightMap[colKey]?.start)
|
||||
},
|
||||
indentOffsetStyle as CSSProperties,
|
||||
resolvedCellProps?.style || ''
|
||||
]}
|
||||
colspan={mergedColSpan}
|
||||
rowspan={isVirtual ? undefined : mergedRowSpan}
|
||||
data-col-key={colKey}
|
||||
class={[
|
||||
`${mergedClsPrefix}-data-table-td`,
|
||||
column.className,
|
||||
resolvedCellProps?.class,
|
||||
isSummary
|
||||
&& `${mergedClsPrefix}-data-table-td--summary`,
|
||||
hoverKey !== null
|
||||
&& cordKey[displayedRowIndex][colIndex].includes(
|
||||
hoverKey
|
||||
)
|
||||
&& `${mergedClsPrefix}-data-table-td--hover`,
|
||||
isColumnSorting(column, mergedSortState)
|
||||
&& `${mergedClsPrefix}-data-table-td--sorting`,
|
||||
column.fixed
|
||||
&& `${mergedClsPrefix}-data-table-td--fixed-${column.fixed}`,
|
||||
column.align
|
||||
&& `${mergedClsPrefix}-data-table-td--${column.align}-align`,
|
||||
column.type === 'selection'
|
||||
&& `${mergedClsPrefix}-data-table-td--selection`,
|
||||
column.type === 'expand'
|
||||
&& `${mergedClsPrefix}-data-table-td--expand`,
|
||||
isLastCol
|
||||
&& `${mergedClsPrefix}-data-table-td--last-col`,
|
||||
isLastRow
|
||||
&& `${mergedClsPrefix}-data-table-td--last-row`
|
||||
]}
|
||||
>
|
||||
{hasChildren && colIndex === childTriggerColIndex
|
||||
? [
|
||||
repeat(
|
||||
(indentOffsetStyle['--indent-offset']
|
||||
= isSummary ? 0 : rowInfo.tmNode.level),
|
||||
<div
|
||||
class={`${mergedClsPrefix}-data-table-indent`}
|
||||
style={indentStyle}
|
||||
/>
|
||||
),
|
||||
isSummary || rowInfo.tmNode.isLeaf ? (
|
||||
<div
|
||||
class={`${mergedClsPrefix}-data-table-expand-placeholder`}
|
||||
/>
|
||||
) : (
|
||||
<ExpandTrigger
|
||||
class={`${mergedClsPrefix}-data-table-expand-trigger`}
|
||||
clsPrefix={mergedClsPrefix}
|
||||
expanded={expanded}
|
||||
rowData={rowData}
|
||||
renderExpandIcon={this.renderExpandIcon}
|
||||
loading={loadingKeySet.has(rowInfo.key)}
|
||||
onClick={() => {
|
||||
handleUpdateExpanded(rowKey, rowInfo.tmNode)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
]
|
||||
: null}
|
||||
{column.type === 'selection' ? (
|
||||
!isSummary ? (
|
||||
column.multiple === false ? (
|
||||
<RenderSafeRadio
|
||||
key={currentPage}
|
||||
rowKey={rowKey}
|
||||
disabled={rowInfo.tmNode.disabled}
|
||||
onUpdateChecked={() => {
|
||||
handleRadioUpdateChecked(rowInfo.tmNode)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<RenderSafeCheckbox
|
||||
key={currentPage}
|
||||
rowKey={rowKey}
|
||||
disabled={rowInfo.tmNode.disabled}
|
||||
onUpdateChecked={(checked: boolean, e) => {
|
||||
handleCheckboxUpdateChecked(
|
||||
rowInfo.tmNode,
|
||||
checked,
|
||||
e.shiftKey
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
) : null
|
||||
) : column.type === 'expand' ? (
|
||||
!isSummary ? (
|
||||
!column.expandable
|
||||
|| column.expandable?.(rowData) ? (
|
||||
<ExpandTrigger
|
||||
clsPrefix={mergedClsPrefix}
|
||||
rowData={rowData}
|
||||
expanded={expanded}
|
||||
renderExpandIcon={this.renderExpandIcon}
|
||||
onClick={() => {
|
||||
handleUpdateExpanded(rowKey, null)
|
||||
}}
|
||||
/>
|
||||
) : null
|
||||
) : null
|
||||
) : (
|
||||
<Cell
|
||||
clsPrefix={mergedClsPrefix}
|
||||
index={actualRowIndex}
|
||||
row={rowData}
|
||||
column={column}
|
||||
isSummary={isSummary}
|
||||
mergedTheme={mergedTheme}
|
||||
renderCell={this.renderCell}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
{cells}
|
||||
</tr>
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
})}
|
||||
</tbody>
|
||||
) : null}
|
||||
@ -987,7 +1076,7 @@ export default defineComponent({
|
||||
<VirtualList
|
||||
ref="virtualListRef"
|
||||
items={displayedData}
|
||||
itemSize={28}
|
||||
itemSize={this.minRowHeight || 28}
|
||||
visibleItemsTag={VirtualListItemWrapper}
|
||||
visibleItemsProps={{
|
||||
clsPrefix: mergedClsPrefix,
|
||||
@ -999,16 +1088,54 @@ export default defineComponent({
|
||||
onResize={this.handleVirtualListResize}
|
||||
onScroll={this.handleVirtualListScroll}
|
||||
itemsStyle={contentStyle}
|
||||
itemResizable
|
||||
itemResizable={!virtualScrollX}
|
||||
columns={cols}
|
||||
renderItemWithCols={
|
||||
virtualScrollX
|
||||
? ({
|
||||
itemIndex,
|
||||
item,
|
||||
startColIndex,
|
||||
endColIndex,
|
||||
getLeft
|
||||
}) => {
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
</VirtualList>
|
||||
)
|
||||
|
@ -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<ColItem[]>,
|
||||
required: true
|
||||
},
|
||||
width: String
|
||||
},
|
||||
render() {
|
||||
const { clsPrefix, id, cols, width } = this
|
||||
return (
|
||||
<table
|
||||
style={{ tableLayout: 'fixed', width }}
|
||||
class={`${clsPrefix}-data-table-table`}
|
||||
>
|
||||
<colgroup>
|
||||
{cols.map(col => (
|
||||
<col key={col.key} style={col.style}></col>
|
||||
))}
|
||||
</colgroup>
|
||||
<thead data-n-id={id} class={`${clsPrefix}-data-table-thead`}>
|
||||
{this.$slots}
|
||||
</thead>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
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<VirtualListInst | null>()
|
||||
const cellElsRef = ref<Record<ColumnKey, HTMLTableCellElement>>({})
|
||||
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 ? (
|
||||
<>
|
||||
<NCheckbox
|
||||
key={currentPage}
|
||||
privateInsideTable
|
||||
checked={allRowsChecked}
|
||||
indeterminate={someRowsChecked}
|
||||
disabled={headerCheckboxDisabled}
|
||||
onUpdateChecked={handleCheckboxUpdateChecked}
|
||||
/>
|
||||
{checkOptions ? (
|
||||
<SelectionMenu clsPrefix={mergedClsPrefix} />
|
||||
) : null}
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div class={`${mergedClsPrefix}-data-table-th__title-wrapper`}>
|
||||
<div class={`${mergedClsPrefix}-data-table-th__title`}>
|
||||
{ellipsis === true || (ellipsis && !ellipsis.tooltip) ? (
|
||||
<div class={`${mergedClsPrefix}-data-table-th__ellipsis`}>
|
||||
{renderTitle(column)}
|
||||
</div>
|
||||
) : ellipsis && typeof ellipsis === 'object' ? (
|
||||
<NEllipsis
|
||||
{...ellipsis}
|
||||
theme={mergedTheme.peers.Ellipsis}
|
||||
themeOverrides={mergedTheme.peerOverrides.Ellipsis}
|
||||
>
|
||||
{{
|
||||
default: () => renderTitle(column)
|
||||
}}
|
||||
</NEllipsis>
|
||||
) : (
|
||||
renderTitle(column)
|
||||
)}
|
||||
</div>
|
||||
{isColumnSortable(column) ? (
|
||||
<SortButton column={column as TableBaseColumn} />
|
||||
) : null}
|
||||
</div>
|
||||
{isColumnFilterable(column) ? (
|
||||
<FilterButton
|
||||
column={column as TableBaseColumn}
|
||||
options={column.filterOptions}
|
||||
/>
|
||||
) : null}
|
||||
{isColumnResizable(column) ? (
|
||||
<ResizeButton
|
||||
onResizeStart={() => {
|
||||
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 (
|
||||
<CellComponent
|
||||
ref={el => (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()}
|
||||
</CellComponent>
|
||||
)
|
||||
})
|
||||
|
||||
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 (
|
||||
<VVirtualList
|
||||
ref="virtualListRef"
|
||||
class={`${mergedClsPrefix}-data-table-base-table-header`}
|
||||
style={{ height: pxfy(headerHeight) }}
|
||||
onScroll={this.handleTableHeaderScroll}
|
||||
columns={cols}
|
||||
itemSize={headerHeight || 28}
|
||||
showScrollbar={false}
|
||||
items={[{}]}
|
||||
itemResizable={false}
|
||||
visibleItemsTag={VirtualListItemWrapper}
|
||||
visibleItemsProps={{
|
||||
clsPrefix: mergedClsPrefix,
|
||||
id: componentId,
|
||||
cols,
|
||||
width: formatLength(this.scrollX)
|
||||
}}
|
||||
renderItemWithCols={({ startColIndex, endColIndex, getLeft }) => {
|
||||
const row = cols
|
||||
.map<RowItem>((col, index) => {
|
||||
return {
|
||||
column: col.column,
|
||||
isLast: index === cols.length - 1,
|
||||
colIndex: col.index,
|
||||
colSpan: 1,
|
||||
rowSpan: 1
|
||||
}
|
||||
})
|
||||
.filter(({ column }, index) => {
|
||||
if (startColIndex <= index && index <= endColIndex) {
|
||||
return true
|
||||
}
|
||||
if (column.fixed) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const cells = renderRow(row, getLeft, pxfy(headerHeight))
|
||||
|
||||
cells.splice(
|
||||
leftFixedColsCount,
|
||||
0,
|
||||
<th
|
||||
colspan={cols.length - leftFixedColsCount - rightFixedColsCount}
|
||||
style={{
|
||||
pointerEvents: 'none',
|
||||
visibility: 'hidden',
|
||||
height: 0
|
||||
}}
|
||||
/>
|
||||
)
|
||||
return <tr style={{ position: 'relative' }}>{cells}</tr>
|
||||
}}
|
||||
>
|
||||
{{
|
||||
default: ({
|
||||
renderedItemWithCols
|
||||
}: {
|
||||
renderedItemWithCols: VNodeChild
|
||||
}) => renderedItemWithCols
|
||||
}}
|
||||
</VVirtualList>
|
||||
)
|
||||
}
|
||||
|
||||
const theadVNode = (
|
||||
<thead
|
||||
class={`${mergedClsPrefix}-data-table-thead`}
|
||||
@ -186,131 +445,7 @@ export default defineComponent({
|
||||
{rows.map((row) => {
|
||||
return (
|
||||
<tr class={`${mergedClsPrefix}-data-table-tr`}>
|
||||
{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 ? (
|
||||
<>
|
||||
<NCheckbox
|
||||
key={currentPage}
|
||||
privateInsideTable
|
||||
checked={allRowsChecked}
|
||||
indeterminate={someRowsChecked}
|
||||
disabled={headerCheckboxDisabled}
|
||||
onUpdateChecked={handleCheckboxUpdateChecked}
|
||||
/>
|
||||
{checkOptions ? (
|
||||
<SelectionMenu clsPrefix={mergedClsPrefix} />
|
||||
) : null}
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
class={`${mergedClsPrefix}-data-table-th__title-wrapper`}
|
||||
>
|
||||
<div class={`${mergedClsPrefix}-data-table-th__title`}>
|
||||
{ellipsis === true
|
||||
|| (ellipsis && !ellipsis.tooltip) ? (
|
||||
<div
|
||||
class={`${mergedClsPrefix}-data-table-th__ellipsis`}
|
||||
>
|
||||
{renderTitle(column)}
|
||||
</div>
|
||||
) : ellipsis && typeof ellipsis === 'object' ? (
|
||||
<NEllipsis
|
||||
{...ellipsis}
|
||||
theme={mergedTheme.peers.Ellipsis}
|
||||
themeOverrides={
|
||||
mergedTheme.peerOverrides.Ellipsis
|
||||
}
|
||||
>
|
||||
{{
|
||||
default: () => renderTitle(column)
|
||||
}}
|
||||
</NEllipsis>
|
||||
) : (
|
||||
renderTitle(column)
|
||||
)}
|
||||
</div>
|
||||
{isColumnSortable(column) ? (
|
||||
<SortButton column={column as TableBaseColumn} />
|
||||
) : null}
|
||||
</div>
|
||||
{isColumnFilterable(column) ? (
|
||||
<FilterButton
|
||||
column={column as TableBaseColumn}
|
||||
options={column.filterOptions}
|
||||
/>
|
||||
) : null}
|
||||
{isColumnResizable(column) ? (
|
||||
<ResizeButton
|
||||
onResizeStart={() => {
|
||||
handleColumnResizeStart(column as TableBaseColumn)
|
||||
}}
|
||||
onResize={(displacementX) => {
|
||||
handleColumnResize(
|
||||
column as TableBaseColumn,
|
||||
displacementX
|
||||
)
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
const leftFixed = key in fixedColumnLeftMap
|
||||
const rightFixed = key in fixedColumnRightMap
|
||||
return (
|
||||
<th
|
||||
ref={el => (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()}
|
||||
</th>
|
||||
)
|
||||
})}
|
||||
{renderRow(row, null, undefined)}
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
@ -326,7 +461,6 @@ export default defineComponent({
|
||||
onScroll={handleTableHeaderScroll}
|
||||
>
|
||||
<table
|
||||
ref="body"
|
||||
class={`${mergedClsPrefix}-data-table-table`}
|
||||
style={{
|
||||
minWidth: formatLength(scrollX),
|
||||
|
@ -8,6 +8,7 @@ import type {
|
||||
Slots,
|
||||
VNodeChild
|
||||
} from 'vue'
|
||||
import type { VirtualListInst } from 'vueuc'
|
||||
import type { ScrollTo, ScrollbarProps } from '../../scrollbar/src/Scrollbar'
|
||||
import type { EllipsisProps } from '../../ellipsis/src/Ellipsis'
|
||||
import type { ExtractPublicPropTypes, MaybeArray } from '../../_utils'
|
||||
@ -90,6 +91,11 @@ export const dataTableProps = {
|
||||
expandedRowKeys: Array as PropType<RowKey[]>,
|
||||
stickyExpandedRows: Boolean,
|
||||
virtualScroll: Boolean,
|
||||
virtualScrollX: Boolean,
|
||||
virtualScrollHeader: Boolean,
|
||||
headerHeight: Number,
|
||||
heightForRow: Function as PropType<DataTableHeightForRow>,
|
||||
minRowHeight: Number,
|
||||
tableLayout: {
|
||||
type: String as PropType<'auto' | 'fixed'>,
|
||||
default: 'auto'
|
||||
@ -231,6 +237,11 @@ export interface CommonColumnInfo<T = InternalRowData> {
|
||||
cellProps?: (rowData: T, rowIndex: number) => HTMLAttributes
|
||||
}
|
||||
|
||||
export type DataTableHeightForRow<T = RowData> = (
|
||||
rowData: T,
|
||||
rowIndex: number
|
||||
) => number
|
||||
|
||||
export type TableColumnTitle =
|
||||
| string
|
||||
| ((column: TableBaseColumn) => VNodeChild)
|
||||
@ -384,6 +395,11 @@ export interface DataTableInjection {
|
||||
summaryRef: Ref<undefined | CreateSummary>
|
||||
rawPaginatedDataRef: Ref<InternalRowData[]>
|
||||
virtualScrollRef: Ref<boolean>
|
||||
virtualScrollXRef: Ref<boolean>
|
||||
minRowHeightRef: Ref<number | undefined>
|
||||
heightForRowRef: Ref<DataTableHeightForRow | undefined>
|
||||
virtualScrollHeaderRef: Ref<boolean>
|
||||
headerHeightRef: Ref<number | undefined>
|
||||
bodyWidthRef: Ref<number | null>
|
||||
mergedTableLayoutRef: Ref<'auto' | 'fixed'>
|
||||
maxHeightRef: Ref<string | number | undefined>
|
||||
@ -502,6 +518,7 @@ export interface MainTableBodyRef {
|
||||
|
||||
export interface MainTableHeaderRef {
|
||||
$el: HTMLElement | null
|
||||
virtualListRef: Ref<VirtualListInst | null>
|
||||
}
|
||||
|
||||
export type OnFilterMenuChange = <
|
||||
|
@ -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);
|
||||
|
@ -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<TableColumn, RowItem>
|
||||
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user