From 60ea53ce914be54bca4608f3439735f14322b7b9 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Fri, 3 Sep 2021 23:07:39 +0800 Subject: [PATCH] refactor: table --- ...etScrollBarSize.js => getScrollBarSize.ts} | 32 +- components/_util/props-util/index.js | 1 + components/_util/util.js | 5 + components/new-table/Body/BodyRow.tsx | 227 ++++++ components/new-table/Body/ExpandedRow.tsx | 78 +++ components/new-table/Body/MeasureCell.tsx | 21 + components/new-table/Body/index.tsx | 134 ++++ components/new-table/Cell/index.tsx | 6 +- components/new-table/FixedHolder/index.tsx | 188 +++++ components/new-table/Footer/Cell.tsx | 60 ++ components/new-table/Footer/Row.tsx | 3 + components/new-table/Footer/Summary.tsx | 26 + components/new-table/Footer/index.tsx | 35 + components/new-table/Header/Header.tsx | 8 +- components/new-table/Header/HeaderRow.tsx | 14 +- components/new-table/Table.tsx | 654 ++++++++++++++++++ .../new-table/context/SummaryContext.tsx | 21 + components/new-table/hooks/useColumns.tsx | 82 ++- .../new-table/hooks/useFlattenRecords.ts | 8 +- components/new-table/interface.ts | 29 +- components/new-table/stickyScrollBar.tsx | 211 ++++++ components/new-table/utils/legacyUtil.ts | 2 +- 22 files changed, 1766 insertions(+), 79 deletions(-) rename components/_util/{getScrollBarSize.js => getScrollBarSize.ts} (53%) create mode 100644 components/new-table/Body/BodyRow.tsx create mode 100644 components/new-table/Body/ExpandedRow.tsx create mode 100644 components/new-table/Body/MeasureCell.tsx create mode 100644 components/new-table/Body/index.tsx create mode 100644 components/new-table/FixedHolder/index.tsx create mode 100644 components/new-table/Footer/Cell.tsx create mode 100644 components/new-table/Footer/Row.tsx create mode 100644 components/new-table/Footer/Summary.tsx create mode 100644 components/new-table/Footer/index.tsx create mode 100644 components/new-table/Table.tsx create mode 100644 components/new-table/context/SummaryContext.tsx create mode 100644 components/new-table/stickyScrollBar.tsx diff --git a/components/_util/getScrollBarSize.js b/components/_util/getScrollBarSize.ts similarity index 53% rename from components/_util/getScrollBarSize.js rename to components/_util/getScrollBarSize.ts index a98df563d..12a142f3f 100644 --- a/components/_util/getScrollBarSize.js +++ b/components/_util/getScrollBarSize.ts @@ -1,6 +1,12 @@ -let cached; +/* eslint-disable no-param-reassign */ + +let cached: number; + +export default function getScrollBarSize(fresh?: boolean) { + if (typeof document === 'undefined') { + return 0; + } -export default function getScrollBarSize(fresh) { if (fresh || cached === undefined) { const inner = document.createElement('div'); inner.style.width = '100%'; @@ -10,8 +16,8 @@ export default function getScrollBarSize(fresh) { const outerStyle = outer.style; outerStyle.position = 'absolute'; - outerStyle.top = 0; - outerStyle.left = 0; + outerStyle.top = '0'; + outerStyle.left = '0'; outerStyle.pointerEvents = 'none'; outerStyle.visibility = 'hidden'; outerStyle.width = '200px'; @@ -36,3 +42,21 @@ export default function getScrollBarSize(fresh) { } return cached; } + +function ensureSize(str: string) { + const match = str.match(/^(.*)px$/); + const value = Number(match?.[1]); + return Number.isNaN(value) ? getScrollBarSize() : value; +} + +export function getTargetScrollBarSize(target: HTMLElement) { + if (typeof document === 'undefined' || !target || !(target instanceof Element)) { + return { width: 0, height: 0 }; + } + + const { width, height } = getComputedStyle(target, '::-webkit-scrollbar'); + return { + width: ensureSize(width), + height: ensureSize(height), + }; +} diff --git a/components/_util/props-util/index.js b/components/_util/props-util/index.js index 440f3a9d2..8a76d49bc 100644 --- a/components/_util/props-util/index.js +++ b/components/_util/props-util/index.js @@ -29,6 +29,7 @@ const parseStyleText = (cssText = '', camel) => { const res = {}; const listDelimiter = /;(?![^(]*\))/g; const propertyDelimiter = /:(.+)/; + if (typeof cssText === 'object') return cssText; cssText.split(listDelimiter).forEach(function (item) { if (item) { const tmp = item.split(propertyDelimiter); diff --git a/components/_util/util.js b/components/_util/util.js index 409bc85e7..d1b647dbd 100644 --- a/components/_util/util.js +++ b/components/_util/util.js @@ -62,4 +62,9 @@ export function getDataAndAriaProps(props) { }, {}); } +export function toPx(val) { + if (typeof val === 'number') return `${val}px`; + return val; +} + export { isOn, cacheStringFunction, camelize, hyphenate, capitalize, resolvePropValue }; diff --git a/components/new-table/Body/BodyRow.tsx b/components/new-table/Body/BodyRow.tsx new file mode 100644 index 000000000..ca881d635 --- /dev/null +++ b/components/new-table/Body/BodyRow.tsx @@ -0,0 +1,227 @@ +import Cell from '../Cell'; +import { getColumnsKey } from '../utils/valueUtil'; +import type { CustomizeComponent, GetComponentProps, Key, GetRowKey } from '../interface'; +import ExpandedRow from './ExpandedRow'; +import { computed, defineComponent, ref, watchEffect } from 'vue'; +import { useInjectTable } from '../context/TableContext'; +import { useInjectBody } from '../context/BodyContext'; +import classNames from 'ant-design-vue/es/_util/classNames'; +import { parseStyleText } from 'ant-design-vue/es/_util/props-util'; + +export interface BodyRowProps { + record: RecordType; + index: number; + recordKey: Key; + expandedKeys: Set; + rowComponent: CustomizeComponent; + cellComponent: CustomizeComponent; + customRow: GetComponentProps; + rowExpandable: (record: RecordType) => boolean; + indent?: number; + rowKey: Key; + getRowKey: GetRowKey; + childrenColumnName: string; +} + +export default defineComponent>({ + props: [ + 'record', + 'index', + 'recordKey', + 'expandedKeys', + 'rowComponent', + 'cellComponent', + 'customRow', + 'rowExpandable', + 'indent', + 'rowKey', + 'getRowKey', + 'childrenColumnName', + ] as any, + name: 'BodyRow', + inheritAttrs: false, + setup(props, { attrs }) { + const tableContext = useInjectTable(); + const bodyContext = useInjectBody(); + const expandRended = ref(false); + + const expanded = computed(() => props.expandedKeys && props.expandedKeys.has(props.recordKey)); + + watchEffect(() => { + if (expanded.value) { + expandRended.value = true; + } + }); + + const rowSupportExpand = computed( + () => + bodyContext.expandableType === 'row' && + (!props.rowExpandable || props.rowExpandable(props.record)), + ); + // Only when row is not expandable and `children` exist in record + const nestExpandable = computed(() => bodyContext.expandableType === 'nest'); + const hasNestChildren = computed( + () => props.childrenColumnName && props.record && props.record[props.childrenColumnName], + ); + const mergedExpandable = computed(() => rowSupportExpand.value || nestExpandable.value); + + const onInternalTriggerExpand = (record, event) => { + bodyContext.onTriggerExpand(record, event); + }; + + // =========================== onRow =========================== + let additionalProps = computed>( + () => props.customRow?.(props.record, props.index) || {}, + ); + + const onClick = (event, ...args) => { + if (bodyContext.expandRowByClick && mergedExpandable.value) { + onInternalTriggerExpand(props.record, event); + } + + if (additionalProps.value?.onClick) { + additionalProps.value.onClick(event, ...args); + } + }; + + let computeRowClassName = computed(() => { + const { record, index, indent } = props; + const { rowClassName } = bodyContext; + if (typeof rowClassName === 'string') { + return rowClassName; + } else if (typeof rowClassName === 'function') { + return rowClassName(record, index, indent); + } + }); + + const columnsKey = computed(() => getColumnsKey(bodyContext.flattenColumns)); + + return () => { + const { class: className, style } = attrs as any; + const { + record, + index, + rowKey, + indent = 0, + rowComponent: RowComponent, + cellComponent, + } = props; + const { prefixCls, fixedInfoList } = tableContext; + const { + fixHeader, + fixColumn, + horizonScroll, + componentWidth, + flattenColumns, + expandedRowClassName, + indentSize, + expandIcon, + expandedRowRender, + expandIconColumnIndex, + } = bodyContext; + const baseRowNode = ( + + {flattenColumns.map((column, colIndex) => { + const { customRender, dataIndex, className: columnClassName } = column; + + const key = columnsKey[colIndex]; + const fixedInfo = fixedInfoList[colIndex]; + + // ============= Used for nest expandable ============= + let appendCellNode; + if (colIndex === (expandIconColumnIndex || 0) && nestExpandable.value) { + appendCellNode = ( + <> + + {expandIcon({ + prefixCls, + expanded: expanded.value, + expandable: hasNestChildren.value, + record, + onExpand: onInternalTriggerExpand, + })} + + ); + } + + let additionalCellProps; + if (column.customCell) { + additionalCellProps = column.customCell(record, index); + } + + return ( + + ); + })} + + ); + + // ======================== Expand Row ========================= + let expandRowNode; + if (rowSupportExpand.value && (expandRended.value || expanded.value)) { + const expandContent = expandedRowRender(record, index, indent + 1, expanded.value); + const computedExpandedRowClassName = + expandedRowClassName && expandedRowClassName(record, index, indent); + expandRowNode = ( + + {expandContent} + + ); + } + + return ( + <> + {baseRowNode} + {expandRowNode} + + ); + }; + }, +}); diff --git a/components/new-table/Body/ExpandedRow.tsx b/components/new-table/Body/ExpandedRow.tsx new file mode 100644 index 000000000..6de09ea3c --- /dev/null +++ b/components/new-table/Body/ExpandedRow.tsx @@ -0,0 +1,78 @@ +import { CustomizeComponent } from '../interface'; +import Cell from '../Cell'; +import { defineComponent } from 'vue'; +import { useInjectTable } from '../context/TableContext'; + +export interface ExpandedRowProps { + prefixCls: string; + component: CustomizeComponent; + cellComponent: CustomizeComponent; + fixHeader: boolean; + fixColumn: boolean; + horizonScroll: boolean; + componentWidth: number; + expanded: boolean; + colSpan: number; +} + +export default defineComponent({ + name: 'ExpandedRow', + props: [ + 'prefixCls', + 'component', + 'cellComponent', + 'fixHeader', + 'fixColumn', + 'horizonScroll', + 'componentWidth', + 'expanded', + 'colSpan', + ] as any, + inheritAttrs: false, + setup(props, { slots, attrs }) { + const tableContext = useInjectTable(); + return () => { + const { + prefixCls, + component: Component, + cellComponent, + fixHeader, + fixColumn, + expanded, + componentWidth, + colSpan, + } = props; + + let contentNode: any = slots.default?.(); + + if (fixColumn) { + contentNode = ( +
+ {contentNode} +
+ ); + } + + return ( + + + {contentNode} + + + ); + }; + }, +}); diff --git a/components/new-table/Body/MeasureCell.tsx b/components/new-table/Body/MeasureCell.tsx new file mode 100644 index 000000000..a972579ed --- /dev/null +++ b/components/new-table/Body/MeasureCell.tsx @@ -0,0 +1,21 @@ +import VCResizeObserver from 'ant-design-vue/es/vc-resize-observer'; +import { Key } from '../interface'; + +export interface MeasureCellProps { + columnKey: Key; + onColumnResize: (key: Key, width: number) => void; +} + +export default function MeasureCell({ columnKey }: MeasureCellProps, { emit }) { + return ( + { + emit('columnResize', columnKey, offsetWidth); + }} + > + +
 
+ +
+ ); +} diff --git a/components/new-table/Body/index.tsx b/components/new-table/Body/index.tsx new file mode 100644 index 000000000..51fa6c6ae --- /dev/null +++ b/components/new-table/Body/index.tsx @@ -0,0 +1,134 @@ +import type { GetRowKey, Key, GetComponentProps } from '../interface'; +import ExpandedRow from './ExpandedRow'; +import { getColumnsKey } from '../utils/valueUtil'; +import MeasureCell from './MeasureCell'; +import BodyRow from './BodyRow'; +import useFlattenRecords from '../hooks/useFlattenRecords'; +import { defineComponent, toRef } from 'vue'; +import { useInjectResize } from '../context/ResizeContext'; +import { useInjectTable } from '../context/TableContext'; +import { useInjectBody } from '../context/BodyContext'; + +export interface BodyProps { + data: RecordType[]; + getRowKey: GetRowKey; + measureColumnWidth: boolean; + expandedKeys: Set; + customRow: GetComponentProps; + rowExpandable: (record: RecordType) => boolean; + // emptyNode: React.ReactNode; + childrenColumnName: string; +} + +export default defineComponent>({ + name: 'Body', + props: [ + 'data', + 'getRowKey', + 'measureColumnWidth', + 'expandedKeys', + 'customRow', + 'rowExpandable', + 'childrenColumnName', + ] as any, + slots: ['emptyNode'], + setup(props, { slots }) { + const resizeContext = useInjectResize(); + const tableContext = useInjectTable(); + const bodyContext = useInjectBody(); + + const flattenData = useFlattenRecords( + toRef(props, 'data'), + toRef(props, 'childrenColumnName'), + toRef(props, 'expandedKeys'), + toRef(props, 'getRowKey'), + ); + + return () => { + const { + data, + getRowKey, + measureColumnWidth, + expandedKeys, + customRow, + rowExpandable, + childrenColumnName, + } = props; + const { onColumnResize } = resizeContext; + const { prefixCls, getComponent } = tableContext; + const { fixHeader, horizonScroll, flattenColumns, componentWidth } = bodyContext; + const WrapperComponent = getComponent(['body', 'wrapper'], 'tbody'); + const trComponent = getComponent(['body', 'row'], 'tr'); + const tdComponent = getComponent(['body', 'cell'], 'td'); + + let rows; + if (data.length) { + rows = flattenData.value.map((item, index) => { + const { record, indent } = item; + + const key = getRowKey(record, index); + + return ( + + ); + }); + } else { + rows = ( + + {slots.emptyNode?.()} + + ); + } + + const columnsKey = getColumnsKey(flattenColumns); + + return ( + + {/* Measure body column width with additional hidden col */} + {measureColumnWidth && ( + + {columnsKey.map(columnKey => ( + + ))} + + )} + + {rows} + + ); + }; + }, +}); diff --git a/components/new-table/Cell/index.tsx b/components/new-table/Cell/index.tsx index a7632be47..8367532c5 100644 --- a/components/new-table/Cell/index.tsx +++ b/components/new-table/Cell/index.tsx @@ -1,5 +1,5 @@ import classNames from 'ant-design-vue/es/_util/classNames'; -import { isValidElement } from 'ant-design-vue/es/_util/props-util'; +import { isValidElement, parseStyleText } from 'ant-design-vue/es/_util/props-util'; import { CSSProperties, defineComponent, HTMLAttributes } from 'vue'; import type { @@ -46,7 +46,7 @@ export interface CellProps { // Additional /** @private Used for `expandable` with nest tree */ appendNode?: any; - additionalProps?: Omit & { style?: CSSProperties }; + additionalProps?: HTMLAttributes; rowType?: 'header' | 'body' | 'footer'; @@ -196,7 +196,7 @@ export default defineComponent({ cellClass, ), style: { - ...additionalProps.style, + ...parseStyleText(additionalProps.style as any), ...alignStyle, ...fixedStyle, ...cellStyle, diff --git a/components/new-table/FixedHolder/index.tsx b/components/new-table/FixedHolder/index.tsx new file mode 100644 index 000000000..6ce1ffaa5 --- /dev/null +++ b/components/new-table/FixedHolder/index.tsx @@ -0,0 +1,188 @@ +import type { HeaderProps } from '../Header/Header'; +import ColGroup from '../ColGroup'; +import type { ColumnsType, ColumnType, DefaultRecordType } from '../interface'; +import { + computed, + defineComponent, + nextTick, + onBeforeUnmount, + onMounted, + ref, + Ref, + toRef, + watchEffect, +} from 'vue'; +import { useInjectTable } from '../context/TableContext'; +import classNames from 'ant-design-vue/es/_util/classNames'; + +function useColumnWidth(colWidthsRef: Ref, columCountRef: Ref) { + return computed(() => { + const cloneColumns: number[] = []; + const colWidths = colWidthsRef.value; + const columCount = columCountRef.value; + for (let i = 0; i < columCount; i += 1) { + const val = colWidths[i]; + if (val !== undefined) { + cloneColumns[i] = val; + } else { + return null; + } + } + return cloneColumns; + }); +} + +export interface FixedHeaderProps extends HeaderProps { + noData: boolean; + maxContentScroll: boolean; + colWidths: readonly number[]; + columCount: number; + direction: 'ltr' | 'rtl'; + fixHeader: boolean; + stickyTopOffset?: number; + stickyBottomOffset?: number; + stickyClassName?: string; + onScroll: (info: { currentTarget: HTMLDivElement; scrollLeft?: number }) => void; +} + +export default defineComponent>({ + name: 'FixedHolder', + props: [ + 'columns', + 'flattenColumns', + 'stickyOffsets', + 'customHeaderRow', + 'noData', + 'maxContentScroll', + 'colWidths', + 'columCount', + 'direction', + 'fixHeader', + 'stickyTopOffset', + 'stickyBottomOffset', + 'stickyClassName', + ] as any, + emits: ['scroll'], + inheritAttrs: false, + setup(props, { attrs, slots, emit }) { + const tableContext = useInjectTable(); + const combinationScrollBarSize = computed(() => + tableContext.isSticky && !props.fixHeader ? 0 : tableContext.scrollbarSize, + ); + const scrollRef = ref(); + function onWheel(e: WheelEvent) { + const { currentTarget, deltaX } = e; + if (deltaX) { + emit('scroll', { currentTarget, scrollLeft: (currentTarget as any).scrollLeft + deltaX }); + e.preventDefault(); + } + } + onMounted(() => { + nextTick(() => { + scrollRef.value?.addEventListener('wheel', onWheel); + }); + }); + onBeforeUnmount(() => { + scrollRef.value?.removeEventListener('wheel', onWheel); + }); + + // Check if all flattenColumns has width + const allFlattenColumnsWithWidth = computed(() => + props.flattenColumns.every( + column => column.width && column.width !== 0 && column.width !== '0px', + ), + ); + + const columnsWithScrollbar = ref>([]); + const flattenColumnsWithScrollbar = ref>([]); + + watchEffect(() => { + // Add scrollbar column + const lastColumn = props.flattenColumns[props.flattenColumns.length - 1]; + const ScrollBarColumn: ColumnType & { scrollbar: true } = { + fixed: lastColumn ? lastColumn.fixed : null, + scrollbar: true, + customHeaderCell: () => ({ + class: `${tableContext.prefixCls}-cell-scrollbar`, + }), + }; + + columnsWithScrollbar.value = combinationScrollBarSize + ? [...props.columns, ScrollBarColumn] + : props.columns; + + flattenColumnsWithScrollbar.value = combinationScrollBarSize + ? [...props.flattenColumns, ScrollBarColumn] + : props.flattenColumns; + }); + + // Calculate the sticky offsets + const headerStickyOffsets = computed(() => { + const { stickyOffsets, direction } = props; + const { right, left } = stickyOffsets; + return { + ...stickyOffsets, + left: + direction === 'rtl' + ? [...left.map(width => width + combinationScrollBarSize.value), 0] + : left, + right: + direction === 'rtl' + ? right + : [...right.map(width => width + combinationScrollBarSize.value), 0], + isSticky: tableContext.isSticky, + }; + }); + + const mergedColumnWidth = useColumnWidth(toRef(props, 'colWidths'), toRef(props, 'columCount')); + + return () => { + const { + noData, + columCount, + stickyTopOffset, + stickyBottomOffset, + stickyClassName, + maxContentScroll, + } = props; + const { isSticky } = tableContext; + return ( +
+ + {(!noData || !maxContentScroll || allFlattenColumnsWithWidth.value) && ( + + )} + {slots.default?.({ + ...props, + stickyOffsets: headerStickyOffsets.value, + columns: columnsWithScrollbar.value, + flattenColumns: flattenColumnsWithScrollbar.value, + })} +
+
+ ); + }; + }, +}); diff --git a/components/new-table/Footer/Cell.tsx b/components/new-table/Footer/Cell.tsx new file mode 100644 index 000000000..043800fce --- /dev/null +++ b/components/new-table/Footer/Cell.tsx @@ -0,0 +1,60 @@ +import { defineComponent } from 'vue'; +import Cell from '../Cell'; +import { useInjectSummary } from '../context/SummaryContext'; +import { useInjectTable } from '../context/TableContext'; +import type { AlignType } from '../interface'; +import { getCellFixedInfo } from '../utils/fixUtil'; + +export interface SummaryCellProps { + className?: string; + children?: any; + index: number; + colSpan?: number; + rowSpan?: number; + align?: AlignType; +} + +export default defineComponent({ + name: 'SummaryCell', + props: ['index', 'colSpan', 'rowSpan', 'align'] as any, + inheritAttrs: false, + setup(props, { attrs, slots }) { + const tableContext = useInjectTable(); + const summaryContext = useInjectSummary(); + return () => { + const { index, colSpan, rowSpan, align } = props; + const { prefixCls, direction } = tableContext; + const { scrollColumnIndex, stickyOffsets, flattenColumns } = summaryContext; + const lastIndex = index + colSpan - 1; + const mergedColSpan = lastIndex + 1 === scrollColumnIndex ? colSpan + 1 : colSpan; + + const fixedInfo = getCellFixedInfo( + index, + index + mergedColSpan - 1, + flattenColumns, + stickyOffsets, + direction, + ); + + return ( + ({ + children: slots.defalut?.(), + props: { + colSpan: mergedColSpan, + rowSpan, + }, + })} + {...fixedInfo} + /> + ); + }; + }, +}); diff --git a/components/new-table/Footer/Row.tsx b/components/new-table/Footer/Row.tsx new file mode 100644 index 000000000..8edf81848 --- /dev/null +++ b/components/new-table/Footer/Row.tsx @@ -0,0 +1,3 @@ +export default function FooterRow(props, { slots }) { + return {slots.default?.()}; +} diff --git a/components/new-table/Footer/Summary.tsx b/components/new-table/Footer/Summary.tsx new file mode 100644 index 000000000..9e22280cf --- /dev/null +++ b/components/new-table/Footer/Summary.tsx @@ -0,0 +1,26 @@ +import { FunctionalComponent } from 'vue'; +import Cell from './Cell'; +import Row from './Row'; + +export interface SummaryProps { + fixed?: boolean | 'top' | 'bottom'; +} + +export interface SummaryFC extends FunctionalComponent { + Row: typeof Row; + Cell: typeof Cell; +} + +/** + * Syntactic sugar. Do not support HOC. + */ +const Summary: SummaryFC = (_props, { slots }) => { + return slots.default?.(); +}; + +Summary.Row = Row; +Summary.Cell = Cell; + +Summary.displayName = 'Summary'; + +export default Summary; diff --git a/components/new-table/Footer/index.tsx b/components/new-table/Footer/index.tsx new file mode 100644 index 000000000..1e1aff176 --- /dev/null +++ b/components/new-table/Footer/index.tsx @@ -0,0 +1,35 @@ +import Summary from './Summary'; +import type { DefaultRecordType, StickyOffsets } from '../interface'; +import { computed, defineComponent, reactive, toRef } from 'vue'; +import { FlattenColumns, useProvideSummary } from '../context/SummaryContext'; +import { useInjectTable } from '../context/TableContext'; + +export interface FooterProps { + stickyOffsets: StickyOffsets; + flattenColumns: FlattenColumns; +} + +export default defineComponent({ + props: ['stickyOffsets', 'flattenColumns'], + name: 'Footer', + setup(props, { slots }) { + const tableContext = useInjectTable(); + useProvideSummary( + reactive({ + stickyOffsets: toRef(props, 'stickyOffsets'), + flattenColumns: toRef(props, 'flattenColumns'), + scrollColumnIndex: computed(() => { + const lastColumnIndex = props.flattenColumns.length - 1; + const scrollColumn = props.flattenColumns[lastColumnIndex]; + return scrollColumn?.scrollbar ? lastColumnIndex : null; + }), + }), + ); + return () => { + const { prefixCls } = tableContext; + return {slots.default?.()}; + }; + }, +}); + +export const FooterComponents = Summary; diff --git a/components/new-table/Header/Header.tsx b/components/new-table/Header/Header.tsx index a723aa72a..81f528565 100644 --- a/components/new-table/Header/Header.tsx +++ b/components/new-table/Header/Header.tsx @@ -87,18 +87,18 @@ export interface HeaderProps { columns: ColumnsType; flattenColumns: readonly ColumnType[]; stickyOffsets: StickyOffsets; - onHeaderRow: GetComponentProps[]>; + customHeaderRow: GetComponentProps[]>; } export default defineComponent({ name: 'Header', - props: ['columns', 'flattenColumns', 'stickyOffsets', 'onHeaderRow'] as any, + props: ['columns', 'flattenColumns', 'stickyOffsets', 'customHeaderRow'] as any, setup(props) { const tableContext = useInjectTable(); const rows = computed(() => parseHeaderRows(props.columns)); return () => { const { prefixCls, getComponent } = tableContext; - const { stickyOffsets, flattenColumns, onHeaderRow } = props; + const { stickyOffsets, flattenColumns, customHeaderRow } = props; const WrapperComponent = getComponent(['header', 'wrapper'], 'thead'); const trComponent = getComponent(['header', 'row'], 'tr'); const thComponent = getComponent(['header', 'cell'], 'th'); @@ -113,7 +113,7 @@ export default defineComponent({ stickyOffsets={stickyOffsets} rowComponent={trComponent} cellComponent={thComponent} - onHeaderRow={onHeaderRow} + customHeaderRow={customHeaderRow} index={rowIndex} /> ); diff --git a/components/new-table/Header/HeaderRow.tsx b/components/new-table/Header/HeaderRow.tsx index af1b4e845..5a50c834e 100644 --- a/components/new-table/Header/HeaderRow.tsx +++ b/components/new-table/Header/HeaderRow.tsx @@ -18,7 +18,7 @@ export interface RowProps { flattenColumns: readonly ColumnType[]; rowComponent: CustomizeComponent; cellComponent: CustomizeComponent; - onHeaderRow: GetComponentProps[]>; + customHeaderRow: GetComponentProps[]>; index: number; } @@ -31,7 +31,7 @@ export default defineComponent({ 'rowComponent', 'cellComponent', 'index', - 'onHeaderRow', + 'customHeaderRow', ] as any, setup(props: RowProps) { const tableContext = useInjectTable(); @@ -43,13 +43,13 @@ export default defineComponent({ flattenColumns, rowComponent: RowComponent, cellComponent: CellComponent, - onHeaderRow, + customHeaderRow, index, } = props; let rowProps; - if (onHeaderRow) { - rowProps = onHeaderRow( + if (customHeaderRow) { + rowProps = customHeaderRow( cells.map(cell => cell.column), index, ); @@ -70,8 +70,8 @@ export default defineComponent({ ); let additionalProps; - if (column && column.onHeaderCell) { - additionalProps = cell.column.onHeaderCell(column); + if (column && column.customHeaderCell) { + additionalProps = cell.column.customHeaderCell(column); } return ( diff --git a/components/new-table/Table.tsx b/components/new-table/Table.tsx new file mode 100644 index 000000000..83630564a --- /dev/null +++ b/components/new-table/Table.tsx @@ -0,0 +1,654 @@ +import ColumnGroup from './sugar/ColumnGroup'; +import Column from './sugar/Column'; +import Header from './Header/Header'; +import type { + GetRowKey, + ColumnsType, + TableComponents, + Key, + DefaultRecordType, + TriggerEventHandler, + GetComponentProps, + ExpandableConfig, + LegacyExpandableProps, + GetComponent, + PanelRender, + TableLayout, + ExpandableType, + RowClassName, + CustomizeComponent, + ColumnType, + CustomizeScrollBody, + TableSticky, + FixedType, +} from './interface'; +import Body from './Body'; +import useColumns from './hooks/useColumns'; +import { useLayoutState, useTimeoutLock } from './hooks/useFrame'; +import { getPathValue, mergeObject, validateValue, getColumnsKey } from './utils/valueUtil'; +import useStickyOffsets from './hooks/useStickyOffsets'; +import ColGroup from './ColGroup'; +import { getExpandableProps, getDataAndAriaProps } from './utils/legacyUtil'; +import Panel from './Panel'; +import Footer, { FooterComponents } from './Footer'; +import { findAllChildrenKeys, renderExpandIcon } from './utils/expandUtil'; +import { getCellFixedInfo } from './utils/fixUtil'; +import StickyScrollBar from './stickyScrollBar'; +import useSticky from './hooks/useSticky'; +import FixedHolder from './FixedHolder'; +import type { SummaryProps } from './Footer/Summary'; +import Summary from './Footer/Summary'; +import { + computed, + CSSProperties, + defineComponent, + nextTick, + onMounted, + reactive, + ref, + toRef, + toRefs, + watch, + watchEffect, +} from 'vue'; +import { warning } from '../vc-util/warning'; +import { reactivePick } from '../_util/reactivePick'; +import useState from '../_util/hooks/useState'; +import { toPx } from '../_util/util'; +import isVisible from '../vc-util/Dom/isVisible'; +import { getTargetScrollBarSize } from '../_util/getScrollBarSize'; +import classNames from '../_util/classNames'; +import { EventHandler } from '../_util/EventInterface'; + +// Used for conditions cache +const EMPTY_DATA = []; + +// Used for customize scroll +const EMPTY_SCROLL_TARGET = {}; + +export const INTERNAL_HOOKS = 'rc-table-internal-hook'; + +export interface TableProps extends LegacyExpandableProps { + prefixCls?: string; + data?: RecordType[]; + columns?: ColumnsType; + rowKey?: string | GetRowKey; + tableLayout?: TableLayout; + + // Fixed Columns + scroll?: { x?: number | true | string; y?: number | string }; + + // Expandable + /** Config expand rows */ + expandable?: ExpandableConfig; + indentSize?: number; + rowClassName?: string | RowClassName; + + // Additional Part + // title?: PanelRender; + // footer?: PanelRender; + // summary?: (data: readonly RecordType[]) => any; + + // Customize + id?: string; + showHeader?: boolean; + components?: TableComponents; + customRow?: GetComponentProps; + customHeaderRow?: GetComponentProps[]>; + // emptyText?: any; + + direction?: 'ltr' | 'rtl'; + + expandFixed?: boolean; + expandColumnWidth?: number; + expandIconColumnIndex?: number; + + // // =================================== Internal =================================== + // /** + // * @private Internal usage, may remove by refactor. Should always use `columns` instead. + // * + // * !!! DO NOT USE IN PRODUCTION ENVIRONMENT !!! + // */ + // internalHooks?: string; + + // /** + // * @private Internal usage, may remove by refactor. Should always use `columns` instead. + // * + // * !!! DO NOT USE IN PRODUCTION ENVIRONMENT !!! + // */ + // // Used for antd table transform column with additional column + // transformColumns?: (columns: ColumnsType) => ColumnsType; + + // /** + // * @private Internal usage, may remove by refactor. + // * + // * !!! DO NOT USE IN PRODUCTION ENVIRONMENT !!! + // */ + // internalRefs?: { + // body: React.MutableRefObject; + // }; + + sticky?: boolean | TableSticky; +} + +export default defineComponent({ + name: 'Table', + slots: ['title', 'footer', 'summary', 'emptyText'], + inheritAttrs: false, + emits: ['expand', 'expandedRowsChange'], + setup(props, { slots, attrs, emit }) { + const mergedData = computed(() => props.data || EMPTY_DATA); + const hasData = computed(() => !!mergedData.value.length); + + // ==================== Customize ===================== + const mergedComponents = computed(() => + mergeObject>(props.components, {}), + ); + + const getComponent = (path, defaultComponent?: string) => + getPathValue>(mergedComponents.value, path) || + defaultComponent; + + const getRowKey = computed(() => { + const rowKey = props.rowKey; + if (typeof rowKey === 'function') { + return rowKey; + } + return record => { + const key = record && record[rowKey]; + + if (process.env.NODE_ENV !== 'production') { + warning( + key !== undefined, + 'Each record in table should have a unique `key` prop, or set `rowKey` to an unique primary key.', + ); + } + + return key; + }; + }); + + // ====================== Expand ====================== + + const mergedExpandIcon = computed(() => props.expandIcon || renderExpandIcon); + + const mergedChildrenColumnName = computed(() => props.childrenColumnName || 'children'); + + const expandableType = computed(() => { + if (props.expandedRowRender) { + return 'row'; + } + /* eslint-disable no-underscore-dangle */ + /** + * Fix https://github.com/ant-design/ant-design/issues/21154 + * This is a workaround to not to break current behavior. + * We can remove follow code after final release. + * + * To other developer: + * Do not use `__PARENT_RENDER_ICON__` in prod since we will remove this when refactor + */ + if ( + mergedData.value.some( + record => record && typeof record === 'object' && record[mergedChildrenColumnName.value], + ) + ) { + return 'nest'; + } + /* eslint-enable */ + return false; + }); + + const innerExpandedKeys = ref([]); + + // defalutXxxx 仅仅第一次生效 不用考虑响应式问题 + if (props.defaultExpandedRowKeys) { + innerExpandedKeys.value = props.defaultExpandedRowKeys; + } + if (props.defaultExpandAllRows) { + innerExpandedKeys.value = findAllChildrenKeys( + mergedData.value, + getRowKey.value, + mergedChildrenColumnName.value, + ); + } + const mergedExpandedKeys = computed( + () => new Set(props.expandedRowKeys || innerExpandedKeys.value || []), + ); + + const onTriggerExpand: TriggerEventHandler = record => { + const key = getRowKey.value(record, mergedData.value.indexOf(record)); + + let newExpandedKeys: Key[]; + const hasKey = mergedExpandedKeys.value.has(key); + if (hasKey) { + mergedExpandedKeys.value.delete(key); + newExpandedKeys = [...mergedExpandedKeys.value]; + } else { + newExpandedKeys = [...mergedExpandedKeys.value, key]; + } + innerExpandedKeys.value = newExpandedKeys; + + emit('expand', !hasKey, record); + emit('expandedRowsChange', newExpandedKeys); + }; + + const componentWidth = ref(0); + + const [columns, flattenColumns] = useColumns({ + ...toRefs(props), + + // children, + expandable: computed(() => !!props.expandedRowRender), + expandedKeys: mergedExpandedKeys, + getRowKey, + onTriggerExpand, + expandIcon: mergedExpandIcon, + }); + + const columnContext = computed(() => ({ + columns: columns.value, + flattenColumns: flattenColumns.value, + })); + + // ====================== Scroll ====================== + const fullTableRef = ref(); + const scrollHeaderRef = ref(); + const scrollBodyRef = ref(); + const scrollSummaryRef = ref(); + const [pingedLeft, setPingedLeft] = useState(false); + const [pingedRight, setPingedRight] = useState(false); + const [colsWidths, updateColsWidths] = useLayoutState(new Map()); + + // Convert map to number width + const colsKeys = computed(() => getColumnsKey(flattenColumns.value)); + const colWidths = computed(() => + colsKeys.value.map(columnKey => colsWidths.value.get(columnKey)), + ); + const columnCount = computed(() => flattenColumns.value.length); + const stickyOffsets = useStickyOffsets(colWidths, columnCount, toRef(props, 'direction')); + const fixHeader = computed(() => props.scroll && validateValue(props.scroll.y)); + const horizonScroll = computed( + () => (props.scroll && validateValue(props.scroll.x)) || Boolean(props.expandFixed), + ); + const fixColumn = computed( + () => horizonScroll.value && flattenColumns.value.some(({ fixed }) => fixed), + ); + + // Sticky + const stickyRef = ref<{ setScrollLeft: (left: number) => void }>(); + const stickyState = useSticky(toRef(props, 'sticky'), toRef(props, 'prefixCls')); + + const summaryFixedInfos = reactive>({}); + const fixFooter = computed(() => { + const info = Object.values(summaryFixedInfos)[0]; + return fixHeader.value || (stickyState.value.isSticky && info); + }); + + const summaryCollect = (uniKey: string, fixed: boolean | string) => { + if (fixed) { + summaryFixedInfos[uniKey] = fixed; + } else { + delete summaryFixedInfos[uniKey]; + } + }; + + // Scroll + let scrollXStyle = ref({}); + let scrollYStyle = ref({}); + let scrollTableStyle = ref({}); + + watchEffect(() => { + if (fixHeader.value) { + scrollYStyle.value = { + overflowY: 'scroll', + maxHeight: toPx(props.scroll.y), + }; + } + + if (horizonScroll.value) { + scrollXStyle.value = { overflowX: 'auto' }; + // When no vertical scrollbar, should hide it + // https://github.com/ant-design/ant-design/pull/20705 + // https://github.com/ant-design/ant-design/issues/21879 + if (!fixHeader.value) { + scrollYStyle.value = { overflowY: 'hidden' }; + } + scrollTableStyle.value = { + width: props.scroll.x === true ? 'auto' : toPx(props.scroll.x), + minWidth: '100%', + }; + } + }); + + const onColumnResize = (columnKey: Key, width: number) => { + if (isVisible(fullTableRef.value)) { + updateColsWidths(widths => { + if (widths.get(columnKey) !== width) { + const newWidths = new Map(widths); + newWidths.set(columnKey, width); + return newWidths; + } + return widths; + }); + } + }; + + const [setScrollTarget, getScrollTarget] = useTimeoutLock(null); + + function forceScroll(scrollLeft: number, target: HTMLDivElement | ((left: number) => void)) { + if (!target) { + return; + } + if (typeof target === 'function') { + target(scrollLeft); + } else if (target.scrollLeft !== scrollLeft) { + // eslint-disable-next-line no-param-reassign + target.scrollLeft = scrollLeft; + } + } + + const onScroll: EventHandler = ({ + currentTarget, + scrollLeft, + }: { + currentTarget: HTMLElement; + scrollLeft?: number; + }) => { + const isRTL = props.direction === 'rtl'; + const mergedScrollLeft = + typeof scrollLeft === 'number' ? scrollLeft : currentTarget.scrollLeft; + + const compareTarget = currentTarget || EMPTY_SCROLL_TARGET; + if (!getScrollTarget() || getScrollTarget() === compareTarget) { + setScrollTarget(compareTarget); + + forceScroll(mergedScrollLeft, scrollHeaderRef.value); + forceScroll(mergedScrollLeft, scrollBodyRef.value); + forceScroll(mergedScrollLeft, scrollSummaryRef.value); + forceScroll(mergedScrollLeft, stickyRef.value?.setScrollLeft); + } + + if (currentTarget) { + const { scrollWidth, clientWidth } = currentTarget; + if (isRTL) { + setPingedLeft(-mergedScrollLeft < scrollWidth - clientWidth); + setPingedRight(-mergedScrollLeft > 0); + } else { + setPingedLeft(mergedScrollLeft > 0); + setPingedRight(mergedScrollLeft < scrollWidth - clientWidth); + } + } + }; + + const triggerOnScroll = () => { + if (scrollBodyRef.value) { + onScroll({ currentTarget: scrollBodyRef.value }); + } + }; + + const onFullTableResize = ({ width }) => { + if (width !== componentWidth) { + triggerOnScroll(); + componentWidth.value = fullTableRef.value ? fullTableRef.value.offsetWidth : width; + } + }; + + watch([horizonScroll, () => props.data, () => props.columns], () => { + if (horizonScroll.value) { + triggerOnScroll(); + } + }); + + const [scrollbarSize, setScrollbarSize] = useState(0); + + onMounted(() => { + nextTick(() => { + triggerOnScroll(); + setScrollbarSize(getTargetScrollBarSize(scrollBodyRef.value).width); + }); + }); + + // Table layout + const mergedTableLayout = computed(() => { + if (props.tableLayout) { + return props.tableLayout; + } + // https://github.com/ant-design/ant-design/issues/25227 + // When scroll.x is max-content, no need to fix table layout + // it's width should stretch out to fit content + if (fixColumn.value) { + return props.scroll.x === 'max-content' ? 'auto' : 'fixed'; + } + if ( + fixHeader.value || + stickyState.value.isSticky || + flattenColumns.value.some(({ ellipsis }) => ellipsis) + ) { + return 'fixed'; + } + return 'auto'; + }); + + const emptyNode = () => { + return hasData.value ? null : slots.emptyText?.() || 'No Data'; + }; + + return () => { + const { + prefixCls, + rowClassName, + data, + rowKey, + scroll, + tableLayout, + direction, + + // Additional Part + title = slots.title, + footer = slots.footer, + + // Customize + id, + showHeader, + components, + customHeaderRow, + rowExpandable, + sticky, + + customRow, + } = props; + const { isSticky, offsetHeader, offsetSummary, offsetScroll, stickyClassName, container } = + stickyState.value; + const TableComponent = getComponent(['table'], 'table'); + + const summaryNode = slots.summary?.(); + + let groupTableNode; + + // Header props + const headerProps = { + colWidths: colWidths.value, + columCount: flattenColumns.value.length, + stickyOffsets: stickyOffsets.value, + customHeaderRow, + fixHeader: fixHeader.value, + scroll, + }; + + // Body + const bodyTable = ( + + ); + + const bodyColGroup = ( + width)} + columns={flattenColumns.value} + /> + ); + + const customizeScrollBody = getComponent(['body']) as unknown as CustomizeScrollBody; + + if ( + process.env.NODE_ENV !== 'production' && + typeof customizeScrollBody === 'function' && + hasData.value && + !fixHeader.value + ) { + warning(false, '`components.body` with render props is only work on `scroll.y`.'); + } + if (fixHeader.value || isSticky) { + // >>>>>> Fixed Header + let bodyContent; + + if (typeof customizeScrollBody === 'function') { + bodyContent = customizeScrollBody(mergedData.value, { + scrollbarSize: scrollbarSize.value, + ref: scrollBodyRef, + onScroll, + }); + + headerProps.colWidths = flattenColumns.value.map(({ width }, index) => { + const colWidth = + index === columns.value.length - 1 ? (width as number) - scrollbarSize.value : width; + if (typeof colWidth === 'number' && !Number.isNaN(colWidth)) { + return colWidth; + } + warning( + false, + 'When use `components.body` with render props. Each column should have a fixed `width` value.', + ); + + return 0; + }) as number[]; + } else { + bodyContent = ( +
+ + {bodyColGroup} + {bodyTable} + {!fixFooter && summaryNode && ( +
+ {summaryNode} +
+ )} +
+
+ ); + } + + // Fixed holder share the props + const fixedHolderProps = { + noData: !mergedData.value.length, + maxContentScroll: horizonScroll.value && scroll.x === 'max-content', + ...headerProps, + ...columnContext.value, + direction, + stickyClassName, + onScroll, + }; + + groupTableNode = ( + <> + {/* Header Table */} + {showHeader !== false && ( + ( + <> +
+ {fixFooter.value === 'top' && ( +
{summaryNode}
+ )} + + ), + }} + > + )} + + {/* Body Table */} + {bodyContent} + + {/* Summary Table */} + {fixFooter.value !== 'top' && ( + ( +
{summaryNode}
+ ), + }} + >
+ )} + + {isSticky && ( + + )} + + ); + } else { + // >>>>>> Unique table + groupTableNode = ( +
+ + {bodyColGroup} + {showHeader !== false &&
} + {bodyTable} + {summaryNode && ( +
+ {summaryNode} +
+ )} + +
+ ); + } + return null; + }; + }, +}); diff --git a/components/new-table/context/SummaryContext.tsx b/components/new-table/context/SummaryContext.tsx new file mode 100644 index 000000000..4ba26a855 --- /dev/null +++ b/components/new-table/context/SummaryContext.tsx @@ -0,0 +1,21 @@ +import { inject, InjectionKey, provide } from 'vue'; +import { ColumnType, StickyOffsets } from '../interface'; + +export type FlattenColumns = readonly (ColumnType & { + scrollbar?: boolean; +})[]; +type SummaryContextProps = { + stickyOffsets?: StickyOffsets; + scrollColumnIndex?: number; + flattenColumns?: FlattenColumns; +}; + +export const SummaryContextKey: InjectionKey = Symbol('SummaryContextProps'); + +export const useProvideSummary = (props: SummaryContextProps) => { + provide(SummaryContextKey, props); +}; + +export const useInjectSummary = () => { + return inject(SummaryContextKey, {} as SummaryContextProps); +}; diff --git a/components/new-table/hooks/useColumns.tsx b/components/new-table/hooks/useColumns.tsx index cc8f2ceb7..f832c8693 100644 --- a/components/new-table/hooks/useColumns.tsx +++ b/components/new-table/hooks/useColumns.tsx @@ -102,40 +102,38 @@ function revertForRtl(columns: ColumnsType): ColumnsType /** * Parse `columns` & `children` into `columns`. */ -function useColumns( - { - prefixCls, - columns: baseColumns, - // children, - expandable, - expandedKeys, - getRowKey, - onTriggerExpand, - expandIcon, - rowExpandable, - expandIconColumnIndex, - direction, - expandRowByClick, - columnWidth, - fixed, - }: { - prefixCls?: Ref; - columns?: Ref>; - // children?: React.ReactNode; - expandable: Ref; - expandedKeys: Ref>; - getRowKey: GetRowKey; - onTriggerExpand: TriggerEventHandler; - expandIcon?: Ref>; - rowExpandable?: Ref<(record: RecordType) => boolean>; - expandIconColumnIndex?: Ref; - direction?: Ref<'ltr' | 'rtl'>; - expandRowByClick?: Ref; - columnWidth?: Ref; - fixed?: Ref; - }, - transformColumns: (columns: ColumnsType) => ColumnsType, -): [ComputedRef>, ComputedRef[]>] { +function useColumns({ + prefixCls, + columns: baseColumns, + // children, + expandable, + expandedKeys, + getRowKey, + onTriggerExpand, + expandIcon, + rowExpandable, + expandIconColumnIndex, + direction, + expandRowByClick, + expandColumnWidth, + expandFixed, +}: { + prefixCls?: Ref; + columns?: Ref>; + // children?: React.ReactNode; + expandable: Ref; + expandedKeys: Ref>; + getRowKey: Ref>; + onTriggerExpand: TriggerEventHandler; + expandIcon?: Ref>; + rowExpandable?: Ref<(record: RecordType) => boolean>; + expandIconColumnIndex?: Ref; + direction?: Ref<'ltr' | 'rtl'>; + expandRowByClick?: Ref; + expandColumnWidth?: Ref; + expandFixed?: Ref; +}): // transformColumns: (columns: ColumnsType) => ColumnsType, +[ComputedRef>, ComputedRef[]>] { // const baseColumns = React.useMemo>( // () => columns || convertChildrenToColumns(children), // [columns, children], @@ -148,10 +146,10 @@ function useColumns( const prevColumn = baseColumns[expandColIndex]; let fixedColumn: FixedType | null; - if ((fixed.value === 'left' || fixed.value) && !expandIconColumnIndex.value) { + if ((expandFixed.value === 'left' || expandFixed.value) && !expandIconColumnIndex.value) { fixedColumn = 'left'; } else if ( - (fixed.value === 'right' || fixed.value) && + (expandFixed.value === 'right' || expandFixed.value) && expandIconColumnIndex.value === baseColumns.value.length ) { fixedColumn = 'right'; @@ -170,9 +168,9 @@ function useColumns( title: '', fixed: fixedColumn, class: `${prefixCls.value}-row-expand-icon-cell`, - width: columnWidth.value, - render: (_, record, index) => { - const rowKey = getRowKey(record, index); + width: expandColumnWidth.value, + customRender: ({ record, index }) => { + const rowKey = getRowKey.value(record, index); const expanded = expandedKeysValue.has(rowKey); const recordExpandable = rowExpandableValue ? rowExpandableValue(record) : true; @@ -203,9 +201,9 @@ function useColumns( const mergedColumns = computed(() => { let finalColumns = withExpandColumns.value; - if (transformColumns) { - finalColumns = transformColumns(finalColumns); - } + // if (transformColumns) { + // finalColumns = transformColumns(finalColumns); + // } // Always provides at least one column for table display if (!finalColumns.length) { diff --git a/components/new-table/hooks/useFlattenRecords.ts b/components/new-table/hooks/useFlattenRecords.ts index 87a284b25..53f3588d3 100644 --- a/components/new-table/hooks/useFlattenRecords.ts +++ b/components/new-table/hooks/useFlattenRecords.ts @@ -50,11 +50,11 @@ function flatRecord( * @param {GetRowKey} getRowKey : 获取当前rowKey的方法 * @returns flattened data */ -export default function useFlattenRecords( - dataRef: Ref<[]>, +export default function useFlattenRecords( + dataRef: Ref, childrenColumnNameRef: Ref, expandedKeysRef: Ref>, - getRowKey: GetRowKey, + getRowKey: Ref>, ) { const arr: Ref<{ record: T; indent: number }[]> = computed(() => { const childrenColumnName = childrenColumnNameRef.value; @@ -67,7 +67,7 @@ export default function useFlattenRecords( for (let i = 0; i < data?.length; i += 1) { const record = data[i]; - temp.push(...flatRecord(record, 0, childrenColumnName, expandedKeys, getRowKey)); + temp.push(...flatRecord(record, 0, childrenColumnName, expandedKeys, getRowKey.value)); } return temp; diff --git a/components/new-table/interface.ts b/components/new-table/interface.ts index bc6553ee8..0a5d3845a 100644 --- a/components/new-table/interface.ts +++ b/components/new-table/interface.ts @@ -64,7 +64,7 @@ interface ColumnSharedType { class?: string; className?: string; fixed?: FixedType; - onHeaderCell?: GetComponentProps[number]>; + customHeaderCell?: GetComponentProps[number]>; ellipsis?: CellEllipsisType; align?: AlignType; } @@ -87,7 +87,7 @@ export interface ColumnType extends ColumnSharedType { }) => any | RenderedCell; rowSpan?: number; width?: number | string; - onCell?: GetComponentProps; + customCell?: GetComponentProps; /** @deprecated Please use `onCell` instead */ onCellClick?: (record: RecordType, e: MouseEvent) => void; } @@ -150,30 +150,31 @@ export type GetComponent = ( export type ExpandableType = false | 'row' | 'nest'; export interface LegacyExpandableProps { - /** @deprecated Use `expandable.expandedRowKeys` instead */ expandedRowKeys?: Key[]; - /** @deprecated Use `expandable.defaultExpandedRowKeys` instead */ + defaultExpandedRowKeys?: Key[]; - /** @deprecated Use `expandable.expandedRowRender` instead */ + expandedRowRender?: ExpandedRowRender; - /** @deprecated Use `expandable.expandRowByClick` instead */ + expandRowByClick?: boolean; - /** @deprecated Use `expandable.expandIcon` instead */ + expandIcon?: RenderExpandIcon; - /** @deprecated Use `expandable.onExpand` instead */ + onExpand?: (expanded: boolean, record: RecordType) => void; - /** @deprecated Use `expandable.onExpandedRowsChange` instead */ + onExpandedRowsChange?: (expandedKeys: Key[]) => void; - /** @deprecated Use `expandable.defaultExpandAllRows` instead */ + defaultExpandAllRows?: boolean; - /** @deprecated Use `expandable.indentSize` instead */ + indentSize?: number; - /** @deprecated Use `expandable.expandIconColumnIndex` instead */ + expandIconColumnIndex?: number; - /** @deprecated Use `expandable.expandedRowClassName` instead */ + expandedRowClassName?: RowClassName; - /** @deprecated Use `expandable.childrenColumnName` instead */ + childrenColumnName?: string; + + rowExpandable?: (record: RecordType) => boolean; } export type ExpandedRowRender = ( diff --git a/components/new-table/stickyScrollBar.tsx b/components/new-table/stickyScrollBar.tsx new file mode 100644 index 000000000..b8e85acb2 --- /dev/null +++ b/components/new-table/stickyScrollBar.tsx @@ -0,0 +1,211 @@ +import { computed, defineComponent, onBeforeUnmount, onMounted, ref, Ref, watch } from 'vue'; +import addEventListenerWrap from '../vc-util/Dom/addEventListener'; +import { getOffset } from '../vc-util/Dom/css'; +import classNames from '../_util/classNames'; +import { MouseEventHandler } from '../_util/EventInterface'; +import getScrollBarSize from '../_util/getScrollBarSize'; +import { useInjectTable } from './context/TableContext'; +import { useLayoutState } from './hooks/useFrame'; + +interface StickyScrollBarProps { + scrollBodyRef: Ref; + onScroll: (params: { scrollLeft?: number }) => void; + offsetScroll: number; + container: HTMLElement | Window; +} + +export default defineComponent({ + name: 'StickyScrollBar', + props: ['offsetScroll', 'container', 'scrollBodyRef'] as any, + emits: ['scroll'], + inheritAttrs: false, + setup(props, { emit, expose }) { + const tableContext = useInjectTable(); + const bodyScrollWidth = computed(() => props.scrollBodyRef.value.scrollWidth || 0); + const bodyWidth = computed(() => props.scrollBodyRef.value.clientWidth || 0); + const scrollBarWidth = computed( + () => bodyScrollWidth.value && bodyWidth.value * (bodyWidth.value / bodyScrollWidth.value), + ); + + const scrollBarRef = ref(); + + const [scrollState, setScrollState] = useLayoutState({ + scrollLeft: 0, + isHiddenScrollBar: false, + }); + + const refState = ref({ + delta: 0, + x: 0, + }); + + const isActive = ref(false); + + const onMouseUp: MouseEventHandler = () => { + isActive.value = false; + }; + + const onMouseDown: MouseEventHandler = event => { + refState.value = { delta: event.pageX - scrollState.value.scrollLeft, x: 0 }; + isActive.value = true; + event.preventDefault(); + }; + + const onMouseMove: MouseEventHandler = event => { + // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons + const { buttons } = event || (window?.event as any); + if (!isActive.value || buttons === 0) { + // If out body mouse up, we can set isActive false when mouse move + if (isActive.value) { + isActive.value = false; + } + return; + } + let left: number = refState.value.x + event.pageX - refState.value.x - refState.value.delta; + + if (left <= 0) { + left = 0; + } + + if (left + scrollBarWidth.value >= bodyWidth.value) { + left = bodyWidth.value - scrollBarWidth.value; + } + emit('scroll', { + scrollLeft: (left / bodyWidth.value) * (bodyScrollWidth.value + 2), + }); + + refState.value.x = event.pageX; + }; + + const onContainerScroll = () => { + const tableOffsetTop = getOffset(props.scrollBodyRef.value).top; + const tableBottomOffset = tableOffsetTop + props.scrollBodyRef.value.offsetHeight; + const currentClientOffset = + props.container === window + ? document.documentElement.scrollTop + window.innerHeight + : getOffset(props.container).top + (props.container as HTMLElement).clientHeight; + + if ( + tableBottomOffset - getScrollBarSize() <= currentClientOffset || + tableOffsetTop >= currentClientOffset - props.offsetScroll + ) { + setScrollState(state => ({ + ...state, + isHiddenScrollBar: true, + })); + } else { + setScrollState(state => ({ + ...state, + isHiddenScrollBar: false, + })); + } + }; + + const setScrollLeft = (left: number) => { + setScrollState(state => { + return { + ...state, + scrollLeft: (left / bodyScrollWidth.value) * bodyWidth.value || 0, + }; + }); + }; + + expose({ + setScrollLeft, + }); + let onMouseUpListener = null; + let onMouseMoveListener = null; + let onResizeListener = null; + let onScrollListener = null; + onMounted(() => { + onMouseUpListener = addEventListenerWrap(document.body, 'mouseup', onMouseUp, false); + onMouseMoveListener = addEventListenerWrap(document.body, 'mousemove', onMouseMove, false); + onResizeListener = addEventListenerWrap(window, 'resize', onContainerScroll, false); + }); + + watch( + [scrollBarWidth, isActive], + () => { + onContainerScroll(); + }, + { immediate: true }, + ); + + watch( + () => props.container, + () => { + onScrollListener?.remove(); + onScrollListener = addEventListenerWrap( + props.container, + 'scroll', + onContainerScroll, + false, + ); + }, + { immediate: true, flush: 'post' }, + ); + + onBeforeUnmount(() => { + onMouseUpListener?.remove(); + onMouseMoveListener?.remove(); + onScrollListener?.remove(); + onResizeListener?.remove(); + }); + + watch( + () => ({ ...scrollState.value }), + (newState, preState) => { + if ( + newState.isHiddenScrollBar !== preState?.isHiddenScrollBar && + !newState.isHiddenScrollBar + ) { + setScrollState(state => { + const bodyNode = props.scrollBodyRef.value; + if (!bodyNode) { + return state; + } + return { + ...state, + scrollLeft: (bodyNode.scrollLeft / bodyNode.scrollWidth) * bodyNode.clientWidth, + }; + }); + } + }, + { immediate: true }, + ); + const scrollbarSize = getScrollBarSize(); + + return () => { + if ( + bodyScrollWidth.value <= bodyWidth.value || + !scrollBarWidth.value || + scrollState.value.isHiddenScrollBar + ) { + return null; + } + const { prefixCls } = tableContext; + return ( +
+
+
+ ); + }; + }, +}); diff --git a/components/new-table/utils/legacyUtil.ts b/components/new-table/utils/legacyUtil.ts index cf9f7e2c9..744111469 100644 --- a/components/new-table/utils/legacyUtil.ts +++ b/components/new-table/utils/legacyUtil.ts @@ -10,7 +10,7 @@ export function getExpandableProps( ): ExpandableConfig { const { expandable, ...legacyExpandableConfig } = props; - if ('expandable' in props) { + if (props.expandable !== undefined) { return { ...legacyExpandableConfig, ...expandable,