├── .babelrc
├── .eslintrc.js
├── .gitignore
├── LICENSE
├── README.md
├── docs
├── Viewport-API.md
└── VirtualScroller-API.md
├── index.html
├── package.json
├── rollup.config.js
├── src
├── components
│ ├── List.js
│ ├── Updater.js
│ └── VirtualScroller.js
├── index.js
├── modules
│ ├── Position.js
│ ├── Rectangle.js
│ ├── ScrollTracker.js
│ ├── Viewport.js
│ └── requestAnimationFrame.js
└── utlis
│ ├── createScheduler.js
│ ├── findIndex.js
│ └── findNewSlice.js
├── test
└── findNewSlice.js
└── website
├── .gitignore
├── core
└── Footer.js
├── i18n
└── en.json
├── package.json
├── pages
├── demo-simple-list.html
├── demo-window-as-scroller.html
└── en
│ ├── demo-index.js
│ ├── help.js
│ ├── index.js
│ └── users.js
├── sidebars.json
├── siteConfig.js
└── static
├── css
└── custom.css
├── img
├── docusaurus.svg
├── favicon.png
├── favicon
│ └── favicon.ico
└── oss_logo.png
└── js
├── react-virtual-scroller.umd.js
└── react-virtual-scroller.umd.min.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "env",
5 | {
6 | "modules": false
7 | }
8 | ]
9 | ],
10 | "plugins": [
11 | ["transform-object-rest-spread", { "useBuiltIns": true }],
12 | "transform-class-properties",
13 | [
14 | "transform-react-jsx",
15 | {
16 | "useBuiltIns": true
17 | }
18 | ],
19 | "transform-react-remove-prop-types"
20 | ],
21 | "env": {
22 | "commonjs": {
23 | "presets": [
24 | [
25 | "env",
26 | {
27 | "modules": "commonjs"
28 | }
29 | ]
30 | ],
31 | "plugins": ["transform-class-properties"]
32 | },
33 | "esm": {
34 | "presets": [
35 | [
36 | "env",
37 | {
38 | "modules": false
39 | }
40 | ]
41 | ],
42 | "plugins": ["transform-class-properties"]
43 | },
44 | "umd": {
45 | "presets": [
46 | [
47 | "env",
48 | {
49 | "targets": {
50 | "ie": 9
51 | },
52 | "modules": false
53 | }
54 | ]
55 | ],
56 | "plugins": ["transform-class-properties"]
57 | },
58 | "test": {
59 | "plugins": ["transform-es2015-modules-commonjs"]
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | 'eslint:recommended',
4 | 'plugin:react/recommended'
5 | ],
6 | plugins: ['react'],
7 | parser: 'babel-eslint',
8 | parserOptions: {
9 | ecmaVersion: 6,
10 | sourceType: 'module',
11 | ecmaFeatures: {
12 | jsx: true,
13 | generators: true,
14 | experimentalObjectRestSpread: true,
15 | },
16 | },
17 | env: {
18 | browser: true,
19 | commonjs: true,
20 | es6: true,
21 | jest: true,
22 | node: true,
23 | },
24 | globals: {},
25 | rules: {
26 | 'prefer-const': 'error',
27 | 'object-shorthand': "error",
28 | 'no-param-reassign': ['error', { props: true }],
29 | 'no-underscore-dangle': 0,
30 | 'no-bitwise': 0,
31 | 'no-use-before-define': 'error',
32 | 'no-shadow': ['error', { builtinGlobals: false, hoist: 'functions' }],
33 | 'new-cap': 1,
34 | eqeqeq: 1,
35 | 'no-console': 1,
36 | 'no-unused-vars': [
37 | 'error',
38 | {
39 | args: 'after-used',
40 | argsIgnorePattern: '^_$',
41 | },
42 | ],
43 | camelcase: 1,
44 | },
45 | };
46 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | package-lock.json
4 | yarn.lock
5 |
6 | /dist
7 | /es
8 | /lib
9 | /website/build/
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 X.L
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 | # react-virtual-scroller
2 |
3 | A react implementation of [twitter VirtualScroller](http://itsze.ro/blog/2017/04/09/infinite-list-and-react.html).
4 |
5 | More in [Document](https://liximomo.github.io/react-virtual-scroller/).
6 |
--------------------------------------------------------------------------------
/docs/Viewport-API.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: Viewport
3 | title: Viewport
4 | ---
5 |
6 |
7 | ## Constructor
8 | ```js
9 | constructor(window, scroller = window)
10 | ```
11 |
12 | ### window
13 | `window` object.
14 |
15 | ### scroller
16 | Scrollable container element of `VirtualScroller` or window.
17 |
--------------------------------------------------------------------------------
/docs/VirtualScroller-API.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: VirtualScroller
3 | title: VirtualScroller
4 | ---
5 |
6 | |Name|Type|Default|Description|
7 | |:---|:---|:---|:---|
8 | |items|any[]||Data list.|
9 | |renderItem|function||`(item, index) => element`. Responsible for rendering a item. |
10 | |viewport|object||A instance of [Viewport](Viewport.html).|
11 | |identityFunction|function| a => a|`item => number or string`. return a unique identity for item.|
12 | |offscreenToViewportRatio|number|1.8|Ratio to determine how height to render above/below the visible bounds of the list. |
13 | |assumedItemHeight|number|400|Estimated average height of items.|
14 | |nearStartProximityRatio|number|1.75|distance between start and nearStart / `viewport` height.|
15 | |nearEndProximityRatio|number|0.25|distance between nearEnd and end / `viewport` height.|
16 | |onAtStart|function|| `info => void` called when scroll to list start.|
17 | |onNearStart|function||called when scroll to list near start.|
18 | |onNearEnd|function||called when scroll to list near end.|
19 | |onAtEnd|function||called when scroll to list end.|
20 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | preview
8 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
135 |
136 |
137 |
138 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@liximomo/react-virtual-scroller",
3 | "version": "1.0.0-alpha.3",
4 | "main": "lib/index.js",
5 | "jsnext:main": "es/index.js",
6 | "module": "es/index.js",
7 | "browser": "dist/react-virtual-scroller.umd.js",
8 | "author": "liximomo (https://liximomo.github.io/)",
9 | "repository": {
10 | "url": "git@github.com:liximomo/react-virtual-scroller.git",
11 | "type": "git"
12 | },
13 | "license": "MIT",
14 | "files": [
15 | "dist",
16 | "es",
17 | "lib"
18 | ],
19 | "scripts": {
20 | "start": "npm-run-all --parallel watch:*",
21 | "watch:js": "BABEL_ENV=umd NODE_ENV=development rollup -c -w",
22 | "prebuild": "npm run lint",
23 | "build": "npm run build:commonjs && npm run build:esm && npm run build:umd && npm run build:umd:min",
24 | "build:commonjs": "BABEL_ENV=commonjs NODE_ENV=production babel src --out-dir lib",
25 | "build:esm": "BABEL_ENV=esm NODE_ENV=production babel src --out-dir es",
26 | "build:umd": "BABEL_ENV=umd NODE_ENV=development rollup -c",
27 | "build:umd:min": "BABEL_ENV=umd NODE_ENV=production rollup -c",
28 | "postbuild": "cp dist/* website/static/js/",
29 | "lint": "eslint src/**/*.js",
30 | "test": "jest"
31 | },
32 | "devDependencies": {
33 | "babel-cli": "^6.26.0",
34 | "babel-core": "^6.26.0",
35 | "babel-eslint": "^8.1.2",
36 | "babel-plugin-external-helpers": "^6.22.0",
37 | "babel-plugin-transform-class-properties": "^6.24.1",
38 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
39 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
40 | "babel-plugin-transform-react-jsx": "^6.24.1",
41 | "babel-plugin-transform-regenerator": "^6.26.0",
42 | "babel-preset-env": "^1.6.1",
43 | "eslint": "^4.14.0",
44 | "jest": "^22.4.2",
45 | "npm-run-all": "^4.1.2",
46 | "rollup": "^0.53.0",
47 | "rollup-plugin-babel": "^3.0.3",
48 | "rollup-plugin-commonjs": "^8.2.6",
49 | "rollup-plugin-node-resolve": "^3.0.0",
50 | "rollup-plugin-replace": "^2.0.0",
51 | "rollup-plugin-uglify": "^2.0.1"
52 | },
53 | "dependencies": {
54 | "babel-plugin-transform-react-remove-prop-types": "^0.4.13",
55 | "eslint-plugin-react": "^7.7.0",
56 | "lodash.throttle": "^4.1.1",
57 | "prop-types": "^15.6.1",
58 | "recomputed": "1.0.1-alpha.4"
59 | },
60 | "peerDependencies": {
61 | "react": "^16.2.0",
62 | "react-dom": "^16.2.0"
63 | },
64 | "jest": {
65 | "testMatch": [
66 | "**/test/*.js"
67 | ]
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import resolve from 'rollup-plugin-node-resolve';
2 | import commonjs from 'rollup-plugin-commonjs';
3 | import babel from 'rollup-plugin-babel';
4 | import replace from 'rollup-plugin-replace'
5 | import uglify from 'rollup-plugin-uglify';
6 | import pkg from './package.json';
7 |
8 | const input = 'src/index.js';
9 |
10 | const onwarn = function(warning) {
11 | // Skip certain warnings
12 |
13 | // should intercept ... but doesn't in some rollup versions
14 | if ( warning.code === 'THIS_IS_UNDEFINED' ) { return; }
15 |
16 | // console.warn everything else
17 | console.log('\x1b[33m(!) %s\x1b[0m', warning.message || warning);
18 | };
19 |
20 | const plugins = [
21 | replace({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) }),
22 | resolve(), // so Rollup can resolve node_modules
23 | babel({
24 | exclude: ['node_modules/**'],
25 | plugins: ["external-helpers"]
26 | }),
27 | commonjs(), // so Rollup can convert commonjs to an ES module
28 | ];
29 |
30 |
31 | // browser-friendly UMD build
32 | const umd = {
33 | input,
34 | output: {
35 | file: pkg.browser,
36 | format: 'umd',
37 | name: 'VirtualScroller',
38 | },
39 | onwarn,
40 | plugins
41 | };
42 |
43 | if (process.env.NODE_ENV === 'development') {
44 | // nothing current
45 | } else if (process.env.NODE_ENV === 'production') {
46 | const prodPlugins = [
47 | uglify({
48 | compress: {
49 | warnings: false,
50 | // Disabled because of an issue with Uglify breaking seemingly valid code:
51 | // https://github.com/facebookincubator/create-react-app/issues/2376
52 | // Pending further investigation:
53 | // https://github.com/mishoo/UglifyJS2/issues/2011
54 | comparisons: false,
55 | },
56 | output: {
57 | comments: false,
58 | // Turned on because emoji and regex is not minified properly using default
59 | // https://github.com/facebookincubator/create-react-app/issues/2488
60 | ascii_only: true,
61 | },
62 | })
63 | ]
64 | umd.output.file = umd.output.file.replace(/js$/, 'min.js');
65 | umd.plugins = umd.plugins.concat(prodPlugins);
66 | }
67 |
68 | export default umd;
69 |
--------------------------------------------------------------------------------
/src/components/List.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const nullFunc = () => null;
5 |
6 | function defaultGetHeightForDomNode(node) {
7 | return node ? node.getBoundingClientRect().height : 0;
8 | }
9 |
10 | class List extends React.PureComponent {
11 | static defaultProps = {
12 | getHeightForDomNode: defaultGetHeightForDomNode,
13 | };
14 |
15 | static propTypes = {
16 | getHeightForDomNode: PropTypes.func,
17 | blankSpaceAbove: PropTypes.number.isRequired,
18 | blankSpaceBelow: PropTypes.number.isRequired,
19 | renderItem: PropTypes.func.isRequired,
20 | list: PropTypes.arrayOf(PropTypes.any),
21 | };
22 |
23 | constructor(props) {
24 | super(props);
25 |
26 | this._refs = {};
27 | this._handleViewRefUpdate = this._handleViewRefUpdate.bind(this);
28 | }
29 |
30 | _handleViewRefUpdate(ref) {
31 | this._view = ref;
32 | }
33 |
34 | _handleItemRefUpdate(id, ref) {
35 | if (!ref) {
36 | delete this._refs[id];
37 | } else {
38 | this._refs[id] = ref;
39 | }
40 | }
41 |
42 | _renderContent() {
43 | return this.props.list.map((item, index) => {
44 | const id = item.id;
45 | const data = item.data;
46 | const reactElement = this.props.renderItem(data, index);
47 | const savedRefFunc = 'function' === typeof reactElement.ref ? reactElement.ref : nullFunc;
48 | return React.cloneElement(reactElement, {
49 | key: id,
50 | ref: r => {
51 | this._handleItemRefUpdate(id, r);
52 | savedRefFunc(r);
53 | },
54 | });
55 | });
56 | }
57 |
58 | getWrapperNode() {
59 | return this._view;
60 | }
61 |
62 | getItemHeights() {
63 | const { list, getHeightForDomNode } = this.props;
64 |
65 | return list.reduce((heightsMap, item) => {
66 | const id = item.id;
67 | const node = this._refs[id];
68 |
69 | // eslint-disable-next-line no-param-reassign
70 | heightsMap[id] = getHeightForDomNode(node);
71 | return heightsMap;
72 | }, {});
73 | }
74 |
75 | render() {
76 | const { blankSpaceAbove, blankSpaceBelow } = this.props;
77 |
78 | return (
79 |
87 | {this._renderContent()}
88 |
89 | );
90 | }
91 | }
92 |
93 | export default List;
94 |
--------------------------------------------------------------------------------
/src/components/Updater.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import recomputed from 'recomputed';
4 | import throttle from 'lodash.throttle';
5 | import List from './List';
6 | import createScheduler from '../utlis/createScheduler';
7 | import findIndex from '../utlis/findIndex';
8 | import findNewSlice from '../utlis/findNewSlice';
9 | import Position from '../modules/Position';
10 | import Rectangle from '../modules/Rectangle';
11 | import Viewport from '../modules/Viewport';
12 | import requestAnimationFrame from '../modules/requestAnimationFrame';
13 |
14 | function findAnchor(prevPos, nextPos) {
15 | const viewportRect = prevPos.getViewportRect();
16 |
17 | const findBest = (list, comparator) => {
18 | if (list.length <= 0) {
19 | return null;
20 | }
21 |
22 | return list.reduce((best, item) => {
23 | return comparator(item, best) > 0 ? item : best;
24 | });
25 | };
26 |
27 | const inViewport = rect => rect && rect.doesIntersectWith(viewportRect);
28 |
29 | const distanceToViewportTop = rect =>
30 | rect ? Math.abs(viewportRect.getTop() - rect.getTop()) : 1 / 0;
31 |
32 | const boolCompartor = (getValue, a, b) => {
33 | const aResult = getValue(a);
34 | const bResult = getValue(b);
35 | if (aResult && !bResult) {
36 | return 1;
37 | } else if (!aResult && bResult) {
38 | return -1;
39 | } else {
40 | return 0;
41 | }
42 | };
43 |
44 | const numberCompartor = (getValue, a, b) => {
45 | const aResult = getValue(a);
46 | const bResult = getValue(b);
47 | return bResult - aResult;
48 | };
49 |
50 | const bothRendered = nextPos.getList().filter(item => {
51 | const id = item.id;
52 | return prevPos.isRendered(id) && nextPos.isRendered(id);
53 | });
54 |
55 | if (bothRendered.length <= 0) {
56 | return null;
57 | }
58 |
59 | const theBest = findBest(bothRendered, (current, best) => {
60 | const item = prevPos.getItemRect(current.id);
61 | const bestItem = prevPos.getItemRect(best.id);
62 | return (
63 | boolCompartor(inViewport, item, bestItem) ||
64 | numberCompartor(distanceToViewportTop, item, bestItem)
65 | );
66 | });
67 |
68 | return theBest;
69 | }
70 |
71 | function offsetCorrection(prevPos, nextPos) {
72 | const anchor = findAnchor(prevPos, nextPos);
73 | if (!anchor) {
74 | return 0;
75 | }
76 |
77 | const anchorId = anchor.id;
78 | const offsetToViewport =
79 | prevPos.getItemRect(anchorId).getTop() - prevPos.getViewportRect().getTop();
80 | return (
81 | nextPos.getItemRect(anchorId).getTop() - nextPos.getViewportRect().getTop() - offsetToViewport
82 | );
83 | }
84 |
85 | function collectRect(list, heights, defaultHeight) {
86 | const rects = {};
87 | let top = 0;
88 | list.forEach(item => {
89 | const id = item.id;
90 | const height = heights[id] !== undefined ? heights[id] : defaultHeight;
91 | rects[id] = new Rectangle({
92 | top,
93 | height,
94 | });
95 |
96 | // eslint-disable-next-line no-param-reassign
97 | top += height;
98 | });
99 |
100 | return rects;
101 | }
102 |
103 | class Updater extends React.PureComponent {
104 | static propTypes = {
105 | list: PropTypes.arrayOf(PropTypes.any).isRequired,
106 | renderItem: PropTypes.func.isRequired,
107 | viewport: PropTypes.instanceOf(Viewport).isRequired,
108 | onPositioningUpdate: PropTypes.func,
109 | assumedItemHeight: PropTypes.number,
110 | offscreenToViewportRatio: PropTypes.number,
111 | };
112 |
113 | static defaultProps = {
114 | offscreenToViewportRatio: 1.8,
115 | assumedItemHeight: 400,
116 | };
117 |
118 | constructor(props) {
119 | super(props);
120 |
121 | this._heights = {};
122 |
123 | /* eslint-disable no-shadow */
124 | this._getRectangles = recomputed(
125 | this,
126 | props => props.list,
127 | (_, __, obj) => obj._heights,
128 | props => props.assumedItemHeight,
129 | collectRect
130 | );
131 |
132 | this._getSlice = recomputed(
133 | this,
134 | props => props.list,
135 | (_, state) => state.sliceStart,
136 | (_, state) => state.sliceEnd,
137 | (list, sliceStart, sliceEnd) => list.slice(sliceStart, sliceEnd)
138 | );
139 | /* eslint-enable no-shadow */
140 |
141 | // $todo add initItemIndex props
142 | this.state = this._getDefaultSlice(props.list);
143 |
144 | this._handleRefUpdate = this._handleRefUpdate.bind(this);
145 | this._update = this._update.bind(this);
146 | this._notifyPositioning = this._notifyPositioning.bind(this);
147 |
148 | this._scheduleUpdate = createScheduler(this._update, requestAnimationFrame);
149 | this._schedulePositioningNotification = createScheduler(
150 | this._notifyPositioning,
151 | requestAnimationFrame
152 | );
153 | this._handleScroll = throttle(this._scheduleUpdate, 100, { trailing: true });
154 | }
155 |
156 | _handleRefUpdate(ref) {
157 | this._listRef = ref;
158 | }
159 |
160 | _onHeightsUpdate(prevPostion, nextPostion) {
161 | this.props.viewport.scrollBy(offsetCorrection(prevPostion, nextPostion));
162 | }
163 |
164 | _recordHeights() {
165 | if (!this._listRef) {
166 | return {
167 | heightDelta: 0,
168 | };
169 | }
170 |
171 | const itemHeights = this._listRef.getItemHeights();
172 |
173 | const heightDelta = Object.keys(itemHeights).reduce((accHeight, key) => {
174 | const itemHeight = this._heights.hasOwnProperty(key)
175 | ? this._heights[key]
176 | : this.props.assumedItemHeight;
177 | return accHeight + itemHeights[key] - itemHeight;
178 | }, 0);
179 |
180 | if (heightDelta !== 0) {
181 | this._heights = Object.assign({}, this._heights, itemHeights);
182 | }
183 |
184 | return {
185 | heightDelta,
186 | };
187 | }
188 |
189 | _postRenderProcessing(hasListChanged) {
190 | const heightState = this._recordHeights();
191 | const wasHeightChange = heightState.heightDelta !== 0;
192 | if ((hasListChanged || wasHeightChange) && this._prevPositioning) {
193 | this._onHeightsUpdate(this._prevPositioning, this.getPositioning());
194 | }
195 |
196 | if (hasListChanged || Math.abs(heightState.heightDelta) >= this.props.assumedItemHeight) {
197 | this._scheduleUpdate();
198 | }
199 | this._schedulePositioningNotification();
200 | }
201 |
202 | _getDefaultSlice(list) {
203 | const startIndex = 0;
204 | const withNewList = {
205 | ...this,
206 | props: {
207 | ...this.props,
208 | list,
209 | },
210 | };
211 |
212 | const viewport = this.props.viewport;
213 | const viewportHeight = viewport.getRect().getHeight();
214 | const rects = this._getRectangles(withNewList);
215 |
216 | const startId = list[startIndex].id;
217 | const startOffset = rects[startId].getTop();
218 | let endIndex = findIndex(
219 | list,
220 | item => rects[item.id].getTop() - startOffset >= viewportHeight,
221 | startIndex
222 | );
223 | if (endIndex < 0) {
224 | endIndex = list.length;
225 | }
226 |
227 | return {
228 | sliceStart: startIndex,
229 | sliceEnd: endIndex,
230 | };
231 | }
232 |
233 | _computeBlankSpace() {
234 | const { list } = this.props;
235 | const { sliceStart, sliceEnd } = this.state;
236 | const rects = this._getRectangles();
237 | const lastIndex = list.length - 1;
238 | return {
239 | blankSpaceAbove:
240 | list.length <= 0 ? 0 : rects[list[sliceStart].id].getTop() - rects[list[0].id].getTop(),
241 | blankSpaceBelow:
242 | sliceEnd >= list.length
243 | ? 0
244 | : rects[list[lastIndex].id].getBottom() - rects[list[sliceEnd].id].getTop(),
245 | };
246 | }
247 |
248 | _getRelativeViewportRect() {
249 | if (!this._listRef) {
250 | return new Rectangle({ top: 0, height: 0 });
251 | }
252 |
253 | const listNode = this._listRef.getWrapperNode();
254 | // const offsetTop = Math.ceil(listNode.getBoundingClientRect().top);
255 | return this.props.viewport.getRectRelativeTo(listNode);
256 | }
257 |
258 | _update() {
259 | const { list } = this.props;
260 |
261 | if (this._unmounted || 0 === list.length) {
262 | return;
263 | }
264 |
265 | const viewportRect = this._getRelativeViewportRect();
266 | const offscreenHeight = viewportRect.getHeight() * this.props.offscreenToViewportRatio;
267 | const renderRectTop = viewportRect.getTop() - offscreenHeight;
268 | const renderRectBottom = viewportRect.getBottom() + offscreenHeight;
269 |
270 | const rects = this._getRectangles();
271 |
272 | let startIndex = findIndex(list, item => rects[item.id].getBottom() > renderRectTop);
273 | if (startIndex < 0) {
274 | startIndex = list.length - 1;
275 | }
276 |
277 | let endIndex = findIndex(list, item => rects[item.id].getTop() >= renderRectBottom, startIndex);
278 | if (endIndex < 0) {
279 | endIndex = list.length;
280 | }
281 |
282 | this._schedulePositioningNotification();
283 | this._setSlice(startIndex, endIndex);
284 | }
285 |
286 | _setSlice(start, end) {
287 | const { sliceStart, sliceEnd } = this.state;
288 |
289 | if (sliceStart !== start || sliceEnd !== end) {
290 | this.setState({
291 | sliceStart: start,
292 | sliceEnd: end,
293 | });
294 | }
295 | }
296 |
297 | _notifyPositioning() {
298 | if (!this._unmounted && this.props.onPositioningUpdate) {
299 | this.props.onPositioningUpdate(this.getPositioning());
300 | }
301 | }
302 |
303 | scrollToIndex(index) {
304 | const { list, viewport } = this.props;
305 | const targetItem = list[index];
306 | const rects = this._getRectangles();
307 | viewport.scrollTo(rects[targetItem.id].getTop() + viewport.getOffsetTop());
308 | }
309 |
310 | getPositioning() {
311 | const { sliceStart, sliceEnd } = this.state;
312 | return new Position({
313 | viewportRectangle: this._getRelativeViewportRect(),
314 | list: this.props.list,
315 | rectangles: this._getRectangles(),
316 | sliceStart,
317 | sliceEnd,
318 | });
319 | }
320 |
321 | componentDidMount() {
322 | this._unlistenScroll = this.props.viewport.addScrollListener(this._handleScroll);
323 | this._postRenderProcessing(true);
324 | }
325 |
326 | componentWillReceiveProps(nextProps) {
327 | const prevList = this.props.list;
328 | const prevState = this.state;
329 | const nextList = nextProps.list;
330 | if (prevList !== nextList) {
331 | const slice =
332 | findNewSlice(prevList, nextList, prevState.sliceStart, prevState.sliceEnd) ||
333 | this._getDefaultSlice(nextList);
334 | this._setSlice(slice.sliceStart, slice.sliceEnd);
335 | }
336 | }
337 |
338 | componentWillUpdate() {
339 | this._prevPositioning = this.getPositioning();
340 | }
341 |
342 | componentDidUpdate(prevProps) {
343 | this._postRenderProcessing(prevProps.list !== this.props.list);
344 | }
345 |
346 | componentWillUnmount() {
347 | this._unmounted = true;
348 |
349 | if (this._unlistenScroll) {
350 | this._unlistenScroll();
351 | }
352 | }
353 |
354 | render() {
355 | const { blankSpaceAbove, blankSpaceBelow } = this._computeBlankSpace();
356 | const { sliceStart } = this.state;
357 |
358 | // eslint-disable-next-line
359 | const { renderItem } = this.props;
360 |
361 | return (
362 | {
368 | return renderItem(data, sliceStart + index);
369 | }}
370 | />
371 | );
372 | }
373 | }
374 |
375 | export default Updater;
376 |
--------------------------------------------------------------------------------
/src/components/VirtualScroller.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import recomputed from 'recomputed';
4 | import Updater from './Updater';
5 | import Viewport from '../modules/Viewport';
6 | import ScrollTracker, { Condition } from '../modules/ScrollTracker';
7 |
8 | const defaultIdentityFunction = a => a.id;
9 |
10 | const nullFunction = () => null;
11 |
12 | class VirtualScroller extends React.PureComponent {
13 | static propTypes = {
14 | items: PropTypes.arrayOf(PropTypes.any).isRequired,
15 | renderItem: PropTypes.func.isRequired,
16 | viewport: PropTypes.instanceOf(Viewport).isRequired,
17 | identityFunction: PropTypes.func,
18 | offscreenToViewportRatio: PropTypes.number,
19 | assumedItemHeight: PropTypes.number,
20 | nearEndProximityRatio: PropTypes.number,
21 | nearStartProximityRatio: PropTypes.number,
22 | onAtStart: PropTypes.func,
23 | onNearStart: PropTypes.func,
24 | onNearEnd: PropTypes.func,
25 | onAtEnd: PropTypes.func,
26 | };
27 |
28 | static defaultProps = {
29 | identityFunction: defaultIdentityFunction,
30 | offscreenToViewportRatio: 1.8,
31 | assumedItemHeight: 400,
32 | nearEndProximityRatio: 1.75,
33 | nearStartProximityRatio: 0.25,
34 | onAtStart: nullFunction,
35 | onNearStart: nullFunction,
36 | onNearEnd: nullFunction,
37 | onAtEnd: nullFunction,
38 | };
39 |
40 | constructor(props) {
41 | super(props);
42 |
43 | /* eslint-disable no-shadow */
44 | this._getList = recomputed(
45 | this,
46 | props => props.items,
47 | items => {
48 | const idMap = {};
49 | const resultList = [];
50 |
51 | items.forEach(item => {
52 | const id = this.props.identityFunction(item);
53 | if (idMap.hasOwnProperty(id)) {
54 | // eslint-disable-next-line no-console
55 | console.warn(
56 | `Duplicate item id generated in VirtualScroller. Latter item (id = "${id}") will be discarded`
57 | );
58 | return;
59 | }
60 |
61 | resultList.push({
62 | id,
63 | data: item,
64 | });
65 | idMap[id] = true;
66 | });
67 |
68 | return resultList;
69 | }
70 | );
71 | /* eslint-enable no-shadow */
72 |
73 | this._handleRefUpdate = this._handleRefUpdate.bind(this);
74 | this._handlePositioningUpdate = this._handlePositioningUpdate.bind(this);
75 | this._createScrollTracker(props.nearStartProximityRatio, props.nearEndProximityRatio);
76 | }
77 |
78 | _handleRefUpdate(ref) {
79 | this._updater = ref;
80 | }
81 |
82 | _handlePositioningUpdate(position) {
83 | if (this._scrollTracker) {
84 | this._scrollTracker.handlePositioningUpdate(position);
85 | }
86 | }
87 |
88 | _createScrollTracker(nearStartProximityRatio, nearEndProximityRatio) {
89 | this._scrollTracker = new ScrollTracker([
90 | {
91 | condition: Condition.nearTop(5),
92 | callback: info => {
93 | return this.props.onAtStart(info);
94 | },
95 | },
96 | {
97 | condition: Condition.nearTopRatio(nearStartProximityRatio),
98 | callback: info => {
99 | return this.props.onNearStart(info);
100 | },
101 | },
102 | {
103 | condition: Condition.nearBottomRatio(nearEndProximityRatio),
104 | callback: info => {
105 | return this.props.onNearEnd(info);
106 | },
107 | },
108 | {
109 | condition: Condition.nearBottom(5),
110 | callback: info => {
111 | return this.props.onAtEnd(info);
112 | },
113 | },
114 | ]);
115 | }
116 |
117 | // only can scroll to knwon height item
118 | scrollToIndex(index) {
119 | if (this._updater) {
120 | this._updater.scrollToIndex(index);
121 | }
122 | }
123 |
124 | componentWillReceiveProps(nextProps) {
125 | this._createScrollTracker(nextProps.nearStartProximityRatio, nextProps.nearEndProximityRatio);
126 | }
127 |
128 | render() {
129 | const { renderItem, assumedItemHeight, viewport } = this.props;
130 |
131 | return (
132 |
140 | );
141 | }
142 | }
143 |
144 | export default VirtualScroller;
145 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import VirtualScroller from './components/VirtualScroller';
2 | import Viewport from './modules/Viewport';
3 |
4 | export default VirtualScroller;
5 |
6 | export { Viewport };
7 |
--------------------------------------------------------------------------------
/src/modules/Position.js:
--------------------------------------------------------------------------------
1 | import Rectangle from './Rectangle';
2 | import findIndex from '../utlis/findIndex';
3 |
4 | class Position {
5 | constructor({ viewportRectangle, list, rectangles, sliceStart, sliceEnd }) {
6 | this._viewportRectangle = viewportRectangle;
7 | this._list = list;
8 | this._rectangles = rectangles;
9 | this._sliceStart = sliceStart;
10 | this._sliceEnd = sliceEnd;
11 | }
12 |
13 | getViewportRect() {
14 | return this._viewportRectangle;
15 | }
16 |
17 | getListRect() {
18 | const list = this._list;
19 | if (list.length <= 0) {
20 | return new Rectangle({
21 | top: 0,
22 | height: 0,
23 | });
24 | }
25 |
26 | const rects = this._rectangles;
27 | const firstItemId = list[0].id;
28 | const lastItemId = list[list.length - 1].id;
29 | const top = rects[firstItemId].getTop();
30 | const height = rects[lastItemId].getBottom() - top;
31 | return new Rectangle({
32 | top,
33 | height,
34 | });
35 | }
36 |
37 | getAllItems() {
38 | return this._list.map(item => {
39 | const id = item.id;
40 | return {
41 | id,
42 | rectangle: this._rectangles[id],
43 | };
44 | });
45 | }
46 |
47 | getList() {
48 | return this._list;
49 | }
50 |
51 | getItemRect(id) {
52 | return this._rectangles[id];
53 | }
54 |
55 | findVisibleItems() {
56 | const viewportRectangle = this._viewportRectangle;
57 | const rectangles = this._rectangles;
58 | const list = this._list;
59 | const startIndex = findIndex(list, item => {
60 | const id = item.id;
61 | return rectangles[id].doesIntersectWith(viewportRectangle);
62 | });
63 | if (startIndex < 0) {
64 | return [];
65 | }
66 |
67 | let endIndex = findIndex(
68 | list,
69 | item => {
70 | const id = item.id;
71 | return !rectangles[id].doesIntersectWith(viewportRectangle);
72 | },
73 | startIndex
74 | );
75 | if (endIndex < 0) {
76 | endIndex = list.length;
77 | }
78 |
79 | return list
80 | .slice(startIndex, endIndex)
81 | .filter(item => {
82 | const id = item.id;
83 | return this.isRendered(id);
84 | })
85 | .map(item => {
86 | const id = item.id;
87 | return {
88 | id,
89 | rectangle: rectangles[id],
90 | };
91 | });
92 | }
93 |
94 | getRenderedItems() {
95 | const rectangles = this._rectangles;
96 | return this._list.slice(this._sliceStart, this._sliceEnd).map(item => {
97 | const id = item.id;
98 | return {
99 | id,
100 | rectangle: rectangles[id],
101 | };
102 | });
103 | }
104 |
105 | isRendered(id) {
106 | return this._getRenderedIdSet().hasOwnProperty(id);
107 | }
108 |
109 | _getRenderedIdSet() {
110 | if (!this._renderedIdSet) {
111 | this._renderedIdSet = {};
112 | for (let t = this._sliceStart; t < this._sliceEnd; t++) {
113 | this._renderedIdSet[this._list[t].id] = true;
114 | }
115 | }
116 |
117 | return this._renderedIdSet;
118 | }
119 | }
120 |
121 | export default Position;
122 |
--------------------------------------------------------------------------------
/src/modules/Rectangle.js:
--------------------------------------------------------------------------------
1 | function isBetween(value, begin, end) {
2 | return value >= begin && value < end;
3 | }
4 |
5 | class Rectangle {
6 | constructor({ top = 0, left = 0, height = 0, width = 0 } = {}) {
7 | this._top = top;
8 | this._left = left;
9 | this._height = height;
10 | this._width = width;
11 | }
12 |
13 | getTop() {
14 | return this._top;
15 | }
16 |
17 | getBottom() {
18 | return this._top + this._height;
19 | }
20 |
21 | getLeft() {
22 | return this._left;
23 | }
24 |
25 | getRight() {
26 | return this._left + this._width;
27 | }
28 |
29 | getHeight() {
30 | return this._height;
31 | }
32 |
33 | getWidth() {
34 | return this._width;
35 | }
36 |
37 | doesIntersectWith(rect) {
38 | const top = this.getTop();
39 | const bottom = this.getBottom();
40 | const left = this.getLeft();
41 | const right = this.getRight();
42 | const aTop = rect.getTop();
43 | const aBottom = rect.getBottom();
44 | const aLeft = rect.getLeft();
45 | const aRight = rect.getRight();
46 |
47 | return (
48 | isBetween(top, aTop, aBottom) ||
49 | isBetween(aTop, top, bottom) ||
50 | isBetween(left, aLeft, aRight) ||
51 | isBetween(aLeft, left, right)
52 | );
53 | }
54 |
55 | contains(point) {
56 | if (point.x === undefined) {
57 | return isBetween(point.y, this.getTop(), this.getBottom());
58 | } else if (point.y === undefined) {
59 | return isBetween(point.x, this.getLeft(), this.getRight());
60 | } else {
61 | return (
62 | isBetween(point.y, this.getTop(), this.getBottom()) &&
63 | isBetween(point.x, this.getLeft(), this.getRight())
64 | );
65 | }
66 | }
67 |
68 | translateBy(x, y) {
69 | let left = this.getLeft();
70 | let top = this.getTop();
71 |
72 | if (x) {
73 | left += x;
74 | }
75 |
76 | if (y) {
77 | top += y;
78 | }
79 |
80 | return new Rectangle({
81 | left,
82 | top,
83 | width: this.getWidth(),
84 | height: this.getHeight(),
85 | });
86 | }
87 | }
88 |
89 | export default Rectangle;
90 |
--------------------------------------------------------------------------------
/src/modules/ScrollTracker.js:
--------------------------------------------------------------------------------
1 | const PROXIMITY = {
2 | INSIDE: 'inside',
3 | OUTSIDE: 'outside',
4 | };
5 |
6 | const TRIGGER_CAUSE = {
7 | INITIAL_POSITION: 'init',
8 | MOVEMENT: 'movement',
9 | LIST_UPDATE: 'list-update',
10 | };
11 |
12 | function getProximity(condition, position) {
13 | return condition(position.getListRect(), position.getViewportRect())
14 | ? PROXIMITY.INSIDE
15 | : PROXIMITY.OUTSIDE;
16 | }
17 |
18 | function findCause(prevState, nextState) {
19 | const isInit = !prevState.proximity && nextState.proximity === PROXIMITY.INSIDE;
20 | if (isInit) {
21 | return TRIGGER_CAUSE.INITIAL_POSITION;
22 | }
23 |
24 | const isMovement =
25 | prevState.proximity === PROXIMITY.OUTSIDE && nextState.proximity === PROXIMITY.INSIDE;
26 | if (isMovement) {
27 | return TRIGGER_CAUSE.MOVEMENT;
28 | }
29 |
30 | const stay = prevState.proximity === PROXIMITY.INSIDE && nextState.proximity === PROXIMITY.INSIDE;
31 | if (stay && prevState.listLength !== nextState.listLength) {
32 | return TRIGGER_CAUSE.LIST_UPDATE;
33 | }
34 |
35 | return null;
36 | }
37 |
38 | const Condition = {
39 | nearTop(distance) {
40 | return (list, viewport) => {
41 | return viewport.getTop() - list.getTop() <= distance;
42 | };
43 | },
44 | nearBottom(distance) {
45 | return (list, viewport) => {
46 | return list.getBottom() - viewport.getBottom() <= distance;
47 | };
48 | },
49 | nearTopRatio(ratio) {
50 | return (list, viewport) => {
51 | const viewportHeight = viewport.getHeight();
52 | const distance = ratio * viewportHeight;
53 | return viewport.getTop() - list.getTop() <= distance;
54 | };
55 | },
56 | nearBottomRatio(ratio) {
57 | return (list, viewport) => {
58 | const viewportHeight = viewport.getHeight();
59 | const distance = ratio * viewportHeight;
60 | return list.getBottom() - viewport.getBottom() <= distance;
61 | };
62 | },
63 | };
64 |
65 | class ScrollTracker {
66 | constructor(zones) {
67 | this._handlers = zones.map(zone => ({
68 | zone,
69 | state: {},
70 | }));
71 | }
72 |
73 | handlePositioningUpdate(position) {
74 | this._handlers.forEach(({ zone, state }) => {
75 | const { condition, callback } = zone;
76 | const newProximity = getProximity(condition, position);
77 | const newListLength = position.getList().length;
78 | const triggerCause = findCause(state, {
79 | proximity: newProximity,
80 | listLength: newListLength,
81 | });
82 |
83 | /* eslint-disable no-param-reassign */
84 | state.proximity = newProximity;
85 | state.listLength = newListLength;
86 | /* eslint-enable */
87 |
88 | if (triggerCause) {
89 | callback({
90 | triggerCause,
91 | });
92 | }
93 | });
94 | }
95 | }
96 |
97 | export default ScrollTracker;
98 |
99 | export { Condition };
100 |
--------------------------------------------------------------------------------
/src/modules/Viewport.js:
--------------------------------------------------------------------------------
1 | import Rectangle from './Rectangle';
2 |
3 | class Viewport {
4 | constructor(window, scroller = window) {
5 | this._scroller = scroller;
6 | this._window = window;
7 | this._programticScrollListeners = [];
8 | this._offsetTop = 0;
9 | this._useWindow = this._scroller === this._window;
10 | }
11 |
12 | _getScrollerHeight() {
13 | if (this._useWindow) {
14 | return Math.ceil(this._window.document.documentElement.clientHeight);
15 | }
16 |
17 | return this._scroller.clientHeight;
18 | }
19 |
20 | _addListener(event, listener, useWindow) {
21 | const target = useWindow ? this._window : this._scroller;
22 |
23 | const eventCallback = () => {
24 | return listener();
25 | };
26 |
27 | target.addEventListener(event, eventCallback);
28 |
29 | return () => {
30 | target.removeEventListener(event, eventCallback);
31 | };
32 | }
33 |
34 | getRectRelativeTo(node) {
35 | const top = node.getBoundingClientRect().top;
36 | const scrollerTop = this._useWindow ? 0 : this._scroller.getBoundingClientRect().top;
37 |
38 | return this.getRect().translateBy(0, Math.ceil(scrollerTop - top));
39 | }
40 |
41 | getOffsetTop() {
42 | return this._offsetTop;
43 | }
44 |
45 | setOffsetTop(value) {
46 | this._offsetTop = Math.ceil(value);
47 | }
48 |
49 | getRect() {
50 | const windowHeight = this._getScrollerHeight();
51 | const height = Math.max(0, windowHeight - this._offsetTop);
52 | return new Rectangle({ top: this._offsetTop, height });
53 | }
54 |
55 | // get scroll left
56 | scrollX() {
57 | if (this._useWindow) {
58 | return -1 * this._window.document.body.getBoundingClientRect().left;
59 | }
60 |
61 | return this._scroller.scrollLeft;
62 | }
63 |
64 | // get scroll top
65 | scrollY() {
66 | if (this._useWindow) {
67 | return -1 * this._window.document.body.getBoundingClientRect().top;
68 | }
69 |
70 | return this._scroller.scrollTop;
71 | }
72 |
73 | scrollBy(vertically) {
74 | if (this._useWindow) {
75 | this._window.scrollBy(0, vertically);
76 | } else {
77 | this._scroller.scrollTop += vertically;
78 | }
79 |
80 | this._programticScrollListeners.forEach(listener => listener(vertically));
81 | }
82 |
83 | scrollTo(yPos) {
84 | if (this._useWindow) {
85 | this._window.scrollTo(0, yPos);
86 | } else {
87 | this._scroller.scrollTop = yPos;
88 | }
89 |
90 | this._programticScrollListeners.forEach(listener => listener(yPos));
91 | }
92 |
93 | addRectChangeListener(listener) {
94 | return this._addListener('resize', listener, true);
95 | }
96 |
97 | addScrollListener(listener) {
98 | return this._addListener('scroll', listener, false);
99 | }
100 |
101 | // listener triggered by programmatic scroll
102 | addProgrammaticScrollListener(listener) {
103 | if (this._programticScrollListeners.indexOf(listener) < 0)
104 | this._programticScrollListeners.push(listener);
105 | return () => this.removeProgrammaticScrollListener(listener);
106 | }
107 |
108 | removeProgrammaticScrollListener(listener) {
109 | const index = this._programticScrollListeners.indexOf(listener);
110 | if (index > -1) this._programticScrollListeners.splice(index, 1);
111 | }
112 | }
113 |
114 | export default Viewport;
115 |
--------------------------------------------------------------------------------
/src/modules/requestAnimationFrame.js:
--------------------------------------------------------------------------------
1 | const requestAnimationFrame = (window.requestAnimationFrame =
2 | window.requestAnimationFrame ||
3 | window.webkitRequestAnimationFrame ||
4 | window.mozRequestAnimationFrame ||
5 | window.oRequestAnimationFrame ||
6 | window.msRequestAnimationFrame ||
7 | function(cb) {
8 | window.setTimeout(cb, 1000 / 60);
9 | });
10 |
11 | export default requestAnimationFrame;
12 |
--------------------------------------------------------------------------------
/src/utlis/createScheduler.js:
--------------------------------------------------------------------------------
1 | function createScheduler(callback, scheduler) {
2 | let ticking = false;
3 |
4 | const update = () => {
5 | ticking = false;
6 | callback();
7 | };
8 |
9 | const requestTick = () => {
10 | if (!ticking) {
11 | scheduler(update);
12 | }
13 | ticking = true;
14 | };
15 |
16 | return requestTick;
17 | }
18 |
19 | export default createScheduler;
20 |
--------------------------------------------------------------------------------
/src/utlis/findIndex.js:
--------------------------------------------------------------------------------
1 | function findIndex(list, predictor, startIndex = 0) {
2 | for (let index = 0 + startIndex; index < list.length; index++) {
3 | if (predictor(list[index], index)) {
4 | return index;
5 | }
6 | }
7 |
8 | return -1;
9 | }
10 |
11 | export default findIndex;
12 |
--------------------------------------------------------------------------------
/src/utlis/findNewSlice.js:
--------------------------------------------------------------------------------
1 | import findIndex from './findIndex';
2 |
3 | function searchIndexWhen(list, initSearchIndex, predicator) {
4 | if (initSearchIndex < 0 || initSearchIndex >= list.length) {
5 | return -1;
6 | }
7 |
8 | if (predicator(list[initSearchIndex])) {
9 | return initSearchIndex;
10 | }
11 |
12 | for (let step = 1; ; step++) {
13 | const back = initSearchIndex - step;
14 | const forward = initSearchIndex + step;
15 | const illegal = back < 0;
16 | const outOfBound = forward >= list.length;
17 | if (illegal && outOfBound) {
18 | break;
19 | }
20 | if (!outOfBound && predicator(list[forward])) {
21 | return forward;
22 | }
23 |
24 | if (!illegal && predicator(list[back])) {
25 | return back;
26 | }
27 | }
28 |
29 | return -1;
30 | }
31 |
32 | function findNewSlice(originList, newList, sliceStart, sliceEnd) {
33 | const newIds = newList.reduce((ids, item) => {
34 | // eslint-disable-next-line no-param-reassign
35 | ids[item.id] = true;
36 | return ids;
37 | }, {});
38 |
39 | const commonItemIndex = searchIndexWhen(originList, sliceStart, item => newIds[item.id]);
40 | if (-1 === commonItemIndex) {
41 | return null;
42 | }
43 |
44 | const newSliceStart = findIndex(newList, item => originList[commonItemIndex].id === item.id);
45 |
46 | return {
47 | sliceStart: newSliceStart,
48 | sliceEnd: Math.min(newList.length, newSliceStart + sliceEnd - sliceStart),
49 | };
50 | }
51 |
52 | export default findNewSlice;
53 |
--------------------------------------------------------------------------------
/test/findNewSlice.js:
--------------------------------------------------------------------------------
1 | import findNewSlice from '../src/utlis/findNewSlice';
2 |
3 | describe('findNewSlice', () => {
4 | test('should find new slice when insert new elements at start', () => {
5 | const slice = findNewSlice(
6 | [{ id: 1 }, { id: 2 }, { id: 3 }],
7 | [{ id: 0 }, { id: 1 }, { id: 2 }, { id: 3 }],
8 | 1,
9 | 3
10 | );
11 |
12 | expect(slice).toEqual({
13 | sliceStart: 2,
14 | sliceEnd: 4,
15 | });
16 | });
17 |
18 | test('should find new slice when insert new elements at end', () => {
19 | const slice = findNewSlice(
20 | [{ id: 1 }, { id: 2 }, { id: 3 }],
21 | [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4}],
22 | 1,
23 | 3
24 | );
25 |
26 | expect(slice).toEqual({
27 | sliceStart: 1,
28 | sliceEnd: 3,
29 | });
30 | });
31 |
32 | test('should find new slice when delete some element', () => {
33 | const slice = findNewSlice(
34 | [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }, { id: 6 }],
35 | [{ id: 1 }, { id: 3 }, { id: 5}, { id: 6 }],
36 | 1,
37 | 5
38 | );
39 |
40 | expect(slice).toEqual({
41 | sliceStart: 1,
42 | sliceEnd: 4,
43 | });
44 | });
45 |
46 | test('should not find new slice when no common item', () => {
47 | const slice = findNewSlice(
48 | [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }, { id: 6 }],
49 | [{ id: 7 }, { id: 8 }, { id: 9 }],
50 | 1,
51 | 5
52 | );
53 |
54 | expect(slice).toBe(null);
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/website/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | lib/core/metadata.js
4 | lib/core/MetadataBlog.js
5 | website/translated_docs
6 | website/build/
7 | website/yarn.lock
8 | website/node_modules
9 |
10 | website/i18n/*
11 | !website/i18n/en.json
12 |
--------------------------------------------------------------------------------
/website/core/Footer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017-present, Facebook, Inc.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 |
8 | const React = require('react');
9 |
10 | class Footer extends React.Component {
11 | docUrl(doc, language) {
12 | const baseUrl = this.props.config.baseUrl;
13 | return baseUrl + 'docs/' + (language ? language + '/' : '') + doc;
14 | }
15 |
16 | pageUrl(doc, language) {
17 | const baseUrl = this.props.config.baseUrl;
18 | return baseUrl + (language ? language + '/' : '') + doc;
19 | }
20 |
21 | render() {
22 | return null;
23 | // const currentYear = new Date().getFullYear();
24 | // return (
25 | //
81 | // );
82 | }
83 | }
84 |
85 | module.exports = Footer;
86 |
--------------------------------------------------------------------------------
/website/i18n/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "_comment": "This file is auto-generated by write-translations.js",
3 | "localized-strings": {
4 | "next": "Next",
5 | "previous": "Previous",
6 | "tagline": "Using virtual scroller with infinite list",
7 | "Viewport": "Viewport",
8 | "VirtualScroller": "VirtualScroller",
9 | "API": "API",
10 | "GitHub": "GitHub",
11 | "Components": "Components",
12 | "Modules": "Modules"
13 | },
14 | "pages-strings": {
15 | "Help Translate|recruit community translators for your project": "Help Translate",
16 | "Edit this Doc|recruitment message asking to edit the doc source": "Edit",
17 | "Translate this Doc|recruitment message asking to translate the docs": "Translate"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/website/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "examples": "docusaurus-examples",
4 | "start": "docusaurus-start",
5 | "build": "docusaurus-build",
6 | "publish-gh-pages": "GIT_USER=liximomo USE_SSH=true docusaurus-publish",
7 | "write-translations": "docusaurus-write-translations",
8 | "version": "docusaurus-version",
9 | "rename-version": "docusaurus-rename-version"
10 | },
11 | "devDependencies": {
12 | "docusaurus": "^1.0.7"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/website/pages/demo-simple-list.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Simple List
8 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
147 |
148 |
149 |
150 |
--------------------------------------------------------------------------------
/website/pages/demo-window-as-scroller.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Window as scroller
8 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
140 |
141 |
142 |
143 |
--------------------------------------------------------------------------------
/website/pages/en/demo-index.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 |
3 | const CompLibrary = require('../../core/CompLibrary.js');
4 | const Container = CompLibrary.Container;
5 | const GridBlock = CompLibrary.GridBlock;
6 |
7 | const siteConfig = require(process.cwd() + '/siteConfig.js');
8 |
9 | function pageUrl(page, language) {
10 | return siteConfig.baseUrl + (language ? language + '/' : '') + page;
11 | }
12 |
13 | const Block = props => (
14 |
18 |
19 |
20 | );
21 |
22 | class Index extends React.Component {
23 | render() {
24 | return (
25 |
26 |
27 | {[
28 | {},
29 | {
30 | title: `[Simple list](${pageUrl('demo-simple-list.html')})`,
31 | },
32 | {},
33 | ]}
34 |
35 |
36 | {[
37 | {},
38 | {
39 | title: `[Window scroller](${pageUrl('demo-window-as-scroller.html')})`,
40 | },
41 | {},
42 | ]}
43 |
44 |
45 | );
46 | }
47 | }
48 |
49 | module.exports = Index;
50 |
--------------------------------------------------------------------------------
/website/pages/en/help.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017-present, Facebook, Inc.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 |
8 | const React = require('react');
9 |
10 | const CompLibrary = require('../../core/CompLibrary.js');
11 | const Container = CompLibrary.Container;
12 | const GridBlock = CompLibrary.GridBlock;
13 |
14 | const siteConfig = require(process.cwd() + '/siteConfig.js');
15 |
16 | class Help extends React.Component {
17 | render() {
18 | const supportLinks = [
19 | {
20 | content:
21 | 'Learn more using the [documentation on this site.](/test-site/docs/en/doc1.html)',
22 | title: 'Browse Docs',
23 | },
24 | {
25 | content: 'Ask questions about the documentation and project',
26 | title: 'Join the community',
27 | },
28 | {
29 | content: "Find out what's new with this project",
30 | title: 'Stay up to date',
31 | },
32 | ];
33 |
34 | return (
35 |
36 |
37 |
38 |
41 |
This project is maintained by a dedicated group of people.
42 |
43 |
44 |
45 |
46 | );
47 | }
48 | }
49 |
50 | module.exports = Help;
51 |
--------------------------------------------------------------------------------
/website/pages/en/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017-present, Facebook, Inc.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 |
8 | const React = require('react');
9 |
10 | const CompLibrary = require('../../core/CompLibrary.js');
11 | const MarkdownBlock = CompLibrary.MarkdownBlock; /* Used to read markdown */
12 | const Container = CompLibrary.Container;
13 | const GridBlock = CompLibrary.GridBlock;
14 |
15 | const siteConfig = require(process.cwd() + '/siteConfig.js');
16 |
17 | function imgUrl(img) {
18 | return siteConfig.baseUrl + 'img/' + img;
19 | }
20 |
21 | function docUrl(doc, language) {
22 | return siteConfig.baseUrl + 'docs/' + (language ? language + '/' : '') + doc;
23 | }
24 |
25 | function pageUrl(page, language) {
26 | return siteConfig.baseUrl + (language ? language + '/' : '') + page;
27 | }
28 |
29 | class Button extends React.Component {
30 | render() {
31 | return (
32 |
37 | );
38 | }
39 | }
40 |
41 | Button.defaultProps = {
42 | target: '_self',
43 | };
44 |
45 | const SplashContainer = props => (
46 |
47 |
48 |
{props.children}
49 |
50 |
51 | );
52 |
53 | const Logo = props => (
54 |
55 |

56 |
57 | );
58 |
59 | const ProjectTitle = props => (
60 |
61 | {siteConfig.title}
62 | {siteConfig.tagline}
63 |
64 | );
65 |
66 | const PromoSection = props => (
67 |
68 |
69 |
{props.children}
70 |
71 |
72 | );
73 |
74 | class HomeSplash extends React.Component {
75 | render() {
76 | let language = this.props.language || '';
77 | return (
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | );
89 | }
90 | }
91 |
92 | const Block = props => (
93 |
97 |
98 |
99 | );
100 |
101 | const Features = props => (
102 |
103 |
104 | {[
105 | {
106 | title: 'Easy To Use',
107 | },
108 | {
109 | title: 'Infinite List',
110 | },
111 | ]}
112 |
113 |
114 | {[
115 | {
116 | title: 'Scroll Load & Scroll Position Restoration',
117 | },
118 | {
119 | title: 'Custom List Container',
120 | },
121 | ]}
122 |
123 |
124 | );
125 |
126 | const FeatureCallout = props => (
127 |
130 |
Feature Callout
131 | These are features of this project
132 |
133 | );
134 |
135 | class Index extends React.Component {
136 | render() {
137 | let language = this.props.language || '';
138 |
139 | return (
140 |
141 |
142 |
143 |
144 |
145 |
146 | );
147 | }
148 | }
149 |
150 | module.exports = Index;
151 |
--------------------------------------------------------------------------------
/website/pages/en/users.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017-present, Facebook, Inc.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 |
8 | const React = require('react');
9 |
10 | const CompLibrary = require('../../core/CompLibrary.js');
11 | const Container = CompLibrary.Container;
12 |
13 | const siteConfig = require(process.cwd() + '/siteConfig.js');
14 |
15 | class Users extends React.Component {
16 | render() {
17 | const showcase = siteConfig.users.map((user, i) => {
18 | return (
19 |
20 |
21 |
22 | );
23 | });
24 |
25 | return (
26 |
27 |
28 |
29 |
30 |
Who's Using This?
31 |
This project is used by many folks
32 |
33 |
{showcase}
34 |
Are you using this project?
35 |
38 | Add your company
39 |
40 |
41 |
42 |
43 | );
44 | }
45 | }
46 |
47 | module.exports = Users;
48 |
--------------------------------------------------------------------------------
/website/sidebars.json:
--------------------------------------------------------------------------------
1 | {
2 | "api": {
3 | "Components": ["VirtualScroller"],
4 | "Modules": ["Viewport"]
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/website/siteConfig.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017-present, Facebook, Inc.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 |
8 | /* List of projects/orgs using your project for the users page */
9 | const users = [
10 | {
11 | caption: 'User1',
12 | image: '/test-site/img/docusaurus.svg',
13 | infoLink: 'https://www.facebook.com',
14 | pinned: true,
15 | },
16 | ];
17 |
18 | const siteConfig = {
19 | title: 'React Virtual Scroller' /* title for your website */,
20 | tagline: 'Using virtual scroller with infinite list',
21 | url: 'https://liximomo.github.io' /* your website url */,
22 | baseUrl: '/react-virtual-scroller/' /* base url for your project */,
23 | projectName: 'react-virtual-scroller',
24 | headerLinks: [
25 | {doc: 'VirtualScroller', label: 'API'},
26 | { href: 'https://github.com/liximomo/react-virtual-scroller', label: 'GitHub' },
27 | // {blog: true, label: 'Blog'},
28 | ],
29 | users,
30 | /* path to images for header/footer */
31 | headerIcon: 'img/docusaurus.svg',
32 | footerIcon: 'img/docusaurus.svg',
33 | favicon: 'img/favicon.png',
34 | /* colors for website */
35 | colors: {
36 | primaryColor: '#009688',
37 | secondaryColor: '#6e7891',
38 | },
39 | /* custom fonts for website */
40 | /*fonts: {
41 | myFont: [
42 | "Times New Roman",
43 | "Serif"
44 | ],
45 | myOtherFont: [
46 | "-apple-system",
47 | "system-ui"
48 | ]
49 | },*/
50 | // This copyright info is used in /core/Footer.js and blog rss/atom feeds.
51 | copyright:
52 | 'Copyright © ' +
53 | new Date().getFullYear() +
54 | ' liximomo',
55 | organizationName: 'liximomo', // or set an env variable ORGANIZATION_NAME
56 | highlight: {
57 | // Highlight.js theme to use for syntax highlighting in code blocks
58 | theme: 'default',
59 | },
60 | scripts: ['https://buttons.github.io/buttons.js'],
61 | // You may provide arbitrary config keys to be used as needed by your template.
62 | repoUrl: 'https://github.com/liximomo/react-virtual-scroller',
63 | };
64 |
65 | module.exports = siteConfig;
66 |
--------------------------------------------------------------------------------
/website/static/css/custom.css:
--------------------------------------------------------------------------------
1 | /* your custom css */
2 |
3 | @media only screen and (min-device-width: 360px) and (max-device-width: 736px) {
4 | }
5 |
6 | @media only screen and (min-width: 1024px) {
7 | }
8 |
9 | @media only screen and (max-width: 1023px) {
10 | }
11 |
12 | @media only screen and (min-width: 1400px) {
13 | }
14 |
15 | @media only screen and (min-width: 1500px) {
16 | }
--------------------------------------------------------------------------------
/website/static/img/docusaurus.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/website/static/img/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liximomo/react-virtual-scroller/3e4793bbbfcc7f6d1fed3d49fd8f7b3262978853/website/static/img/favicon.png
--------------------------------------------------------------------------------
/website/static/img/favicon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liximomo/react-virtual-scroller/3e4793bbbfcc7f6d1fed3d49fd8f7b3262978853/website/static/img/favicon/favicon.ico
--------------------------------------------------------------------------------
/website/static/img/oss_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liximomo/react-virtual-scroller/3e4793bbbfcc7f6d1fed3d49fd8f7b3262978853/website/static/img/oss_logo.png
--------------------------------------------------------------------------------
/website/static/js/react-virtual-scroller.umd.min.js:
--------------------------------------------------------------------------------
1 | !function(t,e){"object"===typeof exports&&"undefined"!==typeof module?e(exports,require("react")):"function"===typeof define&&define.amd?define(["exports","react"],e):e(t.VirtualScroller={},t.React)}(this,function(t,e){"use strict";e=e&&e.hasOwnProperty("default")?e.default:e;var n="undefined"!==typeof window?window:"undefined"!==typeof global?global:"undefined"!==typeof self?self:{};function i(t,e){return t(e={exports:{}},e.exports),e.exports}function r(t){return function(){return t}}var o=function(){};o.thatReturns=r,o.thatReturnsFalse=r(!1),o.thatReturnsTrue=r(!0),o.thatReturnsNull=r(null),o.thatReturnsThis=function(){return this},o.thatReturnsArgument=function(t){return t};var s=o;var u,a=function(t,e,n,i,r,o,s,u){if(!t){var a;if(void 0===e)a=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var c=[n,i,r,o,s,u],l=0;(a=new Error(e.replace(/%s/g,function(){return c[l++]}))).name="Invariant Violation"}throw a.framesToPop=1,a}},c="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED",l=(i(function(t){t.exports=function(){function t(t,e,n,i,r,o){o!==c&&a(!1,"Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types")}function e(){return t}t.isRequired=t;var n={array:t,bool:t,func:t,number:t,object:t,string:t,symbol:t,any:t,arrayOf:e,element:t,instanceOf:e,node:t,objectOf:e,oneOf:e,oneOfType:e,shape:e,exact:e};return n.checkPropTypes=s,n.PropTypes=n,n}()}),i(function(t,e){function n(t,e){return t===e}function i(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:n,i=null,r=null;return function(){return function(t,e,n){if(null===e||null===n||e.length!==n.length)return!1;for(var i=e.length,r=0;r1?e-1:0),r=1;r1&&void 0!==arguments[1]?arguments[1]:o;if("object"!==typeof t)throw new Error("createStructuredSelector expects first argument to be an object where each property is a selector, instead received a "+typeof t);var n=Object.keys(t);return e(n.map(function(e){return t[e]}),function(){for(var t=arguments.length,e=Array(t),i=0;i1?e-1:0),i=1;i0&&void 0!==arguments[0]?arguments[0]:t;return r.apply(void 0,h(d(e)).concat([e]))}},g="Expected a function",v=NaN,_="[object Symbol]",y=/^\s+|\s+$/g,m=/^[-+]0x[0-9a-f]+$/i,w=/^0b[01]+$/i,S=/^0o[0-7]+$/i,R=parseInt,k="object"==typeof n&&n&&n.Object===Object&&n,b="object"==typeof self&&self&&self.Object===Object&&self,T=k||b||Function("return this")(),I=Object.prototype.toString,P=Math.max,E=Math.min,O=function(){return T.Date.now()};function L(t,e,n){var i,r,o,s,u,a,c=0,l=!1,f=!1,h=!0;if("function"!=typeof t)throw new TypeError(g);function d(e){var n=i,o=r;return i=r=void 0,c=e,s=t.apply(o,n)}function p(t){var n=t-a;return void 0===a||n>=e||n<0||f&&t-c>=o}function v(){var t,n,i=O();if(p(i))return _(i);u=setTimeout(v,(n=e-((t=i)-a),f?E(n,o-(t-c)):n))}function _(t){return u=void 0,h&&i?d(t):(i=r=void 0,s)}function y(){var t,n=O(),o=p(n);if(i=arguments,r=this,a=n,o){if(void 0===u)return c=t=a,u=setTimeout(v,e),l?d(t):s;if(f)return u=setTimeout(v,e),d(a)}return void 0===u&&(u=setTimeout(v,e)),s}return e=U(e)||0,x(n)&&(l=!!n.leading,o=(f="maxWait"in n)?P(U(n.maxWait)||0,e):o,h="trailing"in n?!!n.trailing:h),y.cancel=function(){void 0!==u&&clearTimeout(u),c=0,i=a=r=u=void 0},y.flush=function(){return void 0===u?s:_(O())},y}function x(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}function U(t){if("number"==typeof t)return t;if("symbol"==typeof(e=t)||(n=e)&&"object"==typeof n&&I.call(e)==_)return v;var e,n;if(x(t)){var i="function"==typeof t.valueOf?t.valueOf():t;t=x(i)?i+"":i}if("string"!=typeof t)return 0===t?t:+t;t=t.replace(y,"");var r=w.test(t);return r||S.test(t)?R(t.slice(2),r?2:8):m.test(t)?v:+t}var j=function(t,e,n){var i=!0,r=!0;if("function"!=typeof t)throw new TypeError(g);return x(n)&&(i="leading"in n?!!n.leading:i,r="trailing"in n?!!n.trailing:r),L(t,e,{leading:i,maxWait:e,trailing:r})},B=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")},A=function(){function t(t,e){for(var n=0;n2&&void 0!==arguments[2]?arguments[2]:0);n=e&&t0&&void 0!==arguments[0]?arguments[0]:{},n=e.top,i=void 0===n?0:n,r=e.left,o=void 0===r?0:r,s=e.height,u=void 0===s?0:s,a=e.width,c=void 0===a?0:a;B(this,t),this._top=i,this._left=o,this._height=u,this._width=c}return A(t,[{key:"getTop",value:function(){return this._top}},{key:"getBottom",value:function(){return this._top+this._height}},{key:"getLeft",value:function(){return this._left}},{key:"getRight",value:function(){return this._left+this._width}},{key:"getHeight",value:function(){return this._height}},{key:"getWidth",value:function(){return this._width}},{key:"doesIntersectWith",value:function(t){var e=this.getTop(),n=this.getBottom(),i=this.getLeft(),r=this.getRight(),o=t.getTop(),s=t.getBottom(),u=t.getLeft(),a=t.getRight();return C(e,o,s)||C(o,e,n)||C(i,u,a)||C(u,i,r)}},{key:"contains",value:function(t){return void 0===t.x?C(t.y,this.getTop(),this.getBottom()):void 0===t.y?C(t.x,this.getLeft(),this.getRight()):C(t.y,this.getTop(),this.getBottom())&&C(t.x,this.getLeft(),this.getRight())}},{key:"translateBy",value:function(e,n){var i=this.getLeft(),r=this.getTop();return e&&(i+=e),n&&(r+=n),new t({left:i,top:r,width:this.getWidth(),height:this.getHeight()})}}]),t}(),z=function(){function t(e){var n=e.viewportRectangle,i=e.list,r=e.rectangles,o=e.sliceStart,s=e.sliceEnd;B(this,t),this._viewportRectangle=n,this._list=i,this._rectangles=r,this._sliceStart=o,this._sliceEnd=s}return A(t,[{key:"getViewportRect",value:function(){return this._viewportRectangle}},{key:"getListRect",value:function(){var t=this._list;if(t.length<=0)return new F({top:0,height:0});var e=this._rectangles,n=t[0].id,i=t[t.length-1].id,r=e[n].getTop(),o=e[i].getBottom()-r;return new F({top:r,height:o})}},{key:"getAllItems",value:function(){var t=this;return this._list.map(function(e){var n=e.id;return{id:n,rectangle:t._rectangles[n]}})}},{key:"getList",value:function(){return this._list}},{key:"getItemRect",value:function(t){return this._rectangles[t]}},{key:"findVisibleItems",value:function(){var t=this,e=this._viewportRectangle,n=this._rectangles,i=this._list,r=M(i,function(t){var i=t.id;return n[i].doesIntersectWith(e)});if(r<0)return[];var o=M(i,function(t){var i=t.id;return!n[i].doesIntersectWith(e)},r);return o<0&&(o=i.length),i.slice(r,o).filter(function(e){var n=e.id;return t.isRendered(n)}).map(function(t){var e=t.id;return{id:e,rectangle:n[e]}})}},{key:"getRenderedItems",value:function(){var t=this._rectangles;return this._list.slice(this._sliceStart,this._sliceEnd).map(function(e){var n=e.id;return{id:n,rectangle:t[n]}})}},{key:"isRendered",value:function(t){return this._getRenderedIdSet().hasOwnProperty(t)}},{key:"_getRenderedIdSet",value:function(){if(!this._renderedIdSet){this._renderedIdSet={};for(var t=this._sliceStart;t1&&void 0!==arguments[1]?arguments[1]:e;B(this,t),this._scroller=n,this._window=e,this._programticScrollListeners=[],this._offsetTop=0,this._useWindow=this._scroller===this._window}return A(t,[{key:"_getScrollerHeight",value:function(){return this._useWindow?Math.ceil(this._window.document.documentElement.clientHeight):this._scroller.clientHeight}},{key:"_addListener",value:function(t,e,n){var i=n?this._window:this._scroller,r=function(){return e()};return i.addEventListener(t,r),function(){i.removeEventListener(t,r)}}},{key:"getRectRelativeTo",value:function(t){var e=t.getBoundingClientRect().top,n=this._useWindow?0:this._scroller.getBoundingClientRect().top;return this.getRect().translateBy(0,Math.ceil(n-e))}},{key:"setOffsetTop",value:function(t){this._offsetTop=t}},{key:"getRect",value:function(){var t=this._getScrollerHeight(),e=Math.max(0,t-this._offsetTop);return new F({top:this._offsetTop,height:e})}},{key:"scrollX",value:function(){return this._useWindow?-1*this._window.document.body.getBoundingClientRect().left:this._scroller.scrollLeft}},{key:"scrollY",value:function(){return this._useWindow?-1*this._window.document.body.getBoundingClientRect().top:this._scroller.scrollTop}},{key:"scrollBy",value:function(t){this._useWindow?this._window.scrollBy(0,t):this._scroller.scrollTop+=t,this._programticScrollListeners.forEach(function(e){return e(t)})}},{key:"addRectChangeListener",value:function(t){return this._addListener("resize",t,!0)}},{key:"addScrollListener",value:function(t){return this._addListener("scroll",t,!1)}},{key:"addProgrammaticScrollListener",value:function(t){var e=this;return this._programticScrollListeners.indexOf(t)<0&&this._programticScrollListeners.push(t),function(){return e.removeProgrammaticScrollListener(t)}}},{key:"removeProgrammaticScrollListener",value:function(t){var e=this._programticScrollListeners.indexOf(t);e>-1&&this._programticScrollListeners.splice(e,1)}}]),t}();function $(t,e){var n,i,r,o,s,u,a,c,l=(i=e,s=(n=t).getViewportRect(),u=function(t){return t&&t.doesIntersectWith(s)},a=function(t){return t?Math.abs(s.getTop()-t.getTop()):1/0},(c=i.getList().filter(function(t){var e=t.id;return n.isRendered(e)&&i.isRendered(e)})).length<=0?null:(o=function(t,e){var i,r,o,s,c,l,f,h=n.getItemRect(t.id),d=n.getItemRect(e.id);return c=d,l=(s=u)(h),f=s(c),(l&&!f?1:!l&&f?-1:0)||(r=d,o=(i=a)(h),i(r)-o)},(r=c).length<=0?null:r.reduce(function(t,e){return o(e,t)>0?e:t})));if(!l)return 0;var f=l.id,h=t.getItemRect(f).getTop()-t.getViewportRect().getTop();return e.getItemRect(f).getTop()-e.getViewportRect().getTop()-h}function Y(t,e,n){var i={},r=0;return t.forEach(function(t){var o=t.id,s=void 0!==e[o]?e[o]:n;i[o]=new F({top:r,height:s}),r+=s}),i}var X=function(t){function n(t){B(this,n);var e=D(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,t));return e._heights={},e._getRectangles=p(e,function(t){return t.list},function(t,e,n){return n._heights},function(t){return t.assumedItemHeight},Y),e._getSlice=p(e,function(t){return t.list},function(t,e){return e.sliceStart},function(t,e){return e.sliceEnd},function(t,e,n){return t.slice(e,n)}),e.state=e._getDefaultSlice(t.list),e._handleRefUpdate=e._handleRefUpdate.bind(e),e._update=e._update.bind(e),e._notifyPositioning=e._notifyPositioning.bind(e),e._scheduleUpdate=W(e._update,window.requestAnimationFrame),e._schedulePositioningNotification=W(e._notifyPositioning,window.requestAnimationFrame),e._handleScroll=j(e._scheduleUpdate,100,{trailing:!0}),e}return N(n,t),A(n,[{key:"_handleRefUpdate",value:function(t){this._listRef=t}},{key:"_onHeightsUpdate",value:function(t,e){this.props.viewport.scrollBy($(t,e))}},{key:"_recordHeights",value:function(){var t=this;if(!this._listRef)return{heightDelta:0};var e=this._listRef.getItemHeights(),n=Object.keys(e).reduce(function(n,i){var r=t._heights.hasOwnProperty(i)?t._heights[i]:t.props.assumedItemHeight;return n+e[i]-r},0);return 0!==n&&(this._heights=Object.assign({},this._heights,e)),{heightDelta:n}}},{key:"_postRenderProcessing",value:function(t){var e=this._recordHeights(),n=0!==e.heightDelta;(t||n)&&this._prevPositioning&&this._onHeightsUpdate(this._prevPositioning,this.getPositioning()),(t||Math.abs(e.heightDelta)>=this.props.assumedItemHeight)&&this._scheduleUpdate(),this._schedulePositioningNotification()}},{key:"_getDefaultSlice",value:function(t){var e=Object.assign({},this,{props:Object.assign({},this.props,{list:t})}),n=this.props.viewport.getRect().getHeight(),i=this._getRectangles(e),r=t[0].id,o=i[r].getTop(),s=M(t,function(t){return i[t.id].getTop()-o>=n},0);return s<0&&(s=t.length),{sliceStart:0,sliceEnd:s}}},{key:"_computeBlankSpace",value:function(){var t=this.props.list,e=this.state,n=e.sliceStart,i=e.sliceEnd,r=this._getRectangles(),o=t.length-1;return{blankSpaceAbove:t.length<=0?0:r[t[n].id].getTop()-r[t[0].id].getTop(),blankSpaceBelow:i>=t.length?0:r[t[o].id].getBottom()-r[t[i].id].getTop()}}},{key:"_getRelativeViewportRect",value:function(){if(!this._listRef)return new F({top:0,height:0});var t=this._listRef.getWrapperNode();return this.props.viewport.getRectRelativeTo(t)}},{key:"_update",value:function(){var t=this.props.list;if(!this._unmounted&&0!==t.length){var e=this._getRelativeViewportRect(),n=e.getHeight()*this.props.offscreenToViewportRatio,i=e.getTop()-n,r=e.getBottom()+n,o=this._getRectangles(),s=M(t,function(t){return o[t.id].getBottom()>i});s<0&&(s=t.length-1);var u=M(t,function(t){return o[t.id].getTop()>=r},s);u<0&&(u=t.length),this._schedulePositioningNotification(),this._setSlice(s,u)}}},{key:"_setSlice",value:function(t,e){var n=this.state,i=n.sliceStart,r=n.sliceEnd;i===t&&r===e||this.setState({sliceStart:t,sliceEnd:e})}},{key:"_notifyPositioning",value:function(){!this._unmounted&&this.props.onPositioningUpdate&&this.props.onPositioningUpdate(this.getPositioning())}},{key:"getPositioning",value:function(){var t=this.state,e=t.sliceStart,n=t.sliceEnd;return new z({viewportRectangle:this._getRelativeViewportRect(),list:this.props.list,rectangles:this._getRectangles(),sliceStart:e,sliceEnd:n})}},{key:"componentDidMount",value:function(){this._unlistenScroll=this.props.viewport.addScrollListener(this._handleScroll),this._postRenderProcessing(!0)}},{key:"componentWillReceiveProps",value:function(t){var e=this.props.list,n=this.state,i=t.list;if(e!==i){var r=function(t,e,n,i){var r=e.reduce(function(t,e){return t[e.id]=!0,t},{}),o=function(t,e,n){if(e<0||e>=t.length)return-1;if(n(t[e]))return e;for(var i=1;;i++){var r=e-i,o=e+i,s=r<0,u=o>=t.length;if(s&&u)break;if(!u&&n(t[o]))return o;if(!s&&n(t[r]))return r}return-1}(t,n,function(t){return r[t.id]});if(-1===o)return null;var s=M(e,function(e){return t[o].id===e.id});return{sliceStart:s,sliceEnd:Math.min(e.length,s+i-n)}}(e,i,n.sliceStart,n.sliceEnd)||this._getDefaultSlice(i);this._setSlice(r.sliceStart,r.sliceEnd)}}},{key:"componentWillUpdate",value:function(){this._prevPositioning=this.getPositioning()}},{key:"componentDidUpdate",value:function(t){this._postRenderProcessing(t.list!==this.props.list)}},{key:"componentWillUnmount",value:function(){this._unmounted=!0,this._unlistenScroll&&this._unlistenScroll()}},{key:"render",value:function(){var t=this._computeBlankSpace(),n=t.blankSpaceAbove,i=t.blankSpaceBelow,r=this.state.sliceStart,o=this.props.renderItem;return e.createElement(V,{ref:this._handleRefUpdate,list:this._getSlice(),blankSpaceAbove:n,blankSpaceBelow:i,renderItem:function(t,e){return o(t,r+e)}})}}]),n}(e.PureComponent);X.defaultProps={offscreenToViewportRatio:1.8,assumedItemHeight:400};var G={INSIDE:"inside",OUTSIDE:"outside"},J={INITIAL_POSITION:"init",MOVEMENT:"movement",LIST_UPDATE:"list-update"};var K=function(t){return function(e,n){return n.getTop()-e.getTop()<=t}},Q=function(t){return function(e,n){return e.getBottom()-n.getBottom()<=t}},Z=function(t){return function(e,n){var i=n.getHeight(),r=t*i;return n.getTop()-e.getTop()<=r}},tt=function(t){return function(e,n){var i=n.getHeight(),r=t*i;return e.getBottom()-n.getBottom()<=r}},et=function(){function t(e){B(this,t),this._handlers=e.map(function(t){return{zone:t,state:{}}})}return A(t,[{key:"handlePositioningUpdate",value:function(t){this._handlers.forEach(function(e){var n,i,r,o=e.zone,s=e.state,u=o.condition,a=o.callback,c=u((n=t).getListRect(),n.getViewportRect())?G.INSIDE:G.OUTSIDE,l=t.getList().length,f=(r={proximity:c,listLength:l},(i=s).proximity||r.proximity!==G.INSIDE?i.proximity===G.OUTSIDE&&r.proximity===G.INSIDE?J.MOVEMENT:i.proximity===G.INSIDE&&r.proximity===G.INSIDE&&i.listLength!==r.listLength?J.LIST_UPDATE:null:J.INITIAL_POSITION);s.proximity=c,s.listLength=l,f&&a({triggerCause:f})})}}]),t}(),nt=function(){return null},it=function(t){function n(t){B(this,n);var e=D(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,t));return e._getList=p(e,function(t){return t.items},function(t){var n={},i=[];return t.forEach(function(t){var r=e.props.identityFunction(t);n.hasOwnProperty(r)?console.warn('Duplicate item id generated in VirtualScroller. Latter item (id = "'+r+'") will be discarded'):(i.push({id:r,data:t}),n[r]=!0)}),i}),e._handlePositioningUpdate=e._handlePositioningUpdate.bind(e),e._createScrollTracker(t.nearStartProximityRatio,t.nearEndProximityRatio),e}return N(n,t),A(n,[{key:"_handlePositioningUpdate",value:function(t){this._scrollTracker&&this._scrollTracker.handlePositioningUpdate(t)}},{key:"_createScrollTracker",value:function(t,e){var n=this;this._scrollTracker=new et([{condition:K(5),callback:function(t){return n.props.onAtStart(t)}},{condition:Z(t),callback:function(t){return n.props.onNearStart(t)}},{condition:tt(e),callback:function(t){return n.props.onNearEnd(t)}},{condition:Q(5),callback:function(t){return n.props.onAtEnd(t)}}])}},{key:"componentWillReceiveProps",value:function(t){this._createScrollTracker(t.nearStartProximityRatio,t.nearEndProximityRatio)}},{key:"render",value:function(){var t=this.props,n=t.renderItem,i=t.assumedItemHeight,r=t.viewport;return e.createElement(X,{list:this._getList(),renderItem:n,assumedItemHeight:i,viewport:r,onPositioningUpdate:this._handlePositioningUpdate})}}]),n}(e.PureComponent);it.defaultProps={identityFunction:function(t){return t.id},offscreenToViewportRatio:1.8,assumedItemHeight:400,nearEndProximityRatio:1.75,nearStartProximityRatio:.25,onAtStart:nt,onNearStart:nt,onNearEnd:nt,onAtEnd:nt},t.default=it,t.Viewport=q,Object.defineProperty(t,"__esModule",{value:!0})});
2 |
--------------------------------------------------------------------------------