refactor: table

pull/4639/head
tangjinzhou 2021-09-03 23:07:39 +08:00
parent c9db47533f
commit 60ea53ce91
22 changed files with 1766 additions and 79 deletions

View File

@ -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),
};
}

View File

@ -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);

View File

@ -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 };

View File

@ -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<RecordType> {
record: RecordType;
index: number;
recordKey: Key;
expandedKeys: Set<Key>;
rowComponent: CustomizeComponent;
cellComponent: CustomizeComponent;
customRow: GetComponentProps<RecordType>;
rowExpandable: (record: RecordType) => boolean;
indent?: number;
rowKey: Key;
getRowKey: GetRowKey<RecordType>;
childrenColumnName: string;
}
export default defineComponent<BodyRowProps<unknown>>({
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<Record<string, any>>(
() => 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 = (
<RowComponent
{...additionalProps.value}
data-row-key={rowKey}
class={classNames(
className,
`${prefixCls}-row`,
`${prefixCls}-row-level-${indent}`,
computeRowClassName.value,
additionalProps.value.class,
)}
style={{
...style,
...parseStyleText(additionalProps.value.style),
}}
onClick={onClick}
>
{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 = (
<>
<span
style={{ paddingLeft: `${indentSize * indent}px` }}
class={`${prefixCls}-row-indent indent-level-${indent}`}
/>
{expandIcon({
prefixCls,
expanded: expanded.value,
expandable: hasNestChildren.value,
record,
onExpand: onInternalTriggerExpand,
})}
</>
);
}
let additionalCellProps;
if (column.customCell) {
additionalCellProps = column.customCell(record, index);
}
return (
<Cell
class={columnClassName}
ellipsis={column.ellipsis}
align={column.align}
component={cellComponent}
prefixCls={prefixCls}
key={key}
record={record}
index={index}
dataIndex={dataIndex}
customRender={customRender}
{...fixedInfo}
appendNode={appendCellNode}
additionalProps={additionalCellProps}
/>
);
})}
</RowComponent>
);
// ======================== 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 = (
<ExpandedRow
expanded={expanded.value}
class={classNames(
`${prefixCls}-expanded-row`,
`${prefixCls}-expanded-row-level-${indent + 1}`,
computedExpandedRowClassName,
)}
prefixCls={prefixCls}
fixHeader={fixHeader}
fixColumn={fixColumn}
horizonScroll={horizonScroll}
component={RowComponent}
componentWidth={componentWidth}
cellComponent={cellComponent}
colSpan={flattenColumns.length}
>
{expandContent}
</ExpandedRow>
);
}
return (
<>
{baseRowNode}
{expandRowNode}
</>
);
};
},
});

View File

@ -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<ExpandedRowProps>({
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 = (
<div
style={{
width: componentWidth - (fixHeader ? tableContext.scrollbarSize : 0),
position: 'sticky',
left: 0,
overflow: 'hidden',
}}
class={`${prefixCls}-expanded-row-fixed`}
>
{contentNode}
</div>
);
}
return (
<Component
class={attrs.class}
style={{
display: expanded ? null : 'none',
}}
>
<Cell component={cellComponent} prefixCls={prefixCls} colSpan={colSpan}>
{contentNode}
</Cell>
</Component>
);
};
},
});

View File

@ -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 (
<VCResizeObserver
onResize={({ offsetWidth }) => {
emit('columnResize', columnKey, offsetWidth);
}}
>
<td style={{ padding: 0, border: 0, height: 0 }}>
<div style={{ height: 0, overflow: 'hidden' }}>&nbsp;</div>
</td>
</VCResizeObserver>
);
}

View File

@ -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<RecordType> {
data: RecordType[];
getRowKey: GetRowKey<RecordType>;
measureColumnWidth: boolean;
expandedKeys: Set<Key>;
customRow: GetComponentProps<RecordType>;
rowExpandable: (record: RecordType) => boolean;
// emptyNode: React.ReactNode;
childrenColumnName: string;
}
export default defineComponent<BodyProps<any>>({
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 (
<BodyRow
key={key}
rowKey={key}
record={record}
recordKey={key}
index={index}
rowComponent={trComponent}
cellComponent={tdComponent}
expandedKeys={expandedKeys}
customRow={customRow}
getRowKey={getRowKey}
rowExpandable={rowExpandable}
childrenColumnName={childrenColumnName}
indent={indent}
/>
);
});
} else {
rows = (
<ExpandedRow
expanded
class={`${prefixCls}-placeholder`}
prefixCls={prefixCls}
fixHeader={fixHeader}
fixColumn={horizonScroll}
horizonScroll={horizonScroll}
component={trComponent}
componentWidth={componentWidth}
cellComponent={tdComponent}
colSpan={flattenColumns.length}
>
{slots.emptyNode?.()}
</ExpandedRow>
);
}
const columnsKey = getColumnsKey(flattenColumns);
return (
<WrapperComponent className={`${prefixCls}-tbody`}>
{/* Measure body column width with additional hidden col */}
{measureColumnWidth && (
<tr
aria-hidden="true"
class={`${prefixCls}-measure-row`}
style={{ height: 0, fontSize: 0 }}
>
{columnsKey.map(columnKey => (
<MeasureCell
key={columnKey}
columnKey={columnKey}
onColumnResize={onColumnResize}
/>
))}
</tr>
)}
{rows}
</WrapperComponent>
);
};
},
});

