UNPKG

@etsoo/react

Version:

TypeScript ReactJs UI Independent Framework

203 lines (202 loc) 7.8 kB
import { jsx as _jsx } from "react/jsx-runtime"; import { DataTypes, Utils } from "@etsoo/shared"; import React from "react"; import { FixedSizeList, VariableSizeList } from "react-window"; import { useCombinedRefs } from "../uses/useCombinedRefs"; import { GridSizeGet } from "./GridLoader"; // Calculate loadBatchSize const calculateBatchSize = (height, itemSize) => { const size = Utils.getResult(itemSize, 0); return 2 + Math.ceil(height / size); }; /** * Scroller vertical list * @param props Props * @returns Component */ export const ScrollerList = (props) => { // Destruct const { autoLoad = true, defaultOrderBy, height = document.documentElement.clientHeight, width = "100%", mRef, oRef, style = {}, idField = "id", itemRenderer, itemSize, loadBatchSize = calculateBatchSize(height, itemSize), loadData, threshold = GridSizeGet(loadBatchSize, height) / 2, onItemsRendered, onInitLoad, onUpdateRows, ...rest } = props; // Style Object.assign(style, { width: "100%", height: "100%", display: "inline-block" }); // Refs const listRef = React.useRef(); const outerRef = React.useRef(); const refs = useCombinedRefs(oRef, outerRef); // Rows const [rows, updateRows] = React.useState([]); const setRows = (rows, reset = false) => { stateRefs.current.loadedItems = rows.length; updateRows(rows); if (!reset && onUpdateRows) onUpdateRows(rows, stateRefs.current); }; // States const batchSize = GridSizeGet(loadBatchSize, height); const stateRefs = React.useRef({ queryPaging: { currentPage: 0, orderBy: defaultOrderBy, batchSize }, autoLoad, loadedItems: 0, hasNextPage: true, isNextPageLoading: false, selectedItems: [], idCache: {} }); // Load data const loadDataLocal = (pageAdd = 1) => { // Prevent multiple loadings if (!stateRefs.current.hasNextPage || stateRefs.current.isNextPageLoading || stateRefs.current.isMounted === false) return; // Update state stateRefs.current.isNextPageLoading = true; // Parameters const { queryPaging, data } = stateRefs.current; const loadProps = { queryPaging, data }; loadData(loadProps, stateRefs.current.lastItem).then((result) => { if (result == null || stateRefs.current.isMounted === false) { return; } stateRefs.current.isMounted = true; const newItems = result.length; stateRefs.current.lastLoadedItems = newItems; stateRefs.current.lastItem = result.at(-1); stateRefs.current.hasNextPage = newItems >= batchSize; stateRefs.current.isNextPageLoading = false; if (pageAdd === 0) { // New items const newRows = stateRefs.current.lastLoadedItems ? [...rows] .splice(rows.length - stateRefs.current.lastLoadedItems, stateRefs.current.lastLoadedItems) .concat(result) : result; stateRefs.current.idCache = {}; for (const row of newRows) { const id = row[idField]; stateRefs.current.idCache[id] = null; } // Update rows setRows(newRows); } else { if (stateRefs.current.queryPaging.currentPage == null) stateRefs.current.queryPaging.currentPage = pageAdd; else stateRefs.current.queryPaging.currentPage += pageAdd; // Update rows, avoid duplicate items const newRows = [...rows]; for (const item of result) { const id = item[idField]; if (stateRefs.current.idCache[id] === undefined) { newRows.push(item); } } setRows(newRows); } }); }; const itemRendererLocal = (itemProps) => { // Custom render return itemRenderer({ ...itemProps, data: rows[itemProps.index] }); }; // Reset the state and load again const reset = (add, items = []) => { const { queryPaging, ...rest } = add ?? {}; const resetState = { autoLoad: true, loadedItems: 0, hasNextPage: true, isNextPageLoading: false, lastLoadedItems: undefined, lastItem: undefined, ...rest }; Object.assign(stateRefs.current, resetState); Object.assign(stateRefs.current.queryPaging, { currentPage: 0, ...queryPaging }); // Reset if (stateRefs.current.isMounted !== false) setRows(items, true); }; React.useImperativeHandle(mRef, () => { const refMethods = listRef.current; return { delete(index) { const item = rows.at(index); if (item) { const newRows = [...rows]; newRows.splice(index, 1); setRows(newRows); } return item; }, insert(item, start) { const newRows = [...rows]; newRows.splice(start, 0, item); setRows(newRows); }, refresh() { loadDataLocal(0); }, reset, scrollToRef(scrollOffset) { refMethods.scrollTo(scrollOffset); }, scrollToItemRef(index, align) { refMethods.scrollToItem(index, align); } }; }, []); // Row count const rowCount = rows.length; // Local items renderer callback const onItemsRenderedLocal = (props) => { // No items, means no necessary to load more data during reset if (rowCount > 0 && props.visibleStopIndex + threshold > rowCount) { // Auto load next page loadDataLocal(); } // Custom if (onItemsRendered) onItemsRendered(props); }; // Item count const itemCount = stateRefs.current.hasNextPage ? rowCount + 1 : rowCount; React.useEffect(() => { // Auto load data when current page is 0 if (stateRefs.current.queryPaging?.currentPage === 0 && stateRefs.current.autoLoad) { const initItems = onInitLoad == null ? undefined : onInitLoad(listRef.current); if (initItems) reset(initItems[1], initItems[0]); else loadDataLocal(); } }, [onInitLoad, loadDataLocal]); // When layout ready React.useEffect(() => { // Return clear function return () => { stateRefs.current.isMounted = false; }; }, []); // Layout return typeof itemSize === "function" ? (_jsx(VariableSizeList, { height: height, width: width, itemCount: itemCount, itemKey: (index, data) => DataTypes.getIdValue1(data, idField) ?? index, itemSize: itemSize, outerRef: refs, ref: listRef, style: style, onItemsRendered: onItemsRenderedLocal, ...rest, children: itemRendererLocal })) : (_jsx(FixedSizeList, { height: height, width: width, itemCount: itemCount, itemKey: (index, data) => DataTypes.getIdValue1(data, idField) ?? index, itemSize: itemSize, outerRef: refs, ref: listRef, style: style, onItemsRendered: onItemsRenderedLocal, ...rest, children: itemRendererLocal })); };