UNPKG

17.9 kBJavaScriptView Raw
1import _extends from "@babel/runtime/helpers/esm/extends";
2import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
3import _typeof from "@babel/runtime/helpers/esm/typeof";
4import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
5import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
6import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
7var _excluded = ["prefixCls", "className", "height", "itemHeight", "fullHeight", "style", "data", "children", "itemKey", "virtual", "direction", "scrollWidth", "component", "onScroll", "onVirtualScroll", "onVisibleChange", "innerProps", "extraRender", "styles"];
8import * as React from 'react';
9import { useRef, useState } from 'react';
10import { flushSync } from 'react-dom';
11import classNames from 'classnames';
12import ResizeObserver from 'rc-resize-observer';
13import Filler from "./Filler";
14import ScrollBar from "./ScrollBar";
15import useChildren from "./hooks/useChildren";
16import useHeights from "./hooks/useHeights";
17import useScrollTo from "./hooks/useScrollTo";
18import useDiffItem from "./hooks/useDiffItem";
19import useFrameWheel from "./hooks/useFrameWheel";
20import useMobileTouchMove from "./hooks/useMobileTouchMove";
21import useOriginScroll from "./hooks/useOriginScroll";
22import useLayoutEffect from "rc-util/es/hooks/useLayoutEffect";
23import { getSpinSize } from "./utils/scrollbarUtil";
24import { useEvent } from 'rc-util';
25import { useGetSize } from "./hooks/useGetSize";
26var EMPTY_DATA = [];
27var ScrollStyle = {
28 overflowY: 'auto',
29 overflowAnchor: 'none'
30};
31export function RawList(props, ref) {
32 var _props$prefixCls = props.prefixCls,
33 prefixCls = _props$prefixCls === void 0 ? 'rc-virtual-list' : _props$prefixCls,
34 className = props.className,
35 height = props.height,
36 itemHeight = props.itemHeight,
37 _props$fullHeight = props.fullHeight,
38 fullHeight = _props$fullHeight === void 0 ? true : _props$fullHeight,
39 style = props.style,
40 data = props.data,
41 children = props.children,
42 itemKey = props.itemKey,
43 virtual = props.virtual,
44 direction = props.direction,
45 scrollWidth = props.scrollWidth,
46 _props$component = props.component,
47 Component = _props$component === void 0 ? 'div' : _props$component,
48 onScroll = props.onScroll,
49 onVirtualScroll = props.onVirtualScroll,
50 onVisibleChange = props.onVisibleChange,
51 innerProps = props.innerProps,
52 extraRender = props.extraRender,
53 styles = props.styles,
54 restProps = _objectWithoutProperties(props, _excluded);
55
56 // =============================== Item Key ===============================
57 var getKey = React.useCallback(function (item) {
58 if (typeof itemKey === 'function') {
59 return itemKey(item);
60 }
61 return item === null || item === void 0 ? void 0 : item[itemKey];
62 }, [itemKey]);
63
64 // ================================ Height ================================
65 var _useHeights = useHeights(getKey, null, null),
66 _useHeights2 = _slicedToArray(_useHeights, 4),
67 setInstanceRef = _useHeights2[0],
68 collectHeight = _useHeights2[1],
69 heights = _useHeights2[2],
70 heightUpdatedMark = _useHeights2[3];
71
72 // ================================= MISC =================================
73 var useVirtual = !!(virtual !== false && height && itemHeight);
74 var containerHeight = React.useMemo(function () {
75 return Object.values(heights.maps).reduce(function (total, curr) {
76 return total + curr;
77 }, 0);
78 }, [heights.id, heights.maps]);
79 var inVirtual = useVirtual && data && (Math.max(itemHeight * data.length, containerHeight) > height || !!scrollWidth);
80 var isRTL = direction === 'rtl';
81 var mergedClassName = classNames(prefixCls, _defineProperty({}, "".concat(prefixCls, "-rtl"), isRTL), className);
82 var mergedData = data || EMPTY_DATA;
83 var componentRef = useRef();
84 var fillerInnerRef = useRef();
85
86 // =============================== Item Key ===============================
87
88 var _useState = useState(0),
89 _useState2 = _slicedToArray(_useState, 2),
90 offsetTop = _useState2[0],
91 setOffsetTop = _useState2[1];
92 var _useState3 = useState(0),
93 _useState4 = _slicedToArray(_useState3, 2),
94 offsetLeft = _useState4[0],
95 setOffsetLeft = _useState4[1];
96 var _useState5 = useState(false),
97 _useState6 = _slicedToArray(_useState5, 2),
98 scrollMoving = _useState6[0],
99 setScrollMoving = _useState6[1];
100 var onScrollbarStartMove = function onScrollbarStartMove() {
101 setScrollMoving(true);
102 };
103 var onScrollbarStopMove = function onScrollbarStopMove() {
104 setScrollMoving(false);
105 };
106 var sharedConfig = {
107 getKey: getKey
108 };
109
110 // ================================ Scroll ================================
111 function syncScrollTop(newTop) {
112 setOffsetTop(function (origin) {
113 var value;
114 if (typeof newTop === 'function') {
115 value = newTop(origin);
116 } else {
117 value = newTop;
118 }
119 var alignedTop = keepInRange(value);
120 componentRef.current.scrollTop = alignedTop;
121 return alignedTop;
122 });
123 }
124
125 // ================================ Legacy ================================
126 // Put ref here since the range is generate by follow
127 var rangeRef = useRef({
128 start: 0,
129 end: mergedData.length
130 });
131 var diffItemRef = useRef();
132 var _useDiffItem = useDiffItem(mergedData, getKey),
133 _useDiffItem2 = _slicedToArray(_useDiffItem, 1),
134 diffItem = _useDiffItem2[0];
135 diffItemRef.current = diffItem;
136
137 // ========================== Visible Calculation =========================
138 var _React$useMemo = React.useMemo(function () {
139 if (!useVirtual) {
140 return {
141 scrollHeight: undefined,
142 start: 0,
143 end: mergedData.length - 1,
144 offset: undefined
145 };
146 }
147
148 // Always use virtual scroll bar in avoid shaking
149 if (!inVirtual) {
150 var _fillerInnerRef$curre;
151 return {
152 scrollHeight: ((_fillerInnerRef$curre = fillerInnerRef.current) === null || _fillerInnerRef$curre === void 0 ? void 0 : _fillerInnerRef$curre.offsetHeight) || 0,
153 start: 0,
154 end: mergedData.length - 1,
155 offset: undefined
156 };
157 }
158 var itemTop = 0;
159 var startIndex;
160 var startOffset;
161 var endIndex;
162 var dataLen = mergedData.length;
163 for (var i = 0; i < dataLen; i += 1) {
164 var _item = mergedData[i];
165 var key = getKey(_item);
166 var cacheHeight = heights.get(key);
167 var currentItemBottom = itemTop + (cacheHeight === undefined ? itemHeight : cacheHeight);
168
169 // Check item top in the range
170 if (currentItemBottom >= offsetTop && startIndex === undefined) {
171 startIndex = i;
172 startOffset = itemTop;
173 }
174
175 // Check item bottom in the range. We will render additional one item for motion usage
176 if (currentItemBottom > offsetTop + height && endIndex === undefined) {
177 endIndex = i;
178 }
179 itemTop = currentItemBottom;
180 }
181
182 // When scrollTop at the end but data cut to small count will reach this
183 if (startIndex === undefined) {
184 startIndex = 0;
185 startOffset = 0;
186 endIndex = Math.ceil(height / itemHeight);
187 }
188 if (endIndex === undefined) {
189 endIndex = mergedData.length - 1;
190 }
191
192 // Give cache to improve scroll experience
193 endIndex = Math.min(endIndex + 1, mergedData.length - 1);
194 return {
195 scrollHeight: itemTop,
196 start: startIndex,
197 end: endIndex,
198 offset: startOffset
199 };
200 }, [inVirtual, useVirtual, offsetTop, mergedData, heightUpdatedMark, height]),
201 scrollHeight = _React$useMemo.scrollHeight,
202 start = _React$useMemo.start,
203 end = _React$useMemo.end,
204 fillerOffset = _React$useMemo.offset;
205 rangeRef.current.start = start;
206 rangeRef.current.end = end;
207
208 // ================================= Size =================================
209 var _React$useState = React.useState({
210 width: 0,
211 height: height
212 }),
213 _React$useState2 = _slicedToArray(_React$useState, 2),
214 size = _React$useState2[0],
215 setSize = _React$useState2[1];
216 var onHolderResize = function onHolderResize(sizeInfo) {
217 setSize({
218 width: sizeInfo.width || sizeInfo.offsetWidth,
219 height: sizeInfo.height || sizeInfo.offsetHeight
220 });
221 };
222
223 // Hack on scrollbar to enable flash call
224 var verticalScrollBarRef = useRef();
225 var horizontalScrollBarRef = useRef();
226 var horizontalScrollBarSpinSize = React.useMemo(function () {
227 return getSpinSize(size.width, scrollWidth);
228 }, [size.width, scrollWidth]);
229 var verticalScrollBarSpinSize = React.useMemo(function () {
230 return getSpinSize(size.height, scrollHeight);
231 }, [size.height, scrollHeight]);
232
233 // =============================== In Range ===============================
234 var maxScrollHeight = scrollHeight - height;
235 var maxScrollHeightRef = useRef(maxScrollHeight);
236 maxScrollHeightRef.current = maxScrollHeight;
237 function keepInRange(newScrollTop) {
238 var newTop = newScrollTop;
239 if (!Number.isNaN(maxScrollHeightRef.current)) {
240 newTop = Math.min(newTop, maxScrollHeightRef.current);
241 }
242 newTop = Math.max(newTop, 0);
243 return newTop;
244 }
245 var isScrollAtTop = offsetTop <= 0;
246 var isScrollAtBottom = offsetTop >= maxScrollHeight;
247 var originScroll = useOriginScroll(isScrollAtTop, isScrollAtBottom);
248
249 // ================================ Scroll ================================
250 var getVirtualScrollInfo = function getVirtualScrollInfo() {
251 return {
252 x: isRTL ? -offsetLeft : offsetLeft,
253 y: offsetTop
254 };
255 };
256 var lastVirtualScrollInfoRef = useRef(getVirtualScrollInfo());
257 var triggerScroll = useEvent(function () {
258 if (onVirtualScroll) {
259 var nextInfo = getVirtualScrollInfo();
260
261 // Trigger when offset changed
262 if (lastVirtualScrollInfoRef.current.x !== nextInfo.x || lastVirtualScrollInfoRef.current.y !== nextInfo.y) {
263 onVirtualScroll(nextInfo);
264 lastVirtualScrollInfoRef.current = nextInfo;
265 }
266 }
267 });
268 function onScrollBar(newScrollOffset, horizontal) {
269 var newOffset = newScrollOffset;
270 if (horizontal) {
271 flushSync(function () {
272 setOffsetLeft(newOffset);
273 });
274 triggerScroll();
275 } else {
276 syncScrollTop(newOffset);
277 }
278 }
279
280 // When data size reduce. It may trigger native scroll event back to fit scroll position
281 function onFallbackScroll(e) {
282 var newScrollTop = e.currentTarget.scrollTop;
283 if (newScrollTop !== offsetTop) {
284 syncScrollTop(newScrollTop);
285 }
286
287 // Trigger origin onScroll
288 onScroll === null || onScroll === void 0 || onScroll(e);
289 triggerScroll();
290 }
291 var keepInHorizontalRange = function keepInHorizontalRange(nextOffsetLeft) {
292 var tmpOffsetLeft = nextOffsetLeft;
293 var max = !!scrollWidth ? scrollWidth - size.width : 0;
294 tmpOffsetLeft = Math.max(tmpOffsetLeft, 0);
295 tmpOffsetLeft = Math.min(tmpOffsetLeft, max);
296 return tmpOffsetLeft;
297 };
298 var onWheelDelta = useEvent(function (offsetXY, fromHorizontal) {
299 if (fromHorizontal) {
300 // Horizontal scroll no need sync virtual position
301
302 flushSync(function () {
303 setOffsetLeft(function (left) {
304 var nextOffsetLeft = left + (isRTL ? -offsetXY : offsetXY);
305 return keepInHorizontalRange(nextOffsetLeft);
306 });
307 });
308 triggerScroll();
309 } else {
310 syncScrollTop(function (top) {
311 var newTop = top + offsetXY;
312 return newTop;
313 });
314 }
315 });
316
317 // Since this added in global,should use ref to keep update
318 var _useFrameWheel = useFrameWheel(useVirtual, isScrollAtTop, isScrollAtBottom, !!scrollWidth, onWheelDelta),
319 _useFrameWheel2 = _slicedToArray(_useFrameWheel, 2),
320 onRawWheel = _useFrameWheel2[0],
321 onFireFoxScroll = _useFrameWheel2[1];
322
323 // Mobile touch move
324 useMobileTouchMove(useVirtual, componentRef, function (deltaY, smoothOffset) {
325 if (originScroll(deltaY, smoothOffset)) {
326 return false;
327 }
328 onRawWheel({
329 preventDefault: function preventDefault() {},
330 deltaY: deltaY
331 });
332 return true;
333 });
334 useLayoutEffect(function () {
335 // Firefox only
336 function onMozMousePixelScroll(e) {
337 if (useVirtual) {
338 e.preventDefault();
339 }
340 }
341 var componentEle = componentRef.current;
342 componentEle.addEventListener('wheel', onRawWheel);
343 componentEle.addEventListener('DOMMouseScroll', onFireFoxScroll);
344 componentEle.addEventListener('MozMousePixelScroll', onMozMousePixelScroll);
345 return function () {
346 componentEle.removeEventListener('wheel', onRawWheel);
347 componentEle.removeEventListener('DOMMouseScroll', onFireFoxScroll);
348 componentEle.removeEventListener('MozMousePixelScroll', onMozMousePixelScroll);
349 };
350 }, [useVirtual]);
351
352 // Sync scroll left
353 useLayoutEffect(function () {
354 if (scrollWidth) {
355 setOffsetLeft(function (left) {
356 return keepInHorizontalRange(left);
357 });
358 }
359 }, [size.width, scrollWidth]);
360
361 // ================================= Ref ==================================
362 var delayHideScrollBar = function delayHideScrollBar() {
363 var _verticalScrollBarRef, _horizontalScrollBarR;
364 (_verticalScrollBarRef = verticalScrollBarRef.current) === null || _verticalScrollBarRef === void 0 || _verticalScrollBarRef.delayHidden();
365 (_horizontalScrollBarR = horizontalScrollBarRef.current) === null || _horizontalScrollBarR === void 0 || _horizontalScrollBarR.delayHidden();
366 };
367 var _scrollTo = useScrollTo(componentRef, mergedData, heights, itemHeight, getKey, function () {
368 return collectHeight(true);
369 }, syncScrollTop, delayHideScrollBar);
370 React.useImperativeHandle(ref, function () {
371 return {
372 getScrollInfo: getVirtualScrollInfo,
373 scrollTo: function scrollTo(config) {
374 function isPosScroll(arg) {
375 return arg && _typeof(arg) === 'object' && ('left' in arg || 'top' in arg);
376 }
377 if (isPosScroll(config)) {
378 // Scroll X
379 if (config.left !== undefined) {
380 setOffsetLeft(keepInHorizontalRange(config.left));
381 }
382
383 // Scroll Y
384 _scrollTo(config.top);
385 } else {
386 _scrollTo(config);
387 }
388 }
389 };
390 });
391
392 // ================================ Effect ================================
393 /** We need told outside that some list not rendered */
394 useLayoutEffect(function () {
395 if (onVisibleChange) {
396 var renderList = mergedData.slice(start, end + 1);
397 onVisibleChange(renderList, mergedData);
398 }
399 }, [start, end, mergedData]);
400
401 // ================================ Extra =================================
402 var getSize = useGetSize(mergedData, getKey, heights, itemHeight);
403 var extraContent = extraRender === null || extraRender === void 0 ? void 0 : extraRender({
404 start: start,
405 end: end,
406 virtual: inVirtual,
407 offsetX: offsetLeft,
408 offsetY: fillerOffset,
409 rtl: isRTL,
410 getSize: getSize
411 });
412
413 // ================================ Render ================================
414 var listChildren = useChildren(mergedData, start, end, scrollWidth, setInstanceRef, children, sharedConfig);
415 var componentStyle = null;
416 if (height) {
417 componentStyle = _objectSpread(_defineProperty({}, fullHeight ? 'height' : 'maxHeight', height), ScrollStyle);
418 if (useVirtual) {
419 componentStyle.overflowY = 'hidden';
420 if (scrollWidth) {
421 componentStyle.overflowX = 'hidden';
422 }
423 if (scrollMoving) {
424 componentStyle.pointerEvents = 'none';
425 }
426 }
427 }
428 var containerProps = {};
429 if (isRTL) {
430 containerProps.dir = 'rtl';
431 }
432 return /*#__PURE__*/React.createElement("div", _extends({
433 style: _objectSpread(_objectSpread({}, style), {}, {
434 position: 'relative'
435 }),
436 className: mergedClassName
437 }, containerProps, restProps), /*#__PURE__*/React.createElement(ResizeObserver, {
438 onResize: onHolderResize
439 }, /*#__PURE__*/React.createElement(Component, {
440 className: "".concat(prefixCls, "-holder"),
441 style: componentStyle,
442 ref: componentRef,
443 onScroll: onFallbackScroll,
444 onMouseEnter: delayHideScrollBar
445 }, /*#__PURE__*/React.createElement(Filler, {
446 prefixCls: prefixCls,
447 height: scrollHeight,
448 offsetX: offsetLeft,
449 offsetY: fillerOffset,
450 scrollWidth: scrollWidth,
451 onInnerResize: collectHeight,
452 ref: fillerInnerRef,
453 innerProps: innerProps,
454 rtl: isRTL,
455 extra: extraContent
456 }, listChildren))), inVirtual && scrollHeight > height && /*#__PURE__*/React.createElement(ScrollBar, {
457 ref: verticalScrollBarRef,
458 prefixCls: prefixCls,
459 scrollOffset: offsetTop,
460 scrollRange: scrollHeight,
461 rtl: isRTL,
462 onScroll: onScrollBar,
463 onStartMove: onScrollbarStartMove,
464 onStopMove: onScrollbarStopMove,
465 spinSize: verticalScrollBarSpinSize,
466 containerSize: size.height,
467 style: styles === null || styles === void 0 ? void 0 : styles.verticalScrollBar,
468 thumbStyle: styles === null || styles === void 0 ? void 0 : styles.verticalScrollBarThumb
469 }), inVirtual && scrollWidth > size.width && /*#__PURE__*/React.createElement(ScrollBar, {
470 ref: horizontalScrollBarRef,
471 prefixCls: prefixCls,
472 scrollOffset: offsetLeft,
473 scrollRange: scrollWidth,
474 rtl: isRTL,
475 onScroll: onScrollBar,
476 onStartMove: onScrollbarStartMove,
477 onStopMove: onScrollbarStopMove,
478 spinSize: horizontalScrollBarSpinSize,
479 containerSize: size.width,
480 horizontal: true,
481 style: styles === null || styles === void 0 ? void 0 : styles.horizontalScrollBar,
482 thumbStyle: styles === null || styles === void 0 ? void 0 : styles.horizontalScrollBarThumb
483 }));
484}
485var List = /*#__PURE__*/React.forwardRef(RawList);
486List.displayName = 'List';
487export default List;
\No newline at end of file