├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .umirc.ts ├── LICENSE ├── README.md ├── __mocks__ ├── fileMock.js └── styleMock.js ├── babel.config.js ├── docs ├── demo │ ├── AsyncTable.md │ ├── DragHandleRow.md │ ├── DragRow.md │ ├── EasyTable.md │ ├── JumpTreeTable.md │ ├── PaginationTable.md │ ├── SinglePageLoading.md │ ├── TabsDemo.md │ └── TreeTable.md ├── examples │ ├── AsyncTable.tsx │ ├── DragHandleRow.tsx │ ├── DragRow.tsx │ ├── EasyTable.tsx │ ├── JumpTreeTable.tsx │ ├── PaginationTable.tsx │ ├── SinglePageLoading.tsx │ ├── TabsDemo.tsx │ └── TreeTable.tsx ├── index.md └── update.md ├── jest.config.js ├── package.json ├── rollup.config.js ├── src ├── index.tsx └── style.css ├── tests └── basic.test.js ├── tsconfig.json └── update.md /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2020: true, 5 | jest: true, 6 | }, 7 | globals: { 8 | JSX: true, 9 | jsdom: true, 10 | }, 11 | extends: ['plugin:react/recommended', 'airbnb'], 12 | parser: '@typescript-eslint/parser', 13 | parserOptions: { 14 | ecmaFeatures: { 15 | jsx: true, 16 | }, 17 | ecmaVersion: 11, 18 | sourceType: 'module', 19 | }, 20 | plugins: ['react', 'react-hooks', '@typescript-eslint'], 21 | rules: { 22 | 'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks 23 | 'react-hooks/exhaustive-deps': 'warn', 24 | 'react/jsx-filename-extension': [ 25 | 2, 26 | { extensions: ['.js', '.jsx', '.ts', '.tsx'] }, 27 | ], 28 | 'react/jsx-props-no-spreading': 'off', 29 | 'no-use-before-define': 'off', 30 | camelcase: 'off', 31 | 'no-bitwise': 'off', 32 | 'no-underscore-dangle': 'off', 33 | 'jsx-a11y/anchor-is-valid': 'off', 34 | 'jsx-a11y/no-static-element-interactions': 'off', 35 | 'jsx-a11y/click-events-have-key-events': 'off', 36 | 'import/extensions': 'off', 37 | 'import/no-unresolved': 'off', 38 | 'import/no-extraneous-dependencies': 'off', 39 | '@typescript-eslint/no-use-before-define': ['error'], 40 | indent: 'off', 41 | 'react/jsx-indent': 'off', 42 | semi: 'off', 43 | 'react/jsx-indent-props': 'off', 44 | 'implicit-arrow-linebreak': 'off', 45 | 'comma-dangle': 'off', 46 | 'object-curly-newline': 'off', 47 | 'operator-linebreak': 'off', 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | build 13 | dist 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | yarn.lock 26 | package-lock.json 27 | 28 | # VisualStudioCode 29 | .vscode/* 30 | 31 | # Jetbrains 32 | .idea/* 33 | .umi -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | update.md -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "eslintIntegration": true, 3 | "stylelintIntegration": true, 4 | "tabWidth": 4, 5 | "singleQuote": true, 6 | "semi": false 7 | } 8 | -------------------------------------------------------------------------------- /.umirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'dumi'; 2 | 3 | export default defineConfig({ 4 | title: 'virtuallist-antd', 5 | favicon: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4', 6 | logo: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4', 7 | outputPath: 'docs-dist', 8 | // more config: https://d.umijs.org/config 9 | }); 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present crawler-django 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # virtuallist-antd 2 | 3 | > 4 | 5 | [![NPM](https://img.shields.io/npm/v/virtuallist-antd.svg)](https://www.npmjs.com/package/virtuallist-antd) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 6 | 7 | > 仓库(github): https://github.com/crawler-django/virtuallist-antd 8 | 9 | > 版本更新记录(update detail): https://github.com/crawler-django/virtuallist-antd/blob/master/update.md 10 | 11 | virtualList for antd-table, 实现 antd-table 的虚拟列表, antd-table 无限滚动, infinite scrolling for antd-table. 支持 antd 树形表格, antd 版本要在 4.17.0 及以上, virtuallist-antd 要再 0.6.3 及以上. (support tree data, after antd v4.17.0, after virtuallist-antd v0.6.3) 12 | 13 | - 支持antd4, antd5.(4.x / 5.x ---- v0.3.0 后, 3.x ---- v0.2.8. antd3对应的版本不再更新) 14 | 15 | (support antdv4, antdv5 --- ^v0.3.0, 3.x --- v0.2.8. 3.x not updated) 16 | 17 | - 你可以像平常一样在 columns 里使用 fixed 18 | 19 | (u can use fixed as usual) 20 | 21 | - 支持进行条件搜索 变更数据. 22 | 23 | (support search data as usual) 24 | 25 | - 目前用了节流 - 60ms 在滚动的时候刷新窗口 26 | 27 | (use throttle, 60ms) 28 | 29 | - 支持分页, calc(). 30 | 31 | (support pagination, support scrolly for calc()) 32 | 33 | - 只支持纵向虚拟列表. 34 | 35 | (only support vertical virtuallist) 36 | 37 | - 此组件会计算第一行的高度, 并且以第一行的高度为准来固定每行的高度. 组件有自带的 css,  会使每行的 td 不会换行. 38 | 39 | (this component will calculate first line's height and amend following each line's height based on it. It has its own css, which prevents each line's TD from wrapping (td do not wrap)) 40 | 41 | ## example 42 | 43 | - [简单的例子(easy example)](https://codesandbox.io/s/festive-worker-wc5wp) 44 | - [简单的分页例子(easy pagination example)](https://codesandbox.io/s/gracious-resonance-tmw44) 45 | - [简单的 resize 例子(easy resize columns example)](https://codesandbox.io/s/vibrant-darkness-kvt56?file=/index.js) 46 | - [简单的单页无限加载例子(easy infinite load data on single page that example)](https://codesandbox.io/s/reachend-wuxianjiazaixunigundong-y9nhd) 47 | - [简单的 scrollTo 例子(easy scrollTo example)](https://codesandbox.io/s/scrollto-jx10t) 48 | - [简单的树形表格例子(easy tree table example)](https://codesandbox.io/s/reachend-wuxianjiazaixunigundong-forked-63iom?file=/src/index.tsx) 49 | 50 | ## complex example 51 | 52 | - [拖拽行(drag row)](https://codesandbox.io/s/drag-row-1fjg4?file=/index.js) 53 | - [拖拽手柄列(drag row in handle-icon)](https://codesandbox.io/s/tuozhuaishoubinglie-antd4156-forked-1d6z1?file=/index.js) 54 | - [编辑列(edit cell)](https://codesandbox.io/s/editable-example-3656ln?file=/src/App.js) 55 | - [异步列表/onListRender 例子(async Table, onListRender demo)](https://codesandbox.io/s/shu-xing-biao-ge-forked-4lt6u?file=/src/index.tsx) 56 | 57 | ## Install 58 | 59 | ```bash 60 | npm install --save virtuallist-antd 61 | ``` 62 | 63 | ## Usage 64 | 65 | ```tsx 66 | import React, { useMemo } from 'react' 67 | import ReactDom from 'react-dom' 68 | 69 | import { VList } from 'virtuallist-antd' 70 | import { Table } from 'antd' 71 | 72 | function Example(): JSX.Element { 73 | const dataSource = [...] 74 | const columns = [...] 75 | const rowkey = 'xxx' 76 | 77 | const vComponents = useMemo(() => { 78 | // 使用VList 即可有虚拟列表的效果 79 | return VList({ 80 | height: 1000 // 此值和scrollY值相同. 必传. (required). same value for scrolly 81 | }) 82 | }, []) 83 | 84 | return ( 85 | 94 | ) 95 | } 96 | 97 | ReactDom.render(, dom) 98 | 99 | ``` 100 | 101 | ## VList 102 | 103 | ```tsx 104 | VList({ 105 | height: number | string, // (必填) 对应scrollY. 106 | onReachEnd: () => void, // (可选) 滚动条滚到底部触发api. (scrollbar to the end) 107 | onScroll: () => void, // (可选) 滚动时触发的api. (triggered by scrolling) 108 | vid: string, // (可选, 如果同一页面存在多个虚拟表格时必填.) 唯一标识. (unique vid, required when exist more vitual table on a page) 109 | resetTopWhenDataChange: boolean, // 默认为true. 是否数据变更后重置滚动条 (default true, Whether to reset scrollTop when data changes) 110 | }) 111 | 112 | VList returns: { 113 | table: VTable, 114 | body: { 115 | wrapper: VWrapper, 116 | row: VRow, 117 | cell: VCell, 118 | } 119 | } 120 | ``` 121 | 122 | ## api 123 | 124 | ```tsx 125 | import { scrollTo } from 'virtuallist-antd' 126 | 127 | // scrollTo 128 | scrollTo({ 129 | row: number, // 行数. (row number) 130 | y: number, // y偏移量. (offset Y) 131 | vid: string, // 对应VList的vid. (same as VList vid) 132 | }) 133 | ``` 134 | 135 | ## License 136 | 137 | MIT © [crawler-django](https://github.com/crawler-django) 138 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: true } }], 4 | '@babel/preset-react', 5 | '@babel/preset-typescript', 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /docs/demo/AsyncTable.md: -------------------------------------------------------------------------------- 1 | ## AsyncTable 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/demo/DragHandleRow.md: -------------------------------------------------------------------------------- 1 | ## DragHandleRow 2 | 3 | -------------------------------------------------------------------------------- /docs/demo/DragRow.md: -------------------------------------------------------------------------------- 1 | ## DragRow 2 | 3 | -------------------------------------------------------------------------------- /docs/demo/EasyTable.md: -------------------------------------------------------------------------------- 1 | ## EasyTable 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/demo/JumpTreeTable.md: -------------------------------------------------------------------------------- 1 | ## JumpTreeTable 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/demo/PaginationTable.md: -------------------------------------------------------------------------------- 1 | ## PaginationTable 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/demo/SinglePageLoading.md: -------------------------------------------------------------------------------- 1 | ## SinglePageLoading 2 | 3 | -------------------------------------------------------------------------------- /docs/demo/TabsDemo.md: -------------------------------------------------------------------------------- 1 | ## TabsDemo 2 | 3 | -------------------------------------------------------------------------------- /docs/demo/TreeTable.md: -------------------------------------------------------------------------------- 1 | ## TreeTable 2 | 3 | -------------------------------------------------------------------------------- /docs/examples/AsyncTable.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useRef, useEffect, useCallback } from 'react' 2 | import { Skeleton, Table, Button } from 'antd' 3 | import { VList } from '../../src/index' 4 | 5 | import 'antd/dist/antd.css' 6 | 7 | const generateData = () => { 8 | const temp = [] 9 | 10 | for (let i = 0; i < 300; i += 1) { 11 | temp.push({ 12 | a: i, 13 | b: '233', 14 | c: null, 15 | }) 16 | } 17 | 18 | return temp 19 | } 20 | 21 | // const initData = generateData() 22 | 23 | function AsyncTable() { 24 | const [data, setData] = useState(generateData()) 25 | 26 | const refreshFlag = useRef(false) 27 | 28 | const handleClick = (record, e) => { 29 | e.preventDefault() 30 | } 31 | 32 | const lastListRenderInfo = useRef({ start: -1, renderLen: -1 }) 33 | 34 | // 防抖函数 35 | // const useDebounce = (fn, delay) => { 36 | // const { current } = useRef({ fn, timer: null }) 37 | // useEffect(() => { 38 | // current.fn = fn 39 | // }, [fn]) 40 | 41 | // return useCallback(function f(...args) { 42 | // if (current.timer) { 43 | // clearTimeout(current.timer) 44 | // } 45 | // current.timer = setTimeout(() => { 46 | // current.fn.call(this, ...args) 47 | // }, delay) 48 | // }, []) 49 | // } 50 | 51 | const onListRender = useCallback((listInfo) => { 52 | const { start, renderLen } = listInfo 53 | 54 | const lastInfo = lastListRenderInfo?.current 55 | 56 | if ( 57 | start !== lastInfo?.start || 58 | renderLen !== lastInfo?.renderLen || 59 | refreshFlag.current === true 60 | ) { 61 | lastListRenderInfo.current = { start, renderLen } 62 | setData((pre) => { 63 | const currentData = pre.slice(start, start + renderLen) 64 | 65 | currentData.forEach((item, index) => { 66 | item.c = `asyncData${start + index + 1}` 67 | }) 68 | 69 | const newData = JSON.parse(JSON.stringify(pre)) 70 | 71 | return newData 72 | }) 73 | 74 | refreshFlag.current = false 75 | } 76 | 77 | // const currentData = data.slice(start, start + renderLen) 78 | // currentData.forEach((item, index) => { 79 | // item.c = `asyncData${start + index + 1}` 80 | // }) 81 | // const newData = JSON.parse(JSON.stringify(data)) 82 | // setData(newData) 83 | }, []) 84 | 85 | const columns = [ 86 | { 87 | title: 'title1', 88 | dataIndex: 'a', 89 | key: 'a', 90 | width: 150, 91 | }, 92 | { 93 | title: 'title2', 94 | dataIndex: 'b', 95 | key: 'b', 96 | width: 200, 97 | }, 98 | { 99 | title: 'title3', 100 | dataIndex: 'c', 101 | key: 'c', 102 | width: 200, 103 | render: (t) => t || , 104 | }, 105 | { 106 | title: 'Operations', 107 | dataIndex: '', 108 | width: 200, 109 | key: 'x', 110 | render: (text, record) => ( 111 | handleClick(record, e)}> 112 | click {record.a} 113 | 114 | ), 115 | }, 116 | ] 117 | 118 | const handleSearchBtnClick = useCallback(() => { 119 | setData(generateData()) 120 | refreshFlag.current = true 121 | }, []) 122 | 123 | const vComponent = useMemo( 124 | () => 125 | VList({ 126 | height: 500, 127 | // vid: 'asyncTable', 128 | resetTopWhenDataChange: false, 129 | onListRender, 130 | }), 131 | [onListRender] 132 | ) 133 | 134 | return ( 135 |
136 | 137 |

