@etsoo/react
Version:
TypeScript ReactJs UI Independent Framework
203 lines (202 loc) • 7.8 kB
JavaScript
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 }));
};