UNPKG

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