src/components/widgets/Table/Table.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import _, { isEmpty, isEqual, pick } from 'lodash';
import { pure } from 'recompose';
import { HotKeys } from 'react-hotkeys';
import cx from 'classnames';
import propsResolver from '../../../utils/propsResolver';
import TableHeader from './TableHeader';
import TableBody from './TableBody';
import TableRow from './TableRow';
import TableCell from './TableCell';
import { widgetSetSort } from '../../../actions/widgets';
import TextTableHeader from './headers/TextTableHeader';
import TextCell from './cells/TextCell/TextCell';
import SecurityCheck from '../../../core/auth/SecurityCheck';
import withColumn from './withColumn';
export const getIndex = (datasource, selectedId) => {
const index = _.findIndex(datasource, model => model.id == selectedId);
return index >= 0 ? index : 0;
};
const ReduxCell = withColumn(TableCell);
/**
* Компонент таблицы.
* Отображает данные в виде таблицы с возможностью задать различные компоненты для колонок.
* Сами данные ожидает в виде массива объектов.
* Можно задавать заголовки колонок разными компонентами.
* @reactProps {string} className - имя css класса
* @reactProps {string} colorFieldId
* @reactProps {object} style - имя css класса
* @reactProps {boolean} hasSelect - становится ли строка активной при фокусе или нет
* @reactProps {boolean} hasFocus
* @reactProps {boolean} autoFocus - селект при фокусе
* @reactProps {array} cells - массив из объектов ячеек
* @reactProps {array} headers - массив из объектов ячеек-хэдеров
* @reactProps {object} sorting
* @reactProps {function} onSort
* @reactProps {array} datasource - данные
* @reactProps {function} onResolve - резолв модели в редакс
* @reactProps {node} children - элемент потомок компонента Table
* @reactProps {function} onFocus - событие фокуса
* @reactProps {object} redux
* @reactProps {object} resolveModel
* @reactProps {string} widgetId - идентификатор виджета
* @reactProps {boolean} isActive
* @example
* const headers = [
* {
* id: "id",
* label: "ID",
* sortable: false,
* component: FilteredHeader
* },
* {
* id: "name",
* label: "Имя",
* sortable: true,
* component: TextHeader
* },
* {
* id: "vip",
* sortable: false,
* component: IconHeader,
* componentProps: {
* icon: "plus"
* }
* },
*];
*
*const cells = [
* {
* id: "id",
* component: TextCell
* },
* {
* id: "name",
* component: TextCell
* },
* {
* id: "vip",
* component: TextCell
* }
*];
*
*const datasource = [
* {
* id: "1",
* name: "Foo",
* vip: "yes"
* },
* {
* id: "2",
* name: "Bar",
* vip: "no"
* }
*]
*
*<Table headers={headers} cells={cells} datasource={datasource} />
*/
class Table extends React.Component {
constructor(props) {
super(props);
this.rows = [];
this.state = {
focusIndex: props.autoFocus
? getIndex(props.datasource, props.selectedId)
: props.hasFocus
? 0
: -1,
selectIndex: props.hasSelect
? getIndex(props.datasource, props.selectedId)
: -1,
};
this.onKeyDown = this.onKeyDown.bind(this);
}
handleRow(id, index, noResolve) {
const {
datasource,
hasFocus,
hasSelect,
onRowClickAction,
rowClick,
} = this.props;
hasSelect && !noResolve && this.props.onResolve(_.find(datasource, { id }));
if (hasSelect && hasFocus && !rowClick) {
this.setSelectAndFocus(index, index);
} else if (hasFocus) {
this.setNewFocusIndex(index);
} else if (hasSelect && !rowClick) {
this.setNewSelectIndex(index);
}
if (!noResolve && rowClick) {
onRowClickAction();
}
}
setNewFocusIndex(index) {
this.setState({ focusIndex: index }, () => this.focusActiveRow());
}
setNewSelectIndex(index) {
this.setState({ selectIndex: index });
}
setSelectAndFocus(selectIndex, focusIndex) {
const { hasFocus } = this.props;
this.setState({ selectIndex, focusIndex }, () => {
if (hasFocus) {
this.focusActiveRow();
}
});
}
focusActiveRow() {
this.rows[this.state.focusIndex] &&
ReactDOM.findDOMNode(this.rows[this.state.focusIndex]).focus();
}
onKeyDown(e) {
const keyNm = e.key;
const {
datasource,
children,
hasFocus,
hasSelect,
autoFocus,
onResolve,
} = this.props;
const { focusIndex } = this.state;
if (keyNm === 'ArrowUp' || keyNm === 'ArrowDown') {
if (!React.Children.count(children) && hasFocus) {
let newFocusIndex =
keyNm === 'ArrowUp' ? focusIndex - 1 : focusIndex + 1;
newFocusIndex =
newFocusIndex < datasource.length && newFocusIndex >= 0
? newFocusIndex
: focusIndex;
if (hasSelect && autoFocus) {
this.setSelectAndFocus(newFocusIndex, newFocusIndex);
onResolve(datasource[newFocusIndex]);
} else {
this.setNewFocusIndex(newFocusIndex);
}
}
} else if (keyNm === ' ' && hasSelect && !autoFocus) {
onResolve(datasource[this.state.focusIndex]);
this.setNewSelectIndex(this.state.focusIndex);
}
}
componentDidUpdate(prevProps) {
const {
hasSelect,
datasource,
selectedId,
isAnyTableFocused,
isActive,
} = this.props;
if (hasSelect && !isEqual(datasource, prevProps.datasource)) {
const id = getIndex(datasource, selectedId);
isAnyTableFocused && !isActive
? this.setNewSelectIndex(id)
: this.setSelectAndFocus(id, id);
}
}
componentDidMount() {
const { isAnyTableFocused, isActive, focusIndex, selectIndex } = this.state;
!isAnyTableFocused &&
isActive &&
this.setSelectAndFocus(selectIndex, focusIndex);
}
renderCell(props) {
const { redux } = this.props;
const styleProps = pick(props, ['width']);
if (redux) {
return <ReduxCell style={styleProps} {...props} />;
}
return <TableCell style={styleProps} {...props} />;
}
render() {
const {
datasource,
actions,
headers,
cells,
sorting,
onSort,
onFocus,
onResolve,
children,
hasFocus,
rowColor,
widgetId,
isActive,
rowClick,
} = this.props;
if (React.Children.count(children)) {
return (
<div className="table-responsive">
<table className="table table-sm table-hover">{children}</table>
</div>
);
}
return (
<HotKeys
keyMap={{ events: ['up', 'down', 'space'] }}
handlers={{ events: this.onKeyDown }}
>
<div className="table-responsive">
<table
className={cx('n2o-table table table-sm table-hover', {
'has-focus': hasFocus,
})}
ref={table => (this.table = table)}
onFocus={!isActive ? onFocus : undefined}
>
{headers && (
<TableHeader>
<TableRow>
{headers.map(header => {
return this.renderCell({
key: header.id,
columnId: header.id,
widgetId,
as: 'th',
sorting: sorting[header.id],
onSort: onSort,
...header,
});
})}
</TableRow>
</TableHeader>
)}
<TableBody>
{datasource && datasource.length ? (
datasource.map((data, index) => (
<TableRow
onClick={
isActive
? () => this.handleRow(data.id, index)
: undefined
}
onFocus={
!isActive
? () => this.handleRow(data.id, index, true)
: undefined
}
key={index}
color={rowColor && propsResolver(rowColor, data)}
ref={row => {
this.rows[index] = row;
}}
model={data}
className={cx({
'table-active': index === this.state.selectIndex,
'row-click': !!rowClick,
})}
tabIndex={1}
>
{cells.map(cell => {
return this.renderCell({
index,
key: cell.id,
widgetId,
columnId: cell.id,
model: data,
...cell,
});
})}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={headers && headers.length}
style={{ textAlign: 'center' }}
>
<span className="text-muted">
<FormattedMessage
id="table.notFound"
defaultMessage="Нет данных для отображения"
/>
</span>
</TableCell>
</TableRow>
)}
</TableBody>
</table>
</div>
</HotKeys>
);
}
}
Table.propTypes = {
/* Default props */
className: PropTypes.string,
colorFieldId: PropTypes.string,
style: PropTypes.string,
children: PropTypes.node,
widgetId: PropTypes.string,
isActive: PropTypes.bool,
/* Specific props */
hasFocus: PropTypes.bool,
hasSelect: PropTypes.bool,
autoFocus: PropTypes.bool,
headers: PropTypes.array,
cells: PropTypes.array,
sorting: PropTypes.object,
onSort: PropTypes.func,
redux: PropTypes.object,
/* Logic props */
datasource: PropTypes.array,
resolveModel: PropTypes.object,
onResolve: PropTypes.func,
onFocus: PropTypes.func,
onRowClickAction: PropTypes.func,
rowClick: PropTypes.object,
};
Table.defaultProps = {
sorting: {},
onResolve: () => {},
redux: true,
onRowClickAction: () => {},
};
Table.Header = TableHeader;
Table.Body = TableBody;
Table.Row = TableRow;
Table.Cell = TableCell;
//Table = pure(Table);
export default Table;