UNPKG

masonic

Version:

<hr> <div align="center"> <h1 align="center"> 🧱 masonic </h1>

222 lines (200 loc) • 8.1 kB
import memoizeOne from "@essentials/memoize-one"; import OneKeyMap from "@essentials/one-key-map"; import useLatest from "@react-hook/latest"; import * as React from "react"; const __reactCreateElement__ = React.createElement; import trieMemoize from "trie-memoize"; import { elementsCache } from "./elements-cache"; import { useForceUpdate } from "./use-force-update"; /** * This hook handles the render phases of the masonry layout and returns the grid as a React element. * * @param options - Options for configuring the masonry layout renderer. See `UseMasonryOptions`. * @param options.positioner * @param options.resizeObserver * @param options.items * @param options.as * @param options.id * @param options.className * @param options.style * @param options.role * @param options.tabIndex * @param options.containerRef * @param options.itemAs * @param options.itemStyle * @param options.itemHeightEstimate * @param options.itemKey * @param options.overscanBy * @param options.scrollTop * @param options.isScrolling * @param options.height * @param options.render * @param options.onRender */ export function useMasonry(_ref) { let { // Measurement and layout positioner, resizeObserver, // Grid items items, // Container props as: ContainerComponent = "div", id, className, style, role = "grid", tabIndex = 0, containerRef, // Item props itemAs: ItemComponent = "div", itemStyle, itemHeightEstimate = 300, itemKey = defaultGetItemKey, // Rendering props overscanBy = 2, scrollTop, isScrolling, height, render: RenderComponent, onRender } = _ref; let startIndex = 0; let stopIndex; const forceUpdate = useForceUpdate(); const setItemRef = getRefSetter(positioner, resizeObserver); const itemCount = items.length; const { columnWidth, columnCount, range, estimateHeight, size, shortestColumn } = positioner; const measuredCount = size(); const shortestColumnSize = shortestColumn(); const children = []; const itemRole = role === "list" ? "listitem" : role === "grid" ? "gridcell" : undefined; const storedOnRender = useLatest(onRender); overscanBy = height * overscanBy; const rangeEnd = scrollTop + overscanBy; const needsFreshBatch = shortestColumnSize < rangeEnd && measuredCount < itemCount; range( // We overscan in both directions because users scroll both ways, // though one must admit scrolling down is more common and thus // we only overscan by half the downward overscan amount Math.max(0, scrollTop - overscanBy / 2), rangeEnd, (index, left, top) => { const data = items[index]; const key = itemKey(data, index); const phaseTwoStyle = { top, left, width: columnWidth, writingMode: "horizontal-tb", position: "absolute" }; /* istanbul ignore next */ if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") { throwWithoutData(data, index); } children.push( /*#__PURE__*/__reactCreateElement__(ItemComponent, { key: key, ref: setItemRef(index), role: itemRole, style: typeof itemStyle === "object" && itemStyle !== null ? Object.assign({}, phaseTwoStyle, itemStyle) : phaseTwoStyle }, createRenderElement(RenderComponent, index, data, columnWidth))); if (stopIndex === void 0) { startIndex = index; stopIndex = index; } else { startIndex = Math.min(startIndex, index); stopIndex = Math.max(stopIndex, index); } }); if (needsFreshBatch) { const batchSize = Math.min(itemCount - measuredCount, Math.ceil((scrollTop + overscanBy - shortestColumnSize) / itemHeightEstimate * columnCount)); let index = measuredCount; const phaseOneStyle = getCachedSize(columnWidth); for (; index < measuredCount + batchSize; index++) { const data = items[index]; const key = itemKey(data, index); /* istanbul ignore next */ if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") { throwWithoutData(data, index); } children.push( /*#__PURE__*/__reactCreateElement__(ItemComponent, { key: key, ref: setItemRef(index), role: itemRole, style: typeof itemStyle === "object" ? Object.assign({}, phaseOneStyle, itemStyle) : phaseOneStyle }, createRenderElement(RenderComponent, index, data, columnWidth))); } } // Calls the onRender callback if the rendered indices changed React.useEffect(() => { if (typeof storedOnRender.current === "function" && stopIndex !== void 0) storedOnRender.current(startIndex, stopIndex, items); didEverMount = "1"; }, [startIndex, stopIndex, items, storedOnRender]); // If we needed a fresh batch we should reload our components with the measured // sizes React.useEffect(() => { if (needsFreshBatch) forceUpdate(); // eslint-disable-next-line }, [needsFreshBatch, positioner]); // gets the container style object based upon the estimated height and whether or not // the page is being scrolled const containerStyle = getContainerStyle(isScrolling, estimateHeight(itemCount, itemHeightEstimate)); return /*#__PURE__*/__reactCreateElement__(ContainerComponent, { ref: containerRef, key: didEverMount, id: id, role: role, className: className, tabIndex: tabIndex, style: typeof style === "object" ? assignUserStyle(containerStyle, style) : containerStyle, children: children }); } /* istanbul ignore next */ function throwWithoutData(data, index) { if (!data) { throw new Error(`No data was found at index: ${index}\n\n` + `This usually happens when you've mutated or changed the "items" array in a ` + `way that makes it shorter than the previous "items" array. Masonic knows nothing ` + `about your underlying data and when it caches cell positions, it assumes you aren't ` + `mutating the underlying "items".\n\n` + `See https://codesandbox.io/s/masonic-w-react-router-example-2b5f9?file=/src/index.js for ` + `an example that gets around this limitations. For advanced implementations, see ` + `https://codesandbox.io/s/masonic-w-react-router-and-advanced-config-example-8em42?file=/src/index.js\n\n` + `If this was the result of your removing an item from your "items", see this issue: ` + `https://github.com/jaredLunde/masonic/issues/12`); } } // This is for triggering a remount after SSR has loaded in the client w/ hydrate() let didEverMount = "0"; // // Render-phase utilities // ~5.5x faster than createElement without the memo const createRenderElement = /*#__PURE__*/trieMemoize([OneKeyMap, {}, WeakMap, OneKeyMap], (RenderComponent, index, data, columnWidth) => /*#__PURE__*/__reactCreateElement__(RenderComponent, { index: index, data: data, width: columnWidth })); const getContainerStyle = /*#__PURE__*/memoizeOne((isScrolling, estimateHeight) => ({ position: "relative", width: "100%", maxWidth: "100%", height: Math.ceil(estimateHeight), maxHeight: Math.ceil(estimateHeight), willChange: isScrolling ? "contents" : void 0, pointerEvents: isScrolling ? "none" : void 0 })); const cmp2 = (args, pargs) => args[0] === pargs[0] && args[1] === pargs[1]; const assignUserStyle = /*#__PURE__*/memoizeOne((containerStyle, userStyle) => Object.assign({}, containerStyle, userStyle), // @ts-expect-error cmp2); function defaultGetItemKey(_, i) { return i; } // the below memoizations for for ensuring shallow equal is reliable for pure // component children const getCachedSize = /*#__PURE__*/memoizeOne(width => ({ width, zIndex: -1000, visibility: "hidden", position: "absolute", writingMode: "horizontal-tb" }), (args, pargs) => args[0] === pargs[0]); const getRefSetter = /*#__PURE__*/memoizeOne((positioner, resizeObserver) => index => el => { if (el === null) return; if (resizeObserver) { resizeObserver.observe(el); elementsCache.set(el, index); } if (positioner.get(index) === void 0) positioner.set(index, el.offsetHeight); }, // @ts-expect-error cmp2);