From a01905cc6eec54007fdb193b1ed6add1f6d2aa1b Mon Sep 17 00:00:00 2001
From: tjz <415800467@qq.com>
Date: Thu, 29 Mar 2018 22:08:04 +0800
Subject: [PATCH] add table
---
components/pagination/Pagination.jsx | 6 +-
components/spin/Spin.jsx | 25 +-
components/spin/index.js | 1 +
components/table/Column.jsx | 6 +
components/table/ColumnGroup.jsx | 10 +
.../table/FilterDropdownMenuWrapper.jsx | 16 +
components/table/SelectionBox.tsx | 68 ++
components/table/SelectionCheckboxAll.tsx | 183 +++
components/table/Table.tsx | 1002 +++++++++++++++++
components/table/createBodyRow.jsx | 59 +
components/table/createStore.jsx | 11 +
components/table/filterDropdown.tsx | 228 ++++
components/table/index.en-US.md | 210 ++++
components/table/index.jsx | 5 +
components/table/index.zh-CN.md | 210 ++++
components/table/interface.js | 186 +++
components/table/style/index.js | 9 +
components/table/style/index.less | 568 ++++++++++
components/table/style/size.less | 109 ++
components/table/util.js | 66 ++
20 files changed, 2966 insertions(+), 12 deletions(-)
create mode 100644 components/table/Column.jsx
create mode 100644 components/table/ColumnGroup.jsx
create mode 100644 components/table/FilterDropdownMenuWrapper.jsx
create mode 100644 components/table/SelectionBox.tsx
create mode 100644 components/table/SelectionCheckboxAll.tsx
create mode 100755 components/table/Table.tsx
create mode 100644 components/table/createBodyRow.jsx
create mode 100644 components/table/createStore.jsx
create mode 100755 components/table/filterDropdown.tsx
create mode 100644 components/table/index.en-US.md
create mode 100644 components/table/index.jsx
create mode 100644 components/table/index.zh-CN.md
create mode 100644 components/table/interface.js
create mode 100644 components/table/style/index.js
create mode 100644 components/table/style/index.less
create mode 100644 components/table/style/size.less
create mode 100644 components/table/util.js
diff --git a/components/pagination/Pagination.jsx b/components/pagination/Pagination.jsx
index e5be8723c..3b7a5a3b2 100644
--- a/components/pagination/Pagination.jsx
+++ b/components/pagination/Pagination.jsx
@@ -7,7 +7,7 @@ import LocaleReceiver from '../locale-provider/LocaleReceiver'
import { getOptionProps } from '../_util/props-util'
import VcPagination from '../vc-pagination'
-export const PaginationProps = {
+export const PaginationProps = () => ({
total: PropTypes.number,
defaultCurrent: PropTypes.number,
current: PropTypes.number,
@@ -29,11 +29,11 @@ export const PaginationProps = {
prefixCls: PropTypes.string,
selectPrefixCls: PropTypes.string,
itemRender: PropTypes.any,
-}
+})
export default {
props: {
- ...PaginationProps,
+ ...PaginationProps(),
prefixCls: PropTypes.string.def('ant-pagination'),
selectPrefixCls: PropTypes.string.def('ant-select'),
},
diff --git a/components/spin/Spin.jsx b/components/spin/Spin.jsx
index 95b87aafd..74f1ba748 100644
--- a/components/spin/Spin.jsx
+++ b/components/spin/Spin.jsx
@@ -2,20 +2,27 @@
import PropTypes from '../_util/vue-types'
import BaseMixin from '../_util/BaseMixin'
import isCssAnimationSupported from '../_util/isCssAnimationSupported'
-import { filterEmpty } from '../_util/props-util'
+import { filterEmpty, initDefaultProps } from '../_util/props-util'
import getTransitionProps from '../_util/getTransitionProps'
+export const SpinProps = () => ({
+ prefixCls: PropTypes.string,
+ spinning: PropTypes.bool,
+ size: PropTypes.oneOf(['small', 'default', 'large']),
+ wrapperClassName: PropTypes.string,
+ tip: PropTypes.string,
+ delay: PropTypes.number,
+})
+
export default {
name: 'Spin',
mixins: [BaseMixin],
- props: {
- prefixCls: PropTypes.string.def('ant-spin'),
- spinning: PropTypes.bool.def(true),
- size: PropTypes.oneOf(['small', 'default', 'large']).def('default'),
- wrapperClassName: PropTypes.string.def(''),
- tip: PropTypes.string,
- delay: PropTypes.number,
- },
+ props: initDefaultProps(SpinProps(), {
+ prefixCls: 'ant-spin',
+ size: 'default',
+ spinning: true,
+ wrapperClassName: '',
+ }),
data () {
const { spinning } = this
return {
diff --git a/components/spin/index.js b/components/spin/index.js
index fd6f323e0..1ccafd62f 100644
--- a/components/spin/index.js
+++ b/components/spin/index.js
@@ -1,3 +1,4 @@
import Spin from './Spin'
+export { SpinProps } from './Spin'
export default Spin
diff --git a/components/table/Column.jsx b/components/table/Column.jsx
new file mode 100644
index 000000000..34e98b0e1
--- /dev/null
+++ b/components/table/Column.jsx
@@ -0,0 +1,6 @@
+import { ColumnProps } from './interface'
+
+export default {
+ name: 'Column',
+ props: ColumnProps,
+}
diff --git a/components/table/ColumnGroup.jsx b/components/table/ColumnGroup.jsx
new file mode 100644
index 000000000..517c7a2f1
--- /dev/null
+++ b/components/table/ColumnGroup.jsx
@@ -0,0 +1,10 @@
+
+import PropTypes from '../_util/vue-types'
+
+export default {
+ name: 'ColumnGroup',
+ props: {
+ title: PropTypes.any,
+ },
+ __ANT_TABLE_COLUMN_GROUP: true,
+}
diff --git a/components/table/FilterDropdownMenuWrapper.jsx b/components/table/FilterDropdownMenuWrapper.jsx
new file mode 100644
index 000000000..54d9e1f32
--- /dev/null
+++ b/components/table/FilterDropdownMenuWrapper.jsx
@@ -0,0 +1,16 @@
+
+export default {
+ methods: {
+ handelClick (e) {
+ this.$emit('click', e)
+ },
+ },
+ render () {
+ const { $slots, handelClick } = this
+ return (
+
+ {$slots.default}
+
+ )
+ },
+}
diff --git a/components/table/SelectionBox.tsx b/components/table/SelectionBox.tsx
new file mode 100644
index 000000000..17a6f0b60
--- /dev/null
+++ b/components/table/SelectionBox.tsx
@@ -0,0 +1,68 @@
+import * as React from 'react';
+import Checkbox from '../checkbox';
+import Radio from '../radio';
+import { SelectionBoxProps, SelectionBoxState } from './interface';
+
+export default class SelectionBox extends React.Component {
+ unsubscribe: () => void;
+
+ constructor(props: SelectionBoxProps) {
+ super(props);
+
+ this.state = {
+ checked: this.getCheckState(props),
+ };
+ }
+
+ componentDidMount() {
+ this.subscribe();
+ }
+
+ componentWillUnmount() {
+ if (this.unsubscribe) {
+ this.unsubscribe();
+ }
+ }
+
+ subscribe() {
+ const { store } = this.props;
+ this.unsubscribe = store.subscribe(() => {
+ const checked = this.getCheckState(this.props);
+ this.setState({ checked });
+ });
+ }
+
+ getCheckState(props: SelectionBoxProps) {
+ const { store, defaultSelection, rowIndex } = props;
+ let checked = false;
+ if (store.getState().selectionDirty) {
+ checked = store.getState().selectedRowKeys.indexOf(rowIndex) >= 0;
+ } else {
+ checked = (store.getState().selectedRowKeys.indexOf(rowIndex) >= 0 ||
+ defaultSelection.indexOf(rowIndex) >= 0);
+ }
+ return checked;
+ }
+
+ render() {
+ const { type, rowIndex, ...rest } = this.props;
+ const { checked } = this.state;
+
+ if (type === 'radio') {
+ return (
+
+ );
+ } else {
+ return (
+
+ );
+ }
+ }
+}
diff --git a/components/table/SelectionCheckboxAll.tsx b/components/table/SelectionCheckboxAll.tsx
new file mode 100644
index 000000000..8386d1af7
--- /dev/null
+++ b/components/table/SelectionCheckboxAll.tsx
@@ -0,0 +1,183 @@
+import * as React from 'react';
+import Checkbox from '../checkbox';
+import Dropdown from '../dropdown';
+import Menu from '../menu';
+import Icon from '../icon';
+import classNames from 'classnames';
+import { SelectionCheckboxAllProps, SelectionCheckboxAllState, SelectionItem } from './interface';
+
+export default class SelectionCheckboxAll extends
+ React.Component, SelectionCheckboxAllState> {
+ unsubscribe: () => void;
+ defaultSelections: SelectionItem[];
+
+ constructor(props: SelectionCheckboxAllProps) {
+ super(props);
+
+ this.defaultSelections = props.hideDefaultSelections ? [] : [{
+ key: 'all',
+ text: props.locale.selectAll,
+ onSelect: () => {},
+ }, {
+ key: 'invert',
+ text: props.locale.selectInvert,
+ onSelect: () => {},
+ }];
+
+ this.state = {
+ checked: this.getCheckState(props),
+ indeterminate: this.getIndeterminateState(props),
+ };
+ }
+
+ componentDidMount() {
+ this.subscribe();
+ }
+
+ componentWillReceiveProps(nextProps: SelectionCheckboxAllProps) {
+ this.setCheckState(nextProps);
+ }
+
+ componentWillUnmount() {
+ if (this.unsubscribe) {
+ this.unsubscribe();
+ }
+ }
+
+ subscribe() {
+ const { store } = this.props;
+ this.unsubscribe = store.subscribe(() => {
+ this.setCheckState(this.props);
+ });
+ }
+
+ checkSelection(data: T[], type: string, byDefaultChecked: boolean) {
+ const { store, getCheckboxPropsByItem, getRecordKey } = this.props;
+ // type should be 'every' | 'some'
+ if (type === 'every' || type === 'some') {
+ return (
+ byDefaultChecked
+ ? data[type]((item, i) => getCheckboxPropsByItem(item, i).defaultChecked)
+ : data[type]((item, i) =>
+ store.getState().selectedRowKeys.indexOf(getRecordKey(item, i)) >= 0)
+ );
+ }
+ return false;
+ }
+
+ setCheckState(props: SelectionCheckboxAllProps) {
+ const checked = this.getCheckState(props);
+ const indeterminate = this.getIndeterminateState(props);
+ if (checked !== this.state.checked) {
+ this.setState({ checked });
+ }
+ if (indeterminate !== this.state.indeterminate) {
+ this.setState({ indeterminate });
+ }
+ }
+
+ getCheckState(props: SelectionCheckboxAllProps) {
+ const { store, data } = props;
+ let checked;
+ if (!data.length) {
+ checked = false;
+ } else {
+ checked = store.getState().selectionDirty
+ ? this.checkSelection(data, 'every', false)
+ : (
+ this.checkSelection(data, 'every', false) ||
+ this.checkSelection(data, 'every', true)
+ );
+
+ }
+ return checked;
+ }
+
+ getIndeterminateState(props: SelectionCheckboxAllProps) {
+ const { store, data } = props;
+ let indeterminate;
+ if (!data.length) {
+ indeterminate = false;
+ } else {
+ indeterminate = store.getState().selectionDirty
+ ? (
+ this.checkSelection(data, 'some', false) &&
+ !this.checkSelection(data, 'every', false)
+ )
+ : ((this.checkSelection(data, 'some', false) &&
+ !this.checkSelection(data, 'every', false)) ||
+ (this.checkSelection(data, 'some', true) &&
+ !this.checkSelection(data, 'every', true))
+ );
+ }
+ return indeterminate;
+ }
+
+ handleSelectAllChagne = (e: React.ChangeEvent) => {
+ let checked = e.target.checked;
+ this.props.onSelect(checked ? 'all' : 'removeAll', 0, null);
+ }
+
+ renderMenus(selections: SelectionItem[]) {
+ return selections.map((selection, index) => {
+ return (
+
+ {this.props.onSelect(selection.key, index, selection.onSelect); }}
+ >
+ {selection.text}
+
+
+ );
+ });
+ }
+
+ render() {
+ const { disabled, prefixCls, selections, getPopupContainer } = this.props;
+ const { checked, indeterminate } = this.state;
+
+ let selectionPrefixCls = `${prefixCls}-selection`;
+
+ let customSelections: React.ReactNode = null;
+
+ if (selections) {
+ let newSelections = Array.isArray(selections) ? this.defaultSelections.concat(selections)
+ : this.defaultSelections;
+
+ const menu = (
+
+ );
+
+ customSelections = newSelections.length > 0 ? (
+
+
+
+
+
+ ) : null;
+ }
+
+ return (
+
+
+ {customSelections}
+
+ );
+ }
+}
diff --git a/components/table/Table.tsx b/components/table/Table.tsx
new file mode 100755
index 000000000..c01b116e3
--- /dev/null
+++ b/components/table/Table.tsx
@@ -0,0 +1,1002 @@
+import * as React from 'react';
+import * as ReactDOM from 'react-dom';
+import RcTable from 'rc-table';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import Pagination, { PaginationProps } from '../pagination';
+import Icon from '../icon';
+import Spin from '../spin';
+import LocaleReceiver from '../locale-provider/LocaleReceiver';
+import defaultLocale from '../locale-provider/default';
+import warning from '../_util/warning';
+import FilterDropdown from './filterDropdown';
+import createStore, { Store } from './createStore';
+import SelectionBox from './SelectionBox';
+import SelectionCheckboxAll from './SelectionCheckboxAll';
+import Column from './Column';
+import ColumnGroup from './ColumnGroup';
+import createBodyRow from './createBodyRow';
+import { flatArray, treeMap, flatFilter, normalizeColumns } from './util';
+import { SpinProps } from '../spin';
+import {
+ TableProps,
+ TableState,
+ TableComponents,
+ RowSelectionType,
+ TableLocale,
+ ColumnProps,
+ CompareFn,
+ TableStateFilters,
+ SelectionItemSelectFn,
+} from './interface';
+
+function noop() {
+}
+
+function stopPropagation(e: React.SyntheticEvent) {
+ e.stopPropagation();
+ if (e.nativeEvent.stopImmediatePropagation) {
+ e.nativeEvent.stopImmediatePropagation();
+ }
+}
+
+const defaultPagination = {
+ onChange: noop,
+ onShowSizeChange: noop,
+};
+
+/**
+ * Avoid creating new object, so that parent component's shouldComponentUpdate
+ * can works appropriately。
+ */
+const emptyObject = {};
+
+export default class Table extends React.Component, TableState> {
+ static Column = Column;
+ static ColumnGroup = ColumnGroup;
+
+ static propTypes = {
+ dataSource: PropTypes.array,
+ columns: PropTypes.array,
+ prefixCls: PropTypes.string,
+ useFixedHeader: PropTypes.bool,
+ rowSelection: PropTypes.object,
+ className: PropTypes.string,
+ size: PropTypes.string,
+ loading: PropTypes.oneOfType([
+ PropTypes.bool,
+ PropTypes.object,
+ ]),
+ bordered: PropTypes.bool,
+ onChange: PropTypes.func,
+ locale: PropTypes.object,
+ dropdownPrefixCls: PropTypes.string,
+ };
+
+ static defaultProps = {
+ dataSource: [],
+ prefixCls: 'ant-table',
+ useFixedHeader: false,
+ rowSelection: null,
+ className: '',
+ size: 'large',
+ loading: false,
+ bordered: false,
+ indentSize: 20,
+ locale: {},
+ rowKey: 'key',
+ showHeader: true,
+ };
+
+ CheckboxPropsCache: {
+ [key: string]: any;
+ };
+ store: Store;
+ columns: ColumnProps[];
+ components: TableComponents;
+
+ constructor(props: TableProps) {
+ super(props);
+
+ warning(
+ !('columnsPageRange' in props || 'columnsPageSize' in props),
+ '`columnsPageRange` and `columnsPageSize` are removed, please use ' +
+ 'fixed columns instead, see: https://u.ant.design/fixed-columns.',
+ );
+
+ this.columns = props.columns || normalizeColumns(props.children as React.ReactChildren);
+
+ this.createComponents(props.components);
+
+ this.state = {
+ ...this.getDefaultSortOrder(this.columns),
+ // 减少状态
+ filters: this.getFiltersFromColumns(),
+ pagination: this.getDefaultPagination(props),
+ };
+
+ this.CheckboxPropsCache = {};
+
+ this.store = createStore({
+ selectedRowKeys: (props.rowSelection || {}).selectedRowKeys || [],
+ selectionDirty: false,
+ });
+ }
+
+ getCheckboxPropsByItem = (item: T, index: number) => {
+ const { rowSelection = {} } = this.props;
+ if (!rowSelection.getCheckboxProps) {
+ return {};
+ }
+ const key = this.getRecordKey(item, index);
+ // Cache checkboxProps
+ if (!this.CheckboxPropsCache[key]) {
+ this.CheckboxPropsCache[key] = rowSelection.getCheckboxProps(item);
+ }
+ return this.CheckboxPropsCache[key];
+ }
+
+ getDefaultSelection() {
+ const { rowSelection = {} } = this.props;
+ if (!rowSelection.getCheckboxProps) {
+ return [];
+ }
+ return this.getFlatData()
+ .filter((item: T, rowIndex) => this.getCheckboxPropsByItem(item, rowIndex).defaultChecked)
+ .map((record, rowIndex) => this.getRecordKey(record, rowIndex));
+ }
+
+ getDefaultPagination(props: TableProps) {
+ const pagination: PaginationProps = props.pagination || {};
+ return this.hasPagination(props) ?
+ {
+ ...defaultPagination,
+ ...pagination,
+ current: pagination.defaultCurrent || pagination.current || 1,
+ pageSize: pagination.defaultPageSize || pagination.pageSize || 10,
+ } : {};
+ }
+
+ componentWillReceiveProps(nextProps: TableProps) {
+ this.columns = nextProps.columns || normalizeColumns(nextProps.children as React.ReactChildren);
+ if ('pagination' in nextProps || 'pagination' in this.props) {
+ this.setState(previousState => {
+ const newPagination = {
+ ...defaultPagination,
+ ...previousState.pagination,
+ ...nextProps.pagination,
+ };
+ newPagination.current = newPagination.current || 1;
+ newPagination.pageSize = newPagination.pageSize || 10;
+ return { pagination: nextProps.pagination !== false ? newPagination : emptyObject };
+ });
+ }
+ if (nextProps.rowSelection &&
+ 'selectedRowKeys' in nextProps.rowSelection) {
+ this.store.setState({
+ selectedRowKeys: nextProps.rowSelection.selectedRowKeys || [],
+ });
+ const { rowSelection } = this.props;
+ if (rowSelection && (
+ nextProps.rowSelection.getCheckboxProps !== rowSelection.getCheckboxProps
+ )) {
+ this.CheckboxPropsCache = {};
+ }
+ }
+ if ('dataSource' in nextProps &&
+ nextProps.dataSource !== this.props.dataSource) {
+ this.store.setState({
+ selectionDirty: false,
+ });
+ this.CheckboxPropsCache = {};
+ }
+
+ if (this.getSortOrderColumns(this.columns).length > 0) {
+ const sortState = this.getSortStateFromColumns(this.columns);
+ if (sortState.sortColumn !== this.state.sortColumn ||
+ sortState.sortOrder !== this.state.sortOrder) {
+ this.setState(sortState);
+ }
+ }
+
+ const filteredValueColumns = this.getFilteredValueColumns(this.columns);
+ if (filteredValueColumns.length > 0) {
+ const filtersFromColumns = this.getFiltersFromColumns(this.columns);
+ const newFilters = { ...this.state.filters };
+ Object.keys(filtersFromColumns).forEach(key => {
+ newFilters[key] = filtersFromColumns[key];
+ });
+ if (this.isFiltersChanged(newFilters)) {
+ this.setState({ filters: newFilters });
+ }
+ }
+
+ this.createComponents(nextProps.components, this.props.components);
+ }
+
+ onRow = (record: T, index: number) => {
+ const { onRow, prefixCls } = this.props;
+ const custom = onRow ? onRow(record, index) : {};
+ return {
+ ...custom,
+ prefixCls,
+ store: this.store,
+ rowKey: this.getRecordKey(record, index),
+ };
+ }
+
+ setSelectedRowKeys(selectedRowKeys: string[], { selectWay, record, checked, changeRowKeys }: any) {
+ const { rowSelection = {} as any } = this.props;
+ if (rowSelection && !('selectedRowKeys' in rowSelection)) {
+ this.store.setState({ selectedRowKeys });
+ }
+ const data = this.getFlatData();
+ if (!rowSelection.onChange && !rowSelection[selectWay]) {
+ return;
+ }
+ const selectedRows = data.filter(
+ (row, i) => selectedRowKeys.indexOf(this.getRecordKey(row, i)) >= 0,
+ );
+ if (rowSelection.onChange) {
+ rowSelection.onChange(selectedRowKeys, selectedRows);
+ }
+ if (selectWay === 'onSelect' && rowSelection.onSelect) {
+ rowSelection.onSelect(record, checked, selectedRows);
+ } else if (selectWay === 'onSelectAll' && rowSelection.onSelectAll) {
+ const changeRows = data.filter(
+ (row, i) => changeRowKeys.indexOf(this.getRecordKey(row, i)) >= 0,
+ );
+ rowSelection.onSelectAll(checked, selectedRows, changeRows);
+ } else if (selectWay === 'onSelectInvert' && rowSelection.onSelectInvert) {
+ rowSelection.onSelectInvert(selectedRowKeys);
+ }
+ }
+
+ hasPagination(props?: any) {
+ return (props || this.props).pagination !== false;
+ }
+
+ isFiltersChanged(filters: TableStateFilters) {
+ let filtersChanged = false;
+ if (Object.keys(filters).length !== Object.keys(this.state.filters).length) {
+ filtersChanged = true;
+ } else {
+ Object.keys(filters).forEach(columnKey => {
+ if (filters[columnKey] !== this.state.filters[columnKey]) {
+ filtersChanged = true;
+ }
+ });
+ }
+ return filtersChanged;
+ }
+
+ getSortOrderColumns(columns?: ColumnProps[]) {
+ return flatFilter(
+ columns || this.columns || [],
+ (column: ColumnProps) => 'sortOrder' in column,
+ );
+ }
+
+ getFilteredValueColumns(columns?: ColumnProps[]) {
+ return flatFilter(
+ columns || this.columns || [],
+ (column: ColumnProps) => typeof column.filteredValue !== 'undefined',
+ );
+ }
+
+ getFiltersFromColumns(columns?: ColumnProps[]) {
+ let filters: any = {};
+ this.getFilteredValueColumns(columns).forEach((col: ColumnProps) => {
+ const colKey = this.getColumnKey(col) as string;
+ filters[colKey] = col.filteredValue;
+ });
+ return filters;
+ }
+
+ getDefaultSortOrder(columns?: ColumnProps[]) {
+ const definedSortState = this.getSortStateFromColumns(columns);
+
+ let defaultSortedColumn = flatFilter(columns || [], (column: ColumnProps) => column.defaultSortOrder != null)[0];
+
+ if (defaultSortedColumn && !definedSortState.sortColumn) {
+ return {
+ sortColumn: defaultSortedColumn,
+ sortOrder: defaultSortedColumn.defaultSortOrder,
+ };
+ }
+
+ return definedSortState;
+ }
+
+ getSortStateFromColumns(columns?: ColumnProps[]) {
+ // return first column which sortOrder is not falsy
+ const sortedColumn =
+ this.getSortOrderColumns(columns).filter((col: ColumnProps) => col.sortOrder)[0];
+
+ if (sortedColumn) {
+ return {
+ sortColumn: sortedColumn,
+ sortOrder: sortedColumn.sortOrder,
+ };
+ }
+
+ return {
+ sortColumn: null,
+ sortOrder: null,
+ };
+ }
+
+ getSorterFn() {
+ const { sortOrder, sortColumn } = this.state;
+ if (!sortOrder || !sortColumn ||
+ typeof sortColumn.sorter !== 'function') {
+ return;
+ }
+
+ return (a: T, b: T) => {
+ const result = (sortColumn!.sorter as CompareFn)(a, b);
+ if (result !== 0) {
+ return (sortOrder === 'descend') ? -result : result;
+ }
+ return 0;
+ };
+ }
+
+ toggleSortOrder(order: string, column: ColumnProps) {
+ let { sortColumn, sortOrder } = this.state;
+ // 只同时允许一列进行排序,否则会导致排序顺序的逻辑问题
+ let isSortColumn = this.isSortColumn(column);
+ if (!isSortColumn) { // 当前列未排序
+ sortOrder = order;
+ sortColumn = column;
+ } else { // 当前列已排序
+ if (sortOrder === order) { // 切换为未排序状态
+ sortOrder = '';
+ sortColumn = null;
+ } else { // 切换为排序状态
+ sortOrder = order;
+ }
+ }
+ const newState = {
+ sortOrder,
+ sortColumn,
+ };
+
+ // Controlled
+ if (this.getSortOrderColumns().length === 0) {
+ this.setState(newState);
+ }
+
+ const onChange = this.props.onChange;
+ if (onChange) {
+ onChange.apply(null, this.prepareParamsArguments({
+ ...this.state,
+ ...newState,
+ }));
+ }
+ }
+
+ handleFilter = (column: ColumnProps, nextFilters: string[]) => {
+ const props = this.props;
+ let pagination = { ...this.state.pagination };
+ const filters = {
+ ...this.state.filters,
+ [this.getColumnKey(column) as string]: nextFilters,
+ };
+ // Remove filters not in current columns
+ const currentColumnKeys: string[] = [];
+ treeMap(this.columns, c => {
+ if (!c.children) {
+ currentColumnKeys.push(this.getColumnKey(c) as string);
+ }
+ });
+ Object.keys(filters).forEach((columnKey) => {
+ if (currentColumnKeys.indexOf(columnKey) < 0) {
+ delete filters[columnKey];
+ }
+ });
+
+ if (props.pagination) {
+ // Reset current prop
+ pagination.current = 1;
+ pagination.onChange!(pagination.current);
+ }
+
+ const newState = {
+ pagination,
+ filters: {},
+ };
+ const filtersToSetState = { ...filters };
+ // Remove filters which is controlled
+ this.getFilteredValueColumns().forEach((col: ColumnProps) => {
+ const columnKey = this.getColumnKey(col);
+ if (columnKey) {
+ delete filtersToSetState[columnKey];
+ }
+ });
+ if (Object.keys(filtersToSetState).length > 0) {
+ newState.filters = filtersToSetState;
+ }
+
+ // Controlled current prop will not respond user interaction
+ if (typeof props.pagination === 'object' && 'current' in (props.pagination as Object)) {
+ newState.pagination = {
+ ...pagination,
+ current: this.state.pagination.current,
+ };
+ }
+
+ this.setState(newState, () => {
+ this.store.setState({
+ selectionDirty: false,
+ });
+ const onChange = this.props.onChange;
+ if (onChange) {
+ onChange.apply(null, this.prepareParamsArguments({
+ ...this.state,
+ selectionDirty: false,
+ filters,
+ pagination,
+ }));
+ }
+ });
+ }
+
+ handleSelect = (record: T, rowIndex: number, e: React.ChangeEvent) => {
+ const checked = e.target.checked;
+ const defaultSelection = this.store.getState().selectionDirty ? [] : this.getDefaultSelection();
+ let selectedRowKeys = this.store.getState().selectedRowKeys.concat(defaultSelection);
+ let key = this.getRecordKey(record, rowIndex);
+ if (checked) {
+ selectedRowKeys.push(this.getRecordKey(record, rowIndex));
+ } else {
+ selectedRowKeys = selectedRowKeys.filter((i: string) => key !== i);
+ }
+ this.store.setState({
+ selectionDirty: true,
+ });
+ this.setSelectedRowKeys(selectedRowKeys, {
+ selectWay: 'onSelect',
+ record,
+ checked,
+ });
+ }
+
+ handleRadioSelect = (record: T, rowIndex: number, e: React.ChangeEvent) => {
+ const checked = e.target.checked;
+ const defaultSelection = this.store.getState().selectionDirty ? [] : this.getDefaultSelection();
+ let selectedRowKeys = this.store.getState().selectedRowKeys.concat(defaultSelection);
+ let key = this.getRecordKey(record, rowIndex);
+ selectedRowKeys = [key];
+ this.store.setState({
+ selectionDirty: true,
+ });
+ this.setSelectedRowKeys(selectedRowKeys, {
+ selectWay: 'onSelect',
+ record,
+ checked,
+ });
+ }
+
+ handleSelectRow = (selectionKey: string, index: number, onSelectFunc: SelectionItemSelectFn) => {
+ const data = this.getFlatCurrentPageData();
+ const defaultSelection = this.store.getState().selectionDirty ? [] : this.getDefaultSelection();
+ const selectedRowKeys = this.store.getState().selectedRowKeys.concat(defaultSelection);
+ const changeableRowKeys = data
+ .filter((item, i) => !this.getCheckboxPropsByItem(item, i).disabled)
+ .map((item, i) => this.getRecordKey(item, i));
+
+ let changeRowKeys: string[] = [];
+ let selectWay = '';
+ let checked;
+ // handle default selection
+ switch (selectionKey) {
+ case 'all':
+ changeableRowKeys.forEach(key => {
+ if (selectedRowKeys.indexOf(key) < 0) {
+ selectedRowKeys.push(key);
+ changeRowKeys.push(key);
+ }
+ });
+ selectWay = 'onSelectAll';
+ checked = true;
+ break;
+ case 'removeAll':
+ changeableRowKeys.forEach(key => {
+ if (selectedRowKeys.indexOf(key) >= 0) {
+ selectedRowKeys.splice(selectedRowKeys.indexOf(key), 1);
+ changeRowKeys.push(key);
+ }
+ });
+ selectWay = 'onSelectAll';
+ checked = false;
+ break;
+ case 'invert':
+ changeableRowKeys.forEach(key => {
+ if (selectedRowKeys.indexOf(key) < 0) {
+ selectedRowKeys.push(key);
+ } else {
+ selectedRowKeys.splice(selectedRowKeys.indexOf(key), 1);
+ }
+ changeRowKeys.push(key);
+ selectWay = 'onSelectInvert';
+ });
+ break;
+ default:
+ break;
+ }
+
+ this.store.setState({
+ selectionDirty: true,
+ });
+ // when select custom selection, callback selections[n].onSelect
+ const { rowSelection } = this.props;
+ let customSelectionStartIndex = 2;
+ if (rowSelection && rowSelection.hideDefaultSelections) {
+ customSelectionStartIndex = 0;
+ }
+ if (index >= customSelectionStartIndex && typeof onSelectFunc === 'function') {
+ return onSelectFunc(changeableRowKeys);
+ }
+ this.setSelectedRowKeys(selectedRowKeys, {
+ selectWay: selectWay,
+ checked,
+ changeRowKeys,
+ });
+ }
+
+ handlePageChange = (current: number, ...otherArguments: any[]) => {
+ const props = this.props;
+ let pagination = { ...this.state.pagination };
+ if (current) {
+ pagination.current = current;
+ } else {
+ pagination.current = pagination.current || 1;
+ }
+ pagination.onChange!(pagination.current, ...otherArguments);
+
+ const newState = {
+ pagination,
+ };
+ // Controlled current prop will not respond user interaction
+ if (props.pagination &&
+ typeof props.pagination === 'object' &&
+ 'current' in (props.pagination as Object)) {
+ newState.pagination = {
+ ...pagination,
+ current: this.state.pagination.current,
+ };
+ }
+ this.setState(newState);
+
+ this.store.setState({
+ selectionDirty: false,
+ });
+
+ const onChange = this.props.onChange;
+ if (onChange) {
+ onChange.apply(null, this.prepareParamsArguments({
+ ...this.state,
+ selectionDirty: false,
+ pagination,
+ }));
+ }
+ }
+
+ renderSelectionBox = (type: RowSelectionType | undefined) => {
+ return (_: any, record: T, index: number) => {
+ let rowIndex = this.getRecordKey(record, index); // 从 1 开始
+ const props = this.getCheckboxPropsByItem(record, index);
+ const handleChange = (e: React.ChangeEvent) => {
+ type === 'radio' ? this.handleRadioSelect(record, rowIndex, e) :
+ this.handleSelect(record, rowIndex, e);
+ };
+
+ return (
+
+
+
+ );
+ };
+ }
+
+ getRecordKey = (record: T, index: number) => {
+ const rowKey = this.props.rowKey;
+ const recordKey = (typeof rowKey === 'function') ?
+ rowKey(record, index) : (record as any)[rowKey as string];
+ warning(recordKey !== undefined,
+ 'Each record in dataSource of table should have a unique `key` prop, or set `rowKey` to an unique primary key,' +
+ 'see https://u.ant.design/table-row-key',
+ );
+ return recordKey === undefined ? index : recordKey;
+ }
+
+ getPopupContainer = () => {
+ return ReactDOM.findDOMNode(this) as HTMLElement;
+ }
+
+ renderRowSelection(locale: TableLocale) {
+ const { prefixCls, rowSelection } = this.props;
+ const columns = this.columns.concat();
+ if (rowSelection) {
+ const data = this.getFlatCurrentPageData().filter((item, index) => {
+ if (rowSelection.getCheckboxProps) {
+ return !this.getCheckboxPropsByItem(item, index).disabled;
+ }
+ return true;
+ });
+ let selectionColumnClass = classNames(`${prefixCls}-selection-column`, {
+ [`${prefixCls}-selection-column-custom`]: rowSelection.selections,
+ });
+ const selectionColumn: ColumnProps = {
+ key: 'selection-column',
+ render: this.renderSelectionBox(rowSelection.type),
+ className: selectionColumnClass,
+ fixed: rowSelection.fixed,
+ };
+ if (rowSelection.type !== 'radio') {
+ const checkboxAllDisabled = data.every((item, index) => this.getCheckboxPropsByItem(item, index).disabled);
+ selectionColumn.title = (
+
+ );
+ }
+ if ('fixed' in rowSelection) {
+ selectionColumn.fixed = rowSelection.fixed;
+ } else if (columns.some(column => column.fixed === 'left' || column.fixed === true)) {
+ selectionColumn.fixed = 'left';
+ }
+ if (columns[0] && columns[0].key === 'selection-column') {
+ columns[0] = selectionColumn;
+ } else {
+ columns.unshift(selectionColumn);
+ }
+ }
+ return columns;
+ }
+
+ getColumnKey(column: ColumnProps, index?: number) {
+ return column.key || column.dataIndex || index;
+ }
+
+ getMaxCurrent(total: number) {
+ const { current, pageSize } = this.state.pagination;
+ if ((current! - 1) * pageSize! >= total) {
+ return Math.floor((total - 1) / pageSize!) + 1;
+ }
+ return current;
+ }
+
+ isSortColumn(column: ColumnProps) {
+ const { sortColumn } = this.state;
+ if (!column || !sortColumn) {
+ return false;
+ }
+ return this.getColumnKey(sortColumn) === this.getColumnKey(column);
+ }
+
+ renderColumnsDropdown(columns: ColumnProps[], locale: TableLocale) {
+ const { prefixCls, dropdownPrefixCls } = this.props;
+ const { sortOrder } = this.state;
+ return treeMap(columns, (originColumn, i) => {
+ let column = { ...originColumn };
+ let key = this.getColumnKey(column, i) as string;
+ let filterDropdown;
+ let sortButton;
+ if ((column.filters && column.filters.length > 0) || column.filterDropdown) {
+ let colFilters = this.state.filters[key] || [];
+ filterDropdown = (
+
+ );
+ }
+ if (column.sorter) {
+ let isSortColumn = this.isSortColumn(column);
+ if (isSortColumn) {
+ column.className = classNames(column.className, {
+ [`${prefixCls}-column-sort`]: sortOrder,
+ });
+ }
+ const isAscend = isSortColumn && sortOrder === 'ascend';
+ const isDescend = isSortColumn && sortOrder === 'descend';
+ sortButton = (
+
+ this.toggleSortOrder('ascend', column)}
+ >
+
+
+ this.toggleSortOrder('descend', column)}
+ >
+
+
+
+ );
+ }
+ column.title = (
+
+ {column.title}
+ {sortButton}
+ {filterDropdown}
+
+ );
+
+ if (sortButton || filterDropdown) {
+ column.className = classNames(`${prefixCls}-column-has-filters`, column.className);
+ }
+
+ return column;
+ });
+ }
+
+ handleShowSizeChange = (current: number, pageSize: number) => {
+ const pagination = this.state.pagination;
+ pagination.onShowSizeChange!(current, pageSize);
+ const nextPagination = {
+ ...pagination,
+ pageSize,
+ current,
+ };
+ this.setState({ pagination: nextPagination });
+
+ const onChange = this.props.onChange;
+ if (onChange) {
+ onChange.apply(null, this.prepareParamsArguments({
+ ...this.state,
+ pagination: nextPagination,
+ }));
+ }
+ }
+
+ renderPagination() {
+ // 强制不需要分页
+ if (!this.hasPagination()) {
+ return null;
+ }
+ let size = 'default';
+ const { pagination } = this.state;
+ if (pagination.size) {
+ size = pagination.size;
+ } else if (this.props.size as string === 'middle' || this.props.size === 'small') {
+ size = 'small';
+ }
+ let total = pagination.total || this.getLocalData().length;
+ return (total > 0) ? (
+
+ ) : null;
+ }
+
+ // Get pagination, filters, sorter
+ prepareParamsArguments(state: any): [any, string[], Object] {
+ const pagination = { ...state.pagination };
+ // remove useless handle function in Table.onChange
+ delete pagination.onChange;
+ delete pagination.onShowSizeChange;
+ const filters = state.filters;
+ const sorter: any = {};
+ if (state.sortColumn && state.sortOrder) {
+ sorter.column = state.sortColumn;
+ sorter.order = state.sortOrder;
+ sorter.field = state.sortColumn.dataIndex;
+ sorter.columnKey = this.getColumnKey(state.sortColumn);
+ }
+ return [pagination, filters, sorter];
+ }
+
+ findColumn(myKey: string | number) {
+ let column;
+ treeMap(this.columns, c => {
+ if (this.getColumnKey(c) === myKey) {
+ column = c;
+ }
+ });
+ return column;
+ }
+
+ getCurrentPageData() {
+ let data = this.getLocalData();
+ let current: number;
+ let pageSize: number;
+ let state = this.state;
+ // 如果没有分页的话,默认全部展示
+ if (!this.hasPagination()) {
+ pageSize = Number.MAX_VALUE;
+ current = 1;
+ } else {
+ pageSize = state.pagination.pageSize as number;
+ current = this.getMaxCurrent(state.pagination.total || data.length) as number;
+ }
+
+ // 分页
+ // ---
+ // 当数据量少于等于每页数量时,直接设置数据
+ // 否则进行读取分页数据
+ if (data.length > pageSize || pageSize === Number.MAX_VALUE) {
+ data = data.filter((_, i) => {
+ return i >= (current - 1) * pageSize && i < current * pageSize;
+ });
+ }
+ return data;
+ }
+
+ getFlatData() {
+ return flatArray(this.getLocalData());
+ }
+
+ getFlatCurrentPageData() {
+ return flatArray(this.getCurrentPageData());
+ }
+
+ recursiveSort(data: T[], sorterFn: (a: any, b: any) => number): T[] {
+ const { childrenColumnName = 'children' } = this.props;
+ return data.sort(sorterFn).map((item: any) => (item[childrenColumnName] ? {
+ ...item,
+ [childrenColumnName]: this.recursiveSort(item[childrenColumnName], sorterFn),
+ } : item));
+ }
+
+ getLocalData() {
+ const state = this.state;
+ const { dataSource } = this.props;
+ let data = dataSource || [];
+ // 优化本地排序
+ data = data.slice(0);
+ const sorterFn = this.getSorterFn();
+ if (sorterFn) {
+ data = this.recursiveSort(data, sorterFn);
+ }
+ // 筛选
+ if (state.filters) {
+ Object.keys(state.filters).forEach((columnKey) => {
+ let col = this.findColumn(columnKey) as any;
+ if (!col) {
+ return;
+ }
+ let values = state.filters[columnKey] || [];
+ if (values.length === 0) {
+ return;
+ }
+ const onFilter = col.onFilter;
+ data = onFilter ? data.filter(record => {
+ return values.some(v => onFilter(v, record));
+ }) : data;
+ });
+ }
+ return data;
+ }
+
+ createComponents(components: TableComponents = {}, prevComponents?: TableComponents) {
+ const bodyRow = components && components.body && components.body.row;
+ const preBodyRow = prevComponents && prevComponents.body && prevComponents.body.row;
+ if (!this.components || bodyRow !== preBodyRow) {
+ this.components = { ...components };
+ this.components.body = {
+ ...components.body,
+ row: createBodyRow(bodyRow),
+ };
+ }
+ }
+
+ renderTable = (contextLocale: TableLocale, loading: SpinProps) => {
+ const locale = { ...contextLocale, ...this.props.locale };
+ const { style, className, prefixCls, showHeader, ...restProps } = this.props;
+ const data = this.getCurrentPageData();
+ const expandIconAsCell = this.props.expandedRowRender && this.props.expandIconAsCell !== false;
+
+ const classString = classNames({
+ [`${prefixCls}-${this.props.size}`]: true,
+ [`${prefixCls}-bordered`]: this.props.bordered,
+ [`${prefixCls}-empty`]: !data.length,
+ [`${prefixCls}-without-column-header`]: !showHeader,
+ });
+
+ let columns = this.renderRowSelection(locale);
+ columns = this.renderColumnsDropdown(columns, locale);
+ columns = columns.map((column, i) => {
+ const newColumn = { ...column };
+ newColumn.key = this.getColumnKey(newColumn, i);
+ return newColumn;
+ });
+ let expandIconColumnIndex = (columns[0] && columns[0].key === 'selection-column') ? 1 : 0;
+ if ('expandIconColumnIndex' in restProps) {
+ expandIconColumnIndex = restProps.expandIconColumnIndex as number;
+ }
+
+ return (
+
+ );
+ }
+
+ render() {
+ const { style, className, prefixCls } = this.props;
+ const data = this.getCurrentPageData();
+
+ let loading = this.props.loading as SpinProps;
+ if (typeof loading === 'boolean') {
+ loading = {
+ spinning: loading,
+ };
+ }
+
+ const table = (
+
+ {(locale) => this.renderTable(locale, loading)}
+
+ );
+
+ // if there is no pagination or no data,
+ // the height of spin should decrease by half of pagination
+ const paginationPatchClass = (this.hasPagination() && data && data.length !== 0)
+ ? `${prefixCls}-with-pagination` : `${prefixCls}-without-pagination`;
+
+ return (
+
+
+ {table}
+ {this.renderPagination()}
+
+
+ );
+ }
+}
diff --git a/components/table/createBodyRow.jsx b/components/table/createBodyRow.jsx
new file mode 100644
index 000000000..c3c0dc494
--- /dev/null
+++ b/components/table/createBodyRow.jsx
@@ -0,0 +1,59 @@
+import PropTypes from '../_util/vue-types'
+
+import { Store } from './createStore'
+
+const BodyRowProps = {
+ store: Store,
+ rowKey: PropTypes.string,
+ prefixCls: PropTypes.string,
+}
+
+export default function createTableRow (Component = 'tr') {
+ const BodyRow = {
+ name: 'BodyRow',
+ props: BodyRowProps,
+ data () {
+ const { selectedRowKeys } = this.store.getState()
+
+ return {
+ selected: selectedRowKeys.indexOf(this.rowKey) >= 0,
+ }
+ },
+
+ mounted () {
+ this.subscribe()
+ },
+
+ beforeDestroy () {
+ if (this.unsubscribe) {
+ this.unsubscribe()
+ }
+ },
+ methods: {
+ subscribe () {
+ const { store, rowKey } = this
+ this.unsubscribe = store.subscribe(() => {
+ const { selectedRowKeys } = this.store.getState()
+ const selected = selectedRowKeys.indexOf(rowKey) >= 0
+ if (selected !== this.selected) {
+ this.selected = selected
+ }
+ })
+ },
+ },
+
+ render () {
+ const className = {
+ [`${this.props.prefixCls}-row-selected`]: this.selected,
+ }
+
+ return (
+
+ {this.$slots.default}
+
+ )
+ },
+ }
+
+ return BodyRow
+}
diff --git a/components/table/createStore.jsx b/components/table/createStore.jsx
new file mode 100644
index 000000000..f8111c98f
--- /dev/null
+++ b/components/table/createStore.jsx
@@ -0,0 +1,11 @@
+import PropTypes from '../_util/vue-types'
+export const Store = PropTypes.shape({
+ setState: PropTypes.func,
+ getState: PropTypes.func,
+ subscribe: PropTypes.func,
+}).loose
+
+import create from '../_util/store/create'
+const createStore = create
+
+export default createStore
diff --git a/components/table/filterDropdown.tsx b/components/table/filterDropdown.tsx
new file mode 100755
index 000000000..5ff8eb0f2
--- /dev/null
+++ b/components/table/filterDropdown.tsx
@@ -0,0 +1,228 @@
+import * as React from 'react';
+import * as ReactDOM from 'react-dom';
+import Menu, { SubMenu, Item as MenuItem } from 'rc-menu';
+import closest from 'dom-closest';
+import classNames from 'classnames';
+import Dropdown from '../dropdown';
+import Icon from '../icon';
+import Checkbox from '../checkbox';
+import Radio from '../radio';
+import FilterDropdownMenuWrapper from './FilterDropdownMenuWrapper';
+import { FilterMenuProps, FilterMenuState, ColumnProps, ColumnFilterItem } from './interface';
+
+export default class FilterMenu extends React.Component, FilterMenuState> {
+ static defaultProps = {
+ handleFilter() {},
+ column: {},
+ };
+
+ neverShown: boolean;
+
+ constructor(props: FilterMenuProps) {
+ super(props);
+
+ const visible = ('filterDropdownVisible' in props.column) ?
+ props.column.filterDropdownVisible : false;
+
+ this.state = {
+ selectedKeys: props.selectedKeys,
+ keyPathOfSelectedItem: {}, // 记录所有有选中子菜单的祖先菜单
+ visible,
+ };
+ }
+
+ componentDidMount() {
+ const { column } = this.props;
+ this.setNeverShown(column);
+ }
+
+ componentWillReceiveProps(nextProps: FilterMenuProps) {
+ const { column } = nextProps;
+ this.setNeverShown(column);
+ const newState = {} as {
+ selectedKeys: string[];
+ visible: boolean;
+ };
+ if ('selectedKeys' in nextProps) {
+ newState.selectedKeys = nextProps.selectedKeys;
+ }
+ if ('filterDropdownVisible' in column) {
+ newState.visible = column.filterDropdownVisible as boolean;
+ }
+ if (Object.keys(newState).length > 0) {
+ this.setState(newState);
+ }
+ }
+
+ setNeverShown = (column: ColumnProps) => {
+ const rootNode = ReactDOM.findDOMNode(this);
+ const filterBelongToScrollBody = !!closest(rootNode, `.ant-table-scroll`);
+ if (filterBelongToScrollBody) {
+ // When fixed column have filters, there will be two dropdown menus
+ // Filter dropdown menu inside scroll body should never be shown
+ // To fix https://github.com/ant-design/ant-design/issues/5010 and
+ // https://github.com/ant-design/ant-design/issues/7909
+ this.neverShown = !!column.fixed;
+ }
+ }
+
+ setSelectedKeys = ({ selectedKeys }: { selectedKeys: string[] }) => {
+ this.setState({ selectedKeys });
+ }
+
+ setVisible(visible: boolean) {
+ const { column } = this.props;
+ if (!('filterDropdownVisible' in column)) {
+ this.setState({ visible });
+ }
+ if (column.onFilterDropdownVisibleChange) {
+ column.onFilterDropdownVisibleChange(visible);
+ }
+ }
+
+ handleClearFilters = () => {
+ this.setState({
+ selectedKeys: [],
+ }, this.handleConfirm);
+ }
+
+ handleConfirm = () => {
+ this.setVisible(false);
+ this.confirmFilter();
+ }
+
+ onVisibleChange = (visible: boolean) => {
+ this.setVisible(visible);
+ if (!visible) {
+ this.confirmFilter();
+ }
+ }
+
+ confirmFilter() {
+ if (this.state.selectedKeys !== this.props.selectedKeys) {
+ this.props.confirmFilter(this.props.column, this.state.selectedKeys);
+ }
+ }
+
+ renderMenuItem(item: ColumnFilterItem) {
+ const { column } = this.props;
+ const multiple = ('filterMultiple' in column) ? column.filterMultiple : true;
+ const input = multiple ? (
+ = 0} />
+ ) : (
+ = 0} />
+ );
+
+ return (
+
+ );
+ }
+
+ hasSubMenu() {
+ const { column: { filters = [] } } = this.props;
+ return filters.some(item => !!(item.children && item.children.length > 0));
+ }
+
+ renderMenus(items: ColumnFilterItem[]): React.ReactElement[] {
+ return items.map(item => {
+ if (item.children && item.children.length > 0) {
+ const { keyPathOfSelectedItem } = this.state;
+ const containSelected = Object.keys(keyPathOfSelectedItem).some(
+ key => keyPathOfSelectedItem[key].indexOf(item.value) >= 0,
+ );
+ const subMenuCls = containSelected ? `${this.props.dropdownPrefixCls}-submenu-contain-selected` : '';
+ return (
+
+ {this.renderMenus(item.children)}
+
+ );
+ }
+ return this.renderMenuItem(item);
+ });
+ }
+
+ handleMenuItemClick = (info: { keyPath: string, key: string }) => {
+ if (info.keyPath.length <= 1) {
+ return;
+ }
+ const keyPathOfSelectedItem = this.state.keyPathOfSelectedItem;
+ if (this.state.selectedKeys.indexOf(info.key) >= 0) {
+ // deselect SubMenu child
+ delete keyPathOfSelectedItem[info.key];
+ } else {
+ // select SubMenu child
+ keyPathOfSelectedItem[info.key] = info.keyPath;
+ }
+ this.setState({ keyPathOfSelectedItem });
+ }
+
+ renderFilterIcon = () => {
+ const { column, locale, prefixCls } = this.props;
+ const filterIcon = column.filterIcon as any;
+ const dropdownSelectedClass = this.props.selectedKeys.length > 0 ? `${prefixCls}-selected` : '';
+
+ return filterIcon ? React.cloneElement(filterIcon as any, {
+ title: locale.filterTitle,
+ className: classNames(filterIcon.className, {
+ [`${prefixCls}-icon`]: true,
+ }),
+ }) : ;
+ }
+ render() {
+ const { column, locale, prefixCls, dropdownPrefixCls, getPopupContainer } = this.props;
+ // default multiple selection in filter dropdown
+ const multiple = ('filterMultiple' in column) ? column.filterMultiple : true;
+ const dropdownMenuClass = classNames({
+ [`${dropdownPrefixCls}-menu-without-submenu`]: !this.hasSubMenu(),
+ });
+ const menus = column.filterDropdown ? (
+
+ {column.filterDropdown}
+
+ ) : (
+
+
+
+
+ );
+
+ return (
+
+ {this.renderFilterIcon()}
+
+ );
+ }
+}
diff --git a/components/table/index.en-US.md b/components/table/index.en-US.md
new file mode 100644
index 000000000..c7243aa62
--- /dev/null
+++ b/components/table/index.en-US.md
@@ -0,0 +1,210 @@
+---
+category: Components
+cols: 1
+type: Data Display
+title: Table
+---
+
+A table displays rows of data.
+
+## When To Use
+
+- To display a collection of structured data.
+- To sort, search, paginate, filter data.
+
+## How To Use
+
+Specify `dataSource` of Table as an array of data.
+
+```jsx
+const dataSource = [{
+ key: '1',
+ name: 'Mike',
+ age: 32,
+ address: '10 Downing Street'
+}, {
+ key: '2',
+ name: 'John',
+ age: 42,
+ address: '10 Downing Street'
+}];
+
+const columns = [{
+ title: 'Name',
+ dataIndex: 'name',
+ key: 'name',
+}, {
+ title: 'Age',
+ dataIndex: 'age',
+ key: 'age',
+}, {
+ title: 'Address',
+ dataIndex: 'address',
+ key: 'address',
+}];
+
+
+```
+
+## API
+
+### Table
+
+| Property | Description | Type | Default |
+| -------- | ----------- | ---- | ------- |
+| bordered | Whether to show all table borders | boolean | `false` |
+| columns | Columns of table | [ColumnProps](https://git.io/vMMXC)\[] | - |
+| components | Override default table elements | object | - |
+| dataSource | Data record array to be displayed | any\[] | - |
+| defaultExpandAllRows | Expand all rows initially | boolean | `false` |
+| defaultExpandedRowKeys | Initial expanded row keys | string\[] | - |
+| expandedRowKeys | Current expanded row keys | string\[] | - |
+| expandedRowRender | Expanded container render for each row | Function(record):ReactNode | - |
+| expandRowByClick | Whether to expand row by clicking anywhere in the whole row | boolean | `false` |
+| footer | Table footer renderer | Function(currentPageData) | |
+| indentSize | Indent size in pixels of tree data | number | 15 |
+| loading | Loading status of table | boolean\|[object](https://ant.design/components/spin-cn/#API) ([more](https://github.com/ant-design/ant-design/issues/4544#issuecomment-271533135)) | `false` |
+| locale | i18n text including filter, sort, empty text, etc | object | filterConfirm: 'Ok'
filterReset: 'Reset'
emptyText: 'No Data'
[Default](https://github.com/ant-design/ant-design/issues/575#issuecomment-159169511) |
+| pagination | Pagination [config](/components/pagination/), hide it by setting it to `false` | object | |
+| 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):string | `key` |
+| rowSelection | Row selection [config](#rowSelection) | object | null |
+| scroll | Whether table can be scrolled in x/y direction, `x` or `y` can be a number that indicates the width and height of table body | object | - |
+| showHeader | Whether to show table header | boolean | `true` |
+| size | Size of table | `default` \| `middle` \| `small` | `default` |
+| title | Table title renderer | Function(currentPageData) | |
+| onChange | Callback executed when pagination, filters or sorter is changed | Function(pagination, filters, sorter) | |
+| onExpand | Callback executed when the row expand icon is clicked | Function(expanded, record) | |
+| onExpandedRowsChange | Callback executed when the expanded rows change | Function(expandedRows) | |
+| onHeaderRow | Set props on per header row | Function(column, index) | - |
+| onRow | Set props on per row | Function(record, index) | - |
+
+#### onRow usage
+
+Same as `onRow` `onHeaderRow` `onCell` `onHeaderCell`
+
+```jsx
+ {
+ return {
+ onClick: () => {}, // click row
+ onMouseEnter: () => {}, // mouse enter row
+ onXxxx...
+ };
+ )}
+ onHeaderRow={(column) => {
+ return {
+ onClick: () => {}, // click header row
+ };
+ )}
+/>
+```
+
+### Column
+
+One of the Table `columns` prop for describing the table's columns, Column has the same API.
+
+| Property | Description | Type | Default |
+| -------- | ----------- | ---- | ------- |
+| className | className of this column | string | - |
+| colSpan | Span of this column's title | number | |
+| dataIndex | Display field of the data record, could be set like `a.b.c` | string | - |
+| defaultSortOrder | Default order of sorted values: `'ascend'` `'descend'` `null` | string | - |
+| filterDropdown | Customized filter overlay | ReactNode | - |
+| 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 | ReactNode | `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 | - |
+| render | Renderer of the table cell. The return value should be a ReactNode, or an object for [colSpan/rowSpan config](#components-table-demo-colspan-rowspan) | Function(text, record, index) {} | - |
+| 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 | - |
+| title | Title of this column | string\|ReactNode | - |
+| width | Width of this column | string\|number | - |
+| onCell | Set props on per cell | Function(record) | - |
+| onFilter | Callback executed when the confirm filter button is clicked | Function | - |
+| onFilterDropdownVisibleChange | Callback executed when `filterDropdownVisible` is changed | function(visible) {} | - |
+| onHeaderCell | Set props on per header cell | Function(column) | - |
+
+### ColumnGroup
+
+| Property | Description | Type | Default |
+| -------- | ----------- | ---- | ------- |
+| title | Title of the column group | string\|ReactNode | - |
+
+### rowSelection
+
+Properties for row selection.
+
+| Property | Description | Type | Default |
+| -------- | ----------- | ---- | ------- |
+| 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](#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) | - |
+| onSelectAll | Callback executed when select/deselect all rows | Function(selected, selectedRows, changeRows) | - |
+| onSelectInvert | Callback executed when row selection is inverted | Function(selectedRows) | - |
+
+### selection
+
+| Property | Description | Type | Default |
+| -------- | ----------- | ---- | ------- |
+| key | Unique key of this selection | string | - |
+| text | Display text of this selection | string\|React.ReactNode | - |
+| onSelect | Callback executed when this selection is clicked | Function(changeableRowKeys) | - |
+
+## Using in TypeScript
+
+```jsx
+import { Table } from 'antd';
+import { ColumnProps } from 'antd/lib/table';
+
+interface IUser {
+ key: number,
+ name: string;
+}
+
+const columns: ColumnProps[] = [{
+ key: 'name',
+ title: 'Name',
+ dataIndex: 'name',
+}];
+
+const data: IUser[] = [{
+ key: 0,
+ name: 'Jack',
+}];
+
+class UserTable extends Table {}
+
+
+
+// Use JSX style API
+class NameColumn extends Table.Column {}
+
+
+
+
+```
+
+## Note
+
+According to [React documentation](https://facebook.github.io/react/docs/lists-and-keys.html#keys), every child in array should be assigned a unique key. The values inside `dataSource` and `columns` should follow this in Table, and `dataSource[i].key` would be treated as key value default for `dataSource`.
+
+If `dataSource[i].key` is not provided, then you should specify the primary key of dataSource value via `rowKey`. If not, warnings like above will show in browser console.
+
+![](https://os.alipayobjects.com/rmsportal/luLdLvhPOiRpyss.png)
+
+```jsx
+// primary key is uid
+return ;
+// or
+return record.uid} />;
+```
diff --git a/components/table/index.jsx b/components/table/index.jsx
new file mode 100644
index 000000000..ce2a96d01
--- /dev/null
+++ b/components/table/index.jsx
@@ -0,0 +1,5 @@
+import Table from './Table'
+
+export * from './interface'
+
+export default Table
diff --git a/components/table/index.zh-CN.md b/components/table/index.zh-CN.md
new file mode 100644
index 000000000..5f959537c
--- /dev/null
+++ b/components/table/index.zh-CN.md
@@ -0,0 +1,210 @@
+---
+category: Components
+cols: 1
+type: Data Display
+title: Table
+subtitle: 表格
+---
+
+展示行列数据。
+
+## 何时使用
+
+- 当有大量结构化的数据需要展现时;
+- 当需要对数据进行排序、搜索、分页、自定义操作等复杂行为时。
+
+## 如何使用
+
+指定表格的数据源 `dataSource` 为一个数组。
+
+```jsx
+const dataSource = [{
+ key: '1',
+ name: '胡彦斌',
+ age: 32,
+ address: '西湖区湖底公园1号'
+}, {
+ key: '2',
+ name: '胡彦祖',
+ age: 42,
+ address: '西湖区湖底公园1号'
+}];
+
+const columns = [{
+ title: '姓名',
+ dataIndex: 'name',
+ key: 'name',
+}, {
+ title: '年龄',
+ dataIndex: 'age',
+ key: 'age',
+}, {
+ title: '住址',
+ dataIndex: 'address',
+ key: 'address',
+}];
+
+
+```
+
+## API
+
+### Table
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| bordered | 是否展示外边框和列边框 | boolean | false |
+| columns | 表格列的配置描述,具体项见下表 | [ColumnProps](https://git.io/vMMXC)\[] | - |
+| components | 覆盖默认的 table 元素 | object | - |
+| dataSource | 数据数组 | any\[] | |
+| defaultExpandAllRows | 初始时,是否展开所有行 | boolean | false |
+| defaultExpandedRowKeys | 默认展开的行 | string\[] | - |
+| expandedRowKeys | 展开的行,控制属性 | string\[] | - |
+| expandedRowRender | 额外的展开行 | Function(record):ReactNode | - |
+| expandRowByClick | 通过点击行来展开子行 | boolean | `false` |
+| footer | 表格尾部 | Function(currentPageData) | |
+| indentSize | 展示树形数据时,每层缩进的宽度,以 px 为单位 | number | 15 |
+| loading | 页面是否加载中 | boolean\|[object](https://ant.design/components/spin-cn/#API) ([更多](https://github.com/ant-design/ant-design/issues/4544#issuecomment-271533135)) | false |
+| locale | 默认文案设置,目前包括排序、过滤、空数据文案 | object | filterConfirm: '确定'
filterReset: '重置'
emptyText: '暂无数据'
[默认值](https://github.com/ant-design/ant-design/issues/575#issuecomment-159169511) |
+| pagination | 分页器,配置项参考 [pagination](/components/pagination/),设为 false 时不展示和进行分页 | object | |
+| rowClassName | 表格行的类名 | Function(record, index):string | - |
+| rowKey | 表格行 key 的取值,可以是字符串或一个函数 | string\|Function(record):string | 'key' |
+| rowSelection | 列表项是否可选择,[配置项](#rowSelection) | object | null |
+| scroll | 横向或纵向支持滚动,也可用于指定滚动区域的宽高度:`{{ x: true, y: 300 }}` | object | - |
+| showHeader | 是否显示表头 | boolean | true |
+| size | 正常或迷你类型,`default` or `small` | string | default |
+| title | 表格标题 | Function(currentPageData) | |
+| onChange | 分页、排序、筛选变化时触发 | Function(pagination, filters, sorter) | |
+| onExpand | 点击展开图标时触发 | Function(expanded, record) | |
+| onExpandedRowsChange | 展开的行变化时触发 | Function(expandedRows) | |
+| onHeaderRow | 设置头部行属性 | Function(column, index) | - |
+| onRow | 设置行属性 | Function(record, index) | - |
+
+
+#### onRow 用法
+
+适用于 `onRow` `onHeaderRow` `onCell` `onHeaderCell`。
+
+```jsx
+ {
+ return {
+ onClick: () => {}, // 点击行
+ onMouseEnter: () => {}, // 鼠标移入行
+ onXxxx...
+ };
+ )}
+ onHeaderRow={(column) => {
+ return {
+ onClick: () => {}, // 点击表头行
+ };
+ )}
+/>
+```
+
+### Column
+
+列描述数据对象,是 columns 中的一项,Column 使用相同的 API。
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| className | 列的 className | string | - |
+| colSpan | 表头列合并,设置为 0 时,不渲染 | number | |
+| dataIndex | 列数据在数据项中对应的 key,支持 `a.b.c` 的嵌套写法 | string | - |
+| filterDropdown | 可以自定义筛选菜单,此函数只负责渲染图层,需要自行编写各种交互 | ReactNode | - |
+| filterDropdownVisible | 用于控制自定义筛选菜单是否可见 | boolean | - |
+| filtered | 标识数据是否经过过滤,筛选图标会高亮 | boolean | false |
+| filteredValue | 筛选的受控属性,外界可用此控制列的筛选状态,值为已筛选的 value 数组 | string\[] | - |
+| filterIcon | 自定义 fiter 图标。 | ReactNode | false |
+| filterMultiple | 是否多选 | boolean | true |
+| filters | 表头的筛选菜单项 | object\[] | - |
+| fixed | 列是否固定,可选 `true`(等效于 left) `'left'` `'right'` | boolean\|string | false |
+| key | React 需要的 key,如果已经设置了唯一的 `dataIndex`,可以忽略这个属性 | string | - |
+| render | 生成复杂数据的渲染函数,参数分别为当前行的值,当前行数据,行索引,@return里面可以设置表格[行/列合并](#components-table-demo-colspan-rowspan) | Function(text, record, index) {} | - |
+| 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 | - |
+| title | 列头显示文字 | string\|ReactNode | - |
+| width | 列宽度 | string\|number | - |
+| onCell | 设置单元格属性 | Function(record) | - |
+| onFilter | 本地模式下,确定筛选的运行函数 | Function | - |
+| onFilterDropdownVisibleChange | 自定义筛选菜单可见变化时调用 | function(visible) {} | - |
+| onHeaderCell | 设置头部单元格属性 | Function(column) | - |
+
+### ColumnGroup
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| title | 列头显示文字 | string\|ReactNode | - |
+
+### rowSelection
+
+选择功能的配置。
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| fixed | 把选择框列固定在左边 | boolean | - |
+| getCheckboxProps | 选择框的默认属性配置 | Function(record) | - |
+| hideDefaultSelections | 去掉『全选』『反选』两个默认选项 | boolean | false |
+| selectedRowKeys | 指定选中项的 key 数组,需要和 onChange 进行配合 | string\[] | \[] |
+| selections | 自定义选择项 [配置项](#selection), 设为 `true` 时使用默认选择项 | object\[]\|boolean | true |
+| type | 多选/单选,`checkbox` or `radio` | string | `checkbox` |
+| onChange | 选中项发生变化的时的回调 | Function(selectedRowKeys, selectedRows) | - |
+| onSelect | 用户手动选择/取消选择某列的回调 | Function(record, selected, selectedRows) | - |
+| onSelectAll | 用户手动选择/取消选择所有列的回调 | Function(selected, selectedRows, changeRows) | - |
+| onSelectInvert | 用户手动选择反选的回调 | Function(selectedRows) | - |
+
+### selection
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| key | React 需要的 key,建议设置 | string | - |
+| text | 选择项显示的文字 | string\|React.ReactNode | - |
+| onSelect | 选择项点击回调 | Function(changeableRowKeys) | - |
+
+## 在 TypeScript 中使用
+
+```jsx
+import { Table } from 'antd';
+import { ColumnProps } from 'antd/lib/table';
+
+interface IUser {
+ key: number;
+ name: string;
+}
+
+const columns: ColumnProps[] = [{
+ key: 'name',
+ title: 'Name',
+ dataIndex: 'name',
+}];
+
+const data: IUser[] = [{
+ key: 0,
+ name: 'Jack',
+}];
+
+class UserTable extends Table {}
+
+
+// 使用 JSX 风格的 API
+class NameColumn extends Table.Column {}
+
+
+
+
+```
+
+## 注意
+
+按照 [React 的规范](https://facebook.github.io/react/docs/lists-and-keys.html#keys),所有的组件数组必须绑定 key。在 Table 中,`dataSource` 和 `columns` 里的数据值都需要指定 `key` 值。对于 `dataSource` 默认将每列数据的 `key` 属性作为唯一的标识。
+
+如果你的数据没有这个属性,务必使用 `rowKey` 来指定数据列的主键。若没有指定,控制台会出现以下的提示,表格组件也会出现各类奇怪的错误。
+
+![](https://os.alipayobjects.com/rmsportal/luLdLvhPOiRpyss.png)
+
+```jsx
+// 比如你的数据主键是 uid
+return ;
+// 或
+return record.uid} />;
+```
diff --git a/components/table/interface.js b/components/table/interface.js
new file mode 100644
index 000000000..707897ad0
--- /dev/null
+++ b/components/table/interface.js
@@ -0,0 +1,186 @@
+import PropTypes from '../_util/vue-types'
+import { PaginationProps as getPaginationProps } from '../pagination'
+import { SpinProps as getSpinProps } from '../spin'
+import { Store } from './createStore'
+
+const PaginationProps = getPaginationProps()
+const SpinProps = getSpinProps()
+
+// export type CompareFn = ((a: T, b: T) => number);
+export const ColumnFilterItem = PropTypes.shape({
+ text: PropTypes.string,
+ value: PropTypes.string,
+ children: PropTypes.array,
+}).loose
+
+export const ColumnProps = {
+ title: PropTypes.any,
+ // key?: React.Key;
+ dataIndex: PropTypes.string,
+ render: PropTypes.func,
+ filters: PropTypes.arrayOf(ColumnFilterItem),
+ // onFilter: (value: any, record: T) => PropTypes.bool,
+ filterMultiple: PropTypes.bool,
+ filterDropdown: PropTypes.any,
+ filterDropdownVisible: PropTypes.bool,
+ // onFilterDropdownVisibleChange?: (visible: boolean) => void;
+ sorter: PropTypes.oneOfType([PropTypes.boolean, PropTypes.func]),
+ defaultSortOrder: PropTypes.oneOf(['ascend', 'descend']),
+ colSpan: PropTypes.number,
+ width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ // className: string,
+ fixed: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['left', 'right'])]),
+ filterIcon: PropTypes.any,
+ filteredValue: PropTypes.array,
+ sortOrder: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['ascend', 'descend'])]),
+ // children?: ColumnProps[];
+ // onCellClick?: (record: T, event: any) => void;
+ // onCell?: (record: T) => any;
+ // onHeaderCell?: (props: ColumnProps) => any;
+}
+
+// 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,
+}).loose
+
+export const RowSelectionType = PropTypes.oneOf(['checkbox', 'radio'])
+// export type SelectionSelectFn = (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;
+ // onSelectAll?: (selected: boolean, selectedRows: Object[], changeRows: Object[]) => any;
+ // onSelectInvert?: (selectedRows: Object[]) => any;
+ selections: PropTypes.oneOfType([PropTypes.array, PropTypes.bool]),
+ hideDefaultSelections: PropTypes.bool,
+ fixed: PropTypes.bool,
+}
+
+export const TableProps = {
+ prefixCls: PropTypes.string,
+ dropdownPrefixCls: PropTypes.string,
+ rowSelection: PropTypes.shape(TableRowSelection).loose,
+ pagination: PropTypes.oneOfType([PropTypes.shape(PaginationProps).loose, PropTypes.bool]),
+ size: PropTypes.oneOf(['default', 'middle', 'small']),
+ dataSource: PropTypes.array,
+ components: PropTypes.object,
+ columns: PropTypes.array,
+ rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
+ rowClassName: PropTypes.func,
+ expandedRowRender: PropTypes.any,
+ defaultExpandAllRows: PropTypes.bool,
+ defaultExpandedRowKeys: PropTypes.array,
+ expandedRowKeys: PropTypes.array,
+ expandIconAsCell: PropTypes.bool,
+ expandIconColumnIndex: PropTypes.number,
+ expandRowByClick: PropTypes.bool,
+ // onExpandedRowsChange?: (expandedRowKeys: string[] | number[]) => void;
+ // onExpand?: (expanded: boolean, record: T) => void;
+ // onChange?: (pagination: PaginationProps | boolean, filters: string[], sorter: Object) => any;
+ loading: PropTypes.oneOfType([PropTypes.shape(SpinProps).loose, PropTypes.bool]),
+ locale: PropTypes.object,
+ indentSize: PropTypes.number,
+ // onRowClick?: (record: T, index: number, event: Event) => any;
+ // onRow?: (record: T, index: number) => any;
+ useFixedHeader: PropTypes.bool,
+ bordered: PropTypes.bool,
+ showHeader: PropTypes.bool,
+ footer: PropTypes.any,
+ title: PropTypes.any,
+ scroll: PropTypes.object,
+ childrenColumnName: PropTypes.string,
+ bodyStyle: PropTypes.any,
+ // className?: PropTypes.string,
+ // style?: React.CSSProperties;
+ // children?: React.ReactNode;
+}
+
+// export interface TableStateFilters {
+// [key: string]: string[];
+// }
+
+// export interface TableState {
+// pagination: PaginationProps;
+// filters: TableStateFilters;
+// sortColumn: ColumnProps | null;
+// sortOrder: PropTypes.string,
+// }
+
+// export type SelectionItemSelectFn = (key: string[]) => any;
+
+// export interface SelectionItem {
+// key: PropTypes.string,
+// text: PropTypes.any,
+// onSelect: SelectionItemSelectFn;
+// }
+
+export const SelectionCheckboxAllProps = {
+ store: Store,
+ locale: PropTypes.any,
+ disabled: PropTypes.bool,
+ getCheckboxPropsByItem: PropTypes.func,
+ getRecordKey: PropTypes.func,
+ data: PropTypes.array,
+ prefixCls: PropTypes.string,
+ // onSelect: (key: string, index: number, selectFunc: any) => void;
+ hideDefaultSelections: PropTypes.bool,
+ selections: PropTypes.oneOfType([PropTypes.array, PropTypes.bool]),
+ getPopupContainer: PropTypes.func,
+}
+
+// export interface SelectionCheckboxAllState {
+// checked: PropTypes.bool,
+// indeterminate: PropTypes.bool,
+// }
+
+export const SelectionBoxProps = {
+ store: Store,
+ type: RowSelectionType,
+ defaultSelection: PropTypes.arrayOf(PropTypes.string),
+ rowIndex: PropTypes.string,
+ name: PropTypes.string,
+ disabled: PropTypes.bool,
+ // onChange: React.ChangeEventHandler;
+}
+
+// export interface SelectionBoxState {
+// checked?: PropTypes.bool,
+// }
+
+export const FilterMenuProps = {
+ locale: TableLocale,
+ selectedKeys: PropTypes.arrayOf(PropTypes.string),
+ column: PropTypes.shape(ColumnProps),
+ confirmFilter: PropTypes.func,
+ prefixCls: PropTypes.string,
+ dropdownPrefixCls: PropTypes.string,
+ getPopupContainer: PropTypes.func,
+}
+
+// export interface FilterMenuState {
+// selectedKeys: string[];
+// keyPathOfSelectedItem: { [key: string]: string };
+// visible?: PropTypes.bool,
+// }
diff --git a/components/table/style/index.js b/components/table/style/index.js
new file mode 100644
index 000000000..efcf7c842
--- /dev/null
+++ b/components/table/style/index.js
@@ -0,0 +1,9 @@
+import '../../style/index.less'
+import './index.less'
+
+// style dependencies
+import '../../radio/style'
+import '../../checkbox/style'
+import '../../dropdown/style'
+import '../../spin/style'
+import '../../pagination/style'
diff --git a/components/table/style/index.less b/components/table/style/index.less
new file mode 100644
index 000000000..baf996590
--- /dev/null
+++ b/components/table/style/index.less
@@ -0,0 +1,568 @@
+@import "../../style/themes/default";
+@import "../../style/mixins/index";
+
+@table-prefix-cls: ~"@{ant-prefix}-table";
+@table-header-icon-color: @text-color-secondary;
+
+.@{table-prefix-cls}-wrapper {
+ .clearfix;
+}
+
+.@{table-prefix-cls} {
+ .reset-component;
+ position: relative;
+ border-radius: @border-radius-base @border-radius-base 0 0;
+
+ &-body {
+ transition: opacity .3s;
+ }
+
+ table {
+ width: 100%;
+ border-collapse: separate;
+ border-spacing: 0;
+ text-align: left;
+ border-radius: @border-radius-base @border-radius-base 0 0;
+ }
+
+ &-thead > tr > th {
+ background: @table-header-bg;
+ transition: background .3s ease;
+ text-align: left;
+ color: @heading-color;
+ font-weight: 500;
+ border-bottom: @border-width-base @border-style-base @border-color-split;
+
+ &[colspan] {
+ text-align: center;
+ border-bottom: 0;
+ }
+
+ .@{iconfont-css-prefix}-filter,
+ .@{table-prefix-cls}-filter-icon {
+ position: relative;
+ margin-left: 8px;
+ font-size: @font-size-base;
+ cursor: pointer;
+ color: @table-header-icon-color;
+ transition: all .3s;
+ width: 14px;
+ font-weight: normal;
+ vertical-align: text-bottom;
+
+ &:hover {
+ color: @text-color;
+ }
+ }
+
+ .@{table-prefix-cls}-column-sorter + .@{iconfont-css-prefix}-filter {
+ margin-left: 4px;
+ }
+
+ .@{table-prefix-cls}-filter-selected.@{iconfont-css-prefix}-filter {
+ color: @primary-color;
+ }
+
+ // https://github.com/ant-design/ant-design/issues/8979
+ &.@{table-prefix-cls}-column-has-filters {
+ overflow: hidden;
+ }
+ }
+
+ &-tbody > tr > td {
+ border-bottom: @border-width-base @border-style-base @border-color-split;
+ transition: all .3s;
+ }
+
+ &-thead > tr,
+ &-tbody > tr {
+ transition: all .3s;
+ &.@{table-prefix-cls}-row-hover,
+ &:hover {
+ & > td {
+ background: @table-row-hover-bg;
+ }
+ }
+ }
+
+ &-thead > tr:hover {
+ background: none;
+ }
+
+ &-footer {
+ padding: @table-padding-vertical @table-padding-horizontal;
+ background: @table-header-bg;
+ border-radius: 0 0 @border-radius-base @border-radius-base;
+ position: relative;
+ border-top: @border-width-base @border-style-base @border-color-split;
+ &:before {
+ content: '';
+ height: 1px;
+ background: @table-header-bg;
+ position: absolute;
+ top: -1px;
+ width: 100%;
+ left: 0;
+ }
+ }
+
+ &.@{table-prefix-cls}-bordered &-footer {
+ border: @border-width-base @border-style-base @border-color-split;
+ }
+
+ &-title {
+ padding: @table-padding-vertical 0;
+ position: relative;
+ top: 1px;
+ border-radius: @border-radius-base @border-radius-base 0 0;
+ }
+
+ &.@{table-prefix-cls}-bordered &-title {
+ border: @border-width-base @border-style-base @border-color-split;
+ padding-left: @table-padding-horizontal;
+ padding-right: @table-padding-horizontal;
+ }
+
+ &-title + &-content {
+ position: relative;
+ border-radius: @border-radius-base @border-radius-base 0 0;
+ overflow: hidden;
+ .@{table-prefix-cls}-bordered & {
+ &,
+ table {
+ border-radius: 0;
+ }
+ }
+ }
+
+ // https://github.com/ant-design/ant-design/issues/4373
+ &-without-column-header &-title + &-content,
+ &-without-column-header table {
+ border-radius: 0;
+ }
+
+ &-tbody > tr.@{table-prefix-cls}-row-selected td {
+ background: @table-selected-row-bg;
+ }
+
+ &-thead > tr > th.@{table-prefix-cls}-column-sort {
+ background: @table-header-sort-bg;
+ }
+
+ &-thead > tr > th,
+ &-tbody > tr > td {
+ padding: @table-padding-vertical @table-padding-horizontal;
+ word-break: break-all;
+ }
+
+ &-thead > tr > th.@{table-prefix-cls}-selection-column-custom {
+ padding-left: 16px;
+ padding-right: 0;
+ }
+
+ &-thead > tr > th.@{table-prefix-cls}-selection-column,
+ &-tbody > tr > td.@{table-prefix-cls}-selection-column {
+ text-align: center;
+ min-width: 62px;
+ width: 62px;
+ .@{ant-prefix}-radio-wrapper {
+ margin-right: 0;
+ }
+ }
+
+ &-expand-icon-th,
+ &-row-expand-icon-cell {
+ text-align: center;
+ min-width: 50px;
+ width: 50px;
+ }
+
+ &-header {
+ background: @table-header-bg;
+ overflow: hidden;
+ }
+
+ &-header table {
+ border-radius: @border-radius-base @border-radius-base 0 0;
+ }
+
+ &-loading {
+ position: relative;
+ .@{table-prefix-cls}-body {
+ background: @component-background;
+ opacity: 0.5;
+ }
+ .@{table-prefix-cls}-spin-holder {
+ height: 20px;
+ line-height: 20px;
+ left: 50%;
+ top: 50%;
+ margin-left: -30px;
+ position: absolute;
+ }
+ .@{table-prefix-cls}-with-pagination {
+ margin-top: -20px;
+ }
+ .@{table-prefix-cls}-without-pagination {
+ margin-top: 10px;
+ }
+ }
+
+ &-column-sorter {
+ position: relative;
+ margin-left: 8px;
+ display: inline-block;
+ width: 14px;
+ height: 14px;
+ vertical-align: middle;
+ text-align: center;
+ font-weight: normal;
+ color: @table-header-icon-color;
+
+ &-up,
+ &-down {
+ line-height: 6px;
+ display: block;
+ width: 14px;
+ height: 6px;
+ cursor: pointer;
+ position: relative;
+ &:hover .@{iconfont-css-prefix} {
+ color: @primary-4;
+ }
+ &.on {
+ .@{iconfont-css-prefix}-caret-up,
+ .@{iconfont-css-prefix}-caret-down {
+ color: @primary-color;
+ }
+ }
+
+ &:after {
+ position: absolute;
+ content: '';
+ height: 30px;
+ width: 14px;
+ left: 0;
+ }
+ }
+
+ &-up:after {
+ bottom: -2px;
+ }
+
+ &-down:after {
+ top: 2px;
+ }
+
+ .@{iconfont-css-prefix}-caret-up,
+ .@{iconfont-css-prefix}-caret-down {
+ .iconfont-size-under-12px(8px);
+ line-height: 4px;
+ height: 4px;
+ transition: all .3s;
+ }
+ }
+
+ &-bordered {
+ .@{table-prefix-cls}-header > table,
+ .@{table-prefix-cls}-body > table,
+ .@{table-prefix-cls}-fixed-left table,
+ .@{table-prefix-cls}-fixed-right table {
+ border: @border-width-base @border-style-base @border-color-split;
+ border-right: 0;
+ border-bottom: 0;
+ }
+
+ &.@{table-prefix-cls}-empty {
+ .@{table-prefix-cls}-placeholder {
+ border-left: @border-width-base @border-style-base @border-color-split;
+ border-right: @border-width-base @border-style-base @border-color-split;
+ }
+ }
+
+ &.@{table-prefix-cls}-fixed-header {
+ .@{table-prefix-cls}-header > table {
+ border-bottom: 0;
+ }
+
+ .@{table-prefix-cls}-body > table {
+ border-top: 0;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+
+ .@{table-prefix-cls}-body-inner > table {
+ border-top: 0;
+ }
+
+ .@{table-prefix-cls}-placeholder {
+ border: 0;
+ }
+ }
+
+ .@{table-prefix-cls}-thead > tr > th {
+ border-bottom: @border-width-base @border-style-base @border-color-split;
+ }
+
+ .@{table-prefix-cls}-thead > tr > th,
+ .@{table-prefix-cls}-tbody > tr > td {
+ border-right: @border-width-base @border-style-base @border-color-split;
+ }
+ }
+
+ &-placeholder {
+ position: relative;
+ padding: @table-padding-vertical @table-padding-horizontal;
+ background: @component-background;
+ border-bottom: @border-width-base @border-style-base @border-color-split;
+ text-align: center;
+ font-size: @font-size-base;
+ color: @text-color-secondary;
+ z-index: 1;
+ .@{iconfont-css-prefix} {
+ margin-right: 4px;
+ }
+ }
+
+ &-pagination.@{ant-prefix}-pagination {
+ margin: 16px 0;
+ float: right;
+ }
+
+ &-filter-dropdown {
+ min-width: 96px;
+ margin-left: -8px;
+ background: @component-background;
+ border-radius: @border-radius-base;
+ box-shadow: @box-shadow-base;
+
+ .@{ant-prefix}-dropdown-menu {
+ border: 0;
+ box-shadow: none;
+ border-radius: @border-radius-base @border-radius-base 0 0;
+
+ // https://github.com/ant-design/ant-design/issues/4916
+ &-without-submenu {
+ max-height: 400px;
+ overflow-x: hidden;
+ }
+
+ &-item > label + span {
+ padding-right: 0;
+ }
+
+ &-sub {
+ border-radius: @border-radius-base;
+ box-shadow: @box-shadow-base;
+ }
+
+ .@{ant-prefix}-dropdown-submenu-contain-selected {
+ .@{ant-prefix}-dropdown-menu-submenu-title:after {
+ color: @primary-color;
+ font-weight: bold;
+ text-shadow: 0 0 2px @primary-2;
+ }
+ }
+ }
+
+ .@{ant-prefix}-dropdown-menu-item {
+ overflow: hidden;
+ }
+
+ > .@{ant-prefix}-dropdown-menu > .@{ant-prefix}-dropdown-menu-item:last-child,
+ > .@{ant-prefix}-dropdown-menu > .@{ant-prefix}-dropdown-menu-submenu:last-child .@{ant-prefix}-dropdown-menu-submenu-title {
+ border-radius: 0;
+ }
+
+ &-btns {
+ overflow: hidden;
+ padding: 7px 8px;
+ border-top: @border-width-base @border-style-base @border-color-split;
+ }
+
+ &-link {
+ color: @link-color;
+ &:hover {
+ color: @link-hover-color;
+ }
+ &:active {
+ color: @link-active-color;
+ }
+ &.confirm {
+ float: left;
+ }
+ &.clear {
+ float: right;
+ }
+ }
+ }
+
+ &-selection {
+ &-select-all-custom {
+ margin-right: 4px !important;
+ }
+
+ .@{iconfont-css-prefix}-down {
+ color: @table-header-icon-color;
+ transition: all .3s;
+ }
+
+ &-menu {
+ min-width: 96px;
+ margin-top: 5px;
+ margin-left: -30px;
+ background: @component-background;
+ border-radius: @border-radius-base;
+ box-shadow: @box-shadow-base;
+
+ .@{ant-prefix}-action-down {
+ color: @table-header-icon-color;
+ }
+ }
+
+ &-down {
+ cursor: pointer;
+ padding: 0;
+ display: inline-block;
+ line-height: 1;
+ &:hover .@{iconfont-css-prefix}-down {
+ color: #666;
+ }
+ }
+ }
+
+ &-row {
+ &-expand-icon {
+ cursor: pointer;
+ display: inline-block;
+ width: 17px;
+ height: 17px;
+ text-align: center;
+ line-height: 14px;
+ border: @border-width-base @border-style-base @border-color-split;
+ user-select: none;
+ background: @component-background;
+ }
+
+ &-expanded:after {
+ content: '-';
+ }
+
+ &-collapsed:after {
+ content: '+';
+ }
+
+ &-spaced {
+ visibility: hidden;
+ &:after {
+ content: '.';
+ }
+ }
+
+ &[class*="@{table-prefix-cls}-row-level-0"] .@{table-prefix-cls}-selection-column > span {
+ display: inline-block;
+ }
+ }
+
+ tr&-expanded-row {
+ &,
+ &:hover {
+ background: #fbfbfb;
+ }
+ }
+
+ .@{table-prefix-cls}-row-indent + .@{table-prefix-cls}-row-expand-icon {
+ margin-right: 8px;
+ }
+
+ &-scroll {
+ overflow: auto;
+ overflow-x: hidden;
+ table {
+ width: auto;
+ min-width: 100%;
+ }
+ }
+
+ &-body-inner {
+ height: 100%;
+ }
+
+ &-fixed-header > &-content > &-scroll > &-body {
+ position: relative;
+ background: @component-background;
+ }
+
+ &-fixed-header &-body-inner {
+ overflow: scroll;
+ }
+
+ &-fixed-header &-scroll &-header {
+ overflow: scroll;
+ padding-bottom: 20px;
+ margin-bottom: -20px;
+ }
+
+ &-fixed-left,
+ &-fixed-right {
+ position: absolute;
+ top: 0;
+ overflow: hidden;
+ transition: box-shadow .3s ease;
+ border-radius: 0;
+ table {
+ width: auto;
+ background: @component-background;
+ }
+ }
+
+ &-fixed-header &-fixed-left &-body-outer &-fixed,
+ &-fixed-header &-fixed-right &-body-outer &-fixed {
+ border-radius: 0;
+ }
+
+ &-fixed-left {
+ left: 0;
+ box-shadow: 6px 0 6px -4px @shadow-color;
+ .@{table-prefix-cls}-header {
+ overflow-y: hidden;
+ }
+ // hide scrollbar in left fixed columns
+ .@{table-prefix-cls}-body-inner {
+ margin-right: -20px;
+ padding-right: 20px;
+ }
+ .@{table-prefix-cls}-fixed-header & .@{table-prefix-cls}-body-inner {
+ padding-right: 0;
+ }
+ &,
+ table {
+ border-radius: @border-radius-base 0 0 0;
+ }
+ }
+
+ &-fixed-right {
+ right: 0;
+ box-shadow: -6px 0 6px -4px @shadow-color;
+ &,
+ table {
+ border-radius: 0 @border-radius-base 0 0;
+ }
+ // hide expand row content in right-fixed Table
+ // https://github.com/ant-design/ant-design/issues/1898
+ .@{table-prefix-cls}-expanded-row {
+ color: transparent;
+ pointer-events: none;
+ }
+ }
+
+ &&-scroll-position-left &-fixed-left {
+ box-shadow: none;
+ }
+
+ &&-scroll-position-right &-fixed-right {
+ box-shadow: none;
+ }
+}
+
+@import './size';
diff --git a/components/table/style/size.less b/components/table/style/size.less
new file mode 100644
index 000000000..5bd19a5c7
--- /dev/null
+++ b/components/table/style/size.less
@@ -0,0 +1,109 @@
+.@{table-prefix-cls}-middle {
+ > .@{table-prefix-cls}-title,
+ > .@{table-prefix-cls}-footer {
+ padding: @table-padding-vertical*3/4 @table-padding-horizontal/2;
+ }
+ > .@{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: @table-padding-vertical*3/4 @table-padding-horizontal/2;
+ }
+ }
+ }
+}
+
+.@{table-prefix-cls}-small {
+ border: @border-width-base @border-style-base @border-color-split;
+ border-radius: @border-radius-base;
+
+ > .@{table-prefix-cls}-title,
+ > .@{table-prefix-cls}-footer {
+ padding: @table-padding-vertical/2 @table-padding-horizontal/2;
+ }
+
+ > .@{table-prefix-cls}-title {
+ border-bottom: @border-width-base @border-style-base @border-color-split;
+ top: 0;
+ }
+
+ > .@{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 {
+ border: 0;
+ padding: 0 @table-padding-horizontal/2;
+ > .@{table-prefix-cls}-thead > tr > th,
+ > .@{table-prefix-cls}-tbody > tr > td {
+ padding: @table-padding-vertical/2 @table-padding-horizontal/2;
+ }
+ > .@{table-prefix-cls}-thead > tr > th {
+ background: @component-background;
+ border-bottom: @border-width-base @border-style-base @border-color-split;
+ }
+ }
+
+ > .@{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 {
+ padding: 0;
+ }
+
+ .@{table-prefix-cls}-header {
+ background: @component-background;
+ }
+
+ .@{table-prefix-cls}-placeholder,
+ .@{table-prefix-cls}-row:last-child td {
+ border-bottom: 0;
+ }
+ }
+
+ &.@{table-prefix-cls}-bordered {
+ border-right: 0;
+
+ .@{table-prefix-cls}-title {
+ border: 0;
+ border-bottom: @border-width-base @border-style-base @border-color-split;
+ border-right: @border-width-base @border-style-base @border-color-split;
+ }
+
+ .@{table-prefix-cls}-content {
+ border-right: @border-width-base @border-style-base @border-color-split;
+ }
+
+ .@{table-prefix-cls}-footer {
+ border: 0;
+ border-top: @border-width-base @border-style-base @border-color-split;
+ border-right: @border-width-base @border-style-base @border-color-split;
+ &:before {
+ display: none;
+ }
+ }
+
+ .@{table-prefix-cls}-placeholder {
+ border-left: 0;
+ border-bottom: 0;
+ }
+
+ .@{table-prefix-cls}-thead > tr > th:last-child,
+ .@{table-prefix-cls}-tbody > tr > td:last-child {
+ border-right: none;
+ }
+ }
+}
diff --git a/components/table/util.js b/components/table/util.js
new file mode 100644
index 000000000..4251f800d
--- /dev/null
+++ b/components/table/util.js
@@ -0,0 +1,66 @@
+
+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
+}
+
+export function treeMap (tree, mapper, childrenName = 'children') {
+ return tree.map((node, index) => {
+ const extra = {}
+ if (node[childrenName]) {
+ extra[childrenName] = treeMap(node[childrenName], mapper, childrenName)
+ }
+ return {
+ ...mapper(node, index),
+ ...extra,
+ }
+ })
+}
+
+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
+// }