UNPKG

@sms-frontend/components

Version:

SMS Design React UI Library.

466 lines (465 loc) 24.1 kB
var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; }; import React, { useEffect, useImperativeHandle, useRef, useMemo, useState } from 'react'; import { getValidScrollTop, getCompareItemRelativeTop, getItemAbsoluteTop, getItemRelativeTop, getNodeHeight, getRangeIndex, getScrollPercentage, GHOST_ITEM_KEY, getLongestItemIndex, getLocationItem, } from './utils/itemUtil'; import { raf, caf } from '../../_util/raf'; import { isNumber } from '../../_util/is'; import usePrevious from '../../_util/hooks/usePrevious'; import { findListDiffIndex, getIndexByStartLoc } from './utils/algorithmUtil'; import Filler from './Filler'; import useStateWithPromise from '../../_util/hooks/useStateWithPromise'; import useIsFirstRender from '../../_util/hooks/useIsFirstRender'; import useForceUpdate from '../../_util/hooks/useForceUpdate'; import ResizeObserver from '../../_util/resizeObserver'; import useIsomorphicLayoutEffect from '../../_util/hooks/useIsomorphicLayoutEffect'; // height of the virtual element, used to calculate total height of the virtual list var DEFAULT_VIRTUAL_ITEM_HEIGHT = 32; var KEY_VIRTUAL_ITEM_HEIGHT = "__fake_item_height_" + Math.random(); // after collecting the real height of the first screen element, calculate the virtual ItemHeight to trigger list re-rendering var useComputeVirtualItemHeight = function (refItemHeightMap) { var forceUpdate = useForceUpdate(); var heightMap = refItemHeightMap.current; useEffect(function () { if (Object.keys(heightMap).length && !heightMap[KEY_VIRTUAL_ITEM_HEIGHT]) { heightMap[KEY_VIRTUAL_ITEM_HEIGHT] = Object.entries(heightMap).reduce(function (sum, _a, currentIndex, array) { var _b = __read(_a, 2), currentHeight = _b[1]; var nextSum = sum + currentHeight; return currentIndex === array.length - 1 ? Math.round(nextSum / array.length) : nextSum; }, 0); forceUpdate(); } }, [Object.keys(heightMap).length]); }; // cache the constructed results of child nodes to avoid redrawing of child nodes caused by re-construction during drawing var useCacheChildrenNodes = function (children) { var refCacheMap = useRef({}); var refPrevChildren = useRef(children); useEffect(function () { refPrevChildren.current = children; }, [children]); // children change means state of parent component is updated, so clear cache if (children !== refPrevChildren.current) { refCacheMap.current = {}; } return function (item, index, props) { if (!refCacheMap.current.hasOwnProperty(index)) { refCacheMap.current[index] = children(item, index, props); } return refCacheMap.current[index]; }; }; var VirtualList = React.forwardRef(function (props, ref) { var style = props.style, className = props.className, children = props.children, _a = props.data, data = _a === void 0 ? [] : _a, _b = props.wrapper, WrapperTagName = _b === void 0 ? 'div' : _b, _c = props.threshold, threshold = _c === void 0 ? 100 : _c, _d = props.height, propHeight = _d === void 0 ? '100%' : _d, _e = props.isStaticItemHeight, isStaticItemHeight = _e === void 0 ? true : _e, itemKey = props.itemKey, onScroll = props.onScroll, measureLongestItem = props.measureLongestItem, scrollOptions = props.scrollOptions, restProps = __rest(props, ["style", "className", "children", "data", "wrapper", "threshold", "height", "isStaticItemHeight", "itemKey", "onScroll", "measureLongestItem", "scrollOptions"]); // Compatible with setting the height of the list through style.maxHeight var styleListMaxHeight = (style && style.maxHeight) || propHeight; var refItemHeightMap = useRef({}); var _f = __read(useState(200), 2), stateHeight = _f[0], setStateHeight = _f[1]; var renderChild = useCacheChildrenNodes(children); useComputeVirtualItemHeight(refItemHeightMap); // Elements with the same height, the height of the item is based on the first rendering var itemCount = data.length; var viewportHeight = isNumber(styleListMaxHeight) ? styleListMaxHeight : stateHeight; var itemHeight = refItemHeightMap.current[KEY_VIRTUAL_ITEM_HEIGHT] || DEFAULT_VIRTUAL_ITEM_HEIGHT; var itemCountVisible = Math.ceil(viewportHeight / itemHeight); var itemTotalHeight = itemHeight * itemCount; var isVirtual = threshold !== null && itemCount >= threshold && itemTotalHeight > viewportHeight; var refList = useRef(null); var refRafId = useRef(null); var refLockScroll = useRef(false); var refIsVirtual = useRef(isVirtual); // The paddingTop of the record scrolling list is used to correct the scrolling distance var scrollListPadding = useMemo(function () { if (refList.current) { var getPadding = function (property) { return +window.getComputedStyle(refList.current)[property].replace(/\D/g, ''); }; return { top: getPadding('paddingTop'), bottom: getPadding('paddingBottom'), }; } return { top: 0, bottom: 0 }; }, [refList.current]); var _g = __read(useStateWithPromise({ // measure status status: 'NONE', // render range info startIndex: 0, endIndex: 0, itemIndex: 0, itemOffsetPtg: 0, // scroll info startItemTop: 0, scrollTop: 0, }), 2), state = _g[0], setState = _g[1]; var prevData = usePrevious(data) || []; var isFirstRender = useIsFirstRender(); var getItemKey = function (item, index) { return typeof itemKey === 'function' ? itemKey(item, index) : typeof itemKey === 'string' ? item[itemKey] : item.key || index; }; var getItemKeyByIndex = function (index, items) { if (items === void 0) { items = data; } if (index === items.length) { return GHOST_ITEM_KEY; } var item = items[index]; return item !== undefined ? getItemKey(item, index) : null; }; var getCachedItemHeight = function (key) { return refItemHeightMap.current[key] || itemHeight; }; var internalScrollTo = function (relativeScroll) { var compareItemIndex = relativeScroll.itemIndex, compareItemRelativeTop = relativeScroll.relativeTop; var _a = refList.current, scrollHeight = _a.scrollHeight, clientHeight = _a.clientHeight; var originScrollTop = state.scrollTop; var maxScrollTop = scrollHeight - clientHeight; var bestSimilarity = Number.MAX_VALUE; var bestScrollTop = null; var bestItemIndex = null; var bestItemOffsetPtg = null; var bestStartIndex = null; var bestEndIndex = null; var missSimilarity = 0; for (var i = 0; i < maxScrollTop; i++) { var scrollTop = getIndexByStartLoc(0, maxScrollTop, originScrollTop, i); var scrollPtg = getScrollPercentage({ scrollTop: scrollTop, scrollHeight: scrollHeight, clientHeight: clientHeight }); var _b = getRangeIndex(scrollPtg, itemCount, itemCountVisible), itemIndex = _b.itemIndex, itemOffsetPtg = _b.itemOffsetPtg, startIndex = _b.startIndex, endIndex = _b.endIndex; if (startIndex <= compareItemIndex && compareItemIndex <= endIndex) { var locatedItemRelativeTop = getItemRelativeTop({ itemHeight: getCachedItemHeight(getItemKeyByIndex(itemIndex)), itemOffsetPtg: itemOffsetPtg, clientHeight: clientHeight, scrollPtg: scrollPtg, }); var compareItemTop = getCompareItemRelativeTop({ locatedItemRelativeTop: locatedItemRelativeTop, locatedItemIndex: itemIndex, compareItemIndex: compareItemIndex, startIndex: startIndex, endIndex: endIndex, itemHeight: itemHeight, getItemKey: getItemKeyByIndex, itemElementHeights: refItemHeightMap.current, }); var similarity = Math.abs(compareItemTop - compareItemRelativeTop); if (similarity < bestSimilarity) { bestSimilarity = similarity; bestScrollTop = scrollTop; bestItemIndex = itemIndex; bestItemOffsetPtg = itemOffsetPtg; bestStartIndex = startIndex; bestEndIndex = endIndex; missSimilarity = 0; } else { missSimilarity += 1; } } if (missSimilarity > 10) { break; } } if (bestScrollTop !== null) { refLockScroll.current = true; refList.current.scrollTop = bestScrollTop; setState(__assign(__assign({}, state), { status: 'MEASURE_START', scrollTop: bestScrollTop, itemIndex: bestItemIndex, itemOffsetPtg: bestItemOffsetPtg, startIndex: bestStartIndex, endIndex: bestEndIndex })); } refRafId.current = raf(function () { refLockScroll.current = false; }); }; // Record the current element position when the real list is scrolled, and ensure that the position is correct after switching to the virtual list var rawListScrollHandler = function (event) { var _a = refList.current, rawScrollTop = _a.scrollTop, clientHeight = _a.clientHeight, scrollHeight = _a.scrollHeight; var scrollTop = getValidScrollTop(rawScrollTop, scrollHeight - clientHeight); var scrollPtg = getScrollPercentage({ scrollTop: scrollTop, clientHeight: clientHeight, scrollHeight: scrollHeight, }); var _b = getLocationItem(scrollPtg, itemCount), index = _b.index, offsetPtg = _b.offsetPtg; setState(__assign(__assign({}, state), { scrollTop: scrollTop, itemIndex: index, itemOffsetPtg: offsetPtg })); event && onScroll && onScroll(event); }; // Modify the state and recalculate the position in the next render var virtualListScrollHandler = function (event, isInit) { if (isInit === void 0) { isInit = false; } var _a = refList.current, rawScrollTop = _a.scrollTop, clientHeight = _a.clientHeight, scrollHeight = _a.scrollHeight; var scrollTop = getValidScrollTop(rawScrollTop, scrollHeight - clientHeight); // Prevent jitter if (!isInit && (scrollTop === state.scrollTop || refLockScroll.current)) { return; } var scrollPtg = getScrollPercentage({ scrollTop: scrollTop, clientHeight: clientHeight, scrollHeight: scrollHeight, }); var _b = getRangeIndex(scrollPtg, itemCount, itemCountVisible), itemIndex = _b.itemIndex, itemOffsetPtg = _b.itemOffsetPtg, startIndex = _b.startIndex, endIndex = _b.endIndex; setState(__assign(__assign({}, state), { scrollTop: scrollTop, itemIndex: itemIndex, itemOffsetPtg: itemOffsetPtg, startIndex: startIndex, endIndex: endIndex, status: 'MEASURE_START' })); event && onScroll && onScroll(event); }; useEffect(function () { return function () { refRafId.current && caf(refRafId.current); }; }, []); // rerender when the number of visible elements changes useEffect(function () { if (refList.current) { if (isFirstRender) { refList.current.scrollTop = 0; } virtualListScrollHandler(null, true); } }, [itemCountVisible]); // Handle additions and deletions of list items or switching the virtual state useEffect(function () { var changedItemIndex = null; var switchTo = refIsVirtual.current !== isVirtual ? (isVirtual ? 'virtual' : 'raw') : ''; refIsVirtual.current = isVirtual; if (viewportHeight && prevData.length !== data.length) { var diff = findListDiffIndex(prevData, data, getItemKey); changedItemIndex = diff ? diff.index : null; } // No need to correct the position when the number of elements in the real list changes if (switchTo || (isVirtual && changedItemIndex)) { var clientHeight = refList.current.clientHeight; var locatedItemRelativeTop = getItemRelativeTop({ itemHeight: getCachedItemHeight(getItemKeyByIndex(state.itemIndex, prevData)), itemOffsetPtg: state.itemOffsetPtg, scrollPtg: getScrollPercentage({ scrollTop: state.scrollTop, scrollHeight: prevData.length * itemHeight, clientHeight: clientHeight, }), clientHeight: clientHeight, }); if (switchTo === 'raw') { var rawTop = locatedItemRelativeTop; for (var index = 0; index < state.itemIndex; index++) { rawTop -= getCachedItemHeight(getItemKeyByIndex(index)); } refList.current.scrollTop = -rawTop; refLockScroll.current = true; refRafId.current = raf(function () { refLockScroll.current = false; }); } else { internalScrollTo({ itemIndex: state.itemIndex, relativeTop: locatedItemRelativeTop, }); } } }, [data, isVirtual]); useIsomorphicLayoutEffect(function () { if (state.status === 'MEASURE_START') { var _a = refList.current, scrollTop = _a.scrollTop, scrollHeight = _a.scrollHeight, clientHeight = _a.clientHeight; var scrollPtg = getScrollPercentage({ scrollTop: scrollTop, scrollHeight: scrollHeight, clientHeight: clientHeight, }); // Calculate the top value of the first rendering element var startItemTop = getItemAbsoluteTop({ scrollPtg: scrollPtg, clientHeight: clientHeight, scrollTop: scrollTop - (scrollListPadding.top + scrollListPadding.bottom) * scrollPtg, itemHeight: getCachedItemHeight(getItemKeyByIndex(state.itemIndex)), itemOffsetPtg: state.itemOffsetPtg, }); for (var index = state.itemIndex - 1; index >= state.startIndex; index--) { startItemTop -= getCachedItemHeight(getItemKeyByIndex(index)); } setState(__assign(__assign({}, state), { startItemTop: startItemTop, status: 'MEASURE_DONE' })); } }, [state]); useImperativeHandle(ref, function () { return ({ dom: refList.current, // Scroll to a certain height or an element scrollTo: function (arg) { refRafId.current && caf(refRafId.current); refRafId.current = raf(function () { var _a; if (typeof arg === 'number') { refList.current.scrollTop = arg; return; } var index = 'index' in arg ? arg.index : 'key' in arg ? data.findIndex(function (item, index) { return getItemKey(item, index) === arg.key; }) : 0; var item = data[index]; if (!item) { return; } var align = typeof arg === 'object' && ((_a = arg.options) === null || _a === void 0 ? void 0 : _a.block) ? arg.options.block : (scrollOptions === null || scrollOptions === void 0 ? void 0 : scrollOptions.block) || 'nearest'; var _b = refList.current, clientHeight = _b.clientHeight, scrollTop = _b.scrollTop; if (isVirtual && !isStaticItemHeight) { if (align === 'nearest') { var itemIndex = state.itemIndex, itemOffsetPtg = state.itemOffsetPtg; if (Math.abs(itemIndex - index) < itemCountVisible) { var itemTop = getItemRelativeTop({ itemHeight: getCachedItemHeight(getItemKeyByIndex(itemIndex)), itemOffsetPtg: itemOffsetPtg, clientHeight: clientHeight, scrollPtg: getScrollPercentage(refList.current), }); if (index < itemIndex) { for (var i = index; i < itemIndex; i++) { itemTop -= getCachedItemHeight(getItemKeyByIndex(i)); } } else { for (var i = itemIndex; i < index; i++) { itemTop += getCachedItemHeight(getItemKeyByIndex(i)); } } // When the target element is within the field of view, exit directly if (itemTop < 0 || itemTop > clientHeight) { align = itemTop < 0 ? 'start' : 'end'; } else { return; } } else { align = index < itemIndex ? 'start' : 'end'; } } setState(__assign(__assign({}, state), { startIndex: Math.max(0, index - itemCountVisible), endIndex: Math.min(itemCount - 1, index + itemCountVisible) })).then(function () { var itemHeight = getCachedItemHeight(getItemKey(item, index)); internalScrollTo({ itemIndex: index, relativeTop: align === 'start' ? 0 : (clientHeight - itemHeight) / (align === 'center' ? 2 : 1), }); }); } else { var indexItemHeight = getCachedItemHeight(getItemKeyByIndex(index)); var itemTop = 0; for (var i = 0; i < index; i++) { itemTop += getCachedItemHeight(getItemKeyByIndex(i)); } var itemBottom = itemTop + indexItemHeight; if (align === 'nearest') { if (itemTop < scrollTop) { align = 'start'; } else if (itemBottom > scrollTop + clientHeight) { align = 'end'; } } var viewportHeight_1 = clientHeight - indexItemHeight; refList.current.scrollTop = itemTop - (align === 'start' ? 0 : viewportHeight_1 / (align === 'center' ? 2 : 1)); } }); }, }); }, [data, itemHeight]); var renderChildren = function (list, startIndex) { return list.map(function (item, index) { var originIndex = startIndex + index; var node = renderChild(item, originIndex, { style: {}, }); var key = getItemKey(item, originIndex); return React.cloneElement(node, { key: key, ref: function (ele) { var heightMap = refItemHeightMap.current; // Minimize the measurement of element height as much as possible to avoid frequent triggering of browser reflow // Method getNodeHeight get the clientHeight from the DOM referred by React ref. If result is wrong, check the ref of this element if (ele && state.status === 'MEASURE_START' && (!isStaticItemHeight || heightMap[key] === undefined)) { if (isStaticItemHeight) { if (!heightMap[KEY_VIRTUAL_ITEM_HEIGHT]) { heightMap[KEY_VIRTUAL_ITEM_HEIGHT] = getNodeHeight(ele); } heightMap[key] = heightMap[KEY_VIRTUAL_ITEM_HEIGHT]; } else { heightMap[key] = getNodeHeight(ele); } } }, }); }); }; // Render the widest element to provide the maximum width of the container initially var refLongestItemIndex = useRef(null); // Don't add `renderChild` to the array dependency, it will change every time when rerender useEffect(function () { refLongestItemIndex.current = null; }, [data]); var renderLongestItem = function () { if (measureLongestItem) { var index = refLongestItemIndex.current === null ? getLongestItemIndex(data) : refLongestItemIndex.current; var item = data[index]; refLongestItemIndex.current = index; return item ? (React.createElement("div", { style: { height: 1, overflow: 'hidden', opacity: 0 } }, renderChild(item, index, { style: {} }))) : null; } return null; }; return (React.createElement(ResizeObserver, { onResize: function () { if (refList.current && !isNumber(styleListMaxHeight)) { var clientHeight = refList.current.clientHeight; setStateHeight(clientHeight); } } }, React.createElement(WrapperTagName, __assign({ ref: refList, style: __assign(__assign({ overflowY: 'auto', overflowAnchor: 'none' }, style), { maxHeight: styleListMaxHeight }), className: className, onScroll: isVirtual ? virtualListScrollHandler : rawListScrollHandler }, restProps), isVirtual ? (React.createElement(React.Fragment, null, React.createElement(Filler, { height: itemTotalHeight, offset: state.status === 'MEASURE_DONE' ? state.startItemTop : 0 }, renderChildren(data.slice(state.startIndex, state.endIndex + 1), state.startIndex)), renderLongestItem())) : (React.createElement(Filler, { height: viewportHeight }, renderChildren(data, 0)))))); }); VirtualList.displayName = 'VirtualList'; export default VirtualList;