UNPKG

@etsoo/react

Version:

TypeScript ReactJs UI Independent Framework

252 lines (251 loc) 9.53 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import React from "react"; import { VariableSizeGrid } from "react-window"; /** * Scroller vertical grid * @param props Props * @returns Component */ export const ScrollerGrid = (props) => { // Destruct const { autoLoad = true, defaultOrderBy, footerRenderer, headerRenderer, itemRenderer, idField = "id", loadBatchSize, loadData, mRef, onItemsRendered, onSelectChange, rowHeight = 53, threshold = 6, width, onInitLoad, onUpdateRows, ...rest } = props; // Rows const [rows, updateRows] = React.useState([]); const setRows = (rows, reset = false) => { refs.current.loadedItems = rows.length; updateRows(rows); if (!reset && onUpdateRows) onUpdateRows(rows, refs.current); }; // Refs const refs = React.useRef({ queryPaging: { currentPage: 0, orderBy: defaultOrderBy, batchSize: 10 }, autoLoad, hasNextPage: true, isNextPageLoading: false, loadedItems: 0, selectedItems: [], idCache: {} }); const ref = React.useRef(null); // Load data const loadDataLocal = (pageAdd = 1) => { // Prevent multiple loadings if (!refs.current.hasNextPage || refs.current.isNextPageLoading || refs.current.isMounted === false) return; // Update state refs.current.isNextPageLoading = true; // Parameters const { queryPaging, data } = refs.current; const loadProps = { queryPaging, data }; loadData(loadProps, refs.current.lastItem).then((result) => { if (result == null || refs.current.isMounted === false) { return; } refs.current.isMounted = true; const newItems = result.length; refs.current.lastLoadedItems = newItems; refs.current.lastItem = result.at(-1); refs.current.isNextPageLoading = false; refs.current.hasNextPage = newItems >= refs.current.queryPaging.batchSize; if (pageAdd === 0) { // New items const newRows = refs.current.lastLoadedItems ? [...rows] .splice(rows.length - refs.current.lastLoadedItems, refs.current.lastLoadedItems) .concat(result) : result; refs.current.idCache = {}; for (const row of newRows) { const id = row[idField]; refs.current.idCache[id] = null; } // Update rows setRows(newRows); } else { // Set current page if (refs.current.queryPaging.currentPage == null) refs.current.queryPaging.currentPage = pageAdd; else refs.current.queryPaging.currentPage += pageAdd; // Update rows, avoid duplicate items const newRows = [...rows]; for (const item of result) { const id = item[idField]; if (refs.current.idCache[id] === undefined) { newRows.push(item); } } setRows(newRows); } }); }; // Item renderer const itemRendererLocal = (itemProps, state) => { // Custom render const data = itemProps.rowIndex < rows.length ? rows[itemProps.rowIndex] : undefined; return itemRenderer({ ...itemProps, data, selectedItems: state.selectedItems, setItems: (callback) => { const result = callback(rows, instance); if (result == null) return; setRows(result); } }); }; // Local items renderer callback const onItemsRenderedLocal = (props) => { // No items, means no necessary to load more data during reset const itemCount = rows.length; if (itemCount > 0 && props.visibleRowStopIndex + threshold > itemCount) { // Auto load next page loadDataLocal(); } // Custom if (onItemsRendered) onItemsRendered(props); }; // 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(refs.current, resetState); Object.assign(refs.current.queryPaging, { currentPage: 0, ...queryPaging }); // Reset items if (refs.current.isMounted !== false) setRows(items, true); }; const instance = { 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); }, scrollTo(params) { ref.current?.scrollTo(params); }, scrollToItem(params) { ref.current?.scrollToItem(params); }, scrollToRef(scrollOffset) { ref.current?.scrollTo({ scrollLeft: 0, scrollTop: scrollOffset }); }, scrollToItemRef(index, align) { ref.current?.scrollToItem({ rowIndex: index, align }); }, select(rowIndex) { // Select only one item const selectedItems = refs.current.selectedItems; selectedItems[0] = rows[rowIndex]; if (onSelectChange) onSelectChange(selectedItems); }, selectAll(checked) { const selectedItems = refs.current.selectedItems; rows.forEach((row) => { const index = selectedItems.findIndex((selectedItem) => selectedItem[idField] === row[idField]); if (checked) { if (index === -1) selectedItems.push(row); } else if (index !== -1) { selectedItems.splice(index, 1); } }); if (onSelectChange) onSelectChange(selectedItems); }, selectItem(item, checked) { const selectedItems = refs.current.selectedItems; const index = selectedItems.findIndex((selectedItem) => selectedItem[idField] === item[idField]); if (checked) { if (index === -1) selectedItems.push(item); } else { if (index !== -1) selectedItems.splice(index, 1); } if (onSelectChange) onSelectChange(selectedItems); }, reset, resetAfterColumnIndex(index, shouldForceUpdate) { ref.current?.resetAfterColumnIndex(index, shouldForceUpdate); }, resetAfterIndices(params) { ref.current?.resetAfterIndices(params); }, resetAfterRowIndex(index, shouldForceUpdate) { ref.current?.resetAfterRowIndex(index, shouldForceUpdate); } }; React.useImperativeHandle(mRef, () => instance, [rows]); // Force update to work with the new width and rowHeight React.useEffect(() => { ref.current?.resetAfterIndices({ columnIndex: 0, rowIndex: 0, shouldForceUpdate: true }); }, [width, rowHeight]); // Rows const rowLength = rows.length; // Row count const rowCount = refs.current.hasNextPage ? rowLength + 1 : rowLength; React.useEffect(() => { // Auto load data when current page is 0 if (refs.current.queryPaging.currentPage === 0 && refs.current.autoLoad) { const initItems = onInitLoad == null ? undefined : onInitLoad(ref.current); if (initItems) reset(initItems[1], initItems[0]); else loadDataLocal(); } }, [onInitLoad, loadDataLocal]); React.useEffect(() => { return () => { refs.current.isMounted = false; }; }, []); // Layout return (_jsxs(React.Fragment, { children: [headerRenderer && headerRenderer(refs.current), _jsx(VariableSizeGrid, { itemKey: ({ columnIndex, rowIndex, data }) => { if (data == null) return [rowIndex, columnIndex].join(","); // ${data[idField]}-${rowIndex} always unique but no cache for the same item return [`${data[idField]}`, columnIndex].join(","); }, onItemsRendered: onItemsRenderedLocal, ref: ref, rowCount: rowCount, rowHeight: typeof rowHeight === "function" ? rowHeight : () => rowHeight, style: { overflowX: "hidden" }, width: width, ...rest, children: (props) => itemRendererLocal(props, refs.current) }), footerRenderer && footerRenderer(rows, refs.current)] })); };