ant-design/components/table/Table.tsx
2016-08-16 17:54:59 +08:00

866 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import * as React from 'react';
import RcTable from 'rc-table';
import Checkbox from '../checkbox';
import Radio from '../radio';
import FilterDropdown from './filterDropdown';
import Pagination from '../pagination';
import Icon from '../icon';
import Spin from '../spin';
import classNames from 'classnames';
import { flatArray } from './util';
import assign from 'object-assign';
import splitObject from '../_util/splitObject';
function noop() {
}
function stopPropagation(e) {
e.stopPropagation();
if (e.nativeEvent.stopImmediatePropagation) {
e.nativeEvent.stopImmediatePropagation();
}
}
const defaultLocale = {
filterTitle: '筛选',
filterConfirm: '确定',
filterReset: '重置',
emptyText: <span><Icon type="frown" /></span>,
};
const defaultPagination = {
pageSize: 10,
onChange: noop,
onShowSizeChange: noop,
};
interface TableRowSelection {
type?: 'checkbox' | 'radio';
selectedRowKeys?: string[];
onChange?: (selectedRowKeys: string[], selectedRows: Object[]) => any;
getCheckboxProps?: (record: Object) => Object;
onSelect?: (record: Object, selected: boolean, selectedRows: Object[]) => any;
onSelectAll?: (selected: boolean, selectedRows: Object[], changeRows: Object[]) => any;
}
interface TableColumnConfig {
title?: React.ReactNode;
key?: string;
dataIndex?: string;
render?: (text: any, record: Object, index: number) => React.ReactNode;
filters?: string[];
onFilter?: (value: any, record: Object) => boolean;
filterMultiple?: boolean;
filterDropdown?: React.ReactNode;
sorter?: boolean | ((a: any, b: any) => number);
colSpan?: number;
width?: string | number;
className?: string;
fixed?: boolean | ('left' | 'right');
filteredValue?: any[];
sortOrder?: boolean | ('ascend' | 'descend');
}
export interface TableProps {
prefixCls?: string;
rowSelection?: TableRowSelection;
pagination?: any; // 等 Pagination 的 interface以便直接引用
size?: 'default' | 'small';
dataSource?: Object[];
columns?: TableColumnConfig[];
rowKey?: string | ((record: Object, index: number) => string);
rowClassName?: (record: Object, index: number) => string;
expandedRowRender?: any;
defaultExpandedRowKeys?: string[];
expandedRowKeys?: string[];
expandIconAsCell?: boolean;
onChange?: (pagination: any, filters: string[], sorter: Object) => any;
loading?: boolean;
locale?: Object;
indentSize?: number;
onRowClick?: (record: Object, index: number) => any;
useFixedHeader?: boolean;
bordered?: boolean;
showHeader?: boolean;
footer?: (currentPageData: Object[]) => React.ReactNode;
title?: (currentPageData: Object[]) => React.ReactNode;
scroll?: { x?: boolean | number, y?: boolean | number};
}
interface TableContext {
antLocale?: {
Table?: any,
};
}
export default class Table extends React.Component<TableProps, any> {
static propTypes = {
dataSource: React.PropTypes.array,
prefixCls: React.PropTypes.string,
useFixedHeader: React.PropTypes.bool,
rowSelection: React.PropTypes.object,
className: React.PropTypes.string,
size: React.PropTypes.string,
loading: React.PropTypes.bool,
bordered: React.PropTypes.bool,
onChange: React.PropTypes.func,
locale: React.PropTypes.object,
};
static defaultProps = {
dataSource: [],
prefixCls: 'ant-table',
useFixedHeader: false,
rowSelection: null,
className: '',
size: 'large',
loading: false,
bordered: false,
indentSize: 20,
onChange: noop,
locale: {},
rowKey: 'key',
};
static contextTypes = {
antLocale: React.PropTypes.object,
};
context: TableContext;
CheckboxPropsCache: Object;
constructor(props) {
super(props);
const pagination = props.pagination || {};
this.state = assign({}, this.getSortStateFromColumns(), {
// 减少状态
selectedRowKeys: (props.rowSelection || {}).selectedRowKeys || [],
filters: this.getFiltersFromColumns(),
selectionDirty: false,
pagination: this.hasPagination() ?
assign({}, defaultPagination, pagination, {
current: pagination.defaultCurrent || pagination.current || 1,
}) : {},
});
this.CheckboxPropsCache = {};
}
getCheckboxPropsByItem(item) {
const { rowSelection = {} } = this.props;
if (!rowSelection.getCheckboxProps) {
return {};
}
const key = this.getRecordKey(item);
// 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 => this.getCheckboxPropsByItem(item).defaultChecked)
.map((record, rowIndex) => this.getRecordKey(record, rowIndex));
}
getLocale() {
let locale = {};
if (this.context.antLocale && this.context.antLocale.Table) {
locale = this.context.antLocale.Table;
}
return assign({}, defaultLocale, locale, this.props.locale);
}
componentWillReceiveProps(nextProps) {
if (('pagination' in nextProps) && nextProps.pagination !== false) {
this.setState(previousState => {
const newPagination = assign({}, defaultPagination, previousState.pagination, nextProps.pagination);
newPagination.current = newPagination.current || 1;
return { pagination: newPagination };
});
}
// dataSource 的变化会清空选中项
if ('dataSource' in nextProps &&
nextProps.dataSource !== this.props.dataSource) {
this.setState({
selectionDirty: false,
});
this.CheckboxPropsCache = {};
}
if (nextProps.rowSelection &&
'selectedRowKeys' in nextProps.rowSelection) {
this.setState({
selectedRowKeys: nextProps.rowSelection.selectedRowKeys || [],
});
const { rowSelection } = this.props;
if (rowSelection && (
nextProps.rowSelection.getCheckboxProps !== rowSelection.getCheckboxProps
)) {
this.CheckboxPropsCache = {};
}
}
if (this.getSortOrderColumns(nextProps.columns).length > 0) {
const sortState = this.getSortStateFromColumns(nextProps.columns);
if (sortState.sortColumn !== this.state.sortColumn ||
sortState.sortOrder !== this.state.sortOrder) {
this.setState(sortState);
}
}
const filteredValueColumns = this.getFilteredValueColumns(nextProps.columns);
if (filteredValueColumns.length > 0) {
const filtersFromColumns = this.getFiltersFromColumns(nextProps.columns);
const newFilters = assign({}, this.state.filters);
Object.keys(filtersFromColumns).forEach(key => {
newFilters[key] = filtersFromColumns[key];
});
if (this.isFiltersChanged(newFilters)) {
this.setState({ filters: newFilters });
}
}
}
setSelectedRowKeys(selectedRowKeys, { selectWay, record, checked, changeRowKeys }: any) {
const { rowSelection = {} } = this.props;
if (rowSelection && !('selectedRowKeys' in rowSelection)) {
this.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);
}
}
hasPagination() {
return this.props.pagination !== false;
}
isFiltersChanged(filters) {
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?) {
return (columns || this.props.columns || []).filter(column => 'sortOrder' in column);
}
getFilteredValueColumns(columns?) {
return (columns || this.props.columns || []).filter(column => 'filteredValue' in column);
}
getFiltersFromColumns(columns?) {
let filters = {};
this.getFilteredValueColumns(columns).forEach(col => {
filters[this.getColumnKey(col)] = col.filteredValue;
});
return filters;
}
getSortStateFromColumns(columns?) {
// return fisrt column which sortOrder is not falsy
const sortedColumn =
this.getSortOrderColumns(columns).filter(col => 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, b) => {
let result = sortColumn.sorter(a, b);
if (result !== 0) {
return (sortOrder === 'descend') ? -result : result;
}
return a.indexForSort - b.indexForSort;
};
}
toggleSortOrder(order, column) {
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);
}
this.props.onChange.apply(null, this.prepareParamsArguments(assign({}, this.state, newState)));
}
handleFilter = (column, nextFilters) => {
const props = this.props;
let pagination = assign({}, this.state.pagination);
const filters = assign({}, this.state.filters, {
[this.getColumnKey(column)]: nextFilters,
});
// Remove filters not in current columns
const currentColumnKeys = props.columns.map(c => this.getColumnKey(c));
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 = {
selectionDirty: false,
pagination,
filters: null,
};
const filtersToSetState = assign({}, filters);
// Remove filters which is controlled
this.getFilteredValueColumns().forEach(col => {
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 (props.pagination && 'current' in props.pagination) {
newState.pagination = assign({}, pagination, {
current: this.state.pagination.current,
});
}
this.setState(newState, () => {
props.onChange.apply(null, this.prepareParamsArguments(assign({}, this.state, {
selectionDirty: false,
filters,
pagination,
})));
});
}
handleSelect = (record, rowIndex, e) => {
const checked = e.target.checked;
const defaultSelection = this.state.selectionDirty ? [] : this.getDefaultSelection();
let selectedRowKeys = this.state.selectedRowKeys.concat(defaultSelection);
let key = this.getRecordKey(record, rowIndex);
if (checked) {
selectedRowKeys.push(this.getRecordKey(record, rowIndex));
} else {
selectedRowKeys = selectedRowKeys.filter((i) => key !== i);
}
this.setState({
selectionDirty: true,
});
this.setSelectedRowKeys(selectedRowKeys, {
selectWay: 'onSelect',
record,
checked,
});
}
handleRadioSelect = (record, rowIndex, e) => {
const checked = e.target.checked;
const defaultSelection = this.state.selectionDirty ? [] : this.getDefaultSelection();
let selectedRowKeys = this.state.selectedRowKeys.concat(defaultSelection);
let key = this.getRecordKey(record, rowIndex);
selectedRowKeys = [key];
this.setState({
selectionDirty: true,
});
this.setSelectedRowKeys(selectedRowKeys, {
selectWay: 'onSelect',
record,
checked,
});
}
handleSelectAllRow = (e) => {
const checked = e.target.checked;
const data = this.getFlatCurrentPageData();
const defaultSelection = this.state.selectionDirty ? [] : this.getDefaultSelection();
const selectedRowKeys = this.state.selectedRowKeys.concat(defaultSelection);
const changableRowKeys = data
.filter(item => !this.getCheckboxPropsByItem(item).disabled)
.map((item, i) => this.getRecordKey(item, i));
// 记录变化的列
const changeRowKeys = [];
if (checked) {
changableRowKeys.forEach(key => {
if (selectedRowKeys.indexOf(key) < 0) {
selectedRowKeys.push(key);
changeRowKeys.push(key);
}
});
} else {
changableRowKeys.forEach(key => {
if (selectedRowKeys.indexOf(key) >= 0) {
selectedRowKeys.splice(selectedRowKeys.indexOf(key), 1);
changeRowKeys.push(key);
}
});
}
this.setState({
selectionDirty: true,
});
this.setSelectedRowKeys(selectedRowKeys, {
selectWay: 'onSelectAll',
checked,
changeRowKeys,
});
}
handlePageChange = (current) => {
const props = this.props;
let pagination = assign({}, this.state.pagination);
if (current) {
pagination.current = current;
} else {
pagination.current = pagination.current || 1;
}
pagination.onChange(pagination.current);
const newState = {
selectionDirty: false,
pagination,
};
// Controlled current prop will not respond user interaction
if (props.pagination && 'current' in props.pagination) {
newState.pagination = assign({}, pagination, {
current: this.state.pagination.current,
});
}
this.setState(newState);
this.props.onChange.apply(null, this.prepareParamsArguments(assign({}, this.state, {
selectionDirty: false,
pagination,
})));
}
renderSelectionRadio = (value, record, index) => {
let rowIndex = this.getRecordKey(record, index); // 从 1 开始
const props = this.getCheckboxPropsByItem(record);
let checked;
if (this.state.selectionDirty) {
checked = this.state.selectedRowKeys.indexOf(rowIndex) >= 0;
} else {
checked = (this.state.selectedRowKeys.indexOf(rowIndex) >= 0 ||
this.getDefaultSelection().indexOf(rowIndex) >= 0);
}
return (
<span onClick={stopPropagation}>
<Radio disabled={props.disabled}
onChange={(e) => this.handleRadioSelect(record, rowIndex, e)}
value={rowIndex} checked={checked}
/>
</span>
);
}
renderSelectionCheckBox = (value, record, index) => {
let rowIndex = this.getRecordKey(record, index); // 从 1 开始
let checked;
if (this.state.selectionDirty) {
checked = this.state.selectedRowKeys.indexOf(rowIndex) >= 0;
} else {
checked = (this.state.selectedRowKeys.indexOf(rowIndex) >= 0 ||
this.getDefaultSelection().indexOf(rowIndex) >= 0);
}
const props = this.getCheckboxPropsByItem(record);
return (
<span onClick={stopPropagation}>
<Checkbox
checked={checked}
disabled={props.disabled}
onChange={(e) => this.handleSelect(record, rowIndex, e)}
/>
</span>
);
}
getRecordKey(record, index?) {
const rowKey = this.props.rowKey;
if (typeof rowKey === 'function') {
return rowKey(record, index);
}
return record[rowKey as string] || index;
}
renderRowSelection() {
const columns = this.props.columns.concat();
if (this.props.rowSelection) {
const data = this.getFlatCurrentPageData().filter((item) => {
if (this.props.rowSelection.getCheckboxProps) {
return !this.getCheckboxPropsByItem(item).disabled;
}
return true;
});
let checked;
if (!data.length) {
checked = false;
} else {
checked = this.state.selectionDirty
? data.every((item, i) =>
this.state.selectedRowKeys.indexOf(this.getRecordKey(item, i)) >= 0)
: (
data.every((item, i) =>
this.state.selectedRowKeys.indexOf(this.getRecordKey(item, i)) >= 0) ||
data.every(item => this.getCheckboxPropsByItem(item).defaultChecked)
);
}
let selectionColumn;
if (this.props.rowSelection.type === 'radio') {
selectionColumn = {
key: 'selection-column',
render: this.renderSelectionRadio,
className: 'ant-table-selection-column',
};
} else {
const checkboxAllDisabled = data.every(item => this.getCheckboxPropsByItem(item).disabled);
const checkboxAll = (
<Checkbox checked={checked}
disabled={checkboxAllDisabled}
onChange={this.handleSelectAllRow}
/>
);
selectionColumn = {
key: 'selection-column',
title: checkboxAll,
render: this.renderSelectionCheckBox,
className: 'ant-table-selection-column',
};
}
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, index?) {
return column.key || column.dataIndex || index;
}
getMaxCurrent(total) {
const { current, pageSize } = this.state.pagination;
if ((current - 1) * pageSize >= total) {
return current - 1;
}
return current;
}
isSortColumn(column) {
const { sortColumn } = this.state;
if (!column || !sortColumn) {
return false;
}
return this.getColumnKey(sortColumn) === this.getColumnKey(column);
}
renderColumnsDropdown(columns) {
const { sortOrder } = this.state;
const locale = this.getLocale();
return columns.map((originColumn, i) => {
let column = assign({}, originColumn);
let key = this.getColumnKey(column, i);
let filterDropdown;
let sortButton;
if ((column.filters && column.filters.length > 0) || column.filterDropdown) {
let colFilters = this.state.filters[key] || [];
filterDropdown = (
<FilterDropdown
locale={locale}
column={column}
selectedKeys={colFilters}
confirmFilter={this.handleFilter}
/>
);
}
if (column.sorter) {
let isSortColumn = this.isSortColumn(column);
if (isSortColumn) {
column.className = column.className || '';
if (sortOrder) {
column.className += ' ant-table-column-sort';
}
}
const isAscend = isSortColumn && sortOrder === 'ascend';
const isDescend = isSortColumn && sortOrder === 'descend';
sortButton = (
<div className="ant-table-column-sorter">
<span className={`ant-table-column-sorter-up ${isAscend ? 'on' : 'off'}`}
title="↑"
onClick={() => this.toggleSortOrder('ascend', column)}
>
<Icon type="caret-up" />
</span>
<span className={`ant-table-column-sorter-down ${isDescend ? 'on' : 'off'}`}
title="↓"
onClick={() => this.toggleSortOrder('descend', column)}
>
<Icon type="caret-down" />
</span>
</div>
);
}
column.title = (
<span>
{column.title}
{sortButton}
{filterDropdown}
</span>
);
return column;
});
}
handleShowSizeChange = (current, pageSize) => {
const pagination = this.state.pagination;
pagination.onShowSizeChange(current, pageSize);
const nextPagination = assign({}, pagination, { pageSize, current });
this.setState({ pagination: nextPagination });
this.props.onChange.apply(null, this.prepareParamsArguments(assign({}, 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 === 'middle' || this.props.size === 'small') {
size = 'small';
}
let total = pagination.total || this.getLocalData().length;
return (total > 0) ?
<Pagination
{...pagination}
className={`${this.props.prefixCls}-pagination`}
onChange={this.handlePageChange}
total={total}
size={size}
current={this.getMaxCurrent(total)}
onShowSizeChange={this.handleShowSizeChange}
/> : null;
}
prepareParamsArguments(state: any): [any, string[], Object] {
// 准备筛选、排序、分页的参数
const pagination = state.pagination;
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) {
return this.props.columns.filter(c => this.getColumnKey(c) === myKey)[0];
}
getCurrentPageData() {
let data = this.getLocalData();
let current;
let pageSize;
let state = this.state;
// 如果没有分页的话,默认全部展示
if (!this.hasPagination()) {
pageSize = Number.MAX_VALUE;
current = 1;
} else {
pageSize = state.pagination.pageSize;
current = this.getMaxCurrent(state.pagination.total || data.length);
}
// 分页
// ---
// 当数据量少于等于每页数量时,直接设置数据
// 否则进行读取分页数据
if (data.length > pageSize || pageSize === Number.MAX_VALUE) {
data = data.filter((item, i) => {
return i >= (current - 1) * pageSize && i < current * pageSize;
});
}
return data;
}
getFlatData() {
return flatArray(this.getLocalData());
}
getFlatCurrentPageData() {
return flatArray(this.getCurrentPageData());
}
getLocalData() {
const state = this.state;
let data = this.props.dataSource || [];
// 优化本地排序
data = data.slice(0);
for (let i = 0; i < data.length; i++) {
data[i] = assign({}, data[i], { indexForSort: i });
}
const sorterFn = this.getSorterFn();
if (sorterFn) {
data = data.sort(sorterFn);
}
// 筛选
if (state.filters) {
Object.keys(state.filters).forEach((columnKey) => {
let col = this.findColumn(columnKey);
if (!col) {
return;
}
let values = state.filters[columnKey] || [];
if (values.length === 0) {
return;
}
data = col.onFilter ? data.filter(record => {
return values.some(v => col.onFilter(v, record));
}) : data;
});
}
return data;
}
render() {
const [{
style, className, rowKey,
}, restProps] = splitObject(this.props, ['style', 'className', 'rowKey']);
let data = this.getCurrentPageData();
let columns = this.renderRowSelection();
const expandIconAsCell = this.props.expandedRowRender && this.props.expandIconAsCell !== false;
const locale = this.getLocale();
const classString = classNames({
[`ant-table-${this.props.size}`]: true,
'ant-table-bordered': this.props.bordered,
});
columns = this.renderColumnsDropdown(columns);
columns = columns.map((column, i) => {
const newColumn = assign({}, column);
newColumn.key = this.getColumnKey(newColumn, i);
return newColumn;
});
// Empty Data
let emptyRowKey;
if (!data || data.length === 0) {
columns.forEach((column, index) => {
column.render = () => ({
children: !index ? <div className="ant-table-placeholder">{locale.emptyText}</div> : null,
props: {
colSpan: !index ? columns.length : 0,
},
});
});
emptyRowKey = 'key';
data = [{
[emptyRowKey]: 'empty',
}];
}
let table = (
<RcTable {...restProps}
data={data}
columns={columns}
className={classString}
expandIconColumnIndex={(columns[0] && columns[0].key === 'selection-column') ? 1 : 0}
expandIconAsCell={expandIconAsCell}
rowKey={emptyRowKey || rowKey}
/>
);
// 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)
? 'ant-table-with-pagination'
: 'ant-table-without-pagination';
const spinClassName = this.props.loading ? `${paginationPatchClass} ant-table-spin-holder` : '';
table = <Spin className={spinClassName} spinning={this.props.loading}>{table}</Spin>;
return (
<div className={`${className} clearfix`} style={style}>
{table}
{this.renderPagination()}
</div>
);
}
}