async table

138 |
record.a} 142 | pagination={false} 143 | scroll={{ y: 500, x: '100%' }} 144 | components={vComponent} 145 | /> 146 | 147 | ) 148 | } 149 | 150 | export default AsyncTable 151 | -------------------------------------------------------------------------------- /docs/examples/DragHandleRow.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo, useState } from 'react' 2 | import { Table } from 'antd' 3 | import { 4 | sortableContainer, 5 | sortableElement, 6 | sortableHandle, 7 | } from 'react-sortable-hoc' 8 | import { MenuOutlined } from '@ant-design/icons' 9 | import { arrayMoveImmutable } from 'array-move' 10 | import { VList } from '../../src/index' 11 | 12 | const vlistComponents = VList({ height: 500, resetTopWhenDataChange: false }) 13 | const VRow = vlistComponents.body.row 14 | const VWrapper = vlistComponents.body.wrapper 15 | const DragHandle = sortableHandle(() => ( 16 | 17 | )) 18 | 19 | const SortableItem = sortableElement((props) => ) 20 | const SortableContainer = sortableContainer((props) => ) 21 | 22 | const columns = [ 23 | { 24 | title: 'Sort', 25 | dataIndex: 'sort', 26 | width: 100, 27 | className: 'drag-visible', 28 | render: () => , 29 | }, 30 | { 31 | title: 'Name', 32 | dataIndex: 'name', 33 | className: 'drag-visible', 34 | }, 35 | { 36 | title: 'Age', 37 | dataIndex: 'age', 38 | }, 39 | { 40 | title: 'Address', 41 | dataIndex: 'address', 42 | }, 43 | ] 44 | 45 | const generateData = () => { 46 | const temp = [] 47 | 48 | for (let i = 0; i < 100; i += 1) { 49 | temp.push({ 50 | key: `${i + 1}`, 51 | name: `John Brown${i}`, 52 | age: 32, 53 | address: 'New York No. 1 Lake Park', 54 | index: i, 55 | }) 56 | } 57 | 58 | return temp 59 | } 60 | 61 | const data = generateData() 62 | 63 | class SortableTable extends React.Component { 64 | state = { 65 | dataSource: data, 66 | } 67 | 68 | onSortEnd = ({ oldIndex, newIndex }) => { 69 | const { dataSource } = this.state 70 | if (oldIndex !== newIndex) { 71 | const newData = arrayMoveImmutable( 72 | [].concat(dataSource), 73 | oldIndex, 74 | newIndex 75 | ).filter((el) => !!el) 76 | console.log('Sorted items: ', newData) 77 | this.setState({ dataSource: newData }) 78 | } 79 | } 80 | 81 | DraggableContainer = (props) => ( 82 | 89 | ) 90 | 91 | DraggableBodyRow = ({ className, style, ...restProps }) => { 92 | const { dataSource } = this.state 93 | // function findIndex base on Table rowKey props and should always be a right array index 94 | const index = dataSource.findIndex( 95 | (x) => x.index === restProps['data-row-key'] 96 | ) 97 | return 98 | } 99 | 100 | render() { 101 | const { dataSource } = this.state 102 | 103 | return ( 104 |
119 | ) 120 | } 121 | } 122 | 123 | export default SortableTable 124 | -------------------------------------------------------------------------------- /docs/examples/DragRow.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useState, 3 | useMemo, 4 | useCallback, 5 | useEffect, 6 | useRef, 7 | } from 'react'; 8 | import { Table } from 'antd'; 9 | import { Resizable } from 'react-resizable'; 10 | import { DndProvider, useDrag, useDrop } from 'react-dnd'; 11 | import { HTML5Backend } from 'react-dnd-html5-backend'; 12 | import update from 'immutability-helper'; 13 | import { VList } from '../../src/index'; 14 | import 'antd/dist/antd.css'; 15 | 16 | const type = 'DragableBodyRow'; 17 | 18 | const vlistComponent = VList({ 19 | height: 500, 20 | resetTopWhenDataChange: false, 21 | }); 22 | 23 | const generateData = () => { 24 | const arr = []; 25 | 26 | for (let i = 0; i < 100; i += 1) { 27 | arr.push({ 28 | key: i, 29 | date: '天道轮回'.repeat(Math.floor(Math.random() * 10)), 30 | amount: 120, 31 | type: 'income', 32 | note: 'transfer', 33 | }); 34 | } 35 | 36 | return arr; 37 | }; 38 | 39 | const DragableBodyRow = (props: any) => { 40 | const { 41 | index, moveRow, className, style, ...restProps 42 | } = props; 43 | const ref = useRef(); 44 | const [{ isOver, dropClassName }, drop] = useDrop({ 45 | accept: type, 46 | collect: (monitor: any) => { 47 | const { index: dragIndex } = monitor.getItem() || {}; 48 | if (dragIndex === index) { 49 | return {}; 50 | } 51 | return { 52 | isOver: monitor.isOver(), 53 | dropClassName: dragIndex < index ? ' drop-over-downward' : ' drop-over-upward', 54 | }; 55 | }, 56 | drop: (item: any) => { 57 | moveRow(item.index, index); 58 | }, 59 | }); 60 | const [, drag] = useDrag({ 61 | type, 62 | item: { index }, 63 | collect: (monitor) => ({ 64 | isDragging: monitor.isDragging(), 65 | }), 66 | }); 67 | 68 | useEffect(() => { 69 | drop(drag(ref)); 70 | }, [drag, drop]); 71 | 72 | const components = useMemo(() => vlistComponent.body.row, []); 73 | 74 | const tempProps = useMemo(() => ({ 75 | ref, 76 | className: `${className}${isOver ? dropClassName : ''}`, 77 | style: { cursor: 'move', ...style }, 78 | ...restProps, 79 | }), [className, dropClassName, restProps, style, isOver]); 80 | 81 | return ( 82 | <> 83 | {' '} 84 | {components(tempProps, ref)} 85 | {' '} 86 | 87 | ); 88 | }; 89 | 90 | const ResizableTitle = (props: any) => { 91 | const { onResize, width, ...restProps } = props; 92 | 93 | if (!width) { 94 | return
; 95 | } 96 | 97 | return ( 98 | { 105 | e.stopPropagation(); 106 | }} 107 | /> 108 | )} 109 | onResize={onResize} 110 | draggableOpts={{ enableUserSelectHack: false }} 111 | > 112 | 116 | 117 | ); 118 | }; 119 | 120 | function DragRow() { 121 | const [columns, setColumns] = useState(() => [ 122 | { 123 | title: '序号', 124 | key: 'id', 125 | width: 75, 126 | fixed: 'left', 127 | render(text, record, index) { 128 | return index + 1; 129 | }, 130 | }, 131 | { 132 | title: 'Date', 133 | dataIndex: 'date', 134 | width: 300, 135 | }, 136 | { 137 | title: 'Amount', 138 | dataIndex: 'amount', 139 | width: 100, 140 | sorter: (a, b) => a.amount - b.amount, 141 | }, 142 | { 143 | title: 'Type', 144 | dataIndex: 'type', 145 | width: 100, 146 | }, 147 | { 148 | title: 'Note', 149 | dataIndex: 'note', 150 | width: 100, 151 | }, 152 | { 153 | title: 'Note', 154 | width: 200, 155 | render: () => 'aaa', 156 | }, 157 | { 158 | title: 'Note', 159 | width: 200, 160 | render: () => 'aaa', 161 | }, 162 | { 163 | title: 'Action', 164 | dataIndex: 'action', 165 | width: 200, 166 | fixed: 'right', 167 | render: () => Delete, 168 | }, 169 | ].map((col, index) => ({ 170 | ...col, 171 | onHeaderCell: (column) => ({ 172 | width: column.width, 173 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 174 | onResize: handleResize(index), 175 | }), 176 | }))); 177 | 178 | const handleResize = useCallback((index) => (e, { size }) => { 179 | setColumns((pre) => { 180 | const temp = [...pre]; 181 | temp[index] = { 182 | ...temp[index], 183 | width: size.width, 184 | }; 185 | 186 | return temp; 187 | }); 188 | }, []); 189 | 190 | const components = useMemo(() => ({ 191 | ...vlistComponent, 192 | body: { 193 | ...vlistComponent.body, 194 | row: DragableBodyRow, 195 | }, 196 | header: { 197 | cell: ResizableTitle, 198 | }, 199 | }), []); 200 | 201 | const [data, setData] = useState(generateData()); 202 | 203 | const moveRow = useCallback( 204 | (dragIndex, hoverIndex) => { 205 | const dragRow = data[dragIndex]; 206 | setData( 207 | update(data, { 208 | $splice: [ 209 | [dragIndex, 1], 210 | [hoverIndex, 0, dragRow], 211 | ], 212 | }), 213 | ); 214 | }, 215 | [data], 216 | ); 217 | 218 | return ( 219 |
220 | 221 | ({ 229 | index, 230 | moveRow, 231 | } as any)} 232 | sticky 233 | /> 234 | 235 | 236 | ); 237 | } 238 | 239 | export default DragRow; 240 | -------------------------------------------------------------------------------- /docs/examples/EasyTable.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useMemo } from 'react' 2 | import { Table } from 'antd' 3 | import { VList } from '../../src/index' 4 | import 'antd/dist/antd.css' 5 | 6 | const generateData = () => { 7 | const tempDataSource = [] 8 | for (let i = 0; i < 11; i += 1) { 9 | tempDataSource.push({ 10 | company_name: `aaa${i} 富士山下的你好美 你知道吗`, 11 | company_name1: `aaa${i} index index index index`, 12 | company_name2: `aaa${i} company index index index`, 13 | 14 | company_name3: `aaa${i} company company index index`, 15 | company_name4: `aaa${i} company company company index`, 16 | company_name5: `aaa${i} company company company company`, 17 | company_name6: `aaa${i} company index index company`, 18 | }) 19 | } 20 | 21 | return tempDataSource 22 | } 23 | 24 | function SinglePageLoading() { 25 | const [dataSource] = useState(generateData()) 26 | 27 | const columns: any = [ 28 | { 29 | title: '序号', 30 | key: 'id', 31 | fixed: 'left', 32 | render(text, record, index) { 33 | return index + 1 34 | }, 35 | width: 100, 36 | }, 37 | { 38 | title: '公司', 39 | dataIndex: 'company_name', 40 | width: 200, 41 | }, 42 | { 43 | title: '公司1', 44 | dataIndex: 'company_name1', 45 | width: 200, 46 | }, 47 | { 48 | title: '公司2', 49 | dataIndex: 'company_name2', 50 | width: 200, 51 | }, 52 | ] 53 | 54 | const components1 = useMemo( 55 | () => 56 | VList({ 57 | height: 550, 58 | vid: 'first', 59 | }), 60 | [] 61 | ) 62 | 63 | return ( 64 | <> 65 |
73 | 74 | ) 75 | } 76 | 77 | export default SinglePageLoading 78 | -------------------------------------------------------------------------------- /docs/examples/JumpTreeTable.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo, useState } from 'react' 2 | import { Button, Table } from 'antd' 3 | import { VList, scrollTo } from '../../src/index' 4 | 5 | import 'antd/dist/antd.css' 6 | 7 | const generateData = () => { 8 | const temp = [] 9 | 10 | for (let i = 0; i < 100; i += 1) { 11 | temp.push({ 12 | a: i, 13 | b: 'bbbb'.repeat(Math.floor(Math.random() * 10)), 14 | children: [ 15 | { 16 | a: `${i}_${i}`, 17 | b: 'test', 18 | children: [ 19 | { 20 | a: `${i}_${i}_${i}`, 21 | b: 'testtest', 22 | }, 23 | { 24 | a: `${i}_${i}_${i}_${i}`, 25 | b: 'testtest', 26 | }, 27 | 28 | { 29 | a: `nm${i}`, 30 | b: 'testtest', 31 | }, 32 | { 33 | a: `ds${i}`, 34 | b: 'testtest', 35 | }, 36 | { 37 | a: `${i}sss`, 38 | b: 'testtest', 39 | }, 40 | { 41 | a: `llll${i}`, 42 | b: 'testtest', 43 | }, 44 | { 45 | a: `lldsll${i}`, 46 | b: 'testtest', 47 | }, 48 | { 49 | a: `llall${i}`, 50 | b: 'testtest', 51 | }, 52 | { 53 | a: `lllwl${i}`, 54 | b: 'testtest', 55 | }, 56 | { 57 | a: `lllql${i}`, 58 | b: 'testtest', 59 | }, 60 | { 61 | a: `llewll${i}`, 62 | b: 'testtest', 63 | }, 64 | { 65 | a: `lelll${i}`, 66 | b: 'testtest', 67 | }, 68 | { 69 | a: `lllfl${i}`, 70 | b: 'testtest', 71 | }, 72 | { 73 | a: `lllpl${i}`, 74 | b: 'testtest', 75 | }, 76 | { 77 | a: `l]lll${i}`, 78 | b: 'testtest', 79 | }, 80 | { 81 | a: `ll2ll${i}`, 82 | b: 'testtest', 83 | }, 84 | ], 85 | }, 86 | ], 87 | }) 88 | } 89 | 90 | return temp 91 | } 92 | 93 | const initData = generateData() 94 | 95 | function TreeTable() { 96 | const [data, setData] = useState(initData) 97 | 98 | const [expandedRowKeys, setExpandedRowKeys] = useState([]) 99 | 100 | // eslint-disable-next-line no-unused-vars 101 | const [flag, setFlag] = useState(false) 102 | 103 | const handleClick = (record, e) => { 104 | e.preventDefault() 105 | } 106 | 107 | const handleJumpClick = useCallback(() => { 108 | setExpandedRowKeys([ 109 | 0, 110 | 1, 111 | '0_0', 112 | '1_1', 113 | 2, 114 | 3, 115 | 4, 116 | 5, 117 | 6, 118 | 9, 119 | 10, 120 | 12, 121 | 23, 122 | 25, 123 | ]) 124 | 125 | setTimeout(() => { 126 | scrollTo({ 127 | row: 5, 128 | }) 129 | }, 500) 130 | }, []) 131 | 132 | const columns = [ 133 | { 134 | title: 'title1', 135 | dataIndex: 'a', 136 | key: 'a', 137 | width: 150, 138 | }, 139 | { 140 | title: 'title2', 141 | dataIndex: 'b', 142 | key: 'b', 143 | width: 200, 144 | }, 145 | { 146 | title: 'title3', 147 | dataIndex: 'c', 148 | key: 'c', 149 | width: 200, 150 | }, 151 | { 152 | title: 'Operations', 153 | dataIndex: '', 154 | width: 200, 155 | key: 'x', 156 | render: (text, record) => ( 157 | handleClick(record, e)}> 158 | click {record.a} 159 | 160 | ), 161 | }, 162 | ] 163 | 164 | const vComponent = useMemo( 165 | () => VList({ height: 500, resetTopWhenDataChange: false }), 166 | [] 167 | ) 168 | 169 | return ( 170 |
171 |

sub table

172 | 173 |
record.a} 177 | pagination={false} 178 | scroll={{ y: 500, x: '100%' }} 179 | expandable={{ 180 | expandedRowKeys, 181 | }} 182 | components={vComponent} 183 | /> 184 | 185 | ) 186 | } 187 | 188 | export default TreeTable 189 | -------------------------------------------------------------------------------- /docs/examples/PaginationTable.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useMemo } from 'react' 2 | import { Table } from 'antd' 3 | import { VList } from 'virtuallist-antd' 4 | 5 | function PaginationTable() { 6 | const [dataSource, setDataSource] = useState([]) 7 | 8 | useEffect(() => { 9 | const tempDataSource = [] 10 | for (let i = 0; i < 201; i += 1) { 11 | tempDataSource.push({ 12 | company_name: `aaa${i}`, 13 | }) 14 | } 15 | 16 | setTimeout(() => { 17 | setDataSource(tempDataSource) 18 | }, 2000) 19 | }, []) 20 | 21 | const columns = [ 22 | { 23 | title: '序号', 24 | key: 'id', 25 | render(text, record, index) { 26 | return index + 1 27 | }, 28 | width: 100, 29 | }, 30 | { 31 | title: '公司', 32 | dataIndex: 'company_name', 33 | width: 200, 34 | }, 35 | ] 36 | 37 | const pagination = { 38 | defaultPageSize: 50, 39 | defaultCurrent: 1, 40 | showQuickJumper: true, 41 | showTotal(total): any { 42 | return `总共${total}条数据` 43 | }, 44 | showSizeChanger: true, 45 | pageSizeOptions: ['10', '50', '100'], 46 | } 47 | 48 | const vc = useMemo( 49 | () => 50 | VList({ 51 | height: 600, 52 | }), 53 | [] 54 | ) 55 | 56 | return ( 57 |
65 | ) 66 | } 67 | 68 | export default PaginationTable 69 | -------------------------------------------------------------------------------- /docs/examples/SinglePageLoading.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useMemo } from 'react' 2 | import { Button, Table } from 'antd' 3 | import { VList, scrollTo } from '../../src/index' 4 | import 'antd/dist/antd.css' 5 | 6 | const generateData = () => { 7 | const tempDataSource = [] 8 | for (let i = 0; i < 50; i += 1) { 9 | tempDataSource.push({ 10 | company_name: `aaa${i} 富士山下的你好美 你知道吗`, 11 | company_name1: `aaa${i} index index index index`, 12 | company_name2: `aaa${i} company index index index`, 13 | 14 | company_name3: `aaa${i} company company index index`, 15 | company_name4: `aaa${i} company company company index`, 16 | company_name5: `aaa${i} company company company company`, 17 | company_name6: `aaa${i} company index index company`, 18 | }) 19 | } 20 | 21 | return tempDataSource 22 | } 23 | 24 | function SinglePageLoading() { 25 | const [dataSource1, setDataSource1] = useState(generateData()) 26 | const [dataSource2, setDataSource2] = useState(generateData()) 27 | 28 | const [loading1, setLoading1] = useState(false) 29 | const [loading2, setLoading2] = useState(false) 30 | 31 | const columns: any = [ 32 | { 33 | title: '序号', 34 | key: 'id', 35 | fixed: 'left', 36 | render(text, record, index) { 37 | return index + 1 38 | }, 39 | width: 100, 40 | }, 41 | { 42 | title: '公司', 43 | dataIndex: 'company_name', 44 | width: 200, 45 | }, 46 | { 47 | title: '公司1', 48 | dataIndex: 'company_name1', 49 | width: 200, 50 | }, 51 | { 52 | title: '公司2', 53 | dataIndex: 'company_name2', 54 | width: 200, 55 | }, 56 | { 57 | title: '公司3', 58 | dataIndex: 'company_name3', 59 | width: 200, 60 | }, 61 | { 62 | title: '公司4', 63 | dataIndex: 'company_name4', 64 | width: 200, 65 | }, 66 | { 67 | title: '公司5', 68 | dataIndex: 'company_name5', 69 | width: 200, 70 | }, 71 | { 72 | title: '公司6', 73 | dataIndex: 'company_name6', 74 | width: 200, 75 | }, 76 | ] 77 | 78 | const handleReachEnd1 = useCallback(() => { 79 | setLoading1(true) 80 | setDataSource1((pre) => { 81 | const temp = [] 82 | return [...pre, ...temp] 83 | }) 84 | setTimeout(() => { 85 | setLoading1(false) 86 | }, 1000) 87 | }, []) 88 | 89 | const handleReachEnd2 = useCallback(() => { 90 | setLoading2(true) 91 | setDataSource2((pre) => { 92 | const temp = generateData() 93 | return [...pre, ...temp] 94 | }) 95 | setTimeout(() => { 96 | setLoading2(false) 97 | }, 1000) 98 | }, []) 99 | 100 | const components1 = useMemo( 101 | () => 102 | VList({ 103 | height: 500, 104 | onReachEnd: handleReachEnd1, 105 | vid: 'first', 106 | }), 107 | [handleReachEnd1] 108 | ) 109 | const components2 = useMemo( 110 | () => 111 | VList({ 112 | height: 500, 113 | onReachEnd: handleReachEnd2, 114 | vid: 'second', 115 | }), 116 | [handleReachEnd2] 117 | ) 118 | 119 | return ( 120 | <> 121 | 124 | 127 |
136 | 139 |
148 | 149 | ) 150 | } 151 | 152 | export default SinglePageLoading 153 | -------------------------------------------------------------------------------- /docs/examples/TabsDemo.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useMemo } from 'react'; 2 | import { Table, Tabs } from 'antd'; 3 | import { VList } from '../../src/index'; 4 | import 'antd/dist/antd.css'; 5 | 6 | const { TabPane } = Tabs; 7 | 8 | function TabsDemo() { 9 | const [dataSource, setDataSource] = useState([]); 10 | const [activeKey, setActiveKey] = useState('tab1'); 11 | 12 | useEffect(() => { 13 | const tempDataSource = []; 14 | for (let i = 0; i < 1000; i += 1) { 15 | tempDataSource.push({ 16 | company_name: `aaa${i} 富士山下的你好美 你知道吗 aaa${i} 富士山下的你好美 你知道吗 aaa${i} 富士山下的你好美 你知道吗`, 17 | company_name1: `aaa${i} index index index index`, 18 | company_name2: `aaa${i} company index index index`, 19 | 20 | company_name3: `aaa${i} company company index index`, 21 | company_name4: `aaa${i} company company company index`, 22 | company_name5: `aaa${i} company company company company`, 23 | company_name6: `aaa${i} company index index company`, 24 | }); 25 | } 26 | 27 | setDataSource(tempDataSource); 28 | }, []); 29 | 30 | const columns: any[] = [ 31 | { 32 | title: '序号', 33 | key: 'id', 34 | fixed: 'left', 35 | render(text, record, index) { 36 | return index + 1; 37 | }, 38 | width: 100, 39 | }, 40 | { 41 | title: '公司', 42 | dataIndex: 'company_name', 43 | width: 200, 44 | }, 45 | { 46 | title: '公司1', 47 | dataIndex: 'company_name1', 48 | width: 200, 49 | }, 50 | { 51 | title: '公司2', 52 | dataIndex: 'company_name2', 53 | width: 200, 54 | }, 55 | ]; 56 | 57 | const vc1 = useMemo(() => VList({ 58 | height: 500, 59 | vid: 'first', 60 | }), []); 61 | 62 | const vc2 = useMemo(() => VList({ 63 | height: 'calc(20vh)', 64 | vid: 'second', 65 | }), []); 66 | 67 | const vc3 = useMemo(() => VList({ 68 | height: 'calc(60vh)', 69 | vid: 'thrid', 70 | }), []); 71 | 72 | return ( 73 | <> 74 | 75 | 76 |
85 | 86 | 87 | 88 |
96 | 97 | 98 | 99 |
107 | 108 | 109 | 110 | ); 111 | } 112 | 113 | export default TabsDemo; 114 | -------------------------------------------------------------------------------- /docs/examples/TreeTable.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo, useState } from 'react' 2 | import { Button, Table } from 'antd' 3 | import { VList } from '../../src/index' 4 | 5 | import 'antd/dist/antd.css' 6 | 7 | const generateData = () => { 8 | const temp = [] 9 | 10 | for (let i = 0; i < 2; i += 1) { 11 | temp.push({ 12 | a: i, 13 | b: 'bbbb'.repeat(Math.floor(Math.random() * 10)), 14 | children: [ 15 | { 16 | a: `${i}_${i}`, 17 | b: 'test', 18 | children: [ 19 | { 20 | a: `${i}_${i}_${i}`, 21 | b: 'testtest', 22 | }, 23 | { 24 | a: `${i}_${i}_${i}_${i}`, 25 | b: 'testtest', 26 | }, 27 | 28 | { 29 | a: `nm${i}`, 30 | b: 'testtest', 31 | }, 32 | { 33 | a: `ds${i}`, 34 | b: 'testtest', 35 | }, 36 | { 37 | a: `${i}sss`, 38 | b: 'testtest', 39 | }, 40 | { 41 | a: `llll${i}`, 42 | b: 'testtest', 43 | }, 44 | ], 45 | }, 46 | ], 47 | }) 48 | } 49 | 50 | return temp 51 | } 52 | 53 | const initData = generateData() 54 | 55 | function TreeTable() { 56 | const [data, setData] = useState(initData) 57 | 58 | // eslint-disable-next-line no-unused-vars 59 | const [flag, setFlag] = useState(false) 60 | 61 | const handleClick = (record, e) => { 62 | e.preventDefault() 63 | } 64 | 65 | const handleChangeClick = useCallback(() => { 66 | setFlag((pre) => { 67 | if (!pre) { 68 | setData([]) 69 | } else { 70 | setData(generateData()) 71 | } 72 | 73 | // setData((d) => [...d]); 74 | 75 | return !pre 76 | }) 77 | }, []) 78 | 79 | const columns = [ 80 | { 81 | title: 'title1', 82 | dataIndex: 'a', 83 | key: 'a', 84 | width: 150, 85 | }, 86 | { 87 | title: 'title2', 88 | dataIndex: 'b', 89 | key: 'b', 90 | width: 200, 91 | }, 92 | { 93 | title: 'title3', 94 | dataIndex: 'c', 95 | key: 'c', 96 | width: 200, 97 | }, 98 | { 99 | title: 'Operations', 100 | dataIndex: '', 101 | width: 200, 102 | key: 'x', 103 | render: (text, record) => ( 104 | handleClick(record, e)}> 105 | click {record.a} 106 | 107 | ), 108 | }, 109 | ] 110 | 111 | const vComponent = useMemo( 112 | () => VList({ height: 500, resetTopWhenDataChange: false }), 113 | [] 114 | ) 115 | 116 | return ( 117 |
118 |

sub table

119 | 120 |
record.a} 124 | pagination={false} 125 | scroll={{ y: 500, x: '100%' }} 126 | components={vComponent} 127 | /> 128 | 129 | ) 130 | } 131 | 132 | export default TreeTable 133 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: virtuallist-antd 3 | --- 4 | -------------------------------------------------------------------------------- /docs/update.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | verbose: true, 4 | preset: 'ts-jest', 5 | testEnvironment: 'jsdom', 6 | transform: { 7 | '^.+\\.jsx?$': 'babel-jest', // 这个是jest的默认配置 8 | '^.+\\.ts?$': 'ts-jest', // typescript转换 9 | }, 10 | transformIgnorePatterns: [ 11 | 'node_modules/(?!@ngrx|(?!deck.gl)|ng-dynamic)', 12 | ], 13 | moduleNameMapper: { 14 | '\\.(css|less)$': '/__mocks__/styleMock.js', 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "virtuallist-antd", 3 | "version": "0.8.0-beta.1", 4 | "description": "virtualList for antd-table, 实现antd-table的虚拟列表, antd-table无限滚动, infinite scrolling for antd-table", 5 | "main": "dist/index.js", 6 | "module": "dist/index.es.js", 7 | "jsnext:main": "dist/index.es.js", 8 | "engines": { 9 | "node": ">=8", 10 | "npm": ">=5" 11 | }, 12 | "scripts": { 13 | "start": "dumi dev", 14 | "test": "jest", 15 | "build": "rollup -c" 16 | }, 17 | "keywords": [ 18 | "antd-table", 19 | "virtualList", 20 | "infinite", 21 | "scrolling", 22 | "antd-table虚拟列表", 23 | "antd-table无限滚动" 24 | ], 25 | "author": "crawler-django", 26 | "license": "MIT", 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/crawler-django/virtuallist-antd" 30 | }, 31 | "homepage": "https://github.com/crawler-django/virtuallist-antd", 32 | "peerDependencies": { 33 | "antd": "^4.1.0", 34 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0" 35 | }, 36 | "devDependencies": { 37 | "@babel/core": "^7.9.0", 38 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3", 39 | "@babel/plugin-proposal-optional-chaining": "^7.9.0", 40 | "@babel/preset-env": "^7.9.0", 41 | "@babel/preset-react": "^7.9.4", 42 | "@babel/preset-typescript": "^7.16.0", 43 | "@svgr/rollup": "^4.3.3", 44 | "@testing-library/jest-dom": "^5.16.1", 45 | "@testing-library/react": "^12.1.2", 46 | "@types/jest": "^27.0.3", 47 | "@types/lodash-es": "^4.17.3", 48 | "@types/react": "^16.9.32", 49 | "@typescript-eslint/eslint-plugin": "^4.31.1", 50 | "@typescript-eslint/parser": "^4.31.1", 51 | "antd": "4.20.2", 52 | "array-move": "^4.0.0", 53 | "babel-loader": "^8.1.0", 54 | "babel-plugin-import": "^1.13.0", 55 | "babel-preset-es2015": "^6.24.1", 56 | "css-loader": "^3.5.0", 57 | "cssnano": "^4.1.10", 58 | "dumi": "^1.1.19", 59 | "eslint": "^7.32.0", 60 | "eslint-config-airbnb": "^18.2.1", 61 | "eslint-plugin-import": "^2.24.2", 62 | "eslint-plugin-jsx-a11y": "^6.4.1", 63 | "eslint-plugin-react": "^7.25.1", 64 | "eslint-plugin-react-hooks": "^4.2.0", 65 | "immutability-helper": "^3.1.1", 66 | "jest": "^27.4.4", 67 | "lodash-es": "^4.17.21", 68 | "minimist": "^1.2.5", 69 | "postcss-cssnext": "^3.1.0", 70 | "postcss-nested": "^4.2.1", 71 | "postcss-simple-vars": "^5.0.2", 72 | "react-dnd": "^14.0.2", 73 | "react-dnd-html5-backend": "^14.0.0", 74 | "react-resizable": "^3.0.2", 75 | "react-sortable-hoc": "2.0.0", 76 | "rollup-plugin-commonjs": "^10.1.0", 77 | "rollup-plugin-node-resolve": "^5.2.0", 78 | "rollup-plugin-peer-deps-external": "^2.2.2", 79 | "rollup-plugin-postcss": "^2.5.0", 80 | "rollup-plugin-typescript": "^1.0.1", 81 | "rollup-plugin-typescript2": "^0.25.3", 82 | "rollup-plugin-uglify-es": "0.0.1", 83 | "rollup-plugin-url": "^3.0.1", 84 | "style-loader": "^1.1.3", 85 | "ts-babel": "^6.1.7", 86 | "ts-jest": "^27.1.1", 87 | "ts-loader": "^6.2.2", 88 | "typescript": "^3.8.3" 89 | }, 90 | "files": [ 91 | "dist" 92 | ], 93 | "resolutions": { 94 | "minimist": "^1.2.5" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import external from 'rollup-plugin-peer-deps-external'; 4 | // import postcss from 'rollup-plugin-postcss-modules' 5 | import postcss from 'rollup-plugin-postcss'; 6 | import resolve from 'rollup-plugin-node-resolve'; 7 | import uglify from 'rollup-plugin-uglify-es'; 8 | import url from 'rollup-plugin-url'; 9 | import svgr from '@svgr/rollup'; 10 | 11 | import simplevars from 'postcss-simple-vars'; 12 | import nested from 'postcss-nested'; 13 | import cssnext from 'postcss-cssnext'; 14 | import cssnano from 'cssnano'; 15 | 16 | import pkg from './package.json'; 17 | 18 | export default { 19 | input: 'src/index.tsx', 20 | output: [ 21 | { 22 | file: pkg.main, 23 | format: 'cjs', 24 | }, 25 | { 26 | file: pkg.module, 27 | format: 'es', 28 | }, 29 | ], 30 | plugins: [ 31 | external(), 32 | postcss({ 33 | plugins: [ 34 | simplevars(), 35 | nested(), 36 | cssnext({ warnForDuplicates: false }), 37 | cssnano(), 38 | ], 39 | extendsions: ['.css'], 40 | }), 41 | uglify(), 42 | url(), 43 | svgr(), 44 | resolve(), 45 | typescript({ 46 | rollupCommonJSResolveHack: true, 47 | clean: true, 48 | }), 49 | commonjs({ 50 | include: 'node_modules/**', 51 | namedExports: { 52 | 'node_modules/react/index.js': [ 53 | 'cloneElement', 54 | 'createContext', 55 | 'Component', 56 | 'createElement', 57 | 'useContext', 58 | 'useRef', 59 | 'useEffect', 60 | 'useState', 61 | 'useReducer', 62 | 'useMemo', 63 | ], 64 | }, 65 | }), 66 | ], 67 | }; 68 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable arrow-body-style */ 2 | import React, { 3 | useRef, 4 | useEffect, 5 | useContext, 6 | createContext, 7 | useReducer, 8 | useState, 9 | useMemo, 10 | } from 'react' 11 | import { throttle, debounce } from 'lodash-es' 12 | 13 | import './style.css' 14 | 15 | // ==============全局常量 ================== // 16 | const DEFAULT_VID = 'vtable' 17 | const vidMap = new Map() 18 | let debounceListRender: any 19 | 20 | // ===============reducer ============== // 21 | const initialState = { 22 | // 行高度 23 | rowHeight: 0, 24 | // 当前的scrollTop 25 | curScrollTop: 0, 26 | // 总行数 27 | totalLen: 0, 28 | } 29 | 30 | function reducer(state, action) { 31 | const { curScrollTop, rowHeight, totalLen, ifScrollTopClear } = action 32 | 33 | let stateScrollTop = state.curScrollTop 34 | switch (action.type) { 35 | // 改变trs 即 改变渲染的列表trs 36 | case 'changeTrs': 37 | return { 38 | ...state, 39 | curScrollTop, 40 | } 41 | // 初始化每行的高度, 表格总高度, 渲染的条数 42 | case 'initHeight': 43 | return { 44 | ...state, 45 | rowHeight, 46 | } 47 | // 更改totalLen 48 | case 'changeTotalLen': 49 | if (totalLen === 0) { 50 | stateScrollTop = 0 51 | } 52 | 53 | return { 54 | ...state, 55 | totalLen, 56 | curScrollTop: stateScrollTop, 57 | } 58 | 59 | case 'reset': 60 | return { 61 | ...state, 62 | curScrollTop: ifScrollTopClear ? 0 : state.curScrollTop, 63 | } 64 | default: 65 | throw new Error() 66 | } 67 | } 68 | 69 | // ===============context ============== // 70 | const ScrollContext = createContext({ 71 | dispatch: undefined, 72 | renderLen: 1, 73 | start: 0, 74 | offsetStart: 0, 75 | // ============= 76 | rowHeight: initialState.rowHeight, 77 | totalLen: 0, 78 | vid: DEFAULT_VID, 79 | }) 80 | 81 | // =============组件 =================== // 82 | 83 | function VCell(props: any): JSX.Element { 84 | const { children, ...restProps } = props 85 | 86 | return ( 87 | 90 | ) 91 | } 92 | 93 | function VRow(props: any, ref: any): JSX.Element { 94 | const { dispatch, rowHeight, totalLen, vid } = useContext(ScrollContext) 95 | 96 | const { children, style, ...restProps } = props 97 | 98 | const trRef = useRef(null) 99 | 100 | useEffect(() => { 101 | const initHeight = (tempRef) => { 102 | if (tempRef?.current?.offsetHeight && !rowHeight && totalLen) { 103 | const tempRowHeight = tempRef?.current?.offsetHeight ?? 0 104 | 105 | vidMap.set(vid, { 106 | ...vidMap.get(vid), 107 | rowItemHeight: tempRowHeight, 108 | }) 109 | dispatch({ 110 | type: 'initHeight', 111 | rowHeight: tempRowHeight, 112 | }) 113 | } 114 | } 115 | 116 | initHeight( 117 | Object.prototype.hasOwnProperty.call(ref, 'current') ? ref : trRef 118 | ) 119 | }, [trRef, dispatch, rowHeight, totalLen, ref, vid]) 120 | 121 | return ( 122 | 135 | {children} 136 | 137 | ) 138 | } 139 | 140 | function VWrapper(props: any): JSX.Element { 141 | const { children, ...restProps } = props 142 | 143 | const { renderLen, start, dispatch, vid, totalLen } = useContext( 144 | ScrollContext 145 | ) 146 | 147 | const contents = useMemo(() => { 148 | return children[1] 149 | }, [children]) 150 | 151 | const contentsLen = useMemo(() => { 152 | return contents?.length ?? 0 153 | }, [contents]) 154 | 155 | useEffect(() => { 156 | if (totalLen !== contentsLen) { 157 | dispatch({ 158 | type: 'changeTotalLen', 159 | totalLen: contentsLen ?? 0, 160 | }) 161 | } 162 | }, [contentsLen, dispatch, vid, totalLen]) 163 | 164 | let tempNode = null 165 | if (Array.isArray(contents) && contents.length) { 166 | tempNode = [ 167 | children[0], 168 | contents.slice(start, start + (renderLen ?? 1)).map((item) => { 169 | if (Array.isArray(item)) { 170 | // 兼容antd v4.3.5 --- rc-table 7.8.1及以下 171 | return item[0] 172 | } 173 | // 处理antd ^v4.4.0 --- rc-table ^7.8.2 174 | return item 175 | }), 176 | ] 177 | } else { 178 | tempNode = children 179 | } 180 | 181 | return {tempNode} 182 | } 183 | 184 | function VTable(props: any, otherParams): JSX.Element { 185 | const { style, children, ...rest } = props 186 | const { width, ...rest_style } = style 187 | 188 | const { vid, scrollY, reachEnd, onScroll, resetScrollTopWhenDataChange } = 189 | otherParams ?? {} 190 | 191 | const [state, dispatch] = useReducer(reducer, initialState) 192 | 193 | const wrap_tableRef = useRef(null) 194 | const tableRef = useRef(null) 195 | 196 | // 数据的总条数 197 | const [totalLen, setTotalLen] = useState( 198 | children[1]?.props?.data?.length ?? 0 199 | ) 200 | 201 | useEffect(() => { 202 | setTotalLen(state.totalLen) 203 | }, [state.totalLen]) 204 | 205 | // 组件卸载的清除操作 206 | useEffect(() => { 207 | return () => { 208 | vidMap.delete(vid) 209 | } 210 | }, [vid]) 211 | 212 | // table总高度 213 | const tableHeight = useMemo(() => { 214 | let temp: string | number = 'auto' 215 | 216 | if (state.rowHeight && totalLen) { 217 | temp = state.rowHeight * totalLen 218 | } 219 | return temp 220 | }, [state.rowHeight, totalLen]) 221 | 222 | // table的scrollY值 223 | const [tableScrollY, setTableScrollY] = useState(0) 224 | 225 | // tableScrollY 随scrollY / tableHeight 进行变更 226 | useEffect(() => { 227 | let temp = 0 228 | 229 | if (typeof scrollY === 'string') { 230 | temp = 231 | (wrap_tableRef.current?.parentNode as HTMLElement) 232 | ?.offsetHeight ?? 0 233 | } else { 234 | temp = scrollY 235 | } 236 | 237 | // if (isNumber(tableHeight) && tableHeight < temp) { 238 | // temp = tableHeight; 239 | // } 240 | 241 | // 处理tableScrollY <= 0的情况 242 | if (temp <= 0) { 243 | temp = 0 244 | } 245 | 246 | setTableScrollY(temp) 247 | }, [scrollY, tableHeight]) 248 | 249 | // 渲染的条数 250 | const renderLen = useMemo(() => { 251 | let temp = 1 252 | if (state.rowHeight && totalLen && tableScrollY) { 253 | if (tableScrollY <= 0) { 254 | temp = 0 255 | } else { 256 | const tempRenderLen = 257 | ((tableScrollY / state.rowHeight) | 0) + 1 + 2 258 | // console.log('tempRenderLen', tempRenderLen) 259 | // temp = tempRenderLen > totalLen ? totalLen : tempRenderLen; 260 | temp = tempRenderLen 261 | } 262 | } 263 | 264 | return temp 265 | }, [state.rowHeight, totalLen, tableScrollY]) 266 | 267 | // 渲染中的第一条 268 | let start = state.rowHeight ? (state.curScrollTop / state.rowHeight) | 0 : 0 269 | 270 | // 偏移量 271 | let offsetStart = state.rowHeight ? state.curScrollTop % state.rowHeight : 0 272 | 273 | // 用来优化向上滚动出现的空白 274 | if ( 275 | state.curScrollTop && 276 | state.rowHeight && 277 | state.curScrollTop > state.rowHeight 278 | ) { 279 | start -= 1 280 | offsetStart += state.rowHeight 281 | } else { 282 | start = 0 283 | } 284 | 285 | // 数据变更 操作scrollTop 286 | useEffect(() => { 287 | const scrollNode = wrap_tableRef.current?.parentNode as HTMLElement 288 | 289 | if (resetScrollTopWhenDataChange) { 290 | // 重置scrollTop 291 | if (scrollNode) { 292 | scrollNode.scrollTop = 0 293 | } 294 | 295 | dispatch({ type: 'reset', ifScrollTopClear: true }) 296 | } else { 297 | // 不重置scrollTop 不清空curScrollTop 298 | dispatch({ type: 'reset', ifScrollTopClear: false }) 299 | } 300 | 301 | if (vidMap.has(vid)) { 302 | vidMap.set(vid, { 303 | ...vidMap.get(vid), 304 | scrollNode, 305 | }) 306 | } 307 | }, [totalLen, resetScrollTopWhenDataChange, vid, children]) 308 | 309 | useEffect(() => { 310 | const throttleScroll = throttle((e) => { 311 | const historyScrollHeight = vidMap.get(vid)?.scrollHeight 312 | 313 | const scrollTop: number = e?.target?.scrollTop ?? 0 314 | const scrollHeight: number = e?.target?.scrollHeight ?? 0 315 | const clientHeight: number = e?.target?.clientHeight ?? 0 316 | 317 | // 到底了 没有滚动条就不会触发reachEnd. 建议设置scrolly高度少点或者数据量多点. 318 | if (scrollTop === scrollHeight) { 319 | // reachEnd && reachEnd() 320 | } else if ( 321 | scrollTop + clientHeight >= scrollHeight && 322 | historyScrollHeight !== scrollHeight 323 | ) { 324 | // 相同的tableData情况下, 上次reachEnd执行后, scrollHeight不变, 则不会再次请求reachEnd 325 | vidMap.set(vid, { 326 | ...vidMap.get(vid), 327 | scrollHeight, 328 | }) 329 | // 有滚动条的情况 330 | // eslint-disable-next-line no-unused-expressions 331 | reachEnd && reachEnd() 332 | } 333 | 334 | // eslint-disable-next-line no-unused-expressions 335 | onScroll && onScroll() 336 | 337 | // 若renderLen大于totalLen, 置空curScrollTop. => table paddingTop会置空. 338 | dispatch({ 339 | type: 'changeTrs', 340 | curScrollTop: renderLen <= totalLen ? scrollTop : 0, 341 | vid, 342 | }) 343 | }, 60) 344 | 345 | const ref = wrap_tableRef?.current?.parentNode as HTMLElement 346 | 347 | if (ref) { 348 | ref.addEventListener('scroll', throttleScroll) 349 | } 350 | 351 | return () => { 352 | ref.removeEventListener('scroll', throttleScroll) 353 | } 354 | }, [onScroll, reachEnd, renderLen, totalLen, vid]) 355 | 356 | debounceListRender(start, renderLen) 357 | 358 | return ( 359 |
370 | 381 |
88 |
{children}
89 |
391 | {children} 392 |
393 | 394 |
395 | ) 396 | } 397 | 398 | // ================导出=================== 399 | export function VList(props: { 400 | height: number | string 401 | // 到底的回调函数 402 | onReachEnd?: () => void 403 | onScroll?: () => void 404 | // 列表渲染时触发的回调函数(参数可以拿到 start: 渲染开始行, renderLen: 渲染行数) 405 | // listRender: provide info: {start, renderLen} on render list. 406 | // start: start index in render list. 407 | // renderLen: render length in render list. 408 | onListRender?: (listInfo: { start: number; renderLen: number }) => void 409 | // 列表渲染时触发的回调函数防抖毫秒数. 410 | // listRender debounceMs. 411 | debounceListRenderMS?: number 412 | // 唯一标识 413 | vid?: string 414 | // 重置scrollTop 当数据变更的时候. 默认为true 415 | // reset scrollTop when data change 416 | resetTopWhenDataChange?: boolean 417 | }): any { 418 | const { 419 | vid = DEFAULT_VID, 420 | height, 421 | onReachEnd, 422 | onScroll, 423 | onListRender, 424 | debounceListRenderMS, 425 | resetTopWhenDataChange = true, 426 | } = props 427 | 428 | const resetScrollTopWhenDataChange = onReachEnd 429 | ? false 430 | : resetTopWhenDataChange 431 | 432 | if (!vidMap.has(vid)) { 433 | vidMap.set(vid, { _id: vid }) 434 | } 435 | 436 | debounceListRender = onListRender 437 | ? debounce((_start, _renderLen) => { 438 | onListRender({ start: _start, renderLen: _renderLen }) 439 | }, debounceListRenderMS ?? 300) 440 | : () => {} 441 | 442 | return { 443 | table: (p) => 444 | VTable(p, { 445 | vid, 446 | scrollY: height, 447 | reachEnd: onReachEnd, 448 | onScroll, 449 | onListRender, 450 | resetScrollTopWhenDataChange, 451 | }), 452 | body: { 453 | wrapper: VWrapper, 454 | row: VRow, 455 | cell: VCell, 456 | }, 457 | } 458 | } 459 | 460 | export function scrollTo(option: { 461 | /** 462 | * 行数 463 | */ 464 | row?: number 465 | /** 466 | * y的偏移量 467 | */ 468 | y?: number 469 | /** 470 | * 同一页面拥有多个虚拟表格的时候的唯一标识. 471 | */ 472 | vid?: string 473 | }) { 474 | const { row, y, vid = DEFAULT_VID } = option 475 | 476 | try { 477 | const { scrollNode, rowItemHeight } = vidMap.get(vid) 478 | 479 | if (row) { 480 | if (row - 1 > 0) { 481 | scrollNode.scrollTop = (row - 1) * (rowItemHeight ?? 0) 482 | } else { 483 | scrollNode.scrollTop = 0 484 | } 485 | } else { 486 | scrollNode.scrollTop = y ?? 0 487 | } 488 | 489 | return { vid, rowItemHeight } 490 | } catch (e) { 491 | console.error('dont call scrollTo before init table') 492 | 493 | return { vid, rowItemHeight: -1 } 494 | } 495 | } 496 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | .virtuallist .ant-table-tbody > tr > td > div { 2 | box-sizing: border-box; 3 | white-space: nowrap; 4 | vertical-align: middle; 5 | overflow: hidden; 6 | text-overflow: ellipsis; 7 | width: 100%; 8 | } 9 | 10 | .virtuallist .ant-table-tbody > tr > td.ant-table-row-expand-icon-cell > div { 11 | overflow: inherit; 12 | } 13 | 14 | .ant-table-bordered .virtuallist > table > .ant-table-tbody > tr > td { 15 | border-right: 1px solid #f0f0f0; 16 | } 17 | -------------------------------------------------------------------------------- /tests/basic.test.js: -------------------------------------------------------------------------------- 1 | // import '@testing-library/jest-dom'; 2 | import * as React from 'react'; 3 | import { 4 | render, screen, act, fireEvent, waitFor, 5 | } from '@testing-library/react'; 6 | import '@testing-library/jest-dom'; 7 | import TabsDemo from '../docs/examples/TabsDemo'; 8 | import SinglePageLoading from '../docs/examples/SinglePageLoading'; 9 | 10 | describe('Test', () => { 11 | beforeAll(() => { 12 | Object.defineProperty(window, 'matchMedia', { 13 | writable: true, 14 | value: jest.fn().mockImplementation((query) => ({ 15 | matches: false, 16 | media: query, 17 | onchange: null, 18 | addListener: jest.fn(), // Deprecated 19 | removeListener: jest.fn(), // Deprecated 20 | addEventListener: jest.fn(), 21 | removeEventListener: jest.fn(), 22 | dispatchEvent: jest.fn(), 23 | })), 24 | }); 25 | }); 26 | 27 | test('TabsDemo render normally', () => { 28 | render(); 29 | 30 | // expect(screen.getByText('No Data')).toBeInTheDocument(); 31 | expect(screen.getByText('aaa0 index index index index')).toBeInTheDocument(); 32 | 33 | // fireEvent.click(screen.getByText('Tab 2')); 34 | }); 35 | 36 | test('scrollTo run normally', async () => { 37 | render(); 38 | 39 | expect(screen.getAllByText('aaa0 富士山下的你好美 你知道吗').length).toEqual(2); 40 | 41 | fireEvent.click(screen.getByText('跳到1000')); 42 | fireEvent.click(screen.getByText('跳到500')); 43 | // screen.debug(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | "declarationDir": "build", 13 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 14 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | "outDir": "build", /* Redirect output structure to the directory. */ 17 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 18 | // "composite": true, /* Enable project compilation */ 19 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 20 | // "removeComments": true, /* Do not emit comments to output. */ 21 | // "noEmit": true, /* Do not emit outputs. */ 22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 23 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 25 | 26 | /* Strict Type-Checking Options */ 27 | "strict": true, /* Enable all strict type-checking options. */ 28 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 29 | "strictNullChecks": false, /* Enable strict null checks. */ 30 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 31 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 32 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 33 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 34 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 35 | 36 | /* Additional Checks */ 37 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 38 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 39 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 40 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 41 | 42 | /* Module Resolution Options */ 43 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 44 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 45 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 46 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 47 | // "typeRoots": [], /* List of folders to include type definitions from. */ 48 | // "types": [], /* Type declaration files to be included in compilation. */ 49 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 50 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 51 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 52 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 53 | 54 | /* Source Map Options */ 55 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 56 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 57 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 58 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 59 | 60 | /* Experimental Options */ 61 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 62 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 63 | 64 | /* Advanced Options */ 65 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 66 | }, 67 | "include": [ 68 | "./src" 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /update.md: -------------------------------------------------------------------------------- 1 | ## changelog 2 | 3 | ## 最新的代码(lastest) v0.8.0-beta.1 4 | 1. patch #103、#97 5 | 2. fix: #102: scrollX cause call onReachEnd 6 | 7 | ## v0.7.6 8 | 1. fix: scrollX problem 9 | 2. open #91 10 | ## v0.7.5 11 | 1. fix: #87. (表格收起时, 高度变化导致的空白问题) 12 | 2. fix: #90. (same as 1) 13 | 3. fix: #91. (firefox兼容问题, 底部有空白且一直回弹) 14 | 15 | ## v0.7.4 16 | 1. feat: onListRender & debounceListRenderMS & demo. 17 | 2. remove console.log 18 | 3. fix #73 19 | 20 | ## v0.7.3 21 | 1. support react18. 22 | 23 | ## v0.7.2 24 | 1. fix: scrollTo row unusual 25 | 26 | ## v0.7.1 27 | 1. fix: scrollTo fault. 28 | 2. Fix the error caused by less data when the tree table is changed 29 | 30 | ## v0.7.0 31 | 1. fix: #51 32 | 2. new option 'resetTopWhenDataChange' 33 | 34 | ## v0.6.5-beta.2 35 | 1. fix: scrollTop is not reset when data change on same data length 36 | 2. optimized relate code on data change 37 | 38 | ## v0.6.5-beta.0 39 | 1. fix: sometimes data is not complete in hot update when debugging 40 | 41 | ## v0.6.4 42 | 1. fix #48 43 | ## v0.6.3 44 | 1. fix: 树形表格看不全数据的问题. (fix tree table bug of incomplete data) 45 | 2. fix: fix offsetStart bug that multiple rows become 0 rows 46 | 3. fix: 处理notRrefresh报错问题. 47 | 4. add eslint 48 | 49 | ## v0.6.2 50 | 1. fix: 树形表格最后行有很多子数据的时候, 滚动会出现问题. (fix tree table bug) 51 | ## v0.6.1 52 | 1. 去掉start的判断. (remove the judgment of the variable start) 53 | ## v0.6.0 54 | 1. fix: 处理存在多个虚拟表格的时候, 全局属性会覆盖的问题. (fix bug that global attr is overridden when exist different virtual tables) 55 | ## v0.5.8 56 | 1. fix: 处理滚动条滚到最后面的时候 会出现抖动的问题 & 删除无用代码. (fix bug that data shake when scroll on final) 57 | 58 | ## v0.5.7 59 | 1. VRow向外暴露ref. (can do this like VRow(props, ref)) 60 | 61 | ## v0.5.6 62 | 1. 向外暴露onScroll. (export onscroll) 63 | 2. 去掉多余的10padding. (remove excess 10 padding) 64 | 65 | ## v0.5.5 66 | 1. feat: 支持scrollTo的导出. (support scrollTo) 67 | 68 | ## v0.5.4 69 | 1. feat: 扩展onReachEnd, 支持无限加载. (support load more data on a page.) 70 | 71 | ## v0.5.3 72 | 1. (发布错误) 73 | 74 | ## v0.5.2 75 | 1. 将样式文件的width: inherit 改为 100%. 76 | 77 | ## v0.5.1 78 | 1. update rep info. 79 | 80 | ## v0.5.0 81 | 1. update && adjust npm package. 82 | 83 | ## v0.4.9 84 | 1. 将ISC协议改为MIT. 85 | 86 | ## v0.4.8 87 | 1. fix: 在length为0的时候, 没有更新totalLen的问题. (fix data length is 0, not update totalLen.) 88 | 89 | ## v0.4.8-beta 90 | 1. feat: support #10. 91 | 92 | ## v0.4.7 93 | 1. fix: #8 处理rc-table v7.8.2的改动. 同时兼容上一个版本. (deal rc-table v7.8.2 change) 94 | 95 | ## v0.4.6 96 | 1. fix: #7 处理antd 4.3.5 边框不显示的问题. (fix bug: no border) 97 | 98 | ## v0.4.5 99 | 1. fix: 当scroll值有x: max-content且列数很少的时候, 会出现表头和内容行补齐的样式问题. (fix when scroll={x: 'max-content'} and little columns, content unable to align header.) 100 | 101 | reappear: (https://codesandbox.io/s/festive-worker-wc5wp for v0.4.4) 102 | 103 | already fix: (https://codesandbox.io/s/festive-worker-wc5wp for v0.4.5) 104 | 105 | ## v0.4.4 106 | 1. revert 0.4.3. 107 | 108 | ## v0.4.3 & v0.4.3-beta.0 109 | 1. 内部代码不再将scrollY的值作为全局变量, 考虑到可能有一个页面有多个虚拟列表的情况。 110 | 111 | ## v0.4.2 112 | 1. 还是采用导入的样式文件的方式, 取代掉VCell的内联样式. (performance question) 113 | 114 | ## v0.4.1 115 | 1. fix: 处理因样式问题导致 antd fixed表格 阴影失效的问题. (fix antd box-shadow not work) 116 | 2. 新增了VCell, 会替代components的body cell. 每个td下会有一个div包裹. 117 | 118 | ## v0.4.0 119 | 1. fix: 处理css样式失效的问题. (fix internal css not work) 120 | 121 | ## v0.3.3 122 | 1. revert v0.3.2 123 | 124 | ## v0.3.2 125 | 1. fix: 处理因样式问题导致 antd fixed表格 阴影失效的问题. (fix antd box-shadow not work) 126 | 127 | ## v0.3.1 128 | 1. fix: 处理使用calc()下的高度问题. 129 | 130 | ## v0.3.0 131 | 1. feat: 更新支持antd4的Table. 132 | 133 | ## v0.2.8 134 | 1. 去掉了多余且不灵活的字体设置, 让table字体可自定义. 135 | 136 | ## v0.2.7 137 | 1. 处理tableScrollY <= 0的情况. 138 | 139 | ## v0.2.7beta1 140 | 1. 压缩发布包的代码, 大小变为17k 141 | 142 | ## v0.2.5 & v0.2.6 143 | 1. 去掉一些多余的rollup配置. 文件大小160多k变为50多K. 144 | 145 | ## v0.2.4 146 | 1. 修复在页面空间足够的时候却还出现滚动条的问题. 147 | 2. 清除滚动到最后的空白. 148 | 149 | ## v0.2.3 150 | 1. 修复在tableHeight < tableScrollY的时候 数据会抖动的情况 151 | 152 | ## v0.2.2 (此版本后都支持分页) 153 | 1. 修复tableScrollY变化的时候 会出现表格不全的问题. 154 | 155 | ## v0.2.1 156 | 1. 支持scrollY设置为calc() 157 | 2. 调整节流时间为100ms 158 | 159 | ==================================================== 160 | --------------------------------------------------------------------------------