Add ref controls to infinite grid
This commit is contained in:
parent
520b7ce136
commit
26ea4c0cc9
1 changed files with 133 additions and 113 deletions
|
@ -1,4 +1,12 @@
|
||||||
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
import {
|
||||||
|
useState,
|
||||||
|
useRef,
|
||||||
|
useMemo,
|
||||||
|
useCallback,
|
||||||
|
forwardRef,
|
||||||
|
Ref,
|
||||||
|
useImperativeHandle,
|
||||||
|
} from 'react';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import type { FixedSizeListProps } from 'react-window';
|
import type { FixedSizeListProps } from 'react-window';
|
||||||
import InfiniteLoader from 'react-window-infinite-loader';
|
import InfiniteLoader from 'react-window-infinite-loader';
|
||||||
|
@ -6,6 +14,12 @@ import { VirtualGridWrapper } from '/@/renderer/components/virtual-grid/virtual-
|
||||||
import type { CardRoute, CardRow, LibraryItem, PlayQueueAddOptions } from '/@/renderer/types';
|
import type { CardRoute, CardRow, LibraryItem, PlayQueueAddOptions } from '/@/renderer/types';
|
||||||
import { CardDisplayType } from '/@/renderer/types';
|
import { CardDisplayType } from '/@/renderer/types';
|
||||||
|
|
||||||
|
export type VirtualInfiniteGridRef = {
|
||||||
|
resetLoadMoreItemsCache: () => void;
|
||||||
|
scrollTo: (index: number) => void;
|
||||||
|
setItemData: (data: any[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
interface VirtualGridProps extends Omit<FixedSizeListProps, 'children' | 'itemSize'> {
|
interface VirtualGridProps extends Omit<FixedSizeListProps, 'children' | 'itemSize'> {
|
||||||
cardRows: CardRow<any>[];
|
cardRows: CardRow<any>[];
|
||||||
display?: CardDisplayType;
|
display?: CardDisplayType;
|
||||||
|
@ -15,7 +29,6 @@ interface VirtualGridProps extends Omit<FixedSizeListProps, 'children' | 'itemSi
|
||||||
itemSize: number;
|
itemSize: number;
|
||||||
itemType: LibraryItem;
|
itemType: LibraryItem;
|
||||||
minimumBatchSize?: number;
|
minimumBatchSize?: number;
|
||||||
refresh?: any;
|
|
||||||
route?: CardRoute;
|
route?: CardRoute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,131 +40,138 @@ const constrainWidth = (width: number) => {
|
||||||
return 1920;
|
return 1920;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VirtualInfiniteGrid = ({
|
export const VirtualInfiniteGrid = forwardRef(
|
||||||
itemCount,
|
(
|
||||||
itemGap,
|
{
|
||||||
itemSize,
|
itemCount,
|
||||||
itemType,
|
itemGap,
|
||||||
cardRows,
|
itemSize,
|
||||||
route,
|
itemType,
|
||||||
onScroll,
|
cardRows,
|
||||||
display,
|
route,
|
||||||
handlePlayQueueAdd,
|
onScroll,
|
||||||
minimumBatchSize,
|
display,
|
||||||
fetchFn,
|
handlePlayQueueAdd,
|
||||||
initialScrollOffset,
|
minimumBatchSize,
|
||||||
height,
|
fetchFn,
|
||||||
width,
|
initialScrollOffset,
|
||||||
refresh,
|
height,
|
||||||
}: VirtualGridProps) => {
|
width,
|
||||||
const [itemData, setItemData] = useState<any[]>([]);
|
}: VirtualGridProps,
|
||||||
const listRef = useRef<any>(null);
|
ref: Ref<VirtualInfiniteGridRef>,
|
||||||
const loader = useRef<InfiniteLoader>(null);
|
) => {
|
||||||
|
const [itemData, setItemData] = useState<any[]>([]);
|
||||||
|
const listRef = useRef<any>(null);
|
||||||
|
const loader = useRef<InfiniteLoader>(null);
|
||||||
|
|
||||||
const { itemHeight, rowCount, columnCount } = useMemo(() => {
|
const { itemHeight, rowCount, columnCount } = useMemo(() => {
|
||||||
const itemsPerRow = Math.floor(
|
const itemsPerRow = Math.floor(
|
||||||
(constrainWidth(Number(width)) - itemGap + 3) / (itemSize! + itemGap + 2),
|
(constrainWidth(Number(width)) - itemGap + 3) / (itemSize! + itemGap + 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
columnCount: itemsPerRow,
|
||||||
|
itemHeight: itemSize! + cardRows.length * 22 + itemGap,
|
||||||
|
itemWidth: itemSize! + itemGap,
|
||||||
|
rowCount: Math.ceil(itemCount / itemsPerRow),
|
||||||
|
};
|
||||||
|
}, [cardRows.length, itemCount, itemGap, itemSize, width]);
|
||||||
|
|
||||||
|
const isItemLoaded = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
const itemIndex = index * columnCount;
|
||||||
|
|
||||||
|
return itemData[itemIndex] !== undefined;
|
||||||
|
},
|
||||||
|
[columnCount, itemData],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
const loadMoreItems = useCallback(
|
||||||
columnCount: itemsPerRow,
|
async (startIndex: number, stopIndex: number) => {
|
||||||
itemHeight: itemSize! + cardRows.length * 22 + itemGap,
|
// Fixes a caching bug(?) when switching between filters and the itemCount increases
|
||||||
itemWidth: itemSize! + itemGap,
|
if (startIndex === 1) return;
|
||||||
rowCount: Math.ceil(itemCount / itemsPerRow),
|
|
||||||
};
|
|
||||||
}, [cardRows.length, itemCount, itemGap, itemSize, width]);
|
|
||||||
|
|
||||||
const isItemLoaded = useCallback(
|
// Need to multiply by columnCount due to the grid layout
|
||||||
(index: number) => {
|
const start = startIndex * columnCount;
|
||||||
const itemIndex = index * columnCount;
|
const end = stopIndex * columnCount + columnCount;
|
||||||
|
|
||||||
return itemData[itemIndex] !== undefined;
|
const data = await fetchFn({
|
||||||
},
|
columnCount,
|
||||||
[columnCount, itemData],
|
skip: start,
|
||||||
);
|
take: end - start,
|
||||||
|
});
|
||||||
|
|
||||||
const loadMoreItems = useCallback(
|
const newData: any[] = [...itemData];
|
||||||
async (startIndex: number, stopIndex: number) => {
|
|
||||||
// Fixes a caching bug(?) when switching between filters and the itemCount increases
|
|
||||||
if (startIndex === 1) return;
|
|
||||||
|
|
||||||
// Need to multiply by columnCount due to the grid layout
|
let itemIndex = 0;
|
||||||
const start = startIndex * columnCount;
|
for (let rowIndex = start; rowIndex < end; rowIndex += 1) {
|
||||||
const end = stopIndex * columnCount + columnCount;
|
newData[rowIndex] = data.items[itemIndex];
|
||||||
|
itemIndex += 1;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await fetchFn({
|
setItemData(newData);
|
||||||
columnCount,
|
},
|
||||||
skip: start,
|
[columnCount, fetchFn, itemData],
|
||||||
take: end - start,
|
);
|
||||||
});
|
|
||||||
|
|
||||||
const newData: any[] = [...itemData];
|
const debouncedLoadMoreItems = debounce(loadMoreItems, 500);
|
||||||
|
|
||||||
let itemIndex = 0;
|
useImperativeHandle(ref, () => ({
|
||||||
for (let rowIndex = start; rowIndex < end; rowIndex += 1) {
|
resetLoadMoreItemsCache: () => {
|
||||||
newData[rowIndex] = data.items[itemIndex];
|
if (loader.current) {
|
||||||
itemIndex += 1;
|
loader.current.resetloadMoreItemsCache(false);
|
||||||
}
|
setItemData(() => []);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollTo: (index: number) => {
|
||||||
|
listRef.current.scrollToItem(index);
|
||||||
|
},
|
||||||
|
setItemData: (data: any[]) => {
|
||||||
|
setItemData(data);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
setItemData(newData);
|
return (
|
||||||
},
|
<InfiniteLoader
|
||||||
[columnCount, fetchFn, itemData],
|
ref={loader}
|
||||||
);
|
isItemLoaded={(index) => isItemLoaded(index)}
|
||||||
|
itemCount={itemCount || 0}
|
||||||
const debouncedLoadMoreItems = debounce(loadMoreItems, 500);
|
loadMoreItems={debouncedLoadMoreItems}
|
||||||
|
minimumBatchSize={minimumBatchSize}
|
||||||
useEffect(() => {
|
threshold={30}
|
||||||
if (loader.current) {
|
>
|
||||||
listRef.current.scrollTo(0);
|
{({ onItemsRendered, ref: infiniteLoaderRef }) => (
|
||||||
loader.current.resetloadMoreItemsCache(false);
|
<VirtualGridWrapper
|
||||||
setItemData(() => []);
|
cardRows={cardRows}
|
||||||
|
columnCount={columnCount}
|
||||||
loadMoreItems(0, minimumBatchSize! * 2);
|
display={display || CardDisplayType.CARD}
|
||||||
}
|
handlePlayQueueAdd={handlePlayQueueAdd}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
height={height}
|
||||||
}, [minimumBatchSize, fetchFn, refresh]);
|
initialScrollOffset={initialScrollOffset}
|
||||||
|
itemCount={itemCount || 0}
|
||||||
return (
|
itemData={itemData}
|
||||||
<InfiniteLoader
|
itemGap={itemGap}
|
||||||
ref={loader}
|
itemHeight={itemHeight + itemGap / 2}
|
||||||
isItemLoaded={(index) => isItemLoaded(index)}
|
itemType={itemType}
|
||||||
itemCount={itemCount || 0}
|
itemWidth={itemSize}
|
||||||
loadMoreItems={debouncedLoadMoreItems}
|
refInstance={(list) => {
|
||||||
minimumBatchSize={minimumBatchSize}
|
infiniteLoaderRef(list);
|
||||||
threshold={30}
|
listRef.current = list;
|
||||||
>
|
}}
|
||||||
{({ onItemsRendered, ref: infiniteLoaderRef }) => (
|
route={route}
|
||||||
<VirtualGridWrapper
|
rowCount={rowCount}
|
||||||
cardRows={cardRows}
|
width={width}
|
||||||
columnCount={columnCount}
|
onItemsRendered={onItemsRendered}
|
||||||
display={display || CardDisplayType.CARD}
|
onScroll={onScroll}
|
||||||
handlePlayQueueAdd={handlePlayQueueAdd}
|
/>
|
||||||
height={height}
|
)}
|
||||||
initialScrollOffset={initialScrollOffset}
|
</InfiniteLoader>
|
||||||
itemCount={itemCount || 0}
|
);
|
||||||
itemData={itemData}
|
},
|
||||||
itemGap={itemGap}
|
);
|
||||||
itemHeight={itemHeight + itemGap / 2}
|
|
||||||
itemType={itemType}
|
|
||||||
itemWidth={itemSize}
|
|
||||||
refInstance={(list) => {
|
|
||||||
infiniteLoaderRef(list);
|
|
||||||
listRef.current = list;
|
|
||||||
}}
|
|
||||||
route={route}
|
|
||||||
rowCount={rowCount}
|
|
||||||
width={width}
|
|
||||||
onItemsRendered={onItemsRendered}
|
|
||||||
onScroll={onScroll}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</InfiniteLoader>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
VirtualInfiniteGrid.defaultProps = {
|
VirtualInfiniteGrid.defaultProps = {
|
||||||
display: CardDisplayType.CARD,
|
display: CardDisplayType.CARD,
|
||||||
minimumBatchSize: 20,
|
minimumBatchSize: 20,
|
||||||
refresh: undefined,
|
|
||||||
route: undefined,
|
route: undefined,
|
||||||
};
|
};
|
||||||
|
|
Reference in a new issue