├── .eslintrc
├── .gitignore
├── .npmignore
├── .prettierrc
├── .storybook
├── addons.js
└── config.js
├── .travis.yml
├── README.md
├── babel.config.js
├── package.json
├── rollup.config.js
├── src
├── lib
│ ├── Column
│ │ ├── Column.js
│ │ └── index.js
│ ├── ColumnResizer
│ │ ├── ColumnResizer.js
│ │ └── index.js
│ ├── Header
│ │ ├── Header.js
│ │ └── index.js
│ ├── Row
│ │ ├── Row.js
│ │ └── index.js
│ ├── Table
│ │ ├── Table.js
│ │ └── index.js
│ ├── index.js
│ └── style.css
└── stories
│ ├── _data.js
│ ├── autoScroll.js
│ ├── changeColumns.js
│ ├── index.js
│ ├── rowRenderer.js
│ ├── scrollTo.js
│ ├── selection.js
│ ├── simple.js
│ ├── sort.js
│ └── style.css
└── yarn.lock
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "react-app"
3 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 | .idea
23 | .cache
24 |
25 | dist
26 | demo
27 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .babelrc
2 | lib
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 4,
3 | "singleQuote": true,
4 | "printWidth": 120,
5 | "trailingComma": "all"
6 | }
7 |
--------------------------------------------------------------------------------
/.storybook/addons.js:
--------------------------------------------------------------------------------
1 | import '@storybook/addon-actions/register';
2 | import '@storybook/addon-links/register';
3 |
--------------------------------------------------------------------------------
/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import { configure } from '@storybook/react';
2 |
3 | function loadStories() {
4 | require('../src/stories');
5 | }
6 |
7 | configure(loadStories, module);
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - node
4 | before_script:
5 | - yarn
6 | script:
7 | - yarn test
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React-VT-Table
2 |
3 | Table realisation based on [react-window](https://github.com/bvaughn/react-window) library.
4 |
5 | ## Features
6 |
7 | - Efficiently rendering large tables.
8 | - Allow custom renderers for row, cell, and header.
9 | - Built-in resize columns.
10 | - Built-in auto-scrolling.
11 | - Easy to add your own sorting and selecting mechanisms (see examples).
12 | - Works with Immutable.Iterable data lists or native arrays of objects.
13 |
14 | ## Installation
15 |
16 | ```sh
17 | npm install react-vt-table
18 | ```
19 |
20 | ## Demo
21 |
22 | Here are [some live examples](https://avin.github.io/react-vt-table?selectedKind=Table).
23 |
24 | ## Examples
25 |
26 | Check out `./src/stories` folder to find some code examples.
27 |
28 | ## Styling
29 |
30 | You can use built-in CSS style:
31 |
32 | ```js
33 | import 'react-vt-table/dist/style.css';
34 | ```
35 |
36 | or create your own using existing one
37 |
38 | ## API
39 |
40 | ### `
`
41 |
42 | #### Props
43 |
44 | | Property | Type | Required? | Description |
45 | | :-------------------- | :----------------- | :-------: | :---------------------------------------------------------------------------------------------------------------------------- |
46 | | width | Number | ✓ | Table width. |
47 | | height | Number | ✓ | Table height. |
48 | | headerHeight | Number or Func | | Table header height (Default: 30). |
49 | | rowHeight | Number or Func | | Table row height (Default: 30).
Function params: `(rowIndex)`. |
50 | | data | Immutable.Iterable | ✓ | Data list for table content. |
51 | | rowClassName | Func | | Row className determine function.
Function params: `(rowIndex)`. |
52 | | rowRenderer | Func | | Personal row renderer function.
Function params: see `
` props. |
53 | | sortIndicatorRenderer | Func | | Sort indicator render function.
Function params: `({ dataKey, columnIndex })`. |
54 | | onHeaderClick | Func | | Click Mouse action on header row.
Function params: `(event, { dataKey, columnIndex })`. |
55 | | onHeaderDoubleClick | Func | | Double Click Mouse action on header row.
Function params: `(event, { dataKey, columnIndex })`. |
56 | | onHeaderMouseOver | Func | | Mouse Over action on header row.
Function params: `(event, { dataKey, columnIndex })`. |
57 | | onHeaderMouseOut | Func | | Mouse Out action on header row.
Function params: `(event, { dataKey, columnIndex })`. |
58 | | onHeaderRightClick | Func | | Right Click Mouse action on header row.
Function params: `(event, { dataKey, columnIndex })`. |
59 | | onRowClick | Func | | Click Mouse action on table row.
Function params: `(event, { dataKey, columnIndex })`. |
60 | | onRowDoubleClick | Func | | Double Click Mouse action on table row.
Function params: `(event, { dataKey, columnIndex })`. |
61 | | onRowMouseOver | Func | | Mouse Over action on table row.
Function params: `(event, { dataKey, columnIndex })`. |
62 | | onRowMouseOut | Func | | Mouse Out action on table row.
Function params: `(event, { dataKey, columnIndex })`. |
63 | | onRowMouseDown | Func | | Mouse Down action on table row.
Function params: `(event, { dataKey, columnIndex })`. |
64 | | onRowMouseUp | Func | | Mouse Up action on table row.
Function params: `(event, { dataKey, columnIndex })`. |
65 | | onRowRightClick | Func | | Right Click Mouse action on table row.
Function params: `(event, { dataKey, columnIndex })`. |
66 | | onScroll | Func | | Action on table scroll.
Function params: See [React-Window's docs](https://react-window.now.sh/#/api/FixedSizeList). |
67 | | onResizeColumn | Func | | Action on change column width.
Function params: `({ dataKey, columnIndex, resizeDiff, newWidth })`. |
68 | | overflowWidth | Number | | Width of vertical table overflow. |
69 | | minColumnWidth | Number | | Minimal column width. |
70 | | maxColumnWidth | Number | | Maximum column width. |
71 | | dynamicColumnWidth | Bool | | Dynamic width for columns that was not resized. |
72 | | listProps | Object | | Props for inner `react-window` list component. @see See [React-Windows docs](https://react-window.now.sh/#/api/FixedSizeList) |
73 | | noItemsLabel | Node | | No items in data list label. |
74 | | disableHeader | Bool | | Hide table header row. |
75 | | autoScroll | Bool | | Auto scroll to list bottom. |
76 | | className | String | | Optional custom CSS class name to attach to root container element. |
77 | | id | String | | Optional custom id to attach to root container element. |
78 |
79 | #### Methods
80 |
81 | **scrollTo(scrollOffset: number): void**
82 |
83 | **scrollToItem(index: number, align: string = "auto"): void**
84 |
85 | For more info see [React-Window's docs](https://react-window.now.sh/#/api/FixedSizeGrid)
86 |
87 | ### ``
88 |
89 | #### Props
90 |
91 | | Property | Type | Required? | Description |
92 | | :----------------------- | :----- | :-------: | :-------------------------------------------------------------------------------------------- |
93 | | cellRenderer | Func | | Content cell render function.
Function params: `({ dataKey, rowData, columnIndex })`. |
94 | | columnHeaderCellRenderer | Func | | Column header cell render function.
Function params: `({ label, dataKey, columnIndex })`. |
95 | | dataKey | String | | Field key containing data. |
96 | | width | Number | | Default column width in pixels. |
97 |
98 | ### `
`
99 |
100 | Use `Row` component only if you want to low modify your table rows. (See example `./srs/stories/rowRenderer.js`)
101 |
102 | #### Props
103 |
104 | | Property | Type | Required? | Description |
105 | | :------- | :----- | :-------: | :-------------------------------------------------------------------------------------------------------------------------------- |
106 | | index | Number | | Row number |
107 | | style | Object | | Row style |
108 | | data | Object | | Additional row data `({dataList, rowProps})` where `dataList` is table data and rowProps is additional row properties (see below) |
109 |
110 | #### Additional row properties
111 |
112 | Additional row properties are contained in row's `props.data.rowProps`
113 |
114 | | Property | Type | Required? | Description |
115 | | :------------- | :---- | :-------: | :----------------------------------------------------------------------------------- |
116 | | columns | array | | Table columns array |
117 | | rowClassName | Func | | Row className determine function.
Function params: `(rowIndex)`. |
118 | | getRowWidth | Func | | Get row actual width. |
119 | | getDataRowItem | Func | | Function to get cell value.
Function params: `({rowData, dataKey})`. |
120 | | getColumnWidth | Func | | Function to get column width.
Function params: `(columnIndex)`. |
121 | | getDataRow | Func | | Function to get row data item.
Function params: `(rowIndex)`. |
122 | | onClick | Func | | onClick row handler.
Function params: `(event, { dataKey, columnIndex })`. |
123 | | onDoubleClick | Func | | onDoubleClick row handler.
Function params: `(event, { dataKey, columnIndex })`. |
124 | | onMouseOver | Func | | onMouseOver row handler.
Function params: `(event, { dataKey, columnIndex })`. |
125 | | onMouseOut | Func | | onMouseOut row handler.
Function params: `(event, { dataKey, columnIndex })`. |
126 | | onRightClick | Func | | onRightClick row handler.
Function params: `(event, { dataKey, columnIndex })`. |
127 |
128 | ## License
129 |
130 | MIT © [avin](https://github.com/avin)
131 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [['@babel/env', { loose: true }], '@babel/flow', '@babel/preset-react'],
3 | plugins: [['@babel/proposal-class-properties', { loose: true }], 'annotate-pure-calls'],
4 | };
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-vt-table",
3 | "version": "0.8.4",
4 | "main": "dist/index.cjs.js",
5 | "module": "dist/index.esm.js",
6 | "files": [
7 | "dist",
8 | "README.md"
9 | ],
10 | "engines": {
11 | "node": ">=8",
12 | "npm": ">=5"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/avin/react-vt-table"
17 | },
18 | "keywords": [
19 | "react",
20 | "table",
21 | "reactjs",
22 | "react-component",
23 | "virtual",
24 | "virtualized",
25 | "header"
26 | ],
27 | "lint-staged": {
28 | "linters": {
29 | "*.{js,json,css,md}": [
30 | "prettier --write",
31 | "git add"
32 | ]
33 | }
34 | },
35 | "husky": {
36 | "hooks": {
37 | "pre-commit": "lint-staged"
38 | }
39 | },
40 | "scripts": {
41 | "build:demo": "npm run clean:demo && build-storybook -o demo",
42 | "build:dist": "npm run clean:dist && rollup -c",
43 | "clean:demo": "rimraf demo",
44 | "clean:dist": "rimraf dist",
45 | "deploy": "gh-pages -d demo -r https://github.com/avin/react-vt-table",
46 | "postpublish": "npm run deploy",
47 | "precommit": "lint-staged",
48 | "prepare": "npm run build:dist",
49 | "predeploy": "npm run build:demo",
50 | "prettier": "prettier --write '{src}/**/*.js'",
51 | "storybook": "start-storybook -p 9009",
52 | "start": "rollup -c -w",
53 | "test": "cross-env CI=1 react-scripts test --env=jsdom",
54 | "test:watch": "react-scripts test --env=jsdom"
55 | },
56 | "dependencies": {
57 | "@babel/runtime": "^7.3.1",
58 | "classnames": "^2.2.6",
59 | "memoize-one": "^5.0.0",
60 | "react-draggable": "^3.1.1",
61 | "react-window": "^1.5.1"
62 | },
63 | "peerDependencies": {
64 | "react": "^15.0.0 || ^16.0.0",
65 | "react-dom": "^15.0.0 || ^16.0.0"
66 | },
67 | "devDependencies": {
68 | "@avinlab/react-size-me": "^1.1.5",
69 | "@babel/core": "^7.0.0",
70 | "@babel/plugin-proposal-class-properties": "^7.3.0",
71 | "@babel/plugin-transform-runtime": "^7.0.0",
72 | "@babel/preset-env": "^7.3.1",
73 | "@babel/preset-flow": "^7.0.0",
74 | "@babel/preset-react": "^7.0.0",
75 | "@fortawesome/fontawesome-svg-core": "^1.2.15",
76 | "@fortawesome/free-solid-svg-icons": "^5.7.2",
77 | "@fortawesome/react-fontawesome": "^0.1.4",
78 | "@storybook/addon-actions": "^4.1.11",
79 | "@storybook/addon-links": "^4.1.11",
80 | "@storybook/addons": "^4.1.11",
81 | "@storybook/react": "^4.1.11",
82 | "babel-core": "^7.0.0-bridge.0",
83 | "babel-loader": "^8.0.5",
84 | "babel-plugin-annotate-pure-calls": "^0.4.0",
85 | "babel-plugin-flow-react-proptypes": "^25.0.0",
86 | "cpr": "^3.0.1",
87 | "cross-env": "^5.1.4",
88 | "enzyme": "^3.7.0",
89 | "enzyme-adapter-react-16": "^1.9.1",
90 | "eslint-config-prettier": "^4.0.0",
91 | "flow-bin": "^0.93.0",
92 | "gh-pages": "^2.0.1",
93 | "husky": "^1.1.3",
94 | "immutable": "^4.0.0-rc.12",
95 | "jest-environment-enzyme": "^7.0.1",
96 | "jest-enzyme": "^7.0.1",
97 | "lint-staged": "^8.1.4",
98 | "prettier": "^1.16.4",
99 | "prop-types": "^15.7.2",
100 | "react": "^16.8.1",
101 | "react-dom": "^16.8.1",
102 | "react-json-tree": "^0.11.2",
103 | "react-scripts": "^2.1.5",
104 | "react-test-renderer": "^16.8.1",
105 | "rimraf": "^2.6.3",
106 | "rollup": "^1.1.2",
107 | "rollup-plugin-babel": "^4.3.2",
108 | "rollup-plugin-commonjs": "^9.2.0",
109 | "rollup-plugin-copy": "^0.2.3",
110 | "rollup-plugin-node-resolve": "^4.0.0",
111 | "rollup-plugin-size-snapshot": "^0.8.0",
112 | "sass-loader": "^7.1.0",
113 | "sinon": "^7.2.3"
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel';
2 | import commonjs from 'rollup-plugin-commonjs';
3 | import nodeResolve from 'rollup-plugin-node-resolve';
4 | import copy from 'rollup-plugin-copy';
5 | import pkg from './package.json';
6 |
7 | const input = './src/lib/index.js';
8 |
9 | const external = id => !id.includes(':\\') && !id.startsWith('.') && !id.startsWith('/');
10 |
11 | export default [
12 | {
13 | input,
14 | output: {
15 | file: pkg.main,
16 | format: 'cjs',
17 | },
18 | external,
19 | plugins: [
20 | babel({
21 | runtimeHelpers: true,
22 | plugins: ['@babel/transform-runtime'],
23 | }),
24 | nodeResolve(),
25 | commonjs(),
26 | copy({
27 | 'src/lib/style.css': 'dist/style.css',
28 | }),
29 | ],
30 | },
31 |
32 | {
33 | input,
34 | output: {
35 | file: pkg.module,
36 | format: 'esm',
37 | },
38 | external,
39 | plugins: [
40 | babel({
41 | runtimeHelpers: true,
42 | plugins: [['@babel/transform-runtime', { useESModules: true }]],
43 | }),
44 | nodeResolve(),
45 | commonjs(),
46 | ],
47 | },
48 | ];
49 |
--------------------------------------------------------------------------------
/src/lib/Column/Column.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | export default class Column extends React.Component {
5 | static propTypes = {
6 | /**
7 | * Content cell render function
8 | */
9 | cellRenderer: PropTypes.func,
10 | /**
11 | * Column header cell render function
12 | */
13 | columnHeaderCellRenderer: PropTypes.func,
14 | /**
15 | * Field key containing data
16 | */
17 | dataKey: PropTypes.string,
18 | /**
19 | * Default column width in pixels
20 | */
21 | width: PropTypes.number,
22 | };
23 |
24 | render() {
25 | return ;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/lib/Column/index.js:
--------------------------------------------------------------------------------
1 | import Column from './Column';
2 | export default Column;
3 |
--------------------------------------------------------------------------------
/src/lib/ColumnResizer/ColumnResizer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Draggable from 'react-draggable';
4 |
5 | export default class ColumnResizer extends React.Component {
6 | static propTypes = {
7 | /**
8 | * Action on resize column
9 | */
10 | onResizeColumn: PropTypes.func.isRequired,
11 | };
12 |
13 | handleDrag = (event, data) => {
14 | const { onResizeColumn } = this.props;
15 | onResizeColumn(data.x);
16 | };
17 |
18 | handleClick = event => {
19 | event.stopPropagation();
20 | };
21 |
22 | render() {
23 | return (
24 |
35 |
36 |
37 | );
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/lib/ColumnResizer/index.js:
--------------------------------------------------------------------------------
1 | import ColumnResizer from './ColumnResizer';
2 | export default ColumnResizer;
3 |
--------------------------------------------------------------------------------
/src/lib/Header/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ColumnResizer from '../ColumnResizer';
3 | import PropTypes from 'prop-types';
4 |
5 | export default class Header extends React.Component {
6 | static propTypes = {
7 | children: PropTypes.array,
8 | getHeaderHeight: PropTypes.func.isRequired,
9 | getColumnWidth: PropTypes.func.isRequired,
10 | height: PropTypes.number.isRequired,
11 | onClick: PropTypes.func,
12 | onDoubleClick: PropTypes.func,
13 | onMouseOver: PropTypes.func,
14 | onMouseOut: PropTypes.func,
15 | onRightClick: PropTypes.func,
16 | onResizeColumn: PropTypes.func,
17 | sortIndicatorRenderer: PropTypes.func,
18 | };
19 |
20 | static defaultProps = {
21 | onResizeColumn: f => f,
22 | sortIndicatorRenderer: () => null,
23 | };
24 |
25 | render() {
26 | const {
27 | children,
28 | getHeaderHeight,
29 | getColumnWidth,
30 | onResizeColumn,
31 | sortIndicatorRenderer,
32 | overflowWidth,
33 | } = this.props;
34 | let idx = 0;
35 | return (
36 | (this.headerEl = i)}>
37 | {React.Children.map(children, child => {
38 | let columnIndex = idx;
39 | if (!child) {
40 | return null;
41 | }
42 | let { label, dataKey, columnHeaderCellRenderer } = child.props;
43 | const width = getColumnWidth(columnIndex);
44 |
45 | let content;
46 | if (columnHeaderCellRenderer) {
47 | content = columnHeaderCellRenderer({ label, dataKey, columnIndex });
48 | } else {
49 | content = (
50 |
51 | {label}
52 |
53 | );
54 | }
55 |
56 | const getAction = actionName => {
57 | const action = this.props[actionName];
58 | if (action) {
59 | return event => action(event, { dataKey, columnIndex });
60 | }
61 | };
62 |
63 | idx += 1;
64 |
65 | return (
66 |
75 | {content}
76 |
77 | onResizeColumn(columnIndex, diff, dataKey)} />
78 |
79 | {sortIndicatorRenderer({ dataKey: child.props.dataKey, columnIndex })}
80 |
81 | );
82 | })}
83 |
84 |
85 | );
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/lib/Header/index.js:
--------------------------------------------------------------------------------
1 | import Header from './Header';
2 | export default Header;
3 |
--------------------------------------------------------------------------------
/src/lib/Row/Row.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 | import PropTypes from 'prop-types';
4 |
5 | export default class Row extends React.PureComponent {
6 | static propTypes = {
7 | /**
8 | * Row index number
9 | */
10 | index: PropTypes.number,
11 | /**
12 | * Row style object
13 | */
14 | style: PropTypes.object,
15 | /**
16 | * Table columns array
17 | */
18 | children: PropTypes.array,
19 | /**
20 | * Row className determine function
21 | */
22 | rowClassName: PropTypes.func,
23 | /**
24 | * Function to get cell value
25 | */
26 | getCellValue: PropTypes.func,
27 | /**
28 | * Function to get column width
29 | */
30 | getColumnWidth: PropTypes.func,
31 | /**
32 | * Mouse actions
33 | */
34 | onClick: PropTypes.func,
35 | onDoubleClick: PropTypes.func,
36 | onMouseOver: PropTypes.func,
37 | onMouseOut: PropTypes.func,
38 | onMouseDown: PropTypes.func,
39 | onMouseUp: PropTypes.func,
40 | onRightClick: PropTypes.func,
41 | };
42 | render() {
43 | let { data, index, style } = this.props;
44 |
45 | let {
46 | dataList,
47 | getRowData,
48 | getRowWidth,
49 | rowClassName,
50 | getCellValue,
51 | getColumnWidth,
52 | children,
53 | onClick,
54 | onDoubleClick,
55 | onMouseOver,
56 | onMouseOut,
57 | onMouseDown,
58 | onMouseUp,
59 | onRightClick,
60 | } = data.rowProps;
61 |
62 | style = { ...style, width: getRowWidth() };
63 |
64 | const evenClassName = index % 2 === 0 ? 'VTRowOdd' : 'VTRowEven';
65 | const customClassName = rowClassName && rowClassName(index);
66 |
67 | const rowData = getRowData(index, dataList);
68 |
69 | let idx = 0;
70 | return (
71 |
83 | {React.Children.map(children, child => {
84 | if (!child) {
85 | return null;
86 | }
87 | const { cellRenderer, dataKey } = child.props;
88 | const width = getColumnWidth(idx);
89 |
90 | let content;
91 | if (cellRenderer) {
92 | content = cellRenderer({ dataKey, rowData, columnIndex: idx });
93 | } else {
94 | const contentStr = getCellValue({ rowData, dataKey });
95 | content = (
96 |
97 | {contentStr}
98 |
99 | );
100 | }
101 |
102 | idx += 1;
103 | return (
104 |
105 | {content}
106 |
107 | );
108 | })}
109 |
110 | );
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/lib/Row/index.js:
--------------------------------------------------------------------------------
1 | import Row from './Row';
2 | export default Row;
3 |
--------------------------------------------------------------------------------
/src/lib/Table/Table.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { VariableSizeList as List } from 'react-window';
3 | import PropTypes from 'prop-types';
4 | import classNames from 'classnames';
5 | import Header from '../Header/Header';
6 | import Row from '../Row/Row';
7 | import Column from '../Column/Column';
8 | import memoize from 'memoize-one';
9 |
10 | const countChildren = children => {
11 | let count = 0;
12 | React.Children.forEach(children, child => {
13 | if (child) {
14 | count += 1;
15 | }
16 | });
17 | return count;
18 | };
19 |
20 | const getItemData = memoize((dataList, rowProps) => ({
21 | dataList,
22 | rowProps,
23 | }));
24 |
25 | export default class Table extends React.Component {
26 | state = {
27 | customColumnsWidth: [],
28 | };
29 |
30 | autoScrolling = this.props.autoScroll;
31 |
32 | static propTypes = {
33 | /**
34 | * Table width
35 | */
36 | width: PropTypes.number.isRequired,
37 | /**
38 | * Table height
39 | */
40 | height: PropTypes.number.isRequired,
41 | /**
42 | * Table header height
43 | */
44 | headerHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),
45 | /**
46 | * Table row height (may be function)
47 | *
48 | * @param {Number} index Порядковый номер элемента в списке данных
49 | */
50 | rowHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),
51 | /**
52 | * Data list
53 | */
54 | data: PropTypes.any.isRequired,
55 | /**
56 | * Row className determine function
57 | */
58 | rowClassName: PropTypes.func,
59 | /**
60 | * Personal row renderer function (If nothing return, then internal row function will be applied)
61 | *
62 | * @param {object} props
63 | */
64 | rowRenderer: PropTypes.func,
65 | /**
66 | * Sort indicator render function
67 | *
68 | * @param {object} props
69 | */
70 | sortIndicatorRenderer: PropTypes.func,
71 | /**
72 | * Mouse action on header row
73 | *
74 | * @param {SyntheticEvent} event
75 | * @param {object} headerCellParams Параметры ячейки заголовка
76 | */
77 | onHeaderClick: PropTypes.func,
78 | onHeaderDoubleClick: PropTypes.func,
79 | onHeaderMouseOver: PropTypes.func,
80 | onHeaderMouseOut: PropTypes.func,
81 | onHeaderRightClick: PropTypes.func,
82 | /**
83 | * Mouse action on table row
84 | *
85 | * @param {SyntheticEvent} event
86 | * @param {object} rowCellParams Параметры ячейки строки таблицы
87 | */
88 | onRowClick: PropTypes.func,
89 | onRowDoubleClick: PropTypes.func,
90 | onRowMouseOver: PropTypes.func,
91 | onRowMouseOut: PropTypes.func,
92 | onRowRightClick: PropTypes.func,
93 | /**
94 | * Action on table scroll
95 | *
96 | * @param {object} params
97 | * @see See [React-Windows docs](https://react-window.now.sh/#/api/FixedSizeList)
98 | */
99 | onScroll: PropTypes.func,
100 | /**
101 | * Action on change column width
102 | *
103 | * @param {object} params
104 | */
105 | onResizeColumn: PropTypes.func,
106 | /**
107 | * Width of vertical table overflow
108 | */
109 | overflowWidth: PropTypes.number,
110 | /**
111 | * Minimal column width
112 | */
113 | minColumnWidth: PropTypes.number,
114 | /**
115 | * Maximum column width
116 | */
117 | maxColumnWidth: PropTypes.number,
118 | /**
119 | * Dynamic width for columns that was not resized
120 | */
121 | dynamicColumnWidth: PropTypes.bool,
122 | /**
123 | * Props for inner `react-window` list component
124 | * @see See [React-Windows docs](https://react-window.now.sh/#/api/FixedSizeList)
125 | */
126 | listProps: PropTypes.object,
127 | /**
128 | * No items in data list label
129 | */
130 | noItemsLabel: PropTypes.node,
131 | /**
132 | * Hide table header row
133 | */
134 | disableHeader: PropTypes.bool,
135 | /**
136 | * Auto scroll to list bottom
137 | */
138 | autoScroll: PropTypes.bool,
139 | /**
140 | * Optional custom CSS class name to attach to root container element
141 | */
142 | className: PropTypes.string,
143 | /**
144 | * Optional custom id to attach to root container element
145 | */
146 | id: PropTypes.string,
147 | };
148 |
149 | static defaultProps = {
150 | headerHeight: 30,
151 | rowHeight: 30,
152 | onHeaderClick: f => f,
153 | onResizeColumn: f => f,
154 | onRowClick: f => f,
155 | overflowWidth: 17,
156 | minColumnWidth: 30,
157 | dynamicColumnWidth: false,
158 | noItemsLabel: 'No items',
159 | };
160 |
161 | rowWidth = null;
162 |
163 | calculateCustomColumnsWidth() {
164 | const { children, overflowWidth, width, minColumnWidth, dynamicColumnWidth } = this.props;
165 |
166 | let customColumnsWidth = [];
167 | let customColumnsCount = 0;
168 | let customColumnsWidthSum = 0;
169 |
170 | React.Children.forEach(children, child => {
171 | if (!child) {
172 | return;
173 | }
174 | let { width } = child.props;
175 | if (Number.isInteger(width)) {
176 | width = Math.max(width, minColumnWidth);
177 | customColumnsCount += 1;
178 | customColumnsWidthSum += width;
179 | }
180 | customColumnsWidth.push(width || undefined);
181 | });
182 |
183 | if (!dynamicColumnWidth) {
184 | //Fill other width averaged width values
185 | const defaultWidth = Math.max(
186 | (width - overflowWidth - customColumnsWidthSum) / (this.getColumnsCount() - customColumnsCount),
187 | minColumnWidth,
188 | );
189 | customColumnsWidth = customColumnsWidth.map(item => {
190 | if (item === undefined) {
191 | return defaultWidth;
192 | }
193 | return item;
194 | });
195 | }
196 |
197 | return customColumnsWidth;
198 | }
199 |
200 | componentWillUpdate() {
201 | this.rowWidth = null;
202 | }
203 |
204 | componentDidUpdate(prevProps) {
205 | if (countChildren(prevProps.children) !== countChildren(this.props.children)) {
206 | this.setState({
207 | customColumnsWidth: this.calculateCustomColumnsWidth(),
208 | });
209 | }
210 |
211 | if (this.autoScrolling && this.props.autoScroll && this.props.data) {
212 | this.scrollToItem(this.getDataSize());
213 | }
214 | }
215 |
216 | constructor(props) {
217 | super(props);
218 |
219 | this.state.customColumnsWidth = this.calculateCustomColumnsWidth();
220 | }
221 |
222 | getColumnsCount() {
223 | const { children } = this.props;
224 |
225 | let total = 0;
226 | React.Children.forEach(children, child => {
227 | if (child && child.type === Column) {
228 | total += 1;
229 | }
230 | });
231 | return total;
232 | }
233 |
234 | componentDidMount() {
235 | const { disableHeader } = this.props;
236 |
237 | if (!disableHeader) {
238 | this.listOuter &&
239 | this.listOuter.addEventListener('scroll', e => {
240 | this.header.headerEl.scrollLeft = e.target.scrollLeft;
241 | });
242 | }
243 | }
244 |
245 | getColumnWidth = columnIndex => {
246 | const { width, overflowWidth, minColumnWidth, dynamicColumnWidth } = this.props;
247 | const { customColumnsWidth } = this.state;
248 |
249 | if (!dynamicColumnWidth) {
250 | return customColumnsWidth[columnIndex];
251 | }
252 |
253 | if (customColumnsWidth[columnIndex]) {
254 | return customColumnsWidth[columnIndex];
255 | }
256 |
257 | let customColumnsWidthSum = 0;
258 | let customColumnsCount = 0;
259 | customColumnsWidth.forEach(item => {
260 | if (item) {
261 | customColumnsCount += 1;
262 | customColumnsWidthSum += item;
263 | }
264 | });
265 |
266 | return Math.max(
267 | (width - overflowWidth - customColumnsWidthSum) / (this.getColumnsCount() - customColumnsCount),
268 | minColumnWidth,
269 | );
270 | };
271 |
272 | getRowHeight = index => {
273 | const { rowHeight } = this.props;
274 | if (typeof rowHeight === 'function') {
275 | return rowHeight(index);
276 | }
277 | return rowHeight;
278 | };
279 |
280 | getEstimatedItemSize = () => {
281 | const { data } = this.props;
282 | const total = data.reduce((total, item, idx) => {
283 | return total + this.getRowHeight(idx);
284 | }, 0);
285 | return Math.floor(total / this.getDataSize());
286 | };
287 |
288 | getHeaderHeight = () => {
289 | const { headerHeight, disableHeader } = this.props;
290 | if (disableHeader) {
291 | return 0;
292 | }
293 |
294 | if (typeof headerHeight === 'function') {
295 | return headerHeight();
296 | }
297 | return headerHeight;
298 | };
299 |
300 | getRowData = (index, data) => {
301 | data = data || this.props.data;
302 | if (typeof data.toJS === 'function') {
303 | console.log(data);
304 | debugger;
305 | return data.get(index);
306 | } else {
307 | return data[index];
308 | }
309 | };
310 |
311 | getCellValue = ({ rowData, dataKey }) => {
312 | if (typeof rowData.toJS === 'function') {
313 | return rowData.get(dataKey);
314 | } else {
315 | return rowData[dataKey];
316 | }
317 | };
318 |
319 | getDataSize = () => {
320 | const { data } = this.props;
321 |
322 | if (typeof data.toJS === 'function') {
323 | return data.size;
324 | } else {
325 | return data.length;
326 | }
327 | };
328 |
329 | getRowWidth = () => {
330 | const { customColumnsWidth } = this.state;
331 | if (this.rowWidth === null) {
332 | this.rowWidth = customColumnsWidth.reduce((result, item, idx) => result + this.getColumnWidth(idx), 0);
333 | }
334 | return this.rowWidth;
335 | };
336 |
337 | handleResizeColumn = (columnIndex, diff, dataKey) => {
338 | const { minColumnWidth, maxColumnWidth, onResizeColumn } = this.props;
339 | const { customColumnsWidth } = this.state;
340 | const columnWidth = this.getColumnWidth(columnIndex);
341 | const newCustomColumnsWidth = [...customColumnsWidth];
342 | let newWidth = Math.max(columnWidth + diff, minColumnWidth);
343 | if (maxColumnWidth) {
344 | newWidth = Math.min(newCustomColumnsWidth[columnIndex], maxColumnWidth);
345 | }
346 | newCustomColumnsWidth[columnIndex] = newWidth;
347 |
348 | this.setState({
349 | customColumnsWidth: newCustomColumnsWidth,
350 | });
351 |
352 | this.list && this.list.resetAfterIndex(0);
353 |
354 | onResizeColumn({ dataKey, columnIndex, resizeDiff: diff, newWidth });
355 | };
356 |
357 | scrollTo(scrollOffset) {
358 | this.list && this.list.scrollTo(scrollOffset);
359 | }
360 |
361 | scrollToItem(index, align) {
362 | this.list && this.list.scrollToItem(index, align);
363 | }
364 |
365 | renderHeader() {
366 | const { children, disableHeader, headerHeight, sortIndicatorRenderer, overflowWidth } = this.props;
367 |
368 | if (disableHeader) {
369 | return null;
370 | }
371 |
372 | const componentProps = {
373 | children: children,
374 | overflowWidth,
375 | height: headerHeight,
376 | getColumnWidth: this.getColumnWidth,
377 | getHeaderHeight: this.getHeaderHeight,
378 | onResizeColumn: this.handleResizeColumn,
379 | onClick: this.props.onHeaderClick,
380 | onDoubleClick: this.props.onHeaderDoubleClick,
381 | onMouseOver: this.props.onHeaderMouseOver,
382 | onMouseOut: this.props.onHeaderMouseOut,
383 | onRightClick: this.props.onHeaderRightClick,
384 | sortIndicatorRenderer: sortIndicatorRenderer,
385 | ref: i => (this.header = i),
386 | };
387 |
388 | return ;
389 | }
390 |
391 | getRowHandler = actionType => {
392 | if (!this[`handleRow${actionType}`]) {
393 | this[`handleRow${actionType}`] = event => {
394 | const action = this.props[`onRow${actionType}`];
395 | if (action) {
396 | const rowIndex = Number(event.currentTarget.dataset.rowIndex);
397 | const rowData = this.getRowData(rowIndex);
398 | action(event, { rowIndex, rowData });
399 | }
400 | };
401 | }
402 | return this[`handleRow${actionType}`];
403 | };
404 |
405 | renderRow() {
406 | const { rowRenderer } = this.props;
407 |
408 | return rowRenderer ? rowRenderer : Row;
409 | }
410 |
411 | renderNoItemsLabel() {
412 | const { noItemsLabel } = this.props;
413 |
414 | return {noItemsLabel}
;
415 | }
416 |
417 | handleScroll = ({ scrollDirection, scrollOffset, scrollUpdateWasRequested }) => {
418 | const { onScroll } = this.props;
419 |
420 | //react-window fires onScroll every time on mount, fixing it using this condition
421 | if (!(!onScroll || (scrollDirection === 'forward' && scrollOffset === 0 && !scrollUpdateWasRequested))) {
422 | onScroll({ scrollDirection, scrollOffset, scrollUpdateWasRequested });
423 | }
424 |
425 | if (this.props.autoScroll && !scrollUpdateWasRequested) {
426 | const scrollHeight = this.listOuter.scrollHeight;
427 | const clientHeight = this.listOuter.offsetHeight;
428 | this.autoScrolling = scrollHeight === clientHeight + scrollOffset;
429 | }
430 | };
431 |
432 | render() {
433 | const { height, width, listProps, className, data, rowClassName, children } = this.props;
434 |
435 | const rowProps = {
436 | children: children,
437 | rowClassName,
438 | getRowData: this.getRowData,
439 | getRowWidth: this.getRowWidth,
440 | getCellValue: this.getCellValue,
441 | getColumnWidth: this.getColumnWidth,
442 | onClick: this.getRowHandler('Click'),
443 | onDoubleClick: this.getRowHandler('DoubleClick'),
444 | onMouseOver: this.getRowHandler('MouseOver'),
445 | onMouseOut: this.getRowHandler('MouseOut'),
446 | onMouseDown: this.getRowHandler('MouseDown'),
447 | onMouseUp: this.getRowHandler('MouseUp'),
448 | onRightClick: this.getRowHandler('RightClick'),
449 | };
450 |
451 | const itemData = getItemData(data, rowProps);
452 |
453 | return (
454 |
458 | {this.renderHeader()}
459 |
460 | {this.getDataSize() ? (
461 | (this.list = i)}
463 | innerRef={i => (this.listInner = i)}
464 | outerRef={i => (this.listOuter = i)}
465 | className="VTList"
466 | height={height - this.getHeaderHeight()}
467 | itemCount={this.getDataSize()}
468 | itemSize={this.getRowHeight}
469 | itemData={itemData}
470 | width={width}
471 | onScroll={this.handleScroll}
472 | estimatedItemSize={this.getEstimatedItemSize()}
473 | {...listProps}
474 | >
475 | {this.renderRow()}
476 |
477 | ) : (
478 | this.renderNoItemsLabel()
479 | )}
480 |
481 | );
482 | }
483 | }
484 |
--------------------------------------------------------------------------------
/src/lib/Table/index.js:
--------------------------------------------------------------------------------
1 | import Table from './Table';
2 | export default Table;
3 |
--------------------------------------------------------------------------------
/src/lib/index.js:
--------------------------------------------------------------------------------
1 | import Column from './Column';
2 | import ColumnResizer from './ColumnResizer';
3 | import Table from './Table';
4 | import Row from './Row';
5 | import Header from './Header';
6 |
7 | export { Column, ColumnResizer, Table, Row, Header };
8 |
--------------------------------------------------------------------------------
/src/lib/style.css:
--------------------------------------------------------------------------------
1 | .VTContainer {
2 | position: relative;
3 | }
4 |
5 | .VTList {
6 | overflow-y: scroll !important;
7 | }
8 |
9 | .VTHeader {
10 | position: absolute;
11 | top: 0;
12 | left: 0;
13 | right: 0;
14 | background-color: #e1e8ed;
15 | }
16 |
17 | .VTGrid {
18 | outline: 1px solid #5c7080;
19 | }
20 |
21 | .VTHeader {
22 | position: absolute;
23 | display: flex;
24 | flex-flow: row nowrap;
25 | height: 100%;
26 | align-items: stretch;
27 | overflow: hidden;
28 | }
29 |
30 | .VTRow {
31 | display: flex;
32 | flex-flow: row nowrap;
33 | height: 100%;
34 | align-items: stretch;
35 | box-sizing: border-box;
36 | }
37 |
38 | .VTRowOdd {
39 | background-color: #fff;
40 | }
41 |
42 | .VTRowEven {
43 | background-color: #f5f8fa;
44 | }
45 |
46 | .VTCell,
47 | .VTHeaderCell {
48 | flex-grow: 1;
49 | box-sizing: border-box;
50 | position: relative;
51 |
52 | -ms-flex-align: center;
53 | align-items: center;
54 | display: -ms-flexbox;
55 | display: flex;
56 | }
57 |
58 | .VTHeaderCell {
59 | border-right: 1px solid #bfccd6;
60 | }
61 |
62 | .VTCell {
63 | border-right: 1px solid #e1e8ed;
64 | border-bottom: 1px solid #ced9e0;
65 | }
66 |
67 | .VTHeader {
68 | box-sizing: border-box;
69 | border-bottom: 2px solid #5c7080;
70 | font-weight: bold;
71 | }
72 |
73 | .VTHeaderCell:last-of-type {
74 | border-right: 0;
75 | }
76 |
77 | .VTCellContent {
78 | overflow: hidden;
79 | padding: 0 0.5em;
80 | white-space: nowrap;
81 | text-overflow: ellipsis;
82 | }
83 |
84 | .VTColumnResizer {
85 | cursor: col-resize;
86 | position: absolute;
87 | right: -3px;
88 | top: 0;
89 | height: 100%;
90 | background-color: #1f4b99;
91 | opacity: 0;
92 | width: 6px;
93 | z-index: 20;
94 | }
95 |
96 | .VTColumnResizer:hover {
97 | opacity: 0.1;
98 | }
99 |
100 | .VTColumnResizerActive {
101 | opacity: 0.5 !important;
102 | transition: opacity 200ms;
103 | }
104 |
105 | .VTNoItemsLabel {
106 | position: absolute;
107 | top: 0;
108 | bottom: 0;
109 | left: 0;
110 | right: 0;
111 | display: -ms-flexbox;
112 | display: flex;
113 | -ms-flex-align: center;
114 | align-items: center;
115 | -ms-flex-pack: center;
116 | justify-content: center;
117 | font-size: 1em;
118 | color: #bdbdbd;
119 | }
120 |
--------------------------------------------------------------------------------
/src/stories/_data.js:
--------------------------------------------------------------------------------
1 | import * as Immutable from 'immutable';
2 |
3 | const randomStr = (len = 5) => {
4 | let text = '';
5 | const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
6 |
7 | for (let i = 0; i < len; i++) text += possible.charAt(Math.floor(Math.random() * possible.length));
8 |
9 | return text;
10 | };
11 |
12 | function randomColor() {
13 | const letters = '0123456789ABCDEF';
14 | let color = '#';
15 | for (let i = 0; i < 6; i++) {
16 | color += letters[Math.floor(Math.random() * 16)];
17 | }
18 | return color;
19 | }
20 |
21 | let dataList = new Immutable.List();
22 | for (let i = 0; i < 1000; i += 1) {
23 | dataList = dataList.push(
24 | new Immutable.Map({
25 | c1: `${Math.floor(i / 10)}_word_c1`,
26 | c2: `${Math.floor(i / 10)}_word_c2`,
27 | c3: `${Math.floor(i / 10)}_${randomStr()}_word_c3`,
28 | c4: `${Math.floor(i / 10)}_${randomStr()}_word_c4`,
29 | color: randomColor(),
30 | }),
31 | );
32 | }
33 |
34 | export default dataList;
35 |
--------------------------------------------------------------------------------
/src/stories/autoScroll.js:
--------------------------------------------------------------------------------
1 | import Table from '../lib/Table';
2 | import { action } from '@storybook/addon-actions';
3 | import Column from '../lib/Column';
4 | import React from 'react';
5 | import * as Immutable from 'immutable';
6 |
7 | class TableWithAutoScroll extends React.Component {
8 | state = {
9 | list: new Immutable.List(),
10 | };
11 |
12 | addListItem = () => {
13 | const { list } = this.state;
14 |
15 | this.setState(
16 | {
17 | list: list.push(
18 | new Immutable.Map({
19 | c1: Math.random(),
20 | c2: Math.random(),
21 | }),
22 | ),
23 | },
24 | () => {
25 | this.addListItemTimerId = setTimeout(this.addListItem, 200);
26 | },
27 | );
28 | };
29 |
30 | componentDidMount() {
31 | this.addListItem();
32 | }
33 |
34 | componentWillUnmount() {
35 | clearTimeout(this.addListItemTimerId);
36 | }
37 |
38 | render() {
39 | const { list } = this.state;
40 |
41 | return (
42 |
43 |
(this.table = i)}
52 | autoScroll
53 | >
54 |
55 |
56 |
57 |
58 | );
59 | }
60 | }
61 |
62 | export default () => {
63 | return ;
64 | };
65 |
--------------------------------------------------------------------------------
/src/stories/changeColumns.js:
--------------------------------------------------------------------------------
1 | import Table from '../lib/Table';
2 | import dataList from './_data';
3 | import { action } from '@storybook/addon-actions';
4 | import Column from '../lib/Column';
5 | import React from 'react';
6 |
7 | class ChangeColumnsTable extends React.Component {
8 | state = {
9 | condition: 1,
10 | };
11 |
12 | handleChangeCondition = () => {
13 | const { condition } = this.state;
14 | this.setState({
15 | condition: condition === 1 ? 2 : 1,
16 | });
17 | };
18 | render() {
19 | const { condition } = this.state;
20 | return (
21 |
22 |
23 |
26 |
27 |
28 |
Math.floor(20 + Math.random() * 60)}
34 | onRowClick={action('row clicked')}
35 | disableHeader={false}
36 | >
37 |
38 | (
42 |
53 |
{rowData.get(dataKey)}
54 |
55 | )}
56 | />
57 | {condition === 2 && }
58 |
59 |
60 |
61 | );
62 | }
63 | }
64 |
65 | export default () => {
66 | return ;
67 | };
68 |
--------------------------------------------------------------------------------
/src/stories/index.js:
--------------------------------------------------------------------------------
1 | import { storiesOf } from '@storybook/react';
2 | import '../lib/style.css';
3 | import './style.css';
4 | import simple from './simple';
5 | import sort from './sort';
6 | import selection from './selection';
7 | import rowRenderer from './rowRenderer';
8 | import scrollTo from './scrollTo';
9 | import changeColumns from './changeColumns';
10 | import autoScroll from './autoScroll';
11 |
12 | storiesOf('Table', module)
13 | .add('Simple table', simple)
14 | .add('Table with sorting', sort)
15 | .add('Table with selection', selection)
16 | .add('Scroll to item', scrollTo)
17 | .add('Custom row renderer', rowRenderer)
18 | .add('Change columns', changeColumns)
19 | .add('Auto scroll', autoScroll);
20 |
--------------------------------------------------------------------------------
/src/stories/rowRenderer.js:
--------------------------------------------------------------------------------
1 | import Table from '../lib/Table';
2 | import Row from '../lib/Row';
3 | import dataList from './_data';
4 | import { action } from '@storybook/addon-actions';
5 | import Column from '../lib/Column';
6 | import React from 'react';
7 |
8 | const rowRenderer = props => {
9 | const { index, style, data } = props;
10 | const { getRowWidth } = data.rowProps;
11 | if (index % 10 === 0) {
12 | return (
13 |
17 | This is #{index / 10 + 1} row rendered by custom renderer!
18 |
19 | );
20 | }
21 |
22 | return
;
23 | };
24 |
25 | export default () => {
26 | return (
27 |
28 |
38 | {false}
39 |
40 |
41 |
42 |
43 |
44 |
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/src/stories/scrollTo.js:
--------------------------------------------------------------------------------
1 | import Table from '../lib/Table';
2 | import dataList from './_data';
3 | import { action } from '@storybook/addon-actions';
4 | import Column from '../lib/Column';
5 | import React from 'react';
6 |
7 | function randomBetween(min, max) {
8 | return Math.floor(Math.random() * (max - min + 1)) + min;
9 | }
10 |
11 | class TableWithScrollButton extends React.Component {
12 | scrollToRandomElement = () => {
13 | this.table.scrollToItem(randomBetween(0, dataList.size));
14 | };
15 |
16 | scrollToRandomOffset = () => {
17 | const scrollHeight = this.table.listOuter.scrollHeight;
18 | this.table.scrollTo(randomBetween(0, scrollHeight));
19 | };
20 |
21 | render() {
22 | return (
23 |
24 |
25 |
28 |
29 |
32 |
33 |
34 |
(this.table = i)}
43 | >
44 |
45 | (
49 |
53 | {rowData.get(dataKey)}
54 |
55 | )}
56 | />
57 |
58 |
59 |
60 |
61 | );
62 | }
63 | }
64 |
65 | export default () => {
66 | return ;
67 | };
68 |
--------------------------------------------------------------------------------
/src/stories/selection.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Table from '../lib/Table';
3 | import dataList from './_data';
4 | import Column from '../lib/Column';
5 |
6 | function rangeArr(from, to) {
7 | const result = [];
8 | if (to < from) {
9 | [from, to] = [to, from];
10 | }
11 | for (let i = from; i <= to; i++) {
12 | result.push(i);
13 | }
14 | return result;
15 | }
16 |
17 | function min(arr) {
18 | return arr.reduce((result, i) => {
19 | return i < result ? i : result;
20 | }, arr[0]);
21 | }
22 |
23 | function max(arr) {
24 | return arr.reduce((result, i) => {
25 | return i > result ? i : result;
26 | }, arr[0]);
27 | }
28 |
29 | class TableWithSelection extends React.Component {
30 | state = {
31 | selection: [],
32 | };
33 |
34 | handleClickRow = (event, { rowIndex }) => {
35 | const { selection } = this.state;
36 |
37 | if (event.ctrlKey) {
38 | if (!selection.includes(rowIndex)) {
39 | this.setState({ selection: [...selection, rowIndex] });
40 | } else {
41 | const newSelection = selection.filter(i => i !== rowIndex);
42 | this.setState({ selection: [...newSelection] });
43 | }
44 | } else if (event.shiftKey && selection.length) {
45 | selection.push(rowIndex);
46 | this.setState({ selection: rangeArr(min(selection), max(selection)) });
47 | } else {
48 | this.setState({ selection: [rowIndex] });
49 | }
50 | };
51 |
52 | getRowClassName = rowIndex => {
53 | const { selection } = this.state;
54 | if (selection.includes(rowIndex)) {
55 | return 'RowSelected';
56 | }
57 | };
58 |
59 | render() {
60 | return (
61 |
62 |
63 | Use [Ctrl] and [Shift] keys to multi-select rows.
64 |
65 |
76 |
77 | );
78 | }
79 | }
80 |
81 | export default () => ;
82 |
--------------------------------------------------------------------------------
/src/stories/simple.js:
--------------------------------------------------------------------------------
1 | import Table from '../lib/Table';
2 | import dataList from './_data';
3 | import { action } from '@storybook/addon-actions';
4 | import Column from '../lib/Column';
5 | import React from 'react';
6 |
7 | export default () => {
8 | return (
9 |
10 |
Math.floor(20 + Math.random() * 60)}
16 | onRowClick={action('row clicked')}
17 | disableHeader={false}
18 | >
19 |
20 | (
24 |
35 |
{rowData.get(dataKey)}
36 |
37 | )}
38 | />
39 |
40 |
41 |
42 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/src/stories/sort.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Table from '../lib/Table';
3 | import dataList from './_data';
4 | import Column from '../lib/Column';
5 |
6 | export function multiSort(list, keys) {
7 | let k = 0;
8 | keys.forEach(({ key, order }) => {
9 | list = list.sort((a, b) => {
10 | let ok = true;
11 | for (let m = 0; m < k; m += 1) {
12 | if (a.get(keys[m].key).localeCompare(b.get(keys[m].key)) !== 0) {
13 | ok = false;
14 | }
15 | }
16 | if (ok) {
17 | const orderFactor = order === 'DESC' ? -1 : 1;
18 | return a.get(key).localeCompare(b.get(key)) * orderFactor;
19 | } else {
20 | return 0;
21 | }
22 | });
23 |
24 | k += 1;
25 | });
26 |
27 | return list;
28 | }
29 |
30 | class TableWithSorting extends React.Component {
31 | state = {
32 | sortingKeys: [{ key: 'c1', order: 'ASC' }],
33 | };
34 |
35 | getSortedData() {
36 | const { sortingKeys } = this.state;
37 | return multiSort(dataList, sortingKeys);
38 | }
39 |
40 | handleClickHeader = (event, { dataKey }) => {
41 | let { sortingKeys } = this.state;
42 |
43 | const columnSortingIndex = sortingKeys.findIndex(i => i.key === dataKey);
44 | if (event.ctrlKey) {
45 | if (columnSortingIndex >= 0) {
46 | sortingKeys[columnSortingIndex] = {
47 | key: dataKey,
48 | order: sortingKeys[columnSortingIndex]['order'] === 'ASC' ? 'DESC' : 'ASC',
49 | };
50 | } else {
51 | sortingKeys.push({
52 | key: dataKey,
53 | order: 'ASC',
54 | });
55 | }
56 | } else {
57 | let order = 'ASC';
58 | if (columnSortingIndex >= 0) {
59 | order = sortingKeys[columnSortingIndex].order === 'ASC' ? 'DESC' : 'ASC';
60 | }
61 |
62 | sortingKeys = [
63 | {
64 | key: dataKey,
65 | order,
66 | },
67 | ];
68 | }
69 |
70 | this.setState({ sortingKeys: [...sortingKeys] });
71 | };
72 |
73 | sortIndicatorRender = ({ dataKey }) => {
74 | const { sortingKeys } = this.state;
75 | const columnSortingIndex = sortingKeys.findIndex(i => i.key === dataKey);
76 | const columnSorting = sortingKeys[columnSortingIndex];
77 |
78 | if (columnSorting) {
79 | return columnSorting.order === 'ASC' ? (
80 |
81 | ) : (
82 |
83 | );
84 | }
85 | return null;
86 | };
87 |
88 | render() {
89 | return (
90 |
91 |
92 | Click on header's cells to sort data. Use [Ctrl] to multi-sort.
93 |
94 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | );
111 | }
112 | }
113 |
114 | export default () => ;
115 |
--------------------------------------------------------------------------------
/src/stories/style.css:
--------------------------------------------------------------------------------
1 | .MyTable {
2 | border: 1px solid #d8e1e8;
3 | border-radius: 3px;
4 | user-select: none;
5 | transition: transform 0.2s cubic-bezier(0.4, 1, 0.75, 0.9), box-shadow 0.2s cubic-bezier(0.4, 1, 0.75, 0.9),
6 | -webkit-transform 0.2s cubic-bezier(0.4, 1, 0.75, 0.9), -webkit-box-shadow 0.2s cubic-bezier(0.4, 1, 0.75, 0.9);
7 | box-shadow: 0 0 0 1px rgba(16, 22, 26, 0.1), 0 1px 1px rgba(16, 22, 26, 0.2), 0 2px 6px rgba(16, 22, 26, 0.2);
8 | font-size: 14px;
9 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans',
10 | 'Droid Sans', 'Helvetica Neue', sans-serif;
11 | }
12 |
13 | .SortIndicator {
14 | position: absolute;
15 | right: 5px;
16 | top: 5px;
17 | }
18 |
19 | .SortIndicator.ASC {
20 | width: 0;
21 | height: 0;
22 | border-left: 5px solid transparent;
23 | border-right: 5px solid transparent;
24 |
25 | border-bottom: 5px solid #0e5a8a;
26 | }
27 |
28 | .SortIndicator.DESC {
29 | width: 0;
30 | height: 0;
31 | border-left: 5px solid transparent;
32 | border-right: 5px solid transparent;
33 |
34 | border-top: 5px solid #0e5a8a;
35 | }
36 |
37 | .SortIndicator > .counter {
38 | position: absolute;
39 | font-size: small;
40 | }
41 |
42 | .RowSelected {
43 | background-color: #48aff0;
44 | color: #fff;
45 | }
46 |
47 | .CustomRow {
48 | background-color: #f5498b;
49 | color: #fff;
50 | font-weight: bold;
51 | padding: 0 40px;
52 | box-sizing: border-box;
53 | }
54 |
55 | .DescriptionBlock {
56 | margin-bottom: 10px;
57 | }
58 |
--------------------------------------------------------------------------------