refactor: table (#4641)

* refactor: table

* refactor: table

* refactor: table

* refactor: table

* refactor: table

* refactor: table

* refactor: table

* refactor: table

* refactor: table

* fix: column not pass to cell

* doc: uppate table

* fix: update bodyCell headerCell

* doc: remove examples

* refactor: table

* fix: table title not work

* fix: table selection

* fix: table checkStrictly

* refactor: table

* fix: table template error

* feat: table support summary

* test: update snap

* perf: table

* docs(table): fix ajax demo (#4639)

* test: update table

* refactor: remove old table

* doc: update  table doc

* doc: update doc

* doc: update select

* doc: update summary

Co-authored-by: John <John60676@qq.com>
pull/4671/head
tangjinzhou 2021-09-11 23:58:50 +08:00 committed by GitHub
parent 1aee88e0ba
commit 21502ea6b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
152 changed files with 15122 additions and 14267 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

@ -0,0 +1,14 @@
import type { UnwrapRef } from 'vue';
import { reactive, toRef } from 'vue';
/**
* Reactively pick fields from a reactive object
*
* @see https://vueuse.js.org/reactivePick
*/
export function reactivePick<T extends object, K extends keyof T>(
obj: T,
...keys: K[]
): { [S in K]: UnwrapRef<T[S]> } {
return reactive(Object.fromEntries(keys.map(k => [k, toRef(obj, k)]))) as any;
}

View File

@ -0,0 +1,42 @@
import { isRef, reactive } from 'vue';
import type { Ref } from 'vue';
type MaybeRef<T> = T | Ref<T>;
/**
* Converts ref to reactive.
*
* @see https://vueuse.org/toReactive
* @param objectRef A ref of object
*/
export function toReactive<T extends object>(objectRef: MaybeRef<T>): T {
if (!isRef(objectRef)) return reactive(objectRef) as T;
const proxy = new Proxy(
{},
{
get(_, p, receiver) {
return Reflect.get(objectRef.value, p, receiver);
},
set(_, p, value) {
(objectRef.value as any)[p] = value;
return true;
},
deleteProperty(_, p) {
return Reflect.deleteProperty(objectRef.value, p);
},
has(_, p) {
return Reflect.has(objectRef.value, p);
},
ownKeys() {
return Object.keys(objectRef.value);
},
getOwnPropertyDescriptor() {
return {
enumerable: true,
configurable: true,
};
},
},
);
return reactive(proxy) as T;
}

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

@ -1,3 +1,4 @@
import type { ExtractPropTypes } from 'vue';
import { defineComponent, inject, nextTick } from 'vue';
import PropTypes from '../_util/vue-types';
import classNames from '../_util/classNames';
@ -9,11 +10,8 @@ import type { RadioChangeEvent } from '../radio/interface';
import type { EventHandler } from '../_util/EventInterface';
function noop() {}
export default defineComponent({
name: 'ACheckbox',
inheritAttrs: false,
__ANT_CHECKBOX: true,
props: {
export const checkboxProps = () => {
return {
prefixCls: PropTypes.string,
defaultChecked: PropTypes.looseBool,
checked: PropTypes.looseBool,
@ -27,7 +25,17 @@ export default defineComponent({
autofocus: PropTypes.looseBool,
onChange: PropTypes.func,
'onUpdate:checked': PropTypes.func,
},
skipGroup: PropTypes.looseBool,
};
};
export type CheckboxProps = Partial<ExtractPropTypes<ReturnType<typeof checkboxProps>>>;
export default defineComponent({
name: 'ACheckbox',
inheritAttrs: false,
__ANT_CHECKBOX: true,
props: checkboxProps(),
emits: ['change', 'update:checked'],
setup() {
return {
@ -38,6 +46,9 @@ export default defineComponent({
watch: {
value(value, prevValue) {
if (this.skipGroup) {
return;
}
nextTick(() => {
const { checkboxGroupContext: checkboxGroup = {} } = this;
if (checkboxGroup.registerValue && checkboxGroup.cancelValue) {
@ -85,7 +96,7 @@ export default defineComponent({
const props = getOptionProps(this);
const { checkboxGroupContext: checkboxGroup, $attrs } = this;
const children = getSlot(this);
const { indeterminate, prefixCls: customizePrefixCls, ...restProps } = props;
const { indeterminate, prefixCls: customizePrefixCls, skipGroup, ...restProps } = props;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('checkbox', customizePrefixCls);
const {
@ -101,7 +112,7 @@ export default defineComponent({
prefixCls,
...restAttrs,
};
if (checkboxGroup) {
if (checkboxGroup && !skipGroup) {
checkboxProps.onChange = (...args) => {
this.$emit('change', ...args);
checkboxGroup.toggleOption({ label: children, value: props.value });

View File

@ -1,6 +1,7 @@
import type { App, Plugin } from 'vue';
import Checkbox from './Checkbox';
import Checkbox, { checkboxProps } from './Checkbox';
import CheckboxGroup from './Group';
export type { CheckboxProps } from './Checkbox';
Checkbox.Group = CheckboxGroup;
@ -10,7 +11,7 @@ Checkbox.install = function (app: App) {
app.component(CheckboxGroup.name, CheckboxGroup);
return app;
};
export { CheckboxGroup };
export { CheckboxGroup, checkboxProps };
export default Checkbox as typeof Checkbox &
Plugin & {
readonly Group: typeof CheckboxGroup;

View File

@ -164,7 +164,22 @@ export { default as Steps, Step } from './steps';
export type { SwitchProps } from './switch';
export { default as Switch } from './switch';
export { default as Table, TableColumn, TableColumnGroup } from './table';
export type {
TableProps,
TablePaginationConfig,
ColumnGroupType as TableColumnGroupType,
ColumnType as TableColumnType,
ColumnProps as TableColumnProps,
ColumnsType as TableColumnsType,
} from './table';
export {
default as Table,
TableColumn,
TableColumnGroup,
TableSummary,
TableSummaryRow,
TableSummaryCell,
} from './table';
export type { TransferProps } from './transfer';
export { default as Transfer } from './transfer';

View File

@ -113,32 +113,47 @@ exports[`renders ./components/empty/demo/config-provider.vue correctly 1`] = `
<div class="ant-spin-nested-loading">
<!---->
<div class="ant-spin-container">
<div class="ant-table ant-table-default ant-table-empty ant-table-scroll-position-left">
<div class="ant-table ant-table-empty" style="margin-top: 8px;">
<!---->
<div class="ant-table-content">
<!---->
<div class="ant-table-body">
<table class="">
<colgroup>
<col data-key="0">
<col data-key="1">
</colgroup>
<div class="ant-table-container">
<div class="ant-table-content">
<table style="table-layout: auto;">
<colgroup></colgroup>
<thead class="ant-table-thead">
<tr>
<th colstart="0" colspan="1" colend="0" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Name</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="1" colspan="1" colend="1" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Age</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<th class="ant-table-cell" colstart="0" colend="0">
<!---->Name
</th>
<th class="ant-table-cell" colstart="1" colend="1">
<!---->Age
</th>
</tr>
</thead>
<tbody class="ant-table-tbody">
<!---->
<tr class="ant-table-placeholder">
<td colspan="2" class="ant-table-cell">
<!---->No data
</td>
</tr>
</tbody>
<!---->
</table>
</div>
</div>
<!---->
</th>
</tr>
</thead>
<tbody class="ant-table-tbody"></tbody>
</table>
</div>
<div class="ant-table-placeholder">
</div>
</div>
</div>
<h3>List</h3>
<div class="ant-list ant-list-split">
<!---->
<!---->
<div class="ant-spin-nested-loading">
<!---->
<div class="ant-spin-container">
<div class="ant-list-empty-text">
<div class="ant-empty ant-empty-normal">
<div class="ant-empty-image"><svg class="ant-empty-img-simple" width="64" height="41" viewBox="0 0 64 41">
<g transform="translate(0 1)" fill="none" fill-rule="evenodd">
@ -153,39 +168,11 @@ exports[`renders ./components/empty/demo/config-provider.vue correctly 1`] = `
<!---->
</div>
</div>
<!---->
</div>
</div>
</div>
</div>
</div>
<h3>List</h3>
<div class="ant-list ant-list-split">
<!---->
<!---->
<div class="ant-spin-nested-loading">
<!---->
<div class="ant-spin-container">
<div class="ant-list-empty-text">
<div class="ant-empty ant-empty-normal">
<div class="ant-empty-image"><svg class="ant-empty-img-simple" width="64" height="41" viewBox="0 0 64 41">
<g transform="translate(0 1)" fill="none" fill-rule="evenodd">
<ellipse class="ant-empty-img-simple-ellipse" fill="#F5F5F5" cx="32" cy="33" rx="32" ry="7"></ellipse>
<g class="ant-empty-img-simple-g" fill-rule="nonzero" stroke="#D9D9D9">
<path d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"></path>
<path d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z" fill="#FAFAFA" class="ant-empty-img-simple-path"></path>
</g>
</g>
</svg></div>
<p class="ant-empty-description">No Data</p>
<!---->
</div>
</div>
</div>
<!---->
</div>
<!---->
<!---->
</div>
</div>
`;

View File

@ -9,6 +9,7 @@ import type { ValidateMessages } from '../form/interface';
import type { TransferLocale } from '../transfer';
import type { PickerLocale as DatePickerLocale } from '../date-picker/generatePicker';
import type { PaginationLocale } from '../pagination/Pagination';
import type { TableLocale } from '../table/interface';
interface TransferLocaleForEmpty {
description: string;
@ -16,7 +17,7 @@ interface TransferLocaleForEmpty {
export interface Locale {
locale: string;
Pagination?: PaginationLocale;
Table?: Record<string, any>;
Table?: TableLocale;
Popconfirm?: Record<string, any>;
Upload?: Record<string, any>;
Form?: {

View File

@ -24,43 +24,43 @@ Select component to select value from options.
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| allowClear | Show clear button. | boolean | false | |
| autoClearSearchValue | Whether the current search will be cleared on selecting an item. Only applies when `mode` is set to `multiple` or `tags`. | boolean | true | |
| autofocus | Get focus by default | boolean | false | |
| bordered | Whether has border style | boolean | true | |
| defaultActiveFirstOption | Whether active first option by default | boolean | true | |
| disabled | Whether disabled select | boolean | false | |
| dropdownClassName | className of dropdown menu | string | - | |
| dropdownMatchSelectWidth | Whether dropdown's width is same with select. | boolean | true | |
| dropdownRender | Customize dropdown content | ({menuNode: VNode, props}) => VNode \| v-slot | - | |
| dropdownStyle | style of dropdown menu | object | - | |
| dropdownMenuStyle | additional style applied to dropdown menu | object | - | |
| filterOption | If true, filter options by input, if function, filter options against it. The function will receive two arguments, `inputValue` and `option`, if the function returns `true`, the option will be included in the filtered set; Otherwise, it will be excluded. | boolean or function(inputValue, option) | true | |
| firstActiveValue | Value of action option by default | string\|string\[] | - | |
| getPopupContainer | Parent Node which the selector should be rendered to. Default to `body`. When position issues happen, try to modify it into scrollable content and position it relative. | function(triggerNode) | () => document.body | |
| labelInValue | whether to embed label in value, turn the format of value from `string` to `{key: string, label: vNodes}` | boolean | false | |
| maxTagCount | Max tag count to show | number | - | |
| maxTagPlaceholder | Placeholder for not showing tags | slot/function(omittedValues) | - | |
| maxTagTextLength | Max text length to show | number | - | |
| mode | Set mode of Select | 'multiple' \| 'tags' | - | |
| notFoundContent | Specify content to show when no result matches.. | string\|slot | 'Not Found' | |
| optionFilterProp | Which prop value of option will be used for filter if filterOption is true | string | value | |
| optionLabelProp | Which prop value of option will render as content of select. | string | `value` for `combobox`, `children` for other modes | |
| placeholder | Placeholder of select | string\|slot | - | |
| showSearch | Whether show search input in single mode. | boolean | false | |
| showArrow | Whether to show the drop-down arrow | boolean | true | |
| size | Size of Select input. `default` `large` `small` | string | default | |
| suffixIcon | The custom suffix icon | VNode \| slot | - | |
| removeIcon | The custom remove icon | VNode \| slot | - | |
| clearIcon | The custom clear icon | VNode \| slot | - | |
| menuItemSelectedIcon | The custom menuItemSelected icon | VNode \| slot | - | |
| tokenSeparators | Separator used to tokenize on tag/multiple mode | string\[] | | |
| value(v-model) | Current selected option. | string\|number\|string\[]\|number\[] | - | |
| options | Data of the selectOption, manual construction work is no longer needed if this property has been set | array&lt;{value, label, [disabled, key, title]}> | \[] | |
| allowClear | Show clear button. | boolean | false | |
| autoClearSearchValue | Whether the current search will be cleared on selecting an item. Only applies when `mode` is set to `multiple` or `tags`. | boolean | true | |
| autofocus | Get focus by default | boolean | false | |
| bordered | Whether has border style | boolean | true | |
| defaultActiveFirstOption | Whether active first option by default | boolean | true | |
| disabled | Whether disabled select | boolean | false | |
| dropdownClassName | className of dropdown menu | string | - | |
| dropdownMatchSelectWidth | Whether dropdown's width is same with select. | boolean | true | |
| dropdownRender | Customize dropdown content | ({menuNode: VNode, props}) => VNode \| v-slot | - | |
| dropdownStyle | style of dropdown menu | object | - | |
| dropdownMenuStyle | additional style applied to dropdown menu | object | - | |
| filterOption | If true, filter options by input, if function, filter options against it. The function will receive two arguments, `inputValue` and `option`, if the function returns `true`, the option will be included in the filtered set; Otherwise, it will be excluded. | boolean or function(inputValue, option) | true | |
| firstActiveValue | Value of action option by default | string\|string\[] | - | |
| getPopupContainer | Parent Node which the selector should be rendered to. Default to `body`. When position issues happen, try to modify it into scrollable content and position it relative. | function(triggerNode) | () => document.body | |
| labelInValue | whether to embed label in value, turn the format of value from `string` to `{key: string, label: vNodes}` | boolean | false | |
| maxTagCount | Max tag count to show | number | - | |
| maxTagPlaceholder | Placeholder for not showing tags | slot/function(omittedValues) | - | |
| maxTagTextLength | Max text length to show | number | - | |
| mode | Set mode of Select | 'multiple' \| 'tags' | - | |
| notFoundContent | Specify content to show when no result matches.. | string\|slot | 'Not Found' | |
| optionFilterProp | Which prop value of option will be used for filter if filterOption is true | string | value | |
| optionLabelProp | Which prop value of option will render as content of select. | string | `value` for `combobox`, `children` for other modes | |
| placeholder | Placeholder of select | string\|slot | - | |
| showSearch | Whether show search input in single mode. | boolean | false | |
| showArrow | Whether to show the drop-down arrow | boolean | true | |
| size | Size of Select input. `default` `large` `small` | string | default | |
| suffixIcon | The custom suffix icon | VNode \| slot | - | |
| removeIcon | The custom remove icon | VNode \| slot | - | |
| clearIcon | The custom clear icon | VNode \| slot | - | |
| menuItemSelectedIcon | The custom menuItemSelected icon | VNode \| slot | - | |
| tokenSeparators | Separator used to tokenize on tag/multiple mode | string\[] | | |
| value(v-model) | Current selected option. | string\|number\|string\[]\|number\[] | - | |
| options | Data of the selectOption, manual construction work is no longer needed if this property has been set | array&lt;{value, label, [disabled, key, title]}> | \[] | |
| option | custom render option by slot | v-slot:option="{value, label, [disabled, key, title]}" | - | 2.2.5 |
| defaultOpen | Initial open state of dropdown | boolean | - | |
| open | Controlled open state of dropdown | boolean | - | |
| loading | indicate loading state | Boolean | false | |
| defaultOpen | Initial open state of dropdown | boolean | - | |
| open | Controlled open state of dropdown | boolean | - | |
| loading | indicate loading state | Boolean | false | |
> Note, if you find that the drop-down menu scrolls with the page, or you need to trigger Select in other popup layers, please try to use `getPopupContainer={triggerNode => triggerNode.parentElement}` to fix the drop-down popup rendering node in the parent element of the trigger .
@ -109,3 +109,11 @@ Select component to select value from options.
### The dropdown is closed when click `dropdownRender` area?
See the [dropdownRender example](/components/select/#components-select-demo-custom-dropdown).
### Why is `placeholder` not displayed?
`placeholder` will only be displayed when `value = undefined`, and other values such as null, 0,'', etc. are meaningful values for the JS language.
You can check [JS Language Specification](https://262.ecma-international.org/5.1/#sec-4.3.9) for further details.
You can also check [antd issue](https://github.com/ant-design/ant-design/issues/2367) to view the discussion.

View File

@ -25,42 +25,42 @@ cover: https://gw.alipayobjects.com/zos/alicdn/_0XzgOis7/Select.svg
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| allowClear | 支持清除 | boolean | false | |
| autoClearSearchValue | 是否在选中项后清空搜索框,只在 `mode``multiple``tags` 时有效。 | boolean | true | |
| autofocus | 默认获取焦点 | boolean | false | |
| bordered | 是否有边框 | boolean | true | |
| defaultActiveFirstOption | 是否默认高亮第一个选项。 | boolean | true | |
| disabled | 是否禁用 | boolean | false | |
| dropdownClassName | 下拉菜单的 className 属性 | string | - | |
| dropdownMatchSelectWidth | 下拉菜单和选择器同宽 | boolean | true | 」
| dropdownRender | 自定义下拉框内容 | ({menuNode: VNode, props}) => VNode \| v-slot | - | |
| dropdownStyle | 下拉菜单的 style 属性 | object | - | |
| dropdownMenuStyle | dropdown 菜单自定义样式 | object | - | |
| filterOption | 是否根据输入项进行筛选。当其为一个函数时,会接收 `inputValue` `option` 两个参数,当 `option` 符合筛选条件时,应返回 `true`,反之则返回 `false`。 | boolean or function(inputValue, option) | true | |
| firstActiveValue | 默认高亮的选项 | string\|string\[] | - | |
| getPopupContainer | 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。 | Function(triggerNode) | () => document.body | |
| labelInValue | 是否把每个选项的 label 包装到 value 中,会把 Select 的 value 类型从 `string` 变为 `{key: string, label: vNodes}` 的格式 | boolean | false | |
| maxTagCount | 最多显示多少个 tag | number | - | |
| maxTagPlaceholder | 隐藏 tag 时显示的内容 | slot/function(omittedValues) | - | |
| maxTagTextLength | 最大显示的 tag 文本长度 | number | - | |
| mode | 设置 Select 的模式为多选或标签 | 'multiple' \| 'tags' \| 'combobox' | - | |
| notFoundContent | 当下拉列表为空时显示的内容 | string\|slot | 'Not Found' | |
| optionFilterProp | 搜索时过滤对应的 option 属性,不支持 children | string | value | |
| optionLabelProp | 回填到选择框的 Option 的属性值,默认是 Option 的子元素。比如在子元素需要高亮效果时,此值可以设为 `value`。 | string | `children` combobox 模式下为 `value` | |
| placeholder | 选择框默认文字 | string\|slot | - | |
| showSearch | 使单选模式可搜索 | boolean | false | |
| showArrow | 是否显示下拉小箭头 | boolean | true | |
| size | 选择框大小,可选 `large` `small` | string | default | |
| suffixIcon | 自定义的选择框后缀图标 | VNode \| slot | - | |
| removeIcon | 自定义的多选框清除图标 | VNode \| slot | - | |
| clearIcon | 自定义的多选框清空图标 | VNode \| slot | - | |
| menuItemSelectedIcon | 自定义当前选中的条目图标 | VNode \| slot | - | |
| tokenSeparators | 在 tags 和 multiple 模式下自动分词的分隔符 | string\[] | | |
| value(v-model) | 指定当前选中的条目 | string\|string\[]\|number\|number\[] | - | |
| options | options 数据,如果设置则不需要手动构造 selectOption 节点 | array&lt;{value, label, [disabled, key, title]}> | \[] | |
| allowClear | 支持清除 | boolean | false | |
| autoClearSearchValue | 是否在选中项后清空搜索框,只在 `mode``multiple``tags` 时有效。 | boolean | true | |
| autofocus | 默认获取焦点 | boolean | false | |
| bordered | 是否有边框 | boolean | true | |
| defaultActiveFirstOption | 是否默认高亮第一个选项。 | boolean | true | |
| disabled | 是否禁用 | boolean | false | |
| dropdownClassName | 下拉菜单的 className 属性 | string | - | |
| dropdownMatchSelectWidth | 下拉菜单和选择器同宽 | boolean | true | 」 |
| dropdownRender | 自定义下拉框内容 | ({menuNode: VNode, props}) => VNode \| v-slot | - | |
| dropdownStyle | 下拉菜单的 style 属性 | object | - | |
| dropdownMenuStyle | dropdown 菜单自定义样式 | object | - | |
| filterOption | 是否根据输入项进行筛选。当其为一个函数时,会接收 `inputValue` `option` 两个参数,当 `option` 符合筛选条件时,应返回 `true`,反之则返回 `false`。 | boolean or function(inputValue, option) | true | |
| firstActiveValue | 默认高亮的选项 | string\|string\[] | - | |
| getPopupContainer | 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。 | Function(triggerNode) | () => document.body | |
| labelInValue | 是否把每个选项的 label 包装到 value 中,会把 Select 的 value 类型从 `string` 变为 `{key: string, label: vNodes}` 的格式 | boolean | false | |
| maxTagCount | 最多显示多少个 tag | number | - | |
| maxTagPlaceholder | 隐藏 tag 时显示的内容 | slot/function(omittedValues) | - | |
| maxTagTextLength | 最大显示的 tag 文本长度 | number | - | |
| mode | 设置 Select 的模式为多选或标签 | 'multiple' \| 'tags' \| 'combobox' | - | |
| notFoundContent | 当下拉列表为空时显示的内容 | string\|slot | 'Not Found' | |
| optionFilterProp | 搜索时过滤对应的 option 属性,不支持 children | string | value | |
| optionLabelProp | 回填到选择框的 Option 的属性值,默认是 Option 的子元素。比如在子元素需要高亮效果时,此值可以设为 `value`。 | string | `children` combobox 模式下为 `value` | |
| placeholder | 选择框默认文字 | string\|slot | - | |
| showSearch | 使单选模式可搜索 | boolean | false | |
| showArrow | 是否显示下拉小箭头 | boolean | true | |
| size | 选择框大小,可选 `large` `small` | string | default | |
| suffixIcon | 自定义的选择框后缀图标 | VNode \| slot | - | |
| removeIcon | 自定义的多选框清除图标 | VNode \| slot | - | |
| clearIcon | 自定义的多选框清空图标 | VNode \| slot | - | |
| menuItemSelectedIcon | 自定义当前选中的条目图标 | VNode \| slot | - | |
| tokenSeparators | 在 tags 和 multiple 模式下自动分词的分隔符 | string\[] | | |
| value(v-model) | 指定当前选中的条目 | string\|string\[]\|number\|number\[] | - | |
| options | options 数据,如果设置则不需要手动构造 selectOption 节点 | array&lt;{value, label, [disabled, key, title]}> | \[] | |
| option | 通过 option 插槽,自定义节点 | v-slot:option="{value, label, [disabled, key, title]}" | - | 2.2.5 |
| defaultOpen | 是否默认展开下拉菜单 | boolean | - | |
| open | 是否展开下拉菜单 | boolean | - | |
| defaultOpen | 是否默认展开下拉菜单 | boolean | - | |
| open | 是否展开下拉菜单 | boolean | - | |
> 注意,如果发现下拉菜单跟随页面滚动,或者需要在其他弹层中触发 Select请尝试使用 `getPopupContainer={triggerNode => triggerNode.parentNode}` 将下拉弹层渲染节点固定在触发器的父元素中。
@ -109,3 +109,11 @@ cover: https://gw.alipayobjects.com/zos/alicdn/_0XzgOis7/Select.svg
### 点击 `dropdownRender` 里的内容浮层关闭怎么办?
看下 [dropdownRender 例子](/components/select-cn/#components-select-demo-custom-dropdown) 里的说明。
### 为什么 `placeholder` 不显示
`placeholder` 只有在 value = undefined 才会显示,对于其它的 null、0、'' 等等对于 JS 语言都是有意义的值。
你可以查看 [JS 语言规范](https://262.ecma-international.org/5.1/#sec-4.3.9) 进一步了解详情。
也可以查看 [antd issue](https://github.com/ant-design/ant-design/issues/2367) 查看讨论情况。

View File

@ -556,8 +556,8 @@
@table-header-bg: @background-color-light;
@table-header-color: @heading-color;
@table-header-sort-bg: @background-color-base;
@table-body-sort-bg: rgba(0, 0, 0, 0.01);
@table-row-hover-bg: @primary-1;
@table-body-sort-bg: #fafafa;
@table-row-hover-bg: @background-color-light;
@table-selected-row-color: inherit;
@table-selected-row-bg: @primary-1;
@table-body-selected-sort-bg: @table-selected-row-bg;
@ -565,15 +565,31 @@
@table-expanded-row-bg: #fbfbfb;
@table-padding-vertical: 16px;
@table-padding-horizontal: 16px;
@table-padding-vertical-md: (@table-padding-vertical * 3 / 4);
@table-padding-horizontal-md: (@table-padding-horizontal / 2);
@table-padding-vertical-sm: (@table-padding-vertical / 2);
@table-padding-horizontal-sm: (@table-padding-horizontal / 2);
@table-border-color: @border-color-split;
@table-border-radius-base: @border-radius-base;
@table-footer-bg: @background-color-light;
@table-footer-color: @heading-color;
@table-header-bg-sm: transparent;
@table-header-bg-sm: @table-header-bg;
@table-font-size: @font-size-base;
@table-font-size-md: @table-font-size;
@table-font-size-sm: @table-font-size;
@table-header-cell-split-color: rgba(0, 0, 0, 0.06);
// Sorter
// Legacy: `table-header-sort-active-bg` is used for hover not real active
@table-header-sort-active-bg: darken(@table-header-bg, 3%);
@table-header-sort-active-bg: rgba(0, 0, 0, 0.04);
// Filter
@table-header-filter-active-bg: darken(@table-header-sort-active-bg, 5%);
@table-header-filter-active-bg: rgba(0, 0, 0, 0.04);
@table-filter-btns-bg: inherit;
@table-filter-dropdown-bg: @component-background;
@table-expand-icon-bg: @component-background;
@table-selection-column-width: 32px;
// Sticky
@table-sticky-scroll-bar-bg: fade(#000, 35%);
@table-sticky-scroll-bar-radius: 4px;
// Tag
// --

View File

@ -1,9 +1,10 @@
import { defineComponent } from 'vue';
import { columnProps } from './interface';
import type { ColumnType } from './interface';
export default defineComponent({
export type ColumnProps<RecordType = unknown> = ColumnType<RecordType>;
export default defineComponent<ColumnProps>({
name: 'ATableColumn',
props: columnProps,
slots: ['title', 'filterIcon'],
render() {
return null;
},

View File

@ -1,15 +1,9 @@
import { defineComponent } from 'vue';
import PropTypes, { withUndefined } from '../_util/vue-types';
import { tuple } from '../_util/type';
import type { ColumnGroupProps } from '../vc-table/sugar/ColumnGroup';
export default defineComponent({
export default defineComponent<ColumnGroupProps<any>>({
name: 'ATableColumnGroup',
props: {
fixed: withUndefined(
PropTypes.oneOfType([PropTypes.looseBool, PropTypes.oneOf(tuple('left', 'right'))]),
),
title: PropTypes.any,
},
slots: ['title'],
__ANT_TABLE_COLUMN_GROUP: true,
render() {
return null;

View File

@ -0,0 +1,40 @@
import classNames from '../_util/classNames';
import type { TableLocale } from './interface';
interface DefaultExpandIconProps<RecordType> {
prefixCls: string;
onExpand: (record: RecordType, e: MouseEvent) => void;
record: RecordType;
expanded: boolean;
expandable: boolean;
}
function renderExpandIcon(locale: TableLocale) {
return function expandIcon<RecordType>({
prefixCls,
onExpand,
record,
expanded,
expandable,
}: DefaultExpandIconProps<RecordType>) {
const iconPrefix = `${prefixCls}-row-expand-icon`;
return (
<button
type="button"
onClick={e => {
onExpand(record, e!);
e.stopPropagation();
}}
class={classNames(iconPrefix, {
[`${iconPrefix}-spaced`]: !expandable,
[`${iconPrefix}-expanded`]: expandable && expanded,
[`${iconPrefix}-collapsed`]: expandable && !expanded,
})}
aria-label={expanded ? locale.collapse : locale.expand}
/>
);
};
}
export default renderExpandIcon;

View File

@ -1,20 +0,0 @@
import type { FunctionalComponent } from 'vue';
export interface FilterDropdownMenuWrapperProps {
class?: string;
}
const FilterDropdownMenuWrapper: FunctionalComponent<FilterDropdownMenuWrapperProps> = (
props,
{ slots },
) => {
return (
<div class={props.class} onClick={e => e.stopPropagation()}>
{slots.default?.()}
</div>
);
};
FilterDropdownMenuWrapper.inheritAttrs = false;
export default FilterDropdownMenuWrapper;

View File

@ -1,42 +0,0 @@
import { computed, defineComponent } from 'vue';
import Checkbox from '../checkbox';
import Radio from '../radio';
import { SelectionBoxProps } from './interface';
import BaseMixin from '../_util/BaseMixin';
import { getOptionProps } from '../_util/props-util';
export default defineComponent({
name: 'SelectionBox',
mixins: [BaseMixin],
inheritAttrs: false,
props: SelectionBoxProps,
setup(props) {
return {
checked: computed(() => {
const { store, defaultSelection, rowIndex } = props;
let checked = false;
if (store.selectionDirty) {
checked = store.selectedRowKeys.indexOf(rowIndex) >= 0;
} else {
checked =
store.selectedRowKeys.indexOf(rowIndex) >= 0 || defaultSelection.indexOf(rowIndex) >= 0;
}
return checked;
}),
};
},
render() {
const { type, rowIndex, ...rest } = { ...getOptionProps(this), ...this.$attrs } as any;
const { checked } = this;
const checkboxProps = {
checked,
...rest,
};
if (type === 'radio') {
checkboxProps.value = rowIndex;
return <Radio {...checkboxProps} />;
}
return <Checkbox {...checkboxProps} />;
},
});

View File

@ -1,188 +0,0 @@
import DownOutlined from '@ant-design/icons-vue/DownOutlined';
import Checkbox from '../checkbox';
import Dropdown from '../dropdown';
import Menu from '../menu';
import classNames from '../_util/classNames';
import { SelectionCheckboxAllProps } from './interface';
import BaseMixin from '../_util/BaseMixin';
import { computed, defineComponent } from 'vue';
function checkSelection({
store,
getCheckboxPropsByItem,
getRecordKey,
data,
type,
byDefaultChecked,
}) {
return byDefaultChecked
? data[type]((item, i) => getCheckboxPropsByItem(item, i).defaultChecked)
: data[type]((item, i) => store.selectedRowKeys.indexOf(getRecordKey(item, i)) >= 0);
}
function getIndeterminateState(props) {
const { store, data } = props;
if (!data.length) {
return false;
}
const someCheckedNotByDefaultChecked =
checkSelection({
...props,
data,
type: 'some',
byDefaultChecked: false,
}) &&
!checkSelection({
...props,
data,
type: 'every',
byDefaultChecked: false,
});
const someCheckedByDefaultChecked =
checkSelection({
...props,
data,
type: 'some',
byDefaultChecked: true,
}) &&
!checkSelection({
...props,
data,
type: 'every',
byDefaultChecked: true,
});
if (store.selectionDirty) {
return someCheckedNotByDefaultChecked;
}
return someCheckedNotByDefaultChecked || someCheckedByDefaultChecked;
}
function getCheckState(props) {
const { store, data } = props;
if (!data.length) {
return false;
}
if (store.selectionDirty) {
return checkSelection({
...props,
data,
type: 'every',
byDefaultChecked: false,
});
}
return (
checkSelection({
...props,
data,
type: 'every',
byDefaultChecked: false,
}) ||
checkSelection({
...props,
data,
type: 'every',
byDefaultChecked: true,
})
);
}
export default defineComponent({
name: 'SelectionCheckboxAll',
mixins: [BaseMixin],
inheritAttrs: false,
props: SelectionCheckboxAllProps,
setup(props) {
return {
defaultSelections: [],
checked: computed(() => {
return getCheckState(props);
}),
indeterminate: computed(() => {
return getIndeterminateState(props);
}),
};
},
created() {
const { $props: props } = this;
this.defaultSelections = props.hideDefaultSelections
? []
: [
{
key: 'all',
text: props.locale.selectAll,
},
{
key: 'invert',
text: props.locale.selectInvert,
},
];
},
methods: {
handleSelectAllChange(e) {
const { checked } = e.target;
this.$emit('select', checked ? 'all' : 'removeAll', 0, null);
},
renderMenus(selections) {
return selections.map((selection, index) => {
return (
<Menu.Item key={selection.key || index}>
<div
onClick={() => {
this.$emit('select', selection.key, index, selection.onSelect);
}}
>
{selection.text}
</div>
</Menu.Item>
);
});
},
},
render() {
const { disabled, prefixCls, selections, getPopupContainer, checked, indeterminate } = this;
const selectionPrefixCls = `${prefixCls}-selection`;
let customSelections = null;
if (selections) {
const newSelections = Array.isArray(selections)
? this.defaultSelections.concat(selections)
: this.defaultSelections;
const menu = (
<Menu class={`${selectionPrefixCls}-menu`} selectedKeys={[]}>
{this.renderMenus(newSelections)}
</Menu>
);
customSelections =
newSelections.length > 0 ? (
<Dropdown getPopupContainer={getPopupContainer} overlay={menu}>
<div class={`${selectionPrefixCls}-down`}>
<DownOutlined />
</div>
</Dropdown>
) : null;
}
return (
<div class={selectionPrefixCls}>
<Checkbox
class={classNames({ [`${selectionPrefixCls}-select-all-custom`]: customSelections })}
checked={checked}
indeterminate={indeterminate}
disabled={disabled}
onChange={this.handleSelectAllChange}
/>
{customSelections}
</div>
);
},
});

1842
components/table/Table.tsx Executable file → Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,129 +0,0 @@
import * as Vue from 'vue';
import { mount } from '@vue/test-utils';
import SelectionBox from '../SelectionBox';
const getDefaultStore = selectedRowKeys => {
return Vue.reactive({
selectedRowKeys: selectedRowKeys || [],
selectionDirty: false,
});
};
describe('SelectionBox', () => {
it('unchecked by selectedRowKeys ', () => {
const wrapper = mount(SelectionBox, {
props: {
store: getDefaultStore(),
rowIndex: '1',
disabled: false,
onChange: () => {},
defaultSelection: [],
},
listeners: {
change: () => {},
},
sync: false,
});
expect(wrapper.vm.checked).toEqual(false);
});
it('checked by selectedRowKeys ', () => {
const wrapper = mount(SelectionBox, {
props: {
store: getDefaultStore(['1']),
rowIndex: '1',
disabled: false,
onChange: () => {},
defaultSelection: [],
},
sync: false,
});
expect(wrapper.vm.checked).toEqual(true);
});
it('checked by defaultSelection', () => {
const wrapper = mount(SelectionBox, {
props: {
store: getDefaultStore(),
rowIndex: '1',
disabled: false,
onChange: () => {},
defaultSelection: ['1'],
},
sync: false,
});
expect(wrapper.vm.checked).toEqual(true);
});
it('checked when store change', () => {
const store = getDefaultStore();
const wrapper = mount(SelectionBox, {
props: {
store,
rowIndex: '1',
disabled: false,
onChange: () => {},
defaultSelection: [],
},
sync: false,
});
store.selectedRowKeys = ['1'];
store.selectionDirty = true;
expect(wrapper.vm.checked).toEqual(true);
});
it('passes props to Checkbox', done => {
const checkboxProps = {
name: 'testName',
id: 'testId',
};
const wrapper = mount(SelectionBox, {
props: {
store: getDefaultStore(),
rowIndex: '1',
disabled: false,
onChange: () => {},
defaultSelection: ['1'],
...checkboxProps,
},
sync: false,
});
Vue.nextTick(() => {
wrapper.findAllComponents({ name: 'ACheckbox' }).forEach(box => {
expect(box.props().name).toEqual(checkboxProps.name);
expect(box.props().id).toEqual(checkboxProps.id);
});
done();
});
});
it('passes props to Radios', done => {
const radioProps = {
name: 'testName',
id: 'testId',
};
const wrapper = mount(SelectionBox, {
props: {
store: getDefaultStore(),
rowIndex: '1',
disabled: false,
onChange: () => {},
defaultSelection: ['1'],
type: 'radio',
...radioProps,
},
sync: false,
});
Vue.nextTick(() => {
wrapper.findAllComponents({ name: 'ARadio' }).forEach(radio => {
expect(radio.props().name).toEqual(radioProps.name);
expect(radio.props().id).toEqual(radioProps.id);
});
done();
});
});
});

View File

@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils';
import Table from '..';
import * as Vue from 'vue';
import { asyncExpect } from '@/tests/utils';
import { sleep } from '../../../tests/utils';
describe('Table.pagination', () => {
const columns = [
@ -33,7 +34,7 @@ describe('Table.pagination', () => {
}
function renderedNames(wrapper) {
return wrapper.findAllComponents({ name: 'TableRow' }).map(row => {
return wrapper.findAllComponents({ name: 'BodyRow' }).map(row => {
return row.props().record.name;
});
}
@ -76,7 +77,7 @@ describe('Table.pagination', () => {
});
});
xit('paginate data', done => {
it('paginate data', done => {
const wrapper = mount(Table, getTableOptions());
Vue.nextTick(() => {
expect(renderedNames(wrapper)).toEqual(['Jack', 'Lucy']);
@ -89,7 +90,7 @@ describe('Table.pagination', () => {
});
});
xit('repaginates when pageSize change', () => {
it('repaginates when pageSize change', () => {
const wrapper = mount(Table, getTableOptions());
wrapper.setProps({ pagination: { pageSize: 1 } });
Vue.nextTick(() => {
@ -97,7 +98,7 @@ describe('Table.pagination', () => {
});
});
xit('fires change event', done => {
it('fires change event', done => {
const handleChange = jest.fn();
const handlePaginationChange = jest.fn();
const noop = () => {};
@ -108,8 +109,8 @@ describe('Table.pagination', () => {
...pagination,
onChange: handlePaginationChange,
onShowSizeChange: noop,
onChange: handleChange,
},
onChange: handleChange,
}),
);
Vue.nextTick(() => {
@ -131,6 +132,7 @@ describe('Table.pagination', () => {
{ key: 2, name: 'Tom' },
{ key: 3, name: 'Jerry' },
],
action: 'paginate',
},
);
@ -156,7 +158,7 @@ describe('Table.pagination', () => {
// https://github.com/ant-design/ant-design/issues/4532
// https://codepen.io/afc163/pen/pWVRJV?editors=001
xit('should display pagination as prop pagination change between true and false', async () => {
it('should display pagination as prop pagination change between true and false', async () => {
const wrapper = mount(Table, getTableOptions());
await asyncExpect(() => {
expect(wrapper.findAll('.ant-pagination')).toHaveLength(1);
@ -182,8 +184,8 @@ describe('Table.pagination', () => {
});
await asyncExpect(() => {
expect(wrapper.findAll('.ant-pagination')).toHaveLength(1);
expect(wrapper.findAll('.ant-pagination-item')).toHaveLength(1); // pageSize will be 10
expect(renderedNames(wrapper)).toHaveLength(4);
expect(wrapper.findAll('.ant-pagination-item')).toHaveLength(2); // pageSize will be 10
expect(renderedNames(wrapper)).toEqual(['Tom', 'Jerry']);
});
});
@ -202,30 +204,30 @@ describe('Table.pagination', () => {
});
});
xit('specify the position of pagination', async () => {
const wrapper = mount(Table, getTableOptions({ pagination: { position: 'top' } }));
it('specify the position of pagination', async () => {
const wrapper = mount(Table, getTableOptions({ pagination: { position: ['topLeft'] } }));
await asyncExpect(() => {
expect(wrapper.findAll('.ant-spin-container > *')).toHaveLength(2);
expect(wrapper.findAll('.ant-spin-container > *')[0].findAll('.ant-pagination')).toHaveLength(
1,
);
wrapper.setProps({ pagination: { position: 'bottom' } });
expect(wrapper.findAll('.ant-pagination')).toHaveLength(1);
wrapper.setProps({ pagination: { position: 'bottomRight' } });
}, 0);
await asyncExpect(() => {
expect(wrapper.findAll('.ant-spin-container > *')).toHaveLength(2);
expect(wrapper.findAll('.ant-spin-container > *')[1].findAll('.ant-pagination')).toHaveLength(
1,
);
wrapper.setProps({ pagination: { position: 'both' } });
expect(wrapper.findAll('.ant-pagination')).toHaveLength(1);
wrapper.setProps({ pagination: { position: ['topLeft', 'bottomRight'] } });
}, 0);
await asyncExpect(() => {
expect(wrapper.findAll('.ant-spin-container > *')).toHaveLength(3);
expect(wrapper.findAll('.ant-spin-container > *')[0].findAll('.ant-pagination')).toHaveLength(
1,
);
expect(wrapper.findAll('.ant-spin-container > *')[2].findAll('.ant-pagination')).toHaveLength(
1,
);
expect(wrapper.findAll('.ant-pagination')).toHaveLength(2);
}, 0);
wrapper.setProps({ pagination: { position: ['none', 'none'] } });
await sleep();
expect(wrapper.findAll('.ant-pagination')).toHaveLength(0);
wrapper.setProps({ pagination: { position: ['invalid'] } });
await sleep();
expect(wrapper.findAll('.ant-pagination')).toHaveLength(1);
wrapper.setProps({ pagination: { position: ['invalid', 'invalid'] } });
await sleep();
expect(wrapper.findAll('.ant-pagination')).toHaveLength(1);
});
});

View File

@ -4,6 +4,15 @@ import Table from '..';
import { sleep } from '../../../tests/utils';
describe('Table.rowSelection', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
afterEach(() => {
errorSpy.mockReset();
});
afterAll(() => {
errorSpy.mockRestore();
});
const columns = [
{
title: 'Name',
@ -30,13 +39,13 @@ describe('Table.rowSelection', () => {
};
}
function renderedNames(wrapper) {
return wrapper.findAllComponents({ name: 'TableRow' }).map(row => {
return wrapper.findAllComponents({ name: 'BodyRow' }).map(row => {
return row.props().record.name;
});
}
function getStore(wrapper) {
return wrapper.vm.$refs.table.store;
function getSelections(wrapper) {
return [...wrapper.vm.table.selectedKeySet].sort();
}
it('select by checkbox', async () => {
@ -46,26 +55,17 @@ describe('Table.rowSelection', () => {
checkboxAll.element.checked = true;
checkboxAll.trigger('change');
await asyncExpect(() => {
expect(getStore(wrapper)).toEqual({
selectedRowKeys: [0, 1, 2, 3],
selectionDirty: true,
});
expect(getSelections(wrapper)).toEqual([0, 1, 2, 3]);
});
checkboxes[1].element.checked = false;
checkboxes[1].trigger('change');
await asyncExpect(() => {
expect(getStore(wrapper)).toEqual({
selectedRowKeys: [1, 2, 3],
selectionDirty: true,
});
expect(getSelections(wrapper)).toEqual([1, 2, 3]);
});
checkboxes[1].element.checked = true;
checkboxes[1].trigger('change');
await asyncExpect(() => {
expect(getStore(wrapper)).toEqual({
selectedRowKeys: [1, 2, 3, 0],
selectionDirty: true,
});
expect(getSelections(wrapper)).toEqual([0, 1, 2, 3]);
});
});
@ -77,103 +77,84 @@ describe('Table.rowSelection', () => {
radios[0].element.checked = true;
radios[0].trigger('change');
await asyncExpect(() => {
expect(getStore(wrapper)).toEqual({
selectedRowKeys: [0],
selectionDirty: true,
});
expect(getSelections(wrapper)).toEqual([0]);
});
radios[radios.length - 1].element.checked = true;
radios[radios.length - 1].trigger('change');
await asyncExpect(() => {
expect(getStore(wrapper)).toEqual({
selectedRowKeys: [3],
selectionDirty: true,
});
expect(getSelections(wrapper)).toEqual([3]);
});
});
xit('pass getCheckboxProps to checkbox', async () => {
it('pass getCheckboxProps to checkbox', async () => {
const rowSelection = {
getCheckboxProps: record => ({
props: {
disabled: record.name === 'Lucy',
name: record.name,
},
disabled: record.name === 'Lucy',
name: record.name,
}),
};
const wrapper = mount(Table, getTableOptions({ rowSelection }));
const checkboxes = wrapper.findAll('input');
await sleep();
expect(checkboxes[1].props.disabled).toBe(false);
expect(checkboxes[1].vnode.data.attrs.name).toEqual(data[0].name);
expect(checkboxes[2].vnode.data.attrs.disabled).toBe(true);
expect(checkboxes[2].vnode.data.attrs.name).toEqual(data[1].name);
expect(checkboxes[1].wrapperElement.disabled).toBe(false);
expect(checkboxes[1].wrapperElement.name).toEqual(data[0].name);
expect(checkboxes[2].wrapperElement.disabled).toBe(true);
expect(checkboxes[2].wrapperElement.name).toEqual(data[1].name);
});
xit('works with pagination', async () => {
it('works with pagination', async () => {
const wrapper = mount(Table, getTableOptions({ pagination: { pageSize: 2 } }));
const checkboxAll = wrapper.findAllComponents({ name: 'SelectionCheckboxAll' });
await sleep();
const checkboxAll = wrapper.find('input');
checkboxAll.wrapperElement.checked = true;
checkboxAll.trigger('change');
const pagers = wrapper.findAllComponents({ name: 'Pager' });
checkboxAll.find('input').element.checked = true;
checkboxAll.find('input').trigger('change');
await asyncExpect(() => {
expect(checkboxAll.vm.$data).toEqual({ checked: true, indeterminate: false });
expect(wrapper.findComponent({ name: 'ACheckbox' }).props()).toEqual(
expect.objectContaining({ checked: true, indeterminate: false }),
);
});
pagers[1].trigger('click');
await asyncExpect(() => {
expect(checkboxAll.vm.$data).toEqual({ checked: false, indeterminate: false });
expect(wrapper.findComponent({ name: 'ACheckbox' }).props()).toEqual(
expect.objectContaining({ checked: false, indeterminate: false }),
);
});
pagers[0].trigger('click');
await asyncExpect(() => {
expect(checkboxAll.vm.$data).toEqual({ checked: true, indeterminate: false });
expect(wrapper.findComponent({ name: 'ACheckbox' }).props()).toEqual(
expect.objectContaining({ checked: true, indeterminate: false }),
);
});
});
// https://github.com/ant-design/ant-design/issues/4020
xit('handles defaultChecked', async () => {
it('handles defaultChecked', async () => {
const rowSelection = {
getCheckboxProps: record => {
return {
props: {
defaultChecked: record.key === 0,
},
defaultChecked: record.key === 0,
};
},
};
const wrapper = mount(Table, getTableOptions({ rowSelection }));
mount(Table, getTableOptions({ rowSelection }));
await asyncExpect(() => {
const checkboxs = wrapper.findAll('input');
expect(checkboxs[1].vnode.data.domProps.checked).toBe(true);
expect(checkboxs.at(2).vnode.data.domProps.checked).toBe(false);
checkboxs.at(2).element.checked = true;
checkboxs.at(2).trigger('change');
}, 0);
await asyncExpect(() => {
const checkboxs = wrapper.findAll('input');
expect(checkboxs[1].vnode.data.domProps.checked).toBe(true);
expect(checkboxs.at(2).vnode.data.domProps.checked).toBe(true);
}, 1000);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [ant-design-vue: Table] Do not set `checked` or `defaultChecked` in `getCheckboxProps`. Please use `selectedRowKeys` instead.',
);
});
it('can be controlled', async () => {
const wrapper = mount(Table, getTableOptions({ rowSelection: { selectedRowKeys: [0] } }));
expect(getStore(wrapper)).toEqual({
selectedRowKeys: [0],
selectionDirty: false,
});
expect(getSelections(wrapper)).toEqual([0]);
wrapper.setProps({ rowSelection: { selectedRowKeys: [1] } });
await asyncExpect(() => {
expect(getStore(wrapper)).toEqual({
selectedRowKeys: [1],
selectionDirty: false,
});
expect(getSelections(wrapper)).toEqual([1]);
});
});
@ -216,7 +197,7 @@ describe('Table.rowSelection', () => {
});
});
xit('render with default selection correctly', async () => {
it('render with default selection correctly', async () => {
const rowSelection = {
selections: true,
};
@ -224,7 +205,7 @@ describe('Table.rowSelection', () => {
const dropdownWrapper = mount(
{
render() {
return wrapper.find({ name: 'Trigger' }).vm.getComponent();
return wrapper.findComponent({ name: 'Trigger' }).vm.getComponent();
},
},
{ sync: false },
@ -232,14 +213,12 @@ describe('Table.rowSelection', () => {
await asyncExpect(() => {
expect(dropdownWrapper.html()).toMatchSnapshot();
});
await asyncExpect(() => {});
});
xit('click select all selection', () => {
const handleSelectAll = jest.fn();
it('click select all selection', () => {
const handleChange = jest.fn();
const rowSelection = {
onSelectAll: handleSelectAll,
onChange: handleChange,
selections: true,
};
const wrapper = mount(Table, getTableOptions({ rowSelection }));
@ -247,17 +226,17 @@ describe('Table.rowSelection', () => {
const dropdownWrapper = mount(
{
render() {
return wrapper.find({ name: 'Trigger' }).vm.getComponent();
return wrapper.findComponent({ name: 'Trigger' }).vm.getComponent();
},
},
{ sync: false },
);
dropdownWrapper.findAll('.ant-dropdown-menu-item > div')[0].trigger('click');
dropdownWrapper.findAll('.ant-dropdown-menu-item')[0].trigger('click');
expect(handleSelectAll).toBeCalledWith(true, data, data);
expect(handleChange.mock.calls[0][0]).toEqual([0, 1, 2, 3]);
});
xit('fires selectInvert event', () => {
it('fires selectInvert event', async () => {
const handleSelectInvert = jest.fn();
const rowSelection = {
onSelectInvert: handleSelectInvert,
@ -267,25 +246,28 @@ describe('Table.rowSelection', () => {
const checkboxes = wrapper.findAll('input');
checkboxes[1].element.checked = true;
checkboxes[1].trigger('change');
await sleep();
const dropdownWrapper = mount(
{
render() {
return wrapper.find({ name: 'Trigger' }).vm.getComponent();
return wrapper.findComponent({ name: 'Trigger' }).vm.getComponent();
},
},
{ sync: false },
);
const div = dropdownWrapper.findAll('.ant-dropdown-menu-item > div');
div.at(div.length - 1).trigger('click');
const div = dropdownWrapper.findAll('li.ant-dropdown-menu-item');
div.at(1).trigger('click');
expect(handleSelectInvert).toBeCalledWith([1, 2, 3]);
});
xit('fires selection event', () => {
it('fires selection event', () => {
const handleSelectOdd = jest.fn();
const handleSelectEven = jest.fn();
const rowSelection = {
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
{
key: 'odd',
text: '奇数项',
@ -303,21 +285,21 @@ describe('Table.rowSelection', () => {
const dropdownWrapper = mount(
{
render() {
return wrapper.find({ name: 'Trigger' }).vm.getComponent();
return wrapper.findComponent({ name: 'Trigger' }).vm.getComponent();
},
},
{ sync: false },
);
expect(dropdownWrapper.findAll('.ant-dropdown-menu-item').length).toBe(4);
dropdownWrapper.findAll('.ant-dropdown-menu-item > div').at(2).trigger('click');
dropdownWrapper.findAll('.ant-dropdown-menu-item')[2].trigger('click');
expect(handleSelectOdd).toBeCalledWith([0, 1, 2, 3]);
dropdownWrapper.findAll('.ant-dropdown-menu-item > div').at(3).trigger('click');
dropdownWrapper.findAll('.ant-dropdown-menu-item').at(3).trigger('click');
expect(handleSelectEven).toBeCalledWith([0, 1, 2, 3]);
});
xit('could hide default selection options', () => {
it('could hide default selection options', () => {
const rowSelection = {
hideDefaultSelections: true,
selections: [
@ -335,7 +317,7 @@ describe('Table.rowSelection', () => {
const dropdownWrapper = mount(
{
render() {
return wrapper.find({ name: 'Trigger' }).vm.getComponent();
return wrapper.findComponent({ name: 'Trigger' }).vm.getComponent();
},
},
{ sync: false },
@ -343,7 +325,7 @@ describe('Table.rowSelection', () => {
expect(dropdownWrapper.findAll('.ant-dropdown-menu-item').length).toBe(2);
});
xit('handle custom selection onSelect correctly when hide default selection options', () => {
it('handle custom selection onSelect correctly when hide default selection options', () => {
const handleSelectOdd = jest.fn();
const handleSelectEven = jest.fn();
const rowSelection = {
@ -366,25 +348,25 @@ describe('Table.rowSelection', () => {
const dropdownWrapper = mount(
{
render() {
return wrapper.find({ name: 'Trigger' }).vm.getComponent();
return wrapper.findComponent({ name: 'Trigger' }).vm.getComponent();
},
},
{ sync: false },
);
expect(dropdownWrapper.findAll('.ant-dropdown-menu-item').length).toBe(2);
dropdownWrapper.findAll('.ant-dropdown-menu-item > div')[0].trigger('click');
dropdownWrapper.findAll('.ant-dropdown-menu-item')[0].trigger('click');
expect(handleSelectOdd).toBeCalledWith([0, 1, 2, 3]);
dropdownWrapper.findAll('.ant-dropdown-menu-item > div')[1].trigger('click');
dropdownWrapper.findAll('.ant-dropdown-menu-item')[1].trigger('click');
expect(handleSelectEven).toBeCalledWith([0, 1, 2, 3]);
});
// https:// github.com/ant-design/ant-design/issues/4245
xit('handles disabled checkbox correctly when dataSource changes', async () => {
it('handles disabled checkbox correctly when dataSource changes', async () => {
const rowSelection = {
getCheckboxProps: record => {
return { props: { disabled: record.disabled } };
return { disabled: record.disabled };
},
};
const wrapper = mount(Table, getTableOptions({ rowSelection }));
@ -396,14 +378,14 @@ describe('Table.rowSelection', () => {
wrapper.setProps({ dataSource: newData });
});
await asyncExpect(() => {
wrapper.findAll('input').wrappers.forEach(checkbox => {
expect(checkbox.vnode.data.attrs.disabled).toBe(true);
wrapper.findAll('input').forEach(checkbox => {
expect(checkbox.wrapperElement.disabled).toBe(true);
});
});
});
// https://github.com/ant-design/ant-design/issues/4779
xit('should not switch pagination when select record', async () => {
it('should not switch pagination when select record', async () => {
const newData = [];
for (let i = 0; i < 20; i += 1) {
newData.push({
@ -418,7 +400,7 @@ describe('Table.rowSelection', () => {
dataSource: newData,
}),
);
const pager = wrapper.findAll({ name: 'Pager' });
const pager = wrapper.findAllComponents({ name: 'Pager' });
pager.at(pager.length - 1).trigger('click'); // switch to second page
wrapper.findAll('input')[0].element.checked = true;
wrapper.findAll('input')[0].trigger('change');
@ -460,7 +442,7 @@ describe('Table.rowSelection', () => {
});
// https://github.com/ant-design/ant-design/issues/10629
xit('should keep all checked state when remove item from dataSource', async () => {
it('should keep all checked state when remove item from dataSource', async () => {
const wrapper = mount(Table, {
props: {
columns,
@ -510,7 +492,7 @@ describe('Table.rowSelection', () => {
sync: false,
});
await asyncExpect(() => {
expect(wrapper.findAll('thead tr div')[0].text()).toBe('多选');
expect(wrapper.findAll('thead tr th')[0].text()).toBe('多选');
});
await asyncExpect(() => {
wrapper.setProps({
@ -521,12 +503,12 @@ describe('Table.rowSelection', () => {
});
});
await asyncExpect(() => {
expect(wrapper.findAll('thead tr div')[0].text()).toBe('单选');
expect(wrapper.findAll('thead tr th')[0].text()).toBe('单选');
});
});
// https://github.com/ant-design/ant-design/issues/11384
xit('should keep item even if in filter', async () => {
it('should keep item even if in filter', async () => {
const filterColumns = [
{
title: 'Name',
@ -563,11 +545,21 @@ describe('Table.rowSelection', () => {
const dropdownWrapper = mount(
{
render() {
return wrapper.find({ name: 'Trigger' }).vm.getComponent();
return wrapper.findComponent({ name: 'Trigger' }).vm.getComponent();
},
},
{ sync: false },
);
await sleep();
function clickFilter(indexList) {
indexList.forEach(index => {
dropdownWrapper
.findAll('.ant-dropdown-menu-item .ant-checkbox-wrapper')
.at(index)
.trigger('click');
});
dropdownWrapper.find('.ant-table-filter-dropdown-btns .ant-btn-primary').trigger('click');
}
function clickItem() {
wrapper.findAll(
@ -577,10 +569,7 @@ describe('Table.rowSelection', () => {
}
// Check Jack
dropdownWrapper.findAll('.ant-dropdown-menu-item .ant-checkbox-wrapper')[0].trigger('click');
dropdownWrapper
.find('.ant-table-filter-dropdown-btns .ant-table-filter-dropdown-link.confirm')
.trigger('click');
clickFilter([0]);
await asyncExpect(() => {
expect(wrapper.findAll('tbody tr').length).toBe(1);
});
@ -592,19 +581,9 @@ describe('Table.rowSelection', () => {
expect(onChange.mock.calls[0][1].length).toBe(1);
});
await asyncExpect(() => {
dropdownWrapper.findAll('.ant-dropdown-menu-item .ant-checkbox-wrapper')[0].trigger('click');
});
// Check Lucy
clickFilter([0, 1]);
await asyncExpect(() => {
// Check Lucy
dropdownWrapper.findAll('.ant-dropdown-menu-item .ant-checkbox-wrapper')[1].trigger('click');
});
await asyncExpect(() => {
dropdownWrapper
.find('.ant-table-filter-dropdown-btns .ant-table-filter-dropdown-link.confirm')
.trigger('click');
});
await asyncExpect(() => {
expect(wrapper.findAll('tbody tr').length).toBe(1);
});
@ -617,7 +596,7 @@ describe('Table.rowSelection', () => {
});
});
xit('render correctly when set childrenColumnName', async () => {
it('render correctly when set childrenColumnName', async () => {
const newDatas = [
{
key: 1,
@ -652,16 +631,18 @@ describe('Table.rowSelection', () => {
});
const checkboxes = wrapper.findAll('input');
const checkboxAll = wrapper.findAllComponents({ name: 'SelectionCheckboxAll' });
checkboxes[1].element.checked = true;
checkboxes[1].trigger('change');
expect(checkboxAll.$data).toEqual({ indeterminate: true, checked: false });
await sleep();
expect(wrapper.findComponent({ name: 'ACheckbox' }).props()).toEqual(
expect.objectContaining({ checked: false, indeterminate: true }),
);
checkboxes[2].element.checked = true;
checkboxes[2].trigger('change');
await asyncExpect(() => {
expect(checkboxAll.vm.$data).toEqual({ indeterminate: false, checked: true });
expect(wrapper.findComponent({ name: 'ACheckbox' }).props()).toEqual(
expect.objectContaining({ checked: true, indeterminate: false }),
);
});
});
});

View File

@ -9,6 +9,7 @@ describe('Table.sorter', () => {
const column = {
title: 'Name',
dataIndex: 'name',
key: 'name',
sorter: sorterFn,
};
@ -38,7 +39,7 @@ describe('Table.sorter', () => {
}
function renderedNames(wrapper) {
return wrapper.findAllComponents({ name: 'TableRow' }).wrappers.map(row => {
return wrapper.findAllComponents({ name: 'BodyRow' }).map(row => {
return row.props().record.name;
});
}
@ -51,7 +52,7 @@ describe('Table.sorter', () => {
});
});
xit('default sort order ascend', done => {
it('default sort order ascend', done => {
const wrapper = mount(
Table,
getTableOptions(
@ -67,7 +68,7 @@ describe('Table.sorter', () => {
});
});
xit('default sort order descend', done => {
it('default sort order descend', done => {
const wrapper = mount(
Table,
getTableOptions(
@ -104,7 +105,7 @@ describe('Table.sorter', () => {
});
});
xit('can be controlled by sortOrder', done => {
it('can be controlled by sortOrder', done => {
const wrapper = mount(
Table,
getTableOptions({
@ -148,7 +149,7 @@ describe('Table.sorter', () => {
});
});
xit('works with grouping columns in controlled mode', done => {
it('works with grouping columns in controlled mode', done => {
const columns = [
{
title: 'group',

View File

@ -1,4 +1,4 @@
import { shallowMount as shallow, mount } from '@vue/test-utils';
import { mount } from '@vue/test-utils';
import Table from '..';
import * as Vue from 'vue';
import mountTest from '../../../tests/shared/mountTest';
@ -55,7 +55,7 @@ describe('Table', () => {
dataIndex: 'name',
},
];
const wrapper = shallow(Table, {
const wrapper = mount(Table, {
props: {
columns,
},
@ -70,7 +70,7 @@ describe('Table', () => {
];
wrapper.setProps({ columns: newColumns });
await sleep();
expect(wrapper.vm.columns).toStrictEqual(newColumns);
expect(wrapper.find('th').text()).toEqual('Title');
});
it('loading with Spin', async () => {

View File

@ -5,52 +5,49 @@ exports[`Table.pagination renders pagination correctly 1`] = `
<div class="ant-spin-nested-loading">
<!---->
<div class="ant-spin-container">
<div class="ant-table ant-table-default ant-table-scroll-position-left">
<div class="ant-table">
<!---->
<div class="ant-table-content">
<!---->
<div class="ant-table-body">
<table class="">
<colgroup>
<col data-key="name">
</colgroup>
<div class="ant-table-container">
<div class="ant-table-content">
<table style="table-layout: auto;">
<colgroup></colgroup>
<thead class="ant-table-thead">
<tr>
<th colstart="0" colspan="1" colend="0" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Name</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
</tr>
</thead>
<tbody class="ant-table-tbody">
<tr class="ant-table-row ant-table-row-level-0" data-row-key="0">
<td class="">
<th class="ant-table-cell" colstart="0" colend="0">
<!---->Name
</th>
</tr>
</thead>
<tbody class="ant-table-tbody">
<!---->
<!---->Jack
</td>
</tr>
<tr class="ant-table-row ant-table-row-level-0" data-row-key="1">
<td class="">
<tr data-row-key="0" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-with-append">
<!---->Jack
</td>
</tr>
<!---->
<!---->Lucy
</td>
</tr>
</tbody>
</table>
<tr data-row-key="1" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-with-append">
<!---->Lucy
</td>
</tr>
<!---->
</tbody>
<!---->
</table>
</div>
</div>
<!---->
<!---->
</div>
<ul unselectable="on" class="ant-pagination my-page">
<!---->
<li title="Previous Page" class="ant-pagination-prev ant-pagination-disabled" aria-disabled="true"><button class="ant-pagination-item-link" type="button" tabindex="-1" disabled=""><span role="img" aria-label="left" class="anticon anticon-left"><svg focusable="false" class="" data-icon="left" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"></path></svg></span></button></li>
<li title="1" tabindex="0" class="ant-pagination-item ant-pagination-item-1 ant-pagination-item-active"><a rel="nofollow">1</a></li>
<li title="2" tabindex="0" class="ant-pagination-item ant-pagination-item-2"><a rel="nofollow">2</a></li>
<li title="Next Page" tabindex="0" class="ant-pagination-next" aria-disabled="false"><button class="ant-pagination-item-link" type="button" tabindex="-1"><span role="img" aria-label="right" class="anticon anticon-right"><svg focusable="false" class="" data-icon="right" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"></path></svg></span></button></li>
<!---->
</ul>
</div>
<ul unselectable="on" class="ant-pagination my-page ant-table-pagination">
<!---->
<li title="Previous Page" class="ant-pagination-prev ant-pagination-disabled" aria-disabled="true"><button class="ant-pagination-item-link" type="button" tabindex="-1" disabled=""><span role="img" aria-label="left" class="anticon anticon-left"><svg focusable="false" class="" data-icon="left" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"></path></svg></span></button></li>
<li title="1" tabindex="0" class="ant-pagination-item ant-pagination-item-1 ant-pagination-item-active"><a rel="nofollow">1</a></li>
<li title="2" tabindex="0" class="ant-pagination-item ant-pagination-item-2"><a rel="nofollow">2</a></li>
<li title="Next Page" tabindex="0" class="ant-pagination-next" aria-disabled="false"><button class="ant-pagination-item-link" type="button" tabindex="-1"><span role="img" aria-label="right" class="anticon anticon-right"><svg focusable="false" class="" data-icon="right" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"></path></svg></span></button></li>
<!---->
</ul>
</div>
</div>
</div>
`;

View File

@ -5,108 +5,117 @@ exports[`Table.rowSelection fix selection column on the left 1`] = `
<div class="ant-spin-nested-loading">
<!---->
<div class="ant-spin-container">
<div class="ant-table ant-table-default ant-table-scroll-position-left ant-table-scroll-position-right">
<div class="ant-table ant-table-has-fix-left">
<!---->
<div class="ant-table-content">
<div class="ant-table-scroll">
<!---->
<div class="ant-table-body">
<table class="">
<colgroup>
<col data-key="selection-column" class="ant-table-selection-col">
<col data-key="name">
</colgroup>
<thead class="ant-table-thead">
<tr>
<th colstart="0" colspan="1" colend="0" rowspan="1" class="ant-table-selection-column"><span class="ant-table-header-column"><div><span class="ant-table-column-title"><div class="ant-table-selection"><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label>
<div class="ant-table-container">
<div class="ant-table-content">
<table style="table-layout: auto;">
<colgroup>
<col class="ant-table-selection-col">
</colgroup>
<thead class="ant-table-thead">
<tr>
<th class="ant-table-cell ant-table-cell-fix-left ant-table-cell-fix-left-last ant-table-selection-column" style="position: sticky; left: 0px;" colstart="0" colend="0">
<!---->
<div class="ant-table-selection"><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
<!---->
</div></span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="1" colspan="1" colend="1" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Name</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
</div>
</th>
<th class="ant-table-cell" colstart="1" colend="1">
<!---->Name
</th>
</tr>
</thead>
<tbody class="ant-table-tbody">
<!---->
<tr data-row-key="0" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-fix-left ant-table-cell-fix-left-last ant-table-cell-with-append ant-table-selection-column" style="position: sticky; left: 0px;">
<!----><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->Jack
</td>
</tr>
<!---->
<tr data-row-key="1" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-fix-left ant-table-cell-fix-left-last ant-table-cell-with-append ant-table-selection-column" style="position: sticky; left: 0px;">
<!----><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->Lucy
</td>
</tr>
<!---->
<tr data-row-key="2" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-fix-left ant-table-cell-fix-left-last ant-table-cell-with-append ant-table-selection-column" style="position: sticky; left: 0px;">
<!----><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->Tom
</td>
</tr>
<!---->
<tr data-row-key="3" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-fix-left ant-table-cell-fix-left-last ant-table-cell-with-append ant-table-selection-column" style="position: sticky; left: 0px;">
<!----><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->Jerry
</td>
</tr>
<!---->
</tbody>
<!---->
</table>
</div>
</div>
<!---->
</th>
</tr>
</thead>
<tbody class="ant-table-tbody">
<tr class="ant-table-row ant-table-row-level-0" data-row-key="0">
<td class="ant-table-selection-column">
<!---->
<!----><span><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label></span>
</td>
<td class="">
<!---->
<!---->Jack
</td>
</tr>
<tr class="ant-table-row ant-table-row-level-0" data-row-key="1">
<td class="ant-table-selection-column">
<!---->
<!----><span><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label></span>
</td>
<td class="">
<!---->
<!---->Lucy
</td>
</tr>
<tr class="ant-table-row ant-table-row-level-0" data-row-key="2">
<td class="ant-table-selection-column">
<!---->
<!----><span><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label></span>
</td>
<td class="">
<!---->
<!---->Tom
</td>
</tr>
<tr class="ant-table-row ant-table-row-level-0" data-row-key="3">
<td class="ant-table-selection-column">
<!---->
<!----><span><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label></span>
</td>
<td class="">
<!---->
<!---->Jerry
</td>
</tr>
</tbody>
</table>
</div>
<!---->
<!---->
<ul unselectable="on" class="ant-pagination ant-table-pagination ant-table-pagination-right">
<!---->
<li title="Previous Page" class="ant-pagination-prev ant-pagination-disabled" aria-disabled="true"><button class="ant-pagination-item-link" type="button" tabindex="-1" disabled=""><span role="img" aria-label="left" class="anticon anticon-left"><svg focusable="false" class="" data-icon="left" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"></path></svg></span></button></li>
<li title="1" tabindex="0" class="ant-pagination-item ant-pagination-item-1 ant-pagination-item-active"><a rel="nofollow">1</a></li>
<li title="Next Page" class="ant-pagination-next ant-pagination-disabled" aria-disabled="true"><button class="ant-pagination-item-link" type="button" tabindex="-1" disabled=""><span role="img" aria-label="right" class="anticon anticon-right"><svg focusable="false" class="" data-icon="right" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"></path></svg></span></button></li>
<!---->
</ul>
</div>
</div>
</div>
<ul unselectable="on" class="ant-pagination ant-table-pagination">
<!---->
<li title="Previous Page" class="ant-pagination-prev ant-pagination-disabled" aria-disabled="true"><button class="ant-pagination-item-link" type="button" tabindex="-1" disabled=""><span role="img" aria-label="left" class="anticon anticon-left"><svg focusable="false" class="" data-icon="left" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"></path></svg></span></button></li>
<li title="1" tabindex="0" class="ant-pagination-item ant-pagination-item-1 ant-pagination-item-active"><a rel="nofollow">1</a></li>
<li title="Next Page" class="ant-pagination-next ant-pagination-disabled" aria-disabled="true"><button class="ant-pagination-item-link" type="button" tabindex="-1" disabled=""><span role="img" aria-label="right" class="anticon anticon-right"><svg focusable="false" class="" data-icon="right" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"></path></svg></span></button></li>
<!---->
</ul>
</div>
</div>
</div>
`;
exports[`Table.rowSelection render with default selection correctly 1`] = `
<div>
<div class="ant-dropdown ant-dropdown-placement-bottomLeft" style="display: none;">
<ul role="menu" tabindex="0" class="ant-dropdown-menu ant-dropdown-menu-vertical ant-dropdown-menu-root ant-dropdown-menu-light ant-table-selection-menu ant-dropdown-content">
<li role="menuitem" class="ant-dropdown-menu-item">
<div>Select current page</div>
</li>
<li role="menuitem" class="ant-dropdown-menu-item">
<div>Invert current page</div>
</li>
</ul>
<!---->
<div class="ant-dropdown" style="pointer-events: none; display: none;">
<div class="ant-dropdown-content">
<!---->
<ul class="ant-dropdown-menu ant-dropdown-menu-root ant-dropdown-menu-vertical ant-dropdown-menu-light" role="menu" data-menu-list="true">
<!---->
<li class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="menuitem" tabindex="-1" data-menu-id="all" aria-disabled="false">
<!----><span class="ant-dropdown-menu-title-content">Select all data</span>
</li>
<!---->
<li class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="menuitem" tabindex="-1" data-menu-id="invert" aria-disabled="false">
<!----><span class="ant-dropdown-menu-title-content">Invert current page</span>
</li>
<!---->
<li class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" role="menuitem" tabindex="-1" data-menu-id="none" aria-disabled="false">
<!----><span class="ant-dropdown-menu-title-content">Clear all data</span>
</li>
<!---->
<!---->
</ul>
</div>
</div>
</div>
`;

View File

@ -3,8 +3,9 @@
exports[`Table.sorter renders sorter icon correctly 1`] = `
<thead class="ant-table-thead">
<tr>
<th colstart="0" colspan="1" colend="0" rowspan="1" class="ant-table-column-has-actions ant-table-column-has-sorters"><span class="ant-table-header-column"><div class="ant-table-column-sorters"><span class="ant-table-column-title">Name</span><span class="ant-table-column-sorter"><div title="Sort" class="ant-table-column-sorter-inner ant-table-column-sorter-inner-full"><span role="img" aria-label="caret-up" class="anticon anticon-caret-up ant-table-column-sorter-up off"><svg focusable="false" class="" data-icon="caret-up" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="0 0 1024 1024"><path d="M858.9 689L530.5 308.2c-9.4-10.9-27.5-10.9-37 0L165.1 689c-12.2 14.2-1.2 35 18.5 35h656.8c19.7 0 30.7-20.8 18.5-35z"></path></svg></span><span role="img" aria-label="caret-down" class="anticon anticon-caret-down ant-table-column-sorter-down off"><svg focusable="false" class="" data-icon="caret-down" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="0 0 1024 1024"><path d="M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z"></path></svg></span></div></span></div></span>
<th class="ant-table-cell ant-table-column-has-sorters" colstart="0" colend="0">
<!---->
<div class="ant-table-column-sorters"><span class="ant-table-column-title">Name</span><span class="ant-table-column-sorter ant-table-column-sorter-full"><span class="ant-table-column-sorter-inner"><span role="img" aria-label="caret-up" class="anticon anticon-caret-up ant-table-column-sorter-up"><svg focusable="false" class="" data-icon="caret-up" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="0 0 1024 1024"><path d="M858.9 689L530.5 308.2c-9.4-10.9-27.5-10.9-37 0L165.1 689c-12.2 14.2-1.2 35 18.5 35h656.8c19.7 0 30.7-20.8 18.5-35z"></path></svg></span><span role="img" aria-label="caret-down" class="anticon anticon-caret-down ant-table-column-sorter-down"><svg focusable="false" class="" data-icon="caret-down" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="0 0 1024 1024"><path d="M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z"></path></svg></span></span></span></div>
</th>
</tr>
</thead>

View File

@ -5,67 +5,60 @@ exports[`Table align column should not override cell style 1`] = `
<div class="ant-spin-nested-loading">
<!---->
<div class="ant-spin-container">
<div class="ant-table ant-table-default ant-table-scroll-position-left">
<div class="ant-table">
<!---->
<div class="ant-table-content">
<!---->
<div class="ant-table-body">
<table class="">
<colgroup>
<col data-key="name">
<col data-key="age">
</colgroup>
<div class="ant-table-container">
<div class="ant-table-content">
<table style="table-layout: auto;">
<colgroup></colgroup>
<thead class="ant-table-thead">
<tr>
<th colstart="0" colspan="1" colend="0" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Name</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="1" colspan="1" colend="1" rowspan="1" style="text-align: center;" class="ant-table-align-center"><span class="ant-table-header-column"><div><span class="ant-table-column-title">Age</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<th class="ant-table-cell" colstart="0" colend="0">
<!---->Name
</th>
<th class="ant-table-cell" style="text-align: center;" colstart="1" colend="1">
<!---->Age
</th>
</tr>
</thead>
<tbody class="ant-table-tbody">
<!---->
<tr data-row-key="1" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-with-append">
<!---->
<!---->
</td>
<td style="color: red; text-align: center;" class="ant-table-cell ant-table-cell-with-append">
<!---->32
</td>
</tr>
<!---->
<tr data-row-key="2" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-with-append">
<!---->
<!---->
</td>
<td style="color: red; text-align: center;" class="ant-table-cell ant-table-cell-with-append">
<!---->42
</td>
</tr>
<!---->
</tbody>
<!---->
</table>
</div>
</div>
<!---->
</th>
</tr>
</thead>
<tbody class="ant-table-tbody">
<tr class="ant-table-row ant-table-row-level-0" data-row-key="1">
<td class="">
<!---->
<!---->
<!---->
</td>
<td class="" style="text-align: center; color: red;">
<!---->
<!---->32
</td>
</tr>
<tr class="ant-table-row ant-table-row-level-0" data-row-key="2">
<td class="">
<!---->
<!---->
<!---->
</td>
<td class="" style="text-align: center; color: red;">
<!---->
<!---->42
</td>
</tr>
</tbody>
</table>
</div>
<!---->
<!---->
<ul unselectable="on" class="ant-pagination ant-table-pagination ant-table-pagination-right">
<!---->
<li title="Previous Page" class="ant-pagination-prev ant-pagination-disabled" aria-disabled="true"><button class="ant-pagination-item-link" type="button" tabindex="-1" disabled=""><span role="img" aria-label="left" class="anticon anticon-left"><svg focusable="false" class="" data-icon="left" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"></path></svg></span></button></li>
<li title="1" tabindex="0" class="ant-pagination-item ant-pagination-item-1 ant-pagination-item-active"><a rel="nofollow">1</a></li>
<li title="Next Page" class="ant-pagination-next ant-pagination-disabled" aria-disabled="true"><button class="ant-pagination-item-link" type="button" tabindex="-1" disabled=""><span role="img" aria-label="right" class="anticon anticon-right"><svg focusable="false" class="" data-icon="right" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"></path></svg></span></button></li>
<!---->
</ul>
</div>
</div>
<ul unselectable="on" class="ant-pagination ant-table-pagination">
<!---->
<li title="Previous Page" class="ant-pagination-prev ant-pagination-disabled" aria-disabled="true"><button class="ant-pagination-item-link" type="button" tabindex="-1" disabled=""><span role="img" aria-label="left" class="anticon anticon-left"><svg focusable="false" class="" data-icon="left" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"></path></svg></span></button></li>
<li title="1" tabindex="0" class="ant-pagination-item ant-pagination-item-1 ant-pagination-item-active"><a rel="nofollow">1</a></li>
<li title="Next Page" class="ant-pagination-next ant-pagination-disabled" aria-disabled="true"><button class="ant-pagination-item-link" type="button" tabindex="-1" disabled=""><span role="img" aria-label="right" class="anticon anticon-right"><svg focusable="false" class="" data-icon="right" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"></path></svg></span></button></li>
<!---->
</ul>
</div>
</div>
</div>
`;
@ -74,76 +67,64 @@ exports[`Table renders JSX correctly 1`] = `
<div class="ant-spin-nested-loading">
<!---->
<div class="ant-spin-container">
<div class="ant-table ant-table-default ant-table-scroll-position-left">
<div class="ant-table">
<!---->
<div class="ant-table-content">
<!---->
<div class="ant-table-body">
<table class="">
<colgroup>
<col data-key="firstName">
<col data-key="lastName">
<col data-key="age">
</colgroup>
<div class="ant-table-container">
<div class="ant-table-content">
<table style="table-layout: auto;">
<colgroup></colgroup>
<thead class="ant-table-thead">
<tr>
<th colstart="0" hassubcolumns="true" colspan="2" colend="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Name</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="2" rowspan="2" colspan="1" colend="2" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Age</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<th colspan="2" class="ant-table-cell" colstart="0" hassubcolumns="true" colend="1">
<!---->Name
</th>
<th rowspan="2" class="ant-table-cell" colstart="2" colend="2">
<!---->Age
</th>
</tr>
<tr>
<th class="ant-table-cell" colstart="0" colend="0">
<!---->First Name
</th>
<th class="ant-table-cell" colstart="1" colend="1">
<!---->Last Name
</th>
</tr>
</thead>
<tbody class="ant-table-tbody">
<!---->
<tr data-row-key="1" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-with-append">
<!---->John
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->Brown
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->32
</td>
</tr>
<!---->
<tr data-row-key="2" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-with-append">
<!---->Jim
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->Green
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->42
</td>
</tr>
<!---->
</tbody>
<!---->
</table>
</div>
</div>
<!---->
</th>
</tr>
<tr>
<th colstart="0" colspan="1" colend="0" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">First Name</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="1" colspan="1" colend="1" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Last Name</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
</tr>
</thead>
<tbody class="ant-table-tbody">
<tr class="ant-table-row ant-table-row-level-0" data-row-key="1">
<td class="">
<!---->
<!---->John
</td>
<td class="">
<!---->
<!---->Brown
</td>
<td class="">
<!---->
<!---->32
</td>
</tr>
<tr class="ant-table-row ant-table-row-level-0" data-row-key="2">
<td class="">
<!---->
<!---->Jim
</td>
<td class="">
<!---->
<!---->Green
</td>
<td class="">
<!---->
<!---->42
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!---->
<!---->
</div>
</div>
</div>
</div>
</div>
`;

File diff suppressed because it is too large Load Diff

View File

@ -5,80 +5,57 @@ exports[`Table renders empty table 1`] = `
<div class="ant-spin-nested-loading">
<!---->
<div class="ant-spin-container">
<div class="ant-table ant-table-default ant-table-empty ant-table-scroll-position-left">
<div class="ant-table ant-table-empty">
<!---->
<div class="ant-table-content">
<!---->
<div class="ant-table-body">
<table class="">
<colgroup>
<col data-key="1">
<col data-key="2">
<col data-key="3">
<col data-key="4">
<col data-key="5">
<col data-key="6">
<col data-key="7">
<col data-key="8">
</colgroup>
<div class="ant-table-container">
<div class="ant-table-content">
<table style="table-layout: auto;">
<colgroup></colgroup>
<thead class="ant-table-thead">
<tr>
<th colstart="0" colspan="1" colend="0" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 1</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="1" colspan="1" colend="1" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 2</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<th class="ant-table-cell" colstart="0" colend="0">
<!---->Column 1
</th>
<th class="ant-table-cell" colstart="1" colend="1">
<!---->Column 2
</th>
<th class="ant-table-cell" colstart="2" colend="2">
<!---->Column 3
</th>
<th class="ant-table-cell" colstart="3" colend="3">
<!---->Column 4
</th>
<th class="ant-table-cell" colstart="4" colend="4">
<!---->Column 5
</th>
<th class="ant-table-cell" colstart="5" colend="5">
<!---->Column 6
</th>
<th class="ant-table-cell" colstart="6" colend="6">
<!---->Column 7
</th>
<th class="ant-table-cell" colstart="7" colend="7">
<!---->Column 8
</th>
</tr>
</thead>
<tbody class="ant-table-tbody">
<!---->
<tr class="ant-table-placeholder">
<td colspan="8" class="ant-table-cell">
<!---->No data
</td>
</tr>
</tbody>
<!---->
</table>
</div>
</div>
<!---->
</th>
<th colstart="2" colspan="1" colend="2" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 3</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="3" colspan="1" colend="3" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 4</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="4" colspan="1" colend="4" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 5</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="5" colspan="1" colend="5" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 6</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="6" colspan="1" colend="6" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 7</span><span class="ant-table-column-sorter"><!----></span></div></span>
<!---->
</th>
<th colstart="7" colspan="1" colend="7" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 8</span><span class="ant-table-column-sorter"><!----></span></div></span>
<!---->
</th>
</tr>
</thead>
<tbody class="ant-table-tbody"></tbody>
</table>
</div>
<div class="ant-table-placeholder">
<div class="ant-empty ant-empty-normal">
<div class="ant-empty-image"><svg class="ant-empty-img-simple" width="64" height="41" viewBox="0 0 64 41">
<g transform="translate(0 1)" fill="none" fill-rule="evenodd">
<ellipse class="ant-empty-img-simple-ellipse" fill="#F5F5F5" cx="32" cy="33" rx="32" ry="7"></ellipse>
<g class="ant-empty-img-simple-g" fill-rule="nonzero" stroke="#D9D9D9">
<path d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"></path>
<path d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z" fill="#FAFAFA" class="ant-empty-img-simple-path"></path>
</g>
</g>
</svg></div>
<p class="ant-empty-description">No Data</p>
<!---->
</div>
</div>
</div>
</div>
<!---->
</div>
</div>
</div>
</div>
</div>
`;
exports[`Table renders empty table with custom emptyText 1`] = `
@ -86,65 +63,56 @@ exports[`Table renders empty table with custom emptyText 1`] = `
<div class="ant-spin-nested-loading">
<!---->
<div class="ant-spin-container">
<div class="ant-table ant-table-default ant-table-empty ant-table-scroll-position-left">
<div class="ant-table ant-table-empty">
<!---->
<div class="ant-table-content">
<!---->
<div class="ant-table-body">
<table class="">
<colgroup>
<col data-key="1">
<col data-key="2">
<col data-key="3">
<col data-key="4">
<col data-key="5">
<col data-key="6">
<col data-key="7">
<col data-key="8">
</colgroup>
<div class="ant-table-container">
<div class="ant-table-content">
<table style="table-layout: auto;">
<colgroup></colgroup>
<thead class="ant-table-thead">
<tr>
<th colstart="0" colspan="1" colend="0" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 1</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="1" colspan="1" colend="1" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 2</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<th class="ant-table-cell" colstart="0" colend="0">
<!---->Column 1
</th>
<th class="ant-table-cell" colstart="1" colend="1">
<!---->Column 2
</th>
<th class="ant-table-cell" colstart="2" colend="2">
<!---->Column 3
</th>
<th class="ant-table-cell" colstart="3" colend="3">
<!---->Column 4
</th>
<th class="ant-table-cell" colstart="4" colend="4">
<!---->Column 5
</th>
<th class="ant-table-cell" colstart="5" colend="5">
<!---->Column 6
</th>
<th class="ant-table-cell" colstart="6" colend="6">
<!---->Column 7
</th>
<th class="ant-table-cell" colstart="7" colend="7">
<!---->Column 8
</th>
</tr>
</thead>
<tbody class="ant-table-tbody">
<!---->
<tr class="ant-table-placeholder">
<td colspan="8" class="ant-table-cell">
<!---->custom empty text
</td>
</tr>
</tbody>
<!---->
</table>
</div>
</div>
<!---->
</th>
<th colstart="2" colspan="1" colend="2" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 3</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="3" colspan="1" colend="3" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 4</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="4" colspan="1" colend="4" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 5</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="5" colspan="1" colend="5" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 6</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="6" colspan="1" colend="6" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 7</span><span class="ant-table-column-sorter"><!----></span></div></span>
<!---->
</th>
<th colstart="7" colspan="1" colend="7" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 8</span><span class="ant-table-column-sorter"><!----></span></div></span>
<!---->
</th>
</tr>
</thead>
<tbody class="ant-table-tbody"></tbody>
</table>
</div>
<div class="ant-table-placeholder">custom empty text </div>
<!---->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
@ -153,178 +121,138 @@ exports[`Table renders empty table with fixed columns 1`] = `
<div class="ant-spin-nested-loading">
<!---->
<div class="ant-spin-container">
<div class="ant-table ant-table-default ant-table-empty ant-table-scroll-position-left ant-table-scroll-position-right">
<div class="ant-table ant-table-has-fix-left ant-table-has-fix-right ant-table-empty">
<!---->
<div class="ant-table-content">
<div class="ant-table-scroll">
<!---->
<div class="ant-table-body">
<table class="">
<colgroup>
<col data-key="name" style="width: 100px; min-width: 100px;">
<col data-key="age" style="width: 100px; min-width: 100px;">
<col data-key="1">
<col data-key="2">
<col data-key="3">
<col data-key="4">
<col data-key="5">
<col data-key="6">
<col data-key="7">
<col data-key="8">
<col data-key="address" style="width: 100px; min-width: 100px;">
</colgroup>
<thead class="ant-table-thead">
<tr>
<th colstart="0" colspan="1" colend="0" rowspan="1" class="ant-table-row-cell-break-word"><span class="ant-table-header-column"><div><span class="ant-table-column-title">Full Name</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="1" colspan="1" colend="1" rowspan="1" class="ant-table-row-cell-break-word"><span class="ant-table-header-column"><div><span class="ant-table-column-title">Age</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="2" colspan="1" colend="2" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 1</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<div class="ant-table-container">
<div class="ant-table-content">
<table style="table-layout: auto;">
<colgroup>
<col style="width: 100px;">
<col style="width: 100px;">
<col>
<col>
<col>
<col>
<col>
<col>
<col>
<col>
<col style="width: 100px;">
</colgroup>
<thead class="ant-table-thead">
<tr>
<th class="ant-table-cell ant-table-cell-fix-left" style="position: sticky; left: 0px;" colstart="0" colend="0">
<!---->Full Name
</th>
<th class="ant-table-cell ant-table-cell-fix-left ant-table-cell-fix-left-last" style="position: sticky; left: 0px;" colstart="1" colend="1">
<!---->Age
</th>
<th class="ant-table-cell" colstart="2" colend="2">
<!---->Column 1
</th>
<th class="ant-table-cell" colstart="3" colend="3">
<!---->Column 2
</th>
<th class="ant-table-cell" colstart="4" colend="4">
<!---->Column 3
</th>
<th class="ant-table-cell" colstart="5" colend="5">
<!---->Column 4
</th>
<th class="ant-table-cell" colstart="6" colend="6">
<!---->Column 5
</th>
<th class="ant-table-cell" colstart="7" colend="7">
<!---->Column 6
</th>
<th class="ant-table-cell" colstart="8" colend="8">
<!---->Column 7
</th>
<th class="ant-table-cell" colstart="9" colend="9">
<!---->Column 8
</th>
<th class="ant-table-cell ant-table-cell-fix-right ant-table-cell-fix-right-first" style="position: sticky; right: 0px;" colstart="10" colend="10">
<!---->Action
</th>
</tr>
</thead>
<tbody class="ant-table-tbody">
<!---->
<tr class="ant-table-placeholder">
<td colspan="11" class="ant-table-cell">
<!---->No data
</td>
</tr>
</tbody>
<!---->
</table>
</div>
</div>
<!---->
</th>
<th colstart="3" colspan="1" colend="3" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 2</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="4" colspan="1" colend="4" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 3</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="5" colspan="1" colend="5" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 4</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="6" colspan="1" colend="6" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 5</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="7" colspan="1" colend="7" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 6</span><span class="ant-table-column-sorter"><!----></span></div></span>
<!---->
</th>
<th colstart="8" colspan="1" colend="8" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 7</span><span class="ant-table-column-sorter"><!----></span></div></span>
<!---->
</th>
<th colstart="9" colspan="1" colend="9" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 8</span><span class="ant-table-column-sorter"><!----></span></div></span>
<!---->
</th>
<th colstart="10" colspan="1" colend="10" rowspan="1" class="ant-table-row-cell-break-word"><span class="ant-table-header-column"><div><span class="ant-table-column-title">Action</span><span class="ant-table-column-sorter"><!----></span></div></span>
<!---->
</th>
</tr>
</thead>
<tbody class="ant-table-tbody"></tbody>
</table>
</div>
<div class="ant-table-placeholder">
<div class="ant-empty ant-empty-normal">
<div class="ant-empty-image"><svg class="ant-empty-img-simple" width="64" height="41" viewBox="0 0 64 41">
<g transform="translate(0 1)" fill="none" fill-rule="evenodd">
<ellipse class="ant-empty-img-simple-ellipse" fill="#F5F5F5" cx="32" cy="33" rx="32" ry="7"></ellipse>
<g class="ant-empty-img-simple-g" fill-rule="nonzero" stroke="#D9D9D9">
<path d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"></path>
<path d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z" fill="#FAFAFA" class="ant-empty-img-simple-path"></path>
</g>
</g>
</svg></div>
<p class="ant-empty-description">No Data</p>
<!---->
</div>
</div>
</div>
</div>
<!---->
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Table renders empty table without emptyText when loading 1`] = `
<div class="ant-table-wrapper">
<div class="ant-spin-nested-loading">
<div>
<div class="ant-spin ant-spin-spinning ant-table-without-pagination ant-table-spin-holder"><span class="ant-spin-dot ant-spin-dot-spin"><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i></span>
<div class="ant-spin ant-spin-spinning"><span class="ant-spin-dot ant-spin-dot-spin"><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i></span>
<!---->
</div>
</div>
<div class="ant-spin-container ant-spin-blur">
<div class="ant-table ant-table-default ant-table-empty ant-table-scroll-position-left">
<div class="ant-table ant-table-empty">
<!---->
<div class="ant-table-content">
<!---->
<div class="ant-table-body">
<table class="">
<colgroup>
<col data-key="1">
<col data-key="2">
<col data-key="3">
<col data-key="4">
<col data-key="5">
<col data-key="6">
<col data-key="7">
<col data-key="8">
</colgroup>
<div class="ant-table-container">
<div class="ant-table-content">
<table style="table-layout: auto;">
<colgroup></colgroup>
<thead class="ant-table-thead">
<tr>
<th colstart="0" colspan="1" colend="0" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 1</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="1" colspan="1" colend="1" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 2</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<th class="ant-table-cell" colstart="0" colend="0">
<!---->Column 1
</th>
<th class="ant-table-cell" colstart="1" colend="1">
<!---->Column 2
</th>
<th class="ant-table-cell" colstart="2" colend="2">
<!---->Column 3
</th>
<th class="ant-table-cell" colstart="3" colend="3">
<!---->Column 4
</th>
<th class="ant-table-cell" colstart="4" colend="4">
<!---->Column 5
</th>
<th class="ant-table-cell" colstart="5" colend="5">
<!---->Column 6
</th>
<th class="ant-table-cell" colstart="6" colend="6">
<!---->Column 7
</th>
<th class="ant-table-cell" colstart="7" colend="7">
<!---->Column 8
</th>
</tr>
</thead>
<tbody class="ant-table-tbody">
<!---->
<tr class="ant-table-placeholder">
<td colspan="8" class="ant-table-cell">
<!---->No data
</td>
</tr>
</tbody>
<!---->
</table>
</div>
</div>
<!---->
</th>
<th colstart="2" colspan="1" colend="2" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 3</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="3" colspan="1" colend="3" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 4</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="4" colspan="1" colend="4" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 5</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="5" colspan="1" colend="5" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 6</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="6" colspan="1" colend="6" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 7</span><span class="ant-table-column-sorter"><!----></span></div></span>
<!---->
</th>
<th colstart="7" colspan="1" colend="7" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Column 8</span><span class="ant-table-column-sorter"><!----></span></div></span>
<!---->
</th>
</tr>
</thead>
<tbody class="ant-table-tbody"></tbody>
</table>
</div>
<div class="ant-table-placeholder">
<div class="ant-empty ant-empty-normal">
<div class="ant-empty-image"><svg class="ant-empty-img-simple" width="64" height="41" viewBox="0 0 64 41">
<g transform="translate(0 1)" fill="none" fill-rule="evenodd">
<ellipse class="ant-empty-img-simple-ellipse" fill="#F5F5F5" cx="32" cy="33" rx="32" ry="7"></ellipse>
<g class="ant-empty-img-simple-g" fill-rule="nonzero" stroke="#D9D9D9">
<path d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"></path>
<path d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z" fill="#FAFAFA" class="ant-empty-img-simple-path"></path>
</g>
</g>
</svg></div>
<p class="ant-empty-description">No Data</p>
<!---->
</div>
</div>
</div>
</div>
<!---->
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,29 @@
import type { ComputedRef, InjectionKey } from 'vue';
import { computed } from 'vue';
import { inject, provide } from 'vue';
export type ContextSlots = {
emptyText?: (...args: any[]) => void;
expandIcon?: (...args: any[]) => void;
title?: (...args: any[]) => void;
footer?: (...args: any[]) => void;
summary?: (...args: any[]) => void;
bodyCell?: (...args: any[]) => void;
headerCell?: (...args: any[]) => void;
customFilterIcon?: (...args: any[]) => void;
customFilterDropdown?: (...args: any[]) => void;
// 兼容 2.x 的 columns slots 配置
[key: string]: (...args: any[]) => void;
};
export type ContextProps = ComputedRef<ContextSlots>;
export const ContextKey: InjectionKey<ContextProps> = Symbol('ContextProps');
export const useProvideSlots = (props: ContextProps) => {
provide(ContextKey, props);
};
export const useInjectSlots = () => {
return inject(ContextKey, computed(() => ({})) as ContextProps);
};

View File

@ -1,43 +0,0 @@
import PropTypes from '../_util/vue-types';
import { computed, defineComponent } from 'vue';
import { getSlot } from '../_util/props-util';
import omit from 'omit.js';
const BodyRowProps = {
store: PropTypes.object,
rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
prefixCls: PropTypes.string,
};
export default function createBodyRow(Component = 'tr') {
const BodyRow = defineComponent({
name: 'BodyRow',
inheritAttrs: false,
props: BodyRowProps,
setup(props) {
return {
selected: computed(() => props.store?.selectedRowKeys.indexOf(props.rowKey) >= 0),
};
},
render() {
const rowProps = omit({ ...this.$props, ...this.$attrs }, [
'prefixCls',
'rowKey',
'store',
'class',
]);
const className = {
[`${this.prefixCls}-row-selected`]: this.selected,
[this.$attrs.class as string]: !!this.$attrs.class,
};
return (
<Component class={className} {...rowProps}>
{getSlot(this)}
</Component>
);
},
});
return BodyRow;
}

View File

@ -28,20 +28,23 @@ This example shows how to fetch and present data from a remote server, and how t
:loading="loading"
@change="handleTableChange"
>
<template #name="{ text }">{{ text.first }} {{ text.last }}</template>
<template #bodyCell="{ column, text }">
<template v-if="column.dataIndex === 'name'">{{ text.first }} {{ text.last }}</template>
<template v-else>{{ text }}</template>
</template>
</a-table>
</template>
<script lang="ts">
import { TableState, TableStateFilters } from 'ant-design-vue/es/table/interface';
import type { TableProps } from 'ant-design-vue';
import { usePagination } from 'vue-request';
import { computed, defineComponent } from 'vue';
import axios from 'axios';
const columns = [
{
title: 'Name',
dataIndex: 'name',
sorter: true,
width: '20%',
slots: { customRender: 'name' },
},
{
title: 'Gender',
@ -58,7 +61,6 @@ const columns = [
},
];
type Pagination = TableState['pagination'];
type APIParams = {
results: number;
page?: number;
@ -75,21 +77,24 @@ type APIResult = {
};
const queryData = (params: APIParams) => {
return `https://randomuser.me/api?noinfo&${new URLSearchParams(params)}`;
return axios.get<APIResult>('https://randomuser.me/api?noinfo', { params });
};
export default defineComponent({
setup() {
const { data: dataSource, run, loading, current, pageSize } = usePagination<APIResult>(
queryData,
{
formatResult: res => res.results,
pagination: {
currentKey: 'page',
pageSizeKey: 'results',
},
const {
data: dataSource,
run,
loading,
current,
pageSize,
} = usePagination(queryData, {
formatResult: res => res.data.results,
pagination: {
currentKey: 'page',
pageSizeKey: 'results',
},
);
});
const pagination = computed(() => ({
total: 200,
@ -97,9 +102,13 @@ export default defineComponent({
pageSize: pageSize.value,
}));
const handleTableChange = (pag: Pagination, filters: TableStateFilters, sorter: any) => {
const handleTableChange: TableProps['onChange'] = (
pag: { pageSize: number; current: number },
filters: any,
sorter: any,
) => {
run({
results: pag!.pageSize!,
results: pag.pageSize!,
page: pag?.current,
sortField: sorter.field,
sortOrder: sorter.order,

View File

@ -17,37 +17,46 @@ Simple table with actions.
<template>
<a-table :columns="columns" :data-source="data">
<template #name="{ text }">
<a>{{ text }}</a>
<template #headerCell="{ title, column }">
<template v-if="column.key === 'name'">
<span>
<smile-outlined />
Name
</span>
</template>
<template v-else>{{ title }}</template>
</template>
<template #customTitle>
<span>
<smile-outlined />
Name
</span>
</template>
<template #tags="{ text: tags }">
<span>
<a-tag
v-for="tag in tags"
:key="tag"
:color="tag === 'loser' ? 'volcano' : tag.length > 5 ? 'geekblue' : 'green'"
>
{{ tag.toUpperCase() }}
</a-tag>
</span>
</template>
<template #action="{ record }">
<span>
<a>Invite {{ record.name }}</a>
<a-divider type="vertical" />
<a>Delete</a>
<a-divider type="vertical" />
<a class="ant-dropdown-link">
More actions
<down-outlined />
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
<a>
{{ record.name }}
</a>
</span>
</template>
<template v-else-if="column.key === 'tags'">
<span>
<a-tag
v-for="tag in record.tags"
:key="tag"
:color="tag === 'loser' ? 'volcano' : tag.length > 5 ? 'geekblue' : 'green'"
>
{{ tag.toUpperCase() }}
</a-tag>
</span>
</template>
<template v-else-if="column.key === 'action'">
<span>
<a>Invite {{ record.name }}</a>
<a-divider type="vertical" />
<a>Delete</a>
<a-divider type="vertical" />
<a class="ant-dropdown-link">
More actions
<down-outlined />
</a>
</span>
</template>
<template v-else>{{ record.name }}</template>
</template>
</a-table>
</template>
@ -56,9 +65,9 @@ import { SmileOutlined, DownOutlined } from '@ant-design/icons-vue';
import { defineComponent } from 'vue';
const columns = [
{
name: 'Name',
dataIndex: 'name',
key: 'name',
slots: { title: 'customTitle', customRender: 'name' },
},
{
title: 'Age',
@ -74,12 +83,10 @@ const columns = [
title: 'Tags',
key: 'tags',
dataIndex: 'tags',
slots: { customRender: 'tags' },
},
{
title: 'Action',
key: 'action',
slots: { customRender: 'action' },
},
];

View File

@ -17,8 +17,11 @@ Add border, title and footer for table.
<template>
<a-table :columns="columns" :data-source="data" bordered>
<template #name="{ text }">
<a>{{ text }}</a>
<template #bodyCell="{ column, text }">
<template v-if="column.dataIndex === 'name'">
<a>{{ text }}</a>
</template>
<template v-else>{{ text }}</template>
</template>
<template #title>Header</template>
<template #footer>Footer</template>
@ -31,7 +34,6 @@ const columns = [
{
title: 'Name',
dataIndex: 'name',
slots: { customRender: 'name' },
},
{
title: 'Cash Assets',

View File

@ -20,19 +20,21 @@ Table cell supports `colSpan` and `rowSpan` that set in render return object. Wh
<template>
<a-table :columns="columns" :data-source="data" bordered>
<template #name="{ text }">
<a>{{ text }}</a>
<template #bodyCell="{ column, text }">
<template v-if="column.dataIndex === 'name'">
<a href="javascript:;">{{ text }}</a>
</template>
<template v-else>{{ text }}</template>
</template>
</a-table>
</template>
<script lang="ts">
import { defineComponent, h } from 'vue';
import { ColumnProps } from 'ant-design-vue/es/table/interface';
import { defineComponent } from 'vue';
import { TableColumnType } from 'ant-design-vue';
// In the fifth row, other columns are merged into first column
// by setting it's colSpan to be 0
const renderContent = ({ text, index }: any) => {
const renderContent = ({ index }: any) => {
const obj = {
children: text,
props: {} as any,
};
if (index === 4) {
@ -86,16 +88,15 @@ const data = [
export default defineComponent({
setup() {
const columns: ColumnProps[] = [
const columns: TableColumnType[] = [
{
title: 'Name',
dataIndex: 'name',
customRender: ({ text, index }) => {
customRender: ({ index }) => {
if (index < 4) {
return h('a', { href: 'javascript:;' }, text);
return;
}
return {
children: h('a', { href: 'javascript:;' }, text),
props: {
colSpan: 5,
},
@ -111,9 +112,8 @@ export default defineComponent({
title: 'Home phone',
colSpan: 2,
dataIndex: 'tel',
customRender: ({ text, index }) => {
customRender: ({ index }) => {
const obj = {
children: text,
props: {} as any,
};
if (index === 2) {

View File

@ -8,17 +8,25 @@ title:
## zh-CN
通过 `filterDropdown` 定义自定义的列筛选功能并实现一个搜索列的示例
通过 `customFilterDropdown` 定义自定义的列筛选功能并实现一个搜索列的示例
## en-US
Implement a customized column search example via `filterDropdown`.
Implement a customized column search example via `customFilterDropdown`.
</docs>
<template>
<a-table :data-source="data" :columns="columns">
<template #filterDropdown="{ setSelectedKeys, selectedKeys, confirm, clearFilters, column }">
<template #headerCell="{ column }">
<template v-if="column.key === 'name'">
<span style="color: #1890ff">Name</span>
</template>
<template v-else>{{ column.title }}</template>
</template>
<template
#customFilterDropdown="{ setSelectedKeys, selectedKeys, confirm, clearFilters, column }"
>
<div style="padding: 8px">
<a-input
ref="searchInput"
@ -42,10 +50,10 @@ Implement a customized column search example via `filterDropdown`.
</a-button>
</div>
</template>
<template #filterIcon="filtered">
<template #customFilterIcon="{ filtered }">
<search-outlined :style="{ color: filtered ? '#108ee9' : undefined }" />
</template>
<template #customRender="{ text, column }">
<template #bodyCell="{ text, column }">
<span v-if="searchText && searchedColumn === column.dataIndex">
<template
v-for="(fragment, i) in text
@ -71,7 +79,7 @@ Implement a customized column search example via `filterDropdown`.
<script>
import { SearchOutlined } from '@ant-design/icons-vue';
import { defineComponent, reactive, ref } from 'vue';
import { defineComponent, reactive, ref, toRefs } from 'vue';
const data = [
{
key: '1',
@ -116,17 +124,12 @@ export default defineComponent({
title: 'Name',
dataIndex: 'name',
key: 'name',
slots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
customRender: 'customRender',
},
customFilterDropdown: true,
onFilter: (value, record) =>
record.name.toString().toLowerCase().includes(value.toLowerCase()),
onFilterDropdownVisibleChange: visible => {
if (visible) {
setTimeout(() => {
console.log(searchInput.value);
searchInput.value.focus();
}, 100);
}
@ -136,30 +139,12 @@ export default defineComponent({
title: 'Age',
dataIndex: 'age',
key: 'age',
slots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
customRender: 'customRender',
},
onFilter: (value, record) =>
record.age.toString().toLowerCase().includes(value.toLowerCase()),
onFilterDropdownVisibleChange: visible => {
if (visible) {
setTimeout(() => {
searchInput.value.focus();
}, 100);
}
},
},
{
title: 'Address',
dataIndex: 'address',
key: 'address',
slots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
customRender: 'customRender',
},
customFilterDropdown: true,
onFilter: (value, record) =>
record.address.toString().toLowerCase().includes(value.toLowerCase()),
onFilterDropdownVisibleChange: visible => {
@ -188,9 +173,8 @@ export default defineComponent({
columns,
handleSearch,
handleReset,
searchText: '',
searchInput,
searchedColumn: '',
...toRefs(state),
};
},
});

View File

@ -19,26 +19,29 @@ Table with editable cells.
<template>
<a-button class="editable-add-btn" style="margin-bottom: 8px" @click="handleAdd">Add</a-button>
<a-table bordered :data-source="dataSource" :columns="columns">
<template #name="{ text, record }">
<div class="editable-cell">
<div v-if="editableData[record.key]" class="editable-cell-input-wrapper">
<a-input v-model:value="editableData[record.key].name" @pressEnter="save(record.key)" />
<check-outlined class="editable-cell-icon-check" @click="save(record.key)" />
<template #bodyCell="{ column, text, record }">
<template v-if="column.dataIndex === 'name'">
<div class="editable-cell">
<div v-if="editableData[record.key]" class="editable-cell-input-wrapper">
<a-input v-model:value="editableData[record.key].name" @pressEnter="save(record.key)" />
<check-outlined class="editable-cell-icon-check" @click="save(record.key)" />
</div>
<div v-else class="editable-cell-text-wrapper">
{{ text || ' ' }}
<edit-outlined class="editable-cell-icon" @click="edit(record.key)" />
</div>
</div>
<div v-else class="editable-cell-text-wrapper">
{{ text || ' ' }}
<edit-outlined class="editable-cell-icon" @click="edit(record.key)" />
</div>
</div>
</template>
<template #operation="{ record }">
<a-popconfirm
v-if="dataSource.length"
title="Sure to delete?"
@confirm="onDelete(record.key)"
>
<a>Delete</a>
</a-popconfirm>
</template>
<template v-else-if="column.dataIndex === 'operation'">
<a-popconfirm
v-if="dataSource.length"
title="Sure to delete?"
@confirm="onDelete(record.key)"
>
<a>Delete</a>
</a-popconfirm>
</template>
<template v-else>{{ text }}</template>
</template>
</a-table>
</template>
@ -65,7 +68,6 @@ export default defineComponent({
title: 'name',
dataIndex: 'name',
width: '30%',
slots: { customRender: 'name' },
},
{
title: 'age',
@ -78,7 +80,6 @@ export default defineComponent({
{
title: 'operation',
dataIndex: 'operation',
slots: { customRender: 'operation' },
},
];
const dataSource: Ref<DataItem[]> = ref([

View File

@ -17,30 +17,32 @@ Table with editable rows.
<template>
<a-table :columns="columns" :data-source="dataSource" bordered>
<template v-for="col in ['name', 'age', 'address']" #[col]="{ text, record }" :key="col">
<div>
<a-input
v-if="editableData[record.key]"
v-model:value="editableData[record.key][col]"
style="margin: -5px 0"
/>
<template v-else>
{{ text }}
</template>
</div>
</template>
<template #operation="{ record }">
<div class="editable-row-operations">
<span v-if="editableData[record.key]">
<a @click="save(record.key)">Save</a>
<a-popconfirm title="Sure to cancel?" @confirm="cancel(record.key)">
<a>Cancel</a>
</a-popconfirm>
</span>
<span v-else>
<a @click="edit(record.key)">Edit</a>
</span>
</div>
<template #bodyCell="{ column, text, record }">
<template v-if="['name', 'age', 'address'].includes(column.dataIndex)">
<div>
<a-input
v-if="editableData[record.key]"
v-model:value="editableData[record.key][column.dataIndex]"
style="margin: -5px 0"
/>
<template v-else>
{{ text }}
</template>
</div>
</template>
<template v-else-if="column.dataIndex === 'operation'">
<div class="editable-row-operations">
<span v-if="editableData[record.key]">
<a @click="save(record.key)">Save</a>
<a-popconfirm title="Sure to cancel?" @confirm="cancel(record.key)">
<a>Cancel</a>
</a-popconfirm>
</span>
<span v-else>
<a @click="edit(record.key)">Edit</a>
</span>
</div>
</template>
</template>
</a-table>
</template>
@ -53,24 +55,20 @@ const columns = [
title: 'name',
dataIndex: 'name',
width: '25%',
slots: { customRender: 'name' },
},
{
title: 'age',
dataIndex: 'age',
width: '15%',
slots: { customRender: 'age' },
},
{
title: 'address',
dataIndex: 'address',
width: '40%',
slots: { customRender: 'address' },
},
{
title: 'operation',
dataIndex: 'operation',
slots: { customRender: 'operation' },
},
];
interface DataItem {

View File

@ -20,8 +20,11 @@ Ellipsis cell content via setting `column.ellipsis`.
<template>
<a-table :columns="columns" :data-source="data">
<template #name="{ text }">
<a>{{ text }}</a>
<template #bodyCell="{ column, text }">
<template v-if="column.dataIndex === 'name'">
<a>{{ text }}</a>
</template>
<template v-else>{{ text }}</template>
</template>
</a-table>
</template>
@ -32,7 +35,6 @@ const columns = [
title: 'Name',
dataIndex: 'name',
key: 'name',
slots: { customRender: 'name' },
},
{
title: 'Age',

View File

@ -10,21 +10,23 @@ title:
表格支持树形数据的展示当数据中有 `children` 字段时会自动展示为树形表格如果不需要或配置为其他字段可以用 `childrenColumnName` 进行配置
可以通过设置 `indentSize` 以控制每一层的缩进宽度
> 暂不支持父子数据递归关联选择
## en-US
Display tree structure data in Table when there is field key `children` in dataSource, try to customize `childrenColumnName` property to avoid tree table structure.
You can control the indent width by setting `indentSize`.
> Note, no support for recursive selection of tree structure data table yet.
</docs>
<template>
<a-space align="center" style="margin-bottom: 16px">
CheckStrictly:
<a-switch v-model:checked="rowSelection.checkStrictly"></a-switch>
</a-space>
<a-table :columns="columns" :data-source="data" :row-selection="rowSelection" />
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, ref } from 'vue';
const columns = [
{
title: 'Name',
@ -118,7 +120,8 @@ const data: DataItem[] = [
},
];
const rowSelection = {
const rowSelection = ref({
checkStrictly: false,
onChange: (selectedRowKeys: (string | number)[], selectedRows: DataItem[]) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
@ -128,7 +131,7 @@ const rowSelection = {
onSelectAll: (selected: boolean, selectedRows: DataItem[], changeRows: DataItem[]) => {
console.log(selected, selectedRows, changeRows);
},
};
});
export default defineComponent({
setup() {

View File

@ -18,8 +18,11 @@ When there's too much information to show and the table can't display all at onc
<template>
<a-table :columns="columns" :data-source="data">
<template #action>
<a>Delete</a>
<template #bodyCell="{ column, text }">
<template v-if="column.key === 'action'">
<a>Delete</a>
</template>
<template v-else>{{ text }}</template>
</template>
<template #expandedRowRender="{ record }">
<p style="margin: 0">
@ -34,7 +37,7 @@ const columns = [
{ title: 'Name', dataIndex: 'name', key: 'name' },
{ title: 'Age', dataIndex: 'age', key: 'age' },
{ title: 'Address', dataIndex: 'address', key: 'address' },
{ title: 'Action', dataIndex: '', key: 'x', slots: { customRender: 'action' } },
{ title: 'Action', key: 'action' },
];
const data = [

View File

@ -25,8 +25,11 @@ A Solution for displaying large amounts of data with long columns.
<template>
<a-table :columns="columns" :data-source="data" :scroll="{ x: 1500, y: 300 }">
<template #action>
<a>action</a>
<template #bodyCell="{ column, text }">
<template v-if="column.key === 'operation'">
<a>action</a>
</template>
<template v-else>{{ text }}</template>
</template>
</a-table>
</template>
@ -49,7 +52,6 @@ const columns = [
key: 'operation',
fixed: 'right',
width: 100,
slots: { customRender: 'action' },
},
];

View File

@ -25,9 +25,12 @@ To fix some columns and scroll inside other columns, and you must set `scroll.x`
</docs>
<template>
<a-table :columns="columns" :data-source="data" :scroll="{ x: 1300 }">
<template #action>
<a>action</a>
<a-table :columns="columns" :data-source="data" :scroll="{ x: 1300, y: 1000 }">
<template #bodyCell="{ column, text }">
<template v-if="column.key === 'operation'">
<a>action</a>
</template>
<template v-else>{{ text }}</template>
</template>
</a-table>
</template>
@ -50,7 +53,6 @@ const columns = [
key: 'operation',
fixed: 'right',
width: 100,
slots: { customRender: 'action' },
},
];

View File

@ -28,6 +28,7 @@ If a `sortOrder` or `defaultSortOrder` is specified with the value `ascend` or `
<a-table :columns="columns" :data-source="data" @change="onChange" />
</template>
<script lang="ts">
import type { TableColumnType, TableProps } from 'ant-design-vue';
import { defineComponent } from 'vue';
type TableDataType = {
@ -37,35 +38,7 @@ type TableDataType = {
address: string;
};
type PaginationType = {
current: number;
pageSize: number;
};
type FilterType = {
name: string;
address: string;
};
type ColumnType = {
title: string;
dataIndex: string;
filters?: {
text: string;
value: string;
children?: {
text: string;
value: string;
}[];
}[];
onFilter?: (value: string, record: TableDataType) => boolean;
sorter?: (a: TableDataType, b: TableDataType) => number;
sortDirections?: string[];
defaultSortOrder?: string;
filterMultiple?: string[] | boolean;
};
const columns: ColumnType[] = [
const columns: TableColumnType<TableDataType>[] = [
{
title: 'Name',
dataIndex: 'name',
@ -153,7 +126,7 @@ const data: TableDataType[] = [
];
export default defineComponent({
setup() {
const onChange = (pagination: PaginationType, filters: FilterType[], sorter: ColumnType) => {
const onChange: TableProps<TableDataType>['onChange'] = (pagination, filters, sorter) => {
console.log('params', pagination, filters, sorter);
};
return {

View File

@ -15,6 +15,7 @@
<FixedHeader />
<GroupingColumns />
<Head />
<MultipleSorter />
<NestedTable />
<ResetFilter />
<RowSelectionAndOperation />
@ -22,6 +23,7 @@
<RowSelection />
<Size />
<Stripe />
<Summary />
<TemplateCom />
</demo-sort>
</template>
@ -51,6 +53,8 @@ import Size from './size.vue';
import TemplateCom from './template.vue';
import Ellipsis from './ellipsis.vue';
import Stripe from './stripe.vue';
import MultipleSorter from './multiple-sorter.vue';
import Summary from './summary.vue';
import CN from '../index.zh-CN.md';
import US from '../index.en-US.md';
@ -81,6 +85,8 @@ export default {
Size,
TemplateCom,
Stripe,
MultipleSorter,
Summary,
},
};
</script>

View File

@ -0,0 +1,99 @@
<docs>
---
order: 7
title:
en-US: Multiple sorter
zh-CN: 多列排序
---
## zh-CN
`column.sorter` 支持 `multiple` 字段以配置多列排序优先级通过 `sorter.compare` 配置排序逻辑你可以通过不设置该函数只启动多列排序的交互形式
## en-US
`column.sorter` support `multiple` to config the priority of sort columns. Though `sorter.compare` to customize compare function. You can also leave it empty to use the interactive only.
</docs>
<template>
<a-table :columns="columns" :data-source="data" @change="onChange" />
</template>
<script lang="ts">
import { defineComponent } from 'vue';
const columns = [
{
title: 'Name',
dataIndex: 'name',
},
{
title: 'Chinese Score',
dataIndex: 'chinese',
sorter: {
compare: (a, b) => a.chinese - b.chinese,
multiple: 3,
},
},
{
title: 'Math Score',
dataIndex: 'math',
sorter: {
compare: (a, b) => a.math - b.math,
multiple: 2,
},
},
{
title: 'English Score',
dataIndex: 'english',
sorter: {
compare: (a, b) => a.english - b.english,
multiple: 1,
},
},
];
const data = [
{
key: '1',
name: 'John Brown',
chinese: 98,
math: 60,
english: 70,
},
{
key: '2',
name: 'Jim Green',
chinese: 98,
math: 66,
english: 89,
},
{
key: '3',
name: 'Joe Black',
chinese: 98,
math: 90,
english: 70,
},
{
key: '4',
name: 'Jim Red',
chinese: 88,
math: 99,
english: 89,
},
];
export default defineComponent({
setup() {
return {
data,
columns,
onChange: (pagination, filters, sorter, extra) => {
console.log('params', pagination, filters, sorter, extra);
},
};
},
});
</script>

View File

@ -19,34 +19,40 @@ Showing more detailed info of every row.
<template>
<a-table :columns="columns" :data-source="data" class="components-table-demo-nested">
<template #operation>
<a>Publish</a>
<template #bodyCell="{ column, text }">
<template v-if="column.key === 'operation'">
<a>Publish</a>
</template>
<template v-else>{{ text }}</template>
</template>
<template #expandedRowRender>
<a-table :columns="innerColumns" :data-source="innerData" :pagination="false">
<template #status>
<span>
<a-badge status="success" />
Finished
</span>
</template>
<template #operation>
<span class="table-operation">
<a>Pause</a>
<a>Stop</a>
<a-dropdown>
<template #overlay>
<a-menu>
<a-menu-item>Action 1</a-menu-item>
<a-menu-item>Action 2</a-menu-item>
</a-menu>
</template>
<a>
More
<down-outlined />
</a>
</a-dropdown>
</span>
<template #bodyCell="{ column, text }">
<template v-if="column.key === 'state'">
<span>
<a-badge status="success" />
Finished
</span>
</template>
<template v-else-if="column.key === 'operation'">
<span class="table-operation">
<a>Pause</a>
<a>Stop</a>
<a-dropdown>
<template #overlay>
<a-menu>
<a-menu-item>Action 1</a-menu-item>
<a-menu-item>Action 2</a-menu-item>
</a-menu>
</template>
<a>
More
<down-outlined />
</a>
</a-dropdown>
</span>
</template>
<template v-else>{{ text }}</template>
</template>
</a-table>
</template>
@ -63,7 +69,7 @@ const columns = [
{ title: 'Upgraded', dataIndex: 'upgradeNum', key: 'upgradeNum' },
{ title: 'Creator', dataIndex: 'creator', key: 'creator' },
{ title: 'Date', dataIndex: 'createdAt', key: 'createdAt' },
{ title: 'Action', key: 'operation', slots: { customRender: 'operation' } },
{ title: 'Action', key: 'operation' },
];
interface DataItem {
@ -80,7 +86,7 @@ const data: DataItem[] = [];
for (let i = 0; i < 3; ++i) {
data.push({
key: i,
name: 'Screem',
name: `Screem ${i + 1}`,
platform: 'iOS',
version: '10.3.4.5654',
upgradeNum: 500,
@ -92,13 +98,12 @@ for (let i = 0; i < 3; ++i) {
const innerColumns = [
{ title: 'Date', dataIndex: 'date', key: 'date' },
{ title: 'Name', dataIndex: 'name', key: 'name' },
{ title: 'Status', key: 'state', slots: { customRender: 'status' } },
{ title: 'Status', key: 'state' },
{ title: 'Upgrade Status', dataIndex: 'upgradeNum', key: 'upgradeNum' },
{
title: 'Action',
dataIndex: 'operation',
key: 'operation',
slots: { customRender: 'operation' },
},
];
@ -114,7 +119,7 @@ for (let i = 0; i < 3; ++i) {
innerData.push({
key: i,
date: '2014-12-24 23:12:00',
name: 'This is production name',
name: `This is production name ${i + 1}`,
upgradeNum: 'Upgraded: 56',
});
}

View File

@ -35,9 +35,7 @@ Control filters and sorters by `filteredValue` and `sortOrder`.
</template>
<script lang="ts">
import { computed, defineComponent, ref } from 'vue';
import { TableState, TableStateFilters } from 'ant-design-vue/es/table/interface';
type Pagination = TableState['pagination'];
import type { TableColumnType, TableProps } from 'ant-design-vue';
interface DataItem {
key: string;
@ -78,7 +76,7 @@ export default defineComponent({
const filteredInfo = ref();
const sortedInfo = ref();
const columns = computed(() => {
const columns = computed<TableColumnType[]>(() => {
const filtered = filteredInfo.value || {};
const sorted = sortedInfo.value || {};
return [
@ -120,7 +118,7 @@ export default defineComponent({
];
});
const handleChange = (pagination: Pagination, filters: TableStateFilters, sorter: any) => {
const handleChange: TableProps['onChange'] = (pagination, filters, sorter) => {
console.log('Various parameters', pagination, filters, sorter);
filteredInfo.value = filters;
sortedInfo.value = sorter;

View File

@ -37,9 +37,8 @@ To perform operations and clear selections after selecting some rows, use `rowSe
</template>
<script lang="ts">
import { computed, defineComponent, reactive, toRefs } from 'vue';
import { ColumnProps } from 'ant-design-vue/es/table/interface';
type Key = ColumnProps['key'];
type Key = string | number;
interface DataType {
key: Key;

View File

@ -19,12 +19,10 @@ Use `rowSelection.selections` custom selections, default no select dropdown, sho
</template>
<script lang="ts">
import { defineComponent, computed, ref, unref } from 'vue';
import { ColumnProps } from 'ant-design-vue/es/table/interface';
type Key = ColumnProps['key'];
import { Table } from 'ant-design-vue';
interface DataType {
key: Key;
key: string | number;
name: string;
age: number;
address: string;
@ -57,9 +55,9 @@ for (let i = 0; i < 46; i++) {
export default defineComponent({
setup() {
const selectedRowKeys = ref<Key[]>([]); // Check here to configure the default column
const selectedRowKeys = ref<DataType['key'][]>([]); // Check here to configure the default column
const onSelectChange = (changableRowKeys: Key[]) => {
const onSelectChange = (changableRowKeys: string[]) => {
console.log('selectedRowKeys changed: ', changableRowKeys);
selectedRowKeys.value = changableRowKeys;
};
@ -70,19 +68,15 @@ export default defineComponent({
onChange: onSelectChange,
hideDefaultSelections: true,
selections: [
{
key: 'all-data',
text: 'Select All Data',
onSelect: () => {
selectedRowKeys.value = [...Array(46).keys()]; // 0...45
},
},
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE,
{
key: 'odd',
text: 'Select Odd Row',
onSelect: (changableRowKeys: Key[]) => {
onSelect: changableRowKeys => {
let newSelectedRowKeys = [];
newSelectedRowKeys = changableRowKeys.filter((key, index) => {
newSelectedRowKeys = changableRowKeys.filter((_key, index) => {
if (index % 2 !== 0) {
return false;
}
@ -94,9 +88,9 @@ export default defineComponent({
{
key: 'even',
text: 'Select Even Row',
onSelect: (changableRowKeys: Key[]) => {
onSelect: changableRowKeys => {
let newSelectedRowKeys = [];
newSelectedRowKeys = changableRowKeys.filter((key, index) => {
newSelectedRowKeys = changableRowKeys.filter((_key, index) => {
if (index % 2 !== 0) {
return true;
}

View File

@ -7,40 +7,40 @@ title:
---
## zh-CN
第一列是联动的选择框
第一列是联动的选择框
默认点击 checkbox 触发选择行为需要 `点击行` 触发可参考例子https://codesandbox.io/s/row-selection-on-click-tr58v
## en-US
Rows can be selectable by making first column as a selectable column.
Rows can be selectable by making first column as a selectable column.
selection happens when clicking checkbox defaultly. You can see https://codesandbox.io/s/row-selection-on-click-tr58v if you need row-click selection behavior.
</docs>
<template>
<a-table :row-selection="rowSelection" :columns="columns" :data-source="data">
<template #name="{ text }">
<a>{{ text }}</a>
<template #bodyCell="{ column, text }">
<template v-if="column.dataIndex === 'name'">
<a>{{ text }}</a>
</template>
<template v-else>{{ text }}</template>
</template>
</a-table>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { ColumnProps } from 'ant-design-vue/es/table/interface';
type Key = ColumnProps['key'];
import type { TableProps, TableColumnType } from 'ant-design-vue';
interface DataType {
key: Key;
key: string;
name: string;
age: number;
address: string;
}
const columns = [
const columns: TableColumnType<DataType>[] = [
{
title: 'Name',
dataIndex: 'name',
slots: { customRender: 'name' },
},
{
title: 'Age',
@ -80,8 +80,8 @@ const data: DataType[] = [
export default defineComponent({
setup() {
const rowSelection = {
onChange: (selectedRowKeys: Key[], selectedRows: DataType[]) => {
const rowSelection: TableProps['rowSelection'] = {
onChange: (selectedRowKeys: string[], selectedRows: DataType[]) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: (record: DataType) => ({

View File

@ -20,14 +20,14 @@ Use `rowClassName` Customize the table with Striped.
size="middle"
:columns="columns"
:data-source="data"
:row-class-name="(record, index) => (index % 2 === 1 ? 'table-striped' : null)"
:row-class-name="(_record, index) => (index % 2 === 1 ? 'table-striped' : null)"
/>
<a-table
class="ant-table-striped"
size="middle"
:columns="columns"
:data-source="data"
:row-class-name="(record, index) => (index % 2 === 1 ? 'table-striped' : null)"
:row-class-name="(_record, index) => (index % 2 === 1 ? 'table-striped' : null)"
bordered
/>
</template>

View File

@ -0,0 +1,159 @@
<docs>
---
order: 29
title:
en-US: Summary
zh-CN: 总结栏
---
## zh-CN
通过 `summary` 设置总结栏使用 `a-table-summary-cell` 同步 Column 的固定状态你可以通过配置 `a-table-summary` `fixed` 属性使其固定
## en-US
Set summary content by `summary` prop. Sync column fixed status with `a-table-summary-cell`. You can fixed it by set `a-table-summary` `fixed` prop.
</docs>
<template>
<a-table :columns="columns" :data-source="data" :pagination="false" bordered>
<template #summary>
<a-table-summary-row>
<a-table-summary-cell>Total</a-table-summary-cell>
<a-table-summary-cell>
<a-typography-text type="danger">{{ totals.totalBorrow }}</a-typography-text>
</a-table-summary-cell>
<a-table-summary-cell>
<a-typography-text>{{ totals.totalRepayment }}</a-typography-text>
</a-table-summary-cell>
</a-table-summary-row>
<a-table-summary-row>
<a-table-summary-cell>Balance</a-table-summary-cell>
<a-table-summary-cell :col-span="2">
<a-typography-text type="danger">
{{ totals.totalBorrow - totals.totalRepayment }}
</a-typography-text>
</a-table-summary-cell>
</a-table-summary-row>
</template>
</a-table>
<br />
<a-table
:columns="fixedColumns"
:data-source="fixedData"
:pagination="false"
:scroll="{ x: 2000, y: 500 }"
bordered
>
<template #summary>
<a-table-summary fixed>
<a-table-summary-row>
<a-table-summary-cell :index="0">Summary</a-table-summary-cell>
<a-table-summary-cell :index="1">This is a summary content</a-table-summary-cell>
</a-table-summary-row>
</a-table-summary>
</template>
</a-table>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from 'vue';
export default defineComponent({
setup() {
const columns = ref([
{
title: 'Name',
dataIndex: 'name',
},
{
title: 'Borrow',
dataIndex: 'borrow',
},
{
title: 'Repayment',
dataIndex: 'repayment',
},
]);
const data = ref([
{
key: '1',
name: 'John Brown',
borrow: 10,
repayment: 33,
},
{
key: '2',
name: 'Jim Green',
borrow: 100,
repayment: 0,
},
{
key: '3',
name: 'Joe Black',
borrow: 10,
repayment: 10,
},
{
key: '4',
name: 'Jim Red',
borrow: 75,
repayment: 45,
},
]);
const fixedColumns = ref([
{
title: 'Name',
dataIndex: 'name',
fixed: true,
width: 100,
},
{
title: 'Description',
dataIndex: 'description',
},
]);
const fixedData = ref<{ key: number; name: string; description: string }[]>([]);
for (let i = 0; i < 20; i += 1) {
fixedData.value.push({
key: i,
name: ['Light', 'Bamboo', 'Little'][i % 3],
description: 'Everything that has a beginning, has an end.',
});
}
const totals = computed(() => {
let totalBorrow = 0;
let totalRepayment = 0;
data.value.forEach(({ borrow, repayment }) => {
totalBorrow += borrow;
totalRepayment += repayment;
});
return { totalBorrow, totalRepayment };
});
return {
data,
columns,
totals,
fixedColumns,
fixedData,
};
},
});
</script>
<style>
#components-table-demo-summary tfoot th,
#components-table-demo-summary tfoot td {
background: #fafafa;
}
[data-theme='dark'] #components-table-demo-summary tfoot th,
[data-theme='dark'] #components-table-demo-summary tfoot td {
background: #1d1d1d;
}
</style>

View File

@ -8,13 +8,17 @@ title:
## zh-CN
使用 template 风格的 API
使用 template 风格的 API
> 不推荐使用会有一定的性能损耗
> 这个只是一个描述 `columns` 的语法糖所以你不能用其他组件去包裹 `Column` `ColumnGroup`
## en-US
Using template style API
Using template style API.
> Not recommended, there will be a certain performance loss.
> Since this is just a syntax sugar for the prop `columns`, so that you can't compose `Column` and `ColumnGroup` with other Components.

View File

@ -1,290 +0,0 @@
import { reactive, defineComponent, nextTick, computed, watch } from 'vue';
import FilterFilled from '@ant-design/icons-vue/FilterFilled';
import Menu, { SubMenu, MenuItem } from '../menu';
import classNames from '../_util/classNames';
import shallowequal from '../_util/shallowequal';
import Dropdown from '../dropdown';
import Checkbox from '../checkbox';
import Radio from '../radio';
import FilterDropdownMenuWrapper from './FilterDropdownMenuWrapper';
import { FilterMenuProps } from './interface';
import { isValidElement } from '../_util/props-util';
import initDefaultProps from '../_util/props-util/initDefaultProps';
import { cloneElement } from '../_util/vnode';
import BaseMixin2 from '../_util/BaseMixin2';
import { generateValueMaps } from './util';
import type { Key } from '../_util/type';
function stopPropagation(e) {
e.stopPropagation();
}
export default defineComponent({
name: 'FilterMenu',
mixins: [BaseMixin2],
inheritAttrs: false,
props: initDefaultProps(FilterMenuProps, {
column: {},
}),
setup(props) {
const sSelectedKeys = computed(() => props.selectedKeys);
const sVisible = computed(() => {
return 'filterDropdownVisible' in props.column ? props.column.filterDropdownVisible : false;
});
const sValueKeys = computed(() => generateValueMaps(props.column.filters));
const state = reactive({
neverShown: false,
sSelectedKeys: sSelectedKeys.value,
sKeyPathOfSelectedItem: {}, //
sVisible: sVisible.value,
sValueKeys: sValueKeys.value,
});
watch(sSelectedKeys, () => {
state.sSelectedKeys = sSelectedKeys.value;
});
watch(sVisible, () => {
state.sVisible = sVisible.value;
});
watch(sValueKeys, () => {
state.sValueKeys = sValueKeys.value;
});
// watchEffect(
// () => {
// const { column } = nextProps;
// if (!shallowequal(preProps.selectedKeys, nextProps.selectedKeys)) {
// state.sSelectedKeys = nextProps.selectedKeys;
// }
// if (!shallowequal((preProps.column || {}).filters, (nextProps.column || {}).filters)) {
// state.sValueKeys = generateValueMaps(nextProps.column.filters);
// }
// if ('filterDropdownVisible' in column) {
// state.sVisible = column.filterDropdownVisible;
// }
// preProps = { ...nextProps };
// },
// { flush: 'sync' },
// );
return state;
},
methods: {
getDropdownVisible() {
return !!this.sVisible;
},
setSelectedKeys({ selectedKeys }) {
this.setState({ sSelectedKeys: selectedKeys });
},
setVisible(visible: boolean) {
const { column } = this;
if (!('filterDropdownVisible' in column)) {
this.setState({ sVisible: visible });
}
if (column.onFilterDropdownVisibleChange) {
column.onFilterDropdownVisibleChange(visible);
}
},
handleClearFilters() {
this.setState(
{
sSelectedKeys: [],
},
this.handleConfirm,
);
},
handleConfirm() {
this.setVisible(false);
// Call `setSelectedKeys` & `confirm` in the same time will make filter data not up to date
// https://github.com/ant-design/ant-design/issues/12284
(this as any).$forceUpdate();
nextTick(this.confirmFilter2);
},
onVisibleChange(visible: boolean) {
this.setVisible(visible);
const { column } = this.$props;
// https://github.com/ant-design/ant-design/issues/17833
if (!visible && !(column.filterDropdown instanceof Function)) {
this.confirmFilter2();
}
},
handleMenuItemClick(info: { keyPath: Key[]; key: Key }) {
const { sSelectedKeys: selectedKeys } = this;
if (!info.keyPath || info.keyPath.length <= 1) {
return;
}
const { sKeyPathOfSelectedItem: keyPathOfSelectedItem } = this;
if (selectedKeys && selectedKeys.indexOf(info.key) >= 0) {
// deselect SubMenu child
delete keyPathOfSelectedItem[info.key];
} else {
// select SubMenu child
keyPathOfSelectedItem[info.key] = info.keyPath;
}
this.setState({ sKeyPathOfSelectedItem: keyPathOfSelectedItem });
},
hasSubMenu() {
const {
column: { filters = [] },
} = this;
return filters.some(item => !!(item.children && item.children.length > 0));
},
confirmFilter2() {
const { column, selectedKeys: propSelectedKeys, confirmFilter } = this.$props;
const { sSelectedKeys: selectedKeys, sValueKeys: valueKeys } = this;
const { filterDropdown } = column;
if (!shallowequal(selectedKeys, propSelectedKeys)) {
confirmFilter(
column,
filterDropdown
? selectedKeys
: selectedKeys.map((key: any) => valueKeys[key]).filter(key => key !== undefined),
);
}
},
renderMenus(items) {
const { dropdownPrefixCls, prefixCls } = this.$props;
return items.map(item => {
if (item.children && item.children.length > 0) {
const { sKeyPathOfSelectedItem } = this;
const containSelected = Object.keys(sKeyPathOfSelectedItem).some(
key => sKeyPathOfSelectedItem[key].indexOf(item.value) >= 0,
);
const subMenuCls = classNames(`${prefixCls}-dropdown-submenu`, {
[`${dropdownPrefixCls}-submenu-contain-selected`]: containSelected,
});
return (
<SubMenu title={item.text} popupClassName={subMenuCls} key={item.value}>
{this.renderMenus(item.children)}
</SubMenu>
);
}
return this.renderMenuItem(item);
});
},
renderFilterIcon() {
const { column, locale, prefixCls, selectedKeys } = this;
const filtered = selectedKeys && selectedKeys.length > 0;
let filterIcon = column.filterIcon;
if (typeof filterIcon === 'function') {
filterIcon = filterIcon({ filtered, column });
}
const dropdownIconClass = classNames({
[`${prefixCls}-selected`]: 'filtered' in column ? column.filtered : filtered,
[`${prefixCls}-open`]: this.getDropdownVisible(),
});
if (!filterIcon) {
return (
<FilterFilled
title={locale.filterTitle}
class={dropdownIconClass}
onClick={stopPropagation}
/>
);
}
if (filterIcon.length === 1 && isValidElement(filterIcon[0])) {
return cloneElement(filterIcon[0], {
title: filterIcon.props?.title || locale.filterTitle,
onClick: stopPropagation,
class: classNames(`${prefixCls}-icon`, dropdownIconClass, filterIcon.props?.class),
});
}
return (
<span class={classNames(`${prefixCls}-icon`, dropdownIconClass)} onClick={stopPropagation}>
{filterIcon}
</span>
);
},
renderMenuItem(item) {
const { column } = this;
const { sSelectedKeys: selectedKeys } = this;
const multiple = 'filterMultiple' in column ? column.filterMultiple : true;
const input = multiple ? (
<Checkbox checked={selectedKeys && selectedKeys.indexOf(item.value) >= 0} />
) : (
<Radio checked={selectedKeys && selectedKeys.indexOf(item.value) >= 0} />
);
return (
<MenuItem key={item.value}>
{input}
<span>{item.text}</span>
</MenuItem>
);
},
},
render() {
const { sSelectedKeys: originSelectedKeys } = this as any;
const { column, locale, prefixCls, dropdownPrefixCls, getPopupContainer } = this;
// default multiple selection in filter dropdown
const multiple = 'filterMultiple' in column ? column.filterMultiple : true;
const dropdownMenuClass = classNames({
[`${dropdownPrefixCls}-menu-without-submenu`]: !this.hasSubMenu(),
});
let { filterDropdown } = column;
if (filterDropdown instanceof Function) {
filterDropdown = filterDropdown({
prefixCls: `${dropdownPrefixCls}-custom`,
setSelectedKeys: selectedKeys => this.setSelectedKeys({ selectedKeys }),
selectedKeys: originSelectedKeys,
confirm: this.handleConfirm,
clearFilters: this.handleClearFilters,
filters: column.filters,
visible: this.getDropdownVisible(),
column,
});
}
const menus = filterDropdown ? (
<FilterDropdownMenuWrapper class={`${prefixCls}-dropdown`}>
{filterDropdown}
</FilterDropdownMenuWrapper>
) : (
<FilterDropdownMenuWrapper class={`${prefixCls}-dropdown`}>
<Menu
multiple={multiple}
onClick={this.handleMenuItemClick}
prefixCls={`${dropdownPrefixCls}-menu`}
class={dropdownMenuClass}
onSelect={this.setSelectedKeys}
onDeselect={this.setSelectedKeys}
selectedKeys={originSelectedKeys}
getPopupContainer={getPopupContainer}
>
{this.renderMenus(column.filters)}
</Menu>
<div class={`${prefixCls}-dropdown-btns`}>
<a class={`${prefixCls}-dropdown-link confirm`} onClick={this.handleConfirm}>
{locale.filterConfirm}
</a>
<a class={`${prefixCls}-dropdown-link clear`} onClick={this.handleClearFilters}>
{locale.filterReset}
</a>
</div>
</FilterDropdownMenuWrapper>
);
return (
<Dropdown
trigger={['click']}
placement="bottomRight"
visible={this.getDropdownVisible()}
onVisibleChange={this.onVisibleChange}
getPopupContainer={getPopupContainer}
forceRender
overlay={menus}
>
{this.renderFilterIcon()}
</Dropdown>
);
},
});

View File

@ -0,0 +1,45 @@
import devWarning from '../../vc-util/devWarning';
import type { Ref } from 'vue';
import type { ContextSlots } from '../context';
import type { TransformColumns, ColumnsType } from '../interface';
function fillSlots<RecordType>(columns: ColumnsType<RecordType>, contextSlots: Ref<ContextSlots>) {
const $slots = contextSlots.value;
return columns.map(column => {
const cloneColumn = { ...column };
const { slots = {} } = cloneColumn;
cloneColumn.__originColumn__ = column;
devWarning(
!('slots' in cloneColumn),
'Table',
'`column.slots` is deprecated. Please use `v-slot:headerCell` `v-slot:bodyCell` instead.',
);
Object.keys(slots).forEach(key => {
const name = slots[key];
if (cloneColumn[key] === undefined && $slots[name]) {
cloneColumn[key] = $slots[name];
}
});
if (contextSlots.value.headerCell && !column.slots?.title) {
cloneColumn.title = contextSlots.value.headerCell({
title: column.title,
column,
});
}
if ('children' in cloneColumn) {
cloneColumn.children = fillSlots(cloneColumn.children, contextSlots);
}
return cloneColumn;
});
}
export default function useColumns<RecordType>(
contextSlots: Ref<ContextSlots>,
): [TransformColumns<RecordType>] {
const filledColumns = (columns: ColumnsType<RecordType>) => fillSlots(columns, contextSlots);
return [filledColumns];
}

View File

@ -0,0 +1,366 @@
import isEqual from 'lodash-es/isEqual';
import FilterFilled from '@ant-design/icons-vue/FilterFilled';
import Button from '../../../button';
import Menu from '../../../menu';
import Checkbox from '../../../checkbox';
import Radio from '../../../radio';
import Dropdown from '../../../dropdown';
import Empty from '../../../empty';
import type {
ColumnType,
ColumnFilterItem,
Key,
TableLocale,
GetPopupContainer,
} from '../../interface';
import FilterDropdownMenuWrapper from './FilterWrapper';
import type { FilterState } from '.';
import { computed, defineComponent, onBeforeUnmount, ref, watch } from 'vue';
import classNames from '../../../_util/classNames';
import useConfigInject from '../../../_util/hooks/useConfigInject';
import { useInjectSlots } from '../../context';
const { SubMenu, Item: MenuItem } = Menu;
function hasSubMenu(filters: ColumnFilterItem[]) {
return filters.some(({ children }) => children && children.length > 0);
}
function renderFilterItems({
filters,
prefixCls,
filteredKeys,
filterMultiple,
locale,
}: {
filters: ColumnFilterItem[];
prefixCls: string;
filteredKeys: Key[];
filterMultiple: boolean;
locale: TableLocale;
}) {
if (filters.length === 0) {
// wrapped with <div /> to avoid react warning
// https://github.com/ant-design/ant-design/issues/25979
return (
<MenuItem key="empty">
<div
style={{
margin: '16px 0',
}}
>
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={locale.filterEmptyText}
imageStyle={{
height: 24,
}}
/>
</div>
</MenuItem>
);
}
return filters.map((filter, index) => {
const key = String(filter.value);
if (filter.children) {
return (
<SubMenu
key={key || index}
title={filter.text}
popupClassName={`${prefixCls}-dropdown-submenu`}
>
{renderFilterItems({
filters: filter.children,
prefixCls,
filteredKeys,
filterMultiple,
locale,
})}
</SubMenu>
);
}
const Component = filterMultiple ? Checkbox : Radio;
return (
<MenuItem key={filter.value !== undefined ? key : index}>
<Component checked={filteredKeys.includes(key)} />
<span>{filter.text}</span>
</MenuItem>
);
});
}
export interface FilterDropdownProps<RecordType> {
tablePrefixCls: string;
prefixCls: string;
dropdownPrefixCls: string;
column: ColumnType<RecordType>;
filterState?: FilterState<RecordType>;
filterMultiple: boolean;
columnKey: Key;
triggerFilter: (filterState: FilterState<RecordType>) => void;
locale: TableLocale;
getPopupContainer?: GetPopupContainer;
}
export default defineComponent<FilterDropdownProps<any>>({
name: 'FilterDropdown',
props: [
'tablePrefixCls',
'prefixCls',
'dropdownPrefixCls',
'column',
'filterState',
'filterMultiple',
'columnKey',
'triggerFilter',
'locale',
'getPopupContainer',
] as any,
setup(props, { slots }) {
const contextSlots = useInjectSlots();
const filterDropdownVisible = computed(() => props.column.filterDropdownVisible);
const visible = ref(false);
const filtered = computed(
() =>
!!(
props.filterState &&
(props.filterState.filteredKeys?.length || props.filterState.forceFiltered)
),
);
const filterDropdownRef = computed(() => {
const { filterDropdown, slots = {}, customFilterDropdown } = props.column;
return (
filterDropdown ||
(slots.filterDropdown && contextSlots.value[slots.filterDropdown]) ||
(customFilterDropdown && contextSlots.value.customFilterDropdown)
);
});
const filterIconRef = computed(() => {
const { filterIcon, slots = {} } = props.column;
return (
filterIcon ||
(slots.filterIcon && contextSlots.value[slots.filterIcon]) ||
contextSlots.value.customFilterIcon
);
});
const triggerVisible = (newVisible: boolean) => {
visible.value = newVisible;
props.column.onFilterDropdownVisibleChange?.(newVisible);
};
const mergedVisible = computed(() =>
typeof filterDropdownVisible.value === 'boolean'
? filterDropdownVisible.value
: visible.value,
);
const propFilteredKeys = computed(() => props.filterState?.filteredKeys);
const filteredKeys = ref([]);
const onSelectKeys = ({ selectedKeys }: { selectedKeys?: Key[] }) => {
filteredKeys.value = selectedKeys;
};
watch(
propFilteredKeys,
() => {
onSelectKeys({ selectedKeys: propFilteredKeys.value || [] });
},
{ immediate: true },
);
const openKeys = ref([]);
const openRef = ref();
const onOpenChange = (keys: string[]) => {
openRef.value = window.setTimeout(() => {
openKeys.value = keys;
});
};
const onMenuClick = () => {
window.clearTimeout(openRef.value);
};
onBeforeUnmount(() => {
window.clearTimeout(openRef.value);
});
// ======================= Submit ========================
const internalTriggerFilter = (keys: Key[] | undefined | null) => {
const { column, columnKey, filterState } = props;
const mergedKeys = keys && keys.length ? keys : null;
if (mergedKeys === null && (!filterState || !filterState.filteredKeys)) {
return null;
}
if (isEqual(mergedKeys, filterState?.filteredKeys)) {
return null;
}
props.triggerFilter({
column,
key: columnKey,
filteredKeys: mergedKeys,
});
};
const onConfirm = () => {
triggerVisible(false);
internalTriggerFilter(filteredKeys.value);
};
const onReset = () => {
filteredKeys.value = [];
triggerVisible(false);
internalTriggerFilter([]);
};
const doFilter = ({ closeDropdown } = { closeDropdown: true }) => {
if (closeDropdown) {
triggerVisible(false);
}
internalTriggerFilter(filteredKeys.value);
};
const onVisibleChange = (newVisible: boolean) => {
if (newVisible && propFilteredKeys.value !== undefined) {
// Sync filteredKeys on appear in controlled mode (propFilteredKeys.value !== undefiend)
filteredKeys.value = propFilteredKeys.value || [];
}
triggerVisible(newVisible);
// Default will filter when closed
if (!newVisible && !filterDropdownRef.value) {
onConfirm();
}
};
const { direction } = useConfigInject('', props);
return () => {
const {
tablePrefixCls,
prefixCls,
column,
dropdownPrefixCls,
filterMultiple,
locale,
getPopupContainer,
} = props;
// ======================== Style ========================
const dropdownMenuClass = classNames({
[`${dropdownPrefixCls}-menu-without-submenu`]: !hasSubMenu(column.filters || []),
});
let dropdownContent;
if (typeof filterDropdownRef.value === 'function') {
dropdownContent = filterDropdownRef.value({
prefixCls: `${dropdownPrefixCls}-custom`,
setSelectedKeys: (selectedKeys: Key[]) => onSelectKeys({ selectedKeys }),
selectedKeys: filteredKeys.value,
confirm: doFilter,
clearFilters: onReset,
filters: column.filters,
visible: mergedVisible.value,
column: column.__originColumn__,
});
} else if (filterDropdownRef.value) {
dropdownContent = filterDropdownRef.value;
} else {
const selectedKeys = filteredKeys.value as any;
dropdownContent = (
<>
<Menu
multiple={filterMultiple}
prefixCls={`${dropdownPrefixCls}-menu`}
class={dropdownMenuClass}
onClick={onMenuClick}
onSelect={onSelectKeys}
onDeselect={onSelectKeys}
selectedKeys={selectedKeys}
getPopupContainer={getPopupContainer}
openKeys={openKeys.value}
onOpenChange={onOpenChange}
v-slots={{
default: () =>
renderFilterItems({
filters: column.filters || [],
prefixCls,
filteredKeys: filteredKeys.value,
filterMultiple,
locale,
}),
}}
></Menu>
<div class={`${prefixCls}-dropdown-btns`}>
<Button
type="link"
size="small"
disabled={selectedKeys.length === 0}
onClick={onReset}
>
{locale.filterReset}
</Button>
<Button type="primary" size="small" onClick={onConfirm}>
{locale.filterConfirm}
</Button>
</div>
</>
);
}
const menu = (
<FilterDropdownMenuWrapper class={`${prefixCls}-dropdown`}>
{dropdownContent}
</FilterDropdownMenuWrapper>
);
let filterIcon;
if (typeof filterIconRef.value === 'function') {
filterIcon = filterIconRef.value({
filtered: filtered.value,
column: column.__originColumn__,
});
} else if (filterIconRef.value) {
filterIcon = filterIconRef.value;
} else {
filterIcon = <FilterFilled />;
}
return (
<div class={`${prefixCls}-column`}>
<span class={`${tablePrefixCls}-column-title`}>{slots.default?.()}</span>
<Dropdown
overlay={menu}
trigger={['click']}
visible={mergedVisible.value}
onVisibleChange={onVisibleChange}
getPopupContainer={getPopupContainer}
placement={direction.value === 'rtl' ? 'bottomLeft' : 'bottomRight'}
>
<span
role="button"
tabindex={-1}
class={classNames(`${prefixCls}-trigger`, {
active: filtered.value,
})}
onClick={e => {
e.stopPropagation();
}}
>
{filterIcon}
</span>
</Dropdown>
</div>
);
};
},
});

View File

@ -0,0 +1,5 @@
const FilterDropdownMenuWrapper = (_props, { slots }) => (
<div onClick={e => e.stopPropagation()}>{slots.default?.()}</div>
);
export default FilterDropdownMenuWrapper;

View File

@ -0,0 +1,264 @@
import type { DefaultRecordType } from '../../../vc-table/interface';
import devWarning from '../../../vc-util/devWarning';
import useState from '../../../_util/hooks/useState';
import type { Ref } from 'vue';
import { computed } from 'vue';
import type {
TransformColumns,
ColumnsType,
ColumnType,
ColumnTitleProps,
Key,
TableLocale,
FilterValue,
FilterKey,
GetPopupContainer,
ColumnFilterItem,
} from '../../interface';
import { getColumnPos, renderColumnTitle, getColumnKey } from '../../util';
import FilterDropdown from './FilterDropdown';
export interface FilterState<RecordType = DefaultRecordType> {
column: ColumnType<RecordType>;
key: Key;
filteredKeys?: FilterKey;
forceFiltered?: boolean;
}
function collectFilterStates<RecordType>(
columns: ColumnsType<RecordType>,
init: boolean,
pos?: string,
): FilterState<RecordType>[] {
let filterStates: FilterState<RecordType>[] = [];
(columns || []).forEach((column, index) => {
const columnPos = getColumnPos(index, pos);
const hasFilterDropdown =
column.filterDropdown || column?.slots?.filterDropdown || column.customFilterDropdown;
if (column.filters || hasFilterDropdown || 'onFilter' in column) {
if ('filteredValue' in column) {
// Controlled
let filteredValues = column.filteredValue;
if (!hasFilterDropdown) {
filteredValues = filteredValues?.map(String) ?? filteredValues;
}
filterStates.push({
column,
key: getColumnKey(column, columnPos),
filteredKeys: filteredValues as FilterKey,
forceFiltered: column.filtered,
});
} else {
// Uncontrolled
filterStates.push({
column,
key: getColumnKey(column, columnPos),
filteredKeys: (init && column.defaultFilteredValue
? column.defaultFilteredValue!
: undefined) as FilterKey,
forceFiltered: column.filtered,
});
}
}
if ('children' in column) {
filterStates = [...filterStates, ...collectFilterStates(column.children, init, columnPos)];
}
});
return filterStates;
}
function injectFilter<RecordType>(
prefixCls: string,
dropdownPrefixCls: string,
columns: ColumnsType<RecordType>,
filterStates: FilterState<RecordType>[],
triggerFilter: (filterState: FilterState<RecordType>) => void,
getPopupContainer: GetPopupContainer | undefined,
locale: TableLocale,
pos?: string,
): ColumnsType<RecordType> {
return columns.map((column, index) => {
const columnPos = getColumnPos(index, pos);
const { filterMultiple = true } = column as ColumnType<RecordType>;
let newColumn: ColumnsType<RecordType>[number] = column;
const hasFilterDropdown =
column.filterDropdown || column?.slots?.filterDropdown || column.customFilterDropdown;
if (newColumn.filters || hasFilterDropdown) {
const columnKey = getColumnKey(newColumn, columnPos);
const filterState = filterStates.find(({ key }) => columnKey === key);
newColumn = {
...newColumn,
title: (renderProps: ColumnTitleProps<RecordType>) => (
<FilterDropdown
tablePrefixCls={prefixCls}
prefixCls={`${prefixCls}-filter`}
dropdownPrefixCls={dropdownPrefixCls}
column={newColumn}
columnKey={columnKey}
filterState={filterState}
filterMultiple={filterMultiple}
triggerFilter={triggerFilter}
locale={locale}
getPopupContainer={getPopupContainer}
>
{renderColumnTitle(column.title, renderProps)}
</FilterDropdown>
),
};
}
if ('children' in newColumn) {
newColumn = {
...newColumn,
children: injectFilter(
prefixCls,
dropdownPrefixCls,
newColumn.children,
filterStates,
triggerFilter,
getPopupContainer,
locale,
columnPos,
),
};
}
return newColumn;
});
}
function flattenKeys(filters?: ColumnFilterItem[]) {
let keys: FilterValue = [];
(filters || []).forEach(({ value, children }) => {
keys.push(value);
if (children) {
keys = [...keys, ...flattenKeys(children)];
}
});
return keys;
}
function generateFilterInfo<RecordType>(filterStates: FilterState<RecordType>[]) {
const currentFilters: Record<string, FilterValue | null> = {};
filterStates.forEach(({ key, filteredKeys, column }) => {
const hasFilterDropdown =
column.filterDropdown || column?.slots?.filterDropdown || column.customFilterDropdown;
const { filters } = column;
if (hasFilterDropdown) {
currentFilters[key] = filteredKeys || null;
} else if (Array.isArray(filteredKeys)) {
const keys = flattenKeys(filters);
currentFilters[key] = keys.filter(originKey => filteredKeys.includes(String(originKey)));
} else {
currentFilters[key] = null;
}
});
return currentFilters;
}
export function getFilterData<RecordType>(
data: RecordType[],
filterStates: FilterState<RecordType>[],
) {
return filterStates.reduce((currentData, filterState) => {
const {
column: { onFilter, filters },
filteredKeys,
} = filterState;
if (onFilter && filteredKeys && filteredKeys.length) {
return currentData.filter(record =>
filteredKeys.some(key => {
const keys = flattenKeys(filters);
const keyIndex = keys.findIndex(k => String(k) === String(key));
const realKey = keyIndex !== -1 ? keys[keyIndex] : key;
return onFilter(realKey, record);
}),
);
}
return currentData;
}, data);
}
interface FilterConfig<RecordType> {
prefixCls: Ref<string>;
dropdownPrefixCls: Ref<string>;
mergedColumns: Ref<ColumnsType<RecordType>>;
locale: Ref<TableLocale>;
onFilterChange: (
filters: Record<string, FilterValue | null>,
filterStates: FilterState<RecordType>[],
) => void;
getPopupContainer?: Ref<GetPopupContainer>;
}
function useFilter<RecordType>({
prefixCls,
dropdownPrefixCls,
mergedColumns,
locale,
onFilterChange,
getPopupContainer,
}: FilterConfig<RecordType>): [
TransformColumns<RecordType>,
Ref<FilterState<RecordType>[]>,
Ref<Record<string, FilterValue | null>>,
] {
const [filterStates, setFilterStates] = useState<FilterState<RecordType>[]>(
collectFilterStates(mergedColumns.value, true),
);
const mergedFilterStates = computed(() => {
const collectedStates = collectFilterStates(mergedColumns.value, false);
const filteredKeysIsNotControlled = collectedStates.every(
({ filteredKeys }) => filteredKeys === undefined,
);
// Return if not controlled
if (filteredKeysIsNotControlled) {
return filterStates.value;
}
const filteredKeysIsAllControlled = collectedStates.every(
({ filteredKeys }) => filteredKeys !== undefined,
);
devWarning(
filteredKeysIsNotControlled || filteredKeysIsAllControlled,
'Table',
'`FilteredKeys` should all be controlled or not controlled.',
);
return collectedStates;
});
const filters = computed(() => generateFilterInfo(mergedFilterStates.value));
const triggerFilter = (filterState: FilterState<RecordType>) => {
const newFilterStates = mergedFilterStates.value.filter(({ key }) => key !== filterState.key);
newFilterStates.push(filterState);
setFilterStates(newFilterStates);
onFilterChange(generateFilterInfo(newFilterStates), newFilterStates);
};
const transformColumns = (innerColumns: ColumnsType<RecordType>) => {
return injectFilter(
prefixCls.value,
dropdownPrefixCls.value,
innerColumns,
mergedFilterStates.value,
triggerFilter,
getPopupContainer.value,
locale.value,
);
};
return [transformColumns, mergedFilterStates, filters];
}
export default useFilter;

View File

@ -0,0 +1,52 @@
import type { Ref } from 'vue';
import { watch } from 'vue';
import { ref } from 'vue';
import type { Key, GetRowKey } from '../interface';
interface MapCache<RecordType> {
kvMap?: Map<Key, RecordType>;
}
export default function useLazyKVMap<RecordType>(
dataRef: Ref<readonly RecordType[]>,
childrenColumnNameRef: Ref<string>,
getRowKeyRef: Ref<GetRowKey<RecordType>>,
) {
const mapCacheRef = ref<MapCache<RecordType>>({});
watch(
[dataRef, childrenColumnNameRef, getRowKeyRef],
() => {
const kvMap = new Map<Key, RecordType>();
const getRowKey = getRowKeyRef.value;
const childrenColumnName = childrenColumnNameRef.value;
/* eslint-disable no-inner-declarations */
function dig(records: readonly RecordType[]) {
records.forEach((record, index) => {
const rowKey = getRowKey(record, index);
kvMap.set(rowKey, record);
if (record && typeof record === 'object' && childrenColumnName in record) {
dig((record as any)[childrenColumnName] || []);
}
});
}
/* eslint-enable */
dig(dataRef.value);
mapCacheRef.value = {
kvMap,
};
},
{
deep: false,
immediate: true,
},
);
function getRecordByKey(key: Key): RecordType {
return mapCacheRef.value.kvMap!.get(key)!;
}
return [getRecordByKey];
}

View File

@ -0,0 +1,107 @@
import useState from '../../_util/hooks/useState';
import type { Ref } from 'vue';
import { computed } from 'vue';
import type { PaginationProps } from '../../pagination';
import type { TablePaginationConfig } from '../interface';
export const DEFAULT_PAGE_SIZE = 10;
export function getPaginationParam(
pagination: TablePaginationConfig | boolean | undefined,
mergedPagination: TablePaginationConfig,
) {
const param: any = {
current: mergedPagination.current,
pageSize: mergedPagination.pageSize,
};
const paginationObj = pagination && typeof pagination === 'object' ? pagination : {};
Object.keys(paginationObj).forEach(pageProp => {
const value = (mergedPagination as any)[pageProp];
if (typeof value !== 'function') {
param[pageProp] = value;
}
});
return param;
}
function extendsObject<T extends Object>(...list: T[]) {
const result: T = {} as T;
list.forEach(obj => {
if (obj) {
Object.keys(obj).forEach(key => {
const val = (obj as any)[key];
if (val !== undefined) {
(result as any)[key] = val;
}
});
}
});
return result;
}
export default function usePagination(
totalRef: Ref<number>,
paginationRef: Ref<TablePaginationConfig | false | undefined>,
onChange: (current: number, pageSize: number) => void,
): [Ref<TablePaginationConfig>, () => void] {
const pagination = computed(() =>
paginationRef.value && typeof paginationRef.value === 'object' ? paginationRef.value : {},
);
const paginationTotal = computed(() => pagination.value.total || 0);
const [innerPagination, setInnerPagination] = useState<{
current?: number;
pageSize?: number;
}>(() => ({
current: 'defaultCurrent' in pagination.value ? pagination.value.defaultCurrent : 1,
pageSize:
'defaultPageSize' in pagination.value ? pagination.value.defaultPageSize : DEFAULT_PAGE_SIZE,
}));
// ============ Basic Pagination Config ============
const mergedPagination = computed(() =>
extendsObject<Partial<TablePaginationConfig>>(innerPagination.value, pagination.value, {
total: paginationTotal.value > 0 ? paginationTotal.value : totalRef.value,
}),
);
// Reset `current` if data length or pageSize changed
const maxPage = Math.ceil(
(paginationTotal.value || totalRef.value) / mergedPagination.value.pageSize!,
);
if (mergedPagination.value.current! > maxPage) {
// Prevent a maximum page count of 0
mergedPagination.value.current = maxPage || 1;
}
const refreshPagination = (current = 1, pageSize?: number) => {
setInnerPagination({
current,
pageSize: pageSize || mergedPagination.value.pageSize,
});
};
const onInternalChange: PaginationProps['onChange'] = (current, pageSize) => {
if (pagination.value) {
pagination.value.onChange?.(current, pageSize);
}
refreshPagination(current, pageSize);
onChange(current, pageSize || mergedPagination.value.pageSize);
};
if (pagination.value === false) {
return [computed(() => ({})), () => {}];
}
return [
computed(() => ({
...mergedPagination.value,
onChange: onInternalChange,
})),
refreshPagination,
];
}

View File

@ -0,0 +1,611 @@
import DownOutlined from '@ant-design/icons-vue/DownOutlined';
import type { DataNode } from '../../tree';
import { INTERNAL_COL_DEFINE } from '../../vc-table';
import type { ColumnType, FixedType } from '../../vc-table/interface';
import type { GetCheckDisabled } from '../../vc-tree/interface';
import { arrAdd, arrDel } from '../../vc-tree/util';
import { conductCheck } from '../../vc-tree/utils/conductUtil';
import { convertDataToEntities } from '../../vc-tree/utils/treeUtil';
import devWarning from '../../vc-util/devWarning';
import useMergedState from '../../_util/hooks/useMergedState';
import useState from '../../_util/hooks/useState';
import type { Ref } from 'vue';
import { computed, ref, watchEffect } from 'vue';
import type { CheckboxProps } from '../../checkbox';
import Checkbox from '../../checkbox';
import Dropdown from '../../dropdown';
import Menu from '../../menu';
import Radio from '../../radio';
import type {
TableRowSelection,
Key,
ColumnsType,
GetRowKey,
TableLocale,
SelectionItem,
TransformColumns,
ExpandType,
GetPopupContainer,
} from '../interface';
// TODO: warning if use ajax!!!
export const SELECTION_ALL = 'SELECT_ALL' as const;
export const SELECTION_INVERT = 'SELECT_INVERT' as const;
export const SELECTION_NONE = 'SELECT_NONE' as const;
function getFixedType<RecordType>(column: ColumnsType<RecordType>[number]): FixedType | undefined {
return (column && column.fixed) as FixedType;
}
interface UseSelectionConfig<RecordType> {
prefixCls: Ref<string>;
pageData: Ref<RecordType[]>;
data: Ref<RecordType[]>;
getRowKey: Ref<GetRowKey<RecordType>>;
getRecordByKey: (key: Key) => RecordType;
expandType: Ref<ExpandType>;
childrenColumnName: Ref<string>;
expandIconColumnIndex?: Ref<number>;
locale: Ref<TableLocale>;
getPopupContainer?: Ref<GetPopupContainer>;
}
export type INTERNAL_SELECTION_ITEM =
| SelectionItem
| typeof SELECTION_ALL
| typeof SELECTION_INVERT
| typeof SELECTION_NONE;
function flattenData<RecordType>(
data: RecordType[] | undefined,
childrenColumnName: string,
): RecordType[] {
let list: RecordType[] = [];
(data || []).forEach(record => {
list.push(record);
if (record && typeof record === 'object' && childrenColumnName in record) {
list = [
...list,
...flattenData<RecordType>((record as any)[childrenColumnName], childrenColumnName),
];
}
});
return list;
}
export default function useSelection<RecordType>(
rowSelectionRef: Ref<TableRowSelection<RecordType> | undefined>,
configRef: UseSelectionConfig<RecordType>,
): [TransformColumns<RecordType>, Ref<Set<Key>>] {
// ======================== Caches ========================
const preserveRecordsRef = ref(new Map<Key, RecordType>());
const mergedRowSelection = computed(() => {
const temp = rowSelectionRef.value || {};
const { checkStrictly = true } = temp;
return { ...temp, checkStrictly };
});
// ========================= Keys =========================
const [mergedSelectedKeys, setMergedSelectedKeys] = useMergedState(
mergedRowSelection.value.selectedRowKeys ||
mergedRowSelection.value.defaultSelectedRowKeys ||
[],
{
value: computed(() => mergedRowSelection.value.selectedRowKeys),
},
);
const keyEntities = computed(() =>
mergedRowSelection.value.checkStrictly
? { keyEntities: null }
: convertDataToEntities(configRef.data.value as unknown as DataNode[], {
externalGetKey: configRef.getRowKey.value as any,
childrenPropName: configRef.childrenColumnName.value,
}).keyEntities,
);
// Get flatten data
const flattedData = computed(() =>
flattenData(configRef.pageData.value, configRef.childrenColumnName.value),
);
// Get all checkbox props
const checkboxPropsMap = computed(() => {
const map = new Map<Key, Partial<CheckboxProps>>();
const getRowKey = configRef.getRowKey.value;
const getCheckboxProps = mergedRowSelection.value.getCheckboxProps;
flattedData.value.forEach((record, index) => {
const key = getRowKey(record, index);
const checkboxProps = (getCheckboxProps ? getCheckboxProps(record) : null) || {};
map.set(key, checkboxProps);
if (
process.env.NODE_ENV !== 'production' &&
('checked' in checkboxProps || 'defaultChecked' in checkboxProps)
) {
devWarning(
false,
'Table',
'Do not set `checked` or `defaultChecked` in `getCheckboxProps`. Please use `selectedRowKeys` instead.',
);
}
});
return map;
});
const isCheckboxDisabled: GetCheckDisabled<RecordType> = (r: RecordType) =>
!!checkboxPropsMap.value.get(configRef.getRowKey.value(r))?.disabled;
const selectKeysState = computed(() => {
if (mergedRowSelection.value.checkStrictly) {
return [mergedSelectedKeys.value || [], []];
}
const { checkedKeys, halfCheckedKeys } = conductCheck(
mergedSelectedKeys.value,
true,
keyEntities.value,
isCheckboxDisabled as any,
);
return [checkedKeys || [], halfCheckedKeys];
});
const derivedSelectedKeys = computed(() => selectKeysState.value[0]);
const derivedHalfSelectedKeys = computed(() => selectKeysState.value[1]);
const derivedSelectedKeySet = computed<Set<Key>>(() => {
const keys =
mergedRowSelection.value.type === 'radio'
? derivedSelectedKeys.value.slice(0, 1)
: derivedSelectedKeys.value;
return new Set(keys);
});
const derivedHalfSelectedKeySet = computed(() =>
mergedRowSelection.value.type === 'radio' ? new Set() : new Set(derivedHalfSelectedKeys.value),
);
// Save last selected key to enable range selection
const [lastSelectedKey, setLastSelectedKey] = useState<Key | null>(null);
// Reset if rowSelection reset
watchEffect(() => {
if (!rowSelectionRef.value) {
setMergedSelectedKeys([]);
}
});
const setSelectedKeys = (keys: Key[]) => {
let availableKeys: Key[];
let records: RecordType[];
const { preserveSelectedRowKeys, onChange: onSelectionChange } = mergedRowSelection.value;
const { getRecordByKey } = configRef;
if (preserveSelectedRowKeys) {
// Keep key if mark as preserveSelectedRowKeys
const newCache = new Map<Key, RecordType>();
availableKeys = keys;
records = keys.map(key => {
let record = getRecordByKey(key);
if (!record && preserveRecordsRef.value.has(key)) {
record = preserveRecordsRef.value.get(key)!;
}
newCache.set(key, record);
return record;
});
// Refresh to new cache
preserveRecordsRef.value = newCache;
} else {
// Filter key which not exist in the `dataSource`
availableKeys = [];
records = [];
keys.forEach(key => {
const record = getRecordByKey(key);
if (record !== undefined) {
availableKeys.push(key);
records.push(record);
}
});
}
setMergedSelectedKeys(availableKeys);
onSelectionChange?.(availableKeys, records);
};
// ====================== Selections ======================
// Trigger single `onSelect` event
const triggerSingleSelection = (key: Key, selected: boolean, keys: Key[], event: Event) => {
const { onSelect } = mergedRowSelection.value;
const { getRecordByKey } = configRef || {};
if (onSelect) {
const rows = keys.map(k => getRecordByKey(k));
onSelect(getRecordByKey(key), selected, rows, event);
}
setSelectedKeys(keys);
};
const mergedSelections = computed(() => {
const { onSelectInvert, onSelectNone, selections, hideSelectAll } = mergedRowSelection.value;
const { data, pageData, getRowKey, locale: tableLocale } = configRef;
if (!selections || hideSelectAll) {
return null;
}
const selectionList: INTERNAL_SELECTION_ITEM[] =
selections === true ? [SELECTION_ALL, SELECTION_INVERT, SELECTION_NONE] : selections;
return selectionList.map((selection: INTERNAL_SELECTION_ITEM) => {
if (selection === SELECTION_ALL) {
return {
key: 'all',
text: tableLocale.value.selectionAll,
onSelect() {
setSelectedKeys(data.value.map((record, index) => getRowKey.value(record, index)));
},
};
}
if (selection === SELECTION_INVERT) {
return {
key: 'invert',
text: tableLocale.value.selectInvert,
onSelect() {
const keySet = new Set(derivedSelectedKeySet.value);
pageData.value.forEach((record, index) => {
const key = getRowKey.value(record, index);
if (keySet.has(key)) {
keySet.delete(key);
} else {
keySet.add(key);
}
});
const keys = Array.from(keySet);
if (onSelectInvert) {
devWarning(
false,
'Table',
'`onSelectInvert` will be removed in future. Please use `onChange` instead.',
);
onSelectInvert(keys);
}
setSelectedKeys(keys);
},
};
}
if (selection === SELECTION_NONE) {
return {
key: 'none',
text: tableLocale.value.selectNone,
onSelect() {
onSelectNone?.();
setSelectedKeys([]);
},
};
}
return selection as SelectionItem;
});
});
const flattedDataLength = computed(() => flattedData.value.length);
// ======================= Columns ========================
const transformColumns = (columns: ColumnsType<RecordType>): ColumnsType<RecordType> => {
const {
onSelectAll,
onSelectMultiple,
columnWidth: selectionColWidth,
type: selectionType,
fixed,
renderCell: customizeRenderCell,
hideSelectAll,
checkStrictly,
} = mergedRowSelection.value;
const {
prefixCls,
getRecordByKey,
getRowKey,
expandType,
expandIconColumnIndex,
getPopupContainer,
} = configRef;
if (!rowSelectionRef.value) {
return columns;
}
// Support selection
const keySet = new Set(derivedSelectedKeySet.value);
// Record key only need check with enabled
const recordKeys = flattedData.value
.map(getRowKey.value)
.filter(key => !checkboxPropsMap.value.get(key)!.disabled);
const checkedCurrentAll = recordKeys.every(key => keySet.has(key));
const checkedCurrentSome = recordKeys.some(key => keySet.has(key));
const onSelectAllChange = () => {
const changeKeys: Key[] = [];
if (checkedCurrentAll) {
recordKeys.forEach(key => {
keySet.delete(key);
changeKeys.push(key);
});
} else {
recordKeys.forEach(key => {
if (!keySet.has(key)) {
keySet.add(key);
changeKeys.push(key);
}
});
}
const keys = Array.from(keySet);
onSelectAll?.(
!checkedCurrentAll,
keys.map(k => getRecordByKey(k)),
changeKeys.map(k => getRecordByKey(k)),
);
setSelectedKeys(keys);
};
// ===================== Render =====================
// Title Cell
let title;
if (selectionType !== 'radio') {
let customizeSelections;
if (mergedSelections.value) {
const menu = (
<Menu getPopupContainer={getPopupContainer.value}>
{mergedSelections.value.map((selection, index) => {
const { key, text, onSelect: onSelectionClick } = selection;
return (
<Menu.Item
key={key || index}
onClick={() => {
onSelectionClick?.(recordKeys);
}}
>
{text}
</Menu.Item>
);
})}
</Menu>
);
customizeSelections = (
<div class={`${prefixCls.value}-selection-extra`}>
<Dropdown overlay={menu} getPopupContainer={getPopupContainer.value}>
<span>
<DownOutlined />
</span>
</Dropdown>
</div>
);
}
const allDisabledData = flattedData.value
.map((record, index) => {
const key = getRowKey.value(record, index);
const checkboxProps = checkboxPropsMap.value.get(key) || {};
return { checked: keySet.has(key), ...checkboxProps };
})
.filter(({ disabled }) => disabled);
const allDisabled =
!!allDisabledData.length && allDisabledData.length === flattedDataLength.value;
const allDisabledAndChecked = allDisabled && allDisabledData.every(({ checked }) => checked);
const allDisabledSomeChecked = allDisabled && allDisabledData.some(({ checked }) => checked);
title = !hideSelectAll && (
<div class={`${prefixCls.value}-selection`}>
<Checkbox
checked={
!allDisabled ? !!flattedDataLength.value && checkedCurrentAll : allDisabledAndChecked
}
indeterminate={
!allDisabled
? !checkedCurrentAll && checkedCurrentSome
: !allDisabledAndChecked && allDisabledSomeChecked
}
onChange={onSelectAllChange}
disabled={flattedDataLength.value === 0 || allDisabled}
skipGroup
/>
{customizeSelections}
</div>
);
}
// Body Cell
let renderCell: ({ record, index }: { record: RecordType; index: number }) => {
node: any;
checked: boolean;
};
if (selectionType === 'radio') {
renderCell = ({ record, index }) => {
const key = getRowKey.value(record, index);
const checked = keySet.has(key);
return {
node: (
<Radio
{...checkboxPropsMap.value.get(key)}
checked={checked}
onClick={e => e.stopPropagation()}
onChange={event => {
if (!keySet.has(key)) {
triggerSingleSelection(key, true, [key], event.nativeEvent);
}
}}
/>
),
checked,
};
};
} else {
renderCell = ({ record, index }) => {
const key = getRowKey.value(record, index);
const checked = keySet.has(key);
const indeterminate = derivedHalfSelectedKeySet.value.has(key);
const checkboxProps = checkboxPropsMap.value.get(key);
let mergedIndeterminate: boolean;
if (expandType.value === 'nest') {
mergedIndeterminate = indeterminate;
devWarning(
typeof checkboxProps?.indeterminate !== 'boolean',
'Table',
'set `indeterminate` using `rowSelection.getCheckboxProps` is not allowed with tree structured dataSource.',
);
} else {
mergedIndeterminate = checkboxProps?.indeterminate ?? indeterminate;
}
// Record checked
return {
node: (
<Checkbox
{...checkboxProps}
indeterminate={mergedIndeterminate}
checked={checked}
skipGroup
onClick={e => e.stopPropagation()}
onChange={({ nativeEvent }) => {
const { shiftKey } = nativeEvent;
let startIndex = -1;
let endIndex = -1;
// Get range of this
if (shiftKey && checkStrictly) {
const pointKeys = new Set([lastSelectedKey.value, key]);
recordKeys.some((recordKey, recordIndex) => {
if (pointKeys.has(recordKey)) {
if (startIndex === -1) {
startIndex = recordIndex;
} else {
endIndex = recordIndex;
return true;
}
}
return false;
});
}
if (endIndex !== -1 && startIndex !== endIndex && checkStrictly) {
// Batch update selections
const rangeKeys = recordKeys.slice(startIndex, endIndex + 1);
const changedKeys: Key[] = [];
if (checked) {
rangeKeys.forEach(recordKey => {
if (keySet.has(recordKey)) {
changedKeys.push(recordKey);
keySet.delete(recordKey);
}
});
} else {
rangeKeys.forEach(recordKey => {
if (!keySet.has(recordKey)) {
changedKeys.push(recordKey);
keySet.add(recordKey);
}
});
}
const keys = Array.from(keySet);
onSelectMultiple?.(
!checked,
keys.map(recordKey => getRecordByKey(recordKey)),
changedKeys.map(recordKey => getRecordByKey(recordKey)),
);
setSelectedKeys(keys);
} else {
// Single record selected
const originCheckedKeys = derivedSelectedKeys.value;
if (checkStrictly) {
const checkedKeys = checked
? arrDel(originCheckedKeys, key)
: arrAdd(originCheckedKeys, key);
triggerSingleSelection(key, !checked, checkedKeys, nativeEvent);
} else {
// Always fill first
const result = conductCheck(
[...originCheckedKeys, key],
true,
keyEntities.value,
isCheckboxDisabled as any,
);
const { checkedKeys, halfCheckedKeys } = result;
let nextCheckedKeys = checkedKeys;
// If remove, we do it again to correction
if (checked) {
const tempKeySet = new Set(checkedKeys);
tempKeySet.delete(key);
nextCheckedKeys = conductCheck(
Array.from(tempKeySet),
{ checked: false, halfCheckedKeys },
keyEntities.value,
isCheckboxDisabled as any,
).checkedKeys;
}
triggerSingleSelection(key, !checked, nextCheckedKeys, nativeEvent);
}
}
setLastSelectedKey(key);
}}
/>
),
checked,
};
};
}
const renderSelectionCell: ColumnType<RecordType>['customRender'] = ({ record, index }) => {
const { node, checked } = renderCell({ record, index });
if (customizeRenderCell) {
return customizeRenderCell(checked, record, index, node);
}
return node;
};
// Columns
const selectionColumn = {
width: selectionColWidth,
className: `${prefixCls.value}-selection-column`,
title: mergedRowSelection.value.columnTitle || title,
customRender: renderSelectionCell,
[INTERNAL_COL_DEFINE]: {
class: `${prefixCls.value}-selection-col`,
},
};
if (expandType.value === 'row' && columns.length && !expandIconColumnIndex.value) {
const [expandColumn, ...restColumns] = columns;
const selectionFixed = fixed || getFixedType(restColumns[0]);
if (selectionFixed) {
expandColumn.fixed = selectionFixed;
}
return [expandColumn, { ...selectionColumn, fixed: selectionFixed }, ...restColumns];
}
return [{ ...selectionColumn, fixed: fixed || getFixedType(columns[0]) }, ...columns];
};
return [transformColumns, derivedSelectedKeySet];
}

View File

@ -0,0 +1,428 @@
import CaretDownOutlined from '@ant-design/icons-vue/CaretDownOutlined';
import CaretUpOutlined from '@ant-design/icons-vue/CaretUpOutlined';
import type {
TransformColumns,
ColumnsType,
Key,
ColumnType,
SortOrder,
CompareFn,
ColumnTitleProps,
SorterResult,
ColumnGroupType,
TableLocale,
} from '../interface';
import type { TooltipProps } from '../../tooltip';
import Tooltip from '../../tooltip';
import { getColumnKey, getColumnPos, renderColumnTitle } from '../util';
import classNames from '../../_util/classNames';
import type { Ref } from 'vue';
import { computed } from 'vue';
import useState from '../../_util/hooks/useState';
import type { DefaultRecordType } from '../../vc-table/interface';
const ASCEND = 'ascend';
const DESCEND = 'descend';
function getMultiplePriority<RecordType>(column: ColumnType<RecordType>): number | false {
if (typeof column.sorter === 'object' && typeof column.sorter.multiple === 'number') {
return column.sorter.multiple;
}
return false;
}
function getSortFunction<RecordType>(
sorter: ColumnType<RecordType>['sorter'],
): CompareFn<RecordType> | false {
if (typeof sorter === 'function') {
return sorter;
}
if (sorter && typeof sorter === 'object' && sorter.compare) {
return sorter.compare;
}
return false;
}
function nextSortDirection(sortDirections: SortOrder[], current: SortOrder | null) {
if (!current) {
return sortDirections[0];
}
return sortDirections[sortDirections.indexOf(current) + 1];
}
export interface SortState<RecordType = DefaultRecordType> {
column: ColumnType<RecordType>;
key: Key;
sortOrder: SortOrder | null;
multiplePriority: number | false;
}
function collectSortStates<RecordType>(
columns: ColumnsType<RecordType>,
init: boolean,
pos?: string,
): SortState<RecordType>[] {
let sortStates: SortState<RecordType>[] = [];
function pushState(column: ColumnsType<RecordType>[number], columnPos: string) {
sortStates.push({
column,
key: getColumnKey(column, columnPos),
multiplePriority: getMultiplePriority(column),
sortOrder: column.sortOrder!,
});
}
(columns || []).forEach((column, index) => {
const columnPos = getColumnPos(index, pos);
if ((column as ColumnGroupType<RecordType>).children) {
if ('sortOrder' in column) {
// Controlled
pushState(column, columnPos);
}
sortStates = [
...sortStates,
...collectSortStates((column as ColumnGroupType<RecordType>).children, init, columnPos),
];
} else if (column.sorter) {
if ('sortOrder' in column) {
// Controlled
pushState(column, columnPos);
} else if (init && column.defaultSortOrder) {
// Default sorter
sortStates.push({
column,
key: getColumnKey(column, columnPos),
multiplePriority: getMultiplePriority(column),
sortOrder: column.defaultSortOrder!,
});
}
}
});
return sortStates;
}
function injectSorter<RecordType>(
prefixCls: string,
columns: ColumnsType<RecordType>,
sorterSates: SortState<RecordType>[],
triggerSorter: (sorterSates: SortState<RecordType>) => void,
defaultSortDirections: SortOrder[],
tableLocale?: TableLocale,
tableShowSorterTooltip?: boolean | TooltipProps,
pos?: string,
): ColumnsType<RecordType> {
return (columns || []).map((column, index) => {
const columnPos = getColumnPos(index, pos);
let newColumn: ColumnsType<RecordType>[number] = column;
if (newColumn.sorter) {
const sortDirections: SortOrder[] = newColumn.sortDirections || defaultSortDirections;
const showSorterTooltip =
newColumn.showSorterTooltip === undefined
? tableShowSorterTooltip
: newColumn.showSorterTooltip;
const columnKey = getColumnKey(newColumn, columnPos);
const sorterState = sorterSates.find(({ key }) => key === columnKey);
const sorterOrder = sorterState ? sorterState.sortOrder : null;
const nextSortOrder = nextSortDirection(sortDirections, sorterOrder);
const upNode = sortDirections.includes(ASCEND) && (
<CaretUpOutlined
class={classNames(`${prefixCls}-column-sorter-up`, {
active: sorterOrder === ASCEND,
})}
/>
);
const downNode = sortDirections.includes(DESCEND) && (
<CaretDownOutlined
class={classNames(`${prefixCls}-column-sorter-down`, {
active: sorterOrder === DESCEND,
})}
/>
);
const { cancelSort, triggerAsc, triggerDesc } = tableLocale || {};
let sortTip: string | undefined = cancelSort;
if (nextSortOrder === DESCEND) {
sortTip = triggerDesc;
} else if (nextSortOrder === ASCEND) {
sortTip = triggerAsc;
}
const tooltipProps: TooltipProps =
typeof showSorterTooltip === 'object' ? showSorterTooltip : { title: sortTip };
newColumn = {
...newColumn,
className: classNames(newColumn.className, { [`${prefixCls}-column-sort`]: sorterOrder }),
title: (renderProps: ColumnTitleProps<RecordType>) => {
const renderSortTitle = (
<div class={`${prefixCls}-column-sorters`}>
<span class={`${prefixCls}-column-title`}>
{renderColumnTitle(column.title, renderProps)}
</span>
<span
class={classNames(`${prefixCls}-column-sorter`, {
[`${prefixCls}-column-sorter-full`]: !!(upNode && downNode),
})}
>
<span class={`${prefixCls}-column-sorter-inner`}>
{upNode}
{downNode}
</span>
</span>
</div>
);
return showSorterTooltip ? (
<Tooltip {...tooltipProps}>{renderSortTitle}</Tooltip>
) : (
renderSortTitle
);
},
customHeaderCell: col => {
const cell = (column.customHeaderCell && column.customHeaderCell(col)) || {};
const originOnClick = cell.onClick;
cell.onClick = (event: MouseEvent) => {
triggerSorter({
column,
key: columnKey,
sortOrder: nextSortOrder,
multiplePriority: getMultiplePriority(column),
});
if (originOnClick) {
originOnClick(event);
}
};
cell.class = classNames(cell.class, `${prefixCls}-column-has-sorters`);
return cell;
},
};
}
if ('children' in newColumn) {
newColumn = {
...newColumn,
children: injectSorter(
prefixCls,
newColumn.children,
sorterSates,
triggerSorter,
defaultSortDirections,
tableLocale,
tableShowSorterTooltip,
columnPos,
),
};
}
return newColumn;
});
}
function stateToInfo<RecordType>(sorterStates: SortState<RecordType>) {
const { column, sortOrder } = sorterStates;
return { column, order: sortOrder, field: column.dataIndex, columnKey: column.key };
}
function generateSorterInfo<RecordType>(
sorterStates: SortState<RecordType>[],
): SorterResult<RecordType> | SorterResult<RecordType>[] {
const list = sorterStates.filter(({ sortOrder }) => sortOrder).map(stateToInfo);
// =========== Legacy compatible support ===========
// https://github.com/ant-design/ant-design/pull/19226
if (list.length === 0 && sorterStates.length) {
return {
...stateToInfo(sorterStates[sorterStates.length - 1]),
column: undefined,
};
}
if (list.length <= 1) {
return list[0] || {};
}
return list;
}
export function getSortData<RecordType>(
data: readonly RecordType[],
sortStates: SortState<RecordType>[],
childrenColumnName: string,
): RecordType[] {
const innerSorterStates = sortStates
.slice()
.sort((a, b) => (b.multiplePriority as number) - (a.multiplePriority as number));
const cloneData = data.slice();
const runningSorters = innerSorterStates.filter(
({ column: { sorter }, sortOrder }) => getSortFunction(sorter) && sortOrder,
);
// Skip if no sorter needed
if (!runningSorters.length) {
return cloneData;
}
return cloneData
.sort((record1, record2) => {
for (let i = 0; i < runningSorters.length; i += 1) {
const sorterState = runningSorters[i];
const {
column: { sorter },
sortOrder,
} = sorterState;
const compareFn = getSortFunction(sorter);
if (compareFn && sortOrder) {
const compareResult = compareFn(record1, record2, sortOrder);
if (compareResult !== 0) {
return sortOrder === ASCEND ? compareResult : -compareResult;
}
}
}
return 0;
})
.map<RecordType>(record => {
const subRecords = (record as any)[childrenColumnName];
if (subRecords) {
return {
...record,
[childrenColumnName]: getSortData(subRecords, sortStates, childrenColumnName),
};
}
return record;
});
}
interface SorterConfig<RecordType> {
prefixCls: Ref<string>;
mergedColumns: Ref<ColumnsType<RecordType>>;
onSorterChange: (
sorterResult: SorterResult<RecordType> | SorterResult<RecordType>[],
sortStates: SortState<RecordType>[],
) => void;
sortDirections: Ref<SortOrder[]>;
tableLocale?: Ref<TableLocale>;
showSorterTooltip?: Ref<boolean | TooltipProps>;
}
export default function useFilterSorter<RecordType>({
prefixCls,
mergedColumns,
onSorterChange,
sortDirections,
tableLocale,
showSorterTooltip,
}: SorterConfig<RecordType>): [
TransformColumns<RecordType>,
Ref<SortState<RecordType>[]>,
Ref<ColumnTitleProps<RecordType>>,
Ref<SorterResult<RecordType> | SorterResult<RecordType>[]>,
] {
const [sortStates, setSortStates] = useState<SortState<RecordType>[]>(
collectSortStates(mergedColumns.value, true),
);
const mergedSorterStates = computed(() => {
let validate = true;
const collectedStates = collectSortStates(mergedColumns.value, false);
// Return if not controlled
if (!collectedStates.length) {
return sortStates.value;
}
const validateStates: SortState<RecordType>[] = [];
function patchStates(state: SortState<RecordType>) {
if (validate) {
validateStates.push(state);
} else {
validateStates.push({
...state,
sortOrder: null,
});
}
}
let multipleMode: boolean | null = null;
collectedStates.forEach(state => {
if (multipleMode === null) {
patchStates(state);
if (state.sortOrder) {
if (state.multiplePriority === false) {
validate = false;
} else {
multipleMode = true;
}
}
} else if (multipleMode && state.multiplePriority !== false) {
patchStates(state);
} else {
validate = false;
patchStates(state);
}
});
return validateStates;
});
// Get render columns title required props
const columnTitleSorterProps = computed<ColumnTitleProps<RecordType>>(() => {
const sortColumns = mergedSorterStates.value.map(({ column, sortOrder }) => ({
column,
order: sortOrder,
}));
return {
sortColumns,
// Legacy
sortColumn: sortColumns[0] && sortColumns[0].column,
sortOrder: (sortColumns[0] && sortColumns[0].order) as SortOrder,
};
});
function triggerSorter(sortState: SortState<RecordType>) {
let newSorterStates;
if (
sortState.multiplePriority === false ||
!mergedSorterStates.value.length ||
mergedSorterStates.value[0].multiplePriority === false
) {
newSorterStates = [sortState];
} else {
newSorterStates = [
...mergedSorterStates.value.filter(({ key }) => key !== sortState.key),
sortState,
];
}
setSortStates(newSorterStates);
onSorterChange(generateSorterInfo(newSorterStates), newSorterStates);
}
const transformColumns = (innerColumns: ColumnsType<RecordType>) =>
injectSorter(
prefixCls.value,
innerColumns,
mergedSorterStates.value,
triggerSorter,
sortDirections.value,
tableLocale.value,
showSorterTooltip.value,
);
const sorters = computed(() => generateSorterInfo(mergedSorterStates.value));
return [transformColumns, mergedSorterStates, columnTitleSorterProps, sorters];
}

View File

@ -0,0 +1,29 @@
import type { Ref } from 'vue';
import type { TransformColumns, ColumnTitleProps, ColumnsType } from '../interface';
import { renderColumnTitle } from '../util';
function fillTitle<RecordType>(
columns: ColumnsType<RecordType>,
columnTitleProps: ColumnTitleProps<RecordType>,
) {
return columns.map(column => {
const cloneColumn = { ...column };
cloneColumn.title = renderColumnTitle(cloneColumn.title, columnTitleProps);
if ('children' in cloneColumn) {
cloneColumn.children = fillTitle(cloneColumn.children, columnTitleProps);
}
return cloneColumn;
});
}
export default function useTitleColumns<RecordType>(
columnTitleProps: Ref<ColumnTitleProps<RecordType>>,
): [TransformColumns<RecordType>] {
const filledColumns = (columns: ColumnsType<RecordType>) =>
fillTitle(columns, columnTitleProps.value);
return [filledColumns];
}

View File

@ -79,10 +79,11 @@ Specify `dataSource` of Table as an array of data.
| defaultExpandedRowKeys | Initial expanded row keys | string\[] | - | |
| expandedRowKeys | Current expanded row keys | string\[] | - | |
| expandedRowRender | Expanded container render for each row | Function({record, index, indent, expanded}):VNode\|v-slot | - | |
| expandIcon | Customize row expand Icon. | Function(props):VNode \| #expandIcon="props" | - | |
| expandFixed | Set column to be fixed: `true`(same as left) `'left'` `'right'` | boolean \| string | false | 3.0 |
| expandIcon | Customize row expand Icon. | Function(props):VNode \| v-slot:expandIcon="props" | - | |
| expandRowByClick | Whether to expand row by clicking anywhere in the whole row | boolean | `false` | |
| expandIconColumnIndex | The index of `expandIcon` which column will be inserted when `expandIconAsCell` is false | 0 | |
| footer | Table footer renderer | Function(currentPageData)\| v-slot | |
| expandIconColumnIndex | Customize expand icon column index. Not render when `-1` | 0 | |
| footer | Table footer renderer | Function(currentPageData)\| v-slot:footer="currentPageData" | |
| indentSize | Indent size in pixels of tree data | number | 15 | |
| loading | Loading status of table | boolean\|[object](/components/spin) | `false` |
| locale | i18n text including filter, sort, empty text, etc | object | filterConfirm: 'Ok' <br /> filterReset: 'Reset' <br /> emptyText: 'No Data' | |
@ -90,14 +91,27 @@ Specify `dataSource` of Table as an array of data.
| rowClassName | Row's className | Function(record, index):string | - | |
| rowKey | Row's unique key, could be a string or function that returns a string | string\|Function(record, index):string | `key` | |
| rowSelection | Row selection [config](#rowSelection) | object | null | |
| scroll | Set horizontal or vertical scrolling, can also be used to specify the width and height of the scroll area. It is recommended to set a number for `x`, if you want to set it to `true`, you need to add style `.ant-table td { white-space: nowrap; }`. | { x: number \| true, y: number } | - | |
| scroll | Whether the table can be scrollable, [config](#scroll) | object | - | |
| showHeader | Whether to show table header | boolean | `true` | |
| sortDirections | Supported sort way, could be `ascend`, `descend` | Array | \[`ascend`, `descend`] | 3.0 |
| showSorterTooltip | The header show next sorter direction tooltip. It will be set as the property of Tooltip if its type is object | boolean \| [Tooltip props](/components/tooltip/#API) | true | 3.0 |
| size | Size of table | `default` \| `middle` \| `small` \| `large` | `default` |
| title | Table title renderer | Function(currentPageData)\| v-slot | | |
| sticky | Set sticky header and scroll bar | boolean \| `{offsetHeader?: number, offsetScroll?: number, getContainer?: () => HTMLElement}` | - | 3.0 |
| title | Table title renderer | Function(currentPageData)\| v-slot:title="currentPageData" | | |
| customHeaderRow | Set props on per header row | Function(column, index) | - | |
| customRow | Set props on per row | Function(record, index) | - | |
| getPopupContainer | the render container of dropdowns in table | (triggerNode) => HTMLElement | `() => TableHtmlElement` | 1.5.0 |
| transformCellText | Data can be changed again before rendering. The default configuration of general user empty data. You can configured globally through [ConfigProvider](/components/config-provider-cn/) | Function({ text, column, record, index }) => any | - | 1.5.4 |
| headerCell | custom head cell by slot | v-slot:headerCell="{title, column}" | - | 3.0 |
| bodyCell | custom body cell by slot | v-slot:bodyCell="{text, record, index, column}" | - | 3.0 |
| customFilterDropdown | Customized filter overlayneed set `column.customFilterDropdown` | v-slot:customFilterDropdown="[FilterDropdownProps](#FilterDropdownProps)" | - | 3.0 |
| customFilterIcon | Customized filter icon | v-slot:customFilterIcon="{filtered, column}" | - | 3.0 |
| emptyText | Customize the display content when empty data | v-slot:emptyText | - | 3.0 |
| summary | Summary content | v-slot:summary | - | 3.0 |
| transformCellText | The data can be changed again before rendering, generally used for the default configuration of empty data. You can configured globally through [ConfigProvider](/components/config-provider-cn/) | Function({ text, column, record, index }) => any, The `text` here is the data processed by other defined cell api, and it may be of type VNode \| string \| number | - | 1.5.4 |
- `expandFixed`
- When set to true or `left` and `expandIconColumnIndex` is not set or is 0, enable fixed
- When set to true or `right` and `expandIconColumnIndex` is set to the number of table columns, enable fixed
### Events
@ -140,29 +154,37 @@ One of the Table `columns` prop for describing the table's columns, Column has t
| align | specify how content is aligned | 'left' \| 'right' \| 'center' | 'left' | |
| ellipsis | ellipsize cell content, not working with sorter and filters for now.<br />tableLayout would be `fixed` when `ellipsis` is true. | boolean | false | 1.5.0 |
| colSpan | Span of this column's title | number | | |
| dataIndex | Display field of the data record, could be set like `a.b.c` | string | - | |
| dataIndex | Display field of the data record, support nest path by string array | string \| string\[] | - | |
| defaultFilteredValue | Default filtered values | string\[] | - | 1.5.0 |
| defaultSortOrder | Default order of sorted values: `'ascend'` `'descend'` `null` | string | - | |
| filterDropdown | Customized filter overlay | slot | - | |
| ellipsis | The ellipsis cell content, not working with sorter and filters for now.<br />tableLayout would be `fixed` when `ellipsis` is `true` or `{ showTitle?: boolean }` | boolean \| {showTitle?: boolean } | false | 3.0 |
| filterDropdown | Customized filter overlay | VNode | - | |
| customFilterDropdown | use v-slot:customFilterDropdownPriority is lower than filterDropdown | boolean | false | 3.0 |
| filterDropdownVisible | Whether `filterDropdown` is visible | boolean | - | |
| filtered | Whether the `dataSource` is filtered | boolean | `false` | |
| filteredValue | Controlled filtered value, filter icon will highlight | string\[] | - | |
| filterIcon | Customized filter icon | slot \| ({filtered: boolean, column: Column}) | `false` | |
| filterIcon | Customized filter icon | ({filtered: boolean, column: Column}) | `false` | |
| filterMultiple | Whether multiple filters can be selected | boolean | `true` | |
| filters | Filter menu config | object\[] | - | |
| fixed | Set column to be fixed: `true`(same as left) `'left'` `'right'` | boolean\|string | `false` | |
| key | Unique key of this column, you can ignore this prop if you've set a unique `dataIndex` | string | - | |
| customRender | Renderer of the table cell. The return value should be a VNode, or an object for colSpan/rowSpan config | Function({text, record, index}) {}\|v-slot | - | |
| customRender | Renderer of the table cell. The return value should be a VNode, or an object for colSpan/rowSpan config | Function({text, record, index}) {} | - | |
| responsive | The list of breakpoints at which to display this column. Always visible if not set. | [Breakpoint](#Breakpoint)\[] | - | 3.0 |
| sorter | Sort function for local sort, see [Array.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)'s compareFunction. If you need sort buttons only, set to `true` | Function\|boolean | - | |
| sortOrder | Order of sorted values: `'ascend'` `'descend'` `false` | boolean\|string | - | |
| sortDirections | supported sort way, could be `'ascend'`, `'descend'` | Array | `['ascend', 'descend']` | 1.5.0 |
| title | Title of this column | string\|slot | - | |
| title | Title of this column | string | - | |
| width | Width of this column | string\|number | - | |
| customCell | Set props on per cell | Function(record, rowIndex) | - | |
| customHeaderCell | Set props on per header cell | Function(column) | - | |
| onFilter | Callback executed when the confirm filter button is clicked, Use as a `filter` event when using template or jsx | Function | - | |
| onFilterDropdownVisibleChange | Callback executed when `filterDropdownVisible` is changed, Use as a `filterDropdownVisible` event when using template or jsx | function(visible) {} | - | |
| slots | When using columns, you can use this property to configure the properties that support the slot, such as `slots: { filterIcon: 'XXX'}` | object | - | |
#### Breakpoint
```ts
type Breakpoint = 'xxl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs';
```
### ColumnGroup
@ -175,9 +197,9 @@ One of the Table `columns` prop for describing the table's columns, Column has t
Properties for pagination.
| Property | Description | Type | Default |
| -------- | ------------------------------------ | --------------------------- | -------- |
| position | specify the position of `Pagination` | 'top' \| 'bottom' \| 'both' | 'bottom' |
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| position | Specify the position of `Pagination`, could be`topLeft` \| `topCenter` \| `topRight` \|`bottomLeft` \| `bottomCenter` \| `bottomRight` | Array | \[`bottomRight`] |
More about pagination, please check [`Pagination`](/components/pagination/).
@ -185,28 +207,32 @@ More about pagination, please check [`Pagination`](/components/pagination/).
Properties for row selection.
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| columnWidth | Set the width of the selection column | string\|number | - |
| columnTitle | Set the title of the selection column | string\|VNode | - |
| fixed | Fixed selection column on the left | boolean | - |
| getCheckboxProps | Get Checkbox or Radio props | Function(record) | - |
| hideDefaultSelections | Remove the default `Select All` and `Select Invert` selections | boolean | `false` |
| selectedRowKeys | Controlled selected row keys | string\[] | \[] |
| selections | Custom selection config, only displays default selections when set to `true` | object\[]\|boolean | - |
| type | `checkbox` or `radio` | `checkbox` \| `radio` | `checkbox` |
| onChange | Callback executed when selected rows change | Function(selectedRowKeys, selectedRows) | - |
| onSelect | Callback executed when select/deselect one row | Function(record, selected, selectedRows, nativeEvent) | - |
| onSelectAll | Callback executed when select/deselect all rows | Function(selected, selectedRows, changeRows) | - |
| onSelectInvert | Callback executed when row selection is inverted | Function(selectedRows) | - |
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| checkStrictly | Check table row precisely; parent row and children rows are not associated | boolean | true | 3.0 |
| columnWidth | Set the width of the selection column | string\|number | - | |
| columnTitle | Set the title of the selection column | string\|VNode | - | |
| fixed | Fixed selection column on the left | boolean | - | |
| getCheckboxProps | Get Checkbox or Radio props | Function(record) | - | |
| hideSelectAll | Hide the selectAll checkbox and custom selection | boolean | false | 3.0 |
| preserveSelectedRowKeys | Keep selection `key` even when it removed from `dataSource` | boolean | - | 3.0 |
| hideDefaultSelections | Remove the default `Select All` and `Select Invert` selections | boolean | `false` | |
| selectedRowKeys | Controlled selected row keys | string\[] | \[] | |
| selections | Custom selection [config](#rowSelection), only displays default selections when set to `true` | object\[] \| boolean | - | |
| type | `checkbox` or `radio` | `checkbox` \| `radio` | `checkbox` | |
| onChange | Callback executed when selected rows change | Function(selectedRowKeys, selectedRows) | - | |
| onSelect | Callback executed when select/deselect one row | Function(record, selected, selectedRows, nativeEvent) | - | |
| onSelectAll | Callback executed when select/deselect all rows | Function(selected, selectedRows, changeRows) | - | |
| onSelectInvert | Callback executed when row selection is inverted | Function(selectedRows) | - | |
| onSelectNone | Callback executed when row selection is cleared | function() | - | 3.0 |
### scroll
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| x | Set horizontal scrolling, can also be used to specify the width and height of the scroll area, could be number, percent value, true and ['max-content'](https://developer.mozilla.org/zh-CN/docs/Web/CSS/width#max-content) | number \| true | - | |
| y | Set vertical scrolling, can also be used to specify the width and height of the scroll area, could be number, percent value, true and ['max-content'](https://developer.mozilla.org/zh-CN/docs/Web/CSS/width#max-content) | number \| true | - | |
| scrollToFirstRowOnChange | Whether to scroll to the top of the table when paging, sorting, filtering changes | boolean | - | 1.5.0 |
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| scrollToFirstRowOnChange | Whether to scroll to the top of the table when paging, sorting, filtering changes | boolean | - |
| x | Set horizontal scrolling, can also be used to specify the width of the scroll area, could be number, percent value, true and ['max-content'](https://developer.mozilla.org/zh-CN/docs/Web/CSS/width#max-content) | string \| number \| true | - |
| y | Set vertical scrolling, can also be used to specify the height of the scroll area, could be string or number | string \| number | - |
### selection
@ -218,6 +244,21 @@ Custom selection config
| text | Display text of this selection | string\|VNode | - |
| onSelect | Callback executed when this selection is clicked | Function(changeableRowKeys) | - |
### FilterDropdownProps
```ts
interface FilterDropdownProps {
prefixCls: string;
setSelectedKeys: (selectedKeys: Key[]) => void;
selectedKeys: Key[];
confirm: (param?: FilterConfirmProps) => void;
clearFilters?: () => void;
filters?: ColumnFilterItem[];
visible: boolean;
column: ColumnType;
}
```
## Note
The values inside `dataSource` and `columns` should follow this in Table, and `dataSource[i].key` would be treated as key value default for `dataSource`.
@ -230,3 +271,9 @@ return <Table rowKey="uid" />;
// or
return <Table rowKey={record => record.uid} />;
```
## Migrate to v3
Table deprecated `column.slots`, added `v-slot:bodyCell`, `v-slot:headerCell`, custom cells, and added `column.customFilterDropdown` `v-slot:customFilterDropdown`, custom filtering Menu, added `v-slot:customFilterIcon` custom filter button, but `column.slots` is still available, we will remove it in the next major version.
Besides, the breaking change is changing `dataIndex` from nest string path like `user.age` to string array path like `['user', 'age']`. This help to resolve developer should additional work on the field which contains `.`.

View File

@ -1,111 +1,68 @@
import type { App, Plugin } from 'vue';
import Table, { tableProps } from './Table';
import Column from './Column';
import ColumnGroup from './ColumnGroup';
import type { TableProps, TablePaginationConfig } from './Table';
import { defineComponent } from 'vue';
import T, { defaultTableProps } from './Table';
import type Column from './Column';
import type ColumnGroup from './ColumnGroup';
import {
getOptionProps,
getKey,
getPropsData,
getSlot,
flattenChildren,
} from '../_util/props-util';
import type { App } from 'vue';
import { Summary, SummaryCell, SummaryRow } from '../vc-table';
import { SELECTION_ALL, SELECTION_INVERT, SELECTION_NONE } from './hooks/useSelection';
const Table = defineComponent({
name: 'ATable',
Column: T.Column,
ColumnGroup: T.ColumnGroup,
inheritAttrs: false,
props: defaultTableProps,
methods: {
normalize(elements = []) {
const flattenElements = flattenChildren(elements);
const columns = [];
flattenElements.forEach(element => {
if (!element) {
return;
}
const key = getKey(element);
const style = element.props?.style || {};
const cls = element.props?.class || '';
const props = getPropsData(element);
const { default: children, ...restSlots } = element.children || {};
const column = { ...restSlots, ...props, style, class: cls };
if (key) {
column.key = key;
}
if (element.type?.__ANT_TABLE_COLUMN_GROUP) {
column.children = this.normalize(typeof children === 'function' ? children() : children);
} else {
const customRender = element.children?.default;
column.customRender = column.customRender || customRender;
}
columns.push(column);
});
return columns;
},
updateColumns(cols = []) {
const columns = [];
const { $slots } = this;
cols.forEach(col => {
const { slots = {}, ...restProps } = col;
const column = {
...restProps,
};
Object.keys(slots).forEach(key => {
const name = slots[key];
if (column[key] === undefined && $slots[name]) {
column[key] = $slots[name];
}
});
// if (slotScopeName && $scopedSlots[slotScopeName]) {
// column.customRender = column.customRender || $scopedSlots[slotScopeName]
// }
if (col.children) {
column.children = this.updateColumns(column.children);
}
columns.push(column);
});
return columns;
},
},
render() {
const { normalize, $slots } = this;
const props: any = { ...getOptionProps(this), ...this.$attrs };
const columns = props.columns ? this.updateColumns(props.columns) : normalize(getSlot(this));
let { title, footer } = props;
const {
title: slotTitle,
footer: slotFooter,
expandedRowRender = props.expandedRowRender,
expandIcon,
} = $slots;
title = title || slotTitle;
footer = footer || slotFooter;
const tProps = {
...props,
columns,
title,
footer,
expandedRowRender,
expandIcon: this.$props.expandIcon || expandIcon,
};
return <T {...tProps} ref="table" />;
},
export type { ColumnProps } from './Column';
export type { ColumnsType, ColumnType, ColumnGroupType } from './interface';
export type { TableProps, TablePaginationConfig };
const TableSummaryRow = defineComponent({ ...SummaryRow, name: 'ATableSummaryRow' });
const TableSummaryCell = defineComponent({ ...SummaryCell, name: 'ATableSummaryCell' });
const TempSummary = defineComponent({
...Summary,
name: 'ATableSummary',
});
const TableSummary = TempSummary as typeof TempSummary & {
Cell: typeof TableSummaryCell;
Row: typeof TableSummaryRow;
};
TableSummary.Cell = TableSummaryCell;
TableSummary.Row = TableSummaryRow;
const T = Table as typeof Table &
Plugin & {
Column: typeof Column;
ColumnGroup: typeof ColumnGroup;
Summary: typeof TableSummary;
SELECTION_ALL: typeof SELECTION_ALL;
SELECTION_INVERT: typeof SELECTION_INVERT;
SELECTION_NONE: typeof SELECTION_NONE;
};
T.SELECTION_ALL = SELECTION_ALL;
T.SELECTION_INVERT = SELECTION_INVERT;
T.SELECTION_NONE = SELECTION_NONE;
T.Column = Column;
T.ColumnGroup = ColumnGroup;
T.Summary = TableSummary;
/* istanbul ignore next */
Table.install = function (app: App) {
app.component(Table.name, Table);
app.component(Table.Column.name, Table.Column);
app.component(Table.ColumnGroup.name, Table.ColumnGroup);
T.install = function (app: App) {
app.component(TableSummary.name, TableSummary);
app.component(TableSummaryCell.name, TableSummaryCell);
app.component(TableSummaryRow.name, TableSummaryRow);
app.component(T.name, T);
app.component(T.Column.name, Column);
app.component(T.ColumnGroup.name, ColumnGroup);
return app;
};
export const TableColumn = Table.Column;
export const TableColumnGroup = Table.ColumnGroup;
export {
tableProps,
TableSummary,
TableSummaryRow,
TableSummaryCell,
Column as TableColumn,
ColumnGroup as TableColumnGroup,
};
export default Table as typeof Table &
Plugin & {
readonly Column: typeof Column;
readonly ColumnGroup: typeof ColumnGroup;
};
export default T;

View File

@ -74,35 +74,50 @@ cover: https://gw.alipayobjects.com/zos/alicdn/f-SbcX2Lx/Table.svg
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| tableLayout | 表格元素的 [table-layout](https://developer.mozilla.org/zh-CN/docs/Web/CSS/table-layout) 属性,设为 `fixed` 表示内容不会影响列的布局 | - \| 'auto' \| 'fixed' | 无<hr />固定表头/列或使用了 `column.ellipsis` 时,默认值为 `fixed` | 1.5.0 |
| bordered | 是否展示外边框和列边框 | boolean | false | |
| childrenColumnName | 指定树形结构的列名 | string | `children` | |
| columns | 表格列的配置描述,具体项见[下表](#Column) | array | - | |
| components | 覆盖默认的 table 元素 | object | - | |
| dataSource | 数据数组 | any\[] | | |
| childrenColumnName | 指定树形结构的列名 | string | `children` | |
| dataSource | 数据数组 | object\[] | | |
| defaultExpandAllRows | 初始时,是否展开所有行 | boolean | false | |
| defaultExpandedRowKeys | 默认展开的行 | string\[] | - | |
| expandedRowKeys | 展开的行,控制属性 | string\[] | - | |
| expandedRowRender | 额外的展开行 | Function(record, index, indent, expanded):VNode \| #expandedRowRender="{record, index, indent, expanded}" | - | |
| expandIcon | 自定义展开图标 | Function(props):VNode \| #expandIcon="props" | - | |
| expandedRowRender | 额外的展开行 | Function(record, index, indent, expanded):VNode \| v-slot:expandedRowRender="{record, index, indent, expanded}" | - | |
| expandFixed | 控制展开图标是否固定,可选 true `left` `right` | boolean \| string | false | 3.0 |
| expandIcon | 自定义展开图标 | Function(props):VNode \| v-slot:expandIcon="props" | - | |
| expandRowByClick | 通过点击行来展开子行 | boolean | `false` | |
| expandIconColumnIndex | 展开的图标显示在哪一列,如果没有 `rowSelection`,默认显示在第一列,否则显示在选择框后面 | `number` | |
| footer | 表格尾部 | Function(currentPageData)\|v-slot | | |
| indentSize | 展示树形数据时,每层缩进的宽度,以 px 为单位 | number | 15 | |
| expandIconColumnIndex | 自定义展开按钮的列顺序,`-1` 时不展示 | number | - | |
| footer | 表格尾部 | Function(currentPageData)\|v-slot:footer="currentPageData" | | |
| getPopupContainer | 设置表格内各类浮层的渲染节点,如筛选菜单 | (triggerNode) => HTMLElement | `() => TableHtmlElement` | 1.5.0 |
| loading | 页面是否加载中 | boolean\|[object](/components/spin-cn) | false | |
| locale | 默认文案设置,目前包括排序、过滤、空数据文案 | object | filterConfirm: '确定' <br /> filterReset: '重置' <br /> emptyText: '暂无数据' | |
| locale | 默认文案设置,目前包括排序、过滤、空数据文案 | object | filterConfirm: `确定` <br> filterReset: `重置` <br> emptyText: `暂无数据` | |
| pagination | 分页器,参考[配置项](#pagination)或 [pagination](/components/pagination-cn/)文档,设为 false 时不展示和进行分页 | object | | |
| rowClassName | 表格行的类名 | Function(record, index):string | - | |
| rowKey | 表格行 key 的取值,可以是字符串或一个函数 | string\|Function(record):string | 'key' | |
| rowSelection | 列表项是否可选择,[配置项](#rowSelection) | object | null | |
| scroll | 设置横向或纵向滚动,也可用于指定滚动区域的宽和高,建议为 `x` 设置一个数字,如果要设置为 `true`,需要配合样式 `.ant-table td { white-space: nowrap; }` | { x: number \| true, y: number } | - | |
| scroll | 表格是否可滚动,也可以指定滚动区域的宽、高,[配置项](#scroll) | object | - | |
| showHeader | 是否显示表头 | boolean | true | |
| showSorterTooltip | 表头是否显示下一次排序的 tooltip 提示。当参数类型为对象时,将被设置为 Tooltip 的属性 | boolean \| [Tooltip props](/components/tooltip/) | true | 3.0 |
| size | 表格大小 | default \| middle \| small | default | |
| title | 表格标题 | Function(currentPageData)\|v-slot | | |
| sortDirections | 支持的排序方式,取值为 `ascend` `descend` | Array | \[`ascend`, `descend`] | |
| sticky | 设置粘性头部和滚动条 | boolean \| `{offsetHeader?: number, offsetScroll?: number, getContainer?: () => HTMLElement}` | - | 3.0 |
| tableLayout | 表格元素的 [table-layout](https://developer.mozilla.org/zh-CN/docs/Web/CSS/table-layout) 属性,设为 `fixed` 表示内容不会影响列的布局 | - \| 'auto' \| 'fixed' | 无<hr />固定表头/列或使用了 `column.ellipsis` 时,默认值为 `fixed` | 1.5.0 |
| title | 表格标题 | Function(currentPageData)\|v-slot:title="currentPageData" | | |
| indentSize | 展示树形数据时,每层缩进的宽度,以 px 为单位 | number | 15 | |
| rowExpandable | 设置是否允许行展开 | (record) => boolean | - | 3.0 |
| customHeaderRow | 设置头部行属性 | Function(column, index) | - | |
| customRow | 设置行属性 | Function(record, index) | - | |
| getPopupContainer | 设置表格内各类浮层的渲染节点,如筛选菜单 | (triggerNode) => HTMLElement | `() => TableHtmlElement` | 1.5.0 |
| transformCellText | 数据渲染前可以再次改变,一般用户空数据的默认配置,可以通过 [ConfigProvider](/components/config-provider-cn/) 全局统一配置 | Function({ text, column, record, index }) => any | - | 1.5.4 |
| headerCell | 个性化头部单元格 | v-slot:headerCell="{title, column}" | - | 3.0 |
| bodyCell | 个性化单元格 | v-slot:bodyCell="{text, record, index, column}" | - | 3.0 |
| customFilterDropdown | 自定义筛选菜单,需要配合 `column.customFilterDropdown` 使用 | v-slot:customFilterDropdown="[FilterDropdownProps](#FilterDropdownProps)" | - | 3.0 |
| customFilterIcon | 自定义筛选图标 | v-slot:customFilterIcon="{filtered, column}" | - | 3.0 |
| emptyText | 自定义空数据时的显示内容 | v-slot:emptyText | - | 3.0 |
| summary | 总结栏 | v-slot:summary | - | 3.0 |
| transformCellText | 数据渲染前可以再次改变,一般用于空数据的默认配置,可以通过 [ConfigProvider](/components/config-provider-cn/) 全局统一配置 | Function({ text, column, record, index }) => any此处的 text 是经过其它定义单元格 api 处理后的数据,有可能是 VNode \| string \| number 类型 | - | 1.5.4 |
- `expandFixed`
- 当设置为 true 或 `left``expandIconColumnIndex` 未设置或为 0 时,开启固定
- 当设置为 true 或 `right``expandIconColumnIndex` 设置为表格列数时,开启固定
### 事件
@ -142,46 +157,54 @@ cover: https://gw.alipayobjects.com/zos/alicdn/f-SbcX2Lx/Table.svg
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| align | 设置列内容的对齐方式 | 'left' \| 'right' \| 'center' | 'left' | |
| ellipsis | 超过宽度将自动省略,暂不支持和排序筛选一起使用。<br />设置为 `true` 时,表格布局将变成 `tableLayout="fixed"`。 | boolean | false | 1.5.0 |
| align | 设置列的对齐方式 | `left` \| `right` \| `center` | `left` | |
| colSpan | 表头列合并,设置为 0 时,不渲染 | number | | |
| dataIndex | 列数据在数据项中对应的 key支持 `a.b.c` 的嵌套写法 | string | - | |
| dataIndex | 列数据在数据项中对应的路径,支持通过数组查询嵌套路径 | string \| string\[] | - | |
| defaultFilteredValue | 默认筛选值 | string\[] | - | 1.5.0 |
| filterDropdown | 可以自定义筛选菜单,此函数只负责渲染图层,需要自行编写各种交互 | VNode \| v-slot | - | |
| defaultSortOrder | 默认排序顺序 | `ascend` \| `descend` | - | |
| ellipsis | 超过宽度将自动省略,暂不支持和排序筛选一起使用。<br />设置为 `true``{ showTitle?: boolean }` 时,表格布局将变成 `tableLayout="fixed"`。 | boolean \| { showTitle?: boolean } | false | 3.0 |
| filterDropdown | 可以自定义筛选菜单,此函数只负责渲染图层,需要自行编写各种交互 | VNode | - | |
| customFilterDropdown | 启用 v-slot:customFilterDropdown优先级低于 filterDropdown | boolean | false | 3.0 |
| filterDropdownVisible | 用于控制自定义筛选菜单是否可见 | boolean | - | |
| filtered | 标识数据是否经过过滤,筛选图标会高亮 | boolean | false | |
| filteredValue | 筛选的受控属性,外界可用此控制列的筛选状态,值为已筛选的 value 数组 | string\[] | - | |
| filterIcon | 自定义 filter 图标。 | VNode \| ({filtered: boolean, column: Column}) => vNode \|slot | false | |
| filterIcon | 自定义 filter 图标。 | VNode \| ({filtered: boolean, column: Column}) => vNode | false | |
| filterMultiple | 是否多选 | boolean | true | |
| filters | 表头的筛选菜单项 | object\[] | - | |
| fixed | 列是否固定,可选 `true`(等效于 left) `'left'` `'right'` | boolean\|string | false | |
| key | Vue 需要的 key如果已经设置了唯一的 `dataIndex`,可以忽略这个属性 | string | - | |
| customRender | 生成复杂数据的渲染函数,参数分别为当前行的值,当前行数据,行索引,@return 里面可以设置表格行/列合并,可参考 demo 表格行/列合并 | Function({text, record, index}) {}\|v-slot | - | |
| customRender | 生成复杂数据的渲染函数,参数分别为当前行的值,当前行数据,行索引,@return 里面可以设置表格行/列合并,可参考 demo 表格行/列合并 | Function({text, record, index, column}) {} | - | |
| responsive | 响应式 breakpoint 配置列表。未设置则始终可见。 | [Breakpoint](#Breakpoint)\[] | - | 3.0 |
| showSorterTooltip | 表头显示下一次排序的 tooltip 提示, 覆盖 table 中 `showSorterTooltip` | boolean \| [Tooltip props](/components/tooltip/#API) | true | |
| sorter | 排序函数,本地排序使用一个函数(参考 [Array.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) 的 compareFunction),需要服务端排序可设为 true | Function\|boolean | - | |
| sortOrder | 排序的受控属性,外界可用此控制列的排序,可设置为 `'ascend'` `'descend'` `false` | boolean\|string | - | |
| sortDirections | 支持的排序方式,取值为 `'ascend'` `'descend'` | Array | `['ascend', 'descend']` | 1.5.0 |
| title | 列头显示文字 | string\|slot | - | |
| title | 列头显示文字 | string | - | |
| width | 列宽度 | string\|number | - | |
| customCell | 设置单元格属性 | Function(record, rowIndex) | - | |
| customHeaderCell | 设置头部单元格属性 | Function(column) | - | |
| onFilter | 本地模式下,确定筛选的运行函数, 使用 template 或 jsx 时作为`filter`事件使用 | Function | - | |
| onFilterDropdownVisibleChange | 自定义筛选菜单可见变化时调用,使用 template 或 jsx 时作为`filterDropdownVisibleChange`事件使用 | function(visible) {} | - | |
| slots | 使用 columns 时,可以通过该属性配置支持 slot 的属性,如 `slots: { filterIcon: 'XXX'}` | object | - | |
#### Breakpoint
```ts
type Breakpoint = 'xxl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs';
```
### ColumnGroup
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| title | 列头显示文字 | string\|slot | - |
| slots | 使用 columns 时,可以通过该属性配置支持 slot 的属性,如 `slots: { title: 'XXX'}` | object | - |
| 参数 | 说明 | 类型 | 默认值 |
| ----- | ------------ | ------------ | ------ |
| title | 列头显示文字 | string\|slot | - |
### pagination
分页的配置项。
| 参数 | 说明 | 类型 | 默认值 |
| -------- | ------------------ | --------------------------- | -------- |
| position | 指定分页显示的位置 | 'top' \| 'bottom' \| 'both' | 'bottom' |
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| position | 指定分页显示的位置 取值为`topLeft` \| `topCenter` \| `topRight` \|`bottomLeft` \| `bottomCenter` \| `bottomRight` | Array | \[`bottomRight`] |
更多配置项,请查看 [`Pagination`](/components/pagination/)。
@ -189,28 +212,32 @@ cover: https://gw.alipayobjects.com/zos/alicdn/f-SbcX2Lx/Table.svg
选择功能的配置。
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| columnWidth | 自定义列表选择框宽度 | string\|number | - |
| columnTitle | 自定义列表选择框标题 | string\|VNode | - |
| fixed | 把选择框列固定在左边 | boolean | - |
| getCheckboxProps | 选择框的默认属性配置 | Function(record) | - |
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| checkStrictly | checkable 状态下节点选择完全受控(父子数据选中状态不再关联) | boolean | true | 3.0 |
| columnWidth | 自定义列表选择框宽度 | string\|number | - | |
| columnTitle | 自定义列表选择框标题 | string\|VNode | - | |
| fixed | 把选择框列固定在左边 | boolean | - | |
| getCheckboxProps | 选择框的默认属性配置 | Function(record) | - | |
| hideSelectAll | 隐藏全选勾选框与自定义选择项 | boolean | false | 3.0 |
| preserveSelectedRowKeys | 当数据被删除时仍然保留选项的 `key` | boolean | - | 3.0 |
| hideDefaultSelections | 去掉『全选』『反选』两个默认选项 | boolean | false |
| selectedRowKeys | 指定选中项的 key 数组,需要和 onChange 进行配合 | string\[] | \[] |
| selections | 自定义选择配置项, 设为 `true` 时使用默认选择项 | object\[]\|boolean | - |
| selections | 自定义选择项 [配置项](#selection), 设为 `true` 时使用默认选择项 | object\[] \| boolean | true | |
| type | 多选/单选,`checkbox` or `radio` | string | `checkbox` |
| onChange | 选中项发生变化时的回调 | Function(selectedRowKeys, selectedRows) | - |
| onSelect | 用户手动选择/取消选择某列的回调 | Function(record, selected, selectedRows, nativeEvent) | - |
| onSelectAll | 用户手动选择/取消选择所有列的回调 | Function(selected, selectedRows, changeRows) | - |
| onSelectInvert | 用户手动选择反选的回调 | Function(selectedRows) | - |
| onSelectNone | 用户清空选择的回调 | function() | - | 3.0 |
### scroll
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| x | 设置横向滚动也可用于指定滚动区域的宽和高可以设置为像素值百分比true 和 ['max-content'](https://developer.mozilla.org/zh-CN/docs/Web/CSS/width#max-content) | number \| true | - | |
| y | 设置纵向滚动,也可用于指定滚动区域的宽和高可以设置为像素值百分比true 和 ['max-content'](https://developer.mozilla.org/zh-CN/docs/Web/CSS/width#max-content) | number \| true | - | |
| scrollToFirstRowOnChange | 当分页、排序、筛选变化后是否滚动到表格顶部 | boolean | - | 1.5.0 |
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| scrollToFirstRowOnChange | 当分页、排序、筛选变化后是否滚动到表格顶部 | boolean | - |
| x | 设置横向滚动,也可用于指定滚动区域的宽可以设置为像素值百分比true 和 ['max-content'](https://developer.mozilla.org/zh-CN/docs/Web/CSS/width#max-content) | string \| number \| true | - |
| y | 设置纵向滚动,也可用于指定滚动区域的高,可以设置为像素值 | string \| number | - |
### selection
@ -222,6 +249,21 @@ cover: https://gw.alipayobjects.com/zos/alicdn/f-SbcX2Lx/Table.svg
| text | 选择项显示的文字 | string\|VNode | - |
| onSelect | 选择项点击回调 | Function(changeableRowKeys) | - |
### FilterDropdownProps
```ts
interface FilterDropdownProps {
prefixCls: string;
setSelectedKeys: (selectedKeys: Key[]) => void;
selectedKeys: Key[];
confirm: (param?: FilterConfirmProps) => void;
clearFilters?: () => void;
filters?: ColumnFilterItem[];
visible: boolean;
column: ColumnType;
}
```
## 注意
在 Table 中,`dataSource` 和 `columns` 里的数据值都需要指定 `key` 值。对于 `dataSource` 默认将每列数据的 `key` 属性作为唯一的标识。
@ -234,3 +276,9 @@ return <Table rowKey="uid" />;
// 或
return <Table rowKey={record => record.uid} />;
```
## 从 v1 / v2 升级到 v3
Table 废弃了 `column.slots`, 新增 `v-slot:bodyCell`、`v-slot:headerCell`,自定义单元格,新增 `column.customFilterDropdown` `v-slot:customFilterDropdown`,自定义筛选菜单,新增了 `v-slot:customFilterIcon` 自定义筛选按钮,但 `column.slots` 还可用,我们会在下一个大版本时移除。
此外,比较重大的改动为 `dataIndex` 从支持路径嵌套如 `user.age` 改成了数组路径如 `['user', 'age']`。以解决过去属性名带 `.` 需要额外的数据转化问题。

View File

@ -1,261 +0,0 @@
import type { ExtractPropTypes, PropType, UnwrapRef } from 'vue';
import PropTypes, { withUndefined } from '../_util/vue-types';
import { paginationProps as getPaginationProps, paginationConfig } from '../pagination';
import { spinProps } from '../spin';
import { tuple } from '../_util/type';
const PaginationProps = getPaginationProps();
export type CompareFn<T> = (a: T, b: T, sortOrder?: SortOrder) => number;
export const ColumnFilterItem = PropTypes.shape({
text: PropTypes.string,
value: PropTypes.string,
children: PropTypes.array,
}).loose;
export const columnProps = {
title: PropTypes.VNodeChild,
key: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
dataIndex: PropTypes.string,
customRender: PropTypes.func,
customCell: PropTypes.func,
customHeaderCell: PropTypes.func,
align: PropTypes.oneOf(tuple('left', 'right', 'center')),
ellipsis: PropTypes.looseBool,
filters: PropTypes.arrayOf(ColumnFilterItem),
onFilter: {
type: Function as PropType<(value: any, record: any) => boolean>,
},
filterMultiple: PropTypes.looseBool,
filterDropdown: PropTypes.any,
filterDropdownVisible: PropTypes.looseBool,
onFilterDropdownVisibleChange: {
type: Function as PropType<(visible: boolean) => void>,
},
sorter: PropTypes.oneOfType([PropTypes.looseBool, PropTypes.func]),
defaultSortOrder: PropTypes.oneOf(tuple('ascend', 'descend')),
colSpan: PropTypes.number,
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
className: PropTypes.string,
fixed: withUndefined(
PropTypes.oneOfType([PropTypes.looseBool, PropTypes.oneOf(tuple('left', 'right'))]),
),
filterIcon: PropTypes.any,
filteredValue: PropTypes.array,
filtered: PropTypes.looseBool,
defaultFilteredValue: PropTypes.array,
sortOrder: withUndefined(
PropTypes.oneOfType([PropTypes.looseBool, PropTypes.oneOf(tuple('ascend', 'descend'))]),
),
sortDirections: PropTypes.array,
// children?: ColumnProps<T>[];
// onCellClick?: (record: T, event: any) => void;
// onCell?: (record: T) => any;
// onHeaderCell?: (props: ColumnProps<T>) => any;
};
export type ColumnProps = Partial<ExtractPropTypes<typeof columnProps>> & {
slots?: {
title?: string;
filterIcon?: string;
filterDropdown?: string;
customRender?: string;
[key: string]: string | undefined;
};
};
export interface TableComponents {
table?: any;
header?: {
wrapper?: any;
row?: any;
cell?: any;
};
body?: {
wrapper?: any;
row?: any;
cell?: any;
};
}
export const TableLocale = PropTypes.shape({
filterTitle: PropTypes.string,
filterConfirm: PropTypes.any,
filterReset: PropTypes.any,
emptyText: PropTypes.any,
selectAll: PropTypes.any,
selectInvert: PropTypes.any,
sortTitle: PropTypes.string,
expand: PropTypes.string,
collapse: PropTypes.string,
}).loose;
export const RowSelectionType = PropTypes.oneOf(tuple('checkbox', 'radio'));
// export type SelectionSelectFn<T> = (record: T, selected: boolean, selectedRows: Object[]) => any;
export const tableRowSelection = {
type: RowSelectionType,
selectedRowKeys: PropTypes.array,
// onChange?: (selectedRowKeys: string[] | number[], selectedRows: Object[]) => any;
getCheckboxProps: PropTypes.func,
// onSelect?: SelectionSelectFn<T>;
// onSelectAll?: (selected: boolean, selectedRows: Object[], changeRows: Object[]) => any;
// onSelectInvert?: (selectedRows: Object[]) => any;
selections: withUndefined(PropTypes.oneOfType([PropTypes.array, PropTypes.looseBool])),
hideDefaultSelections: PropTypes.looseBool,
fixed: PropTypes.looseBool,
columnWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
selectWay: PropTypes.oneOf(
tuple('onSelect', 'onSelectMultiple', 'onSelectAll', 'onSelectInvert'),
),
columnTitle: PropTypes.any,
};
export type SortOrder = 'descend' | 'ascend';
const paginationProps = paginationConfig();
export const tableProps = {
prefixCls: PropTypes.string,
dropdownPrefixCls: PropTypes.string,
rowSelection: PropTypes.oneOfType([PropTypes.shape(tableRowSelection).loose, Object]),
pagination: withUndefined(
PropTypes.oneOfType([
PropTypes.shape<Partial<ExtractPropTypes<typeof paginationProps>>>(paginationProps).loose,
PropTypes.looseBool,
]),
),
size: PropTypes.oneOf(tuple('default', 'middle', 'small', 'large')),
dataSource: PropTypes.array,
components: PropTypes.object,
columns: {
type: Array as PropType<ColumnProps>,
},
rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
rowClassName: PropTypes.func,
expandedRowRender: PropTypes.any,
defaultExpandAllRows: PropTypes.looseBool,
defaultExpandedRowKeys: PropTypes.array,
expandedRowKeys: PropTypes.array,
expandIconAsCell: PropTypes.looseBool,
expandIconColumnIndex: PropTypes.number,
expandRowByClick: PropTypes.looseBool,
loading: PropTypes.oneOfType([PropTypes.shape(spinProps()).loose, PropTypes.looseBool]),
locale: TableLocale,
indentSize: PropTypes.number,
customRow: PropTypes.func,
customHeaderRow: PropTypes.func,
useFixedHeader: PropTypes.looseBool,
bordered: PropTypes.looseBool,
showHeader: PropTypes.looseBool,
footer: PropTypes.func,
title: PropTypes.func,
scroll: {
type: Object as PropType<{
x?: boolean | number | string;
y?: boolean | number | string;
scrollToFirstRowOnChange?: boolean;
}>,
},
childrenColumnName: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
bodyStyle: PropTypes.style,
sortDirections: {
type: Array as PropType<SortOrder[]>,
},
tableLayout: PropTypes.string,
getPopupContainer: PropTypes.func,
expandIcon: PropTypes.func,
transformCellText: PropTypes.func,
onExpandedRowsChange: PropTypes.func,
onExpand: PropTypes.func,
onChange: PropTypes.func,
onRowClick: PropTypes.func,
// style?: React.CSSProperties;
// children?: React.ReactNode;
};
export type TableRowSelection = Partial<ExtractPropTypes<typeof tableRowSelection>>;
export type TableProps = Partial<ExtractPropTypes<typeof tableProps>>;
export interface TableStateFilters {
[key: string]: string[];
}
export interface TableState {
pagination?: Partial<ExtractPropTypes<typeof PaginationProps>>;
filters?: TableStateFilters;
sortColumn?: ColumnProps | null;
sortOrder?: SortOrder;
columns?: ColumnProps[];
}
export interface TransformCellTextProps {
text: any;
column: ColumnProps;
record: any;
index: number;
}
// export type SelectionItemSelectFn = (key: string[]) => any;
// export interface SelectionItem {
// key: PropTypes.string,
// text: PropTypes.any,
// onSelect: SelectionItemSelectFn;
// }
export type TableStore = UnwrapRef<{
selectedRowKeys: any[];
selectionDirty: boolean;
}>;
export const SelectionCheckboxAllProps = {
propsSymbol: PropTypes.any,
store: PropTypes.any,
locale: PropTypes.any,
disabled: PropTypes.looseBool,
getCheckboxPropsByItem: PropTypes.func,
getRecordKey: PropTypes.func,
data: PropTypes.array,
prefixCls: PropTypes.string,
hideDefaultSelections: PropTypes.looseBool,
selections: PropTypes.oneOfType([PropTypes.array, PropTypes.looseBool]),
getPopupContainer: PropTypes.func,
onSelect: PropTypes.func,
};
// export interface SelectionCheckboxAllState {
// checked: PropTypes.looseBool,
// indeterminate: PropTypes.looseBool,
// }
export const SelectionBoxProps = {
store: PropTypes.any,
type: RowSelectionType,
defaultSelection: PropTypes.array,
rowIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
name: PropTypes.string,
disabled: PropTypes.looseBool,
id: PropTypes.string,
// onChange: React.ChangeEventHandler<HTMLInputElement>;
};
// export interface SelectionBoxState {
// checked?: PropTypes.looseBool,
// }
export const FilterMenuProps = {
locale: TableLocale,
selectedKeys: PropTypes.array,
column: PropTypes.object,
confirmFilter: PropTypes.func,
prefixCls: PropTypes.string,
dropdownPrefixCls: PropTypes.string,
getPopupContainer: PropTypes.func,
handleFilter: PropTypes.func,
};
// export interface FilterMenuState {
// selectedKeys: string[];
// keyPathOfSelectedItem: { [key: string]: string };
// visible?: PropTypes.looseBool,
// }

View File

@ -0,0 +1,205 @@
import type {
GetRowKey,
ColumnType as RcColumnType,
RenderedCell as RcRenderedCell,
ExpandableConfig,
DefaultRecordType,
} from '../vc-table/interface';
import type { TooltipProps } from '../tooltip';
import type { CheckboxProps } from '../checkbox';
import type { PaginationProps } from '../pagination';
import type { Breakpoint } from '../_util/responsiveObserve';
import type { INTERNAL_SELECTION_ITEM } from './hooks/useSelection';
import type { VueNode } from '../_util/type';
import { tuple } from '../_util/type';
// import { TableAction } from './Table';
export type { GetRowKey, ExpandableConfig };
export type Key = string | number;
export type RowSelectionType = 'checkbox' | 'radio';
export type SelectionItemSelectFn = (currentRowKeys: Key[]) => void;
export type ExpandType = null | 'row' | 'nest';
export interface TableLocale {
filterTitle?: string;
filterConfirm?: any;
filterReset?: any;
filterEmptyText?: any;
emptyText?: any | (() => any);
selectAll?: any;
selectNone?: any;
selectInvert?: any;
selectionAll?: any;
sortTitle?: string;
expand?: string;
collapse?: string;
triggerDesc?: string;
triggerAsc?: string;
cancelSort?: string;
}
export type SortOrder = 'descend' | 'ascend' | null;
const TableActions = tuple('paginate', 'sort', 'filter');
export type TableAction = typeof TableActions[number];
export type CompareFn<T> = (a: T, b: T, sortOrder?: SortOrder) => number;
export interface ColumnFilterItem {
text: VueNode;
value: string | number | boolean;
children?: ColumnFilterItem[];
}
export interface ColumnTitleProps<RecordType> {
/** @deprecated Please use `sorterColumns` instead. */
sortOrder?: SortOrder;
/** @deprecated Please use `sorterColumns` instead. */
sortColumn?: ColumnType<RecordType>;
sortColumns?: { column: ColumnType<RecordType>; order: SortOrder }[];
filters?: Record<string, string[]>;
}
export type ColumnTitle<RecordType> = VueNode | ((props: ColumnTitleProps<RecordType>) => VueNode);
export type FilterValue = (Key | boolean)[];
export type FilterKey = Key[] | null;
export interface FilterConfirmProps {
closeDropdown: boolean;
}
export interface FilterDropdownProps<RecordType> {
prefixCls: string;
setSelectedKeys: (selectedKeys: Key[]) => void;
selectedKeys: Key[];
confirm: (param?: FilterConfirmProps) => void;
clearFilters?: () => void;
filters?: ColumnFilterItem[];
visible: boolean;
column: ColumnType<RecordType>;
}
export interface ColumnType<RecordType = DefaultRecordType> extends RcColumnType<RecordType> {
title?: ColumnTitle<RecordType>;
// Sorter
sorter?:
| boolean
| CompareFn<RecordType>
| {
compare?: CompareFn<RecordType>;
/** Config multiple sorter order priority */
multiple?: number;
};
sortOrder?: SortOrder;
defaultSortOrder?: SortOrder;
sortDirections?: SortOrder[];
showSorterTooltip?: boolean | TooltipProps;
// Filter
filtered?: boolean;
filters?: ColumnFilterItem[];
filterDropdown?: VueNode | ((props: FilterDropdownProps<RecordType>) => VueNode);
filterMultiple?: boolean;
filteredValue?: FilterValue | null;
defaultFilteredValue?: FilterValue | null;
filterIcon?: VueNode | ((opt: { filtered: boolean; column: ColumnType }) => VueNode);
onFilter?: (value: string | number | boolean, record: RecordType) => boolean;
filterDropdownVisible?: boolean;
onFilterDropdownVisibleChange?: (visible: boolean) => void;
// Responsive
responsive?: Breakpoint[];
}
export interface ColumnGroupType<RecordType> extends Omit<ColumnType<RecordType>, 'dataIndex'> {
children: ColumnsType<RecordType>;
}
export type ColumnsType<RecordType = DefaultRecordType> = (
| ColumnGroupType<RecordType>
| ColumnType<RecordType>
)[];
export interface SelectionItem {
key: string;
text: VueNode;
onSelect?: SelectionItemSelectFn;
}
export type SelectionSelectFn<T> = (
record: T,
selected: boolean,
selectedRows: T[],
nativeEvent: Event,
) => void;
export interface TableRowSelection<T = DefaultRecordType> {
/** Keep the selection keys in list even the key not exist in `dataSource` anymore */
preserveSelectedRowKeys?: boolean;
type?: RowSelectionType;
selectedRowKeys?: Key[];
defaultSelectedRowKeys?: Key[];
onChange?: (selectedRowKeys: Key[], selectedRows: T[]) => void;
getCheckboxProps?: (record: T) => Partial<Omit<CheckboxProps, 'checked' | 'defaultChecked'>>;
onSelect?: SelectionSelectFn<T>;
onSelectMultiple?: (selected: boolean, selectedRows: T[], changeRows: T[]) => void;
/** @deprecated This function is meaningless and should use `onChange` instead */
onSelectAll?: (selected: boolean, selectedRows: T[], changeRows: T[]) => void;
/** @deprecated This function is meaningless and should use `onChange` instead */
onSelectInvert?: (selectedRowKeys: Key[]) => void;
onSelectNone?: () => void;
selections?: INTERNAL_SELECTION_ITEM[] | boolean;
hideSelectAll?: boolean;
fixed?: boolean;
columnWidth?: string | number;
columnTitle?: string | VueNode;
checkStrictly?: boolean;
renderCell?: (
value: boolean,
record: T,
index: number,
originNode: VueNode,
) => VueNode | RcRenderedCell<T>;
}
export type TransformColumns<RecordType> = (
columns: ColumnsType<RecordType>,
) => ColumnsType<RecordType>;
export interface TableCurrentDataSource<RecordType = DefaultRecordType> {
currentDataSource: RecordType[];
action: TableAction;
}
export interface SorterResult<RecordType = DefaultRecordType> {
column?: ColumnType<RecordType>;
order?: SortOrder;
field?: Key | readonly Key[];
columnKey?: Key;
}
export type GetPopupContainer = (triggerNode: HTMLElement) => HTMLElement;
type TablePaginationPosition =
| 'topLeft'
| 'topCenter'
| 'topRight'
| 'bottomLeft'
| 'bottomCenter'
| 'bottomRight';
export interface TablePaginationConfig extends PaginationProps {
position?: TablePaginationPosition[];
}
export interface TransformCellTextProps {
text: any;
column: ColumnType;
record: any;
index: number;
}

View File

@ -0,0 +1,129 @@
@import './index';
@import './size';
@table-border: @border-width-base @border-style-base @table-border-color;
.@{table-prefix-cls}.@{table-prefix-cls}-bordered {
// ============================ Title =============================
> .@{table-prefix-cls}-title {
border: @table-border;
border-bottom: 0;
}
> .@{table-prefix-cls}-container {
// ============================ Content ============================
border: @table-border;
border-right: 0;
border-bottom: 0;
> .@{table-prefix-cls}-content,
> .@{table-prefix-cls}-header,
> .@{table-prefix-cls}-body,
> .@{table-prefix-cls}-summary {
> table {
// ============================= Cell =============================
> thead > tr > th,
> tbody > tr > td,
> tfoot > tr > th,
> tfoot > tr > td {
border-right: @table-border;
}
// ============================ Header ============================
> thead {
> tr:not(:last-child) > th {
border-bottom: @border-width-base @border-style-base @table-border-color;
}
> tr > th {
&::before {
background-color: transparent !important;
}
}
}
// Fixed right should provides additional border
> thead > tr,
> tbody > tr,
> tfoot > tr {
> .@{table-prefix-cls}-cell-fix-right-first::after {
border-right: @table-border;
}
}
}
// ========================== Expandable ==========================
> table > tbody > tr > td {
> .@{table-prefix-cls}-expanded-row-fixed {
margin: -@table-padding-vertical (-@table-padding-horizontal - @border-width-base);
&::after {
position: absolute;
top: 0;
right: @border-width-base;
bottom: 0;
border-right: @table-border;
content: '';
}
}
}
}
}
&.@{table-prefix-cls}-scroll-horizontal {
> .@{table-prefix-cls}-container > .@{table-prefix-cls}-body {
> table > tbody {
> tr.@{table-prefix-cls}-expanded-row,
> tr.@{table-prefix-cls}-placeholder {
> td {
border-right: 0;
}
}
}
}
}
// Size related
&.@{table-prefix-cls}-middle {
> .@{table-prefix-cls}-container {
> .@{table-prefix-cls}-content,
> .@{table-prefix-cls}-body {
> table > tbody > tr > td {
> .@{table-prefix-cls}-expanded-row-fixed {
margin: -@table-padding-vertical-md (-@table-padding-horizontal-md - @border-width-base);
}
}
}
}
}
&.@{table-prefix-cls}-small {
> .@{table-prefix-cls}-container {
> .@{table-prefix-cls}-content,
> .@{table-prefix-cls}-body {
> table > tbody > tr > td {
> .@{table-prefix-cls}-expanded-row-fixed {
margin: -@table-padding-vertical-sm (-@table-padding-horizontal-sm - @border-width-base);
}
}
}
}
}
// ============================ Footer ============================
> .@{table-prefix-cls}-footer {
border: @table-border;
border-top: 0;
}
}
.@{table-prefix-cls}-cell {
// ============================ Nested ============================
.@{table-prefix-cls}-container:first-child {
// :first-child to avoid the case when bordered and title is set
border-top: 0;
}
&-scrollbar {
box-shadow: 0 @border-width-base 0 @border-width-base @table-header-bg;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -3,9 +3,12 @@ import './index.less';
// style dependencies
// deps-lint-skip: menu
// deps-lint-skip: grid
import '../../button/style';
import '../../empty/style';
import '../../radio/style';
import '../../checkbox/style';
import '../../dropdown/style';
import '../../spin/style';
import '../../pagination/style';
import '../../tooltip/style';

View File

@ -0,0 +1,45 @@
// ================================================================
// = Border Radio =
// ================================================================
.@{table-prefix-cls} {
/* title + table */
&-title {
border-radius: @table-border-radius-base @table-border-radius-base 0 0;
}
&-title + &-container {
border-top-left-radius: 0;
border-top-right-radius: 0;
table > thead > tr:first-child {
th:first-child {
border-radius: 0;
}
th:last-child {
border-radius: 0;
}
}
}
/* table */
&-container {
border-top-left-radius: @table-border-radius-base;
border-top-right-radius: @table-border-radius-base;
table > thead > tr:first-child {
th:first-child {
border-top-left-radius: @table-border-radius-base;
}
th:last-child {
border-top-right-radius: @table-border-radius-base;
}
}
}
/* table + footer */
&-footer {
border-radius: 0 0 @table-border-radius-base @table-border-radius-base;
}
}

View File

@ -0,0 +1,162 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@table-prefix-cls: ~'@{ant-prefix}-table';
@table-wrapepr-cls: ~'@{table-prefix-cls}-wrapper';
@table-wrapepr-rtl-cls: ~'@{table-prefix-cls}-wrapper-rtl';
.@{table-prefix-cls}-wrapper {
&-rtl {
direction: rtl;
}
}
.@{table-prefix-cls} {
&-rtl {
direction: rtl;
}
table {
.@{table-wrapepr-rtl-cls} & {
text-align: right;
}
}
// ============================ Header ============================
&-thead {
> tr {
> th {
&[colspan]:not([colspan='1']) {
.@{table-wrapepr-rtl-cls} & {
text-align: center;
}
}
.@{table-wrapepr-rtl-cls} & {
text-align: right;
}
}
}
}
// ============================= Body =============================
&-tbody {
> tr {
// ========================= Nest Table ===========================
.@{table-prefix-cls}-wrapper:only-child {
.@{table-prefix-cls}.@{table-prefix-cls}-rtl {
margin: -@table-padding-vertical (@table-padding-horizontal + ceil(@font-size-sm * 1.4)) -@table-padding-vertical -@table-padding-horizontal;
}
}
}
}
// ========================== Pagination ==========================
&-pagination {
&-left {
.@{table-wrapepr-cls}.@{table-wrapepr-rtl-cls} & {
justify-content: flex-end;
}
}
&-right {
.@{table-wrapepr-cls}.@{table-wrapepr-rtl-cls} & {
justify-content: flex-start;
}
}
}
// ================================================================
// = Function =
// ================================================================
// ============================ Sorter ============================
&-column-sorter {
.@{table-wrapepr-rtl-cls} & {
margin-right: @padding-xs;
margin-left: 0;
}
}
// ============================ Filter ============================
&-filter-column-title {
.@{table-wrapepr-rtl-cls} & {
padding: @table-padding-vertical @table-padding-horizontal @table-padding-vertical 2.3em;
}
}
&-thead tr th.@{table-prefix-cls}-column-has-sorters {
.@{table-prefix-cls}-filter-column-title {
.@{table-prefix-cls}-rtl & {
padding: 0 0 0 2.3em;
}
}
}
&-filter-trigger-container {
.@{table-wrapepr-rtl-cls} & {
right: auto;
left: 0;
}
}
// Dropdown
&-filter-dropdown {
// Checkbox
&,
&-submenu {
.@{ant-prefix}-checkbox-wrapper + span {
.@{ant-prefix}-dropdown-rtl &,
.@{ant-prefix}-dropdown-menu-submenu-rtl& {
padding-right: 8px;
padding-left: 0;
}
}
}
}
// ========================== Selections ==========================
&-selection {
.@{table-wrapepr-rtl-cls} & {
text-align: center;
}
}
// ========================== Expandable ==========================
&-row-indent {
.@{table-wrapepr-rtl-cls} & {
float: right;
}
}
&-row-expand-icon {
.@{table-wrapepr-rtl-cls} & {
float: right;
}
.@{table-prefix-cls}-row-indent + & {
.@{table-wrapepr-rtl-cls} & {
margin-right: 0;
margin-left: @padding-xs;
}
}
&::after {
.@{table-wrapepr-rtl-cls} & {
transform: rotate(-90deg);
}
}
&-collapsed::before {
.@{table-wrapepr-rtl-cls} & {
transform: rotate(180deg);
}
}
&-collapsed::after {
.@{table-wrapepr-rtl-cls} & {
transform: rotate(0deg);
}
}
}
}

View File

@ -1,38 +1,34 @@
@table-padding-vertical-md: (@table-padding-vertical * 3 / 4);
@table-padding-horizontal-md: (@table-padding-horizontal / 2);
@table-padding-vertical-sm: (@table-padding-vertical / 2);
@table-padding-horizontal-sm: (@table-padding-horizontal / 2);
@import './index';
.table-size(@size, @padding-vertical, @padding-horizontal) {
.table-size(@size, @padding-vertical, @padding-horizontal, @font-size) {
.@{table-prefix-cls}.@{table-prefix-cls}-@{size} {
> .@{table-prefix-cls}-title,
> .@{table-prefix-cls}-content > .@{table-prefix-cls}-footer {
font-size: @font-size;
.@{table-prefix-cls}-title,
.@{table-prefix-cls}-footer,
.@{table-prefix-cls}-thead > tr > th,
.@{table-prefix-cls}-tbody > tr > td,
tfoot > tr > th,
tfoot > tr > td {
padding: @padding-vertical @padding-horizontal;
}
> .@{table-prefix-cls}-content {
> .@{table-prefix-cls}-header > table,
> .@{table-prefix-cls}-body > table,
> .@{table-prefix-cls}-scroll > .@{table-prefix-cls}-header > table,
> .@{table-prefix-cls}-scroll > .@{table-prefix-cls}-body > table,
> .@{table-prefix-cls}-fixed-left > .@{table-prefix-cls}-header > table,
> .@{table-prefix-cls}-fixed-right > .@{table-prefix-cls}-header > table,
> .@{table-prefix-cls}-fixed-left
> .@{table-prefix-cls}-body-outer
> .@{table-prefix-cls}-body-inner
> table,
> .@{table-prefix-cls}-fixed-right
> .@{table-prefix-cls}-body-outer
> .@{table-prefix-cls}-body-inner
> table {
> .@{table-prefix-cls}-thead > tr > th,
> .@{table-prefix-cls}-tbody > tr > td {
padding: @padding-vertical @padding-horizontal;
}
}
.@{table-prefix-cls}-filter-trigger {
margin-right: -(@padding-horizontal / 2);
}
tr.@{table-prefix-cls}-expanded-row td > .@{table-prefix-cls}-wrapper {
margin: -@padding-vertical (-@table-padding-horizontal / 2) -@padding-vertical - 1px;
.@{table-prefix-cls}-expanded-row-fixed {
margin: -@padding-vertical -@padding-horizontal;
}
.@{table-prefix-cls}-tbody {
// ========================= Nest Table ===========================
.@{table-prefix-cls}-wrapper:only-child {
.@{table-prefix-cls} {
margin: -@padding-vertical -@padding-horizontal -@padding-vertical (@padding-horizontal +
ceil((@font-size-sm * 1.4)));
}
}
}
}
}
@ -40,14 +36,17 @@
// ================================================================
// = Middle =
// ================================================================
.table-size(~'middle', @table-padding-vertical-md, @table-padding-horizontal-md);
.table-size(~'middle', @table-padding-vertical-md, @table-padding-horizontal-md, @table-font-size-md);
// ================================================================
// = Small =
// ================================================================
.table-size(~'small', @table-padding-vertical-sm, @table-padding-horizontal-sm);
.table-size(~'small', @table-padding-vertical-sm, @table-padding-horizontal-sm, @table-font-size-sm);
.@{table-prefix-cls}-small {
.@{table-prefix-cls}-thead > tr > th {
background-color: @table-header-bg-sm;
}
.@{table-prefix-cls}-selection-column {
width: 46px;
min-width: 46px;

View File

@ -1,73 +1,63 @@
export function flatArray(data = [], childrenName = 'children') {
const result = [];
const loop = array => {
array.forEach(item => {
if (item[childrenName]) {
const newItem = { ...item };
delete newItem[childrenName];
result.push(newItem);
if (item[childrenName].length > 0) {
loop(item[childrenName]);
}
} else {
result.push(item);
}
});
};
loop(data);
return result;
import { camelize } from 'vue';
import { flattenChildren } from '../_util/props-util';
import type { ColumnType, ColumnsType, ColumnTitle, ColumnTitleProps, Key } from './interface';
export function getColumnKey<RecordType>(column: ColumnType<RecordType>, defaultKey: string): Key {
if ('key' in column && column.key !== undefined && column.key !== null) {
return column.key;
}
if (column.dataIndex) {
return (Array.isArray(column.dataIndex) ? column.dataIndex.join('.') : column.dataIndex) as Key;
}
return defaultKey;
}
export function treeMap(tree, mapper, childrenName = 'children') {
return tree.map((node, index) => {
const extra = {};
if (node[childrenName]) {
extra[childrenName] = treeMap(node[childrenName], mapper, childrenName);
export function getColumnPos(index: number, pos?: string) {
return pos ? `${pos}-${index}` : `${index}`;
}
export function renderColumnTitle<RecordType>(
title: ColumnTitle<RecordType>,
props: ColumnTitleProps<RecordType>,
) {
if (typeof title === 'function') {
return title(props);
}
return title;
}
export function convertChildrenToColumns<RecordType>(
elements: any[] = [],
): ColumnsType<RecordType> {
const flattenElements = flattenChildren(elements);
const columns = [];
flattenElements.forEach(element => {
if (!element) {
return;
}
return {
...mapper(node, index),
...extra,
};
const key = element.key;
const style = element.props?.style || {};
const cls = element.props?.class || '';
const props = element.props || {};
for (const [k, v] of Object.entries(props)) {
props[camelize(k)] = v;
}
const { default: children, ...restSlots } = element.children || {};
const column = { ...restSlots, ...props, style, class: cls };
if (key) {
column.key = key;
}
if (element.type?.__ANT_TABLE_COLUMN_GROUP) {
column.children = convertChildrenToColumns(
typeof children === 'function' ? children() : children,
);
} else {
const customRender = element.children?.default;
column.customRender = column.customRender || customRender;
}
columns.push(column);
});
}
export function flatFilter(tree, callback) {
return tree.reduce((acc, node) => {
if (callback(node)) {
acc.push(node);
}
if (node.children) {
const children = flatFilter(node.children, callback);
acc.push(...children);
}
return acc;
}, []);
}
// export function normalizeColumns (elements) {
// const columns = []
// React.Children.forEach(elements, (element) => {
// if (!React.isValidElement(element)) {
// return
// }
// const column = {
// ...element.props,
// }
// if (element.key) {
// column.key = element.key
// }
// if (element.type && element.type.__ANT_TABLE_COLUMN_GROUP) {
// column.children = normalizeColumns(column.children)
// }
// columns.push(column)
// })
// return columns
// }
export function generateValueMaps(items, maps = {}) {
(items || []).forEach(({ value, children }) => {
maps[value.toString()] = value;
generateValueMaps(children, maps);
});
return maps;
return columns;
}

View File

@ -421,340 +421,327 @@ exports[`renders ./components/transfer/demo/table-transfer.vue correctly 1`] = `
<div class="ant-spin-nested-loading">
<!---->
<div class="ant-spin-container">
<div class="ant-table ant-table-small ant-table-scroll-position-left">
<div class="ant-table ant-table-small">
<!---->
<div class="ant-table-content">
<!---->
<div class="ant-table-body">
<table class="">
<div class="ant-table-container">
<div class="ant-table-content">
<table style="table-layout: auto;">
<colgroup>
<col data-key="selection-column" class="ant-table-selection-col">
<col data-key="title">
<col data-key="description">
<col class="ant-table-selection-col">
</colgroup>
<thead class="ant-table-thead">
<tr>
<th colstart="0" colspan="1" colend="0" rowspan="1" class="ant-table-selection-column"><span class="ant-table-header-column"><div><span class="ant-table-column-title"><div class="ant-table-selection"><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label>
<th class="ant-table-cell ant-table-selection-column" colstart="0" colend="0">
<!---->
</div></span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="1" colspan="1" colend="1" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Name</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="2" colspan="1" colend="2" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Description</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
</tr>
</thead>
<tbody class="ant-table-tbody">
<tr class="ant-table-row ant-table-row-level-0" data-row-key="0">
<td class="ant-table-selection-column">
<!---->
<!----><span><label class="ant-checkbox-wrapper ant-checkbox-wrapper-disabled"><span class="ant-checkbox ant-checkbox-disabled"><input type="checkbox" disabled="" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label></span>
</td>
<td class="">
<!---->
<!---->content1
</td>
<td class="">
<!---->
<!---->description of content1
</td>
</tr>
<tr class="ant-table-row ant-table-row-level-0" data-row-key="1">
<td class="ant-table-selection-column">
<!---->
<!----><span><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label></span>
</td>
<td class="">
<!---->
<!---->content2
</td>
<td class="">
<!---->
<!---->description of content2
</td>
</tr>
<tr class="ant-table-row ant-table-row-level-0" data-row-key="3">
<td class="ant-table-selection-column">
<!---->
<!----><span><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label></span>
</td>
<td class="">
<!---->
<!---->content4
</td>
<td class="">
<!---->
<!---->description of content4
</td>
</tr>
<tr class="ant-table-row ant-table-row-level-0" data-row-key="4">
<td class="ant-table-selection-column">
<!---->
<!----><span><label class="ant-checkbox-wrapper ant-checkbox-wrapper-disabled"><span class="ant-checkbox ant-checkbox-disabled"><input type="checkbox" disabled="" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label></span>
</td>
<td class="">
<!---->
<!---->content5
</td>
<td class="">
<!---->
<!---->description of content5
</td>
</tr>
<tr class="ant-table-row ant-table-row-level-0" data-row-key="6">
<td class="ant-table-selection-column">
<!---->
<!----><span><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label></span>
</td>
<td class="">
<!---->
<!---->content7
</td>
<td class="">
<!---->
<!---->description of content7
</td>
</tr>
<tr class="ant-table-row ant-table-row-level-0" data-row-key="7">
<td class="ant-table-selection-column">
<!---->
<!----><span><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label></span>
</td>
<td class="">
<!---->
<!---->content8
</td>
<td class="">
<!---->
<!---->description of content8
</td>
</tr>
<tr class="ant-table-row ant-table-row-level-0" data-row-key="9">
<td class="ant-table-selection-column">
<!---->
<!----><span><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label></span>
</td>
<td class="">
<!---->
<!---->content10
</td>
<td class="">
<!---->
<!---->description of content10
</td>
</tr>
<tr class="ant-table-row ant-table-row-level-0" data-row-key="10">
<td class="ant-table-selection-column">
<!---->
<!----><span><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label></span>
</td>
<td class="">
<!---->
<!---->content11
</td>
<td class="">
<!---->
<!---->description of content11
</td>
</tr>
<tr class="ant-table-row ant-table-row-level-0" data-row-key="12">
<td class="ant-table-selection-column">
<!---->
<!----><span><label class="ant-checkbox-wrapper ant-checkbox-wrapper-disabled"><span class="ant-checkbox ant-checkbox-disabled"><input type="checkbox" disabled="" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label></span>
</td>
<td class="">
<!---->
<!---->content13
</td>
<td class="">
<!---->
<!---->description of content13
</td>
</tr>
<tr class="ant-table-row ant-table-row-level-0" data-row-key="13">
<td class="ant-table-selection-column">
<!---->
<!----><span><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label></span>
</td>
<td class="">
<!---->
<!---->content14
</td>
<td class="">
<!---->
<!---->description of content14
</td>
</tr>
</tbody>
</table>
</div>
<!---->
<!---->
</div>
</div>
<ul unselectable="on" class="ant-pagination mini ant-table-pagination">
<!---->
<li title="Previous Page" class="ant-pagination-prev ant-pagination-disabled" aria-disabled="true"><button class="ant-pagination-item-link" type="button" tabindex="-1" disabled=""><span role="img" aria-label="left" class="anticon anticon-left"><svg focusable="false" class="" data-icon="left" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"></path></svg></span></button></li>
<li title="1" tabindex="0" class="ant-pagination-item ant-pagination-item-1 ant-pagination-item-active"><a rel="nofollow">1</a></li>
<li title="2" tabindex="0" class="ant-pagination-item ant-pagination-item-2"><a rel="nofollow">2</a></li>
<li title="Next Page" tabindex="0" class="ant-pagination-next" aria-disabled="false"><button class="ant-pagination-item-link" type="button" tabindex="-1"><span role="img" aria-label="right" class="anticon anticon-right"><svg focusable="false" class="" data-icon="right" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"></path></svg></span></button></li>
<!---->
</ul>
</div>
</div>
</div>
</div>
</div>
<!---->
</div>
<div class="ant-transfer-operation"><button disabled="" class="ant-btn ant-btn-primary ant-btn-sm ant-btn-icon-only" type="button"><span role="img" aria-label="right" class="anticon anticon-right"><svg focusable="false" class="" data-icon="right" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"></path></svg></span></button><button disabled="" class="ant-btn ant-btn-primary ant-btn-sm ant-btn-icon-only" type="button"><span role="img" aria-label="left" class="anticon anticon-left"><svg focusable="false" class="" data-icon="left" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"></path></svg></span></button></div>
<div class="ant-transfer-list">
<div class="ant-transfer-list-header">
<!---->
<!----><span tabindex="-1" role="img" aria-label="down" class="anticon anticon-down ant-dropdown-trigger ant-transfer-list-header-dropdown"><svg focusable="false" class="" data-icon="down" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"></path></svg></span><span class="ant-transfer-list-header-selected"><span>6 items</span><span class="ant-transfer-list-header-title"></span></span>
</div>
<div class="ant-transfer-list-body">
<!---->
<div class="ant-transfer-list-body-customize-wrapper">
<div class="ant-table-wrapper">
<div class="ant-spin-nested-loading">
<!---->
<div class="ant-spin-container">
<div class="ant-table ant-table-small ant-table-scroll-position-left">
<!---->
<div class="ant-table-content">
<!---->
<div class="ant-table-body">
<table class="">
<colgroup>
<col data-key="selection-column" class="ant-table-selection-col">
<col data-key="title">
</colgroup>
<thead class="ant-table-thead">
<tr>
<th colstart="0" colspan="1" colend="0" rowspan="1" class="ant-table-selection-column"><span class="ant-table-header-column"><div><span class="ant-table-column-title"><div class="ant-table-selection"><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label>
<div class="ant-table-selection"><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
<!---->
</div>
</th>
<th class="ant-table-cell" colstart="1" colend="1">
<!---->Name
</th>
<th class="ant-table-cell" colstart="2" colend="2">
<!---->Description
</th>
</tr>
</thead>
<tbody class="ant-table-tbody">
<!---->
</div></span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
<th colstart="1" colspan="1" colend="1" rowspan="1" class=""><span class="ant-table-header-column"><div><span class="ant-table-column-title">Name</span><span class="ant-table-column-sorter"><!----></span>
</div></span>
<!---->
</th>
</tr>
</thead>
<tbody class="ant-table-tbody">
<tr class="ant-table-row ant-table-row-level-0" data-row-key="2">
<td class="ant-table-selection-column">
<tr data-row-key="0" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-with-append ant-table-selection-column">
<!----><label class="ant-checkbox-wrapper ant-checkbox-wrapper-disabled"><span class="ant-checkbox ant-checkbox-disabled"><input type="checkbox" disabled="" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->content1
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->description of content1
</td>
</tr>
<!---->
<tr data-row-key="1" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-with-append ant-table-selection-column">
<!----><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->content2
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->description of content2
</td>
</tr>
<!---->
<tr data-row-key="3" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-with-append ant-table-selection-column">
<!----><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->content4
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->description of content4
</td>
</tr>
<!---->
<tr data-row-key="4" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-with-append ant-table-selection-column">
<!----><label class="ant-checkbox-wrapper ant-checkbox-wrapper-disabled"><span class="ant-checkbox ant-checkbox-disabled"><input type="checkbox" disabled="" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->content5
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->description of content5
</td>
</tr>
<!---->
<tr data-row-key="6" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-with-append ant-table-selection-column">
<!----><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->content7
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->description of content7
</td>
</tr>
<!---->
<tr data-row-key="7" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-with-append ant-table-selection-column">
<!----><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->content8
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->description of content8
</td>
</tr>
<!---->
<tr data-row-key="9" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-with-append ant-table-selection-column">
<!----><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->content10
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->description of content10
</td>
</tr>
<!---->
<tr data-row-key="10" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-with-append ant-table-selection-column">
<!----><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->content11
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->description of content11
</td>
</tr>
<!---->
<tr data-row-key="12" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-with-append ant-table-selection-column">
<!----><label class="ant-checkbox-wrapper ant-checkbox-wrapper-disabled"><span class="ant-checkbox ant-checkbox-disabled"><input type="checkbox" disabled="" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->content13
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->description of content13
</td>
</tr>
<!---->
<tr data-row-key="13" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-with-append ant-table-selection-column">
<!----><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->content14
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->description of content14
</td>
</tr>
<!---->
</tbody>
<!---->
</table>
</div>
</div>
<!---->
<!----><span><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label></span>
</td>
<td class="">
</div>
<ul unselectable="on" class="ant-pagination mini ant-table-pagination ant-table-pagination-right">
<!---->
<!---->content3
</td>
</tr>
<tr class="ant-table-row ant-table-row-level-0" data-row-key="5">
<td class="ant-table-selection-column">
<li title="Previous Page" class="ant-pagination-prev ant-pagination-disabled" aria-disabled="true"><button class="ant-pagination-item-link" type="button" tabindex="-1" disabled=""><span role="img" aria-label="left" class="anticon anticon-left"><svg focusable="false" class="" data-icon="left" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"></path></svg></span></button></li>
<li title="1" tabindex="0" class="ant-pagination-item ant-pagination-item-1 ant-pagination-item-active"><a rel="nofollow">1</a></li>
<li title="2" tabindex="0" class="ant-pagination-item ant-pagination-item-2"><a rel="nofollow">2</a></li>
<li title="Next Page" tabindex="0" class="ant-pagination-next" aria-disabled="false"><button class="ant-pagination-item-link" type="button" tabindex="-1"><span role="img" aria-label="right" class="anticon anticon-right"><svg focusable="false" class="" data-icon="right" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"></path></svg></span></button></li>
<!---->
<!----><span><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label></span>
</td>
<td class="">
<!---->
<!---->content6
</td>
</tr>
<tr class="ant-table-row ant-table-row-level-0" data-row-key="8">
<td class="ant-table-selection-column">
<!---->
<!----><span><label class="ant-checkbox-wrapper ant-checkbox-wrapper-disabled"><span class="ant-checkbox ant-checkbox-disabled"><input type="checkbox" disabled="" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label></span>
</td>
<td class="">
<!---->
<!---->content9
</td>
</tr>
<tr class="ant-table-row ant-table-row-level-0" data-row-key="11">
<td class="ant-table-selection-column">
<!---->
<!----><span><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label></span>
</td>
<td class="">
<!---->
<!---->content12
</td>
</tr>
<tr class="ant-table-row ant-table-row-level-0" data-row-key="14">
<td class="ant-table-selection-column">
<!---->
<!----><span><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label></span>
</td>
<td class="">
<!---->
<!---->content15
</td>
</tr>
<tr class="ant-table-row ant-table-row-level-0" data-row-key="17">
<td class="ant-table-selection-column">
<!---->
<!----><span><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!----></label></span>
</td>
<td class="">
<!---->
<!---->content18
</td>
</tr>
</tbody>
</table>
</ul>
</div>
</div>
</div>
<!---->
<!---->
</div>
</div>
<ul unselectable="on" class="ant-pagination mini ant-table-pagination">
<!---->
<li title="Previous Page" class="ant-pagination-prev ant-pagination-disabled" aria-disabled="true"><button class="ant-pagination-item-link" type="button" tabindex="-1" disabled=""><span role="img" aria-label="left" class="anticon anticon-left"><svg focusable="false" class="" data-icon="left" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"></path></svg></span></button></li>
<li title="1" tabindex="0" class="ant-pagination-item ant-pagination-item-1 ant-pagination-item-active"><a rel="nofollow">1</a></li>
<li title="Next Page" class="ant-pagination-next ant-pagination-disabled" aria-disabled="true"><button class="ant-pagination-item-link" type="button" tabindex="-1" disabled=""><span role="img" aria-label="right" class="anticon anticon-right"><svg focusable="false" class="" data-icon="right" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"></path></svg></span></button></li>
<!---->
</ul>
<!---->
</div>
</div>
<div class="ant-transfer-operation"><button disabled="" class="ant-btn ant-btn-primary ant-btn-sm ant-btn-icon-only" type="button"><span role="img" aria-label="right" class="anticon anticon-right"><svg focusable="false" class="" data-icon="right" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"></path></svg></span></button><button disabled="" class="ant-btn ant-btn-primary ant-btn-sm ant-btn-icon-only" type="button"><span role="img" aria-label="left" class="anticon anticon-left"><svg focusable="false" class="" data-icon="left" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"></path></svg></span></button></div>
<div class="ant-transfer-list">
<div class="ant-transfer-list-header">
<!---->
<!----><span tabindex="-1" role="img" aria-label="down" class="anticon anticon-down ant-dropdown-trigger ant-transfer-list-header-dropdown"><svg focusable="false" class="" data-icon="down" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"></path></svg></span><span class="ant-transfer-list-header-selected"><span>6 items</span><span class="ant-transfer-list-header-title"></span></span>
</div>
<div class="ant-transfer-list-body">
<!---->
<div class="ant-transfer-list-body-customize-wrapper">
<div class="ant-table-wrapper">
<div class="ant-spin-nested-loading">
<!---->
<div class="ant-spin-container">
<div class="ant-table ant-table-small">
<!---->
<div class="ant-table-container">
<div class="ant-table-content">
<table style="table-layout: auto;">
<colgroup>
<col class="ant-table-selection-col">
</colgroup>
<thead class="ant-table-thead">
<tr>
<th class="ant-table-cell ant-table-selection-column" colstart="0" colend="0">
<!---->
<div class="ant-table-selection"><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
<!---->
</div>
</th>
<th class="ant-table-cell" colstart="1" colend="1">
<!---->Name
</th>
</tr>
</thead>
<tbody class="ant-table-tbody">
<!---->
<tr data-row-key="2" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-with-append ant-table-selection-column">
<!----><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->content3
</td>
</tr>
<!---->
<tr data-row-key="5" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-with-append ant-table-selection-column">
<!----><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->content6
</td>
</tr>
<!---->
<tr data-row-key="8" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-with-append ant-table-selection-column">
<!----><label class="ant-checkbox-wrapper ant-checkbox-wrapper-disabled"><span class="ant-checkbox ant-checkbox-disabled"><input type="checkbox" disabled="" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->content9
</td>
</tr>
<!---->
<tr data-row-key="11" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-with-append ant-table-selection-column">
<!----><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->content12
</td>
</tr>
<!---->
<tr data-row-key="14" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-with-append ant-table-selection-column">
<!----><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->content15
</td>
</tr>
<!---->
<tr data-row-key="17" class="ant-table-row ant-table-row-level-0">
<td class="ant-table-cell ant-table-cell-with-append ant-table-selection-column">
<!----><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label>
</td>
<td class="ant-table-cell ant-table-cell-with-append">
<!---->content18
</td>
</tr>
<!---->
</tbody>
<!---->
</table>
</div>
</div>
<!---->
</div>
<ul unselectable="on" class="ant-pagination mini ant-table-pagination ant-table-pagination-right">
<!---->
<li title="Previous Page" class="ant-pagination-prev ant-pagination-disabled" aria-disabled="true"><button class="ant-pagination-item-link" type="button" tabindex="-1" disabled=""><span role="img" aria-label="left" class="anticon anticon-left"><svg focusable="false" class="" data-icon="left" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"></path></svg></span></button></li>
<li title="1" tabindex="0" class="ant-pagination-item ant-pagination-item-1 ant-pagination-item-active"><a rel="nofollow">1</a></li>
<li title="Next Page" class="ant-pagination-next ant-pagination-disabled" aria-disabled="true"><button class="ant-pagination-item-link" type="button" tabindex="-1" disabled=""><span role="img" aria-label="right" class="anticon anticon-right"><svg focusable="false" class="" data-icon="right" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"></path></svg></span></button></li>
<!---->
</ul>
</div>
</div>
</div>
</div>
</div>
<!---->
</div>
</div><button style="margin-top: 16px;" type="button" role="switch" aria-checked="false" class="ant-switch">
<!----><span class="ant-switch-inner">disabled</span>
</button><button style="margin-top: 16px;" type="button" role="switch" aria-checked="false" class="ant-switch">
<!----><span class="ant-switch-inner">showSearch</span>
</button>
</div>
</div>
</div>
<!---->
</div>
</div><button style="margin-top: 16px;" type="button" role="switch" aria-checked="false" class="ant-switch">
<!----><span class="ant-switch-inner">disabled</span>
</button><button style="margin-top: 16px;" type="button" role="switch" aria-checked="false" class="ant-switch">
<!----><span class="ant-switch-inner">showSearch</span>
</button></div>
`;
exports[`renders ./components/transfer/demo/tree-transfer.vue correctly 1`] = `

View File

@ -0,0 +1,238 @@
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 '../../_util/classNames';
import { parseStyleText } from '../../_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>>({
name: 'BodyRow',
inheritAttrs: false,
props: [
'record',
'index',
'recordKey',
'expandedKeys',
'rowComponent',
'cellComponent',
'customRow',
'rowExpandable',
'indent',
'rowKey',
'getRowKey',
'childrenColumnName',
] as any,
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 ===========================
const 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);
}
};
const 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);
}
return '';
});
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, transformCellText } = 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];
let additionalCellProps;
if (column.customCell) {
additionalCellProps = column.customCell(record, index);
}
return (
<Cell
cellType="body"
class={columnClassName}
ellipsis={column.ellipsis}
align={column.align}
component={cellComponent}
prefixCls={prefixCls}
key={key}
record={record}
index={index}
dataIndex={dataIndex}
customRender={customRender}
{...fixedInfo}
additionalProps={additionalCellProps}
column={column}
transformCellText={transformCellText}
v-slots={{
// ============= Used for nest expandable =============
appendNode: () => {
if (colIndex === (expandIconColumnIndex || 0) && nestExpandable.value) {
return (
<>
<span
style={{ paddingLeft: `${indentSize * indent}px` }}
class={`${prefixCls}-row-indent indent-level-${indent}`}
/>
{expandIcon({
prefixCls,
expanded: expanded.value,
expandable: hasNestChildren.value,
record,
onExpand: onInternalTriggerExpand,
})}
</>
);
}
return null;
},
}}
/>
);
})}
</RowComponent>
);
// ======================== Expand Row =========================
let expandRowNode;
if (rowSupportExpand.value && (expandRended.value || expanded.value)) {
const expandContent = expandedRowRender({
record,
index,
indent: indent + 1,
expanded: 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,84 @@
import type { 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',
inheritAttrs: false,
props: [
'prefixCls',
'component',
'cellComponent',
'fixHeader',
'fixColumn',
'horizonScroll',
'componentWidth',
'expanded',
'colSpan',
] as any,
setup(props, { slots, attrs }) {
const tableContext = useInjectTable();
return () => {
const {
prefixCls,
component: Component,
cellComponent,
fixHeader,
fixColumn,
expanded,
componentWidth,
colSpan,
} = props;
return (
<Component
class={attrs.class}
style={{
display: expanded ? null : 'none',
}}
>
<Cell
component={cellComponent}
prefixCls={prefixCls}
colSpan={colSpan}
v-slots={{
default: () => {
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 contentNode;
},
}}
></Cell>
</Component>
);
};
},
});

View File

@ -0,0 +1,34 @@
import { defineComponent, onMounted, ref } from 'vue';
import VCResizeObserver from '../../vc-resize-observer';
import type { Key } from '../interface';
export interface MeasureCellProps {
columnKey: Key;
onColumnResize: (key: Key, width: number) => void;
}
export default defineComponent<MeasureCellProps>({
name: 'MeasureCell',
props: ['columnKey'] as any,
setup(props, { emit }) {
const tdRef = ref();
onMounted(() => {
if (tdRef.value) {
emit('columnResize', props.columnKey, tdRef.value.offsetWidth);
}
});
return () => {
return (
<VCResizeObserver
onResize={({ offsetWidth }) => {
emit('columnResize', props.columnKey, offsetWidth);
}}
>
<td ref={tdRef} style={{ padding: 0, border: 0, height: 0 }}>
<div style={{ height: 0, overflow: 'hidden' }}>&nbsp;</div>
</td>
</VCResizeObserver>
);
};
},
});

View File

@ -0,0 +1,133 @@
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;
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 class={`${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

@ -0,0 +1,276 @@
import classNames from '../../_util/classNames';
import { flattenChildren, isValidElement, parseStyleText } from '../../_util/props-util';
import { CSSProperties, HTMLAttributes } from 'vue';
import { defineComponent, isVNode } from 'vue';
import type {
DataIndex,
ColumnType,
RenderedCell,
CustomizeComponent,
CellType,
DefaultRecordType,
AlignType,
CellEllipsisType,
TransformCellText,
} from '../interface';
import { getPathValue, validateValue } from '../utils/valueUtil';
import { useInjectSlots } from '../../table/context';
import { INTERNAL_COL_DEFINE } from '../utils/legacyUtil';
function isRenderCell<RecordType = DefaultRecordType>(
data: RenderedCell<RecordType>,
): data is RenderedCell<RecordType> {
return data && typeof data === 'object' && !Array.isArray(data) && !isValidElement(data);
}
export interface CellProps<RecordType = DefaultRecordType> {
prefixCls?: string;
record?: RecordType;
/** `record` index. Not `column` index. */
index?: number;
dataIndex?: DataIndex;
customRender?: ColumnType<RecordType>['customRender'];
component?: CustomizeComponent;
colSpan?: number;
rowSpan?: number;
ellipsis?: CellEllipsisType;
align?: AlignType;
// Fixed
fixLeft?: number | false;
fixRight?: number | false;
firstFixLeft?: boolean;
lastFixLeft?: boolean;
firstFixRight?: boolean;
lastFixRight?: boolean;
// Additional
/** @private Used for `expandable` with nest tree */
appendNode?: any;
additionalProps?: HTMLAttributes;
rowType?: 'header' | 'body' | 'footer';
isSticky?: boolean;
column?: ColumnType<RecordType>;
cellType?: 'header' | 'body';
transformCellText?: TransformCellText<RecordType>;
}
export default defineComponent<CellProps>({
name: 'Cell',
props: [
'prefixCls',
'record',
'index',
'dataIndex',
'customRender',
'component',
'colSpan',
'rowSpan',
'fixLeft',
'fixRight',
'firstFixLeft',
'lastFixLeft',
'firstFixRight',
'lastFixRight',
'appendNode',
'additionalProps',
'ellipsis',
'align',
'rowType',
'isSticky',
'column',
'cellType',
'transformCellText',
] as any,
slots: ['appendNode'],
setup(props, { slots }) {
const contextSlots = useInjectSlots();
return () => {
const {
prefixCls,
record,
index,
dataIndex,
customRender,
component: Component = 'td',
colSpan,
rowSpan,
fixLeft,
fixRight,
firstFixLeft,
lastFixLeft,
firstFixRight,
lastFixRight,
appendNode = slots.appendNode?.(),
additionalProps = {},
ellipsis,
align,
rowType,
isSticky,
column = {},
cellType,
} = props;
const cellPrefixCls = `${prefixCls}-cell`;
// ==================== Child Node ====================
let cellProps: CellType;
let childNode;
const children = slots.default?.();
if (validateValue(children) || cellType === 'header') {
childNode = children;
} else {
const value = getPathValue(record, dataIndex);
// Customize render node
childNode = value;
if (customRender) {
const renderData = customRender({
text: value,
value,
record,
index,
column: column.__originColumn__,
});
if (isRenderCell(renderData)) {
childNode = renderData.children;
cellProps = renderData.props;
} else {
childNode = renderData;
}
}
if (
!(INTERNAL_COL_DEFINE in column) &&
cellType === 'body' &&
contextSlots.value.bodyCell &&
!column.slots?.customRender
) {
childNode = flattenChildren(
contextSlots.value.bodyCell({
text: value,
value,
record,
index,
column: column.__originColumn__,
}) as any,
);
}
/** maybe we should @deprecated */
if (props.transformCellText) {
childNode = props.transformCellText({
text: childNode,
record,
index,
column: column.__originColumn__,
});
}
}
// Not crash if final `childNode` is not validate ReactNode
if (
typeof childNode === 'object' &&
!Array.isArray(childNode) &&
!isValidElement(childNode)
) {
childNode = null;
}
if (ellipsis && (lastFixLeft || firstFixRight)) {
childNode = <span class={`${cellPrefixCls}-content`}>{childNode}</span>;
}
if (Array.isArray(childNode) && childNode.length === 1) {
childNode = childNode[0];
}
const {
colSpan: cellColSpan,
rowSpan: cellRowSpan,
style: cellStyle,
class: cellClassName,
...restCellProps
} = cellProps || {};
const mergedColSpan = cellColSpan !== undefined ? cellColSpan : colSpan;
const mergedRowSpan = cellRowSpan !== undefined ? cellRowSpan : rowSpan;
if (mergedColSpan === 0 || mergedRowSpan === 0) {
return null;
}
// ====================== Fixed =======================
const fixedStyle: CSSProperties = {};
const isFixLeft = typeof fixLeft === 'number';
const isFixRight = typeof fixRight === 'number';
if (isFixLeft) {
fixedStyle.position = 'sticky';
fixedStyle.left = `${fixLeft}px`;
}
if (isFixRight) {
fixedStyle.position = 'sticky';
fixedStyle.right = `${fixRight}px`;
}
// ====================== Align =======================
const alignStyle: CSSProperties = {};
if (align) {
alignStyle.textAlign = align;
}
// ====================== Render ======================
let title: string;
const ellipsisConfig: CellEllipsisType = ellipsis === true ? { showTitle: true } : ellipsis;
if (ellipsisConfig && (ellipsisConfig.showTitle || rowType === 'header')) {
if (typeof childNode === 'string' || typeof childNode === 'number') {
title = childNode.toString();
} else if (isVNode(childNode) && typeof childNode.children === 'string') {
title = childNode.children;
}
}
const componentProps = {
title,
...restCellProps,
...additionalProps,
colSpan: mergedColSpan && mergedColSpan !== 1 ? mergedColSpan : null,
rowSpan: mergedRowSpan && mergedRowSpan !== 1 ? mergedRowSpan : null,
class: classNames(
cellPrefixCls,
{
[`${cellPrefixCls}-fix-left`]: isFixLeft,
[`${cellPrefixCls}-fix-left-first`]: firstFixLeft,
[`${cellPrefixCls}-fix-left-last`]: lastFixLeft,
[`${cellPrefixCls}-fix-right`]: isFixRight,
[`${cellPrefixCls}-fix-right-first`]: firstFixRight,
[`${cellPrefixCls}-fix-right-last`]: lastFixRight,
[`${cellPrefixCls}-ellipsis`]: ellipsis,
[`${cellPrefixCls}-with-append`]: appendNode,
[`${cellPrefixCls}-fix-sticky`]: (isFixLeft || isFixRight) && isSticky,
},
additionalProps.class,
cellClassName,
),
style: {
...parseStyleText(additionalProps.style as any),
...alignStyle,
...fixedStyle,
...cellStyle,
},
};
return (
<Component {...componentProps}>
{appendNode}
{childNode}
</Component>
);
};
},
});

View File

@ -0,0 +1,37 @@
import type { ColumnType } from './interface';
import { INTERNAL_COL_DEFINE } from './utils/legacyUtil';
export interface ColGroupProps<RecordType> {
colWidths: readonly (number | string)[];
columns?: readonly ColumnType<RecordType>[];
columCount?: number;
}
function ColGroup<RecordType>({ colWidths, columns, columCount }: ColGroupProps<RecordType>) {
const cols = [];
const len = columCount || columns.length;
// Only insert col with width & additional props
// Skip if rest col do not have any useful info
let mustInsert = false;
for (let i = len - 1; i >= 0; i -= 1) {
const width = colWidths[i];
const column = columns && columns[i];
const additionalProps = column && column[INTERNAL_COL_DEFINE];
if (width || additionalProps || mustInsert) {
cols.unshift(
<col
key={i}
style={{ width: typeof width === 'number' ? `${width}px` : width }}
{...additionalProps}
/>,
);
mustInsert = true;
}
}
return <colgroup>{cols}</colgroup>;
}
export default ColGroup;

View File

@ -0,0 +1,190 @@
import type { HeaderProps } from '../Header/Header';
import ColGroup from '../ColGroup';
import type { ColumnsType, ColumnType, DefaultRecordType } from '../interface';
import type { Ref } from 'vue';
import {
computed,
defineComponent,
nextTick,
onBeforeUnmount,
onMounted,
ref,
toRef,
watchEffect,
} from 'vue';
import { useInjectTable } from '../context/TableContext';
import classNames from '../../_util/classNames';
import addEventListenerWrap from '../../vc-util/Dom/addEventListener';
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',
inheritAttrs: false,
props: [
'columns',
'flattenColumns',
'stickyOffsets',
'customHeaderRow',
'noData',
'maxContentScroll',
'colWidths',
'columCount',
'direction',
'fixHeader',
'stickyTopOffset',
'stickyBottomOffset',
'stickyClassName',
] as any,
emits: ['scroll'],
setup(props, { attrs, slots, emit }) {
const tableContext = useInjectTable();
const combinationScrollBarSize = computed(() =>
tableContext.isSticky && !props.fixHeader ? 0 : tableContext.scrollbarSize,
);
const scrollRef = ref();
const onWheel = (e: WheelEvent) => {
const { currentTarget, deltaX } = e;
if (deltaX) {
emit('scroll', { currentTarget, scrollLeft: (currentTarget as any).scrollLeft + deltaX });
e.preventDefault();
}
};
const wheelEvent = ref();
onMounted(() => {
nextTick(() => {
wheelEvent.value = addEventListenerWrap(scrollRef.value, 'wheel', onWheel);
});
});
onBeforeUnmount(() => {
wheelEvent.value?.remove();
});
// 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.value
? [...props.columns, ScrollBarColumn]
: props.columns;
flattenColumnsWithScrollbar.value = combinationScrollBarSize.value
? [...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,57 @@
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 {
index: number;
colSpan?: number;
rowSpan?: number;
align?: AlignType;
}
export default defineComponent<SummaryCellProps>({
name: 'SummaryCell',
inheritAttrs: false,
props: ['index', 'colSpan', 'rowSpan', 'align'] as any,
setup(props, { attrs, slots }) {
const tableContext = useInjectTable();
const summaryContext = useInjectSummary();
return () => {
const { index, colSpan = 1, 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
class={attrs.class as string}
index={index}
component="td"
prefixCls={prefixCls}
record={null}
dataIndex={null}
align={align}
customRender={() => ({
children: slots.default?.(),
props: {
colSpan: mergedColSpan,
rowSpan,
},
})}
{...fixedInfo}
/>
);
};
},
});

View File

@ -0,0 +1,8 @@
import { defineComponent } from 'vue';
export default defineComponent({
name: 'FooterRow',
setup(_props, { slots }) {
return () => <tr>{slots.default?.()}</tr>;
},
});

View File

@ -0,0 +1,26 @@
import { computed, defineComponent, onBeforeUnmount, watchEffect } from 'vue';
import { useInjectTable } from '../context/TableContext';
export interface SummaryProps {
fixed?: boolean | 'top' | 'bottom';
}
let indexGuid = 0;
const Summary = defineComponent<SummaryProps>({
name: 'Summary',
props: ['fixed'] as any,
setup(props, { slots }) {
const tableContext = useInjectTable();
const uniKey = `table-summary-uni-key-${++indexGuid}`;
const fixed = computed(() => (props.fixed as string) === '' || props.fixed);
watchEffect(() => {
tableContext.summaryCollect(uniKey, fixed.value);
});
onBeforeUnmount(() => {
tableContext.summaryCollect(uniKey, false);
});
return () => slots.default?.();
},
});
export default Summary;

View File

@ -0,0 +1,40 @@
import Summary from './Summary';
import SummaryRow from './Row';
import SummaryCell from './Cell';
import type { DefaultRecordType, StickyOffsets } from '../interface';
import { computed, defineComponent, reactive, toRef } from 'vue';
import type { FlattenColumns } from '../context/SummaryContext';
import { useProvideSummary } from '../context/SummaryContext';
import { useInjectTable } from '../context/TableContext';
export interface FooterProps<RecordType = DefaultRecordType> {
stickyOffsets: StickyOffsets;
flattenColumns: FlattenColumns<RecordType>;
}
export default defineComponent({
name: 'Footer',
inheritAttrs: false,
props: ['stickyOffsets', 'flattenColumns'],
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 { SummaryRow, SummaryCell };
export const FooterComponents = Summary;

View File

@ -0,0 +1,128 @@
import classNames from '../../_util/classNames';
import { computed, defineComponent } from 'vue';
import { useInjectTable } from '../context/TableContext';
import type {
ColumnsType,
CellType,
StickyOffsets,
ColumnType,
GetComponentProps,
ColumnGroupType,
DefaultRecordType,
} from '../interface';
import HeaderRow from './HeaderRow';
function parseHeaderRows<RecordType>(
rootColumns: ColumnsType<RecordType>,
): CellType<RecordType>[][] {
const rows: CellType<RecordType>[][] = [];
function fillRowCells(
columns: ColumnsType<RecordType>,
colIndex: number,
rowIndex = 0,
): number[] {
// Init rows
rows[rowIndex] = rows[rowIndex] || [];
let currentColIndex = colIndex;
const colSpans: number[] = columns.filter(Boolean).map(column => {
const cell: CellType<RecordType> = {
key: column.key,
class: classNames(column.className, column.class),
// children: column.title,
column,
colStart: currentColIndex,
};
let colSpan = 1;
const subColumns = (column as ColumnGroupType<RecordType>).children;
if (subColumns && subColumns.length > 0) {
colSpan = fillRowCells(subColumns, currentColIndex, rowIndex + 1).reduce(
(total, count) => total + count,
0,
);
cell.hasSubColumns = true;
}
if ('colSpan' in column) {
({ colSpan } = column);
}
if ('rowSpan' in column) {
cell.rowSpan = column.rowSpan;
}
cell.colSpan = colSpan;
cell.colEnd = cell.colStart + colSpan - 1;
rows[rowIndex].push(cell);
currentColIndex += colSpan;
return colSpan;
});
return colSpans;
}
// Generate `rows` cell data
fillRowCells(rootColumns, 0);
// Handle `rowSpan`
const rowCount = rows.length;
for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) {
rows[rowIndex].forEach(cell => {
if (!('rowSpan' in cell) && !cell.hasSubColumns) {
// eslint-disable-next-line no-param-reassign
cell.rowSpan = rowCount - rowIndex;
}
});
}
return rows;
}
export interface HeaderProps<RecordType = DefaultRecordType> {
columns: ColumnsType<RecordType>;
flattenColumns: readonly ColumnType<RecordType>[];
stickyOffsets: StickyOffsets;
customHeaderRow: GetComponentProps<readonly ColumnType<RecordType>[]>;
}
export default defineComponent<HeaderProps>({
name: 'Header',
inheritAttrs: false,
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, customHeaderRow } = props;
const WrapperComponent = getComponent(['header', 'wrapper'], 'thead');
const trComponent = getComponent(['header', 'row'], 'tr');
const thComponent = getComponent(['header', 'cell'], 'th');
return (
<WrapperComponent class={`${prefixCls}-thead`}>
{rows.value.map((row, rowIndex) => {
const rowNode = (
<HeaderRow
key={rowIndex}
flattenColumns={flattenColumns}
cells={row}
stickyOffsets={stickyOffsets}
rowComponent={trComponent}
cellComponent={thComponent}
customHeaderRow={customHeaderRow}
index={rowIndex}
/>
);
return rowNode;
})}
</WrapperComponent>
);
};
},
});

View File

@ -0,0 +1,98 @@
import { defineComponent } from 'vue';
import Cell from '../Cell';
import { useInjectTable } from '../context/TableContext';
import type {
CellType,
StickyOffsets,
ColumnType,
CustomizeComponent,
GetComponentProps,
DefaultRecordType,
} from '../interface';
import { getCellFixedInfo } from '../utils/fixUtil';
import { getColumnsKey } from '../utils/valueUtil';
export interface RowProps<RecordType = DefaultRecordType> {
cells: readonly CellType<RecordType>[];
stickyOffsets: StickyOffsets;
flattenColumns: readonly ColumnType<RecordType>[];
rowComponent: CustomizeComponent;
cellComponent: CustomizeComponent;
customHeaderRow: GetComponentProps<readonly ColumnType<RecordType>[]>;
index: number;
}
export default defineComponent<RowProps>({
name: 'HeaderRow',
props: [
'cells',
'stickyOffsets',
'flattenColumns',
'rowComponent',
'cellComponent',
'index',
'customHeaderRow',
] as any,
setup(props: RowProps) {
const tableContext = useInjectTable();
return () => {
const { prefixCls, direction } = tableContext;
const {
cells,
stickyOffsets,
flattenColumns,
rowComponent: RowComponent,
cellComponent: CellComponent,
customHeaderRow,
index,
} = props;
let rowProps;
if (customHeaderRow) {
rowProps = customHeaderRow(
cells.map(cell => cell.column),
index,
);
}
const columnsKey = getColumnsKey(cells.map(cell => cell.column));
return (
<RowComponent {...rowProps}>
{cells.map((cell: CellType, cellIndex) => {
const { column } = cell;
const fixedInfo = getCellFixedInfo(
cell.colStart,
cell.colEnd,
flattenColumns,
stickyOffsets,
direction,
);
let additionalProps;
if (column && column.customHeaderCell) {
additionalProps = cell.column.customHeaderCell(column);
}
return (
<Cell
{...cell}
cellType="header"
ellipsis={column.ellipsis}
align={column.align}
component={CellComponent}
prefixCls={prefixCls}
key={columnsKey[cellIndex]}
{...fixedInfo}
additionalProps={additionalProps}
rowType="header"
column={column}
v-slots={{ default: () => column.title }}
/>
);
})}
</RowComponent>
);
};
},
});

View File

@ -0,0 +1,7 @@
function Panel(_, { slots }) {
return <div>{slots.default?.()}</div>;
}
Panel.displayName = 'Panel';
export default Panel;

View File

@ -0,0 +1,804 @@
import Header from './Header/Header';
import type {
GetRowKey,
ColumnsType,
TableComponents,
Key,
TriggerEventHandler,
GetComponentProps,
PanelRender,
TableLayout,
RowClassName,
CustomizeComponent,
ColumnType,
CustomizeScrollBody,
TableSticky,
ExpandedRowRender,
RenderExpandIcon,
TransformCellText,
} 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';
import { getDataAndAriaProps } from './utils/legacyUtil';
// 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> {
prefixCls?: string;
data?: RecordType[];
columns?: ColumnsType<RecordType>;
rowKey?: string | GetRowKey<RecordType>;
tableLayout?: TableLayout;
// Fixed Columns
scroll?: { x?: number | true | string; y?: number | string };
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';
// Expandable
expandFixed?: boolean;
expandColumnWidth?: number;
expandedRowKeys?: Key[];
defaultExpandedRowKeys?: Key[];
expandedRowRender?: ExpandedRowRender<RecordType>;
expandRowByClick?: boolean;
expandIcon?: RenderExpandIcon<RecordType>;
onExpand?: (expanded: boolean, record: RecordType) => void;
onExpandedRowsChange?: (expandedKeys: Key[]) => void;
defaultExpandAllRows?: boolean;
indentSize?: number;
expandIconColumnIndex?: number;
expandedRowClassName?: RowClassName<RecordType>;
childrenColumnName?: string;
rowExpandable?: (record: RecordType) => boolean;
// =================================== 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: HTMLDivElement;
};
sticky?: boolean | TableSticky;
canExpandable?: boolean;
onUpdateInternalRefs?: (refs: Record<string, any>) => void;
transformCellText?: TransformCellText<RecordType>;
}
export default defineComponent<TableProps>({
name: 'Table',
inheritAttrs: false,
props: [
'prefixCls',
'data',
'columns',
'rowKey',
'tableLayout',
'scroll',
'rowClassName',
'title',
'footer',
'id',
'showHeader',
'components',
'customRow',
'customHeaderRow',
'direction',
'expandFixed',
'expandColumnWidth',
'expandedRowKeys',
'defaultExpandedRowKeys',
'expandedRowRender',
'expandRowByClick',
'expandIcon',
'onExpand',
'onExpandedRowsChange',
'defaultExpandAllRows',
'indentSize',
'expandIconColumnIndex',
'expandedRowClassName',
'childrenColumnName',
'rowExpandable',
'sticky',
'transformColumns',
'internalHooks',
'internalRefs',
'canExpandable',
'onUpdateInternalRefs',
'transformCellText',
] as any,
slots: ['title', 'footer', 'summary', 'emptyText'],
emits: ['expand', 'expandedRowsChange', 'updateInternalRefs'],
setup(props, { attrs, slots, 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 (
props.canExpandable ||
mergedData.value.some(
record => record && typeof record === 'object' && record[mergedChildrenColumnName.value],
)
) {
return 'nest';
}
/* eslint-enable */
return false;
});
const innerExpandedKeys = ref([]);
const stop = watchEffect(() => {
if (props.defaultExpandedRowKeys) {
innerExpandedKeys.value = props.defaultExpandedRowKeys;
}
if (props.defaultExpandAllRows) {
innerExpandedKeys.value = findAllChildrenKeys(
mergedData.value,
getRowKey.value,
mergedChildrenColumnName.value,
);
}
});
// defalutXxxx
stop();
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,
},
computed(() => (props.internalHooks === INTERNAL_HOOKS ? props.transformColumns : null)),
);
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
const scrollXStyle = ref<CSSProperties>({});
const scrollYStyle = ref<CSSProperties>({});
const 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);
return;
}
const domTarget = (target as any).$el || target;
if (domTarget.scrollLeft !== scrollLeft) {
// eslint-disable-next-line no-param-reassign
domTarget.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);
});
});
watchEffect(
() => {
if (props.internalHooks === INTERNAL_HOOKS && props.internalRefs) {
props.onUpdateInternalRefs({
body: scrollBodyRef.value
? (scrollBodyRef.value as any).$el || scrollBodyRef.value
: null,
});
}
},
{ flush: 'post' },
);
// 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', 'transformCellText'),
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,
});
// Body
const bodyTable = () => (
<Body
data={mergedData.value}
measureColumnWidth={fixHeader.value || horizonScroll.value || stickyState.value.isSticky}
expandedKeys={mergedExpandedKeys.value}
rowExpandable={props.rowExpandable}
getRowKey={getRowKey.value}
customRow={props.customRow}
childrenColumnName={mergedChildrenColumnName.value}
v-slots={{ emptyNode }}
/>
);
const bodyColGroup = () => (
<ColGroup
colWidths={flattenColumns.value.map(({ width }) => width)}
columns={flattenColumns.value}
/>
);
return () => {
const {
prefixCls,
scroll,
tableLayout,
direction,
// Additional Part
title = slots.title,
footer = slots.footer,
// Customize
id,
showHeader,
customHeaderRow,
} = props;
const { isSticky, offsetHeader, offsetSummary, offsetScroll, stickyClassName, container } =
stickyState.value;
const TableComponent = getComponent(['table'], 'table');
const customizeScrollBody = getComponent(['body']) as unknown as CustomizeScrollBody<any>;
const summaryNode = slots.summary?.({ pageData: mergedData.value });
let groupTableNode = () => null;
// Header props
const headerProps = {
colWidths: colWidths.value,
columCount: flattenColumns.value.length,
stickyOffsets: stickyOffsets.value,
customHeaderRow,
fixHeader: fixHeader.value,
scroll,
};
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 = () => null;
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.value && 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 && 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>
);
}
const ariaProps = getDataAndAriaProps(attrs);
const fullTable = () => (
<div
{...ariaProps}
class={classNames(prefixCls, {
[`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-ping-left`]: pingedLeft.value,
[`${prefixCls}-ping-right`]: pingedRight.value,
[`${prefixCls}-layout-fixed`]: tableLayout === 'fixed',
[`${prefixCls}-fixed-header`]: fixHeader.value,
/** No used but for compatible */
[`${prefixCls}-fixed-column`]: fixColumn.value,
[`${prefixCls}-scroll-horizontal`]: horizonScroll.value,
[`${prefixCls}-has-fix-left`]: flattenColumns.value[0] && flattenColumns.value[0].fixed,
[`${prefixCls}-has-fix-right`]:
flattenColumns.value[columnCount.value - 1] &&
flattenColumns.value[columnCount.value - 1].fixed === 'right',
[attrs.class as string]: attrs.class,
})}
style={attrs.style}
id={id}
ref={fullTableRef}
>
{title && <Panel class={`${prefixCls}-title`}>{title(mergedData.value)}</Panel>}
<div class={`${prefixCls}-container`}>{groupTableNode()}</div>
{footer && <Panel class={`${prefixCls}-footer`}>{footer(mergedData.value)}</Panel>}
</div>
);
if (horizonScroll.value) {
return (
<VCResizeObserver
onResize={onFullTableResize}
v-slots={{ default: fullTable }}
></VCResizeObserver>
);
}
return fullTable();
};
},
});

View File

@ -1,60 +0,0 @@
.move-enter,
.move-appear {
opacity: 0;
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
animation-duration: 2.5s;
animation-fill-mode: both;
animation-play-state: paused;
}
.move-leave {
animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
animation-duration: 0.5s;
animation-fill-mode: both;
animation-play-state: paused;
}
.move-enter.move-enter-active,
.move-appear.move-enter-active {
animation-name: moveLeftIn;
animation-play-state: running;
}
.move-leave.move-leave-active {
animation-name: moveRightOut;
animation-play-state: running;
}
@keyframes moveLeftIn {
0% {
transform-origin: 0 0;
transform: translateX(30px);
opacity: 0;
background: #fff6de;
}
20% {
transform-origin: 0 0;
transform: translateX(0);
opacity: 1;
}
80% {
background: #fff6de;
}
100% {
background: transparent;
opacity: 1;
}
}
@keyframes moveRightOut {
0% {
transform-origin: 0 0;
transform: translateX(0);
opacity: 1;
}
100% {
transform-origin: 0 0;
transform: translateX(-30px);
opacity: 0;
}
}

View File

@ -1,12 +0,0 @@
@tablePrefixCls: rc-table;
@table-border-color: #e9e9e9;
.@{tablePrefixCls}.bordered {
table {
border-collapse: collapse;
}
th,
td {
border: 1px solid @table-border-color;
}
}

View File

@ -1,233 +0,0 @@
@tablePrefixCls: rc-table;
@text-color : #666;
@font-size-base : 12px;
@line-height: 1.5;
@table-border-color: #e9e9e9;
@table-head-background-color: #f7f7f7;
@vertical-padding: 16px;
@horizontal-padding: 8px;
.@{tablePrefixCls} {
font-size: @font-size-base;
color: @text-color;
transition: opacity 0.3s ease;
position: relative;
line-height: @line-height;
overflow: hidden;
.@{tablePrefixCls}-scroll {
overflow: auto;
table {
width: auto;
min-width: 100%;
}
}
.@{tablePrefixCls}-header {
overflow: hidden;
background: @table-head-background-color;
}
&-fixed-header &-body {
background: #fff;
position: relative;
}
&-fixed-header &-body-inner {
height: 100%;
overflow: scroll;
}
&-fixed-header &-scroll &-header {
overflow-x: scroll;
padding-bottom: 20px;
margin-bottom: -20px;
overflow-y: scroll;
box-sizing: border-box;
}
// https://github.com/ant-design/ant-design/issues/10828
&-fixed-columns-in-body {
visibility: hidden;
pointer-events: none;
}
.@{tablePrefixCls}-title {
padding: @vertical-padding @horizontal-padding;
border-top: 1px solid @table-border-color;
}
.@{tablePrefixCls}-content {
position: relative;
}
.@{tablePrefixCls}-footer {
padding: @vertical-padding @horizontal-padding;
border-bottom: 1px solid @table-border-color;
}
.@{tablePrefixCls}-placeholder {
padding: 16px 8px;
background: #fff;
border-bottom: 1px solid @table-border-color;
text-align: center;
position: relative;
&-fixed-columns {
position: absolute;
bottom: 0;
width: 100%;
background: transparent;
pointer-events: none;
}
}
table {
width: 100%;
border-collapse: separate;
text-align: left;
}
th {
background: @table-head-background-color;
font-weight: bold;
transition: background 0.3s ease;
}
td {
border-bottom: 1px solid @table-border-color;
&:empty:after {
content: '.'; // empty cell placeholder
visibility: hidden;
}
}
tr {
transition: all 0.3s ease;
&:hover {
background: #eaf8fe;
}
&.@{tablePrefixCls}-row-hover {
background: #eaf8fe;
}
}
th,
td {
padding: @vertical-padding @horizontal-padding;
white-space: nowrap;
}
}
.@{tablePrefixCls} {
&-expand-icon-col {
width: 34px;
}
&-row,
&-expanded-row {
&-expand-icon {
cursor: pointer;
display: inline-block;
width: 16px;
height: 16px;
text-align: center;
line-height: 16px;
border: 1px solid @table-border-color;
user-select: none;
background: #fff;
}
&-spaced {
visibility: hidden;
}
&-spaced:after {
content: '.';
}
&-expanded:after {
content: '-';
}
&-collapsed:after {
content: '+';
}
}
tr&-expanded-row {
background: #f7f7f7;
&:hover {
background: #f7f7f7;
}
}
&-column-hidden {
display: none;
}
&-prev-columns-page,
&-next-columns-page {
cursor: pointer;
color: #666;
z-index: 1;
&:hover {
color: #2db7f5;
}
&-disabled {
cursor: not-allowed;
color: #999;
&:hover {
color: #999;
}
}
}
&-prev-columns-page {
margin-right: 8px;
&:before {
content: '<';
}
}
&-next-columns-page {
float: right;
&:before {
content: '>';
}
}
&-fixed-left,
&-fixed-right {
position: absolute;
top: 0;
overflow: hidden;
table {
width: auto;
background: #fff;
}
}
&-fixed-left {
left: 0;
box-shadow: 4px 0 4px rgba(100, 100, 100, 0.1);
& .@{tablePrefixCls}-body-inner {
margin-right: -20px;
padding-right: 20px;
}
.@{tablePrefixCls}-fixed-header & .@{tablePrefixCls}-body-inner {
padding-right: 0;
}
}
&-fixed-right {
right: 0;
box-shadow: -4px 0 4px rgba(100, 100, 100, 0.1);
// hide expand row content in right fixed Table
// https://github.com/ant-design/ant-design/issues/1898
.@{tablePrefixCls}-expanded-row {
color: transparent;
pointer-events: none;
}
}
&&-scroll-position-left &-fixed-left {
box-shadow: none;
}
&&-scroll-position-right &-fixed-right {
box-shadow: none;
}
}

View File

@ -1,220 +0,0 @@
/*! normalize.css v3.0.1 | MIT License | git.io/normalize */
html {
font-family: sans-serif;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%
}
body {
margin: 0
}
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
nav,
section,
summary {
display: block
}
audio,
canvas,
progress,
video {
display: inline-block;
vertical-align: baseline
}
audio:not([controls]) {
display: none;
height: 0
}
[hidden],
template {
display: none
}
a {
background: 0 0
}
a:active,
a:hover {
outline: 0
}
abbr[title] {
border-bottom: 1px dotted
}
b,
strong {
font-weight: 700
}
dfn {
font-style: italic
}
h1 {
font-size: 2em;
margin: .67em 0
}
mark {
background: #ff0;
color: #000
}
small {
font-size: 80%
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline
}
sup {
top: -.5em
}
sub {
bottom: -.25em
}
img {
border: 0
}
svg:not(:root) {
overflow: hidden
}
figure {
margin: 1em 40px
}
hr {
-moz-box-sizing: content-box;
box-sizing: content-box;
height: 0
}
pre {
overflow: auto
}
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em
}
button,
input,
optgroup,
select,
textarea {
color: inherit;
font: inherit;
margin: 0
}
button {
overflow: visible
}
button,
select {
text-transform: none
}
button,
html input[type=button],
input[type=reset],
input[type=submit] {
-webkit-appearance: button;
cursor: pointer
}
button[disabled],
html input[disabled] {
cursor: default
}
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0
}
input {
line-height: normal
}
input[type=checkbox],
input[type=radio] {
box-sizing: border-box;
padding: 0
}
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
height: auto
}
input[type=search] {
-webkit-appearance: textfield;
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box;
box-sizing: content-box
}
input[type=search]::-webkit-search-cancel-button,
input[type=search]::-webkit-search-decoration {
-webkit-appearance: none
}
fieldset {
border: 1px solid silver;
margin: 0 2px;
padding: .35em .625em .75em
}
legend {
border: 0;
padding: 0
}
textarea {
overflow: auto
}
optgroup {
font-weight: 700
}
table {
border-collapse: collapse;
border-spacing: 0
}
td,
th {
padding: 0
}

View File

@ -0,0 +1,44 @@
import type {
ColumnType,
DefaultRecordType,
ColumnsType,
TableLayout,
RenderExpandIcon,
ExpandableType,
RowClassName,
TriggerEventHandler,
ExpandedRowRender,
} from '../interface';
import type { InjectionKey } from 'vue';
import { inject, provide } from 'vue';
export interface BodyContextProps<RecordType = DefaultRecordType> {
rowClassName: string | RowClassName<RecordType>;
expandedRowClassName: RowClassName<RecordType>;
columns: ColumnsType<RecordType>;
flattenColumns: readonly ColumnType<RecordType>[];
componentWidth: number;
tableLayout: TableLayout;
fixHeader: boolean;
fixColumn: boolean;
horizonScroll: boolean;
indentSize: number;
expandableType: ExpandableType;
expandRowByClick: boolean;
expandedRowRender: ExpandedRowRender<RecordType>;
expandIcon: RenderExpandIcon<RecordType>;
onTriggerExpand: TriggerEventHandler<RecordType>;
expandIconColumnIndex: number;
}
export const BodyContextKey: InjectionKey<BodyContextProps> = Symbol('BodyContextProps');
export const useProvideBody = (props: BodyContextProps) => {
provide(BodyContextKey, props);
};
export const useInjectBody = () => {
return inject(BodyContextKey, {} as BodyContextProps);
};

Some files were not shown because too many files have changed in this diff Show More