View File

@ -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<RecordType = DefaultRecordType> {
// Additional
/** @private Used for `expandable` with nest tree */
appendNode?: any;
additionalProps?: Omit<HTMLAttributes, 'style'> & { style?: CSSProperties };
additionalProps?: HTMLAttributes;
rowType?: 'header' | 'body' | 'footer';
@ -196,7 +196,7 @@ export default defineComponent<CellProps>({
cellClass,
),
style: {
...additionalProps.style,
...parseStyleText(additionalProps.style as any),
...alignStyle,
...fixedStyle,
...cellStyle,

View File

@ -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<readonly number[]>, columCountRef: Ref<number>) {
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<RecordType> extends HeaderProps<RecordType> {
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<FixedHeaderProps<DefaultRecordType>>({
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<ColumnsType<unknown>>([]);
const flattenColumnsWithScrollbar = ref<ColumnsType<unknown>>([]);
watchEffect(() => {
// Add scrollbar column
const lastColumn = props.flattenColumns[props.flattenColumns.length - 1];
const ScrollBarColumn: ColumnType<unknown> & { 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 (
<div
style={{
overflow: 'hidden',
...(isSticky ? { top: `${stickyTopOffset}px`, bottom: `${stickyBottomOffset}px` } : {}),
}}
ref={scrollRef}
class={classNames(attrs.class, {
[stickyClassName]: !!stickyClassName,
})}
>
<table
style={{
tableLayout: 'fixed',
visibility: noData || mergedColumnWidth.value ? null : 'hidden',
}}
>
{(!noData || !maxContentScroll || allFlattenColumnsWithWidth.value) && (
<ColGroup
colWidths={
mergedColumnWidth.value
? [...mergedColumnWidth.value, combinationScrollBarSize.value]
: []
}
columCount={columCount + 1}
columns={flattenColumnsWithScrollbar.value}
/>
)}
{slots.default?.({
...props,
stickyOffsets: headerStickyOffsets.value,
columns: columnsWithScrollbar.value,
flattenColumns: flattenColumnsWithScrollbar.value,
})}
</table>
</div>
);
};
},
});

View File

@ -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<SummaryCellProps>({
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 (
<Cell
className={attrs.class as string}
index={index}
component="td"
prefixCls={prefixCls}
record={null}
dataIndex={null}
align={align}
customRender={() => ({
children: slots.defalut?.(),
props: {
colSpan: mergedColSpan,
rowSpan,
},
})}
{...fixedInfo}
/>
);
};
},
});

View File

@ -0,0 +1,3 @@
export default function FooterRow(props, { slots }) {
return <tr {...props}>{slots.default?.()}</tr>;
}

View File

@ -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<SummaryProps> {
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;

View File

@ -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<RecordType = DefaultRecordType> {
stickyOffsets: StickyOffsets;
flattenColumns: FlattenColumns<RecordType>;
}
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 <tfoot class={`${prefixCls}-summary`}>{slots.default?.()}</tfoot>;
};
},
});
export const FooterComponents = Summary;

View File

