UNPKG

@nutui/nutui-react

Version:

京东风格的轻量级移动端 React 组件库,支持一套代码生成 H5 和小程序

254 lines (253 loc) 8.72 kB
import { _ as __rest } from "./tslib.es6.js"; import React__default, { useRef, useState, useEffect, useCallback } from "react"; import classNames from "classnames"; import { C as ComponentDefaults } from "./typings.js"; const initPositinoCache = (reaItemSize, length = 0) => { let index = 0; const positions = Array(length); while (index < length) { positions[index] = { index, height: reaItemSize, width: reaItemSize, top: index * reaItemSize, bottom: (index + 1) * reaItemSize, left: index * reaItemSize, right: (index + 1) * reaItemSize }; index++; } return positions; }; const getListTotalSize = (positions, horizontal) => { const index = positions.length - 1; let size = 0; if (index < 0) { size = 0; } else { size = horizontal ? positions[index].right : positions[index].bottom; } return size; }; const binarySearch = (positionsList, horizontal, value = 0) => { let start = 0; let end = positionsList.length - 1; let tempIndex = null; const key = horizontal ? "right" : "bottom"; while (start <= end) { const midIndex = Math.floor((start + end) / 2); const midValue = positionsList[midIndex][key]; if (midValue === value) { return midIndex + 1; } if (midValue < value) { start = midIndex + 1; } else if (midValue > value) { if (tempIndex === null || tempIndex > midIndex) { tempIndex = midIndex; } end = midIndex - 1; } } tempIndex = tempIndex || 0; return tempIndex; }; const getEndIndex = ({ list, startIndex, visibleCount, itemEqual = true, positions, offSetSize, overscan, sizeKey = "width" }) => { const dataLength = list.length; let tempIndex = null; if (itemEqual) { const endIndex = startIndex + visibleCount; tempIndex = dataLength > 0 ? Math.min(dataLength, endIndex) : endIndex; } else { let sizeNum = 0; for (let i = startIndex; i < dataLength; i++) { sizeNum += positions[i][sizeKey] || 0; if (sizeNum > offSetSize) { const endIndex = i + overscan; tempIndex = dataLength > 0 ? Math.min(dataLength, endIndex) : endIndex; break; } } if (sizeNum < offSetSize) { tempIndex = dataLength; } } tempIndex = tempIndex || 0; return tempIndex; }; const updateItemSize = (positions, items, sizeKey, margin) => { const newPos = positions.concat(); Array.from(items).forEach((item) => { const index = Number(item.getAttribute("data-index")); const styleVal = item.getAttribute("style"); if (styleVal && styleVal.includes("none")) return; let nowSize = item.getBoundingClientRect()[sizeKey]; const oldSize = positions[index][sizeKey]; const dValue = oldSize - nowSize; if (dValue) { if (sizeKey === "width") { newPos[index].right -= dValue; newPos[index][sizeKey] = nowSize; for (let k = index + 1; k < positions.length; k++) { newPos[k].left = positions[k - 1].right; newPos[k].right -= dValue; } } else if (sizeKey === "height") { newPos[index].bottom -= dValue; newPos[index][sizeKey] = nowSize; for (let k = index + 1; k < positions.length; k++) { newPos[k].top = positions[k - 1].bottom; newPos[k].bottom -= dValue; } } } }); }; const defaultProps = Object.assign(Object.assign({}, ComponentDefaults), { list: [], itemHeight: 66, itemEqual: true, direction: "vertical", overscan: 2 }); const VirtualList = (props) => { const _a = Object.assign(Object.assign({}, defaultProps), props), { list, itemRender, itemEqual, itemHeight, direction, overscan, key, onScroll, className, containerHeight } = _a, rest = __rest(_a, ["list", "itemRender", "itemEqual", "itemHeight", "direction", "overscan", "key", "onScroll", "className", "containerHeight"]); const horizontal = direction === "horizontal"; const sizeKey = horizontal ? "width" : "height"; const scrollKey = horizontal ? "scrollLeft" : "scrollTop"; const offsetKey = horizontal ? "left" : "top"; const scrollRef = useRef(null); const itemsRef = useRef(null); const [positions, setPositions] = useState([ { index: 0, left: 0, top: 0, bottom: 0, width: 0, height: 0, right: 0 } ]); const [listTotalSize, setListTotalSize] = useState(99999999); const [visibleCount, setVisibleCount] = useState(0); const [offSetSize, setOffSetSize] = useState(containerHeight || 0); const [options, setOptions] = useState({ startOffset: 0, // 可视区域距离顶部的偏移量 startIndex: 0, // 可视区域开始索引 overStart: 0, endIndex: 10 // 可视区域结束索引 }); useEffect(() => { const pos = initPositinoCache(itemHeight, list.length); setPositions(pos); const totalSize = getListTotalSize(pos, horizontal); setListTotalSize(totalSize); }, [list, itemHeight, horizontal]); const getElement = useCallback(() => { var _a2; return ((_a2 = scrollRef.current) === null || _a2 === void 0 ? void 0 : _a2.parentElement) || document.body; }, []); useEffect(() => { if (containerHeight) return; const size = horizontal ? getElement().offsetWidth : getElement().offsetHeight; setOffSetSize(size); }, [getElement, horizontal, containerHeight]); useEffect(() => { if (offSetSize === 0) return; const count = Math.ceil(offSetSize / itemHeight) + overscan; setVisibleCount(count); setOptions((options2) => { return Object.assign(Object.assign({}, options2), { endIndex: count }); }); }, [getElement, horizontal, itemHeight, overscan, offSetSize]); const updateTotalSize = useCallback(() => { if (!itemsRef.current) return; const items = itemsRef.current.children; if (!items.length) return; updateItemSize(positions, items, sizeKey); const totalSize = getListTotalSize(positions, horizontal); setListTotalSize(totalSize); }, [positions, sizeKey, horizontal]); const scroll = useCallback(() => { requestAnimationFrame((e) => { const scrollSize = getElement()[scrollKey]; const startIndex = binarySearch(positions, horizontal, scrollSize); const overStart = startIndex - overscan > -1 ? startIndex - overscan : 0; if (!itemEqual) { updateTotalSize(); } const endIndex = getEndIndex({ list, startIndex, visibleCount, itemEqual, positions, offSetSize, sizeKey, overscan }); const startOffset = positions[startIndex][offsetKey]; setOptions({ startOffset, startIndex, overStart, endIndex }); if (endIndex > list.length - 1) { if (onScroll) { onScroll(); } } }); }, [ positions, getElement, list, visibleCount, itemEqual, updateTotalSize, offsetKey, sizeKey, scrollKey, horizontal, overscan, offSetSize ]); useEffect(() => { const element = getElement(); element.addEventListener("scroll", scroll, false); return () => { element.removeEventListener("scroll", scroll, false); }; }, [getElement, scroll]); return React__default.createElement( "div", Object.assign({ className: classNames("nut-virtualList-box", className) }, rest, { style: { [sizeKey]: containerHeight ? `${offSetSize}px` : "" } }), React__default.createElement( "div", { ref: scrollRef, className: classNames({ "nut-horizontal-box": horizontal, "nut-vertical-box": !horizontal }), style: { position: "relative", [sizeKey]: `${listTotalSize}px` } }, React__default.createElement("ul", { className: classNames("nut-virtuallist-items", { "nut-horizontal-items": horizontal, "nut-vertical-items": !horizontal }), ref: itemsRef, style: { transform: horizontal ? `translate3d(${options.startOffset}px,0,0)` : `translate3d(0,${options.startOffset}px,0)` } }, list.slice(options.overStart, options.endIndex).map((data, index) => { const { startIndex, overStart } = options; const dataIndex = overStart + index; const styleVal = dataIndex < startIndex ? "none" : "block"; const keyVal = key && data[key] ? data[key] : dataIndex; return React__default.createElement("li", { "data-index": `${dataIndex}`, className: "nut-virtuallist-item", key: `${keyVal}`, style: { display: styleVal } }, itemRender ? itemRender(data, dataIndex, index) : data); })) ) ); }; VirtualList.displayName = "NutVirtualList"; export { VirtualList as default };