├── .npmrc ├── .babelrc ├── example1.gif ├── src ├── index.js └── virtualTable │ ├── BaseTable.jsx │ └── VirtualTable.jsx ├── .npmignore ├── .travis.yml ├── .gitignore ├── examples ├── fixed │ ├── index.html │ └── index.js ├── src │ ├── index.html │ └── index.js └── onSelectAll │ ├── index.html │ └── index.js ├── dist ├── index.js └── virtualTable │ └── VirtualTable.js ├── webpack.config.js ├── package.json ├── README.md └── README-en_EN.md /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env","stage-0","react" 4 | ] 5 | } -------------------------------------------------------------------------------- /example1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctq123/ant-virtual-table/HEAD/example1.gif -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as VirtualTable } from './virtualTable/VirtualTable' 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # .npmignore 2 | src 3 | examples 4 | .babelrc 5 | .gitignore 6 | webpack.config.js -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | sudo: false 4 | 5 | notifications: 6 | email: 7 | - chengtianqing123@sina.cn 8 | 9 | node_js: 10 | - "4" 11 | 12 | install: 13 | - npm install -g codecov 14 | 15 | script: 16 | - codecov 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist 3 | .DS_Store 4 | 5 | # Log files 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Editor directories and files 11 | .idea 12 | .vscode 13 | *.suo 14 | *.ntvs* 15 | *.njsproj 16 | *.sln 17 | *.sw* 18 | .git 19 | -------------------------------------------------------------------------------- /examples/fixed/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ant-virtual-table 4 | 5 | 6 | 7 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /examples/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ant-virtual-table 4 | 5 | 6 | 7 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /examples/onSelectAll/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ant-virtual-table 4 | 5 | 6 | 7 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _VirtualTable = require('./virtualTable/VirtualTable'); 8 | 9 | Object.defineProperty(exports, 'VirtualTable', { 10 | enumerable: true, 11 | get: function get() { 12 | return _interopRequireDefault(_VirtualTable).default; 13 | } 14 | }); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const htmlWebpackPlugin = require('html-webpack-plugin') 3 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 4 | const hwp = new htmlWebpackPlugin({ 5 | template: path.join(__dirname, 'examples/src/index.html'), 6 | filename: './index.html' 7 | }) 8 | 9 | module.exports = { 10 | entry: path.join(__dirname, 'examples/src/index.js'), 11 | module: { 12 | rules: [{ 13 | test: /\.(js|jsx)$/, 14 | use: 'babel-loader', 15 | exclude: /node_modules/ 16 | }, { 17 | test: /\.css$/, 18 | use: ['style-loader', 'css-loader'] 19 | }] 20 | }, 21 | plugins: [hwp, new BundleAnalyzerPlugin()], 22 | resolve: { 23 | extensions: ['.js', '.jsx'] 24 | }, 25 | devtool: 'source-map', 26 | stats: { 27 | entrypoints: false, 28 | children: false, 29 | modules: false, 30 | errors: true, 31 | errorDetails: true, 32 | warnings: true 33 | }, 34 | devServer: { 35 | stats: { 36 | assets: false, 37 | entrypoints: false, 38 | children: false, 39 | modules: false, 40 | errors: true, 41 | errorDetails: true, 42 | warnings: true 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ant-virtual-table", 3 | "version": "0.1.11", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "directories": { 7 | "example": "examples" 8 | }, 9 | "scripts": { 10 | "test": "jest", 11 | "coverage": "jest --coverage", 12 | "start": "webpack-dev-server --mode development --open --port 3001", 13 | "analyzer": "webpack --mode production", 14 | "prepublish": "babel src -d dist --copy-files" 15 | }, 16 | "jest": { 17 | "setupFiles": [ 18 | "./tests/setup.js" 19 | ], 20 | "collectCoverageFrom": [ 21 | "**/src/virtualTable/**" 22 | ], 23 | "transform": { 24 | "^.+\\.(js|jsx)$": "babel-jest", 25 | "^.+\\.css$": "./tests/css-transform.js" 26 | } 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/ctq123/ant-virtual-table.git" 31 | }, 32 | "keywords": [ 33 | "virtual-table", 34 | "ant", 35 | "ant-table", 36 | "virtualied", 37 | "虚拟渲染", 38 | "虚拟表格", 39 | "antd", 40 | "virtual" 41 | ], 42 | "author": "tqcheng", 43 | "license": "ISC", 44 | "bugs": { 45 | "url": "https://github.com/ctq123/ant-virtual-table/issues" 46 | }, 47 | "homepage": "https://github.com/ctq123/ant-virtual-table#readme", 48 | "files": [ 49 | "dist" 50 | ], 51 | "devDependencies": { 52 | "babel-cli": "6.26.0", 53 | "babel-core": "6.26.3", 54 | "babel-jest": "23.6.0", 55 | "babel-loader": "7.1.5", 56 | "babel-plugin-import": "^1.12.0", 57 | "babel-preset-env": "1.7.0", 58 | "babel-preset-react": "6.24.1", 59 | "babel-preset-stage-0": "6.24.1", 60 | "css-loader": "1.0.0", 61 | "html-webpack-plugin": "3.2.0", 62 | "style-loader": "0.23.1", 63 | "webpack": "4.22.0", 64 | "webpack-bundle-analyzer": "^3.8.0", 65 | "webpack-cli": "3.1.2", 66 | "webpack-dev-server": "3.1.10" 67 | }, 68 | "dependencies": { 69 | "antd": "^3.26.2", 70 | "enzyme": "^3.1.0", 71 | "enzyme-adapter-react-16": "^1.0.2", 72 | "lodash.throttle": "^4.1.1", 73 | "react": "^16.12.0", 74 | "react-dom": "^16.12.0", 75 | "jest": "^21.2.1", 76 | "sinon": "^7.3.2" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/virtualTable/BaseTable.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import Table from 'antd/es/table'; 3 | 4 | class BaseTable extends PureComponent { 5 | constructor(props) { 6 | super(props) 7 | const { rowSelection } = props 8 | const newRowSelection = rowSelection ? { ...rowSelection } : null 9 | if (rowSelection) { 10 | const { onSelectAll, onChange } = rowSelection 11 | if (onSelectAll) { 12 | newRowSelection.onSelectAll = this.reOnSelectAll 13 | // 原方法保存下来 14 | this.onSelectAllCB = onSelectAll 15 | } 16 | if (onChange) { 17 | newRowSelection.onChange = this.reOnChange 18 | // 原方法保存下来 19 | this.onChangeCB = onChange 20 | } 21 | } 22 | this.state = { 23 | selectedRowKeys: [], 24 | newRowSelection, 25 | } 26 | } 27 | 28 | reOnSelectAll = (selected, selectedRows, changeRows) => {// 处理全选函数 29 | const { dataSource, rowKey } = this.props 30 | if (selected) { 31 | const selectedRowKeys = dataSource.map(item => item[rowKey]) 32 | this.setState({ 33 | selectedRowKeys 34 | }, () => { 35 | this.onSelectAllCB(selected, dataSource, changeRows) 36 | }) 37 | } else { 38 | this.setState({ 39 | selectedRowKeys: [] 40 | }, () => { 41 | this.onSelectAllCB(selected, [], dataSource) 42 | }) 43 | } 44 | } 45 | 46 | reOnChange = (selectedRowKeys, selectedRows) => {// 处理onChange函数 47 | if (selectedRowKeys) { 48 | this.setState({ 49 | selectedRowKeys 50 | }, () => { 51 | this.onChangeCB(selectedRowKeys, selectedRows) 52 | }) 53 | } 54 | } 55 | 56 | render() { 57 | const { renderSource, dataSource, rowSelection, ...rest } = this.props 58 | const { selectedRowKeys, newRowSelection } = this.state 59 | 60 | if (newRowSelection) { 61 | /**需要兼容用户传递进来的selectedRowKeys, 62 | * 用户selectedRowKeys优先级高于内部selectedRowKeys */ 63 | newRowSelection.selectedRowKeys = rowSelection['selectedRowKeys'] || selectedRowKeys 64 | } 65 | 66 | return ( 67 | 72 | ) 73 | } 74 | 75 | } 76 | 77 | export default BaseTable -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ant-virtual-table 2 | 3 | ## 提示:已不再维护,antd4已有替代方案 4 | 5 | [English](./README-en_EN.md) | 简体中文 6 | 7 | 这是一个ant的虚拟表格,用于解决大数据渲染时页面卡顿的问题,本组件是对ant.desigin中Table组件进行一层封装,属性完全与原组件Table保持一致 [AntDesign Table](https://ant.design/components/table-cn/),可以让你像使用普通table一样使用虚拟table。例子中处理渲染1000万条数据,页面也非常流畅。[在线demo](https://codesandbox.io/s/antdxunibiao-demo-rj5qc?file=/index.js) 8 | 9 | ## 设计说明 10 | 考虑到兼容性问题,内部通过监听Table的滚动事件判断滑动行的位置,没有采用H5新特性IntersectionObserver。因此兼容性问题是比较好的。另外组件引入loash的throttle处理抖动问题,目前没有采用raf 11 | 12 | ## React ant-virtual-table 13 | [![Build Status](https://travis-ci.org/ctq123/ant-virtual-table.svg?branch=master&foo=bar)](https://travis-ci.org/ctq123/ant-virtual-table) 14 | [![NPM version](https://img.shields.io/badge/npm-v5.7.1-green.svg?style=flat)](https://www.npmjs.com/package/ant-virtual-table) 15 | 16 | # install 17 | npm install ant-virtual-table --save-dev 18 | # Usage 19 | 20 | ## 例子 21 | ![image](https://github.com/ctq123/ant-virtual-table/blob/master/example1.gif) 22 | ``` 23 | import React, { Component, Fragment } from 'react' 24 | import { VirtualTable } from 'ant-virtual-table' 25 | import 'antd/dist/antd.css' 26 | 27 | const columns = [ 28 | { 29 | title: '序号', 30 | dataIndex: 'id', 31 | width: 100 32 | }, 33 | { 34 | title: '姓名', 35 | dataIndex: 'name', 36 | width: 150 37 | }, 38 | { 39 | title: '年龄', 40 | dataIndex: 'age', 41 | width: 100 42 | }, 43 | { 44 | title: '性别', 45 | dataIndex: 'sex', 46 | width: 100, 47 | render: (text) => { 48 | return text === 'male' ? '男' : '女' 49 | } 50 | }, 51 | { 52 | title: '地址', 53 | dataIndex: 'address', 54 | key: 'address' 55 | } 56 | ] 57 | 58 | function generateData () { 59 | const res = [] 60 | const names = ['Tom', 'Marry', 'Jack', 'Lorry', 'Tanken', 'Salla'] 61 | const sexs = ['male', 'female'] 62 | for (let i = 0; i < 10000000; i++) { 63 | let obj = { 64 | id: i, 65 | name: names[i % names.length] + i, 66 | sex: sexs[i % sexs.length], 67 | age: 15 + Math.round(10 * Math.random()), 68 | address: '浙江省杭州市西湖区华星时代广场2号楼' 69 | } 70 | res.push(obj) 71 | } 72 | return res 73 | } 74 | 75 | const dataSource = generateData() 76 | 77 | class App extends Component { 78 | render () { 79 | return ( 80 | 81 | 89 | 90 | ) 91 | } 92 | } 93 | ``` 94 | 95 | # Prop Types 96 | 97 | 为降低迁移成本,属性与antd的Table完全保持一致,暂时没有自身独特的属性 98 | 101 | 102 | # 注意 103 | **目前暂不支持内嵌tree等复杂的表单结构,任何复杂的表单结构都不建议使用,后续跟进当中...** 104 | -------------------------------------------------------------------------------- /examples/fixed/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import { render } from 'react-dom' 3 | import { VirtualTable } from '../../src' 4 | import { Pagination } from 'antd' 5 | import 'antd/dist/antd.css' 6 | 7 | const columns = [ 8 | { 9 | title: '序号', 10 | dataIndex: 'id', 11 | fixed: 'left', 12 | width: 100 13 | }, 14 | { 15 | title: '姓名', 16 | dataIndex: 'name', 17 | width: 150 18 | }, 19 | { 20 | title: '年龄', 21 | dataIndex: 'age', 22 | width: 100 23 | }, 24 | { 25 | title: '性别', 26 | dataIndex: 'sex', 27 | width: 100, 28 | render: (text) => { 29 | return text === 'male' ? '男' : '女' 30 | } 31 | }, 32 | { 33 | title: '地址', 34 | dataIndex: 'address', 35 | key: 'address' 36 | } 37 | ] 38 | 39 | function generateData (count) { 40 | const res = [] 41 | const names = ['Tom', 'Marry', 'Jack', 'Lorry', 'Tanken', 'Salla'] 42 | const sexs = ['male', 'female'] 43 | for (let i = 0; i < count; i++) { 44 | let obj = { 45 | id: i, 46 | name: names[i % names.length] + i, 47 | sex: sexs[i % sexs.length], 48 | age: 15 + Math.round(10 * Math.random()), 49 | address: '浙江省杭州市西湖区华星时代广场2号楼' 50 | } 51 | res.push(obj) 52 | } 53 | return res 54 | } 55 | 56 | const dataSource = generateData(100) 57 | 58 | class App extends Component { 59 | constructor (props) { 60 | super(props) 61 | this.state = { 62 | pageNumber: 1, 63 | objectsPerPage: 10, 64 | list: dataSource 65 | } 66 | } 67 | 68 | // 改变页面数字第几页发起的请求 69 | onPageChange (pageNumber) { 70 | this.setState({ 71 | pageNumber 72 | }) 73 | } 74 | 75 | // 改变页面显示条数发起的请求 76 | onShowSizeChange (current, objectsPerPage) { 77 | const list = dataSource.slice((current - 1) * objectsPerPage, objectsPerPage) 78 | this.setState({ 79 | list, 80 | pageNumber: current, 81 | objectsPerPage 82 | }) 83 | } 84 | 85 | render () { 86 | const { list = [] } = this.state 87 | return ( 88 | 89 |
90 | 98 | `共 ${list.length} 条`} 108 | /> 109 | 110 | ) 111 | } 112 | } 113 | 114 | render(, document.getElementById('root')) 115 | -------------------------------------------------------------------------------- /examples/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import { render } from 'react-dom' 3 | import { VirtualTable } from '../../src' 4 | import { Pagination } from 'antd' 5 | import 'antd/dist/antd.css' 6 | 7 | const columns = [ 8 | { 9 | title: '序号', 10 | dataIndex: 'id', 11 | // fixed: 'left', 12 | width: 100 13 | }, 14 | { 15 | title: '姓名', 16 | dataIndex: 'name', 17 | width: 150 18 | }, 19 | { 20 | title: '年龄', 21 | dataIndex: 'age', 22 | width: 100 23 | }, 24 | { 25 | title: '性别', 26 | dataIndex: 'sex', 27 | width: 100, 28 | render: (text) => { 29 | return text === 'male' ? '男' : '女' 30 | } 31 | }, 32 | { 33 | title: '地址', 34 | dataIndex: 'address', 35 | key: 'address' 36 | } 37 | ] 38 | 39 | function generateData (count) { 40 | const res = [] 41 | const names = ['Tom', 'Marry', 'Jack', 'Lorry', 'Tanken', 'Salla'] 42 | const sexs = ['male', 'female'] 43 | for (let i = 0; i < count; i++) { 44 | let obj = { 45 | id: i, 46 | name: names[i % names.length] + i, 47 | sex: sexs[i % sexs.length], 48 | age: 15 + Math.round(10 * Math.random()), 49 | address: '浙江省杭州市西湖区华星时代广场2号楼' 50 | } 51 | res.push(obj) 52 | } 53 | return res 54 | } 55 | 56 | const dataSource = generateData(1000000) 57 | 58 | class App extends Component { 59 | constructor (props) { 60 | super(props) 61 | this.state = { 62 | pageNumber: 1, 63 | objectsPerPage: 10, 64 | list: dataSource 65 | } 66 | } 67 | 68 | // 改变页面数字第几页发起的请求 69 | onPageChange (pageNumber) { 70 | this.setState({ 71 | pageNumber 72 | }) 73 | } 74 | 75 | // 改变页面显示条数发起的请求 76 | onShowSizeChange (current, objectsPerPage) { 77 | const list = dataSource.slice((current - 1) * objectsPerPage, objectsPerPage) 78 | this.setState({ 79 | list, 80 | pageNumber: current, 81 | objectsPerPage 82 | }) 83 | } 84 | 85 | render () { 86 | const { list = [] } = this.state 87 | return ( 88 | 89 |
90 | 98 | `共 ${list.length} 条`} 108 | /> 109 | 110 | ) 111 | } 112 | } 113 | 114 | render(, document.getElementById('root')) 115 | -------------------------------------------------------------------------------- /README-en_EN.md: -------------------------------------------------------------------------------- 1 | # ant-virtual-table 2 | 3 | ## Tip: No longer maintained, antd4 has an alternative 4 | 5 | English | [简体中文](./README.md) 6 | 7 | This is an ant.design virtual table, which is used to solve the problem of page jamming during big data rendering. This component encapsulates the Table component in ant.desigin and its properties are completely consistent with the original component Table [AntDesign Table](https://ant.design/components/table-cn/), it allows you to use a virtual table like a normal table. The example handles rendering 10 million pieces of data, and the page is very smooth. [online demo](https://codesandbox.io/s/antdxunibiao-demo-rj5qc?file=/index.js) 8 | 9 | ## Design Notes 10 | Considering the compatibility issue, the internal scrolling event of the Listening Table determines the position of the sliding line, and does not adopt the new H5 feature IntersectionObserver. Therefore the compatibility issue is better. In addition, the component introduces the loose handle of the loash to deal with the jitter problem. Currently, raf is not used. 11 | 12 | ## React ant-virtual-table 13 | [![Build Status](https://travis-ci.org/ctq123/ant-virtual-table.svg?branch=master&foo=bar)](https://travis-ci.org/ctq123/ant-virtual-table) 14 | [![NPM version](https://img.shields.io/badge/npm-v5.7.1-green.svg?style=flat)](https://www.npmjs.com/package/ant-virtual-table) 15 | 16 | # install 17 | npm install ant-virtual-table --save-dev 18 | # Usage 19 | 20 | ## demo 21 | ![image](https://github.com/ctq123/ant-virtual-table/blob/master/example1.gif) 22 | ``` 23 | import React, { Component, Fragment } from 'react' 24 | import { VirtualTable } from 'ant-virtual-table' 25 | import 'antd/dist/antd.css' 26 | 27 | const columns = [ 28 | { 29 | title: '序号', 30 | dataIndex: 'id', 31 | width: 100 32 | }, 33 | { 34 | title: '姓名', 35 | dataIndex: 'name', 36 | width: 150 37 | }, 38 | { 39 | title: '年龄', 40 | dataIndex: 'age', 41 | width: 100 42 | }, 43 | { 44 | title: '性别', 45 | dataIndex: 'sex', 46 | width: 100, 47 | render: (text) => { 48 | return text === 'male' ? '男' : '女' 49 | } 50 | }, 51 | { 52 | title: '地址', 53 | dataIndex: 'address', 54 | key: 'address' 55 | } 56 | ] 57 | 58 | function generateData () { 59 | const res = [] 60 | const names = ['Tom', 'Marry', 'Jack', 'Lorry', 'Tanken', 'Salla'] 61 | const sexs = ['male', 'female'] 62 | for (let i = 0; i < 10000000; i++) { 63 | let obj = { 64 | id: i, 65 | name: names[i % names.length] + i, 66 | sex: sexs[i % sexs.length], 67 | age: 15 + Math.round(10 * Math.random()), 68 | address: '浙江省杭州市西湖区华星时代广场2号楼' 69 | } 70 | res.push(obj) 71 | } 72 | return res 73 | } 74 | 75 | const dataSource = generateData() 76 | 77 | class App extends Component { 78 | render () { 79 | return ( 80 | 81 | 89 | 90 | ) 91 | } 92 | } 93 | ``` 94 | 95 | # Prop Types 96 | 97 | The attribute is consistent with the ant.design Table, and there are no unique attributes for the time being. 98 | -------------------------------------------------------------------------------- /examples/onSelectAll/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import { render } from 'react-dom' 3 | import { VirtualTable } from '../../src' 4 | import { Pagination } from 'antd' 5 | import 'antd/dist/antd.css' 6 | 7 | const columns = [ 8 | { 9 | title: '序号', 10 | dataIndex: 'id', 11 | // fixed: 'left', 12 | width: 100 13 | }, 14 | { 15 | title: '姓名', 16 | dataIndex: 'name', 17 | width: 150 18 | }, 19 | { 20 | title: '年龄', 21 | dataIndex: 'age', 22 | width: 100 23 | }, 24 | { 25 | title: '性别', 26 | dataIndex: 'sex', 27 | width: 100, 28 | render: (text) => { 29 | return text === 'male' ? '男' : '女' 30 | } 31 | }, 32 | { 33 | title: '地址', 34 | dataIndex: 'address', 35 | key: 'address' 36 | } 37 | ] 38 | 39 | function generateData (count) { 40 | const res = [] 41 | const names = ['Tom', 'Marry', 'Jack', 'Lorry', 'Tanken', 'Salla'] 42 | const sexs = ['male', 'female'] 43 | for (let i = 0; i < count; i++) { 44 | let obj = { 45 | id: i, 46 | name: names[i % names.length] + i, 47 | sex: sexs[i % sexs.length], 48 | age: 15 + Math.round(10 * Math.random()), 49 | address: '浙江省杭州市西湖区华星时代广场2号楼' 50 | } 51 | res.push(obj) 52 | } 53 | return res 54 | } 55 | 56 | const dataSource = generateData(20) 57 | 58 | class App extends Component { 59 | constructor (props) { 60 | super(props) 61 | this.state = { 62 | pageNumber: 1, 63 | objectsPerPage: 10, 64 | list: dataSource 65 | } 66 | } 67 | 68 | // 改变页面数字第几页发起的请求 69 | onPageChange (pageNumber) { 70 | this.setState({ 71 | pageNumber 72 | }) 73 | } 74 | 75 | // 改变页面显示条数发起的请求 76 | onShowSizeChange (current, objectsPerPage) { 77 | const list = dataSource.slice((current - 1) * objectsPerPage, objectsPerPage) 78 | this.setState({ 79 | list, 80 | pageNumber: current, 81 | objectsPerPage 82 | }) 83 | } 84 | 85 | onChange = (selectedRowKeys, selectedRows) => { 86 | console.log(`onChange selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows) 87 | } 88 | 89 | onSelect = (record, selected, selectedRows, nativeEvent) => { 90 | console.log(`onSelect record: ${record} selected: ${selected}`, 'selectedRows: ', selectedRows) 91 | } 92 | 93 | onSelectAll = (selected, selectedRows) => { 94 | console.log(`onSelectAll selected: ${selected}`, 'selectedRows: ', selectedRows) 95 | } 96 | 97 | render () { 98 | const { list = [] } = this.state 99 | const rowSelection = { 100 | onChange: this.onChange, 101 | onSelect: this.onSelect, 102 | onSelectAll: this.onSelectAll, 103 | } 104 | return ( 105 | 106 |
107 | 116 | `共 ${list.length} 条`} 126 | /> 127 | 128 | ) 129 | } 130 | } 131 | 132 | render(, document.getElementById('root')) 133 | -------------------------------------------------------------------------------- /src/virtualTable/VirtualTable.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, Fragment } from 'react' 2 | import ReactDOM from 'react-dom' 3 | import throttle from 'lodash/throttle'; 4 | import BaseTable from './BaseTable' 5 | 6 | class VirtualTable extends PureComponent { 7 | static FillNode ({ height, node, marginTop, marginBottom }) { 8 | if (node) { 9 | marginTop = marginTop || 0 10 | marginBottom = marginBottom || 0 11 | height = height || 0 12 | return ReactDOM.createPortal( 13 |
, 14 | node 15 | ) 16 | } 17 | return null 18 | } 19 | constructor (props) { 20 | super(props) 21 | this.state = { 22 | startIndex: 0, 23 | visibleRowCount: 0, 24 | thresholdCount: 40, 25 | rowHeight: 0, 26 | topBlankHeight: 0, 27 | bottomBlankHeight: 0, 28 | maxTotalHeight: 15000000 29 | } 30 | } 31 | 32 | componentDidMount () { 33 | // 普通table布局 34 | this.refScroll = ReactDOM.findDOMNode(this).getElementsByClassName('ant-table-body')[0] 35 | // 固定列的布局 36 | const fixedEles = ReactDOM.findDOMNode(this).getElementsByClassName('ant-table-body-inner') 37 | this.refFixedLeftScroll = fixedEles && fixedEles.length ? fixedEles[0] : null 38 | this.refFixedRightScroll = fixedEles && fixedEles.length > 1 ? fixedEles[1] : null 39 | 40 | this.listenEvent = throttle(this.handleScrollEvent, 50) 41 | 42 | if (this.refScroll) { 43 | this.refScroll.addEventListener('scroll', this.listenEvent) 44 | } 45 | 46 | this.createTopFillNode() 47 | this.createBottomFillNode() 48 | // 初始化设置滚动条 49 | this.setRowHeight() 50 | this.handleScrollEvent() 51 | } 52 | 53 | componentDidUpdate(prevProps) { 54 | const { dataSource } = prevProps 55 | const { dataSource: tdataSource } = this.props 56 | if (dataSource && dataSource.length != tdataSource.length) { 57 | this.refScroll.scrollTop = 0 58 | this.handleScroll(dataSource.length) 59 | } 60 | } 61 | 62 | componentWillUnmount () { 63 | if (this.refScroll) { 64 | this.refScroll.removeEventListener('scroll', this.listenEvent) 65 | } 66 | } 67 | 68 | createTopFillNode () { 69 | if (this.refScroll) { 70 | const ele = document.createElement('div') 71 | this.refScroll.insertBefore(ele, this.refScroll.firstChild) 72 | this.refTopNode = ele 73 | } 74 | if (this.refFixedLeftScroll) { 75 | const ele = document.createElement('div') 76 | this.refFixedLeftScroll.insertBefore(ele, this.refFixedLeftScroll.firstChild) 77 | this.refFixedLeftTopNode = ele 78 | } 79 | if (this.refFixedRightScroll) { 80 | const ele = document.createElement('div') 81 | this.refFixedRightScroll.insertBefore(ele, this.refFixedRightScroll.firstChild) 82 | this.refFixedRightTopNode = ele 83 | } 84 | } 85 | 86 | createBottomFillNode () { 87 | if (this.refScroll) { 88 | const ele = document.createElement('div') 89 | this.refScroll.appendChild(ele) 90 | this.refBottomNode = ele 91 | } 92 | if (this.refFixedLeftScroll) { 93 | const ele = document.createElement('div') 94 | this.refFixedLeftScroll.appendChild(ele) 95 | this.refFixedLeftBottomNode = ele 96 | } 97 | if (this.refFixedRightScroll) { 98 | const ele = document.createElement('div') 99 | this.refFixedRightScroll.appendChild(ele) 100 | this.refFixedRightBottomNode = ele 101 | } 102 | } 103 | 104 | setRowHeight () { 105 | this.refTable = this.refScroll.getElementsByTagName('table')[0] 106 | // this.refFixedLeftTable = this.refFixedLeftScroll.getElementsByTagName('table')[0] 107 | if (this.refTable) { 108 | const tr = this.refTable.getElementsByTagName('tr')[0] 109 | const rowHeight = (tr && tr.clientHeight) || 0 110 | this.state.rowHeight = rowHeight 111 | } 112 | } 113 | 114 | handleScrollEvent = (e) => { 115 | const { dataSource } = this.props 116 | this.handleScroll((dataSource || []).length) 117 | } 118 | 119 | handleScroll = (length) => { 120 | const { rowHeight, maxTotalHeight } = this.state 121 | if (rowHeight && length) { 122 | const visibleHeight = this.refScroll.clientHeight // 显示的高度 123 | const scrollTop = this.refScroll.scrollTop // 滑动的距离 124 | this.handleBlankHeight(length, rowHeight, maxTotalHeight, visibleHeight, scrollTop) 125 | } else { 126 | this.setRowHeight() 127 | } 128 | } 129 | 130 | getIndexByScrollTop(rowHeight, scrollTop) { 131 | const index = (scrollTop - scrollTop % rowHeight) / rowHeight 132 | return index 133 | } 134 | 135 | handleBlankHeight(length, rowHeight, maxTotalHeight, visibleHeight, scrollTop) { 136 | let oriRowHeight = rowHeight 137 | let totalHeight = length * rowHeight 138 | let isBigData = false 139 | if (totalHeight > maxTotalHeight) { 140 | isBigData = true 141 | totalHeight = maxTotalHeight 142 | rowHeight = totalHeight / length 143 | scrollTop = scrollTop > maxTotalHeight ? maxTotalHeight : scrollTop 144 | } 145 | if (length >= 10000) { 146 | isBigData = true 147 | } 148 | let topBlankHeight, bottomBlankHeight, startIndex, visibleRowCount 149 | startIndex = this.getIndexByScrollTop(rowHeight, scrollTop) 150 | visibleRowCount = Math.ceil(visibleHeight / oriRowHeight) 151 | topBlankHeight = rowHeight * startIndex 152 | topBlankHeight = this.getValidValue(topBlankHeight, 0, totalHeight) 153 | bottomBlankHeight = totalHeight - topBlankHeight - visibleHeight 154 | bottomBlankHeight = bottomBlankHeight > 0 ? bottomBlankHeight : 0 155 | 156 | const slideUpHeight = Math.abs(topBlankHeight - this.state.topBlankHeight) 157 | const slideDownHeight = Math.abs(bottomBlankHeight - this.state.bottomBlankHeight) 158 | 159 | if (!this.lastSlideUpHeight) { 160 | this.sameSlideHeightCount = 0 161 | this.lastSlideUpHeight = slideUpHeight 162 | } else if (this.lastSlideUpHeight === slideUpHeight) { 163 | this.sameSlideHeightCount++ 164 | } else { 165 | this.lastSlideUpHeight = slideUpHeight 166 | this.sameSlideHeightCount = 0 167 | } 168 | 169 | let isValid = slideUpHeight >= rowHeight 170 | isValid = isValid || slideDownHeight >= rowHeight 171 | isValid = isValid || startIndex === 0 172 | if (isValid) { 173 | startIndex = startIndex - 5 174 | visibleRowCount = visibleRowCount + 5 175 | this.setState({ 176 | startIndex, 177 | visibleRowCount, 178 | topBlankHeight, 179 | bottomBlankHeight 180 | }) 181 | 182 | if (isBigData && this.sameSlideHeightCount >= 1) { // 防止大数据持续滚动期间出现空白的问题 183 | this.refScroll.scrollTop = scrollTop 184 | this.sameSlideHeightCount = 0 185 | } 186 | } 187 | } 188 | 189 | getValidValue (val, min = 0, max = 40) { 190 | if (val < min) { 191 | return min 192 | } else if (val > max) { 193 | return max 194 | } 195 | return val 196 | } 197 | 198 | render () { 199 | const { dataSource, ...rest } = this.props 200 | const { topBlankHeight, bottomBlankHeight, startIndex, visibleRowCount, rowHeight, thresholdCount } = this.state 201 | const { length } = dataSource || [] 202 | let startCount = length - visibleRowCount 203 | startCount = startCount > 0 ? startCount : length 204 | let startIn = this.getValidValue(startIndex, 0, startCount) 205 | let endIn = startIndex + visibleRowCount 206 | if (!endIn) { // 初始化渲染数据 207 | endIn = length > thresholdCount ? thresholdCount : length 208 | } 209 | endIn = this.getValidValue(endIn, startIndex, length) 210 | const renderSource = (dataSource || []).slice(startIn, endIn) 211 | 212 | return ( 213 | 214 | 218 | 223 | 227 | 228 | { 229 | /**fixed 针对固定列的*/ 230 | 231 | 235 | 239 | 243 | 247 | 248 | } 249 | 250 | ) 251 | } 252 | } 253 | 254 | export default VirtualTable 255 | -------------------------------------------------------------------------------- /dist/virtualTable/VirtualTable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 10 | 11 | var _react = require('react'); 12 | 13 | var _react2 = _interopRequireDefault(_react); 14 | 15 | var _reactDom = require('react-dom'); 16 | 17 | var _reactDom2 = _interopRequireDefault(_reactDom); 18 | 19 | var _throttle = require('lodash/throttle'); 20 | 21 | var _throttle2 = _interopRequireDefault(_throttle); 22 | 23 | var _BaseTable = require('./BaseTable'); 24 | 25 | var _BaseTable2 = _interopRequireDefault(_BaseTable); 26 | 27 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 28 | 29 | function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } 30 | 31 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 32 | 33 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 34 | 35 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 36 | 37 | var VirtualTable = function (_PureComponent) { 38 | _inherits(VirtualTable, _PureComponent); 39 | 40 | _createClass(VirtualTable, null, [{ 41 | key: 'FillNode', 42 | value: function FillNode(_ref) { 43 | var height = _ref.height, 44 | node = _ref.node, 45 | marginTop = _ref.marginTop, 46 | marginBottom = _ref.marginBottom; 47 | 48 | if (node) { 49 | marginTop = marginTop || 0; 50 | marginBottom = marginBottom || 0; 51 | height = height || 0; 52 | return _reactDom2.default.createPortal(_react2.default.createElement('div', { style: { height: height + 'px', marginTop: marginTop + 'px', marginBottom: marginBottom + 'px' } }), node); 53 | } 54 | return null; 55 | } 56 | }]); 57 | 58 | function VirtualTable(props) { 59 | _classCallCheck(this, VirtualTable); 60 | 61 | var _this = _possibleConstructorReturn(this, (VirtualTable.__proto__ || Object.getPrototypeOf(VirtualTable)).call(this, props)); 62 | 63 | _this.handleScrollEvent = function (e) { 64 | var dataSource = _this.props.dataSource; 65 | 66 | _this.handleScroll((dataSource || []).length); 67 | }; 68 | 69 | _this.handleScroll = function (length) { 70 | var _this$state = _this.state, 71 | rowHeight = _this$state.rowHeight, 72 | maxTotalHeight = _this$state.maxTotalHeight; 73 | 74 | if (rowHeight && length) { 75 | var visibleHeight = _this.refScroll.clientHeight; // 显示的高度 76 | var scrollTop = _this.refScroll.scrollTop; // 滑动的距离 77 | _this.handleBlankHeight(length, rowHeight, maxTotalHeight, visibleHeight, scrollTop); 78 | } else { 79 | _this.setRowHeight(); 80 | } 81 | }; 82 | 83 | _this.state = { 84 | startIndex: 0, 85 | visibleRowCount: 0, 86 | thresholdCount: 40, 87 | rowHeight: 0, 88 | topBlankHeight: 0, 89 | bottomBlankHeight: 0, 90 | maxTotalHeight: 15000000 91 | }; 92 | return _this; 93 | } 94 | 95 | _createClass(VirtualTable, [{ 96 | key: 'componentDidMount', 97 | value: function componentDidMount() { 98 | // 普通table布局 99 | this.refScroll = _reactDom2.default.findDOMNode(this).getElementsByClassName('ant-table-body')[0]; 100 | // 固定列的布局 101 | var fixedEles = _reactDom2.default.findDOMNode(this).getElementsByClassName('ant-table-body-inner'); 102 | this.refFixedLeftScroll = fixedEles && fixedEles.length ? fixedEles[0] : null; 103 | this.refFixedRightScroll = fixedEles && fixedEles.length > 1 ? fixedEles[1] : null; 104 | 105 | this.listenEvent = (0, _throttle2.default)(this.handleScrollEvent, 50); 106 | 107 | if (this.refScroll) { 108 | this.refScroll.addEventListener('scroll', this.listenEvent); 109 | } 110 | 111 | this.createTopFillNode(); 112 | this.createBottomFillNode(); 113 | // 初始化设置滚动条 114 | this.setRowHeight(); 115 | this.handleScrollEvent(); 116 | } 117 | }, { 118 | key: 'componentDidUpdate', 119 | value: function componentDidUpdate(prevProps) { 120 | var dataSource = prevProps.dataSource; 121 | var tdataSource = this.props.dataSource; 122 | 123 | if (dataSource && dataSource.length != tdataSource.length) { 124 | this.refScroll.scrollTop = 0; 125 | this.handleScroll(dataSource.length); 126 | } 127 | } 128 | }, { 129 | key: 'componentWillUnmount', 130 | value: function componentWillUnmount() { 131 | if (this.refScroll) { 132 | this.refScroll.removeEventListener('scroll', this.listenEvent); 133 | } 134 | } 135 | }, { 136 | key: 'createTopFillNode', 137 | value: function createTopFillNode() { 138 | if (this.refScroll) { 139 | var ele = document.createElement('div'); 140 | this.refScroll.insertBefore(ele, this.refScroll.firstChild); 141 | this.refTopNode = ele; 142 | } 143 | if (this.refFixedLeftScroll) { 144 | var _ele = document.createElement('div'); 145 | this.refFixedLeftScroll.insertBefore(_ele, this.refFixedLeftScroll.firstChild); 146 | this.refFixedLeftTopNode = _ele; 147 | } 148 | if (this.refFixedRightScroll) { 149 | var _ele2 = document.createElement('div'); 150 | this.refFixedRightScroll.insertBefore(_ele2, this.refFixedRightScroll.firstChild); 151 | this.refFixedRightTopNode = _ele2; 152 | } 153 | } 154 | }, { 155 | key: 'createBottomFillNode', 156 | value: function createBottomFillNode() { 157 | if (this.refScroll) { 158 | var ele = document.createElement('div'); 159 | this.refScroll.appendChild(ele); 160 | this.refBottomNode = ele; 161 | } 162 | if (this.refFixedLeftScroll) { 163 | var _ele3 = document.createElement('div'); 164 | this.refFixedLeftScroll.appendChild(_ele3); 165 | this.refFixedLeftBottomNode = _ele3; 166 | } 167 | if (this.refFixedRightScroll) { 168 | var _ele4 = document.createElement('div'); 169 | this.refFixedRightScroll.appendChild(_ele4); 170 | this.refFixedRightBottomNode = _ele4; 171 | } 172 | } 173 | }, { 174 | key: 'setRowHeight', 175 | value: function setRowHeight() { 176 | this.refTable = this.refScroll.getElementsByTagName('table')[0]; 177 | // this.refFixedLeftTable = this.refFixedLeftScroll.getElementsByTagName('table')[0] 178 | if (this.refTable) { 179 | var tr = this.refTable.getElementsByTagName('tr')[0]; 180 | var rowHeight = tr && tr.clientHeight || 0; 181 | this.state.rowHeight = rowHeight; 182 | } 183 | } 184 | }, { 185 | key: 'getIndexByScrollTop', 186 | value: function getIndexByScrollTop(rowHeight, scrollTop) { 187 | var index = (scrollTop - scrollTop % rowHeight) / rowHeight; 188 | return index; 189 | } 190 | }, { 191 | key: 'handleBlankHeight', 192 | value: function handleBlankHeight(length, rowHeight, maxTotalHeight, visibleHeight, scrollTop) { 193 | var oriRowHeight = rowHeight; 194 | var totalHeight = length * rowHeight; 195 | var isBigData = false; 196 | if (totalHeight > maxTotalHeight) { 197 | isBigData = true; 198 | totalHeight = maxTotalHeight; 199 | rowHeight = totalHeight / length; 200 | scrollTop = scrollTop > maxTotalHeight ? maxTotalHeight : scrollTop; 201 | } 202 | if (length >= 10000) { 203 | isBigData = true; 204 | } 205 | var topBlankHeight = void 0, 206 | bottomBlankHeight = void 0, 207 | startIndex = void 0, 208 | visibleRowCount = void 0; 209 | startIndex = this.getIndexByScrollTop(rowHeight, scrollTop); 210 | visibleRowCount = Math.ceil(visibleHeight / oriRowHeight); 211 | topBlankHeight = rowHeight * startIndex; 212 | topBlankHeight = this.getValidValue(topBlankHeight, 0, totalHeight); 213 | bottomBlankHeight = totalHeight - topBlankHeight - visibleHeight; 214 | bottomBlankHeight = bottomBlankHeight > 0 ? bottomBlankHeight : 0; 215 | 216 | var slideUpHeight = Math.abs(topBlankHeight - this.state.topBlankHeight); 217 | var slideDownHeight = Math.abs(bottomBlankHeight - this.state.bottomBlankHeight); 218 | 219 | if (!this.lastSlideUpHeight) { 220 | this.sameSlideHeightCount = 0; 221 | this.lastSlideUpHeight = slideUpHeight; 222 | } else if (this.lastSlideUpHeight === slideUpHeight) { 223 | this.sameSlideHeightCount++; 224 | } else { 225 | this.lastSlideUpHeight = slideUpHeight; 226 | this.sameSlideHeightCount = 0; 227 | } 228 | 229 | var isValid = slideUpHeight >= rowHeight; 230 | isValid = isValid || slideDownHeight >= rowHeight; 231 | isValid = isValid || startIndex === 0; 232 | if (isValid) { 233 | startIndex = startIndex - 5; 234 | visibleRowCount = visibleRowCount + 5; 235 | this.setState({ 236 | startIndex: startIndex, 237 | visibleRowCount: visibleRowCount, 238 | topBlankHeight: topBlankHeight, 239 | bottomBlankHeight: bottomBlankHeight 240 | }); 241 | 242 | if (isBigData && this.sameSlideHeightCount >= 1) { 243 | // 防止大数据持续滚动期间出现空白的问题 244 | this.refScroll.scrollTop = scrollTop; 245 | this.sameSlideHeightCount = 0; 246 | } 247 | } 248 | } 249 | }, { 250 | key: 'getValidValue', 251 | value: function getValidValue(val) { 252 | var min = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; 253 | var max = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 40; 254 | 255 | if (val < min) { 256 | return min; 257 | } else if (val > max) { 258 | return max; 259 | } 260 | return val; 261 | } 262 | }, { 263 | key: 'render', 264 | value: function render() { 265 | var _props = this.props, 266 | dataSource = _props.dataSource, 267 | rest = _objectWithoutProperties(_props, ['dataSource']); 268 | 269 | var _state = this.state, 270 | topBlankHeight = _state.topBlankHeight, 271 | bottomBlankHeight = _state.bottomBlankHeight, 272 | startIndex = _state.startIndex, 273 | visibleRowCount = _state.visibleRowCount, 274 | rowHeight = _state.rowHeight, 275 | thresholdCount = _state.thresholdCount; 276 | 277 | var _ref2 = dataSource || [], 278 | length = _ref2.length; 279 | 280 | var startCount = length - visibleRowCount; 281 | startCount = startCount > 0 ? startCount : length; 282 | var startIn = this.getValidValue(startIndex, 0, startCount); 283 | var endIn = startIndex + visibleRowCount; 284 | if (!endIn) { 285 | // 初始化渲染数据 286 | endIn = length > thresholdCount ? thresholdCount : length; 287 | } 288 | endIn = this.getValidValue(endIn, startIndex, length); 289 | var renderSource = (dataSource || []).slice(startIn, endIn); 290 | 291 | return _react2.default.createElement( 292 | _react.Fragment, 293 | null, 294 | _react2.default.createElement(VirtualTable.FillNode, { 295 | height: topBlankHeight, 296 | node: this.refTopNode 297 | }), 298 | _react2.default.createElement(_BaseTable2.default, _extends({}, rest, { 299 | dataSource: dataSource, 300 | renderSource: renderSource 301 | })), 302 | _react2.default.createElement(VirtualTable.FillNode, { 303 | height: bottomBlankHeight, 304 | node: this.refBottomNode 305 | }), 306 | 307 | /**fixed 针对固定列的*/ 308 | _react2.default.createElement( 309 | _react.Fragment, 310 | null, 311 | _react2.default.createElement(VirtualTable.FillNode, { 312 | height: topBlankHeight, 313 | node: this.refFixedLeftTopNode 314 | }), 315 | _react2.default.createElement(VirtualTable.FillNode, { 316 | height: bottomBlankHeight, 317 | node: this.refFixedLeftBottomNode 318 | }), 319 | _react2.default.createElement(VirtualTable.FillNode, { 320 | height: topBlankHeight, 321 | node: this.refFixedRightTopNode 322 | }), 323 | _react2.default.createElement(VirtualTable.FillNode, { 324 | height: bottomBlankHeight, 325 | node: this.refFixedRightBottomNode 326 | }) 327 | ) 328 | ); 329 | } 330 | }]); 331 | 332 | return VirtualTable; 333 | }(_react.PureComponent); 334 | 335 | exports.default = VirtualTable; --------------------------------------------------------------------------------