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 = ( + + {this.renderMenus(newSelections)} + + ); + + 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 ( + + {input} + {item.text} + + ); + } + + 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} + + ) : ( + + + {this.renderMenus(column.filters!)} + + + + ); + + 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 +// }