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