mirror of
https://github.com/tusen-ai/naive-ui.git
synced 2025-03-07 13:48:31 +08:00
feat(data-table): multiple column sorting (#1035)
* feat(data-table): multiple sorter * fix(data-table): cell color while sorting * feat(data-table): multiple sort finished * feat: add test * feat(data-table): sort uncontrolled test * feat(data-table): finish test * feat(data-table): add controlled mutiple sort * chore(data-table): clean code * optimization * optimization * optimization * optimization * optimization Co-authored-by: Jiwen Bai <56228105@qq.com> Co-authored-by: 07akioni <07akioni2@gmail.com>
This commit is contained in:
parent
f19822bdd1
commit
8fcf0558f6
130
src/data-table/demos/zhCN/controlled-multiple-sorter.demo.md
Normal file
130
src/data-table/demos/zhCN/controlled-multiple-sorter.demo.md
Normal file
@ -0,0 +1,130 @@
|
||||
# 受控的多列排序
|
||||
|
||||
如果列对象的 sortOrder 属性被设为 'ascend'、'descend' 或者 false,表格的排序将为受控状态。
|
||||
|
||||
```html
|
||||
<n-space vertical :size="12">
|
||||
<n-data-table
|
||||
ref="table"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:pagination="pagination"
|
||||
@update:sorter="handleUpdateSorter"
|
||||
/>
|
||||
</n-space>
|
||||
```
|
||||
|
||||
```js
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const data = [
|
||||
{
|
||||
key: 0,
|
||||
name: 'John Brown',
|
||||
age: 32,
|
||||
address: 'New York No. 1 Lake Park',
|
||||
chinese: 98,
|
||||
math: 60,
|
||||
english: 70
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
name: 'Jim Green',
|
||||
age: 42,
|
||||
address: 'London No. 1 Lake Park',
|
||||
chinese: 98,
|
||||
math: 66,
|
||||
english: 89
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
name: 'Joe Black',
|
||||
age: 32,
|
||||
address: 'Sidney No. 1 Lake Park',
|
||||
chinese: 98,
|
||||
math: 66,
|
||||
english: 89
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
name: 'Jim Red',
|
||||
age: 32,
|
||||
address: 'London No. 2 Lake Park',
|
||||
chinese: 88,
|
||||
math: 99,
|
||||
english: 89
|
||||
}
|
||||
]
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
data: data,
|
||||
pagination: { pageSize: 5 }
|
||||
}
|
||||
},
|
||||
setup () {
|
||||
const sortStatesRef = ref([])
|
||||
const sortKeyMapOrderRef = computed(() =>
|
||||
sortStatesRef.value.reduce((result, { columnKey, order }) => {
|
||||
result[columnKey] = order
|
||||
return result
|
||||
}, {})
|
||||
)
|
||||
const paginationRef = ref({ pageSize: 5 })
|
||||
|
||||
const columnsRef = computed(() => [
|
||||
{
|
||||
title: 'Name',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
title: 'Age',
|
||||
key: 'age',
|
||||
sortOrder: sortKeyMapOrderRef.value.age || false,
|
||||
sorter (rowA, rowB) {
|
||||
return rowA.age - rowB.age
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Chinese Score',
|
||||
key: 'chinese',
|
||||
sortOrder: sortKeyMapOrderRef.value.chinese || false,
|
||||
sorter: {
|
||||
compare: (a, b) => a.chinese - b.chinese,
|
||||
multiple: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Math Score',
|
||||
key: 'math',
|
||||
sortOrder: sortKeyMapOrderRef.value.math || false,
|
||||
sorter: {
|
||||
compare: (a, b) => a.math - b.math,
|
||||
multiple: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'English Score',
|
||||
sortOrder: sortKeyMapOrderRef.value.english || false,
|
||||
key: 'english',
|
||||
sorter: {
|
||||
compare: (a, b) => a.english - b.english,
|
||||
multiple: 1
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
function handleUpdateSorter (sorters) {
|
||||
console.log(sorters)
|
||||
sortStatesRef.value = [].concat(sorters)
|
||||
}
|
||||
return {
|
||||
columns: columnsRef,
|
||||
handleUpdateSorter,
|
||||
data,
|
||||
pagination: paginationRef
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
@ -19,12 +19,14 @@ size
|
||||
row-props
|
||||
merge-cell
|
||||
filter-and-sorter
|
||||
multiple-sorter
|
||||
select
|
||||
custom-select
|
||||
group-header
|
||||
controlled-page
|
||||
controlled-filter
|
||||
controlled-sorter
|
||||
controlled-multiple-sorter
|
||||
fixed-header
|
||||
fixed-header-column
|
||||
summary
|
||||
|
143
src/data-table/demos/zhCN/multiple-sorter.demo.md
Normal file
143
src/data-table/demos/zhCN/multiple-sorter.demo.md
Normal file
@ -0,0 +1,143 @@
|
||||
# 多列排序
|
||||
|
||||
如果仅想使用多列排序的 UI,`sorter` 不传 `compare` 函数即可。
|
||||
|
||||
```html
|
||||
<n-space vertical :size="12">
|
||||
<n-space>
|
||||
<n-button @click="sortName">Sort By Name (Ascend)</n-button>
|
||||
<n-button @click="filterAddress">Filter Address (London)</n-button>
|
||||
<n-button @click="clearFilters">Clear Filters</n-button>
|
||||
<n-button @click="clearSorter">Clear Sorter</n-button>
|
||||
</n-space>
|
||||
<n-data-table
|
||||
ref="table"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:pagination="pagination"
|
||||
/>
|
||||
</n-space>
|
||||
```
|
||||
|
||||
```js
|
||||
const columns = [
|
||||
{
|
||||
title: 'Name',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
title: 'Age',
|
||||
key: 'age',
|
||||
sorter: (row1, row2) => row1.age - row2.age
|
||||
},
|
||||
{
|
||||
title: 'Chinese Score',
|
||||
key: 'chinese',
|
||||
defaultSortOrder: false,
|
||||
sorter: {
|
||||
compare: (a, b) => a.chinese - b.chinese,
|
||||
multiple: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Math Score',
|
||||
defaultSortOrder: false,
|
||||
key: 'math',
|
||||
sorter: {
|
||||
compare: (a, b) => a.math - b.math,
|
||||
multiple: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'English Score',
|
||||
defaultSortOrder: false,
|
||||
key: 'english',
|
||||
sorter: {
|
||||
compare: (a, b) => a.english - b.english,
|
||||
multiple: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Address',
|
||||
key: 'address',
|
||||
filterOptions: [
|
||||
{
|
||||
label: 'London',
|
||||
value: 'London'
|
||||
},
|
||||
{
|
||||
label: 'New York',
|
||||
value: 'New York'
|
||||
}
|
||||
],
|
||||
filter (value, row) {
|
||||
return ~row.address.indexOf(value)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const data = [
|
||||
{
|
||||
key: 0,
|
||||
name: 'John Brown',
|
||||
age: 32,
|
||||
address: 'New York No. 1 Lake Park',
|
||||
chinese: 98,
|
||||
math: 60,
|
||||
english: 70
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
name: 'Jim Green',
|
||||
age: 42,
|
||||
address: 'London No. 1 Lake Park',
|
||||
chinese: 98,
|
||||
math: 66,
|
||||
english: 89
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
name: 'Joe Black',
|
||||
age: 32,
|
||||
address: 'Sidney No. 1 Lake Park',
|
||||
chinese: 98,
|
||||
math: 66,
|
||||
english: 89
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
name: 'Jim Red',
|
||||
age: 32,
|
||||
address: 'London No. 2 Lake Park',
|
||||
chinese: 88,
|
||||
math: 99,
|
||||
english: 89
|
||||
}
|
||||
]
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
data: data,
|
||||
columns,
|
||||
pagination: { pageSize: 5 }
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
filterAddress () {
|
||||
this.$refs.table.filter({
|
||||
address: ['London']
|
||||
})
|
||||
},
|
||||
sortName () {
|
||||
this.$refs.table.sort('name', 'ascend')
|
||||
},
|
||||
clearFilters () {
|
||||
this.$refs.table.filter(null)
|
||||
},
|
||||
clearSorter () {
|
||||
this.$refs.table.sort(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
@ -19,15 +19,19 @@ export default defineComponent({
|
||||
const { mergedSortStateRef, mergedClsPrefixRef } = inject(
|
||||
dataTableInjectionKey
|
||||
)!
|
||||
const sortStateRef = mergedSortStateRef
|
||||
const sortStateRef = computed(() =>
|
||||
mergedSortStateRef.value.find(
|
||||
(state) => state.columnKey === props.column.key
|
||||
)
|
||||
)
|
||||
|
||||
const activeRef = computed(() => {
|
||||
const { value } = sortStateRef
|
||||
if (value) return value.columnKey === props.column.key
|
||||
return false
|
||||
return sortStateRef.value !== undefined
|
||||
})
|
||||
const mergedSortOrderRef = computed(() => {
|
||||
const { value } = sortStateRef
|
||||
if (value) return activeRef.value ? value.order : false
|
||||
if (sortStateRef.value && activeRef.value) {
|
||||
return sortStateRef.value.order
|
||||
}
|
||||
return false
|
||||
})
|
||||
const mergedRenderSorterRef = computed(() => {
|
||||
|
@ -22,7 +22,7 @@ import {
|
||||
MainTableBodyRef,
|
||||
TmNode
|
||||
} from '../interface'
|
||||
import { createRowClassName, getColKey } from '../utils'
|
||||
import { createRowClassName, getColKey, isColumnSorting } from '../utils'
|
||||
import Cell from './Cell'
|
||||
import ExpandTrigger from './ExpandTrigger'
|
||||
import RenderSafeCheckbox from './BodyCheckbox'
|
||||
@ -384,10 +384,6 @@ export default defineComponent({
|
||||
paginatedData.forEach((tmNode, rowIndex) => {
|
||||
rowIndexToKey[rowIndex] = tmNode.key
|
||||
})
|
||||
const sorterKey =
|
||||
!!mergedSortState &&
|
||||
mergedSortState.order &&
|
||||
mergedSortState.columnKey
|
||||
|
||||
let mergedData: RowRenderInfo[]
|
||||
|
||||
@ -503,7 +499,7 @@ export default defineComponent({
|
||||
isSummary && `${mergedClsPrefix}-data-table-td--summary`,
|
||||
((hoverKey !== null &&
|
||||
cordKey[rowIndex][colIndex].includes(hoverKey)) ||
|
||||
(sorterKey !== false && sorterKey === colKey)) &&
|
||||
isColumnSorting(column, mergedSortState)) &&
|
||||
`${mergedClsPrefix}-data-table-td--hover`,
|
||||
column.fixed &&
|
||||
`${mergedClsPrefix}-data-table-td--fixed-${column.fixed}`,
|
||||
|
@ -9,7 +9,8 @@ import {
|
||||
isColumnSortable,
|
||||
isColumnFilterable,
|
||||
createNextSorter,
|
||||
getColKey
|
||||
getColKey,
|
||||
isColumnSorting
|
||||
} from '../utils'
|
||||
import {
|
||||
TableExpandColumn,
|
||||
@ -72,7 +73,10 @@ export default defineComponent({
|
||||
): void {
|
||||
if (happensIn(e, 'dataTableFilter')) return
|
||||
if (!isColumnSortable(column)) return
|
||||
const activeSorter = mergedSortStateRef.value
|
||||
const activeSorter =
|
||||
mergedSortStateRef.value.find(
|
||||
(state) => state.columnKey === column.key
|
||||
) || null
|
||||
const nextSorter = createNextSorter(column, activeSorter)
|
||||
doUpdateSorter(nextSorter)
|
||||
}
|
||||
@ -109,7 +113,6 @@ export default defineComponent({
|
||||
currentPage,
|
||||
allRowsChecked,
|
||||
someRowsChecked,
|
||||
mergedSortState,
|
||||
rows,
|
||||
cols,
|
||||
mergedTheme,
|
||||
@ -118,6 +121,7 @@ export default defineComponent({
|
||||
discrete,
|
||||
mergedTableLayout,
|
||||
headerCheckboxDisabled,
|
||||
mergedSortState,
|
||||
handleColHeaderClick,
|
||||
handleCheckboxUpdateChecked
|
||||
} = this
|
||||
@ -151,8 +155,7 @@ export default defineComponent({
|
||||
`${mergedClsPrefix}-data-table-th--fixed-${column.fixed}`,
|
||||
{
|
||||
[`${mergedClsPrefix}-data-table-th--hover`]:
|
||||
mergedSortState?.order &&
|
||||
mergedSortState.columnKey === key,
|
||||
isColumnSorting(column, mergedSortState),
|
||||
[`${mergedClsPrefix}-data-table-th--filterable`]:
|
||||
isColumnFilterable(column),
|
||||
[`${mergedClsPrefix}-data-table-th--sortable`]:
|
||||
|
@ -34,8 +34,13 @@ export type CreateRowProps<T = InternalRowData> = (
|
||||
row: T,
|
||||
index: number
|
||||
) => HTMLAttributes
|
||||
export type CompareFn<T = InternalRowData> = (row1: T, row2: T) => number
|
||||
export type Sorter<T = InternalRowData> = CompareFn<T> | SorterMultiple<T>
|
||||
export interface SorterMultiple<T = InternalRowData> {
|
||||
multiple: number
|
||||
compare?: CompareFn<T> | 'default'
|
||||
}
|
||||
|
||||
export type Sorter<T = InternalRowData> = (row1: T, row2: T) => number
|
||||
export type Filter<T = InternalRowData> = (
|
||||
filterOptionValue: FilterOptionValue,
|
||||
row: T
|
||||
@ -174,7 +179,7 @@ export interface DataTableInjection {
|
||||
mergedCurrentPageRef: Ref<number>
|
||||
someRowsCheckedRef: Ref<boolean>
|
||||
allRowsCheckedRef: Ref<boolean>
|
||||
mergedSortStateRef: Ref<SortState | null>
|
||||
mergedSortStateRef: Ref<SortState[]>
|
||||
mergedFilterStateRef: Ref<FilterState>
|
||||
loadingRef: Ref<boolean>
|
||||
rowClassNameRef: Ref<string | CreateRowClassName | undefined>
|
||||
@ -231,7 +236,7 @@ export type RenderFilterMenu = (actions: { hide: () => void }) => VNodeChild
|
||||
|
||||
export type OnUpdateExpandedRowKeys = (keys: RowKey[]) => void
|
||||
export type OnUpdateCheckedRowKeys = (keys: RowKey[]) => void
|
||||
export type OnUpdateSorter = (sortState: SortState | null) => void
|
||||
export type OnUpdateSorter = (sortState: SortState | SortState[] | null) => void
|
||||
export type OnUpdateFilters = (
|
||||
filterState: FilterState,
|
||||
sourceColumn?: TableBaseColumn
|
||||
|
250
src/data-table/src/use-sorter.ts
Normal file
250
src/data-table/src/use-sorter.ts
Normal file
@ -0,0 +1,250 @@
|
||||
import { computed, ref, ComputedRef } from 'vue'
|
||||
import {
|
||||
ColumnKey,
|
||||
InternalRowData,
|
||||
SortOrder,
|
||||
SortState,
|
||||
TmNode,
|
||||
TableBaseColumn,
|
||||
TableExpandColumn,
|
||||
TableSelectionColumn,
|
||||
CompareFn
|
||||
} from './interface'
|
||||
import { getFlagOfOrder } from './utils'
|
||||
import { call } from '../../_utils'
|
||||
import type { DataTableSetupProps } from './DataTable'
|
||||
|
||||
function getMultiplePriority ({
|
||||
sorter
|
||||
}: {
|
||||
sorter: TableBaseColumn['sorter']
|
||||
}): number | false {
|
||||
if (typeof sorter === 'object' && typeof sorter.multiple === 'number') {
|
||||
return sorter.multiple
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function getSortFunction (
|
||||
sorter: TableBaseColumn['sorter'],
|
||||
columnKey: ColumnKey
|
||||
): CompareFn | false {
|
||||
if (
|
||||
columnKey &&
|
||||
(sorter === undefined ||
|
||||
sorter === 'default' ||
|
||||
(typeof sorter === 'object' && sorter.compare === 'default'))
|
||||
) {
|
||||
return getDefaultSorterFn(columnKey)
|
||||
}
|
||||
if (typeof sorter === 'function') {
|
||||
return sorter
|
||||
}
|
||||
if (
|
||||
sorter &&
|
||||
typeof sorter === 'object' &&
|
||||
sorter.compare &&
|
||||
sorter.compare !== 'default'
|
||||
) {
|
||||
return sorter.compare
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
function getDefaultSorterFn (columnKey: ColumnKey) {
|
||||
return (row1: InternalRowData, row2: InternalRowData) => {
|
||||
const value1 = row1[columnKey]
|
||||
const value2 = row2[columnKey]
|
||||
|
||||
if (typeof value1 === 'number' && typeof value2 === 'number') {
|
||||
return value1 - value2
|
||||
} else if (typeof value1 === 'string' && typeof value2 === 'string') {
|
||||
return value1.localeCompare(value2)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export function useSorter (
|
||||
props: DataTableSetupProps,
|
||||
{
|
||||
dataRelatedColsRef,
|
||||
filteredDataRef
|
||||
}: {
|
||||
dataRelatedColsRef: ComputedRef<
|
||||
Array<TableSelectionColumn | TableBaseColumn | TableExpandColumn>
|
||||
>
|
||||
filteredDataRef: ComputedRef<TmNode[]>
|
||||
}
|
||||
) {
|
||||
const uncontrolledSortStateRef = ref<SortState[]>([])
|
||||
const mergedSortStateRef = computed(() => {
|
||||
// If one of the columns's sort order is false or 'ascend' or 'descend',
|
||||
// the table's controll functionality should work in controlled manner.
|
||||
const columnsWithControlledSortOrder = dataRelatedColsRef.value.filter(
|
||||
(column) =>
|
||||
column.type !== 'selection' &&
|
||||
column.sorter !== undefined &&
|
||||
(column.sortOrder === 'ascend' ||
|
||||
column.sortOrder === 'descend' ||
|
||||
column.sortOrder === false)
|
||||
)
|
||||
// if multiple columns are controlled sortable, then we need to find columns with active sortOrder
|
||||
const columnToSort: TableBaseColumn[] | undefined = (
|
||||
columnsWithControlledSortOrder as TableBaseColumn[]
|
||||
).filter((col: TableBaseColumn) => col.sortOrder !== false)
|
||||
if (columnToSort.length) {
|
||||
return columnToSort.map((column) => {
|
||||
return {
|
||||
columnKey: column.key,
|
||||
// column to sort has controlled sorter
|
||||
// sorter && sort order won't be undefined
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
order: column.sortOrder!,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
sorter: column.sorter!
|
||||
}
|
||||
})
|
||||
}
|
||||
if (columnsWithControlledSortOrder.length) return []
|
||||
return uncontrolledSortStateRef.value
|
||||
})
|
||||
const sortedDataRef = computed<TmNode[]>(() => {
|
||||
const activeSorters = mergedSortStateRef.value.slice().sort((a, b) => {
|
||||
const item1Priority = getMultiplePriority(a) || 0
|
||||
const item2Priority = getMultiplePriority(b) || 0
|
||||
return item2Priority - item1Priority
|
||||
})
|
||||
if (activeSorters.length) {
|
||||
const filteredData = filteredDataRef.value.slice()
|
||||
return filteredData.sort((tmNode1, tmNode2) => {
|
||||
let compareResult = 0
|
||||
activeSorters.some((sorterState) => {
|
||||
const { columnKey, sorter, order } = sorterState
|
||||
|
||||
const compareFn = getSortFunction(sorter, columnKey)
|
||||
if (compareFn && order) {
|
||||
compareResult = compareFn(tmNode1.rawNode, tmNode2.rawNode)
|
||||
|
||||
if (compareResult !== 0) {
|
||||
compareResult = compareResult * getFlagOfOrder(order)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
return compareResult
|
||||
})
|
||||
}
|
||||
return filteredDataRef.value
|
||||
})
|
||||
|
||||
dataRelatedColsRef.value.forEach((column) => {
|
||||
if (column.sorter !== undefined) {
|
||||
addSortSate({
|
||||
columnKey: column.key,
|
||||
sorter: column.sorter,
|
||||
order: column.defaultSortOrder ?? false
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
function getUpdatedSorterState (
|
||||
sortState: SortState | null
|
||||
): SortState | null | SortState[] {
|
||||
let currentSortState = mergedSortStateRef.value.slice()
|
||||
// Multiple sorter
|
||||
if (
|
||||
sortState &&
|
||||
getMultiplePriority({ sorter: sortState.sorter }) !== false
|
||||
) {
|
||||
// clear column is not multiple sort
|
||||
currentSortState = currentSortState.filter(
|
||||
(sortState) =>
|
||||
getMultiplePriority({ sorter: sortState.sorter }) !== false
|
||||
)
|
||||
updateSortInSortStates(currentSortState, sortState)
|
||||
return currentSortState
|
||||
} else if (sortState) {
|
||||
// single sorter
|
||||
return sortState
|
||||
}
|
||||
// no sorter
|
||||
return null
|
||||
}
|
||||
|
||||
function doUpdateSorter (sortState: SortState | null): void {
|
||||
const {
|
||||
'onUpdate:sorter': _onUpdateSorter,
|
||||
onUpdateSorter,
|
||||
onSorterChange
|
||||
} = props
|
||||
|
||||
const updateSorterState: SortState | SortState[] | null =
|
||||
getUpdatedSorterState(sortState)
|
||||
|
||||
if (_onUpdateSorter) call(_onUpdateSorter, updateSorterState)
|
||||
if (onUpdateSorter) call(onUpdateSorter, updateSorterState)
|
||||
if (onSorterChange) call(onSorterChange, updateSorterState)
|
||||
if (Array.isArray(updateSorterState)) {
|
||||
uncontrolledSortStateRef.value = updateSorterState
|
||||
} else if (updateSorterState) {
|
||||
uncontrolledSortStateRef.value = [updateSorterState]
|
||||
} else {
|
||||
uncontrolledSortStateRef.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function sort (columnKey: ColumnKey, order: SortOrder = 'ascend'): void {
|
||||
if (!columnKey) {
|
||||
clearSorter()
|
||||
} else {
|
||||
const columnToSort = dataRelatedColsRef.value.find(
|
||||
(column) =>
|
||||
column.type !== 'selection' &&
|
||||
column.type !== 'expand' &&
|
||||
column.key === columnKey
|
||||
)
|
||||
if (!columnToSort || !columnToSort.sorter) return
|
||||
const sorter = columnToSort.sorter
|
||||
doUpdateSorter({
|
||||
columnKey,
|
||||
sorter,
|
||||
order: order
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function clearSorter (): void {
|
||||
doUpdateSorter(null)
|
||||
}
|
||||
|
||||
function updateSortInSortStates (
|
||||
sortStates: SortState[],
|
||||
sortState: SortState
|
||||
): void {
|
||||
const index = sortStates.findIndex(
|
||||
(state) => sortState?.columnKey && state.columnKey === sortState.columnKey
|
||||
)
|
||||
if (index !== undefined && index >= 0) {
|
||||
sortStates[index] = sortState
|
||||
} else {
|
||||
sortStates.push(sortState)
|
||||
}
|
||||
}
|
||||
|
||||
function addSortSate (sortState: SortState): void {
|
||||
updateSortInSortStates(uncontrolledSortStateRef.value, sortState)
|
||||
}
|
||||
|
||||
return {
|
||||
clearSorter,
|
||||
sort,
|
||||
sortedDataRef,
|
||||
mergedSortStateRef,
|
||||
uncontrolledSortStateRef,
|
||||
doUpdateSorter
|
||||
}
|
||||
}
|
@ -8,7 +8,6 @@ import {
|
||||
FilterOptionValue,
|
||||
FilterState,
|
||||
SortOrder,
|
||||
SortState,
|
||||
TableBaseColumn,
|
||||
TableSelectionColumn,
|
||||
InternalRowData,
|
||||
@ -16,10 +15,10 @@ import {
|
||||
TableExpandColumn,
|
||||
RowKey
|
||||
} from './interface'
|
||||
import { createShallowClonedObject, getFlagOfOrder } from './utils'
|
||||
import { createShallowClonedObject } from './utils'
|
||||
import { PaginationProps } from '../../pagination/src/Pagination'
|
||||
import { call, warn } from '../../_utils'
|
||||
|
||||
import { useSorter } from './use-sorter'
|
||||
// useTableData combines filter, sorter and pagination
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
@ -70,92 +69,9 @@ export function useTableData (
|
||||
})
|
||||
|
||||
const uncontrolledFilterStateRef = ref<FilterState>({})
|
||||
const uncontrolledSortStateRef = ref<SortState | null>(null)
|
||||
const uncontrolledCurrentPageRef = ref(1)
|
||||
const uncontrolledPageSizeRef = ref(10)
|
||||
|
||||
dataRelatedColsRef.value.forEach((column) => {
|
||||
if (column.sorter !== undefined) {
|
||||
uncontrolledSortStateRef.value = {
|
||||
columnKey: column.key,
|
||||
sorter: column.sorter,
|
||||
order: column.defaultSortOrder ?? false
|
||||
}
|
||||
}
|
||||
if (column.filter) {
|
||||
const defaultFilterOptionValues = column.defaultFilterOptionValues
|
||||
if (column.filterMultiple) {
|
||||
uncontrolledFilterStateRef.value[column.key] =
|
||||
defaultFilterOptionValues || []
|
||||
} else if (defaultFilterOptionValues !== undefined) {
|
||||
// this branch is for compatibility, someone may use `values` in single filter mode
|
||||
uncontrolledFilterStateRef.value[column.key] =
|
||||
defaultFilterOptionValues === null ? [] : defaultFilterOptionValues
|
||||
} else {
|
||||
uncontrolledFilterStateRef.value[column.key] =
|
||||
column.defaultFilterOptionValue ?? null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const controlledCurrentPageRef = computed(() => {
|
||||
const { pagination } = props
|
||||
if (pagination === false) return undefined
|
||||
return pagination.page
|
||||
})
|
||||
const controlledPageSizeRef = computed(() => {
|
||||
const { pagination } = props
|
||||
if (pagination === false) return undefined
|
||||
return pagination.pageSize
|
||||
})
|
||||
|
||||
const mergedCurrentPageRef = useMergedState(
|
||||
controlledCurrentPageRef,
|
||||
uncontrolledCurrentPageRef
|
||||
)
|
||||
const mergedPageSizeRef = useMergedState(
|
||||
controlledPageSizeRef,
|
||||
uncontrolledPageSizeRef
|
||||
)
|
||||
const mergedPageCountRef = computed(() => {
|
||||
const { pagination } = props
|
||||
if (pagination) {
|
||||
const { pageCount } = pagination
|
||||
if (pageCount !== undefined) return pageCount
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const mergedSortStateRef = computed<SortState | null>(() => {
|
||||
// If one of the columns's sort order is false or 'ascend' or 'descend',
|
||||
// the table's controll functionality should work in controlled manner.
|
||||
const columnsWithControlledSortOrder = dataRelatedColsRef.value.filter(
|
||||
(column) =>
|
||||
column.type !== 'selection' &&
|
||||
column.sorter !== undefined &&
|
||||
(column.sortOrder === 'ascend' ||
|
||||
column.sortOrder === 'descend' ||
|
||||
column.sortOrder === false)
|
||||
)
|
||||
// if multiple column is controlled sortable, then we need to find a column with active sortOrder
|
||||
const columnToSort: TableBaseColumn | undefined = (
|
||||
columnsWithControlledSortOrder as TableBaseColumn[]
|
||||
).filter((col: TableBaseColumn) => col.sortOrder !== false)[0]
|
||||
if (columnToSort) {
|
||||
return {
|
||||
columnKey: columnToSort.key,
|
||||
// column to sort has controlled sorter
|
||||
// sorter && sort order won't be undefined
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
order: columnToSort.sortOrder!,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
sorter: columnToSort.sorter!
|
||||
}
|
||||
}
|
||||
if (columnsWithControlledSortOrder.length) return null
|
||||
return uncontrolledSortStateRef.value
|
||||
})
|
||||
|
||||
const mergedFilterStateRef = computed<FilterState>(() => {
|
||||
const columnsWithControlledFilter = dataRelatedColsRef.value.filter(
|
||||
(column) => {
|
||||
@ -242,41 +158,56 @@ export function useTableData (
|
||||
: []
|
||||
})
|
||||
|
||||
const sortedDataRef = computed<TmNode[]>(() => {
|
||||
const activeSorter = mergedSortStateRef.value
|
||||
if (activeSorter) {
|
||||
// When async, mergedSortState.sorter should be true
|
||||
// and we sort nothing, just return the filtered data
|
||||
if (activeSorter.sorter === true || activeSorter.sorter === false) {
|
||||
return filteredDataRef.value
|
||||
const { sortedDataRef, doUpdateSorter, mergedSortStateRef } = useSorter(
|
||||
props,
|
||||
{
|
||||
dataRelatedColsRef,
|
||||
filteredDataRef
|
||||
}
|
||||
const filteredData = filteredDataRef.value.slice(0)
|
||||
const columnKey = activeSorter.columnKey
|
||||
// 1 for asc
|
||||
// -1 for desc
|
||||
const order = activeSorter.order
|
||||
const sorter =
|
||||
activeSorter.sorter === undefined || activeSorter.sorter === 'default'
|
||||
? (row1: InternalRowData, row2: InternalRowData) => {
|
||||
const value1 = row1[columnKey]
|
||||
const value2 = row2[columnKey]
|
||||
if (typeof value1 === 'number' && typeof value2 === 'number') {
|
||||
return value1 - value2
|
||||
} else if (
|
||||
typeof value1 === 'string' &&
|
||||
typeof value2 === 'string'
|
||||
) {
|
||||
return value1.localeCompare(value2)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
: activeSorter.sorter
|
||||
return filteredData.sort(
|
||||
(tmNode1, tmNode2) =>
|
||||
getFlagOfOrder(order) * sorter(tmNode1.rawNode, tmNode2.rawNode)
|
||||
)
|
||||
dataRelatedColsRef.value.forEach((column) => {
|
||||
if (column.filter) {
|
||||
const defaultFilterOptionValues = column.defaultFilterOptionValues
|
||||
if (column.filterMultiple) {
|
||||
uncontrolledFilterStateRef.value[column.key] =
|
||||
defaultFilterOptionValues || []
|
||||
} else if (defaultFilterOptionValues !== undefined) {
|
||||
// this branch is for compatibility, someone may use `values` in single filter mode
|
||||
uncontrolledFilterStateRef.value[column.key] =
|
||||
defaultFilterOptionValues === null ? [] : defaultFilterOptionValues
|
||||
} else {
|
||||
uncontrolledFilterStateRef.value[column.key] =
|
||||
column.defaultFilterOptionValue ?? null
|
||||
}
|
||||
return filteredDataRef.value
|
||||
}
|
||||
})
|
||||
|
||||
const controlledCurrentPageRef = computed(() => {
|
||||
const { pagination } = props
|
||||
if (pagination === false) return undefined
|
||||
return pagination.page
|
||||
})
|
||||
const controlledPageSizeRef = computed(() => {
|
||||
const { pagination } = props
|
||||
if (pagination === false) return undefined
|
||||
return pagination.pageSize
|
||||
})
|
||||
|
||||
const mergedCurrentPageRef = useMergedState(
|
||||
controlledCurrentPageRef,
|
||||
uncontrolledCurrentPageRef
|
||||
)
|
||||
const mergedPageSizeRef = useMergedState(
|
||||
controlledPageSizeRef,
|
||||
uncontrolledPageSizeRef
|
||||
)
|
||||
const mergedPageCountRef = computed(() => {
|
||||
const { pagination } = props
|
||||
if (pagination) {
|
||||
const { pageCount } = pagination
|
||||
if (pageCount !== undefined) return pageCount
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const paginatedDataRef = computed<TmNode[]>(() => {
|
||||
@ -365,17 +296,7 @@ export function useTableData (
|
||||
if (_onUpdatePageSize) call(_onUpdatePageSize, pageSize)
|
||||
uncontrolledPageSizeRef.value = pageSize
|
||||
}
|
||||
function doUpdateSorter (sortState: SortState | null): void {
|
||||
const {
|
||||
'onUpdate:sorter': _onUpdateSorter,
|
||||
onUpdateSorter,
|
||||
onSorterChange
|
||||
} = props
|
||||
if (_onUpdateSorter) call(_onUpdateSorter, sortState)
|
||||
if (onUpdateSorter) call(onUpdateSorter, sortState)
|
||||
if (onSorterChange) call(onSorterChange, sortState)
|
||||
uncontrolledSortStateRef.value = sortState
|
||||
}
|
||||
|
||||
function doUpdateFilters (
|
||||
filters: FilterState,
|
||||
sourceColumn?: TableBaseColumn
|
||||
@ -397,6 +318,7 @@ export function useTableData (
|
||||
if (!columnKey) {
|
||||
clearSorter()
|
||||
} else {
|
||||
// TODO:
|
||||
const columnToSort = dataRelatedColsRef.value.find(
|
||||
(column) =>
|
||||
column.type !== 'selection' &&
|
||||
|
@ -108,3 +108,15 @@ export function createNextSorter (
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isColumnSorting (
|
||||
column: TableColumn,
|
||||
mergedSortState: SortState[]
|
||||
): boolean {
|
||||
return (
|
||||
mergedSortState.find(
|
||||
(state) =>
|
||||
state.columnKey === (column as TableBaseColumn).key && state.order
|
||||
) !== undefined
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { h, HTMLAttributes, nextTick } from 'vue'
|
||||
import { h, HTMLAttributes, nextTick, ref } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { NDataTable } from '../index'
|
||||
import { DataTableInst, NDataTable } from '../index'
|
||||
import type { DataTableColumns } from '../index'
|
||||
import { NButton, NButtonGroup } from '../../button'
|
||||
|
||||
@ -319,6 +319,267 @@ describe('n-data-table', () => {
|
||||
expect(wrapper.find('tbody .n-data-table-tr').classes()).toContain('0-test')
|
||||
})
|
||||
|
||||
describe('should work with multiple sorter', () => {
|
||||
interface UserData {
|
||||
name: string
|
||||
age: number
|
||||
address: string
|
||||
chinese: number
|
||||
math: number
|
||||
english: number
|
||||
}
|
||||
const columns: DataTableColumns<UserData> = [
|
||||
{
|
||||
title: 'Name',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
title: () => h('span', { id: 'age-title' }, 'Age'),
|
||||
className: 'age-col',
|
||||
key: 'age',
|
||||
sorter: (a, b) => a.age - b.age
|
||||
},
|
||||
{
|
||||
title: () => <span id="chinese-title">Chinese Score</span>,
|
||||
key: 'chinese',
|
||||
defaultSortOrder: false,
|
||||
className: 'chinese-col',
|
||||
sorter: {
|
||||
compare: (a, b) => a.chinese - b.chinese,
|
||||
multiple: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
title: () => <span id="math-title">Math Score</span>,
|
||||
defaultSortOrder: false,
|
||||
className: 'math-col',
|
||||
key: 'math',
|
||||
sorter: {
|
||||
compare: (a, b) => a.math - b.math,
|
||||
multiple: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
title: () => <span id="english-title">English Score</span>,
|
||||
className: 'english-col',
|
||||
defaultSortOrder: false,
|
||||
key: 'english',
|
||||
sorter: {
|
||||
compare: (a, b) => a.english - b.english,
|
||||
multiple: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Address',
|
||||
key: 'address',
|
||||
filterOptions: [
|
||||
{
|
||||
label: 'London',
|
||||
value: 'London'
|
||||
},
|
||||
{
|
||||
label: 'New York',
|
||||
value: 'New York'
|
||||
},
|
||||
{
|
||||
label: 'Sidney',
|
||||
value: 'Sidney'
|
||||
}
|
||||
],
|
||||
filter (value: any, row) {
|
||||
return row.address.includes(value)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const data = [
|
||||
{
|
||||
name: 'John Brown',
|
||||
age: 32,
|
||||
address: 'New York No. 1 Lake Park',
|
||||
chinese: 98,
|
||||
math: 60,
|
||||
english: 70
|
||||
},
|
||||
{
|
||||
name: 'Jim Green',
|
||||
age: 42,
|
||||
address: 'London No. 1 Lake Park',
|
||||
chinese: 98,
|
||||
math: 66,
|
||||
english: 89
|
||||
},
|
||||
{
|
||||
name: 'Joe Black',
|
||||
age: 32,
|
||||
address: 'Sidney No. 1 Lake Park',
|
||||
chinese: 98,
|
||||
math: 66,
|
||||
english: 89
|
||||
},
|
||||
{
|
||||
name: 'Jim Red',
|
||||
age: 32,
|
||||
address: 'London No. 2 Lake Park',
|
||||
chinese: 88,
|
||||
math: 99,
|
||||
english: 89
|
||||
}
|
||||
]
|
||||
const tableRef = ref<DataTableInst | null>(null)
|
||||
const wrapper = mount(
|
||||
() => <NDataTable ref={tableRef} columns={columns} data={data} />,
|
||||
{
|
||||
attachTo: document.body
|
||||
}
|
||||
)
|
||||
const checkIsMatched = async (
|
||||
colClassName: string,
|
||||
target: number[]
|
||||
): Promise<boolean> => {
|
||||
const cols = await wrapper.findAll(colClassName)
|
||||
const colNums = cols.slice(1).map((item) => Number(item.text()))
|
||||
const matchResult = String(colNums) === String(target)
|
||||
if (!matchResult) {
|
||||
console.log(colClassName, String(colNums), String(target))
|
||||
}
|
||||
return String(colNums) === String(target)
|
||||
}
|
||||
const checkScoreIsMatched = async (
|
||||
targets: [number[], number[], number[]]
|
||||
): Promise<boolean> => {
|
||||
const matchResult =
|
||||
(await checkIsMatched('.chinese-col', targets[0])) &&
|
||||
(await checkIsMatched('.math-col', targets[1])) &&
|
||||
(await checkIsMatched('.english-col', targets[2]))
|
||||
|
||||
return matchResult
|
||||
}
|
||||
const chineseDom: HTMLElement | null =
|
||||
document.querySelector('#chinese-title')
|
||||
const mathDom: HTMLElement | null = document.querySelector('#math-title')
|
||||
const englishDom: HTMLElement | null =
|
||||
document.querySelector('#english-title')
|
||||
const ageDom: HTMLElement | null = document.querySelector('#age-title')
|
||||
|
||||
it('chinese: descend, math: false, english: false', async () => {
|
||||
await chineseDom?.click()
|
||||
expect(
|
||||
await checkScoreIsMatched([
|
||||
[98, 98, 98, 88],
|
||||
[60, 66, 66, 99],
|
||||
[70, 89, 89, 89]
|
||||
])
|
||||
).toEqual(true)
|
||||
})
|
||||
it('chinese: descend, math: descend, english: false', async () => {
|
||||
await mathDom?.click()
|
||||
expect(
|
||||
await checkScoreIsMatched([
|
||||
[98, 98, 98, 88],
|
||||
[66, 66, 60, 99],
|
||||
[89, 89, 70, 89]
|
||||
])
|
||||
).toEqual(true)
|
||||
})
|
||||
|
||||
it('chinese: descend, math: descend, english: descend', async () => {
|
||||
await englishDom?.click()
|
||||
expect(
|
||||
await checkScoreIsMatched([
|
||||
[98, 98, 98, 88],
|
||||
[66, 66, 60, 99],
|
||||
[89, 89, 70, 89]
|
||||
])
|
||||
).toEqual(true)
|
||||
})
|
||||
|
||||
it('chinese: ascend, math: descend, english: descend', async () => {
|
||||
await chineseDom?.click()
|
||||
expect(
|
||||
await checkScoreIsMatched([
|
||||
[88, 98, 98, 98],
|
||||
[99, 66, 66, 60],
|
||||
[89, 89, 89, 70]
|
||||
])
|
||||
).toEqual(true)
|
||||
})
|
||||
|
||||
it('chinese: false, math: descend, english: descend', async () => {
|
||||
await chineseDom?.click()
|
||||
expect(
|
||||
await checkScoreIsMatched([
|
||||
[88, 98, 98, 98],
|
||||
[99, 66, 66, 60],
|
||||
[89, 89, 89, 70]
|
||||
])
|
||||
).toEqual(true)
|
||||
})
|
||||
|
||||
it('chinese: false, math: ascend, english: descend', async () => {
|
||||
await mathDom?.click()
|
||||
expect(
|
||||
await checkScoreIsMatched([
|
||||
[98, 98, 98, 88],
|
||||
[60, 66, 66, 99],
|
||||
[70, 89, 89, 89]
|
||||
])
|
||||
).toEqual(true)
|
||||
})
|
||||
|
||||
it('chinese: false, math: false, english: descend', async () => {
|
||||
await mathDom?.click()
|
||||
expect(
|
||||
await checkScoreIsMatched([
|
||||
[98, 98, 88, 98],
|
||||
[66, 66, 99, 60],
|
||||
[89, 89, 89, 70]
|
||||
])
|
||||
).toEqual(true)
|
||||
})
|
||||
|
||||
it('chinese: descend, math: false, english: descend', async () => {
|
||||
await chineseDom?.click()
|
||||
expect(
|
||||
await checkScoreIsMatched([
|
||||
[98, 98, 98, 88],
|
||||
[66, 66, 60, 99],
|
||||
[89, 89, 70, 89]
|
||||
])
|
||||
).toEqual(true)
|
||||
})
|
||||
|
||||
it('filter: Sidney and New York', async () => {
|
||||
if (tableRef.value) {
|
||||
tableRef.value.filter({
|
||||
address: ['Sidney', 'New York']
|
||||
})
|
||||
}
|
||||
await nextTick()
|
||||
const result = await checkScoreIsMatched([
|
||||
[98, 98],
|
||||
[66, 60],
|
||||
[89, 70]
|
||||
])
|
||||
expect(result).toEqual(true)
|
||||
if (tableRef.value) {
|
||||
tableRef.value.filter(null)
|
||||
}
|
||||
})
|
||||
|
||||
it('age: descend', async () => {
|
||||
await ageDom?.click()
|
||||
const result =
|
||||
(await checkIsMatched('.age-col', [42, 32, 32, 32])) &&
|
||||
(await checkScoreIsMatched([
|
||||
[98, 98, 98, 88],
|
||||
[66, 60, 66, 99],
|
||||
[89, 70, 89, 89]
|
||||
]))
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('should work with `indent` prop', async () => {
|
||||
const columns = [
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user