mirror of
https://github.com/tusen-ai/naive-ui.git
synced 2025-03-01 13:36:55 +08:00
feat(data-table): expandable rows
This commit is contained in:
parent
02adb504a8
commit
0756513259
@ -1,5 +1,11 @@
|
||||
# CHANGELOG
|
||||
|
||||
## Pending
|
||||
|
||||
### Feats
|
||||
|
||||
- `n-data-table` supports expanding rows.
|
||||
|
||||
## 2.1.3
|
||||
|
||||
### Fixes
|
||||
|
@ -1,5 +1,11 @@
|
||||
# CHANGELOG
|
||||
|
||||
## Pending
|
||||
|
||||
### Feats
|
||||
|
||||
- `n-data-table` 支持行展开
|
||||
|
||||
## 2.1.3
|
||||
|
||||
### Fixes
|
||||
|
110
src/data-table/demos/enUS/expand.demo.md
Normal file
110
src/data-table/demos/enUS/expand.demo.md
Normal file
@ -0,0 +1,110 @@
|
||||
# Expand Rows
|
||||
|
||||
```html
|
||||
<n-data-table :columns="columns" :data="data" :pagination="pagination" />
|
||||
```
|
||||
|
||||
```js
|
||||
import { h, defineComponent } from 'vue'
|
||||
import { NTag, NButton, useMessage } from 'naive-ui'
|
||||
|
||||
const createColumns = ({ sendMail }) => {
|
||||
return [
|
||||
{
|
||||
type: 'expand',
|
||||
expandable: (_, index) => index !== 1,
|
||||
renderExpand: (rowData) => {
|
||||
return `${rowData.name} is a good guy.`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Name',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
title: 'Age',
|
||||
key: 'age'
|
||||
},
|
||||
{
|
||||
title: 'Address',
|
||||
key: 'address'
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
key: 'tags',
|
||||
render (row) {
|
||||
const tags = row.tags.map((tagKey) => {
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
style: {
|
||||
marginRight: '6px'
|
||||
},
|
||||
type: 'info'
|
||||
},
|
||||
{
|
||||
default: () => tagKey
|
||||
}
|
||||
)
|
||||
})
|
||||
return tags
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
key: 'actions',
|
||||
render (row) {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
onClick: () => sendMail(row)
|
||||
},
|
||||
{ default: () => 'Send Email' }
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const createData = () => [
|
||||
{
|
||||
key: 0,
|
||||
name: 'John Brown',
|
||||
age: 32,
|
||||
address: 'New York No. 1 Lake Park',
|
||||
tags: ['nice', 'developer']
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
name: 'Jim Green',
|
||||
age: 42,
|
||||
address: 'London No. 1 Lake Park',
|
||||
tags: ['loser']
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
name: 'Joe Black',
|
||||
age: 32,
|
||||
address: 'Sidney No. 1 Lake Park',
|
||||
tags: ['cool', 'teacher']
|
||||
}
|
||||
]
|
||||
|
||||
export default defineComponent({
|
||||
setup () {
|
||||
const message = useMessage()
|
||||
return {
|
||||
data: createData(),
|
||||
columns: createColumns({
|
||||
sendMail (rowData) {
|
||||
message.info('send mail to ' + rowData.name)
|
||||
}
|
||||
}),
|
||||
pagination: {
|
||||
pageSize: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
@ -26,6 +26,7 @@ fixed-header
|
||||
fixed-header-column
|
||||
ellipsis
|
||||
ellipsis-tooltip
|
||||
expand
|
||||
render-header
|
||||
custom-style
|
||||
ajax-usage
|
||||
@ -47,7 +48,7 @@ custom-filter-menu
|
||||
| min-height | `number \| string` | `undefined` | The min-height of the table. |
|
||||
| pagination | `false \| Object` | `false` | See [Pagination props](n-pagination#Props) |
|
||||
| paging | `boolean` | `true` | If data-table do automatic paging. You may set it to `false` in async usage. |
|
||||
| row-class-name | `string \| (rowData: Object, index : number) => string \| Object` | `undefined` | |
|
||||
| row-class-name | `string \| (rowData: Object, rowIndex : number) => string \| Object` | `undefined` | |
|
||||
| row-key | `(rowData: Object) => number \| string` | `undefined` | Generate the key of the row by row data (if you don't want to set the key) |
|
||||
| scroll-x | `number \| string` | `undefined` | If columns are horizontal fixed, scroll-x need to be set |
|
||||
| single-column | `boolean` | `false` | |
|
||||
@ -84,8 +85,9 @@ These methods can help you control table in an uncontrolled manner. However, it'
|
||||
| defaultFilterOptionValue | `string \| number \| null` | `null` | The default active filter option value in uncontrolled manner. (works when not using multiple filters) |
|
||||
| defaultFilterOptionValues | `Array<string \| number>` | `[]` | The default active filter option values in uncontrolled manner. (works when there are multiple filters) |
|
||||
| defaultSortOrder | `'descend' \| 'ascend' \| false` | `false` | The default sort order of the table in uncontrolled manner |
|
||||
| disabled | `(rowData: Object, index: number) => boolean` | `() => false` | |
|
||||
| disabled | `(rowData: Object, rowIndex: number) => boolean` | `() => false` | |
|
||||
| ellipsis | `boolean \| EllipsisProps` | `false` | |
|
||||
| expandable | `(rowData: Object, rowIndex: number) => boolean` | `undefined` | Whethe the row is expandable. Only works when `type` is `'expand'`. |
|
||||
| filter | `boolean \| (optionValue: string \| number, rowData: Object) => boolean \| 'default'` | `false` | The filter of the column. If set to `true`, it will only display filter button on the column, which can be used in async status. |
|
||||
| filterMode | `'and' \| 'or'` | `'or'` | |
|
||||
| filterMultiple | `boolean` | `true` | |
|
||||
@ -93,13 +95,14 @@ These methods can help you control table in an uncontrolled manner. However, it'
|
||||
| filterOptionValues | `Array<string \| number> \| null` | `undefined` | The active filter option values in controlled manner. If not set, the filter of the column works in an uncontrolled manner. (works when there are multiple filters) |
|
||||
| filterOptions | `Array<{ label: string, value: string \| number}>` | `undefined` | |
|
||||
| fixed | `'left \| 'right' \| false` | `false` | |
|
||||
| key | `string \| number` | **required** | Unique key of this column, **required** when table's row-key is not set. |
|
||||
| render | `(rowData: Object) => VNodeChild` | `undefined` | Render function of column row cell. |
|
||||
| key | `string \| number` | `undefined` | Unique key of this column, **required** when table's row-key is not set. |
|
||||
| render | `(rowData: Object, rowIndex: number) => VNodeChild` | `undefined` | Render function of column row cell. |
|
||||
| renderExpand | `(rowData: Object, rowIndex: number) => VNodeChild` | `undefined` | Render function of the expand area. Only works when `type` is `'expand'`. |
|
||||
| renderFilterMenu | `() => VNodeChild` | `undefined` | Render function of column filter menu. |
|
||||
| rowSpan | `(rowData: Object, rowIndex: number) => number` | `undefined` | |
|
||||
| sortOrder | `'descend' \| 'ascend' \| false` | `undefined` | The controlled sort order of the column. If multiple columns' sortOrder is set, the first one will affect. |
|
||||
| sorter | `boolean \| function \| 'default'` | `false` | The sorter of the column. If set `'default'`, it will use a basic builtin compare function. If set to `true`, it will only display sort icon on the column, which can be used in async status. Otherwise it works like `Array.sort`'s compare function. |
|
||||
| title | `string \| (() => VNodeChild)` | `undefined` | Can be a render function |
|
||||
| titleRowSpan | `number` | `undefined` | |
|
||||
| type | `'default' \| 'selection'` | `default` | |
|
||||
| type | `'selection' \| 'expand'` | `undefined` | |
|
||||
| width | `number \| string` | `undefined` | Width of the column, **required** when fixed |
|
||||
|
110
src/data-table/demos/zhCN/expand.demo.md
Normal file
110
src/data-table/demos/zhCN/expand.demo.md
Normal file
@ -0,0 +1,110 @@
|
||||
# 可展开
|
||||
|
||||
```html
|
||||
<n-data-table :columns="columns" :data="data" :pagination="pagination" />
|
||||
```
|
||||
|
||||
```js
|
||||
import { h, defineComponent } from 'vue'
|
||||
import { NTag, NButton, useMessage } from 'naive-ui'
|
||||
|
||||
const createColumns = ({ sendMail }) => {
|
||||
return [
|
||||
{
|
||||
type: 'expand',
|
||||
expandable: (_, index) => index !== 1,
|
||||
renderExpand: (rowData) => {
|
||||
return `${rowData.name} is a good guy.`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Name',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
title: 'Age',
|
||||
key: 'age'
|
||||
},
|
||||
{
|
||||
title: 'Address',
|
||||
key: 'address'
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
key: 'tags',
|
||||
render (row) {
|
||||
const tags = row.tags.map((tagKey) => {
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
style: {
|
||||
marginRight: '6px'
|
||||
},
|
||||
type: 'info'
|
||||
},
|
||||
{
|
||||
default: () => tagKey
|
||||
}
|
||||
)
|
||||
})
|
||||
return tags
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
key: 'actions',
|
||||
render (row) {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
onClick: () => sendMail(row)
|
||||
},
|
||||
{ default: () => 'Send Email' }
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const createData = () => [
|
||||
{
|
||||
key: 0,
|
||||
name: 'John Brown',
|
||||
age: 32,
|
||||
address: 'New York No. 1 Lake Park',
|
||||
tags: ['nice', 'developer']
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
name: 'Jim Green',
|
||||
age: 42,
|
||||
address: 'London No. 1 Lake Park',
|
||||
tags: ['loser']
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
name: 'Joe Black',
|
||||
age: 32,
|
||||
address: 'Sidney No. 1 Lake Park',
|
||||
tags: ['cool', 'teacher']
|
||||
}
|
||||
]
|
||||
|
||||
export default defineComponent({
|
||||
setup () {
|
||||
const message = useMessage()
|
||||
return {
|
||||
data: createData(),
|
||||
columns: createColumns({
|
||||
sendMail (rowData) {
|
||||
message.info('send mail to ' + rowData.name)
|
||||
}
|
||||
}),
|
||||
pagination: {
|
||||
pageSize: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
@ -26,6 +26,7 @@ fixed-header
|
||||
fixed-header-column
|
||||
ellipsis
|
||||
ellipsis-tooltip
|
||||
expand
|
||||
render-header
|
||||
custom-style
|
||||
ajax-usage
|
||||
@ -84,8 +85,9 @@ custom-filter-menu
|
||||
| defaultFilterOptionValue | `string \| number \| null` | `null` | 非受控状态下默认的过滤器选项值(过滤器单选时生效) |
|
||||
| defaultFilterOptionValues | `Array<string \| number>` | `[]` | 非受控状态下默认的过滤器选项值(过滤器多选时生效) |
|
||||
| defaultSortOrder | `'descend' \| 'ascend' \| false` | `false` | 非受控状态下表格默认的排序方式 |
|
||||
| disabled | `(rowData: Object, index: number) => boolean` | `undefined` | |
|
||||
| disabled | `(rowData: Object, rowIndex: number) => boolean` | `undefined` | |
|
||||
| ellipsis | `boolean \| EllipsisProps` | `false` | |
|
||||
| expandable | `(rowData: Object, rowIndex: number) => boolean` | `undefined` | 行是否可展开,仅在 `type` 为 `'expand'` 时生效 |
|
||||
| filter | `boolean \| (optionValue: string \| number, rowData: Object) => boolean \| 'default'` | `undefined` | 这一列的过滤方法。如果设为 `true`,表格将只会在这列展示一个排序图标,在异步的时候可能有用。 |
|
||||
| filterMode | `'and' \| 'or'` | `'or'` | |
|
||||
| filterMultiple | `boolean` | `true` | |
|
||||
@ -93,13 +95,14 @@ custom-filter-menu
|
||||
| filterOptionValues | `Array<string \| number> \| null` | `undefined` | 受控状态下,当前激活的过滤器选项值数组。如果不做设定,这一列的过滤行为将是非受控的(过滤器多选时生效) |
|
||||
| filterOptions | `Array<{ label: string, value: string \| number}>` | `undefined` | |
|
||||
| fixed | `'left \| 'right' \| false` | `false` | |
|
||||
| key | `string \| number` | **必须** | 这一列的 key,在表格未设定 row-key 的时候是**必须**的。 |
|
||||
| render | `(rowData: Object) => VNodeChild` | `undefined` | 渲染函数,渲染这一列的每一行的单元格 |
|
||||
| key | `string \| number` | `undefined` | 这一列的 key,在表格未设定 row-key 的时候是**必须**的。 |
|
||||
| render | `(rowData: Object, rowIndex: number) => VNodeChild` | `undefined` | 渲染函数,渲染这一列的每一行的单元格 |
|
||||
| renderExpand | `(rowData: Object, rowIndex: number) => VNodeChild` | `undefined` | 展开区域的渲染函数,仅在 `type` 为 `'expand'` 的时候生效 |
|
||||
| renderFilterMenu | `() => VNodeChild` | `undefined` | 渲染函数,渲染这一列的过滤器菜单 |
|
||||
| rowSpan | `(rowData: Object, rowIndex: number) => number` | `undefined` | |
|
||||
| sortOrder | `'descend' \| 'ascend' \| false` | `undefined` | 受控状态下表格的排序方式。如果多列都设定了有效值,那么只有第一个会生效 |
|
||||
| sorter | `boolean \| function \| 'default'` | `undefined` | 这一列的排序方法。如果设为 `'default'` 表格将会使用一个内置的排序函数;如果设为 `true`,表格将只会在这列展示一个排序图标,在异步的时候可能有用。其他情况下它工作的方式类似 `Array.sort` 的对比函数 |
|
||||
| title | `string \| (() => VNodeChild)` | `undefined` | 可以是渲染函数 |
|
||||
| titleRowSpan | `number` | `undefined` | |
|
||||
| type | `'selection'` | `undefined` | |
|
||||
| type | `'selection' \| 'expand'` | `undefined` | |
|
||||
| width | `number \| string` | `undefined` | 列的宽度,在列固定时是**必需**的 |
|
||||
|
@ -33,10 +33,12 @@ import {
|
||||
OnUpdateFilters,
|
||||
MainTableRef,
|
||||
DataTableInjection,
|
||||
DataTableRef
|
||||
DataTableRef,
|
||||
OnUpdateExpandedRowKeys
|
||||
} from './interface'
|
||||
import style from './styles/index.cssr'
|
||||
import { useGroupHeader } from './use-group-header'
|
||||
import { useExpand } from './use-expand'
|
||||
|
||||
export const dataTableProps = {
|
||||
...(useTheme.props as ThemeProps<DataTableTheme>),
|
||||
@ -90,6 +92,11 @@ export const dataTableProps = {
|
||||
default: 'medium'
|
||||
},
|
||||
remote: Boolean,
|
||||
defaultExpandedRowKeys: {
|
||||
type: Array as PropType<RowKey[]>,
|
||||
default: []
|
||||
},
|
||||
expandedRowKeys: Array as PropType<RowKey[]>,
|
||||
// eslint-disable-next-line vue/prop-name-casing
|
||||
'onUpdate:page': [Function, Array] as PropType<
|
||||
PaginationProps['onUpdate:page']
|
||||
@ -108,6 +115,12 @@ export const dataTableProps = {
|
||||
'onUpdate:checkedRowKeys': [Function, Array] as PropType<
|
||||
MaybeArray<OnUpdateCheckedRowKeys>
|
||||
>,
|
||||
'onUpdate:expandedRowKeys': [Function, Array] as PropType<
|
||||
MaybeArray<OnUpdateExpandedRowKeys>
|
||||
>,
|
||||
onUpdateExpandedRowKeys: [Function, Array] as PropType<
|
||||
MaybeArray<OnUpdateExpandedRowKeys>
|
||||
>,
|
||||
// deprecated
|
||||
onPageChange: {
|
||||
type: [Function, Array] as PropType<PaginationProps['onUpdate:page']>,
|
||||
@ -233,6 +246,11 @@ export default defineComponent({
|
||||
paginatedDataRef,
|
||||
treeMateRef
|
||||
})
|
||||
const {
|
||||
mergedExpandedRowKeys,
|
||||
renderExpand,
|
||||
doUpdateExpandedRowKeys
|
||||
} = useExpand(props)
|
||||
const {
|
||||
handleTableBodyScroll,
|
||||
handleTableHeaderScroll,
|
||||
@ -272,6 +290,7 @@ export default defineComponent({
|
||||
loading: toRef(props, 'loading'),
|
||||
rowClassName: toRef(props, 'rowClassName'),
|
||||
mergedCheckedRowKeys,
|
||||
mergedExpandedRowKeys,
|
||||
locale,
|
||||
rowKey: toRef(props, 'rowKey'),
|
||||
filterMenuCssVars: computed(() => {
|
||||
@ -285,6 +304,7 @@ export default defineComponent({
|
||||
'--action-divider-color': actionDividerColor
|
||||
} as CSSProperties
|
||||
}),
|
||||
renderExpand,
|
||||
deriveActiveRightFixedColumn,
|
||||
deriveActiveLeftFixedColumn,
|
||||
doUpdateFilters,
|
||||
@ -292,6 +312,7 @@ export default defineComponent({
|
||||
doUpdateCheckedRowKeys,
|
||||
doCheckAll,
|
||||
doUncheckAll,
|
||||
doUpdateExpandedRowKeys,
|
||||
handleTableHeaderScroll,
|
||||
handleTableBodyScroll
|
||||
})
|
||||
|
@ -3,9 +3,10 @@ import { pxfy } from 'seemly'
|
||||
import { NCheckbox } from '../../../checkbox'
|
||||
import { NScrollbar, ScrollbarRef } from '../../../scrollbar'
|
||||
import { formatLength } from '../../../_utils'
|
||||
import { DataTableInjection, TmNode } from '../interface'
|
||||
import { DataTableInjection, RowKey, TmNode } from '../interface'
|
||||
import { createRowClassName } from '../utils'
|
||||
import Cell from './Cell'
|
||||
import ExpandTrigger from './ExpandTrigger'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DataTableBody',
|
||||
@ -38,12 +39,24 @@ export default defineComponent({
|
||||
function handleScroll (event: Event): void {
|
||||
NDataTable.handleTableBodyScroll(event)
|
||||
}
|
||||
function handleUpdateExpanded (key: RowKey): void {
|
||||
const { mergedExpandedRowKeys, doUpdateExpandedRowKeys } = NDataTable
|
||||
const index = mergedExpandedRowKeys.indexOf(key)
|
||||
const nextExpandedKeys = Array.from(mergedExpandedRowKeys)
|
||||
if (~index) {
|
||||
nextExpandedKeys.splice(index, 1)
|
||||
} else {
|
||||
nextExpandedKeys.push(key)
|
||||
}
|
||||
doUpdateExpandedRowKeys(nextExpandedKeys)
|
||||
}
|
||||
return {
|
||||
NDataTable,
|
||||
scrollbarInstRef,
|
||||
getScrollContainer,
|
||||
handleScroll,
|
||||
handleCheckboxUpdateChecked
|
||||
handleCheckboxUpdateChecked,
|
||||
handleUpdateExpanded
|
||||
}
|
||||
},
|
||||
render () {
|
||||
@ -65,10 +78,167 @@ export default defineComponent({
|
||||
>
|
||||
{{
|
||||
default: () => {
|
||||
let hasExpandedRows = false
|
||||
const cordToPass: Record<number, number[]> = {}
|
||||
const { cols, paginatedData } = NDataTable
|
||||
const {
|
||||
cols,
|
||||
paginatedData,
|
||||
mergedTheme,
|
||||
fixedColumnLeftMap,
|
||||
fixedColumnRightMap,
|
||||
currentPage,
|
||||
mergedCheckedRowKeys,
|
||||
rowClassName,
|
||||
leftActiveFixedColKey,
|
||||
rightActiveFixedColKey,
|
||||
renderExpand,
|
||||
mergedExpandedRowKeys
|
||||
} = NDataTable
|
||||
const { length: colCount } = cols
|
||||
const { length: rowCount } = paginatedData
|
||||
const { handleCheckboxUpdateChecked, handleUpdateExpanded } = this
|
||||
const rows = paginatedData.map((tmNode, rowIndex) => {
|
||||
const { rawNode: rowData, key: rowKey } = tmNode
|
||||
const expanded =
|
||||
renderExpand && mergedExpandedRowKeys.includes(rowKey)
|
||||
const row = (
|
||||
<tr
|
||||
key={rowKey}
|
||||
class={[
|
||||
'n-data-table-tr',
|
||||
createRowClassName(rowData, rowIndex, rowClassName)
|
||||
]}
|
||||
>
|
||||
{cols.map((col, colIndex) => {
|
||||
if (rowIndex in cordToPass) {
|
||||
const cordOfRowToPass = cordToPass[rowIndex]
|
||||
const indexInCordOfRowToPass = cordOfRowToPass.indexOf(
|
||||
colIndex
|
||||
)
|
||||
if (~indexInCordOfRowToPass) {
|
||||
cordOfRowToPass.splice(indexInCordOfRowToPass, 1)
|
||||
return null
|
||||
}
|
||||
}
|
||||
const { key: colKey, column } = col
|
||||
const { rowSpan, colSpan } = column
|
||||
const mergedColSpan = colSpan
|
||||
? colSpan(rowData, rowIndex)
|
||||
: 1
|
||||
const mergedRowSpan = rowSpan
|
||||
? rowSpan(rowData, rowIndex)
|
||||
: 1
|
||||
const isLastCol = colIndex + mergedColSpan === colCount
|
||||
const isLastRow = rowIndex + mergedRowSpan === rowCount
|
||||
if (mergedColSpan > 1 || mergedRowSpan > 1) {
|
||||
for (
|
||||
let i = rowIndex;
|
||||
i < rowIndex + mergedRowSpan;
|
||||
++i
|
||||
) {
|
||||
for (
|
||||
let j = colIndex;
|
||||
j < colIndex + mergedColSpan;
|
||||
++j
|
||||
) {
|
||||
if (i === rowIndex && j === colIndex) continue
|
||||
if (!(i in cordToPass)) {
|
||||
cordToPass[i] = [j]
|
||||
} else {
|
||||
cordToPass[i].push(j)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<td
|
||||
key={colKey}
|
||||
style={{
|
||||
textAlign: column.align || undefined,
|
||||
left: pxfy(fixedColumnLeftMap[colKey]),
|
||||
right: pxfy(fixedColumnRightMap[colKey])
|
||||
}}
|
||||
colspan={mergedColSpan}
|
||||
rowspan={mergedRowSpan}
|
||||
class={[
|
||||
'n-data-table-td',
|
||||
column.className,
|
||||
column.fixed &&
|
||||
`n-data-table-td--fixed-${column.fixed}`,
|
||||
column.align &&
|
||||
`n-data-table-td--${column.align}-align`,
|
||||
{
|
||||
'n-data-table-td--ellipsis':
|
||||
column.ellipsis === true ||
|
||||
// don't add ellpisis class if tooltip exists
|
||||
(column.ellipsis && !column.ellipsis.tooltip),
|
||||
'n-data-table-td--shadow-after':
|
||||
leftActiveFixedColKey === colKey,
|
||||
'n-data-table-td--shadow-before':
|
||||
rightActiveFixedColKey === colKey,
|
||||
'n-data-table-td--selection':
|
||||
column.type === 'selection',
|
||||
'n-data-table-td--expand': column.type === 'expand',
|
||||
'n-data-table-td--last-col': isLastCol,
|
||||
'n-data-table-td--last-row': isLastRow && !expanded
|
||||
}
|
||||
]}
|
||||
>
|
||||
{column.type === 'selection' ? (
|
||||
<NCheckbox
|
||||
key={currentPage}
|
||||
disabled={column.disabled?.(rowData)}
|
||||
checked={mergedCheckedRowKeys.includes(rowKey)}
|
||||
onUpdateChecked={(checked) =>
|
||||
handleCheckboxUpdateChecked(tmNode, checked)
|
||||
}
|
||||
/>
|
||||
) : column.type === 'expand' ? (
|
||||
!column.expandable ||
|
||||
column.expandable?.(rowData, rowIndex) ? (
|
||||
<ExpandTrigger
|
||||
expanded={expanded}
|
||||
onClick={() => handleUpdateExpanded(rowKey)}
|
||||
/>
|
||||
) : null
|
||||
) : (
|
||||
<Cell
|
||||
index={rowIndex}
|
||||
row={rowData}
|
||||
column={column}
|
||||
mergedTheme={mergedTheme}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
if (expanded && renderExpand) {
|
||||
if (!hasExpandedRows) {
|
||||
hasExpandedRows = true
|
||||
}
|
||||
return [
|
||||
row,
|
||||
<tr class="n-data-table-tr" key={`${rowKey}__expand`}>
|
||||
<td
|
||||
class={[
|
||||
'n-data-table-td',
|
||||
'n-data-table-td--last-col',
|
||||
{
|
||||
'n-data-table-td--last-row': rowIndex + 1 === rowCount
|
||||
}
|
||||
]}
|
||||
colspan={colCount}
|
||||
>
|
||||
{renderExpand(rowData, rowIndex)}
|
||||
</td>
|
||||
</tr>
|
||||
]
|
||||
}
|
||||
return row
|
||||
})
|
||||
|
||||
return (
|
||||
<table ref="body" class="n-data-table-table">
|
||||
<colgroup>
|
||||
@ -77,130 +247,7 @@ export default defineComponent({
|
||||
))}
|
||||
</colgroup>
|
||||
<tbody ref="tbody" class="n-data-table-tbody">
|
||||
{paginatedData.map((tmNode, rowIndex) => {
|
||||
const { rawNode: row } = tmNode
|
||||
const { handleCheckboxUpdateChecked } = this
|
||||
const {
|
||||
mergedTheme,
|
||||
cols,
|
||||
fixedColumnLeftMap,
|
||||
fixedColumnRightMap,
|
||||
currentPage,
|
||||
mergedCheckedRowKeys,
|
||||
rowClassName,
|
||||
leftActiveFixedColKey,
|
||||
rightActiveFixedColKey
|
||||
} = NDataTable
|
||||
return (
|
||||
<tr
|
||||
key={tmNode.key}
|
||||
class={[
|
||||
'n-data-table-tr',
|
||||
createRowClassName(row, rowIndex, rowClassName)
|
||||
]}
|
||||
>
|
||||
{cols.map((col, colIndex) => {
|
||||
if (rowIndex in cordToPass) {
|
||||
const cordOfRowToPass = cordToPass[rowIndex]
|
||||
const indexInCordOfRowToPass = cordOfRowToPass.indexOf(
|
||||
colIndex
|
||||
)
|
||||
if (~indexInCordOfRowToPass) {
|
||||
cordOfRowToPass.splice(indexInCordOfRowToPass, 1)
|
||||
return null
|
||||
}
|
||||
}
|
||||
const { key, column } = col
|
||||
const { rowSpan, colSpan } = column
|
||||
const mergedColSpan = colSpan
|
||||
? colSpan(row, rowIndex)
|
||||
: 1
|
||||
const mergedRowSpan = rowSpan
|
||||
? rowSpan(row, rowIndex)
|
||||
: 1
|
||||
const isLastCol =
|
||||
colIndex + mergedColSpan === colCount
|
||||
const isLastRow =
|
||||
rowIndex + mergedRowSpan === rowCount
|
||||
if (mergedColSpan > 1 || mergedRowSpan > 1) {
|
||||
for (
|
||||
let i = rowIndex;
|
||||
i < rowIndex + mergedRowSpan;
|
||||
++i
|
||||
) {
|
||||
for (
|
||||
let j = colIndex;
|
||||
j < colIndex + mergedColSpan;
|
||||
++j
|
||||
) {
|
||||
if (i === rowIndex && j === colIndex) continue
|
||||
if (!(i in cordToPass)) {
|
||||
cordToPass[i] = [j]
|
||||
} else {
|
||||
cordToPass[i].push(j)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<td
|
||||
key={key}
|
||||
style={{
|
||||
textAlign: column.align || undefined,
|
||||
left: pxfy(fixedColumnLeftMap[key]),
|
||||
right: pxfy(fixedColumnRightMap[key])
|
||||
}}
|
||||
colspan={mergedColSpan}
|
||||
rowspan={mergedRowSpan}
|
||||
class={[
|
||||
'n-data-table-td',
|
||||
column.className,
|
||||
column.fixed &&
|
||||
`n-data-table-td--fixed-${column.fixed}`,
|
||||
column.align &&
|
||||
`n-data-table-td--${column.align}-align`,
|
||||
{
|
||||
'n-data-table-td--ellipsis':
|
||||
column.ellipsis === true ||
|
||||
// don't add ellpisis class if tooltip exists
|
||||
(column.ellipsis &&
|
||||
!column.ellipsis.tooltip),
|
||||
'n-data-table-td--shadow-after':
|
||||
leftActiveFixedColKey === key,
|
||||
'n-data-table-td--shadow-before':
|
||||
rightActiveFixedColKey === key,
|
||||
'n-data-table-td--selection':
|
||||
column.type === 'selection',
|
||||
'n-data-table-td--last-col': isLastCol,
|
||||
'n-data-table-td--last-row': isLastRow
|
||||
}
|
||||
]}
|
||||
>
|
||||
{column.type === 'selection' ? (
|
||||
<NCheckbox
|
||||
key={currentPage}
|
||||
disabled={column.disabled?.(row)}
|
||||
checked={mergedCheckedRowKeys.includes(
|
||||
tmNode.key
|
||||
)}
|
||||
onUpdateChecked={(checked) =>
|
||||
handleCheckboxUpdateChecked(tmNode, checked)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Cell
|
||||
index={rowIndex}
|
||||
row={row}
|
||||
column={column}
|
||||
mergedTheme={mergedTheme}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
{hasExpandedRows ? rows.flat() : rows}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
|
33
src/data-table/src/TableParts/ExpandTrigger.tsx
Normal file
33
src/data-table/src/TableParts/ExpandTrigger.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { h, defineComponent, PropType, CSSProperties } from 'vue'
|
||||
import { ChevronRightIcon } from '../../../_internal/icons'
|
||||
import { NBaseIcon } from '../../../_internal'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DataTableExpandTrigger',
|
||||
props: {
|
||||
expanded: Boolean,
|
||||
onClick: {
|
||||
type: Function as PropType<() => void>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
render () {
|
||||
const style: CSSProperties = {
|
||||
cursor: 'pointer',
|
||||
fontSize: '16px'
|
||||
}
|
||||
return (
|
||||
<NBaseIcon onClick={this.onClick} style={style}>
|
||||
{{
|
||||
default: () => {
|
||||
return (
|
||||
<ChevronRightIcon
|
||||
style={this.expanded ? 'transform: rotate(90deg);' : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}}
|
||||
</NBaseIcon>
|
||||
)
|
||||
}
|
||||
})
|
@ -13,14 +13,17 @@ import {
|
||||
} from '../utils'
|
||||
import {
|
||||
DataTableInjection,
|
||||
ExpandColInfo,
|
||||
SelectionColInfo,
|
||||
TableColumnGroup,
|
||||
TableColumnInfo
|
||||
} from '../interface'
|
||||
|
||||
function renderTitle (column: TableColumnInfo | TableColumnGroup): VNodeChild {
|
||||
function renderTitle (
|
||||
column: ExpandColInfo | TableColumnInfo | TableColumnGroup
|
||||
): VNodeChild {
|
||||
return typeof column.title === 'function'
|
||||
? column.title(column)
|
||||
? column.title(column as any)
|
||||
: column.title
|
||||
}
|
||||
|
||||
@ -131,10 +134,15 @@ export default defineComponent({
|
||||
},
|
||||
column.className
|
||||
]}
|
||||
onClick={(e) => {
|
||||
onClick={
|
||||
column.type !== 'selection' &&
|
||||
handleColHeaderClick(e, column)
|
||||
}}
|
||||
column.type !== 'expand' &&
|
||||
!('children' in column)
|
||||
? (e) => {
|
||||
handleColHeaderClick(e, column)
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{column.type === 'selection' ? (
|
||||
<NCheckbox
|
||||
@ -162,10 +170,8 @@ export default defineComponent({
|
||||
default: () => renderTitle(column)
|
||||
}}
|
||||
</NEllipsis>
|
||||
) : typeof column.title === 'function' ? (
|
||||
column.title(column)
|
||||
) : (
|
||||
column.title
|
||||
renderTitle(column)
|
||||
)}
|
||||
{isColumnSortable(column) ? (
|
||||
<SortButton column={column as TableColumnInfo} />
|
||||
|
@ -50,8 +50,14 @@ export type TableColumnTitle =
|
||||
| string
|
||||
| ((column: TableColumnInfo) => VNodeChild)
|
||||
|
||||
export type ExpandColTitle = string | ((column: ExpandColInfo) => VNodeChild)
|
||||
|
||||
export type TableColumnGroupTitle =
|
||||
| string
|
||||
| ((column: TableColumnGroup) => VNodeChild)
|
||||
|
||||
export type TableColumnGroup = {
|
||||
title?: TableColumnTitle
|
||||
title?: TableColumnGroupTitle
|
||||
type?: never
|
||||
key: ColumnKey
|
||||
children: TableColumnInfo[]
|
||||
@ -81,12 +87,12 @@ export type TableColumnInfo = {
|
||||
defaultFilterOptionValue?: FilterOptionValue | null
|
||||
filterMultiple?: boolean
|
||||
|
||||
render?: (data: TableNode, index: number) => VNodeChild
|
||||
render?: (rowData: TableNode, rowIndex: number) => VNodeChild
|
||||
renderFilterMenu?: FilterMenuRender
|
||||
renderSorter?: SorterRender
|
||||
renderFilter?: FilterRender
|
||||
colSpan?: (data: TableNode, index: number) => number
|
||||
rowSpan?: (data: TableNode, index: number) => number
|
||||
colSpan?: (rowData: TableNode, rowIndex: number) => number
|
||||
rowSpan?: (rowData: TableNode, rowIndex: number) => number
|
||||
} & CommonColInfo
|
||||
|
||||
export type SelectionColInfo = {
|
||||
@ -103,7 +109,20 @@ export type SelectionColInfo = {
|
||||
rowSpan?: never
|
||||
} & CommonColInfo
|
||||
|
||||
export type TableColumn = TableColumnGroup | TableColumnInfo | SelectionColInfo
|
||||
export type RenderExpand = (row: TableNode, index: number) => VNodeChild
|
||||
export type Expandable = (row: TableNode, index: number) => boolean
|
||||
export interface ExpandColInfo extends Omit<SelectionColInfo, 'type'> {
|
||||
type: 'expand'
|
||||
title?: ExpandColTitle
|
||||
renderExpand: RenderExpand
|
||||
expandable?: Expandable
|
||||
}
|
||||
|
||||
export type TableColumn =
|
||||
| TableColumnGroup
|
||||
| TableColumnInfo
|
||||
| SelectionColInfo
|
||||
| ExpandColInfo
|
||||
export type TableColumns = TableColumn[]
|
||||
|
||||
export interface DataTableInjection {
|
||||
@ -129,6 +148,9 @@ export interface DataTableInjection {
|
||||
mergedCheckedRowKeys: RowKey[]
|
||||
locale: NLocale['DataTable']
|
||||
filterMenuCssVars: CSSProperties
|
||||
mergedExpandedRowKeys: RowKey[]
|
||||
renderExpand: undefined | RenderExpand
|
||||
doUpdateExpandedRowKeys: (keys: RowKey[]) => void
|
||||
rowKey: CreateRowKey | undefined
|
||||
doUpdateFilters: (
|
||||
filters: FilterState,
|
||||
@ -161,6 +183,7 @@ export type SorterRender = (props: { order: SortOrder | false }) => VNodeChild
|
||||
|
||||
export type FilterMenuRender = () => VNodeChild
|
||||
|
||||
export type OnUpdateExpandedRowKeys = (keys: RowKey[]) => void
|
||||
export type OnUpdateCheckedRowKeys = (keys: RowKey[]) => void
|
||||
export type OnUpdateSorter = (sortState: SortState | null) => void
|
||||
export type OnUpdateFilters = (
|
||||
|
@ -246,7 +246,7 @@ export default c([
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
`),
|
||||
cM('selection', `
|
||||
cM('selection, expand', `
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
line-height: 0;
|
||||
|
46
src/data-table/src/use-expand.ts
Normal file
46
src/data-table/src/use-expand.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { toRef, ref } from 'vue'
|
||||
import { useMemo, useMergedState } from 'vooks'
|
||||
import type { DataTableProps } from './DataTable'
|
||||
import { RowKey } from './interface'
|
||||
import { call, warn } from '../../_utils'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export function useExpand (props: DataTableProps) {
|
||||
const renderExpandRef = useMemo(() => {
|
||||
for (const col of props.columns) {
|
||||
if (col.type === 'expand') {
|
||||
if (__DEV__ && !col.renderExpand) {
|
||||
warn(
|
||||
'data-table',
|
||||
'column with type `expand` has no `renderExpand` prop.'
|
||||
)
|
||||
}
|
||||
return col.renderExpand
|
||||
}
|
||||
}
|
||||
})
|
||||
const uncontrolledExpandedRowKeysRef = ref(props.defaultExpandedRowKeys)
|
||||
const controlledExpandedRowKeysRef = toRef(props, 'expandedRowKeys')
|
||||
const mergedExpandedRowKeys = useMergedState(
|
||||
controlledExpandedRowKeysRef,
|
||||
uncontrolledExpandedRowKeysRef
|
||||
)
|
||||
function doUpdateExpandedRowKeys (expandedKeys: RowKey[]): void {
|
||||
const {
|
||||
onUpdateExpandedRowKeys,
|
||||
'onUpdate:expandedRowKeys': _onUpdateExpandedRowKeys
|
||||
} = props
|
||||
if (onUpdateExpandedRowKeys) {
|
||||
call(onUpdateExpandedRowKeys, expandedKeys)
|
||||
}
|
||||
if (_onUpdateExpandedRowKeys) {
|
||||
call(_onUpdateExpandedRowKeys, expandedKeys)
|
||||
}
|
||||
uncontrolledExpandedRowKeysRef.value = expandedKeys
|
||||
}
|
||||
return {
|
||||
mergedExpandedRowKeys: mergedExpandedRowKeys,
|
||||
renderExpand: renderExpandRef,
|
||||
doUpdateExpandedRowKeys
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { CSSProperties, ComputedRef, computed } from 'vue'
|
||||
import { DataTableProps } from './DataTable'
|
||||
import type {
|
||||
ExpandColInfo,
|
||||
SelectionColInfo,
|
||||
TableColumn,
|
||||
TableColumnInfo,
|
||||
@ -17,7 +18,7 @@ export interface RowItem {
|
||||
export interface ColItem {
|
||||
key: string | number
|
||||
style: CSSProperties
|
||||
column: SelectionColInfo | TableColumnInfo
|
||||
column: SelectionColInfo | ExpandColInfo | TableColumnInfo
|
||||
}
|
||||
|
||||
type RowItemMap = WeakMap<TableColumn, RowItem>
|
||||
@ -26,11 +27,13 @@ function getRowsAndCols (
|
||||
): {
|
||||
rows: RowItem[][]
|
||||
cols: ColItem[]
|
||||
dataRelatedCols: Array<SelectionColInfo | TableColumnInfo>
|
||||
dataRelatedCols: Array<SelectionColInfo | TableColumnInfo | ExpandColInfo>
|
||||
} {
|
||||
const rows: RowItem[][] = []
|
||||
const cols: ColItem[] = []
|
||||
const dataRelatedCols: Array<SelectionColInfo | TableColumnInfo> = []
|
||||
const dataRelatedCols: Array<
|
||||
SelectionColInfo | TableColumnInfo | ExpandColInfo
|
||||
> = []
|
||||
const rowItemMap: RowItemMap = new WeakMap()
|
||||
let maxDepth = -1
|
||||
let totalRowSpan = 0
|
||||
@ -116,7 +119,9 @@ export function useGroupHeader (
|
||||
): {
|
||||
rows: ComputedRef<RowItem[][]>
|
||||
cols: ComputedRef<ColItem[]>
|
||||
dataRelatedCols: ComputedRef<Array<SelectionColInfo | TableColumnInfo>>
|
||||
dataRelatedCols: ComputedRef<
|
||||
Array<SelectionColInfo | TableColumnInfo | ExpandColInfo>
|
||||
>
|
||||
} {
|
||||
const rowsAndCols = computed(() => getRowsAndCols(props.columns))
|
||||
return {
|
||||
|
@ -12,7 +12,8 @@ import type {
|
||||
TableColumnInfo,
|
||||
SelectionColInfo,
|
||||
TableNode,
|
||||
TmNode
|
||||
TmNode,
|
||||
ExpandColInfo
|
||||
} from './interface'
|
||||
import { createShallowClonedObject, getFlagOfOrder } from './utils'
|
||||
import { PaginationProps } from '../../pagination/src/Pagination'
|
||||
@ -26,7 +27,9 @@ export function useTableData (
|
||||
{
|
||||
dataRelatedCols
|
||||
}: {
|
||||
dataRelatedCols: ComputedRef<Array<SelectionColInfo | TableColumnInfo>>
|
||||
dataRelatedCols: ComputedRef<
|
||||
Array<SelectionColInfo | TableColumnInfo | ExpandColInfo>
|
||||
>
|
||||
}
|
||||
) {
|
||||
const treeMateRef = computed(() =>
|
||||
@ -138,7 +141,7 @@ export function useTableData (
|
||||
)
|
||||
const controlledFilterState: FilterState = {}
|
||||
columnsWithControlledFilter.forEach((column) => {
|
||||
if (column.type === 'selection') return
|
||||
if (column.type === 'selection' || column.type === 'expand') return
|
||||
controlledFilterState[column.key] =
|
||||
column.filterOptionValues || column.filterOptionValue || null
|
||||
})
|
||||
@ -161,7 +164,13 @@ export function useTableData (
|
||||
} = treeMateRef
|
||||
const columnEntries: Array<[ColumnKey, TableColumnInfo]> = []
|
||||
columns.forEach((column) => {
|
||||
if (column.type === 'selection') return
|
||||
if (
|
||||
column.type === 'selection' ||
|
||||
column.type === 'expand' ||
|
||||
'children' in column
|
||||
) {
|
||||
return
|
||||
}
|
||||
columnEntries.push([column.key, column])
|
||||
})
|
||||
return data
|
||||
@ -326,7 +335,10 @@ export function useTableData (
|
||||
clearSorter()
|
||||
} else {
|
||||
const columnToSort = dataRelatedCols.value.find(
|
||||
(column) => column.type !== 'selection' && column.key === columnKey
|
||||
(column) =>
|
||||
column.type !== 'selection' &&
|
||||
column.type !== 'expand' &&
|
||||
column.key === columnKey
|
||||
)
|
||||
if (!columnToSort || !columnToSort.sorter) return
|
||||
const sorter = columnToSort.sorter
|
||||
|
@ -8,23 +8,23 @@ import type {
|
||||
SortState,
|
||||
CreateRowClassName,
|
||||
SelectionColInfo,
|
||||
TableColumnGroup,
|
||||
TableColumn
|
||||
TableColumn,
|
||||
ExpandColInfo
|
||||
} from './interface'
|
||||
|
||||
export const selectionColWidth = 40
|
||||
export const expandColWidth = 40
|
||||
|
||||
export function getColWidth (
|
||||
col: TableColumnInfo | SelectionColInfo
|
||||
): number | undefined {
|
||||
export function getColWidth (col: TableColumn): number | undefined {
|
||||
if (col.type === 'selection') return selectionColWidth
|
||||
if (col.type === 'expand') return expandColWidth
|
||||
if ('children' in col) return undefined
|
||||
return col.width
|
||||
}
|
||||
|
||||
export function getColKey (
|
||||
col: TableColumnInfo | SelectionColInfo | TableColumnGroup
|
||||
): string | number {
|
||||
export function getColKey (col: TableColumn): string | number {
|
||||
if (col.type === 'selection') return '__n_selection__'
|
||||
if (col.type === 'expand') return '__n_expand__'
|
||||
return col.key
|
||||
}
|
||||
|
||||
@ -43,7 +43,7 @@ export function getFlagOfOrder (order: SortOrder): SortOrderFlag {
|
||||
}
|
||||
|
||||
export function createCustomWidthStyle (
|
||||
column: TableColumnInfo | SelectionColInfo
|
||||
column: TableColumnInfo | SelectionColInfo | ExpandColInfo
|
||||
): CSSProperties {
|
||||
return {
|
||||
width: pxfy(getColWidth(column))
|
||||
|
Loading…
Reference in New Issue
Block a user