masonic
Version:
<hr> <div align="center"> <h1 align="center"> 🧱 masonic </h1>
1 lines • 106 kB
Source Map (JSON)
{"version":3,"file":"index.dev.mjs","sources":["../../src/interval-tree.ts","../../src/elements-cache.ts","../../src/use-force-update.ts","../../src/use-masonry.tsx","../../src/use-scroller.ts","../../src/masonry-scroller.tsx","../../src/use-container-position.ts","../../src/use-positioner.ts","../../src/use-resize-observer.ts","../../src/use-scroll-to-index.ts","../../src/masonry.tsx","../../src/list.tsx","../../src/use-infinite-loader.ts"],"sourcesContent":["type Color = 0 | 1 | 2;\nconst RED = 0;\nconst BLACK = 1;\nconst NIL = 2;\n\nconst DELETE = 0;\nconst KEEP = 1;\n\ntype ListNode = {\n index: number;\n high: number;\n next: ListNode | null;\n};\n\ninterface TreeNode {\n max: number;\n low: number;\n high: number;\n // color\n C: Color;\n // P\n P: TreeNode;\n // right\n R: TreeNode;\n // left\n L: TreeNode;\n list: ListNode;\n}\n\ninterface Tree {\n root: TreeNode;\n size: number;\n}\n\nfunction addInterval(treeNode: TreeNode, high: number, index: number): boolean {\n let node: ListNode | null = treeNode.list;\n let prevNode: ListNode | undefined;\n\n while (node) {\n if (node.index === index) return false;\n if (high > node.high) break;\n prevNode = node;\n node = node.next;\n }\n\n if (!prevNode) treeNode.list = { index, high, next: node };\n if (prevNode) prevNode.next = { index, high, next: prevNode.next };\n\n return true;\n}\n\nfunction removeInterval(treeNode: TreeNode, index: number) {\n let node: ListNode | null = treeNode.list;\n if (node.index === index) {\n if (node.next === null) return DELETE;\n treeNode.list = node.next;\n return KEEP;\n }\n\n let prevNode: ListNode | undefined = node;\n node = node.next;\n\n while (node !== null) {\n if (node.index === index) {\n prevNode.next = node.next;\n return KEEP;\n }\n prevNode = node;\n node = node.next;\n }\n}\n\nconst NULL_NODE: TreeNode = {\n low: 0,\n max: 0,\n high: 0,\n C: NIL,\n // @ts-expect-error\n P: undefined,\n // @ts-expect-error\n R: undefined,\n // @ts-expect-error\n L: undefined,\n // @ts-expect-error\n list: undefined,\n};\n\nNULL_NODE.P = NULL_NODE;\nNULL_NODE.L = NULL_NODE;\nNULL_NODE.R = NULL_NODE;\n\nfunction updateMax(node: TreeNode) {\n const max = node.high;\n if (node.L === NULL_NODE && node.R === NULL_NODE) node.max = max;\n else if (node.L === NULL_NODE) node.max = Math.max(node.R.max, max);\n else if (node.R === NULL_NODE) node.max = Math.max(node.L.max, max);\n else node.max = Math.max(Math.max(node.L.max, node.R.max), max);\n}\n\nfunction updateMaxUp(node: TreeNode) {\n let x = node;\n\n while (x.P !== NULL_NODE) {\n updateMax(x.P);\n x = x.P;\n }\n}\n\nfunction rotateLeft(tree: Tree, x: TreeNode) {\n if (x.R === NULL_NODE) return;\n const y = x.R;\n x.R = y.L;\n if (y.L !== NULL_NODE) y.L.P = x;\n y.P = x.P;\n\n if (x.P === NULL_NODE) tree.root = y;\n else if (x === x.P.L) x.P.L = y;\n else x.P.R = y;\n\n y.L = x;\n x.P = y;\n\n updateMax(x);\n updateMax(y);\n}\n\nfunction rotateRight(tree: Tree, x: TreeNode) {\n if (x.L === NULL_NODE) return;\n const y = x.L;\n x.L = y.R;\n if (y.R !== NULL_NODE) y.R.P = x;\n y.P = x.P;\n\n if (x.P === NULL_NODE) tree.root = y;\n else if (x === x.P.R) x.P.R = y;\n else x.P.L = y;\n\n y.R = x;\n x.P = y;\n\n updateMax(x);\n updateMax(y);\n}\n\nfunction replaceNode(tree: Tree, x: TreeNode, y: TreeNode) {\n if (x.P === NULL_NODE) tree.root = y;\n else if (x === x.P.L) x.P.L = y;\n else x.P.R = y;\n y.P = x.P;\n}\n\nfunction fixRemove(tree: Tree, x: TreeNode) {\n let w;\n\n while (x !== NULL_NODE && x.C === BLACK) {\n if (x === x.P.L) {\n w = x.P.R;\n\n if (w.C === RED) {\n w.C = BLACK;\n x.P.C = RED;\n rotateLeft(tree, x.P);\n w = x.P.R;\n }\n\n if (w.L.C === BLACK && w.R.C === BLACK) {\n w.C = RED;\n x = x.P;\n } else {\n if (w.R.C === BLACK) {\n w.L.C = BLACK;\n w.C = RED;\n rotateRight(tree, w);\n w = x.P.R;\n }\n\n w.C = x.P.C;\n x.P.C = BLACK;\n w.R.C = BLACK;\n rotateLeft(tree, x.P);\n x = tree.root;\n }\n } else {\n w = x.P.L;\n\n if (w.C === RED) {\n w.C = BLACK;\n x.P.C = RED;\n rotateRight(tree, x.P);\n w = x.P.L;\n }\n\n if (w.R.C === BLACK && w.L.C === BLACK) {\n w.C = RED;\n x = x.P;\n } else {\n if (w.L.C === BLACK) {\n w.R.C = BLACK;\n w.C = RED;\n rotateLeft(tree, w);\n w = x.P.L;\n }\n\n w.C = x.P.C;\n x.P.C = BLACK;\n w.L.C = BLACK;\n rotateRight(tree, x.P);\n x = tree.root;\n }\n }\n }\n\n x.C = BLACK;\n}\n\nfunction minimumTree(x: TreeNode) {\n while (x.L !== NULL_NODE) x = x.L;\n return x;\n}\n\nfunction fixInsert(tree: Tree, z: TreeNode) {\n let y: TreeNode;\n while (z.P.C === RED) {\n if (z.P === z.P.P.L) {\n y = z.P.P.R;\n\n if (y.C === RED) {\n z.P.C = BLACK;\n y.C = BLACK;\n z.P.P.C = RED;\n z = z.P.P;\n } else {\n if (z === z.P.R) {\n z = z.P;\n rotateLeft(tree, z);\n }\n\n z.P.C = BLACK;\n z.P.P.C = RED;\n rotateRight(tree, z.P.P);\n }\n } else {\n y = z.P.P.L;\n\n if (y.C === RED) {\n z.P.C = BLACK;\n y.C = BLACK;\n z.P.P.C = RED;\n z = z.P.P;\n } else {\n if (z === z.P.L) {\n z = z.P;\n rotateRight(tree, z);\n }\n\n z.P.C = BLACK;\n z.P.P.C = RED;\n rotateLeft(tree, z.P.P);\n }\n }\n }\n tree.root.C = BLACK;\n}\n\nexport interface IIntervalTree {\n insert(low: number, high: number, index: number): void;\n remove(index: number): void;\n search(\n low: number,\n high: number,\n callback: (index: number, low: number) => any\n ): void;\n size: number;\n}\n\nexport function createIntervalTree(): IIntervalTree {\n const tree = {\n root: NULL_NODE,\n size: 0,\n };\n // we know these indexes are a consistent, safe way to make look ups\n // for our case so it's a solid O(1) alternative to\n // the O(log n) searchNode() in typical interval trees\n const indexMap: Record<number, TreeNode> = {};\n\n return {\n insert(low, high, index) {\n let x: TreeNode = tree.root;\n let y: TreeNode = NULL_NODE;\n\n while (x !== NULL_NODE) {\n y = x;\n if (low === y.low) break;\n if (low < x.low) x = x.L;\n else x = x.R;\n }\n\n if (low === y.low && y !== NULL_NODE) {\n if (!addInterval(y, high, index)) return;\n y.high = Math.max(y.high, high);\n updateMax(y);\n updateMaxUp(y);\n indexMap[index] = y;\n tree.size++;\n return;\n }\n\n const z: TreeNode = {\n low,\n high,\n max: high,\n C: RED,\n P: y,\n L: NULL_NODE,\n R: NULL_NODE,\n list: { index, high, next: null },\n };\n\n if (y === NULL_NODE) {\n tree.root = z;\n } else {\n if (z.low < y.low) y.L = z;\n else y.R = z;\n updateMaxUp(z);\n }\n\n fixInsert(tree, z);\n indexMap[index] = z;\n tree.size++;\n },\n\n remove(index) {\n const z = indexMap[index];\n if (z === void 0) return;\n delete indexMap[index];\n\n const intervalResult = removeInterval(z, index);\n if (intervalResult === void 0) return;\n if (intervalResult === KEEP) {\n z.high = z.list.high;\n updateMax(z);\n updateMaxUp(z);\n tree.size--;\n return;\n }\n\n let y = z;\n let originalYColor = y.C;\n let x: TreeNode;\n\n if (z.L === NULL_NODE) {\n x = z.R;\n replaceNode(tree, z, z.R);\n } else if (z.R === NULL_NODE) {\n x = z.L;\n replaceNode(tree, z, z.L);\n } else {\n y = minimumTree(z.R);\n originalYColor = y.C;\n x = y.R;\n\n if (y.P === z) {\n x.P = y;\n } else {\n replaceNode(tree, y, y.R);\n y.R = z.R;\n y.R.P = y;\n }\n\n replaceNode(tree, z, y);\n y.L = z.L;\n y.L.P = y;\n y.C = z.C;\n }\n\n updateMax(x);\n updateMaxUp(x);\n\n if (originalYColor === BLACK) fixRemove(tree, x);\n tree.size--;\n },\n\n search(low, high, callback) {\n const stack = [tree.root];\n while (stack.length !== 0) {\n const node = stack.pop() as TreeNode;\n if (node === NULL_NODE || low > node.max) continue;\n if (node.L !== NULL_NODE) stack.push(node.L);\n if (node.R !== NULL_NODE) stack.push(node.R);\n if (node.low <= high && node.high >= low) {\n let curr: ListNode | null = node.list;\n while (curr !== null) {\n if (curr.high >= low) callback(curr.index, node.low);\n curr = curr.next;\n }\n }\n }\n },\n\n get size() {\n return tree.size;\n },\n };\n}\n","export const elementsCache: WeakMap<Element, number> = new WeakMap();\n","import * as React from \"react\";\n\nexport function useForceUpdate() {\n const setState = React.useState(emptyObj)[1];\n return React.useRef(() => setState({})).current;\n}\n\nconst emptyObj = {};\n","import memoizeOne from \"@essentials/memoize-one\";\nimport OneKeyMap from \"@essentials/one-key-map\";\nimport useLatest from \"@react-hook/latest\";\nimport * as React from \"react\";\nimport trieMemoize from \"trie-memoize\";\nimport { elementsCache } from \"./elements-cache\";\nimport { useForceUpdate } from \"./use-force-update\";\nimport type { Positioner } from \"./use-positioner\";\n\n/**\n * This hook handles the render phases of the masonry layout and returns the grid as a React element.\n *\n * @param options - Options for configuring the masonry layout renderer. See `UseMasonryOptions`.\n * @param options.positioner\n * @param options.resizeObserver\n * @param options.items\n * @param options.as\n * @param options.id\n * @param options.className\n * @param options.style\n * @param options.role\n * @param options.tabIndex\n * @param options.containerRef\n * @param options.itemAs\n * @param options.itemStyle\n * @param options.itemHeightEstimate\n * @param options.itemKey\n * @param options.overscanBy\n * @param options.scrollTop\n * @param options.isScrolling\n * @param options.height\n * @param options.render\n * @param options.onRender\n */\nexport function useMasonry<Item>({\n // Measurement and layout\n positioner,\n resizeObserver,\n // Grid items\n items,\n // Container props\n as: ContainerComponent = \"div\",\n id,\n className,\n style,\n role = \"grid\",\n tabIndex = 0,\n containerRef,\n // Item props\n itemAs: ItemComponent = \"div\",\n itemStyle,\n itemHeightEstimate = 300,\n itemKey = defaultGetItemKey,\n // Rendering props\n overscanBy = 2,\n scrollTop,\n isScrolling,\n height,\n render: RenderComponent,\n onRender,\n}: UseMasonryOptions<Item>) {\n let startIndex = 0;\n let stopIndex: number | undefined;\n const forceUpdate = useForceUpdate();\n const setItemRef = getRefSetter(positioner, resizeObserver);\n const itemCount = items.length;\n const {\n columnWidth,\n columnCount,\n range,\n estimateHeight,\n size,\n shortestColumn,\n } = positioner;\n const measuredCount = size();\n const shortestColumnSize = shortestColumn();\n const children: React.ReactElement[] = [];\n const itemRole =\n role === \"list\" ? \"listitem\" : role === \"grid\" ? \"gridcell\" : undefined;\n const storedOnRender = useLatest(onRender);\n\n overscanBy = height * overscanBy;\n const rangeEnd = scrollTop + overscanBy;\n const needsFreshBatch =\n shortestColumnSize < rangeEnd && measuredCount < itemCount;\n\n range(\n // We overscan in both directions because users scroll both ways,\n // though one must admit scrolling down is more common and thus\n // we only overscan by half the downward overscan amount\n Math.max(0, scrollTop - overscanBy / 2),\n rangeEnd,\n (index, left, top) => {\n const data = items[index];\n const key = itemKey(data, index);\n const phaseTwoStyle: React.CSSProperties = {\n top,\n left,\n width: columnWidth,\n writingMode: \"horizontal-tb\",\n position: \"absolute\",\n };\n\n /* istanbul ignore next */\n if (\n typeof process !== \"undefined\" &&\n process.env.NODE_ENV !== \"production\"\n ) {\n throwWithoutData(data, index);\n }\n\n children.push(\n <ItemComponent\n key={key}\n ref={setItemRef(index)}\n role={itemRole}\n style={\n typeof itemStyle === \"object\" && itemStyle !== null\n ? Object.assign({}, phaseTwoStyle, itemStyle)\n : phaseTwoStyle\n }\n >\n {createRenderElement(RenderComponent, index, data, columnWidth)}\n </ItemComponent>\n );\n\n if (stopIndex === void 0) {\n startIndex = index;\n stopIndex = index;\n } else {\n startIndex = Math.min(startIndex, index);\n stopIndex = Math.max(stopIndex, index);\n }\n }\n );\n\n if (needsFreshBatch) {\n const batchSize = Math.min(\n itemCount - measuredCount,\n Math.ceil(\n ((scrollTop + overscanBy - shortestColumnSize) / itemHeightEstimate) *\n columnCount\n )\n );\n\n let index = measuredCount;\n const phaseOneStyle = getCachedSize(columnWidth);\n\n for (; index < measuredCount + batchSize; index++) {\n const data = items[index];\n const key = itemKey(data, index);\n\n /* istanbul ignore next */\n if (\n typeof process !== \"undefined\" &&\n process.env.NODE_ENV !== \"production\"\n ) {\n throwWithoutData(data, index);\n }\n\n children.push(\n <ItemComponent\n key={key}\n ref={setItemRef(index)}\n role={itemRole}\n style={\n typeof itemStyle === \"object\"\n ? Object.assign({}, phaseOneStyle, itemStyle)\n : phaseOneStyle\n }\n >\n {createRenderElement(RenderComponent, index, data, columnWidth)}\n </ItemComponent>\n );\n }\n }\n\n // Calls the onRender callback if the rendered indices changed\n React.useEffect(() => {\n if (typeof storedOnRender.current === \"function\" && stopIndex !== void 0)\n storedOnRender.current(startIndex, stopIndex, items);\n\n didEverMount = \"1\";\n }, [startIndex, stopIndex, items, storedOnRender]);\n // If we needed a fresh batch we should reload our components with the measured\n // sizes\n React.useEffect(() => {\n if (needsFreshBatch) forceUpdate();\n // eslint-disable-next-line\n }, [needsFreshBatch, positioner]);\n\n // gets the container style object based upon the estimated height and whether or not\n // the page is being scrolled\n const containerStyle = getContainerStyle(\n isScrolling,\n estimateHeight(itemCount, itemHeightEstimate)\n );\n\n return (\n <ContainerComponent\n ref={containerRef}\n key={didEverMount}\n id={id}\n role={role}\n className={className}\n tabIndex={tabIndex}\n style={\n typeof style === \"object\"\n ? assignUserStyle(containerStyle, style)\n : containerStyle\n }\n children={children}\n />\n );\n}\n\n/* istanbul ignore next */\nfunction throwWithoutData(data: any, index: number) {\n if (!data) {\n throw new Error(\n `No data was found at index: ${index}\\n\\n` +\n `This usually happens when you've mutated or changed the \"items\" array in a ` +\n `way that makes it shorter than the previous \"items\" array. Masonic knows nothing ` +\n `about your underlying data and when it caches cell positions, it assumes you aren't ` +\n `mutating the underlying \"items\".\\n\\n` +\n `See https://codesandbox.io/s/masonic-w-react-router-example-2b5f9?file=/src/index.js for ` +\n `an example that gets around this limitations. For advanced implementations, see ` +\n `https://codesandbox.io/s/masonic-w-react-router-and-advanced-config-example-8em42?file=/src/index.js\\n\\n` +\n `If this was the result of your removing an item from your \"items\", see this issue: ` +\n `https://github.com/jaredLunde/masonic/issues/12`\n );\n }\n}\n\n// This is for triggering a remount after SSR has loaded in the client w/ hydrate()\nlet didEverMount = \"0\";\n\nexport interface UseMasonryOptions<Item> {\n /**\n * An array containing the data used by the grid items.\n */\n items: Item[];\n /**\n * A grid cell positioner and cache created by the `usePositioner()` hook or\n * the `createPositioner` utility.\n */\n positioner: Positioner;\n /**\n * A resize observer that tracks mutations to the grid cells and forces the\n * Masonry grid to recalculate its layout if any cells affect column heights\n * change. Check out the `useResizeObserver()` hook.\n */\n resizeObserver?: {\n observe: ResizeObserver[\"observe\"];\n disconnect: ResizeObserver[\"observe\"];\n unobserve: ResizeObserver[\"unobserve\"];\n };\n /**\n * This is the type of element the grid container will be rendered as.\n *\n * @default \"div\"`\n */\n as?: keyof JSX.IntrinsicElements | React.ComponentType<any>;\n /**\n * Optionally gives the grid container an `id` prop.\n */\n id?: string;\n /**\n * Optionally gives the grid container a `className` prop.\n */\n className?: string;\n /**\n * Adds extra `style` attributes to the container in addition to those\n * created by the `useMasonry()` hook.\n */\n style?: React.CSSProperties;\n /**\n * Optionally swap out the accessibility `role` prop of the container and its items.\n *\n * @default \"grid\"\n */\n role?: \"grid\" | \"list\";\n /**\n * Change the `tabIndex` of the grid container.\n *\n * @default 0\n */\n tabIndex?: number;\n /**\n * Forwards a React ref to the grid container.\n */\n containerRef?:\n | ((element: HTMLElement) => void)\n | React.MutableRefObject<HTMLElement | null>;\n /**\n * This is the type of element the grid items will be rendered as.\n *\n * @default \"div\"\n */\n itemAs?: keyof JSX.IntrinsicElements | React.ComponentType<any>;\n /**\n * Adds extra `style` attributes to the grid items in addition to those\n * created by the `useMasonry()` hook.\n */\n itemStyle?: React.CSSProperties;\n /**\n * This value is used for estimating the initial height of the masonry grid. It is important for\n * the UX of the scrolling behavior and in determining how many `items` to render in a batch, so it's\n * wise to set this value with some level accuracy, though it doesn't need to be perfect.\n *\n * @default 300\n */\n itemHeightEstimate?: number;\n /**\n * The value returned here must be unique to the item. By default, the key is the item's index. This is ok\n * if your collection of items is never modified. Setting this property ensures that the component in `render`\n * is reused each time the masonry grid is reflowed. A common pattern would be to return the item's database\n * ID here if there is one, e.g. `data => data.id`\n *\n * @default (data, index) => index`\n */\n itemKey?: (data: Item, index: number) => string | number;\n /**\n * This number is used for determining the number of grid cells outside of the visible window to render.\n * The default value is `2` which means \"render 2 windows worth (2 * `height`) of content before and after\n * the items in the visible window\". A value of `3` would be 3 windows worth of grid cells, so it's a\n * linear relationship.\n *\n * Overscanning is important for preventing tearing when scrolling through items in the grid, but setting\n * too high of a vaimport { useForceUpdate } from './use-force-update';\nlue may create too much work for React to handle, so it's best that you tune this\n * value accordingly.\n *\n * @default 2\n */\n overscanBy?: number;\n\n /**\n * This is the height of the window. If you're rendering the grid relative to the browser `window`,\n * the current `document.documentElement.clientHeight` is the value you'll want to set here. If you're\n * rendering the grid inside of another HTML element, you'll want to provide the current `element.offsetHeight`\n * here.\n */\n height: number;\n /**\n * The current scroll progress in pixel of the window the grid is rendered in. If you're rendering\n * the grid relative to the browser `window`, you'll want the most current `window.scrollY` here.\n * If you're rendering the grid inside of another HTML element, you'll want the current `element.scrollTop`\n * value here. The `useScroller()` hook and `<MasonryScroller>` components will help you if you're\n * rendering the grid relative to the browser `window`.\n */\n scrollTop: number;\n /**\n * This property is used for determining whether or not the grid container should add styles that\n * dramatically increase scroll performance. That is, turning off `pointer-events` and adding a\n * `will-change: contents;` value to the style string. You can forgo using this prop, but I would\n * not recommend that. The `useScroller()` hook and `<MasonryScroller>` components will help you if\n * you're rendering the grid relative to the browser `window`.\n *\n * @default false\n */\n isScrolling?: boolean;\n /**\n * This component is rendered for each item of your `items` prop array. It should accept three props:\n * `index`, `width`, and `data`. See RenderComponentProps.\n */\n render: React.ComponentType<RenderComponentProps<Item>>;\n /**\n * This callback is invoked any time the items currently being rendered by the grid change.\n */\n onRender?: (startIndex: number, stopIndex: number, items: Item[]) => void;\n}\n\nexport interface RenderComponentProps<Item> {\n /**\n * The index of the cell in the `items` prop array.\n */\n index: number;\n /**\n * The rendered width of the cell's column.\n */\n width: number;\n /**\n * The data at `items[index]` of your `items` prop array.\n */\n data: Item;\n}\n\n//\n// Render-phase utilities\n\n// ~5.5x faster than createElement without the memo\nconst createRenderElement = trieMemoize(\n [OneKeyMap, {}, WeakMap, OneKeyMap],\n (RenderComponent, index, data, columnWidth) => (\n <RenderComponent index={index} data={data} width={columnWidth} />\n )\n);\n\nconst getContainerStyle = memoizeOne(\n (isScrolling: boolean | undefined, estimateHeight: number) => ({\n position: \"relative\",\n width: \"100%\",\n maxWidth: \"100%\",\n height: Math.ceil(estimateHeight),\n maxHeight: Math.ceil(estimateHeight),\n willChange: isScrolling ? \"contents\" : void 0,\n pointerEvents: isScrolling ? \"none\" : void 0,\n })\n);\n\nconst cmp2 = (args: IArguments, pargs: IArguments | any[]): boolean =>\n args[0] === pargs[0] && args[1] === pargs[1];\n\nconst assignUserStyle = memoizeOne(\n (containerStyle, userStyle) => Object.assign({}, containerStyle, userStyle),\n // @ts-expect-error\n cmp2\n);\n\nfunction defaultGetItemKey<Item>(_: Item, i: number) {\n return i;\n}\n\n// the below memoizations for for ensuring shallow equal is reliable for pure\n// component children\nconst getCachedSize = memoizeOne(\n (width: number): React.CSSProperties => ({\n width,\n zIndex: -1000,\n visibility: \"hidden\",\n position: \"absolute\",\n writingMode: \"horizontal-tb\",\n }),\n (args, pargs) => args[0] === pargs[0]\n);\n\nconst getRefSetter = memoizeOne(\n (\n positioner: Positioner,\n resizeObserver?: UseMasonryOptions<any>[\"resizeObserver\"]\n ) =>\n (index: number) =>\n (el: HTMLElement | null): void => {\n if (el === null) return;\n if (resizeObserver) {\n resizeObserver.observe(el);\n elementsCache.set(el, index);\n }\n if (positioner.get(index) === void 0)\n positioner.set(index, el.offsetHeight);\n },\n // @ts-expect-error\n cmp2\n);\n","import {\n clearRequestTimeout,\n requestTimeout,\n} from \"@essentials/request-timeout\";\nimport useScrollPosition from \"@react-hook/window-scroll\";\nimport * as React from \"react\";\n\n/**\n * A hook for tracking whether the `window` is currently being scrolled and it's scroll position on\n * the y-axis. These values are used for determining which grid cells to render and when\n * to add styles to the masonry container that maximize scroll performance.\n *\n * @param offset - The vertical space in pixels between the top of the grid container and the top\n * of the browser `document.documentElement`.\n * @param fps - This determines how often (in frames per second) to update the scroll position of the\n * browser `window` in state, and as a result the rate the masonry grid recalculates its visible cells.\n * The default value of `12` has been very reasonable in my own testing, but if you have particularly\n * heavy `render` components it may be prudent to reduce this number.\n */\nexport function useScroller(\n offset = 0,\n fps = 12\n): { scrollTop: number; isScrolling: boolean } {\n const scrollTop = useScrollPosition(fps);\n const [isScrolling, setIsScrolling] = React.useState(false);\n const didMount = React.useRef(0);\n\n React.useEffect(() => {\n if (didMount.current === 1) setIsScrolling(true);\n let didUnsubscribe = false;\n const to = requestTimeout(() => {\n if (didUnsubscribe) return;\n // This is here to prevent premature bail outs while maintaining high resolution\n // unsets. Without it there will always bee a lot of unnecessary DOM writes to style.\n setIsScrolling(false);\n }, 40 + 1000 / fps);\n didMount.current = 1;\n return () => {\n didUnsubscribe = true;\n clearRequestTimeout(to);\n };\n }, [fps, scrollTop]);\n\n return { scrollTop: Math.max(0, scrollTop - offset), isScrolling };\n}\n","import { useMasonry } from \"./use-masonry\";\nimport type { UseMasonryOptions } from \"./use-masonry\";\nimport { useScroller } from \"./use-scroller\";\n/**\n * A heavily-optimized component that updates `useMasonry()` when the scroll position of the browser `window`\n * changes. This bare-metal component is used by `<Masonry>` under the hood.\n *\n * @param props\n */\nexport function MasonryScroller<Item>(props: MasonryScrollerProps<Item>) {\n // We put this in its own layer because it's the thing that will trigger the most updates\n // and we don't want to slower ourselves by cycling through all the functions, objects, and effects\n // of other hooks\n const { scrollTop, isScrolling } = useScroller(props.offset, props.scrollFps);\n // This is an update-heavy phase and while we could just Object.assign here,\n // it is way faster to inline and there's a relatively low hit to he bundle\n // size.\n return useMasonry<Item>({\n scrollTop,\n isScrolling,\n positioner: props.positioner,\n resizeObserver: props.resizeObserver,\n items: props.items,\n onRender: props.onRender,\n as: props.as,\n id: props.id,\n className: props.className,\n style: props.style,\n role: props.role,\n tabIndex: props.tabIndex,\n containerRef: props.containerRef,\n itemAs: props.itemAs,\n itemStyle: props.itemStyle,\n itemHeightEstimate: props.itemHeightEstimate,\n itemKey: props.itemKey,\n overscanBy: props.overscanBy,\n height: props.height,\n render: props.render,\n });\n}\n\nexport interface MasonryScrollerProps<Item>\n extends Omit<UseMasonryOptions<Item>, \"scrollTop\" | \"isScrolling\"> {\n /**\n * This determines how often (in frames per second) to update the scroll position of the\n * browser `window` in state, and as a result the rate the masonry grid recalculates its visible cells.\n * The default value of `12` has been very reasonable in my own testing, but if you have particularly\n * heavy `render` components it may be prudent to reduce this number.\n *\n * @default 12\n */\n scrollFps?: number;\n /**\n * The vertical space in pixels between the top of the grid container and the top\n * of the browser `document.documentElement`.\n *\n * @default 0\n */\n offset?: number;\n}\n\nif (typeof process !== \"undefined\" && process.env.NODE_ENV !== \"production\") {\n MasonryScroller.displayName = \"MasonryScroller\";\n}\n","import useLayoutEffect from \"@react-hook/passive-layout-effect\";\nimport * as React from \"react\";\n\n/**\n * A hook for measuring the width of the grid container, as well as its distance\n * from the top of the document. These values are necessary to correctly calculate the number/width\n * of columns to render, as well as the number of rows to render.\n *\n * @param elementRef - A `ref` object created by `React.useRef()`. That ref should be provided to the\n * `containerRef` property in `useMasonry()`.\n * @param deps - You can force this hook to recalculate the `offset` and `width` whenever this\n * dependencies list changes. A common dependencies list might look like `[windowWidth, windowHeight]`,\n * which would force the hook to recalculate any time the size of the browser `window` changed.\n */\nexport function useContainerPosition(\n elementRef: React.MutableRefObject<HTMLElement | null>,\n deps: React.DependencyList = emptyArr\n): ContainerPosition {\n const [containerPosition, setContainerPosition] =\n React.useState<ContainerPosition>({ offset: 0, width: 0 });\n\n useLayoutEffect(() => {\n const { current } = elementRef;\n if (current !== null) {\n let offset = 0;\n let el = current;\n\n do {\n offset += el.offsetTop || 0;\n el = el.offsetParent as HTMLElement;\n } while (el);\n\n if (\n offset !== containerPosition.offset ||\n current.offsetWidth !== containerPosition.width\n ) {\n setContainerPosition({\n offset,\n width: current.offsetWidth,\n });\n }\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n\n return containerPosition;\n}\n\nexport interface ContainerPosition {\n /**\n * The distance in pixels between the top of the element in `elementRef` and the top of\n * the `document.documentElement`.\n */\n offset: number;\n /**\n * The `offsetWidth` of the element in `elementRef`.\n */\n width: number;\n}\n\nconst emptyArr: [] = [];\n","import * as React from \"react\";\nimport { createIntervalTree } from \"./interval-tree\";\n\n/**\n * This hook creates the grid cell positioner and cache required by `useMasonry()`. This is\n * the meat of the grid's layout algorithm, determining which cells to render at a given scroll\n * position, as well as where to place new items in the grid.\n *\n * @param options - Properties that determine the number of columns in the grid, as well\n * as their widths.\n * @param options.columnWidth\n * @param options.width\n * @param deps - This hook will create a new positioner, clearing all existing cached positions,\n * whenever the dependencies in this list change.\n * @param options.columnGutter\n * @param options.rowGutter\n * @param options.columnCount\n * @param options.maxColumnCount\n * @param options.maxColumnWidth\n */\nexport function usePositioner(\n {\n width,\n columnWidth = 200,\n columnGutter = 0,\n rowGutter,\n columnCount,\n maxColumnCount,\n maxColumnWidth,\n }: UsePositionerOptions,\n deps: React.DependencyList = emptyArr\n): Positioner {\n const initPositioner = (): Positioner => {\n const [computedColumnWidth, computedColumnCount] = getColumns(\n width,\n columnWidth,\n columnGutter,\n columnCount,\n maxColumnCount,\n maxColumnWidth\n );\n return createPositioner(\n computedColumnCount,\n computedColumnWidth,\n columnGutter,\n rowGutter ?? columnGutter\n );\n };\n const positionerRef = React.useRef<Positioner>();\n if (positionerRef.current === undefined)\n positionerRef.current = initPositioner();\n\n const prevDeps = React.useRef(deps);\n const opts = [\n width,\n columnWidth,\n columnGutter,\n rowGutter,\n columnCount,\n maxColumnCount,\n maxColumnWidth,\n ];\n const prevOpts = React.useRef(opts);\n const optsChanged = !opts.every((item, i) => prevOpts.current[i] === item);\n\n if (typeof process !== \"undefined\" && process.env.NODE_ENV !== \"production\") {\n if (deps.length !== prevDeps.current.length) {\n throw new Error(\n \"usePositioner(): The length of your dependencies array changed.\"\n );\n }\n }\n\n // Create a new positioner when the dependencies or sizes change\n // Thanks to https://github.com/khmm12 for pointing this out\n // https://github.com/jaredLunde/masonic/pull/41\n if (optsChanged || !deps.every((item, i) => prevDeps.current[i] === item)) {\n const prevPositioner = positionerRef.current;\n const positioner = initPositioner();\n prevDeps.current = deps;\n prevOpts.current = opts;\n\n if (optsChanged) {\n const cacheSize = prevPositioner.size();\n for (let index = 0; index < cacheSize; index++) {\n const pos = prevPositioner.get(index);\n positioner.set(index, pos !== void 0 ? pos.height : 0);\n }\n }\n\n positionerRef.current = positioner;\n }\n\n return positionerRef.current;\n}\n\nexport interface UsePositionerOptions {\n /**\n * The width of the container you're rendering the grid within, i.e. the container\n * element's `element.offsetWidth`\n */\n width: number;\n /**\n * The minimum column width. The `usePositioner()` hook will automatically size the\n * columns to fill their container based upon the `columnWidth` and `columnGutter` values.\n * It will never render anything smaller than this width unless its container itself is\n * smaller than its value. This property is optional if you're using a static `columnCount`.\n *\n * @default 200\n */\n columnWidth?: number;\n /**\n * The maximum column width. Calculated column widths will be capped at this value.\n */\n maxColumnWidth?: number;\n /**\n * This sets the horizontal space between grid columns in pixels. If `rowGutter` is not set, this\n * also sets the vertical space between cells within a column in pixels.\n *\n * @default 0\n */\n columnGutter?: number;\n /**\n * This sets the vertical space between cells within a column in pixels. If not set, the value of\n * `columnGutter` is used instead.\n */\n rowGutter?: number;\n /**\n * By default, `usePositioner()` derives the column count from the `columnWidth`, `columnGutter`,\n * and `width` props. However, in some situations it is nice to be able to override that behavior\n * (e.g. creating a `List` component).\n */\n columnCount?: number;\n /**\n * The upper bound of column count. This property won't work if `columnCount` is set.\n */\n maxColumnCount?: number;\n}\n\n/**\n * Creates a cell positioner for the `useMasonry()` hook. The `usePositioner()` hook uses\n * this utility under the hood.\n *\n * @param columnCount - The number of columns in the grid\n * @param columnWidth - The width of each column in the grid\n * @param columnGutter - The amount of horizontal space between columns in pixels.\n * @param rowGutter - The amount of vertical space between cells within a column in pixels (falls back\n * to `columnGutter`).\n */\nexport const createPositioner = (\n columnCount: number,\n columnWidth: number,\n columnGutter = 0,\n rowGutter = columnGutter\n): Positioner => {\n // O(log(n)) lookup of cells to render for a given viewport size\n // Store tops and bottoms of each cell for fast intersection lookup.\n const intervalTree = createIntervalTree();\n // Track the height of each column.\n // Layout algorithm below always inserts into the shortest column.\n const columnHeights: number[] = new Array(columnCount);\n // Used for O(1) item access\n const items: PositionerItem[] = [];\n // Tracks the item indexes within an individual column\n const columnItems: number[][] = new Array(columnCount);\n\n for (let i = 0; i < columnCount; i++) {\n columnHeights[i] = 0;\n columnItems[i] = [];\n }\n\n return {\n columnCount,\n columnWidth,\n set: (index, height = 0) => {\n let column = 0;\n\n // finds the shortest column and uses it\n for (let i = 1; i < columnHeights.length; i++) {\n if (columnHeights[i] < columnHeights[column]) column = i;\n }\n\n const top = columnHeights[column] || 0;\n columnHeights[column] = top + height + rowGutter;\n columnItems[column].push(index);\n items[index] = {\n left: column * (columnWidth + columnGutter),\n top,\n height,\n column,\n };\n intervalTree.insert(top, top + height, index);\n },\n get: (index) => items[index],\n // This only updates items in the specific columns that have changed, on and after the\n // specific items that have changed\n update: (updates) => {\n const columns: number[] = new Array(columnCount);\n let i = 0,\n j = 0;\n\n // determines which columns have items that changed, as well as the minimum index\n // changed in that column, as all items after that index will have their positions\n // affected by the change\n for (; i < updates.length - 1; i++) {\n const index = updates[i];\n const item = items[index];\n item.height = updates[++i];\n intervalTree.remove(index);\n intervalTree.insert(item.top, item.top + item.height, index);\n columns[item.column] =\n columns[item.column] === void 0\n ? index\n : Math.min(index, columns[item.column]);\n }\n\n for (i = 0; i < columns.length; i++) {\n // bails out if the column didn't change\n if (columns[i] === void 0) continue;\n const itemsInColumn = columnItems[i];\n // the index order is sorted with certainty so binary search is a great solution\n // here as opposed to Array.indexOf()\n const startIndex = binarySearch(itemsInColumn, columns[i]);\n const index = columnItems[i][startIndex];\n const startItem = items[index];\n columnHeights[i] = startItem.top + startItem.height + rowGutter;\n\n for (j = startIndex + 1; j < itemsInColumn.length; j++) {\n const index = itemsInColumn[j];\n const item = items[index];\n item.top = columnHeights[i];\n columnHeights[i] = item.top + item.height + rowGutter;\n intervalTree.remove(index);\n intervalTree.insert(item.top, item.top + item.height, index);\n }\n }\n },\n // Render all cells visible within the viewport range defined.\n range: (lo, hi, renderCallback) =>\n intervalTree.search(lo, hi, (index, top) =>\n renderCallback(index, items[index].left, top)\n ),\n estimateHeight: (itemCount, defaultItemHeight): number => {\n const tallestColumn = Math.max(0, Math.max.apply(null, columnHeights));\n\n return itemCount === intervalTree.size\n ? tallestColumn\n : tallestColumn +\n Math.ceil((itemCount - intervalTree.size) / columnCount) *\n defaultItemHeight;\n },\n shortestColumn: () => {\n if (columnHeights.length > 1) return Math.min.apply(null, columnHeights);\n return columnHeights[0] || 0;\n },\n size(): number {\n return intervalTree.size;\n },\n all(): PositionerItem[] {\n return items;\n },\n };\n};\n\nexport interface Positioner {\n /**\n * The number of columns in the grid\n */\n columnCount: number;\n /**\n * The width of each column in the grid\n */\n columnWidth: number;\n /**\n * Sets the position for the cell at `index` based upon the cell's height\n */\n set: (index: number, height: number) => void;\n /**\n * Gets the `PositionerItem` for the cell at `index`\n */\n get: (index: number) => PositionerItem | undefined;\n /**\n * Updates cells based on their indexes and heights\n * positioner.update([index, height, index, height, index, height...])\n */\n update: (updates: number[]) => void;\n /**\n * Searches the interval tree for grid cells with a `top` value in\n * betwen `lo` and `hi` and invokes the callback for each item that\n * is discovered\n */\n range: (\n lo: number,\n hi: number,\n renderCallback: (index: number, left: number, top: number) => void\n ) => void;\n /**\n * Returns the number of grid cells in the cache\n */\n\n size: () => number;\n /**\n * Estimates the total height of the grid\n */\n\n estimateHeight: (itemCount: number, defaultItemHeight: number) => number;\n /**\n * Returns the height of the shortest column in the grid\n */\n\n shortestColumn: () => number;\n /**\n * Returns all `PositionerItem` items\n */\n all: () => PositionerItem[];\n}\n\nexport interface PositionerItem {\n /**\n * This is how far from the top edge of the grid container in pixels the\n * item is placed\n */\n top: number;\n /**\n * This is how far from the left edge of the grid container in pixels the\n * item is placed\n */\n left: number;\n /**\n * This is the height of the grid cell\n */\n height: number;\n /**\n * This is the column number containing the grid cell\n */\n column: number;\n}\n\n/* istanbul ignore next */\nconst binarySearch = (a: number[], y: number): number => {\n let l = 0;\n let h = a.length - 1;\n\n while (l <= h) {\n const m = (l + h) >>> 1;\n const x = a[m];\n if (x === y) return m;\n else if (x <= y) l = m + 1;\n else h = m - 1;\n }\n\n return -1;\n};\n\nconst getColumns = (\n width = 0,\n minimumWidth = 0,\n gutter = 8,\n columnCount?: number,\n maxColumnCount?: number,\n maxColumnWidth?: number\n): [number, number] => {\n columnCount =\n columnCount ||\n Math.min(\n Math.floor((width + gutter) / (minimumWidth + gutter)),\n maxColumnCount || Infinity\n ) ||\n 1;\n let columnWidth = Math.floor(\n (width - gutter * (columnCount - 1)) / columnCount\n );\n\n // Cap the column width if maxColumnWidth is specified\n if (maxColumnWidth !== undefined && columnWidth > maxColumnWidth) {\n columnWidth = maxColumnWidth;\n }\n\n return [columnWidth, columnCount];\n};\n\nconst emptyArr: [] = [];\n","import rafSchd from \"raf-schd\";\nimport * as React from \"react\";\nimport trieMemoize from \"trie-memoize\";\nimport { elementsCache } from \"./elements-cache\";\nimport { useForceUpdate } from \"./use-force-update\";\nimport type { Positioner } from \"./use-positioner\";\n\n/**\n * Creates a resize observer that forces updates to the grid cell positions when mutations are\n * made to cells affecting their height.\n *\n * @param positioner - The masonry cell positioner created by the `usePositioner()` hook.\n */\nexport function useResizeObserver(positioner: Positioner) {\n const forceUpdate = useForceUpdate();\n const resizeObserver = createResizeObserver(positioner, forceUpdate);\n // Cleans up the resize observers when they change or the\n // component unmounts\n React.useEffect(() => () => resizeObserver.disconnect(), [resizeObserver]);\n return resizeObserver;\n}\n\nconst _handlerForType = rafSchd((target: HTMLElement) => {});\n\ntype IHandler = typeof _handlerForType;\n\n/**\n * Creates a resize observer that fires an `updater` callback whenever the height of\n * one or many cells change. The `useResizeObserver()` hook is using this under the hood.\n *\n * @param positioner - A cell positioner created by the `usePositioner()` hook or the `createPositioner()` utility\n * @param updater - A callback that fires whenever one or many cell heights change.\n */\nexport const createResizeObserver = trieMemoize(\n [WeakMap],\n // TODO: figure out a way to test this\n /* istanbul ignore next */\n (positioner: Positioner, updater: (updates: number[]) => void) => {\n const updates: number[] = [];\n\n const update = rafSchd(() => {\n if (updates.length > 0) {\n // Updates the size/positions of the cell with the resize\n // observer updates\n positioner.update(updates);\n updater(updates);\n }\n updates.length = 0;\n });\n\n const commonHandler = (target: HTMLElement) => {\n const height = target.offsetHeight;\n if (height > 0) {\n const index = elementsCache.get(target);\n if (index !== void 0) {\n const position = positioner.get(index);\n if (position !== void 0 && height !== position.height)\n updates.push(index, height);\n }\n }\n update();\n };\n\n const handlers = new Map<number, IHandler>();\n const handleEntries: ResizeObserverCallback = (entries) => {\n let i = 0;\n\n for (; i < entries.length; i++) {\n const entry = entries[i];\n const index = elementsCache.get(entry.target);\n\n if (index === void 0) continue;\n let handler = handlers.get(index);\n if (!handler) {\n handler = rafSchd(commonHandler);\n handlers.set(index, handler);\n }\n handler(entry.target as HTMLElement);\n }\n };\n\n const ro = new ResizeObserver(handleEntries);\n // Overrides the original disconnect to include cancelling handling the entries.\n // Ideally this would be its own method but that would result in a breaking\n // change.\n const disconnect = ro.disconnect.bind(ro);\n ro.disconnect = () => {\n disconnect();\n handlers.forEach((handler) => {\n handler.cancel();\n });\n };\n\n return ro;\n }\n);\n","import useEvent from \"@react-hook/event\";\nimport useLatest from \"@react-hook/latest\";\nimport { useThrottleCallback } from \"@react-hook/throttle\";\nimport * as React from \"react\";\nimport type { Positioner, PositionerItem } from \"./use-positioner\";\n\n/**\n * A hook that creates a callback for scrolling to a specific index in\n * the \"items\" array.\n *\n * @param positioner - A positioner created by the `usePositioner()` hook\n * @param options - Configuration options\n */\nexport function useScrollToIndex(\n positioner: Positioner,\n options: UseScrollToIndexOptions\n) {\n const {\n align = \"top\",\n element = typeof window !== \"undefined\" && window,\n offset = 0,\n height = typeof window !== \"undefined\" ? window.innerHeight : 0,\n } = options;\n const latestOptions = useLatest({\n positioner,\n element,\n align,\n offset,\n height,\n } as const);\n const getTarget = React.useRef(() => {\n const latestElement = latestOptions.current.element;\n return latestElement && \"current\" in latestElement\n ? latestElement.current\n : latestElement;\n }).current;\n const [state, dispatch] = React.useReducer(\n (\n state: {\n position: PositionerItem | undefined;\n index: number | undefined;\n prevTop: number | undefined;\n },\n action:\n | { type: \"scrollToIndex\"; value: number | undefined }\n | { type: \"setPosition\"; value: PositionerItem | undefined }\n | { type: \"setPrevTop\"; value: number | undefined }\n | { type: \"reset\" }\n ) => {\n const nextState = {\n position: state.position,\n index: state.index,\n prevTop: state.prevTop,\n };\n\n /* istanbul ignore next */\n if (action.type === \"scrollToIndex\") {\n return {\n position: latestOptions.current.positioner.get(action.value ?? -1),\n index: action.value,\n prevTop: void 0,\n };\n } else if (action.type === \"setPosition\") {\n nextState.position = action.value;\n } else if (action.type === \"setPrevTop\") {\n nextState.prevTop = action.value;\n } else if (action.type === \"reset\") {\n return defaultState;\n }\n\n return nextState;\n },\n defaultState\n );\n const throttledDispatch = useThrottleCallback(dispatch, 15);\n\n // If we find the position along the way we can immediately take off\n // to the correct spot.\n useEvent(getTarget() as Window, \"scroll\", () => {\n if (!state.position && state.index) {\n const position = latestOptions.current.positioner.get(state.index);\n\n if (position) {\n dispatch({ type: \"setPosition\", value: position });\n }\n }\n });\n\n // If the top changes out from under us in the case of dynamic cells, we\n // want to keep following it.\n const currentTop =\n state.index !== void 0 &&\n latestOptions.current.positioner.get(state.index)?.top;\n\n React.useEffect(() => {\n const target = getTarget();\n if (!target) return;\n const { height, align, offset, positioner } = latestOptions.current;\n\n if (state.position) {\n let scrollTop = state.position.top;\n\n if (align === \"bottom\") {\n scrollTop = scrollTop - height + state.position.height;\n } else if (align === \"center\") {\n scrollTop -= (height - state.position.height) / 2;\n }\n\n target.scrollTo(0, Math.max(0, (scrollTop += offset)));\n // Resets state after 400ms, an arbitrary time I determined to be\n // still visually pleasing if there is a slow network reply in dynamic\n // cells\n let didUnsubscribe = false;\n const timeout = setTimeout(\n () => !didUnsubscribe && dispat