@nutui/nutui-react
Version:
京东风格的轻量级移动端 React 组件库,支持一套代码生成 H5 和小程序
254 lines (253 loc) • 8.72 kB
JavaScript
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
};