diff --git a/components/VirtualList.tsx b/components/VirtualList.tsx new file mode 100644 index 0000000..1d6b285 --- /dev/null +++ b/components/VirtualList.tsx @@ -0,0 +1,98 @@ +export default function VirtualList(props: VirtualListProps): React.ReactElement +{ + const [columns, setColumns] = useState(1); + const [rowsToRender, setRowsToRender] = useState(0); + const [rowIndex, setRowIndex] = useState(0); + const additionalRows = 3; + + const heights: number[] = useMemo(() => + { + if (typeof props.itemHeight === "number") + return props.items.map(() => props.itemHeight as number); + + return props.items.map(props.itemHeight); + }, [props.items, props.itemHeight]); + const minHeight: number = Math.min(...heights); + + const totalRows: number = Math.ceil(props.items.length / columns); + const takeCount: number = rowsToRender * columns; + + const renderStartIndex: number = rowIndex * columns; + const paddingTop: number = calculateHeight(heights, 0, rowIndex, columns); + const paddingBottom: number = calculateHeight(heights, rowIndex + rowsToRender, totalRows, columns); + + const renderedItems = props.items.slice(renderStartIndex, renderStartIndex + takeCount); + + useEffect(() => + { + const container = document.querySelector(props.containerSelector); + + const handleResize = (): void => + { + if (!container) + return; + + const newRowsToRender: number = Math.ceil(container.clientHeight / minHeight) + additionalRows; + + setRowsToRender(newRowsToRender); + + if (!props.columnMinWidth) + { + setColumns(1); + return; + } + + const newColumns: number = Math.floor((container.clientWidth - (props.horizontalOffset ?? 0)) / (props.columnMinWidth)); + setColumns(Math.max(1, newColumns)); + }; + handleResize(); + + const handleScroll = (e: Event): void => + { + const target = e.target as HTMLElement; + const topOffset: number = target.scrollHeight - calculateHeight(heights, 0, totalRows, columns); + const scrollTop: number = Math.max(0, target.scrollTop - topOffset); + + const newIndex: number = Math.floor(scrollTop / minHeight - Math.floor(additionalRows / 2)); + console.log("scroll", scrollTop, newIndex); + setRowIndex(Math.max(0, newIndex)); + }; + container?.addEventListener("scroll", handleScroll); + window.addEventListener("resize", handleResize); + + return () => + { + container?.removeEventListener("scroll", handleScroll); + window.removeEventListener("resize", handleResize); + }; + }, [totalRows, props.columnMinWidth, props.horizontalOffset, heights, columns]); + + return ( +
+ { renderedItems.map((item, index) => props.itemRenderer(item, rowIndex * columns + index)) } +
+ ); +} + +export type VirtualListProps = { + className?: string; + itemHeight: number | ((item: T, index: number) => number); + containerSelector: string; + horizontalOffset?: number; + columnMinWidth?: number; + items: T[]; + itemRenderer: (item: T, index: number) => React.ReactElement; +}; + +function calculateHeight(heights: number[], rowStart: number, rowEnd: number, columns: number): number +{ + let height = 0; + + for (let i = rowStart; i < rowEnd; i++) + { + const rowHeights = heights.slice(i * columns, (i + 1) * columns); + height += Math.max(...rowHeights); + } + + return height; +} diff --git a/entrypoints/sidepanel/contexts/CollectionsProvider.tsx b/entrypoints/sidepanel/contexts/CollectionsProvider.tsx index 6213a5c..f717039 100644 --- a/entrypoints/sidepanel/contexts/CollectionsProvider.tsx +++ b/entrypoints/sidepanel/contexts/CollectionsProvider.tsx @@ -45,7 +45,37 @@ export default function CollectionsProvider({ children }: React.PropsWithChildre const addCollection = async (collection: CollectionItem): Promise => { - await updateStorage([collection, ...collections]); + // TEMP + // await updateStorage([collection, ...collections]); + const items: CollectionItem[] = []; + + for (let i = 0; i < 128; i++) + items.push({ + title: i.toString(), + items: [ + { + type: "tab", + title: "Google", + url: "https://www.google.com" + }, + { + type: "group", + title: "Group", + color: "blue", + items: [ + { + type: "tab", + title: "Facebook", + url: "https://www.facebook.com" + } + ] + } + ], + timestamp: Date.now() + i, + type: "collection" + }); + + await updateStorage(items); }; const removeItem = async (...indices: number[]): Promise => diff --git a/entrypoints/sidepanel/layouts/collections/CollectionListView.tsx b/entrypoints/sidepanel/layouts/collections/CollectionListView.tsx index d84c361..ebf7632 100644 --- a/entrypoints/sidepanel/layouts/collections/CollectionListView.tsx +++ b/entrypoints/sidepanel/layouts/collections/CollectionListView.tsx @@ -22,6 +22,8 @@ import { snapHandleToCursor } from "../../utils/dnd/snapHandleToCursor"; import { useStyles_CollectionListView } from "./CollectionListView.styles"; import SearchBar from "./SearchBar"; import StorageCapacityIssueMessage from "./messages/StorageCapacityIssueMessage"; +import VirtualList from "@/components/VirtualList"; +import calculateCollectionHeight from "../../utils/calculateCollectionHeight"; export default function CollectionListView(): ReactElement { @@ -60,6 +62,8 @@ export default function CollectionListView(): ReactElement setShowHidden(newShowHidden); }, []); + // FIXME: disable drag and drop if filters are active!!! + const handleDragStart = (event: DragStartEvent): void => { setActive(event.active.data.current as DndItem); @@ -115,7 +119,55 @@ export default function CollectionListView(): ReactElement : -
+ + index.toString()) } + strategy={ tilesView ? verticalListSortingStrategy : rectSortingStrategy } + > + + calculateCollectionHeight(item, tilesView, compactView ?? true) } + horizontalOffset={ 32 } + columnMinWidth={ !tilesView ? 360 : undefined } + items={ resultList } + containerSelector="article" + itemRenderer={ (item, index) => + + } /> + + + + { active !== null ? + active.item.type === "collection" ? + + : + + { active.item.type === "group" ? + + : + + } + + : + <> + } + + + + /*
-
+
*/ } ); diff --git a/entrypoints/sidepanel/utils/calculateCollectionHeight.tsx b/entrypoints/sidepanel/utils/calculateCollectionHeight.tsx new file mode 100644 index 0000000..b30b446 --- /dev/null +++ b/entrypoints/sidepanel/utils/calculateCollectionHeight.tsx @@ -0,0 +1,37 @@ +import { CollectionItem, GroupItem } from "@/models/CollectionModels"; + +export default function calculateCollectionHeight(collection: CollectionItem, tilesView: boolean, compactView: boolean): number +{ + if (compactView) + return collection.color ? 69.2 : 67.6; + + if (collection.items.length < 1) + return collection.color ? 217.6 : 219.2; + + if (tilesView) + { + let height = 201.6; + + if (collection.items.some(i => i.type === "group")) + height = 242.4; + + if (collection.color) + height += 1.6; + + return height; + } + + else + { + let baseHeight: number = collection.color ? 81.2 : 79.6; + + baseHeight += 39.6 * collection.items.flatMap(i => i.type === "group" ? i.items : [i]).length - 6; + + const groups: GroupItem[] = collection.items.filter(i => i.type === "group"); + + for (const group of groups) + baseHeight += group.items.length < 1 ? 126 : 50; + + return Math.min(baseHeight, 572); + } +}