From 6d9e56a106a4040fcf657d46e6cca36655d35f9a Mon Sep 17 00:00:00 2001 From: JeremyWuuuuu <15975785+JeremyWuuuuu@users.noreply.github.com> Date: Sat, 23 Apr 2022 20:05:20 +0800 Subject: [PATCH] feat(components): [virtual-table] compsables (#7341) - Split `use-table` into separate files for better readability --- .../table-v2/src/composables/index.ts | 5 + .../src/{ => composables}/use-columns.ts | 72 ++- .../table-v2/src/composables/use-data.ts | 71 +++ .../table-v2/src/composables/use-row.ts | 170 +++++++ .../table-v2/src/composables/use-scrollbar.ts | 83 ++++ .../table-v2/src/composables/use-styles.ts | 127 +++++ .../table-v2/src/composables/utils.ts | 30 ++ packages/components/table-v2/src/table-v2.tsx | 4 +- packages/components/table-v2/src/types.ts | 1 + packages/components/table-v2/src/use-table.ts | 445 ++++-------------- packages/components/virtual-list/src/types.ts | 4 +- 11 files changed, 613 insertions(+), 399 deletions(-) create mode 100644 packages/components/table-v2/src/composables/index.ts rename packages/components/table-v2/src/{ => composables}/use-columns.ts (65%) create mode 100644 packages/components/table-v2/src/composables/use-data.ts create mode 100644 packages/components/table-v2/src/composables/use-row.ts create mode 100644 packages/components/table-v2/src/composables/use-scrollbar.ts create mode 100644 packages/components/table-v2/src/composables/use-styles.ts create mode 100644 packages/components/table-v2/src/composables/utils.ts diff --git a/packages/components/table-v2/src/composables/index.ts b/packages/components/table-v2/src/composables/index.ts new file mode 100644 index 0000000000..7a18027a07 --- /dev/null +++ b/packages/components/table-v2/src/composables/index.ts @@ -0,0 +1,5 @@ +export * from './use-columns' +export * from './use-scrollbar' +export * from './use-row' +export * from './use-data' +export * from './use-styles' diff --git a/packages/components/table-v2/src/use-columns.ts b/packages/components/table-v2/src/composables/use-columns.ts similarity index 65% rename from packages/components/table-v2/src/use-columns.ts rename to packages/components/table-v2/src/composables/use-columns.ts index 033ff8af07..69a5fc4047 100644 --- a/packages/components/table-v2/src/use-columns.ts +++ b/packages/components/table-v2/src/composables/use-columns.ts @@ -1,40 +1,18 @@ import { computed, unref } from 'vue' -import { placeholderSign } from './private' +import { isObject } from '@element-plus/utils' +import { SortOrder, oppositeOrderMap } from '../constants' +import { placeholderSign } from '../private' +import { calcColumnStyle } from './utils' import type { CSSProperties, Ref } from 'vue' -import type { Column, Columns, KeyType } from './types' +import type { TableV2Props } from '../table' +import type { AnyColumns, Column, KeyType } from '../types' -type AnyColumn = Columns - -const calcColumnStyle = ( - column: Column, - fixedColumn: boolean -): CSSProperties => { - const flex = { - flexGrow: 0, - flexShrink: 0, - } - - if (column.fixed) { - flex.flexShrink = 1 - } - - const style = { - ...(column.style ?? {}), - ...flex, - flexBasis: 'auto', - width: column.width, - } - - if (!fixedColumn) { - if (column.maxWidth) style.maxWidth = column.maxWidth - if (column.minWidth) style.maxWidth = column.minWidth - } - - return style -} - -function useColumns(columns: Ref, fixed: Ref) { +function useColumns( + props: TableV2Props, + columns: Ref, + fixed: Ref +) { const visibleColumns = computed(() => { return unref(columns).filter((column) => !column.hidden) }) @@ -45,7 +23,7 @@ function useColumns(columns: Ref, fixed: Ref) { ) ) - const fixedColumnOnRight = computed(() => + const fixedColumnsOnRight = computed(() => unref(visibleColumns).filter((column) => column.fixed === 'right') ) @@ -54,7 +32,7 @@ function useColumns(columns: Ref, fixed: Ref) { ) const mainColumns = computed(() => { - const ret: AnyColumn = [] + const ret: AnyColumns = [] unref(fixedColumnsOnLeft).forEach((column) => { ret.push({ @@ -67,7 +45,7 @@ function useColumns(columns: Ref, fixed: Ref) { ret.push(column) }) - unref(fixedColumnOnRight).forEach((column) => { + unref(fixedColumnsOnRight).forEach((column) => { ret.push({ ...column, placeholderSign, @@ -78,7 +56,7 @@ function useColumns(columns: Ref, fixed: Ref) { }) const hasFixedColumns = computed(() => { - return unref(fixedColumnsOnLeft).length || unref(fixedColumnOnRight).length + return unref(fixedColumnsOnLeft).length || unref(fixedColumnsOnRight).length }) const columnsStyles = computed(() => { @@ -112,12 +90,28 @@ function useColumns(columns: Ref, fixed: Ref) { column.width = width } + function onColumnSorted(e: MouseEvent) { + const { key } = (e.currentTarget as HTMLElement).dataset + if (!key) return + const { sortState, sortBy } = props + + let order = SortOrder.ASC + + if (isObject(sortState)) { + order = oppositeOrderMap[sortState[key]] + } else { + order = oppositeOrderMap[sortBy.order] + } + + props.onColumnSort?.({ column: getColumn(key)!, key, order }) + } + return { columns, columnsStyles, columnsTotalWidth, fixedColumnsOnLeft, - fixedColumnOnRight, + fixedColumnsOnRight, hasFixedColumns, mainColumns, normalColumns, @@ -126,7 +120,9 @@ function useColumns(columns: Ref, fixed: Ref) { getColumn, getColumnStyle, updateColumnWidth, + onColumnSorted, } } export { useColumns } +export type UseColumnsReturn = ReturnType diff --git a/packages/components/table-v2/src/composables/use-data.ts b/packages/components/table-v2/src/composables/use-data.ts new file mode 100644 index 0000000000..33dd85eefa --- /dev/null +++ b/packages/components/table-v2/src/composables/use-data.ts @@ -0,0 +1,71 @@ +import { computed, ref, unref, watch } from 'vue' + +import type { TableV2Props } from '../table' +import type { KeyType } from '../types' +import type { UseRowReturn } from './use-row' + +type UseDataProps = { + expandedRowKeys: UseRowReturn['expandedRowKeys'] + lastRenderedRowIndex: UseRowReturn['lastRenderedRowIndex'] + resetAfterIndex: UseRowReturn['resetAfterIndex'] +} + +export const useData = ( + props: TableV2Props, + { expandedRowKeys, lastRenderedRowIndex, resetAfterIndex }: UseDataProps +) => { + const depthMap = ref>({}) + + const flattenedData = computed(() => { + const depths: Record = {} + const { data, rowKey } = props + + const _expandedRowKeys = unref(expandedRowKeys) + + if (!_expandedRowKeys || !_expandedRowKeys.length) return data + + const array: any[] = [] + const keysSet = new Set() + _expandedRowKeys.forEach((x) => keysSet.add(x)) + + let copy: any[] = data.slice() + copy.forEach((x) => (depths[x[rowKey]] = 0)) + while (copy.length > 0) { + const item = copy.shift()! + + array.push(item) + if ( + keysSet.has(item[rowKey]) && + Array.isArray(item.children) && + item.children.length > 0 + ) { + copy = [...item.children, ...copy] + item.children.forEach( + (child: any) => (depths[child[rowKey]] = depths[item[rowKey]] + 1) + ) + } + } + + depthMap.value = depths + return array + }) + + const data = computed(() => { + const { data, expandColumnKey } = props + return expandColumnKey ? unref(flattenedData) : data + }) + + watch(data, (val, prev) => { + if (val !== prev) { + lastRenderedRowIndex.value = -1 + resetAfterIndex(0, true) + } + }) + + return { + data, + depthMap, + } +} + +export type UseDataReturn = ReturnType diff --git a/packages/components/table-v2/src/composables/use-row.ts b/packages/components/table-v2/src/composables/use-row.ts new file mode 100644 index 0000000000..776cacf65d --- /dev/null +++ b/packages/components/table-v2/src/composables/use-row.ts @@ -0,0 +1,170 @@ +import { + computed, + getCurrentInstance, + ref, + shallowRef, + unref, + watch, +} from 'vue' +import { debounce } from 'lodash-unified' +import { isNumber } from '@element-plus/utils' +import { FixedDir } from '../constants' + +import type { Ref } from 'vue' +import type { TableV2Props } from '../table' +import type { + RowExpandParams, + RowHeightChangedParams, + RowHoverParams, +} from '../row' +import type { FixedDirection, KeyType } from '../types' +import type { onRowRenderedParams } from '../grid' +import type { TableGridInstance } from '../table-grid' + +type Heights = Record +type GridInstanceRef = Ref + +type UseRowProps = { + mainTableRef: GridInstanceRef + leftTableRef: GridInstanceRef + rightTableRef: GridInstanceRef + + onMaybeEndReached: () => void +} + +export const useRow = ( + props: TableV2Props, + { mainTableRef, leftTableRef, rightTableRef, onMaybeEndReached }: UseRowProps +) => { + const vm = getCurrentInstance()! + const { emit } = vm + const isResetting = shallowRef(false) + const hoveringRowKey = shallowRef(null) + const expandedRowKeys = ref(props.defaultExpandedRowKeys || []) + const lastRenderedRowIndex = ref(-1) + const resetIndex = shallowRef(null) + const rowHeights = ref({}) + const pendingRowHeights = ref({}) + const leftTableHeights = shallowRef({}) + const mainTableHeights = shallowRef({}) + const rightTableHeights = shallowRef({}) + const isDynamic = computed(() => isNumber(props.estimatedRowHeight)) + + function onRowsRendered(params: onRowRenderedParams) { + props.onRowRendered?.(params) + + if (params.rowCacheEnd > unref(lastRenderedRowIndex)) { + lastRenderedRowIndex.value = params.rowCacheEnd + } + } + + function onRowHovered({ hovered, rowKey }: RowHoverParams) { + hoveringRowKey.value = hovered ? rowKey : null + } + + function onRowExpanded({ + expanded, + rowData, + rowIndex, + rowKey, + }: RowExpandParams) { + const _expandedRowKeys = [...unref(expandedRowKeys)] + const currentKeyIndex = _expandedRowKeys.indexOf(rowKey) + if (expanded) { + if (currentKeyIndex === -1) _expandedRowKeys.push(rowKey) + } else { + if (currentKeyIndex > -1) _expandedRowKeys.splice(currentKeyIndex, 1) + } + expandedRowKeys.value = _expandedRowKeys + + emit('update:expandedRowKeys', _expandedRowKeys) + props.onRowExpand?.({ + expanded, + rowData, + rowIndex, + rowKey, + }) + // If this is not controlled, then use this to notify changes + props.onExpandedRowsChange?.(_expandedRowKeys) + } + + const flushingRowHeights = debounce(() => { + isResetting.value = true + rowHeights.value = { ...unref(rowHeights), ...unref(pendingRowHeights) } + resetAfterIndex(unref(resetIndex)!, false) + pendingRowHeights.value = {} + // force update + resetIndex.value = null + mainTableRef.value?.forceUpdate() + leftTableRef.value?.forceUpdate() + rightTableRef.value?.forceUpdate() + vm.proxy?.$forceUpdate() + isResetting.value = false + }, 0) + + function resetAfterIndex(index: number, forceUpdate = false) { + if (!unref(isDynamic)) return + ;[mainTableRef, leftTableRef, rightTableRef].forEach((tableRef) => { + const table = unref(tableRef) + if (table) table.resetAfterRowIndex(index, forceUpdate) + }) + } + + function resetHeights(rowKey: KeyType, height: number, rowIdx: number) { + const resetIdx = unref(resetIndex) + if (resetIdx === null) { + resetIndex.value = rowIdx + } else { + if (resetIdx > rowIdx) { + resetIndex.value = rowIdx + } + } + + pendingRowHeights.value[rowKey] = height + } + + function onRowHeightChange( + { rowKey, height, rowIndex }: RowHeightChangedParams, + fixedDir: FixedDirection + ) { + if (!fixedDir) { + mainTableHeights.value[rowKey] = height + } else { + if (fixedDir === FixedDir.RIGHT) { + rightTableHeights.value[rowKey] = height + } else { + leftTableHeights.value[rowKey] = height + } + } + + const maximumHeight = Math.max( + ...[leftTableHeights, rightTableHeights, mainTableHeights].map( + (records) => records.value[rowKey] || 0 + ) + ) + + if (unref(rowHeights)[rowKey] !== maximumHeight) { + resetHeights(rowKey, maximumHeight, rowIndex) + flushingRowHeights() + } + } + // when rendered row changes, maybe reaching the bottom + watch(lastRenderedRowIndex, () => onMaybeEndReached()) + + return { + hoveringRowKey, + expandedRowKeys, + lastRenderedRowIndex, + isDynamic, + isResetting, + rowHeights, + + resetAfterIndex, + onRowExpanded, + onRowHovered, + onRowsRendered, + onRowHeightChange, + } +} + +export type UseRowReturn = ReturnType diff --git a/packages/components/table-v2/src/composables/use-scrollbar.ts b/packages/components/table-v2/src/composables/use-scrollbar.ts new file mode 100644 index 0000000000..9e16a439eb --- /dev/null +++ b/packages/components/table-v2/src/composables/use-scrollbar.ts @@ -0,0 +1,83 @@ +import { ref, unref, watch } from 'vue' + +import type { Ref } from 'vue' +import type { TableV2Props } from '../table' +import type { TableGridInstance } from '../table-grid' + +export type ScrollPos = { scrollLeft: number; scrollTop: number } +type GridInstanceRef = Ref + +type UseScrollBarProps = { + mainTableRef: GridInstanceRef + leftTableRef: GridInstanceRef + rightTableRef: GridInstanceRef + + onMaybeEndReached: () => void +} + +export const useScrollbar = ( + props: TableV2Props, + { + mainTableRef, + leftTableRef, + rightTableRef, + onMaybeEndReached, + }: UseScrollBarProps +) => { + const scrollPos = ref({ scrollLeft: 0, scrollTop: 0 }) + + function doScroll(params: ScrollPos) { + const { scrollTop } = params + + mainTableRef.value?.scrollTo(params) + leftTableRef.value?.scrollToTop(scrollTop) + rightTableRef.value?.scrollToTop(scrollTop) + } + + // methods + function scrollTo(params: ScrollPos) { + scrollPos.value = params + + doScroll(params) + } + + function scrollToTop(scrollTop: number) { + scrollPos.value.scrollTop = scrollTop + + doScroll(unref(scrollPos)) + } + + function scrollToLeft(scrollLeft: number) { + scrollPos.value.scrollLeft = scrollLeft + + mainTableRef.value?.scrollTo?.(unref(scrollPos)) + } + + function onScroll(params: ScrollPos) { + scrollTo(params) + props.onScroll?.(params) + } + + function onVerticalScroll({ scrollTop }: ScrollPos) { + const { scrollTop: currentScrollTop } = unref(scrollPos) + if (scrollTop !== currentScrollTop) scrollToTop(scrollTop) + } + + // When scrollTop changes, maybe reaching the bottom + watch( + () => unref(scrollPos).scrollTop, + (cur, prev) => { + if (cur > prev) onMaybeEndReached() + } + ) + + return { + scrollPos, + + scrollTo, + scrollToLeft, + scrollToTop, + onScroll, + onVerticalScroll, + } +} diff --git a/packages/components/table-v2/src/composables/use-styles.ts b/packages/components/table-v2/src/composables/use-styles.ts new file mode 100644 index 0000000000..9415ac3fcd --- /dev/null +++ b/packages/components/table-v2/src/composables/use-styles.ts @@ -0,0 +1,127 @@ +import { computed, unref } from 'vue' +import { addUnit, isNumber } from '@element-plus/utils' +import { enforceUnit, sum } from '../utils' + +import type { CSSProperties } from 'vue' +import type { TableV2Props } from '../table' +import type { UseColumnsReturn } from './use-columns' +import type { UseDataReturn } from './use-data' + +type UseStyleProps = { + columnsTotalWidth: UseColumnsReturn['columnsTotalWidth'] + data: UseDataReturn['data'] + fixedColumnsOnLeft: UseColumnsReturn['fixedColumnsOnLeft'] + fixedColumnsOnRight: UseColumnsReturn['fixedColumnsOnRight'] +} + +export const useStyles = ( + props: TableV2Props, + { + columnsTotalWidth, + data, + fixedColumnsOnLeft, + fixedColumnsOnRight, + }: UseStyleProps +) => { + const bodyWidth = computed(() => { + const { fixed, width, vScrollbarSize } = props + const ret = width - vScrollbarSize + return fixed ? Math.max(Math.round(unref(columnsTotalWidth)), ret) : ret + }) + + const headerWidth = computed( + () => unref(bodyWidth) + (props.fixed ? props.vScrollbarSize : 0) + ) + + const mainTableHeight = computed(() => { + const { height = 0, maxHeight = 0, footerHeight, hScrollbarSize } = props + + if (maxHeight > 0) { + const _fixedRowsHeight = unref(fixedRowsHeight) + const _rowsHeight = unref(rowsHeight) + const _headerHeight = unref(headerHeight) + const total = + _headerHeight + _fixedRowsHeight + _rowsHeight + hScrollbarSize + + return Math.min(total, maxHeight - footerHeight) + } + + return height - footerHeight + }) + + const rowsHeight = computed(() => { + const { rowHeight, estimatedRowHeight } = props + const _data = unref(data) + if (isNumber(estimatedRowHeight)) { + return _data.length * estimatedRowHeight + } + + return _data.length * rowHeight + }) + + const fixedTableHeight = computed(() => { + const { maxHeight } = props + const tableHeight = unref(mainTableHeight) + if (isNumber(maxHeight) && maxHeight > 0) return tableHeight + + const totalHeight = + unref(rowsHeight) + unref(headerHeight) + unref(fixedRowsHeight) + + return Math.min(tableHeight, totalHeight) + }) + + const mapColumn = (column: TableV2Props['columns'][number]) => column.width + + const leftTableWidth = computed(() => + sum(unref(fixedColumnsOnLeft).map(mapColumn)) + ) + + const rightTableWidth = computed(() => + sum(unref(fixedColumnsOnRight).map(mapColumn)) + ) + + const headerHeight = computed(() => sum(props.headerHeight)) + + const fixedRowsHeight = computed(() => { + return (props.fixedData?.length || 0) * props.rowHeight + }) + + const windowHeight = computed(() => { + return unref(mainTableHeight) - unref(headerHeight) - unref(fixedRowsHeight) + }) + + const rootStyle = computed(() => { + const { style = {}, height, width } = props + return enforceUnit({ + ...style, + height, + width, + }) + }) + + const footerHeight = computed(() => + enforceUnit({ height: props.footerHeight }) + ) + + const emptyStyle = computed(() => ({ + top: addUnit(unref(headerHeight)), + bottom: addUnit(props.footerHeight), + width: addUnit(props.width), + })) + + return { + bodyWidth, + fixedTableHeight, + mainTableHeight, + leftTableWidth, + rightTableWidth, + headerWidth, + rowsHeight, + windowHeight, + footerHeight, + emptyStyle, + rootStyle, + } +} + +export type UseStyleReturn = ReturnType diff --git a/packages/components/table-v2/src/composables/utils.ts b/packages/components/table-v2/src/composables/utils.ts new file mode 100644 index 0000000000..95b184e7a8 --- /dev/null +++ b/packages/components/table-v2/src/composables/utils.ts @@ -0,0 +1,30 @@ +import type { CSSProperties } from 'vue' +import type { AnyColumns } from '../types' + +export const calcColumnStyle = ( + column: AnyColumns[number], + fixedColumn: boolean +): CSSProperties => { + const flex = { + flexGrow: 0, + flexShrink: 0, + } + + if (column.fixed) { + flex.flexShrink = 1 + } + + const style = { + ...(column.style ?? {}), + ...flex, + flexBasis: 'auto', + width: column.width, + } + + if (!fixedColumn) { + if (column.maxWidth) style.maxWidth = column.maxWidth + if (column.minWidth) style.maxWidth = column.minWidth + } + + return style +} diff --git a/packages/components/table-v2/src/table-v2.tsx b/packages/components/table-v2/src/table-v2.tsx index 565e6399ea..30857db555 100644 --- a/packages/components/table-v2/src/table-v2.tsx +++ b/packages/components/table-v2/src/table-v2.tsx @@ -32,7 +32,7 @@ const TableV2 = defineComponent({ const { columnsStyles, fixedColumnsOnLeft, - fixedColumnOnRight, + fixedColumnsOnRight, mainColumns, mainTableHeight, fixedTableHeight, @@ -159,7 +159,7 @@ const TableV2 = defineComponent({ const rightTableProps = { cache, class: ns.e('right'), - columns: unref(fixedColumnOnRight), + columns: unref(fixedColumnsOnRight), data: _data, estimatedRowHeight, rightTableRef, diff --git a/packages/components/table-v2/src/types.ts b/packages/components/table-v2/src/types.ts index 1266ced4d9..5a0a796a1d 100644 --- a/packages/components/table-v2/src/types.ts +++ b/packages/components/table-v2/src/types.ts @@ -89,6 +89,7 @@ export type Column = { } export type Columns = Column[] +export type AnyColumns = Columns export type SortBy = { key: KeyType diff --git a/packages/components/table-v2/src/use-table.ts b/packages/components/table-v2/src/use-table.ts index 5e63d77a22..a995ef7538 100644 --- a/packages/components/table-v2/src/use-table.ts +++ b/packages/components/table-v2/src/use-table.ts @@ -1,120 +1,97 @@ +import { computed, ref, shallowRef, toRef, unref, watch } from 'vue' +import { isArray } from '@element-plus/utils' import { - computed, - getCurrentInstance, - ref, - shallowRef, - toRef, - unref, - watch, -} from 'vue' -import { debounce } from 'lodash-unified' -import { addUnit, isArray, isNumber, isObject } from '@element-plus/utils' -import { enforceUnit, sum } from './utils' -import { useColumns } from './use-columns' -import { FixedDir, SortOrder, oppositeOrderMap } from './constants' + useColumns, + useData, + useRow, + useScrollbar, + useStyles, +} from './composables' -import type { CSSProperties } from 'vue' -import type { FixedDirection, KeyType } from './types' import type { TableV2Props } from './table' -import type { onRowRenderedParams } from './grid' -import type { - RowExpandParams, - RowHeightChangedParams, - RowHoverParams, -} from './row' -// component import type { TableGridInstance } from './table-grid' -type Nullable = T | null -type ScrollPos = { scrollLeft: number; scrollTop: number } -type Heights = Record - function useTable(props: TableV2Props) { - const vm = getCurrentInstance()! - const { emit } = vm - + const mainTableRef = ref() + const leftTableRef = ref() + const rightTableRef = ref() const { columns, columnsStyles, columnsTotalWidth, fixedColumnsOnLeft, - fixedColumnOnRight, + fixedColumnsOnRight, hasFixedColumns, mainColumns, - getColumn, - } = useColumns(toRef(props, 'columns'), toRef(props, 'fixed')) + + onColumnSorted, + } = useColumns(props, toRef(props, 'columns'), toRef(props, 'fixed')) + + const { + scrollTo, + scrollToLeft, + scrollToTop, + onScroll, + onVerticalScroll, + scrollPos, + } = useScrollbar(props, { + mainTableRef, + leftTableRef, + rightTableRef, + + onMaybeEndReached, + }) + + const { + expandedRowKeys, + hoveringRowKey, + lastRenderedRowIndex, + isDynamic, + isResetting, + rowHeights, + + resetAfterIndex, + onRowExpanded, + onRowHeightChange, + onRowHovered, + onRowsRendered, + } = useRow(props, { + mainTableRef, + leftTableRef, + rightTableRef, + + onMaybeEndReached, + }) + + const { data, depthMap } = useData(props, { + expandedRowKeys, + lastRenderedRowIndex, + resetAfterIndex, + }) + + const { + bodyWidth, + fixedTableHeight, + mainTableHeight, + leftTableWidth, + rightTableWidth, + headerWidth, + rowsHeight, + windowHeight, + footerHeight, + emptyStyle, + rootStyle, + } = useStyles(props, { + columnsTotalWidth, + data, + fixedColumnsOnLeft, + fixedColumnsOnRight, + }) // state - const expandedRowKeys = ref(props.defaultExpandedRowKeys || []) - const depthMap = ref>({}) - const hoveringRowKey = shallowRef>(null) - // const resizingKey = shallowRef>(null) - // const resizingWidth = shallowRef(0) - const resetIndex = shallowRef>(null) - const isResetting = shallowRef(false) const isScrolling = shallowRef(false) // DOM/Component refs const containerRef = ref() - const mainTableRef = ref() - const leftTableRef = ref() - const rightTableRef = ref() - - const scrollPos = ref({ scrollLeft: 0, scrollTop: 0 }) - const lastRenderedRowIndex = ref(-1) - const rowHeights = ref({}) - const pendingRowHeights = ref({}) - const leftTableHeights = shallowRef({}) - const mainTableHeights = shallowRef({}) - const rightTableHeights = shallowRef({}) - - const rowsHeight = computed(() => { - const { rowHeight, estimatedRowHeight } = props - const _data = unref(data) - if (isNumber(estimatedRowHeight)) { - return _data.length * estimatedRowHeight - } - - return _data.length * rowHeight - }) - - const flattenedData = computed(() => { - const depths: Record = {} - const { data, rowKey } = props - - const _expandedRowKeys = unref(expandedRowKeys) - - if (!_expandedRowKeys || !_expandedRowKeys.length) return data - - const array: any[] = [] - const keysSet = new Set() - _expandedRowKeys.forEach((x) => keysSet.add(x)) - - let copy: any[] = data.slice() - copy.forEach((x) => (depths[x[rowKey]] = 0)) - while (copy.length > 0) { - const item = copy.shift()! - - array.push(item) - if ( - keysSet.has(item[rowKey]) && - Array.isArray(item.children) && - item.children.length > 0 - ) { - copy = [...item.children, ...copy] - item.children.forEach( - (child: any) => (depths[child[rowKey]] = depths[item[rowKey]] + 1) - ) - } - } - - depthMap.value = depths - return array - }) - - const data = computed(() => { - const { data, expandColumnKey } = props - return expandColumnKey ? unref(flattenedData) : data - }) const showEmpty = computed(() => { const noData = unref(data).length === 0 @@ -124,109 +101,14 @@ function useTable(props: TableV2Props) { : noData }) - const isDynamic = computed(() => isNumber(props.estimatedRowHeight)) + function getRowHeight(rowIndex: number) { + const { estimatedRowHeight, rowHeight, rowKey } = props - const bodyWidth = computed(() => { - const { fixed, width, vScrollbarSize } = props - const ret = width - vScrollbarSize - return fixed ? Math.max(Math.round(unref(columnsTotalWidth)), ret) : ret - }) + if (!estimatedRowHeight) return rowHeight - const rootStyle = computed(() => { - const { style = {}, height, width } = props - return enforceUnit({ - ...style, - height, - width, - }) - }) - - const headerWidth = computed( - () => unref(bodyWidth) + (props.fixed ? props.vScrollbarSize : 0) - ) - - const mainTableHeight = computed(() => { - const { height = 0, maxHeight = 0, footerHeight, hScrollbarSize } = props - - if (maxHeight > 0) { - const _fixedRowsHeight = unref(fixedRowsHeight) - const _rowsHeight = unref(rowsHeight) - const _headerHeight = unref(headerHeight) - const total = - _headerHeight + _fixedRowsHeight + _rowsHeight + hScrollbarSize - - return Math.min(total, maxHeight - footerHeight) - } - - return height - footerHeight - }) - - const footerHeight = computed(() => - enforceUnit({ height: props.footerHeight }) - ) - - const emptyStyle = computed(() => ({ - top: addUnit(unref(headerHeight)), - bottom: addUnit(props.footerHeight), - width: addUnit(props.width), - })) - - const fixedTableHeight = computed(() => { - const { maxHeight } = props - const tableHeight = unref(mainTableHeight) - if (isNumber(maxHeight) && maxHeight > 0) return tableHeight - - const totalHeight = - unref(rowsHeight) + unref(headerHeight) + unref(fixedRowsHeight) - - return Math.min(tableHeight, totalHeight) - }) - - const mapColumn = (column: TableV2Props['columns'][number]) => column.width - - const leftTableWidth = computed(() => - sum(unref(fixedColumnsOnLeft).map(mapColumn)) - ) - - const rightTableWidth = computed(() => - sum(unref(fixedColumnOnRight).map(mapColumn)) - ) - - const headerHeight = computed(() => sum(props.headerHeight)) - - const fixedRowsHeight = computed(() => { - return (props.fixedData?.length || 0) * props.rowHeight - }) - - const windowHeight = computed(() => { - return unref(mainTableHeight) - unref(headerHeight) - unref(fixedRowsHeight) - }) - - function doScroll(params: ScrollPos) { - const { scrollTop } = params - - mainTableRef.value?.scrollTo(params) - leftTableRef.value?.scrollToTop(scrollTop) - rightTableRef.value?.scrollToTop(scrollTop) - } - - // methods - function scrollTo(params: ScrollPos) { - scrollPos.value = params - - doScroll(params) - } - - function scrollToTop(scrollTop: number) { - scrollPos.value.scrollTop = scrollTop - - doScroll(unref(scrollPos)) - } - - function scrollToLeft(scrollLeft: number) { - scrollPos.value.scrollLeft = scrollLeft - - mainTableRef.value?.scrollTo?.(unref(scrollPos)) + return ( + unref(rowHeights)[unref(data)[rowIndex][rowKey]] || estimatedRowHeight + ) } function onMaybeEndReached() { @@ -249,162 +131,8 @@ function useTable(props: TableV2Props) { } } - function getRowHeight(rowIndex: number) { - const { estimatedRowHeight, rowHeight, rowKey } = props - - if (!estimatedRowHeight) return rowHeight - - return ( - unref(rowHeights)[unref(data)[rowIndex][rowKey]] || estimatedRowHeight - ) - } - - const flushingRowHeights = debounce(() => { - // console.log('update') - isResetting.value = true - // console.log(JSON.stringify(unref(pendingRowHeights))) - rowHeights.value = { ...unref(rowHeights), ...unref(pendingRowHeights) } - resetAfterIndex(unref(resetIndex)!, false) - pendingRowHeights.value = {} - // force update - resetIndex.value = null - mainTableRef.value?.forceUpdate() - leftTableRef.value?.forceUpdate() - rightTableRef.value?.forceUpdate() - vm.proxy?.$forceUpdate() - isResetting.value = false - }, 0) - - function resetAfterIndex(index: number, forceUpdate = false) { - if (!unref(isDynamic)) return - ;[mainTableRef, leftTableRef, rightTableRef].forEach((tableRef) => { - const table = unref(tableRef) - if (table) table.resetAfterRowIndex(index, forceUpdate) - }) - } // events - function onScroll(params: ScrollPos) { - scrollTo(params) - props.onScroll?.(params) - } - - function onVerticalScroll({ scrollTop }: ScrollPos) { - const { scrollTop: currentScrollTop } = unref(scrollPos) - if (scrollTop !== currentScrollTop) scrollToTop(scrollTop) - } - - function onRowsRendered(params: onRowRenderedParams) { - props.onRowRendered?.(params) - - if (params.rowCacheEnd > unref(lastRenderedRowIndex)) { - lastRenderedRowIndex.value = params.rowCacheEnd - } - } - - function onRowHovered({ hovered, rowKey }: RowHoverParams) { - hoveringRowKey.value = hovered ? rowKey : null - } - - function onRowExpanded({ - expanded, - rowData, - rowIndex, - rowKey, - }: RowExpandParams) { - const _expandedRowKeys = [...unref(expandedRowKeys)] - const currentKeyIndex = _expandedRowKeys.indexOf(rowKey) - if (expanded) { - if (currentKeyIndex === -1) _expandedRowKeys.push(rowKey) - } else { - if (currentKeyIndex > -1) _expandedRowKeys.splice(currentKeyIndex, 1) - } - expandedRowKeys.value = _expandedRowKeys - - emit('update:expandedRowKeys', _expandedRowKeys) - props.onRowExpand?.({ - expanded, - rowData, - rowIndex, - rowKey, - }) - // If this is not controlled, then use this to notify changes - props.onExpandedRowsChange?.(_expandedRowKeys) - } - - function onColumnSorted(e: MouseEvent) { - const { key } = (e.currentTarget as HTMLElement).dataset - if (!key) return - const { sortState, sortBy } = props - - let order = SortOrder.ASC - - if (isObject(sortState)) { - order = oppositeOrderMap[sortState[key]] - } else { - order = oppositeOrderMap[sortBy.order] - } - - props.onColumnSort?.({ column: getColumn(key)!, key, order }) - } - - function resetHeights(rowKey: KeyType, height: number, rowIdx: number) { - const resetIdx = unref(resetIndex) - if (resetIdx === null) { - resetIndex.value = rowIdx - } else { - if (resetIdx > rowIdx) { - resetIndex.value = rowIdx - } - } - - pendingRowHeights.value[rowKey] = height - } - - function onRowHeightChange( - { rowKey, height, rowIndex }: RowHeightChangedParams, - fixedDir: FixedDirection - ) { - if (!fixedDir) { - mainTableHeights.value[rowKey] = height - } else { - if (fixedDir === FixedDir.RIGHT) { - rightTableHeights.value[rowKey] = height - } else { - leftTableHeights.value[rowKey] = height - } - } - - const maximumHeight = Math.max( - ...[leftTableHeights, rightTableHeights, mainTableHeights].map( - (records) => records.value[rowKey] || 0 - ) - ) - - if (unref(rowHeights)[rowKey] !== maximumHeight) { - resetHeights(rowKey, maximumHeight, rowIndex) - flushingRowHeights() - } - } - - // When scrollTop changes, maybe reaching the bottom - watch( - () => unref(scrollPos).scrollTop, - (cur, prev) => { - if (cur > prev) onMaybeEndReached() - } - ) - - watch(data, (val, prev) => { - if (val !== prev) { - lastRenderedRowIndex.value = -1 - resetAfterIndex(0, true) - } - }) - - // when rendered row changes, maybe reaching the bottom - watch(lastRenderedRowIndex, () => onMaybeEndReached()) - watch( () => props.expandedRowKeys, (val) => (expandedRowKeys.value = val), @@ -433,7 +161,7 @@ function useTable(props: TableV2Props) { expandedRowKeys, depthMap, fixedColumnsOnLeft, - fixedColumnOnRight, + fixedColumnsOnRight, mainColumns, // metadata bodyWidth, @@ -450,9 +178,6 @@ function useTable(props: TableV2Props) { // methods getRowHeight, - scrollTo, - scrollToLeft, - scrollToTop, // event handlers onColumnSorted, @@ -460,6 +185,10 @@ function useTable(props: TableV2Props) { onRowExpanded, onRowsRendered, onRowHeightChange, + // use scrollbars + scrollTo, + scrollToLeft, + scrollToTop, onScroll, onVerticalScroll, } diff --git a/packages/components/virtual-list/src/types.ts b/packages/components/virtual-list/src/types.ts index 9081573bc1..f39f805c76 100644 --- a/packages/components/virtual-list/src/types.ts +++ b/packages/components/virtual-list/src/types.ts @@ -204,7 +204,9 @@ export type GridItemRenderedEvtParams = { export type GridScrollOptions = { scrollLeft?: number; scrollTop?: number } -export type GridItemKeyGetter = (args: { +export type GridItemKeyGetter = < + T extends { [key: string | number]: any } +>(args: { columnIndex: number data: T rowIndex: number