├── .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 | 73 | 74 | 75 |
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 | --------------------------------------------------------------------------------