@ -87,18 +87,18 @@ export interface HeaderProps<RecordType = DefaultRecordType> {
columns: ColumnsType<RecordType>;
flattenColumns: readonly ColumnType<RecordType>[];
stickyOffsets: StickyOffsets;
onHeaderRow: GetComponentProps<readonly ColumnType<RecordType>[]>;
customHeaderRow: GetComponentProps<readonly ColumnType<RecordType>[]>;
}
export default defineComponent<HeaderProps>({
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<HeaderProps>({
stickyOffsets={stickyOffsets}
rowComponent={trComponent}
cellComponent={thComponent}
onHeaderRow={onHeaderRow}
customHeaderRow={customHeaderRow}
index={rowIndex}
/>
);

View File

@ -18,7 +18,7 @@ export interface RowProps<RecordType = DefaultRecordType> {
flattenColumns: readonly ColumnType<RecordType>[];
rowComponent: CustomizeComponent;
cellComponent: CustomizeComponent;
onHeaderRow: GetComponentProps<readonly ColumnType<RecordType>[]>;
customHeaderRow: GetComponentProps<readonly ColumnType<RecordType>[]>;
index: number;
}
@ -31,7 +31,7 @@ export default defineComponent<RowProps>({
'rowComponent',
'cellComponent',
'index',
'onHeaderRow',
'customHeaderRow',
] as any,
setup(props: RowProps) {
const tableContext = useInjectTable();
@ -43,13 +43,13 @@ export default defineComponent<RowProps>({
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<RowProps>({
);
let additionalProps;
if (column && column.onHeaderCell) {
additionalProps = cell.column.onHeaderCell(column);
if (column && column.customHeaderCell) {
additionalProps = cell.column.customHeaderCell(column);
}
return (

View File

@ -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<RecordType = unknown> extends LegacyExpandableProps<RecordType> {
prefixCls?: string;
data?: RecordType[];
columns?: ColumnsType<RecordType>;
rowKey?: string | GetRowKey<RecordType>;
tableLayout?: TableLayout;
// Fixed Columns
scroll?: { x?: number | true | string; y?: number | string };
// Expandable
/** Config expand rows */
expandable?: ExpandableConfig<RecordType>;
indentSize?: number;
rowClassName?: string | RowClassName<RecordType>;
// Additional Part
// title?: PanelRender<RecordType>;
// footer?: PanelRender<RecordType>;
// summary?: (data: readonly RecordType[]) => any;
// Customize
id?: string;
showHeader?: boolean;
components?: TableComponents<RecordType>;
customRow?: GetComponentProps<RecordType>;
customHeaderRow?: GetComponentProps<ColumnType<RecordType>[]>;
// 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<RecordType>) => ColumnsType<RecordType>;
// /**
// * @private Internal usage, may remove by refactor.
// *
// * !!! DO NOT USE IN PRODUCTION ENVIRONMENT !!!
// */
// internalRefs?: {
// body: React.MutableRefObject<HTMLDivElement>;
// };
sticky?: boolean | TableSticky;
}
export default defineComponent<TableProps>({
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<TableComponents<any>>(props.components, {}),
);
const getComponent = (path, defaultComponent?: string) =>
getPathValue<CustomizeComponent, TableComponents<any>>(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<any> = 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<HTMLDivElement>();
const scrollHeaderRef = ref<HTMLDivElement>();
const scrollBodyRef = ref<HTMLDivElement>();
const scrollSummaryRef = ref<HTMLDivElement>();
const [pingedLeft, setPingedLeft] = useState(false);
const [pingedRight, setPingedRight] = useState(false);
const [colsWidths, updateColsWidths] = useLayoutState(new Map<Key, number>());
// 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<Record<string, boolean | string>>({});
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<CSSProperties>({});
let scrollYStyle = ref<CSSProperties>({});
let scrollTableStyle = ref<CSSProperties>({});
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 = (
<Body
data={mergedData.value}
measureColumnWidth={fixHeader.value || horizonScroll.value || isSticky}
expandedKeys={mergedExpandedKeys.value}
rowExpandable={rowExpandable}
getRowKey={getRowKey.value}
customRow={customRow}
childrenColumnName={mergedChildrenColumnName.value}
v-slots={{ emptyText: emptyNode }}
/>
);
const bodyColGroup = (
<ColGroup
colWidths={flattenColumns.value.map(({ width }) => width)}
columns={flattenColumns.value}
/>
);
const customizeScrollBody = getComponent(['body']) as unknown as CustomizeScrollBody<any>;
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 = (
<div
style={{
...scrollXStyle.value,
...scrollYStyle.value,
}}
onScroll={onScroll}
ref={scrollBodyRef}
class={classNames(`${prefixCls}-body`)}
>
<TableComponent
style={{
...scrollTableStyle.value,
tableLayout: mergedTableLayout.value,
}}
>
{bodyColGroup}
{bodyTable}
{!fixFooter && summaryNode && (
<Footer stickyOffsets={stickyOffsets} flattenColumns={flattenColumns.value}>
{summaryNode}
</Footer>
)}
</TableComponent>
</div>
);
}
// 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 && (
<FixedHolder
{...fixedHolderProps}
stickyTopOffset={offsetHeader}
class={`${prefixCls}-header`}
ref={scrollHeaderRef}
v-slots={{
default: fixedHolderPassProps => (
<>
<Header {...fixedHolderPassProps} />
{fixFooter.value === 'top' && (
<Footer {...fixedHolderPassProps}>{summaryNode}</Footer>
)}
</>
),
}}
></FixedHolder>
)}
{/* Body Table */}
{bodyContent}
{/* Summary Table */}
{fixFooter.value !== 'top' && (
<FixedHolder
{...fixedHolderProps}
stickyBottomOffset={offsetSummary}
class={`${prefixCls}-summary`}
ref={scrollSummaryRef}
v-slots={{
default: fixedHolderPassProps => (
<Footer {...fixedHolderPassProps}>{summaryNode}</Footer>
),
}}
></FixedHolder>
)}
{isSticky && (
<StickyScrollBar
ref={stickyRef}
offsetScroll={offsetScroll}
scrollBodyRef={scrollBodyRef}
onScroll={onScroll}
container={container}
/>
)}
</>
);
} else {
// >>>>>> Unique table
groupTableNode = (
<div
style={{
...scrollXStyle.value,
...scrollYStyle.value,
}}
class={classNames(`${prefixCls}-content`)}
onScroll={onScroll}
ref={scrollBodyRef}
>
<TableComponent
style={{ ...scrollTableStyle.value, tableLayout: mergedTableLayout.value }}
>
{bodyColGroup}
{showHeader !== false && <Header {...headerProps} {...columnContext.value} />}
{bodyTable}
{summaryNode && (
<Footer stickyOffsets={stickyOffsets.value} flattenColumns={flattenColumns.value}>
{summaryNode}
</Footer>
)}
</TableComponent>
</div>
);
}
return null;
};
},
});

View File

@ -0,0 +1,21 @@
import { inject, InjectionKey, provide } from 'vue';
import { ColumnType, StickyOffsets } from '../interface';
export type FlattenColumns<RecordType> = readonly (ColumnType<RecordType> & {
scrollbar?: boolean;
})[];
type SummaryContextProps = {
stickyOffsets?: StickyOffsets;
scrollColumnIndex?: number;
flattenColumns?: FlattenColumns<any>;
};
export const SummaryContextKey: InjectionKey<SummaryContextProps> = Symbol('SummaryContextProps');
export const useProvideSummary = (props: SummaryContextProps) => {
provide(SummaryContextKey, props);
};
export const useInjectSummary = () => {
return inject(SummaryContextKey, {} as SummaryContextProps);
};

View File

@ -102,40 +102,38 @@ function revertForRtl<RecordType>(columns: ColumnsType<RecordType>): ColumnsType
/**
* Parse `columns` & `children` into `columns`.
*/
function useColumns<RecordType>(
{
prefixCls,
columns: baseColumns,
// children,
expandable,
expandedKeys,
getRowKey,
onTriggerExpand,
expandIcon,
rowExpandable,
expandIconColumnIndex,
direction,
expandRowByClick,
columnWidth,
fixed,
}: {
prefixCls?: Ref<string>;
columns?: Ref<ColumnsType<RecordType>>;
// children?: React.ReactNode;
expandable: Ref<boolean>;
expandedKeys: Ref<Set<Key>>;
getRowKey: GetRowKey<RecordType>;
onTriggerExpand: TriggerEventHandler<RecordType>;
expandIcon?: Ref<RenderExpandIcon<RecordType>>;
rowExpandable?: Ref<(record: RecordType) => boolean>;
expandIconColumnIndex?: Ref<number>;
direction?: Ref<'ltr' | 'rtl'>;
expandRowByClick?: Ref<boolean>;
columnWidth?: Ref<number | string>;
fixed?: Ref<FixedType>;
},
transformColumns: (columns: ColumnsType<RecordType>) => ColumnsType<RecordType>,
): [ComputedRef<ColumnsType<RecordType>>, ComputedRef<readonly ColumnType<RecordType>[]>] {
function useColumns<RecordType>({
prefixCls,
columns: baseColumns,
// children,
expandable,
expandedKeys,
getRowKey,
onTriggerExpand,
expandIcon,
rowExpandable,
expandIconColumnIndex,
direction,
expandRowByClick,
expandColumnWidth,
expandFixed,
}: {
prefixCls?: Ref<string>;
columns?: Ref<ColumnsType<RecordType>>;
// children?: React.ReactNode;
expandable: Ref<boolean>;
expandedKeys: Ref<Set<Key>>;
getRowKey: Ref<GetRowKey<RecordType>>;
onTriggerExpand: TriggerEventHandler<RecordType>;
expandIcon?: Ref<RenderExpandIcon<RecordType>>;
rowExpandable?: Ref<(record: RecordType) => boolean>;
expandIconColumnIndex?: Ref<number>;
direction?: Ref<'ltr' | 'rtl'>;
expandRowByClick?: Ref<boolean>;
expandColumnWidth?: Ref<number | string>;
expandFixed?: Ref<FixedType>;
}): // transformColumns: (columns: ColumnsType<RecordType>) => ColumnsType<RecordType>,
[ComputedRef<ColumnsType<RecordType>>, ComputedRef<readonly ColumnType<RecordType>[]>] {
// const baseColumns = React.useMemo<ColumnsType<RecordType>>(
// () => columns || convertChildrenToColumns(children),
// [columns, children],
@ -148,10 +146,10 @@ function useColumns<RecordType>(
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<RecordType>(
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<RecordType>(
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) {

View File

@ -50,11 +50,11 @@ function flatRecord<T>(
* @param {GetRowKey<T>} getRowKey : rowKey
* @returns flattened data
*/
export default function useFlattenRecords<T>(
dataRef: Ref<[]>,
export default function useFlattenRecords<T = unknown>(
dataRef: Ref<T[]>,
childrenColumnNameRef: Ref<string>,
expandedKeysRef: Ref<Set<Key>>,
getRowKey: GetRowKey<T>,
getRowKey: Ref<GetRowKey<T>>,
) {
const arr: Ref<{ record: T; indent: number }[]> = computed(() => {
const childrenColumnName = childrenColumnNameRef.value;
@ -67,7 +67,7 @@ export default function useFlattenRecords<T>(
for (let i = 0; i < data?.length; i += 1) {
const record = data[i];
temp.push(...flatRecord<T>(record, 0, childrenColumnName, expandedKeys, getRowKey));
temp.push(...flatRecord<T>(record, 0, childrenColumnName, expandedKeys, getRowKey.value));
}
return temp;

View File

@ -64,7 +64,7 @@ interface ColumnSharedType<RecordType> {
class?: string;
className?: string;
fixed?: FixedType;
onHeaderCell?: GetComponentProps<ColumnsType<RecordType>[number]>;
customHeaderCell?: GetComponentProps<ColumnsType<RecordType>[number]>;
ellipsis?: CellEllipsisType;
align?: AlignType;
}
@ -87,7 +87,7 @@ export interface ColumnType<RecordType> extends ColumnSharedType<RecordType> {
}) => any | RenderedCell<RecordType>;
rowSpan?: number;
width?: number | string;
onCell?: GetComponentProps<RecordType>;
customCell?: GetComponentProps<RecordType>;
/** @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<RecordType> {
/** @deprecated Use `expandable.expandedRowKeys` instead */
expandedRowKeys?: Key[];
/** @deprecated Use `expandable.defaultExpandedRowKeys` instead */
defaultExpandedRowKeys?: Key[];
/** @deprecated Use `expandable.expandedRowRender` instead */
expandedRowRender?: ExpandedRowRender<RecordType>;
/** @deprecated Use `expandable.expandRowByClick` instead */
expandRowByClick?: boolean;
/** @deprecated Use `expandable.expandIcon` instead */
expandIcon?: RenderExpandIcon<RecordType>;
/** @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<RecordType>;
/** @deprecated Use `expandable.childrenColumnName` instead */
childrenColumnName?: string;
rowExpandable?: (record: RecordType) => boolean;
}
export type ExpandedRowRender<ValueType> = (

View File

@ -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<HTMLElement>;
onScroll: (params: { scrollLeft?: number }) => void;
offsetScroll: number;
container: HTMLElement | Window;
}
export default defineComponent<StickyScrollBarProps>({
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 (
<div
style={{
height: `${scrollbarSize}px`,
width: `${bodyWidth}px`,
bottom: `${props.offsetScroll}px`,
}}
class={`${prefixCls}-sticky-scroll`}
>
<div
onMousedown={onMouseDown}
ref={scrollBarRef}
class={classNames(`${prefixCls}-sticky-scroll-bar`, {
[`${prefixCls}-sticky-scroll-bar-active`]: isActive,
})}
style={{
width: `${scrollBarWidth.value}px`,
transform: `translate3d(${scrollState.value.scrollLeft}px, 0, 0)`,
}}
/>
</div>
);
};
},
});

View File

@ -10,7 +10,7 @@ export function getExpandableProps<RecordType>(
): ExpandableConfig<RecordType> {
const { expandable, ...legacyExpandableConfig } = props;
if ('expandable' in props) {
if (props.expandable !== undefined) {
return {
...legacyExpandableConfig,
...expandable,