ant-design-vue/components/table/Table.tsx
2022-05-21 10:56:28 +08:00

708 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import RcTable from '../vc-table';
import type { TableProps as RcTableProps } from '../vc-table/Table';
import { INTERNAL_HOOKS } from '../vc-table/Table';
import type { SpinProps } from '../spin';
import Spin from '../spin';
import Pagination from '../pagination';
import type { TooltipProps } from '../tooltip';
import usePagination, { DEFAULT_PAGE_SIZE, getPaginationParam } from './hooks/usePagination';
import useLazyKVMap from './hooks/useLazyKVMap';
import type { Breakpoint } from '../_util/responsiveObserve';
import type {
TableRowSelection,
GetRowKey,
ColumnType,
ColumnsType,
TableCurrentDataSource,
SorterResult,
GetPopupContainer,
ExpandType,
TablePaginationConfig,
SortOrder,
TableLocale,
TableAction,
FilterValue,
} from './interface';
import useSelection from './hooks/useSelection';
import type { SortState } from './hooks/useSorter';
import useSorter, { getSortData } from './hooks/useSorter';
import type { FilterState } from './hooks/useFilter';
import useFilter, { getFilterData } from './hooks/useFilter';
import useTitleColumns from './hooks/useTitleColumns';
import renderExpandIcon from './ExpandIcon';
import scrollTo from '../_util/scrollTo';
import defaultLocale from '../locale/en_US';
import type { SizeType } from '../config-provider';
import devWarning from '../vc-util/devWarning';
import type { CSSProperties, PropType } from 'vue';
import { nextTick, reactive, ref, computed, defineComponent, toRef, watchEffect, watch } from 'vue';
import type { DefaultRecordType } from '../vc-table/interface';
import useBreakpoint from '../_util/hooks/useBreakpoint';
import useConfigInject from '../_util/hooks/useConfigInject';
import { useLocaleReceiver } from '../locale-provider/LocaleReceiver';
import classNames from '../_util/classNames';
import omit from '../_util/omit';
import { initDefaultProps } from '../_util/props-util';
import { useProvideSlots, useProvideTableContext } from './context';
import type { ContextSlots } from './context';
import useColumns from './hooks/useColumns';
import { convertChildrenToColumns } from './util';
export type { ColumnsType, TablePaginationConfig };
const EMPTY_LIST: any[] = [];
interface ChangeEventInfo<RecordType = DefaultRecordType> {
pagination: {
current?: number;
pageSize?: number;
total?: number;
};
filters: Record<string, FilterValue | null>;
sorter: SorterResult<RecordType> | SorterResult<RecordType>[];
filterStates: FilterState<RecordType>[];
sorterStates: SortState<RecordType>[];
resetPagination: Function;
}
export interface TableProps<RecordType = DefaultRecordType>
extends Omit<
RcTableProps<RecordType>,
| 'transformColumns'
| 'internalHooks'
| 'internalRefs'
| 'data'
| 'columns'
| 'scroll'
| 'emptyText'
| 'canExpandable'
| 'onUpdateInternalRefs'
> {
dropdownPrefixCls?: string;
dataSource?: RcTableProps<RecordType>['data'];
columns?: ColumnsType<RecordType>;
pagination?: false | TablePaginationConfig;
loading?: boolean | SpinProps;
size?: SizeType;
bordered?: boolean;
locale?: TableLocale;
onChange?: (
pagination: TablePaginationConfig,
filters: Record<string, FilterValue | null>,
sorter: SorterResult<RecordType> | SorterResult<RecordType>[],
extra: TableCurrentDataSource<RecordType>,
) => void;
rowSelection?: TableRowSelection<RecordType>;
getPopupContainer?: GetPopupContainer;
scroll?: RcTableProps<RecordType>['scroll'] & {
scrollToFirstRowOnChange?: boolean;
};
sortDirections?: SortOrder[];
showSorterTooltip?: boolean | TooltipProps;
}
export const tableProps = () => {
return {
prefixCls: { type: String as PropType<string>, default: undefined },
columns: { type: Array as PropType<ColumnsType>, default: undefined },
rowKey: { type: [String, Function] as PropType<TableProps['rowKey']>, default: undefined },
tableLayout: { type: String as PropType<TableProps['tableLayout']>, default: undefined },
rowClassName: {
type: [String, Function] as PropType<TableProps['rowClassName']>,
default: undefined,
},
title: { type: Function as PropType<TableProps['title']>, default: undefined },
footer: { type: Function as PropType<TableProps['footer']>, default: undefined },
id: { type: String as PropType<TableProps['id']>, default: undefined },
showHeader: { type: Boolean as PropType<TableProps['showHeader']>, default: undefined },
components: { type: Object as PropType<TableProps['components']>, default: undefined },
customRow: { type: Function as PropType<TableProps['customRow']>, default: undefined },
customHeaderRow: {
type: Function as PropType<TableProps['customHeaderRow']>,
default: undefined,
},
direction: { type: String as PropType<TableProps['direction']>, default: undefined },
expandFixed: {
type: [Boolean, String] as PropType<TableProps['expandFixed']>,
default: undefined,
},
expandColumnWidth: {
type: Number as PropType<TableProps['expandColumnWidth']>,
default: undefined,
},
expandedRowKeys: {
type: Array as PropType<TableProps['expandedRowKeys']>,
default: undefined as TableProps['expandedRowKeys'],
},
defaultExpandedRowKeys: {
type: Array as PropType<TableProps['defaultExpandedRowKeys']>,
default: undefined as TableProps['defaultExpandedRowKeys'],
},
expandedRowRender: {
type: Function as PropType<TableProps['expandedRowRender']>,
default: undefined,
},
expandRowByClick: {
type: Boolean as PropType<TableProps['expandRowByClick']>,
default: undefined,
},
expandIcon: { type: Function as PropType<TableProps['expandIcon']>, default: undefined },
onExpand: { type: Function as PropType<TableProps['onExpand']>, default: undefined },
onExpandedRowsChange: {
type: Function as PropType<TableProps['onExpandedRowsChange']>,
default: undefined,
},
defaultExpandAllRows: {
type: Boolean as PropType<TableProps['defaultExpandAllRows']>,
default: undefined,
},
indentSize: { type: Number as PropType<TableProps['indentSize']>, default: undefined },
/** @deprecated Please use `EXPAND_COLUMN` in `columns` directly */
expandIconColumnIndex: {
type: Number as PropType<TableProps['expandIconColumnIndex']>,
default: undefined,
},
showExpandColumn: { type: Boolean, default: undefined },
expandedRowClassName: {
type: Function as PropType<TableProps['expandedRowClassName']>,
default: undefined,
},
childrenColumnName: {
type: String as PropType<TableProps['childrenColumnName']>,
default: undefined,
},
rowExpandable: { type: Function as PropType<TableProps['rowExpandable']>, default: undefined },
sticky: { type: [Boolean, Object] as PropType<TableProps['sticky']>, default: undefined },
dropdownPrefixCls: String,
dataSource: { type: Array as PropType<RcTableProps['data']>, default: undefined },
pagination: {
type: [Boolean, Object] as PropType<false | TablePaginationConfig>,
default: undefined,
},
loading: { type: [Boolean, Object] as PropType<false | SpinProps>, default: undefined },
size: { type: String as PropType<SizeType>, default: undefined },
bordered: Boolean,
locale: { type: Object as PropType<TableLocale>, default: undefined },
onChange: {
type: Function as PropType<
(
pagination: TablePaginationConfig,
filters: Record<string, FilterValue | null>,
sorter: SorterResult | SorterResult[],
extra: TableCurrentDataSource,
) => void
>,
default: undefined,
},
onResizeColumn: {
type: Function as PropType<(w: number, col: ColumnType) => void>,
default: undefined,
},
rowSelection: { type: Object as PropType<TableRowSelection>, default: undefined },
getPopupContainer: { type: Function as PropType<GetPopupContainer>, default: undefined },
scroll: {
type: Object as PropType<
RcTableProps['scroll'] & {
scrollToFirstRowOnChange?: boolean;
}
>,
default: undefined,
},
sortDirections: { type: Array as PropType<SortOrder[]>, default: undefined },
showSorterTooltip: {
type: [Boolean, Object] as PropType<boolean | TooltipProps>,
default: true,
},
contextSlots: {
type: Object as PropType<ContextSlots>,
},
transformCellText: {
type: Function as PropType<TableProps['transformCellText']>,
},
};
};
const InteralTable = defineComponent<
TableProps & {
contextSlots: ContextSlots;
}
>({
name: 'InteralTable',
inheritAttrs: false,
props: initDefaultProps(tableProps(), {
rowKey: 'key',
}) as any,
// emits: ['expandedRowsChange', 'change', 'expand'],
slots: [
'emptyText',
'expandIcon',
'title',
'footer',
'summary',
'expandedRowRender',
'bodyCell',
'headerCell',
'customFilterIcon',
'customFilterDropdown',
],
setup(props, { attrs, slots, expose, emit }) {
devWarning(
!(typeof props.rowKey === 'function' && props.rowKey.length > 1),
'Table',
'`index` parameter of `rowKey` function is deprecated. There is no guarantee that it will work as expected.',
);
useProvideSlots(computed(() => props.contextSlots));
useProvideTableContext({
onResizeColumn: (w, col) => {
emit('resizeColumn', w, col);
},
});
const screens = useBreakpoint();
const mergedColumns = computed(() => {
const matched = new Set(
Object.keys(screens.value).filter((m: Breakpoint) => screens.value[m]),
);
return props.columns.filter(
(c: ColumnType<DefaultRecordType>) =>
!c.responsive || c.responsive.some((r: Breakpoint) => matched.has(r)),
);
});
const {
size: mergedSize,
renderEmpty,
direction,
prefixCls,
configProvider,
} = useConfigInject('table', props);
const transformCellText = computed(
() => props.transformCellText || configProvider.transformCellText,
);
const [tableLocale] = useLocaleReceiver('Table', defaultLocale.Table, toRef(props, 'locale'));
const rawData = computed(() => props.dataSource || EMPTY_LIST);
const dropdownPrefixCls = computed(() =>
configProvider.getPrefixCls('dropdown', props.dropdownPrefixCls),
);
const childrenColumnName = computed(() => props.childrenColumnName || 'children');
const expandType = computed<ExpandType>(() => {
if (rawData.value.some(item => (item as any)?.[childrenColumnName.value])) {
return 'nest';
}
if (props.expandedRowRender) {
return 'row';
}
return null;
});
const internalRefs = reactive({
body: null,
});
const updateInternalRefs = refs => {
Object.assign(internalRefs, refs);
};
// ============================ RowKey ============================
const getRowKey = computed<GetRowKey<DefaultRecordType>>(() => {
if (typeof props.rowKey === 'function') {
return props.rowKey;
}
return record => (record as any)?.[props.rowKey as string];
});
const [getRecordByKey] = useLazyKVMap(rawData, childrenColumnName, getRowKey);
// ============================ Events =============================
const changeEventInfo: Partial<ChangeEventInfo> = {};
const triggerOnChange = (
info: Partial<ChangeEventInfo>,
action: TableAction,
reset = false,
) => {
const { pagination, scroll, onChange } = props;
const changeInfo = {
...changeEventInfo,
...info,
};
if (reset) {
changeEventInfo.resetPagination!();
// Reset event param
if (changeInfo.pagination!.current) {
changeInfo.pagination!.current = 1;
}
// Trigger pagination events
if (pagination && pagination.onChange) {
pagination.onChange(1, changeInfo.pagination!.pageSize);
}
}
if (scroll && scroll.scrollToFirstRowOnChange !== false && internalRefs.body) {
scrollTo(0, {
getContainer: () => internalRefs.body,
});
}
onChange?.(changeInfo.pagination!, changeInfo.filters!, changeInfo.sorter!, {
currentDataSource: getFilterData(
getSortData(rawData.value, changeInfo.sorterStates!, childrenColumnName.value),
changeInfo.filterStates!,
),
action,
});
};
/**
* Controlled state in `columns` is not a good idea that makes too many code (1000+ line?) to read
* state out and then put it back to title render. Move these code into `hooks` but still too
* complex. We should provides Table props like `sorter` & `filter` to handle control in next big version.
*/
// ============================ Sorter =============================
const onSorterChange = (sorter: SorterResult | SorterResult[], sorterStates: SortState[]) => {
triggerOnChange(
{
sorter,
sorterStates,
},
'sort',
false,
);
};
const [transformSorterColumns, sortStates, sorterTitleProps, sorters] = useSorter({
prefixCls,
mergedColumns,
onSorterChange,
sortDirections: computed(() => props.sortDirections || ['ascend', 'descend']),
tableLocale,
showSorterTooltip: toRef(props, 'showSorterTooltip'),
});
const sortedData = computed(() =>
getSortData(rawData.value, sortStates.value, childrenColumnName.value),
);
// ============================ Filter ============================
const onFilterChange = (filters: Record<string, FilterValue>, filterStates: FilterState[]) => {
triggerOnChange(
{
filters,
filterStates,
},
'filter',
true,
);
};
const [transformFilterColumns, filterStates, filters] = useFilter({
prefixCls,
locale: tableLocale,
dropdownPrefixCls,
mergedColumns,
onFilterChange,
getPopupContainer: toRef(props, 'getPopupContainer'),
});
const mergedData = computed(() => getFilterData(sortedData.value, filterStates.value));
// ============================ Column ============================
const [transformBasicColumns] = useColumns(toRef(props, 'contextSlots'));
const columnTitleProps = computed(() => ({
...sorterTitleProps.value,
}));
const [transformTitleColumns] = useTitleColumns(columnTitleProps);
// ========================== Pagination ==========================
const onPaginationChange = (current: number, pageSize: number) => {
triggerOnChange(
{
pagination: { ...changeEventInfo.pagination, current, pageSize },
},
'paginate',
);
};
const [mergedPagination, resetPagination] = usePagination(
computed(() => mergedData.value.length),
toRef(props, 'pagination'),
onPaginationChange,
);
watchEffect(() => {
changeEventInfo.sorter = sorters.value;
changeEventInfo.sorterStates = sortStates.value;
changeEventInfo.filters = filters.value;
changeEventInfo.filterStates = filterStates.value;
changeEventInfo.pagination =
props.pagination === false
? {}
: getPaginationParam(props.pagination, mergedPagination.value);
changeEventInfo.resetPagination = resetPagination;
});
// ============================= Data =============================
const pageData = computed(() => {
if (props.pagination === false || !mergedPagination.value.pageSize) {
return mergedData.value;
}
const { current = 1, total, pageSize = DEFAULT_PAGE_SIZE } = mergedPagination.value;
devWarning(current > 0, 'Table', '`current` should be positive number.');
// Dynamic table data
if (mergedData.value.length < total!) {
if (mergedData.value.length > pageSize) {
return mergedData.value.slice((current - 1) * pageSize, current * pageSize);
}
return mergedData.value;
}
return mergedData.value.slice((current - 1) * pageSize, current * pageSize);
});
watchEffect(
() => {
nextTick(() => {
const { total, pageSize = DEFAULT_PAGE_SIZE } = mergedPagination.value;
// Dynamic table data
if (mergedData.value.length < total!) {
if (mergedData.value.length > pageSize) {
devWarning(
false,
'Table',
'`dataSource` length is less than `pagination.total` but large than `pagination.pageSize`. Please make sure your config correct data with async mode.',
);
}
}
});
},
{ flush: 'post' },
);
const expandIconColumnIndex = computed(() => {
if (props.showExpandColumn === false) return -1;
// Adjust expand icon index, no overwrite expandIconColumnIndex if set.
if (expandType.value === 'nest' && props.expandIconColumnIndex === undefined) {
return props.rowSelection ? 1 : 0;
} else if (props.expandIconColumnIndex! > 0 && props.rowSelection) {
return props.expandIconColumnIndex - 1;
}
return props.expandIconColumnIndex;
});
const rowSelection = ref();
watch(
() => props.rowSelection,
() => {
rowSelection.value = props.rowSelection ? { ...props.rowSelection } : props.rowSelection;
},
{ deep: true, immediate: true },
);
// ========================== Selections ==========================
const [transformSelectionColumns, selectedKeySet] = useSelection(rowSelection, {
prefixCls,
data: mergedData,
pageData,
getRowKey,
getRecordByKey,
expandType,
childrenColumnName,
locale: tableLocale,
getPopupContainer: computed(() => props.getPopupContainer),
});
const internalRowClassName = (record: any, index: number, indent: number) => {
let mergedRowClassName;
const { rowClassName } = props;
if (typeof rowClassName === 'function') {
mergedRowClassName = classNames(rowClassName(record, index, indent));
} else {
mergedRowClassName = classNames(rowClassName);
}
return classNames(
{
[`${prefixCls.value}-row-selected`]: selectedKeySet.value.has(
getRowKey.value(record, index),
),
},
mergedRowClassName,
);
};
expose({
selectedKeySet,
});
const indentSize = computed(() => {
// Indent size
return typeof props.indentSize === 'number' ? props.indentSize : 15;
});
const transformColumns = (innerColumns: ColumnsType<any>): ColumnsType<any> => {
const res = transformTitleColumns(
transformSelectionColumns(
transformFilterColumns(transformSorterColumns(transformBasicColumns(innerColumns))),
),
);
return res;
};
return () => {
const {
expandIcon = slots.expandIcon || renderExpandIcon(tableLocale.value),
pagination,
loading,
bordered,
} = props;
let topPaginationNode;
let bottomPaginationNode;
if (pagination !== false && mergedPagination.value?.total) {
let paginationSize: TablePaginationConfig['size'];
if (mergedPagination.value.size) {
paginationSize = mergedPagination.value.size;
} else {
paginationSize =
mergedSize.value === 'small' || mergedSize.value === 'middle' ? 'small' : undefined;
}
const renderPagination = (position: string) => (
<Pagination
{...mergedPagination.value}
class={[
`${prefixCls.value}-pagination ${prefixCls.value}-pagination-${position}`,
mergedPagination.value.class,
]}
size={paginationSize}
/>
);
const defaultPosition = direction.value === 'rtl' ? 'left' : 'right';
const { position } = mergedPagination.value;
if (position !== null && Array.isArray(position)) {
const topPos = position.find(p => p.indexOf('top') !== -1);
const bottomPos = position.find(p => p.indexOf('bottom') !== -1);
const isDisable = position.every(p => `${p}` === 'none');
if (!topPos && !bottomPos && !isDisable) {
bottomPaginationNode = renderPagination(defaultPosition);
}
if (topPos) {
topPaginationNode = renderPagination(topPos!.toLowerCase().replace('top', ''));
}
if (bottomPos) {
bottomPaginationNode = renderPagination(bottomPos!.toLowerCase().replace('bottom', ''));
}
} else {
bottomPaginationNode = renderPagination(defaultPosition);
}
}
// >>>>>>>>> Spinning
let spinProps: SpinProps | undefined;
if (typeof loading === 'boolean') {
spinProps = {
spinning: loading,
};
} else if (typeof loading === 'object') {
spinProps = {
spinning: true,
...loading,
};
}
const wrapperClassNames = classNames(
`${prefixCls.value}-wrapper`,
{
[`${prefixCls.value}-wrapper-rtl`]: direction.value === 'rtl',
},
attrs.class,
);
const tableProps = omit(props, ['columns']);
return (
<div class={wrapperClassNames} style={attrs.style as CSSProperties}>
<Spin spinning={false} {...spinProps}>
{topPaginationNode}
<RcTable
{...attrs}
{...tableProps}
expandedRowKeys={props.expandedRowKeys as any}
defaultExpandedRowKeys={props.defaultExpandedRowKeys as any}
expandIconColumnIndex={expandIconColumnIndex.value}
indentSize={indentSize.value}
expandIcon={expandIcon}
columns={mergedColumns.value}
direction={direction.value}
prefixCls={prefixCls.value}
class={classNames({
[`${prefixCls.value}-middle`]: mergedSize.value === 'middle',
[`${prefixCls.value}-small`]: mergedSize.value === 'small',
[`${prefixCls.value}-bordered`]: bordered,
[`${prefixCls.value}-empty`]: rawData.value.length === 0,
})}
data={pageData.value}
rowKey={getRowKey.value}
rowClassName={internalRowClassName}
// Internal
internalHooks={INTERNAL_HOOKS}
internalRefs={internalRefs}
onUpdateInternalRefs={updateInternalRefs}
transformColumns={transformColumns}
transformCellText={transformCellText.value}
v-slots={{
...slots,
emptyText: () =>
slots.emptyText?.() || props.locale?.emptyText || renderEmpty.value('Table'),
}}
/>
{bottomPaginationNode}
</Spin>
</div>
);
};
},
});
const Table = defineComponent<TableProps>({
name: 'ATable',
inheritAttrs: false,
setup(_props, { attrs, slots, expose }) {
const table = ref();
expose({
table,
});
return () => {
const props = attrs as TableProps;
const columns = props.columns || convertChildrenToColumns(slots.default?.());
return (
<InteralTable
ref={table}
{...attrs}
columns={columns || []}
expandedRowRender={slots.expandedRowRender}
contextSlots={{ ...slots }} // use new object, 否则slot热更新失效原因需进一步探究
v-slots={slots}
/>
);
};
},
});
export default Table;