import Header from './Header/Header'; import type { GetRowKey, ColumnsType, TableComponents, Key, TriggerEventHandler, GetComponentProps, ExpandableConfig, LegacyExpandableProps, PanelRender, TableLayout, RowClassName, CustomizeComponent, ColumnType, CustomizeScrollBody, TableSticky, } 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 Panel from './Panel'; import Footer 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 { CSSProperties } from 'vue'; import { computed, 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 type { EventHandler } from '../_util/EventInterface'; import VCResizeObserver from '../vc-resize-observer'; import { useProvideTable } from './context/TableContext'; import { useProvideBody } from './context/BodyContext'; import { useProvideResize } from './context/ResizeContext'; // 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; sticky?: boolean | TableSticky; } export default defineComponent({ name: 'Table', slots: ['title', 'footer', 'summary', 'emptyText'], emits: ['expand', 'expandedRowsChange'], setup(props, { slots, 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 const scrollXStyle = ref({}); const scrollYStyle = ref({}); const 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.value) { 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'; }; useProvideTable( reactive({ ...reactivePick(props, 'prefixCls', 'direction'), getComponent, scrollbarSize, fixedInfoList: computed(() => flattenColumns.value.map((_, colIndex) => getCellFixedInfo( colIndex, colIndex, flattenColumns.value, stickyOffsets.value, props.direction, ), ), ), isSticky: computed(() => stickyState.value.isSticky), summaryCollect, }), ); useProvideBody( reactive({ ...reactivePick( props, 'rowClassName', 'expandedRowClassName', 'expandRowByClick', 'expandedRowRender', 'expandIconColumnIndex', 'indentSize', ), columns, flattenColumns, tableLayout: mergedTableLayout, componentWidth, fixHeader, fixColumn, horizonScroll, expandIcon: mergedExpandIcon, expandableType, onTriggerExpand, }), ); useProvideResize({ onColumnResize, }); return () => { const { prefixCls, scroll, tableLayout, direction, // Additional Part title = slots.title, footer = slots.footer, // Customize id, showHeader, customHeaderRow, rowExpandable, 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.value && 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}
)}
); } let fullTable = (
{title && {title(mergedData.value)}}
{groupTableNode}
{footer && {footer(mergedData.value)}}
); if (horizonScroll.value) { fullTable = {fullTable}; } return fullTable; }; }, });