zent
Version:
一套前端设计语言和基于React的实现
551 lines (481 loc) • 14.7 kB
JavaScript
import React, { PureComponent, Component } from 'react';
import PropTypes from 'prop-types';
import Loading from 'loading';
import classnames from 'classnames';
import has from 'lodash/has';
import get from 'lodash/get';
import every from 'lodash/every';
import assign from 'lodash/assign';
import debounce from 'lodash/debounce';
import isEqual from 'lodash/isEqual';
import indexOf from 'lodash/indexOf';
import forEach from 'lodash/forEach';
import noop from 'lodash/noop';
import size from 'lodash/size';
import some from 'lodash/some';
import isFunction from 'lodash/isFunction';
import filter from 'lodash/filter';
import cloneDeep from 'lodash/cloneDeep';
import includes from 'lodash/includes';
import WindowResizeHandler from 'utils/component/WindowResizeHandler';
import Store from './Store';
import ColGroup from './ColGroup';
import Header from './Header';
import Body from './Body';
import Footer from './Footer';
import SelectionCheckbox from './SelectionCheckbox';
import SelectionCheckboxAll from './SelectionCheckboxAll';
function stopPropagation(e) {
e.stopPropagation();
if (e.nativeEvent.stopImmediatePropagation) {
e.nativeEvent.stopImmediatePropagation();
}
}
class Grid extends (PureComponent || Component) {
constructor(props) {
super(props);
this.checkboxPropsCache = {};
this.store = new Store(props);
this.store.setState({
columns: this.getColumns(props, props.columns),
selectedRowKeys: get(props, 'selection.selectedRowKeys')
});
this.setScrollPosition('left');
this.state = {
fixedColumnsBodyRowsHeight: []
};
}
syncFixedTableRowHeight = () => {
const tableRect = this.tableNode.getBoundingClientRect();
if (tableRect.height !== undefined && tableRect.height <= 0) {
return;
}
const { prefix } = this.props;
const bodyRows =
this.bodyTable.querySelectorAll(`tbody .${prefix}-grid-tr`) || [];
const fixedColumnsBodyRowsHeight = [].map.call(
bodyRows,
row => row.getBoundingClientRect().height || 'auto'
);
if (
isEqual(this.state.fixedColumnsBodyRowsHeight, fixedColumnsBodyRowsHeight)
) {
return;
}
this.setState({
fixedColumnsBodyRowsHeight
});
};
onChange = conf => {
const params = assign({}, this.store.getState('conf'), conf);
this.store.setState('conf', params);
this.props.onChange(params);
};
getDataKey = (data, rowIndex) => {
const { rowKey } = this.props;
return rowKey ? get(data, rowKey) : rowIndex;
};
isAnyColumnsFixed = () => {
return this.store.getState('isAnyColumnsFixed', () =>
some(this.store.getState('columns'), column => !!column.fixed)
);
};
isAnyColumnsLeftFixed = () => {
return this.store.getState('isAnyColumnsLeftFixed', () =>
some(
this.store.getState('columns'),
column => column.fixed === 'left' || column.fixed === true
)
);
};
isAnyColumnsRightFixed = () => {
return this.store.getState('isAnyColumnsRightFixed', () =>
some(this.store.getState('columns'), column => column.fixed === 'right')
);
};
getLeftColumns = () => {
return filter(
this.store.getState('columns'),
column => column.fixed === 'left' || column.fixed === true
);
};
getRightColumns = () => {
return filter(
this.store.getState('columns'),
column => column.fixed === 'right'
);
};
getColumns = (props, columns) => {
let { selection, datasets } = props || this.props;
columns = cloneDeep(columns || this.store.getState('columns'));
if (selection) {
const data = filter(datasets, (item, index) => {
const rowIndex = this.getDataKey(item, index);
if (selection.getCheckboxProps) {
return !get(this.getCheckboxPropsByItem(item, rowIndex), 'disabled');
}
return true;
});
const selectionColumn = {
key: 'selection-column',
width: '20px',
bodyRender: this.renderSelectionCheckbox(selection.type)
};
const checkboxAllDisabled = every(data, (item, index) => {
const rowIndex = this.getDataKey(item, index);
return get(this.getCheckboxPropsByItem(item, rowIndex), 'disabled');
});
selectionColumn.title = (
<SelectionCheckboxAll
store={this.store}
datasets={data}
getDataKey={this.getDataKey}
onSelect={this.handleBatchSelect}
disabled={checkboxAllDisabled}
/>
);
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;
};
getLeftFixedTable = () => {
return this.getTable({
columns: this.getLeftColumns(),
fixed: 'left'
});
};
getRightFixedTable = () => {
return this.getTable({
columns: this.getRightColumns(),
fixed: 'right'
});
};
setScrollPosition(position) {
this.scrollPosition = position;
if (this.tableNode) {
const { prefix } = this.props;
if (position === 'both') {
this.tableNode.className = this.tableNode.className.replace(
new RegExp(`${prefix}-grid-scroll-position-.+$`, 'gi'),
' '
);
this.tableNode.classList.add(`${prefix}-grid-scroll-position-left`);
this.tableNode.classList.add(`${prefix}-grid-scroll-position-right`);
} else {
this.tableNode.className = this.tableNode.className.replace(
new RegExp(`${prefix}-grid-scroll-position-.+$`, 'gi'),
' '
);
this.tableNode.classList.add(
`${prefix}-grid-scroll-position-${position}`
);
}
}
}
handleBodyScroll = e => {
if (e.currentTarget !== e.target) {
return;
}
const target = e.target;
const { scroll = {} } = this.props;
if (target.scrollLeft !== this.lastScrollLeft && scroll.x) {
const node = target || this.bodyTable;
const scrollToLeft = node.scrollLeft === 0;
const scrollToRight =
node.scrollLeft + 1 >=
node.children[0].getBoundingClientRect().width -
node.getBoundingClientRect().width;
if (scrollToLeft && scrollToRight) {
this.setScrollPosition('both');
} else if (scrollToLeft) {
this.setScrollPosition('left');
} else if (scrollToRight) {
this.setScrollPosition('right');
} else if (this.scrollPosition !== 'middle') {
this.setScrollPosition('middle');
}
}
this.lastScrollLeft = target.scrollLeft;
};
getTable = (options = {}) => {
const {
prefix,
datasets,
scroll,
sortType,
sortBy,
rowClassName,
onRowClick,
ellipsis
} = this.props;
const { fixed } = options;
const columns = options.columns || this.store.getState('columns');
let tableClassName = '';
let bodyStyle = {};
let tableStyle = {};
if (fixed || scroll.x) {
tableClassName = `${prefix}-grid-fixed`;
bodyStyle.overflowX = 'auto';
}
if (!fixed && scroll.x) {
tableStyle.width = scroll.x;
}
return (
<div
style={bodyStyle}
ref={ref => {
if (!fixed) this.bodyTable = ref;
}}
onScroll={this.handleBodyScroll}
key="table"
>
<table
className={classnames(`${prefix}-grid-table`, tableClassName, {
[`${prefix}-grid-table-ellipsis`]: ellipsis
})}
style={tableStyle}
>
<ColGroup columns={columns} />
<Header
prefix={prefix}
columns={columns}
store={this.store}
onChange={this.onChange}
sortType={sortType}
sortBy={sortBy}
/>
<Body
prefix={prefix}
columns={columns}
datasets={datasets}
rowClassName={rowClassName}
onRowClick={onRowClick}
fixed={fixed}
fixedColumnsBodyRowsHeight={this.state.fixedColumnsBodyRowsHeight}
/>
</table>
</div>
);
};
getEmpty = () => {
const { datasets, prefix, emptyLabel } = this.props;
if (size(datasets) === 0) {
return (
<div className={`${prefix}-grid-empty`} key="empty">
{emptyLabel}
</div>
);
}
return null;
};
getCheckboxPropsByItem = (data, rowIndex) => {
const { selection } = this.props;
if (!get(selection, 'getCheckboxProps')) {
return {};
}
if (!this.checkboxPropsCache[rowIndex]) {
this.checkboxPropsCache[rowIndex] = selection.getCheckboxProps(data);
}
return this.checkboxPropsCache[rowIndex];
};
onSelectChange = (selectedRowKeys, data) => {
const { datasets, selection } = this.props;
const onSelect = get(selection, 'onSelect');
if (isFunction(onSelect)) {
const selectedRows = filter(datasets, (row, i) =>
includes(selectedRowKeys, this.getDataKey(row, i))
);
onSelect(selectedRowKeys, selectedRows, data);
}
};
handleSelect = (data, rowIndex, e) => {
const checked = e.target.checked;
let selectedRowKeys = this.store.getState('selectedRowKeys');
if (checked) {
selectedRowKeys.push(rowIndex);
} else {
selectedRowKeys = filter(selectedRowKeys, i => rowIndex !== i);
}
this.store.setState({ selectedRowKeys });
this.onSelectChange(selectedRowKeys, data);
};
handleBatchSelect = (type, data) => {
let selectedRowKeys = cloneDeep(this.store.getState('selectedRowKeys'));
let changeRowKeys = [];
switch (type) {
case 'selectAll':
forEach(data, (key, index) => {
const rowIndex = this.getDataKey(key, index);
if (!includes(selectedRowKeys, rowIndex)) {
selectedRowKeys.push(rowIndex);
changeRowKeys.push(rowIndex);
}
});
break;
case 'removeAll':
forEach(data, (key, index) => {
const rowIndex = this.getDataKey(key, index);
if (includes(selectedRowKeys, rowIndex)) {
selectedRowKeys.splice(indexOf(selectedRowKeys, rowIndex), 1);
changeRowKeys.push(key);
}
});
break;
default:
break;
}
this.store.setState({ selectedRowKeys });
const changeRow = filter(data, (row, i) =>
includes(changeRowKeys, this.getDataKey(row, i))
);
this.onSelectChange(selectedRowKeys, changeRow);
};
renderSelectionCheckbox = () => {
return (data, { row }) => {
const rowIndex = this.getDataKey(data, row);
const props = this.getCheckboxPropsByItem(data, rowIndex);
return (
<span onClick={stopPropagation}>
<SelectionCheckbox
disabled={props.disabled}
rowIndex={rowIndex}
store={this.store}
onChange={e =>
this.handleSelect(data, this.getDataKey(data, row), e)}
/>
</span>
);
};
};
componentDidMount() {
if (this.isAnyColumnsFixed()) {
this.syncFixedTableRowHeight();
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.selection && has(nextProps.selection, 'selectedRowKeys')) {
this.store.setState({
selectedRowKeys: nextProps.selection.selectedRowKeys || [],
columns: this.getColumns(nextProps)
});
const { selection } = this.props;
if (
selection &&
nextProps.selection.getCheckboxProps !== selection.getCheckboxProps
) {
this.CheckboxPropsCache = {};
}
}
if (nextProps.columns && nextProps.columns !== this.props.columns) {
this.store.setState({
columns: this.getColumns(nextProps, nextProps.columns)
});
}
if (
has(nextProps, 'datasets') &&
nextProps.datasets !== this.props.datasets
) {
this.CheckboxPropsCache = {};
}
}
componentDidUpdate() {
if (this.isAnyColumnsFixed()) {
this.syncFixedTableRowHeight();
}
}
render() {
const { prefix, loading, pageInfo } = this.props;
let className = `${prefix}-grid`;
className = classnames(className, this.props.className);
if (this.scrollPosition === 'both') {
className = classnames(
className,
`${prefix}-grid-scroll-position-left`,
`${prefix}-grid-scroll-position-right`
);
} else {
className = classnames(
className,
`${prefix}-grid-scroll-position-left`,
`${prefix}-grid-scroll-position-${this.scrollPosition}`
);
}
const content = [
this.getTable(),
this.getEmpty(),
<Footer
key="footer"
prefix={prefix}
pageInfo={pageInfo}
onChange={this.onChange}
/>
];
const scrollTable = this.isAnyColumnsFixed() ? (
<div className={`${prefix}-grid-scroll`}>{content}</div>
) : (
content
);
return (
<div className={className} ref={node => (this.tableNode = node)}>
<Loading show={loading}>
{scrollTable}
{this.isAnyColumnsLeftFixed() && (
<div className={`${prefix}-grid-fixed-left`}>
{this.getLeftFixedTable()}
</div>
)}
{this.isAnyColumnsRightFixed() && (
<div className={`${prefix}-grid-fixed-right`}>
{this.getRightFixedTable()}
</div>
)}
</Loading>
<WindowResizeHandler
onResize={debounce(this.syncFixedTableRowHeight, 500)}
/>
</div>
);
}
}
Grid.propTypes = {
className: PropTypes.string,
rowClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
prefix: PropTypes.string,
datasets: PropTypes.array,
columns: PropTypes.array,
loading: PropTypes.bool,
pageInfo: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
onChange: PropTypes.func,
selection: PropTypes.object,
rowKey: PropTypes.string,
scroll: PropTypes.object,
sortBy: PropTypes.string,
sortType: PropTypes.string,
onRowClick: PropTypes.func,
ellipsis: PropTypes.bool
};
Grid.defaultProps = {
className: '',
prefix: 'zent',
datasets: [],
columns: [],
loading: false,
pageInfo: false,
onChange: noop,
selection: null,
rowKey: 'id',
emptyLabel: '没有更多数据了',
scroll: {},
onRowClick: noop,
ellipsis: false
};
export default Grid;