UNPKG

15.3 kBJavaScriptView Raw
1import _classCallCheck from 'babel-runtime/helpers/classCallCheck';
2import _possibleConstructorReturn from 'babel-runtime/helpers/possibleConstructorReturn';
3import _inherits from 'babel-runtime/helpers/inherits';
4
5var _class, _temp;
6
7import PropTypes from 'prop-types';
8import React, { Component } from 'react';
9import cx from 'classnames';
10import { polyfill } from 'react-lifecycles-compat';
11import { findDOMNode } from 'react-dom';
12import { events } from '../util';
13
14var NOOP = function NOOP() {};
15var MAX_SYNC_UPDATES = 40;
16
17var isEqualSubset = function isEqualSubset(a, b) {
18 for (var key in b) {
19 if (a[key] !== b[key]) {
20 return false;
21 }
22 }
23
24 return true;
25};
26
27var getOffset = function getOffset(el) {
28 var offset = el.clientLeft || 0;
29 do {
30 offset += el.offsetTop || 0;
31 el = el.offsetParent;
32 } while (el);
33 return offset;
34};
35
36var constrain = function constrain(from, size, _ref) {
37 var children = _ref.children,
38 minSize = _ref.minSize;
39
40 var length = children && children.length;
41 size = Math.max(size, minSize);
42 if (size > length) {
43 size = length;
44 }
45 from = from ? Math.max(Math.min(from, length - size), 0) : 0;
46
47 return { from: from, size: size };
48};
49/** VirtualList */
50var VirtualList = (_temp = _class = function (_Component) {
51 _inherits(VirtualList, _Component);
52
53 function VirtualList(props) {
54 _classCallCheck(this, VirtualList);
55
56 var _this = _possibleConstructorReturn(this, _Component.call(this, props));
57
58 var jumpIndex = props.jumpIndex;
59
60 var _constrain = constrain(jumpIndex, 0, props),
61 from = _constrain.from,
62 size = _constrain.size;
63
64 _this.state = { from: from, size: size };
65 _this.cache = {};
66 _this.cacheAdd = {};
67 _this.scrollTo = _this.scrollTo.bind(_this);
68 _this.cachedScroll = null;
69 _this.unstable = false;
70 _this.updateCounter = 0;
71 return _this;
72 }
73
74 VirtualList.getDerivedStateFromProps = function getDerivedStateFromProps(nextProps, prevState) {
75 var from = prevState.from,
76 size = prevState.size;
77
78
79 return constrain(from, size, nextProps);
80 };
81
82 VirtualList.prototype.componentDidMount = function componentDidMount() {
83 var jumpIndex = this.props.jumpIndex;
84
85
86 this.updateFrameAndClearCache = this.updateFrameAndClearCache.bind(this);
87
88 events.on(window, 'resize', this.updateFrameAndClearCache);
89
90 this.updateFrame(this.scrollTo.bind(this, jumpIndex));
91 };
92
93 VirtualList.prototype.componentDidUpdate = function componentDidUpdate(prevProps) {
94 var _this2 = this;
95
96 var oldIndex = prevProps.jumpIndex;
97 var newIndex = this.props.jumpIndex;
98
99 if (oldIndex !== newIndex) {
100 this.updateFrame(this.scrollTo.bind(this, newIndex));
101 }
102
103 // If the list has reached an unstable state, prevent an infinite loop.
104 if (this.unstable) {
105 return;
106 }
107
108 if (++this.updateCounter > MAX_SYNC_UPDATES) {
109 this.unstable = true;
110 }
111
112 if (!this.updateCounterTimeoutId) {
113 this.updateCounterTimeoutId = setTimeout(function () {
114 _this2.updateCounter = 0;
115 delete _this2.updateCounterTimeoutId;
116 }, 0);
117 }
118
119 this.updateFrame();
120 };
121
122 VirtualList.prototype.componentWillUnmount = function componentWillUnmount() {
123 events.off(window, 'resize', this.updateFrameAndClearCache);
124
125 events.off(this.scrollParent, 'scroll', this.updateFrameAndClearCache);
126 events.off(this.scrollParent, 'mousewheel', NOOP);
127 };
128
129 VirtualList.prototype.maybeSetState = function maybeSetState(b, cb) {
130 if (isEqualSubset(this.state, b)) {
131 return cb();
132 }
133
134 this.setState(b, cb);
135 };
136
137 VirtualList.prototype.getEl = function getEl() {
138 return this.el || this.items || {};
139 };
140
141 VirtualList.prototype.getScrollParent = function getScrollParent() {
142 var el = this.getEl();
143 el = el.parentElement;
144
145 switch (window.getComputedStyle(el).overflowY) {
146 case 'auto':
147 case 'scroll':
148 case 'overlay':
149 case 'visible':
150 return el;
151 }
152
153 return window;
154 };
155
156 VirtualList.prototype.getScroll = function getScroll() {
157 // Cache scroll position as this causes a forced synchronous layout.
158 // if (typeof this.cachedScroll === 'number') {
159 // return this.cachedScroll;
160 // }
161 var scrollParent = this.scrollParent;
162
163 var scrollKey = 'scrollTop';
164 var actual = scrollParent === window ? // Firefox always returns document.body[scrollKey] as 0 and Chrome/Safari
165 // always return document.documentElement[scrollKey] as 0, so take
166 // whichever has a value.
167 document.body[scrollKey] || document.documentElement[scrollKey] : scrollParent[scrollKey];
168 var max = this.getScrollSize() - this.getViewportSize();
169
170 var scroll = Math.max(0, Math.min(actual, max));
171 var el = this.getEl();
172 this.cachedScroll = getOffset(scrollParent) + scroll - getOffset(el);
173
174 return this.cachedScroll;
175 };
176
177 VirtualList.prototype.setScroll = function setScroll(offset) {
178 var scrollParent = this.scrollParent;
179
180 offset += getOffset(this.getEl());
181 if (scrollParent === window) {
182 return window.scrollTo(0, offset);
183 }
184
185 offset -= getOffset(this.scrollParent);
186 scrollParent.scrollTop = offset;
187 };
188
189 VirtualList.prototype.getViewportSize = function getViewportSize() {
190 var scrollParent = this.scrollParent;
191
192 return scrollParent === window ? window.innerHeight : scrollParent.clientHeight;
193 };
194
195 VirtualList.prototype.getScrollSize = function getScrollSize() {
196 var scrollParent = this.scrollParent;
197 var _document = document,
198 body = _document.body,
199 documentElement = _document.documentElement;
200
201 var key = 'scrollHeight';
202 return scrollParent === window ? Math.max(body[key], documentElement[key]) : scrollParent[key];
203 };
204
205 VirtualList.prototype.getStartAndEnd = function getStartAndEnd() {
206 var threshold = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.props.threshold;
207
208 var scroll = this.getScroll();
209
210 var trueScroll = scroll;
211 var start = Math.max(0, trueScroll - threshold);
212 var end = trueScroll + this.getViewportSize() + threshold;
213
214 return { start: start, end: end };
215 };
216
217 // Called by 'scroll' and 'resize' events, clears scroll position cache.
218
219
220 VirtualList.prototype.updateFrameAndClearCache = function updateFrameAndClearCache(cb) {
221 this.cachedScroll = null;
222 return this.updateFrame(cb);
223 };
224
225 VirtualList.prototype.updateFrame = function updateFrame(cb) {
226 this.updateScrollParent();
227
228 if (typeof cb !== 'function') {
229 cb = NOOP;
230 }
231
232 return this.updateVariableFrame(cb);
233 };
234
235 VirtualList.prototype.updateScrollParent = function updateScrollParent() {
236 var prev = this.scrollParent;
237 this.scrollParent = this.getScrollParent();
238
239 if (prev === this.scrollParent) {
240 return;
241 }
242 if (prev) {
243 events.off(prev, 'scroll', this.updateFrameAndClearCache);
244 events.off(prev, 'mousewheel', NOOP);
245 }
246
247 events.on(this.scrollParent, 'scroll', this.updateFrameAndClearCache);
248 events.on(this.scrollParent, 'mousewheel', NOOP);
249
250 // You have to attach mousewheel listener to the scrollable element.
251 // Just an empty listener. After that onscroll events will be fired synchronously.
252 };
253
254 VirtualList.prototype.updateVariableFrame = function updateVariableFrame(cb) {
255 if (!this.props.itemSizeGetter) {
256 this.cacheSizes();
257 }
258
259 var _getStartAndEnd = this.getStartAndEnd(),
260 start = _getStartAndEnd.start,
261 end = _getStartAndEnd.end;
262
263 var _props = this.props,
264 pageSize = _props.pageSize,
265 children = _props.children;
266
267 var length = children.length;
268 var space = 0;
269 var from = 0;
270 var size = 0;
271 var maxFrom = length - 1;
272
273 while (from < maxFrom) {
274 var itemSize = this.getSizeOf(from);
275 if (itemSize === null || itemSize === undefined || space + itemSize > start) {
276 break;
277 }
278 space += itemSize;
279 ++from;
280 }
281
282 var maxSize = length - from;
283
284 while (size < maxSize && space < end) {
285 var _itemSize = this.getSizeOf(from + size);
286 if (_itemSize === null || _itemSize === undefined) {
287 size = Math.min(size + pageSize, maxSize);
288 break;
289 }
290 space += _itemSize;
291 ++size;
292 }
293
294 this.maybeSetState({ from: from, size: size }, cb);
295 };
296
297 VirtualList.prototype.getSpaceBefore = function getSpaceBefore(index) {
298 var cache = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
299
300 if (!index) {
301 return 0;
302 }
303 if (cache[index] !== null && cache[index] !== undefined) {
304 return cache[index] || 0;
305 }
306
307 // Find the closest space to index there is a cached value for.
308 var from = index;
309 while (from > 0 && (cache[from] === null || cache[from] === undefined)) {
310 from--;
311 }
312
313 // Finally, accumulate sizes of items from - index.
314 var space = cache[from] || 0;
315 for (var i = from; i < index; ++i) {
316 cache[i] = space;
317 var itemSize = this.getSizeOf(i);
318 if (itemSize === null || itemSize === undefined) {
319 break;
320 }
321 space += itemSize;
322 }
323
324 cache[index] = space;
325
326 return cache[index] || 0;
327 };
328
329 VirtualList.prototype.cacheSizes = function cacheSizes() {
330 var cache = this.cache;
331 var from = this.state.from;
332 var _items = this.items,
333 children = _items.children,
334 _items$props = _items.props,
335 props = _items$props === undefined ? {} : _items$props;
336
337 var itemEls = children || props.children || [];
338
339 try {
340 // <Select useVirtual /> 模式下,在快速点击切换Tab的情况下(Select实例快速出现、消失) 有时会出现this.items不存在,导致页面报错。怀疑是Select的异步timer渲染逻辑引起的
341 for (var i = 0, l = itemEls.length; i < l; ++i) {
342 var ulRef = findDOMNode(this.items);
343 var height = ulRef.children[i].offsetHeight;
344 if (height > 0) {
345 cache[from + i] = height;
346 }
347 }
348 } catch (error) {
349 // ...
350 }
351 };
352
353 VirtualList.prototype.getSizeOf = function getSizeOf(index) {
354 var cache = this.cache;
355 var _props2 = this.props,
356 itemSizeGetter = _props2.itemSizeGetter,
357 jumpIndex = _props2.jumpIndex;
358
359 // Try the cache.
360
361 if (index in cache) {
362 return cache[index];
363 }
364 if (itemSizeGetter) {
365 return itemSizeGetter(index);
366 }
367
368 if (!this.defaultItemHeight && jumpIndex > -1) {
369 var keysList = Object.keys(this.cache);
370 var len = keysList.length;
371 var height = this.cache[len - 1];
372 this.defaultItemHeight = height;
373 }
374
375 if (this.defaultItemHeight) {
376 return this.defaultItemHeight;
377 }
378 };
379
380 VirtualList.prototype.scrollTo = function scrollTo(index) {
381 this.setScroll(this.getSpaceBefore(index, this.cacheAdd));
382 };
383
384 VirtualList.prototype.renderMenuItems = function renderMenuItems() {
385 var _this3 = this;
386
387 var _props3 = this.props,
388 children = _props3.children,
389 itemsRenderer = _props3.itemsRenderer;
390 var _state = this.state,
391 from = _state.from,
392 size = _state.size;
393
394 var items = [];
395
396 for (var i = 0; i < size; ++i) {
397 items.push(children[from + i]);
398 }
399
400 return itemsRenderer(items, function (c) {
401 _this3.items = c;
402 return _this3.items;
403 });
404 };
405
406 VirtualList.prototype.render = function render() {
407 var _cx,
408 _this4 = this;
409
410 var _props4 = this.props,
411 _props4$children = _props4.children,
412 children = _props4$children === undefined ? [] : _props4$children,
413 prefix = _props4.prefix,
414 className = _props4.className;
415
416 var length = children.length;
417 var from = this.state.from;
418
419 var items = this.renderMenuItems();
420
421 var style = { position: 'relative' };
422
423 var size = this.getSpaceBefore(length, {});
424
425 if (size) {
426 style.height = size;
427 }
428 var offset = this.getSpaceBefore(from, this.cacheAdd);
429 var transform = 'translate(0px, ' + offset + 'px)';
430 var listStyle = {
431 msTransform: transform,
432 WebkitTransform: transform,
433 transform: transform
434 };
435
436 var cls = cx((_cx = {}, _cx[prefix + 'virtual-list-wrapper'] = true, _cx[className] = !!className, _cx));
437
438 return React.createElement(
439 'div',
440 {
441 className: cls,
442 style: style,
443 ref: function ref(c) {
444 _this4.el = c;
445 return _this4.el;
446 }
447 },
448 React.createElement(
449 'div',
450 { style: listStyle },
451 items
452 )
453 );
454 };
455
456 return VirtualList;
457}(Component), _class.displayName = 'VirtualList', _class.propTypes = {
458 prefix: PropTypes.string,
459 /**
460 * 渲染的子节点
461 */
462 children: PropTypes.any,
463 /**
464 * 最小加载数量
465 */
466 minSize: PropTypes.number,
467 /**
468 * 一屏数量
469 */
470 pageSize: PropTypes.number,
471 /**
472 * 父渲染函数,默认为 (items, ref) => <ul ref={ref}>{items}</ul>
473 */
474 itemsRenderer: PropTypes.func,
475 /**
476 * 缓冲区高度
477 */
478 threshold: PropTypes.number,
479 /**
480 * 获取item高度的函数
481 */
482 itemSizeGetter: PropTypes.func,
483 /**
484 * 设置跳转位置,需要设置 itemSizeGetter 才能生效, 不设置认为元素等高并取第一个元素高度作为默认高
485 */
486 jumpIndex: PropTypes.number,
487 className: PropTypes.string
488}, _class.defaultProps = {
489 prefix: 'next-',
490 itemsRenderer: function itemsRenderer(items, ref) {
491 return React.createElement(
492 'ul',
493 { ref: ref },
494 items
495 );
496 },
497 minSize: 1,
498 pageSize: 10,
499 jumpIndex: 0,
500 threshold: 100
501}, _temp);
502VirtualList.displayName = 'VirtualList';
503
504
505export default polyfill(VirtualList);
\No newline at end of file