masonic
Version:
<hr> <div align="center"> <h1 align="center"> 🧱 masonic </h1>
1,405 lines (1,203 loc) • 42 kB
JavaScript
import * as React from 'react';
import { useWindowSize } from '@react-hook/window-size';
import memoizeOne from '@essentials/memoize-one';
import OneKeyMap from '@essentials/one-key-map';
import useLatest from '@react-hook/latest';
import trieMemoize from 'trie-memoize';
import { requestTimeout, clearRequestTimeout } from '@essentials/request-timeout';
import useScrollPosition from '@react-hook/window-scroll';
import useLayoutEffect from '@react-hook/passive-layout-effect';
import rafSchd from 'raf-schd';
import useEvent from '@react-hook/event';
import { useThrottleCallback } from '@react-hook/throttle';
var RED = 0;
var BLACK = 1;
var NIL = 2;
var DELETE = 0;
var KEEP = 1;
function addInterval(treeNode, high, index) {
var node = treeNode.list;
var prevNode;
while (node) {
if (node.index === index) return false;
if (high > node.high) break;
prevNode = node;
node = node.next;
}
if (!prevNode) treeNode.list = {
index,
high,
next: node
};
if (prevNode) prevNode.next = {
index,
high,
next: prevNode.next
};
return true;
}
function removeInterval(treeNode, index) {
var node = treeNode.list;
if (node.index === index) {
if (node.next === null) return DELETE;
treeNode.list = node.next;
return KEEP;
}
var prevNode = node;
node = node.next;
while (node !== null) {
if (node.index === index) {
prevNode.next = node.next;
return KEEP;
}
prevNode = node;
node = node.next;
}
}
var NULL_NODE = {
low: 0,
max: 0,
high: 0,
C: NIL,
// @ts-expect-error
P: undefined,
// @ts-expect-error
R: undefined,
// @ts-expect-error
L: undefined,
// @ts-expect-error
list: undefined
};
NULL_NODE.P = NULL_NODE;
NULL_NODE.L = NULL_NODE;
NULL_NODE.R = NULL_NODE;
function updateMax(node) {
var max = node.high;
if (node.L === NULL_NODE && node.R === NULL_NODE) node.max = max;else if (node.L === NULL_NODE) node.max = Math.max(node.R.max, max);else if (node.R === NULL_NODE) node.max = Math.max(node.L.max, max);else node.max = Math.max(Math.max(node.L.max, node.R.max), max);
}
function updateMaxUp(node) {
var x = node;
while (x.P !== NULL_NODE) {
updateMax(x.P);
x = x.P;
}
}
function rotateLeft(tree, x) {
if (x.R === NULL_NODE) return;
var y = x.R;
x.R = y.L;
if (y.L !== NULL_NODE) y.L.P = x;
y.P = x.P;
if (x.P === NULL_NODE) tree.root = y;else if (x === x.P.L) x.P.L = y;else x.P.R = y;
y.L = x;
x.P = y;
updateMax(x);
updateMax(y);
}
function rotateRight(tree, x) {
if (x.L === NULL_NODE) return;
var y = x.L;
x.L = y.R;
if (y.R !== NULL_NODE) y.R.P = x;
y.P = x.P;
if (x.P === NULL_NODE) tree.root = y;else if (x === x.P.R) x.P.R = y;else x.P.L = y;
y.R = x;
x.P = y;
updateMax(x);
updateMax(y);
}
function replaceNode(tree, x, y) {
if (x.P === NULL_NODE) tree.root = y;else if (x === x.P.L) x.P.L = y;else x.P.R = y;
y.P = x.P;
}
function fixRemove(tree, x) {
var w;
while (x !== NULL_NODE && x.C === BLACK) {
if (x === x.P.L) {
w = x.P.R;
if (w.C === RED) {
w.C = BLACK;
x.P.C = RED;
rotateLeft(tree, x.P);
w = x.P.R;
}
if (w.L.C === BLACK && w.R.C === BLACK) {
w.C = RED;
x = x.P;
} else {
if (w.R.C === BLACK) {
w.L.C = BLACK;
w.C = RED;
rotateRight(tree, w);
w = x.P.R;
}
w.C = x.P.C;
x.P.C = BLACK;
w.R.C = BLACK;
rotateLeft(tree, x.P);
x = tree.root;
}
} else {
w = x.P.L;
if (w.C === RED) {
w.C = BLACK;
x.P.C = RED;
rotateRight(tree, x.P);
w = x.P.L;
}
if (w.R.C === BLACK && w.L.C === BLACK) {
w.C = RED;
x = x.P;
} else {
if (w.L.C === BLACK) {
w.R.C = BLACK;
w.C = RED;
rotateLeft(tree, w);
w = x.P.L;
}
w.C = x.P.C;
x.P.C = BLACK;
w.L.C = BLACK;
rotateRight(tree, x.P);
x = tree.root;
}
}
}
x.C = BLACK;
}
function minimumTree(x) {
while (x.L !== NULL_NODE) x = x.L;
return x;
}
function fixInsert(tree, z) {
var y;
while (z.P.C === RED) {
if (z.P === z.P.P.L) {
y = z.P.P.R;
if (y.C === RED) {
z.P.C = BLACK;
y.C = BLACK;
z.P.P.C = RED;
z = z.P.P;
} else {
if (z === z.P.R) {
z = z.P;
rotateLeft(tree, z);
}
z.P.C = BLACK;
z.P.P.C = RED;
rotateRight(tree, z.P.P);
}
} else {
y = z.P.P.L;
if (y.C === RED) {
z.P.C = BLACK;
y.C = BLACK;
z.P.P.C = RED;
z = z.P.P;
} else {
if (z === z.P.L) {
z = z.P;
rotateRight(tree, z);
}
z.P.C = BLACK;
z.P.P.C = RED;
rotateLeft(tree, z.P.P);
}
}
}
tree.root.C = BLACK;
}
function createIntervalTree() {
var tree = {
root: NULL_NODE,
size: 0
}; // we know these indexes are a consistent, safe way to make look ups
// for our case so it's a solid O(1) alternative to
// the O(log n) searchNode() in typical interval trees
var indexMap = {};
return {
insert(low, high, index) {
var x = tree.root;
var y = NULL_NODE;
while (x !== NULL_NODE) {
y = x;
if (low === y.low) break;
if (low < x.low) x = x.L;else x = x.R;
}
if (low === y.low && y !== NULL_NODE) {
if (!addInterval(y, high, index)) return;
y.high = Math.max(y.high, high);
updateMax(y);
updateMaxUp(y);
indexMap[index] = y;
tree.size++;
return;
}
var z = {
low,
high,
max: high,
C: RED,
P: y,
L: NULL_NODE,
R: NULL_NODE,
list: {
index,
high,
next: null
}
};
if (y === NULL_NODE) {
tree.root = z;
} else {
if (z.low < y.low) y.L = z;else y.R = z;
updateMaxUp(z);
}
fixInsert(tree, z);
indexMap[index] = z;
tree.size++;
},
remove(index) {
var z = indexMap[index];
if (z === void 0) return;
delete indexMap[index];
var intervalResult = removeInterval(z, index);
if (intervalResult === void 0) return;
if (intervalResult === KEEP) {
z.high = z.list.high;
updateMax(z);
updateMaxUp(z);
tree.size--;
return;
}
var y = z;
var originalYColor = y.C;
var x;
if (z.L === NULL_NODE) {
x = z.R;
replaceNode(tree, z, z.R);
} else if (z.R === NULL_NODE) {
x = z.L;
replaceNode(tree, z, z.L);
} else {
y = minimumTree(z.R);
originalYColor = y.C;
x = y.R;
if (y.P === z) {
x.P = y;
} else {
replaceNode(tree, y, y.R);
y.R = z.R;
y.R.P = y;
}
replaceNode(tree, z, y);
y.L = z.L;
y.L.P = y;
y.C = z.C;
}
updateMax(x);
updateMaxUp(x);
if (originalYColor === BLACK) fixRemove(tree, x);
tree.size--;
},
search(low, high, callback) {
var stack = [tree.root];
while (stack.length !== 0) {
var node = stack.pop();
if (node === NULL_NODE || low > node.max) continue;
if (node.L !== NULL_NODE) stack.push(node.L);
if (node.R !== NULL_NODE) stack.push(node.R);
if (node.low <= high && node.high >= low) {
var curr = node.list;
while (curr !== null) {
if (curr.high >= low) callback(curr.index, node.low);
curr = curr.next;
}
}
}
},
get size() {
return tree.size;
}
};
}
function _extends() {
_extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
var elementsCache = /*#__PURE__*/new WeakMap();
function useForceUpdate() {
var setState = React.useState(emptyObj$1)[1];
return React.useRef(() => setState({})).current;
}
var emptyObj$1 = {};
var __reactCreateElement__$2 = React.createElement;
/**
* 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
*/
function useMasonry(_ref) {
var {
// 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;
var startIndex = 0;
var stopIndex;
var forceUpdate = useForceUpdate();
var setItemRef = getRefSetter(positioner, resizeObserver);
var itemCount = items.length;
var {
columnWidth,
columnCount,
range,
estimateHeight,
size,
shortestColumn
} = positioner;
var measuredCount = size();
var shortestColumnSize = shortestColumn();
var children = [];
var itemRole = role === "list" ? "listitem" : role === "grid" ? "gridcell" : undefined;
var storedOnRender = useLatest(onRender);
overscanBy = height * overscanBy;
var rangeEnd = scrollTop + overscanBy;
var 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) => {
var data = items[index];
var key = itemKey(data, index);
var phaseTwoStyle = {
top,
left,
width: columnWidth,
writingMode: "horizontal-tb",
position: "absolute"
};
/* istanbul ignore next */
if (typeof process !== "undefined" && "production" !== "production") {
throwWithoutData(data, index);
}
children.push( /*#__PURE__*/__reactCreateElement__$2(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) {
var batchSize = Math.min(itemCount - measuredCount, Math.ceil((scrollTop + overscanBy - shortestColumnSize) / itemHeightEstimate * columnCount));
var _index = measuredCount;
var phaseOneStyle = getCachedSize(columnWidth);
for (; _index < measuredCount + batchSize; _index++) {
var _data = items[_index];
var key = itemKey(_data, _index);
/* istanbul ignore next */
if (typeof process !== "undefined" && "production" !== "production") {
throwWithoutData(_data, _index);
}
children.push( /*#__PURE__*/__reactCreateElement__$2(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
var containerStyle = getContainerStyle(isScrolling, estimateHeight(itemCount, itemHeightEstimate));
return /*#__PURE__*/__reactCreateElement__$2(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()
var didEverMount = "0";
//
// Render-phase utilities
// ~5.5x faster than createElement without the memo
var createRenderElement = /*#__PURE__*/trieMemoize([OneKeyMap, {}, WeakMap, OneKeyMap], (RenderComponent, index, data, columnWidth) => /*#__PURE__*/__reactCreateElement__$2(RenderComponent, {
index: index,
data: data,
width: columnWidth
}));
var 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
}));
var cmp2 = (args, pargs) => args[0] === pargs[0] && args[1] === pargs[1];
var 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
var getCachedSize = /*#__PURE__*/memoizeOne(width => ({
width,
zIndex: -1000,
visibility: "hidden",
position: "absolute",
writingMode: "horizontal-tb"
}), (args, pargs) => args[0] === pargs[0]);
var 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);
/**
* A hook for tracking whether the `window` is currently being scrolled and it's scroll position on
* the y-axis. These values are used for determining which grid cells to render and when
* to add styles to the masonry container that maximize scroll performance.
*
* @param offset - The vertical space in pixels between the top of the grid container and the top
* of the browser `document.documentElement`.
* @param fps - This determines how often (in frames per second) to update the scroll position of the
* browser `window` in state, and as a result the rate the masonry grid recalculates its visible cells.
* The default value of `12` has been very reasonable in my own testing, but if you have particularly
* heavy `render` components it may be prudent to reduce this number.
*/
function useScroller(offset, fps) {
if (offset === void 0) {
offset = 0;
}
if (fps === void 0) {
fps = 12;
}
var scrollTop = useScrollPosition(fps);
var [isScrolling, setIsScrolling] = React.useState(false);
var didMount = React.useRef(0);
React.useEffect(() => {
if (didMount.current === 1) setIsScrolling(true);
var didUnsubscribe = false;
var to = requestTimeout(() => {
if (didUnsubscribe) return; // This is here to prevent premature bail outs while maintaining high resolution
// unsets. Without it there will always bee a lot of unnecessary DOM writes to style.
setIsScrolling(false);
}, 40 + 1000 / fps);
didMount.current = 1;
return () => {
didUnsubscribe = true;
clearRequestTimeout(to);
};
}, [fps, scrollTop]);
return {
scrollTop: Math.max(0, scrollTop - offset),
isScrolling
};
}
/**
* A heavily-optimized component that updates `useMasonry()` when the scroll position of the browser `window`
* changes. This bare-metal component is used by `<Masonry>` under the hood.
*
* @param props
*/
function MasonryScroller(props) {
// We put this in its own layer because it's the thing that will trigger the most updates
// and we don't want to slower ourselves by cycling through all the functions, objects, and effects
// of other hooks
var {
scrollTop,
isScrolling
} = useScroller(props.offset, props.scrollFps); // This is an update-heavy phase and while we could just Object.assign here,
// it is way faster to inline and there's a relatively low hit to he bundle
// size.
return useMasonry({
scrollTop,
isScrolling,
positioner: props.positioner,
resizeObserver: props.resizeObserver,
items: props.items,
onRender: props.onRender,
as: props.as,
id: props.id,
className: props.className,
style: props.style,
role: props.role,
tabIndex: props.tabIndex,
containerRef: props.containerRef,
itemAs: props.itemAs,
itemStyle: props.itemStyle,
itemHeightEstimate: props.itemHeightEstimate,
itemKey: props.itemKey,
overscanBy: props.overscanBy,
height: props.height,
render: props.render
});
}
if (typeof process !== "undefined" && "production" !== "production") {
MasonryScroller.displayName = "MasonryScroller";
}
/**
* A hook for measuring the width of the grid container, as well as its distance
* from the top of the document. These values are necessary to correctly calculate the number/width
* of columns to render, as well as the number of rows to render.
*
* @param elementRef - A `ref` object created by `React.useRef()`. That ref should be provided to the
* `containerRef` property in `useMasonry()`.
* @param deps - You can force this hook to recalculate the `offset` and `width` whenever this
* dependencies list changes. A common dependencies list might look like `[windowWidth, windowHeight]`,
* which would force the hook to recalculate any time the size of the browser `window` changed.
*/
function useContainerPosition(elementRef, deps) {
if (deps === void 0) {
deps = emptyArr$1;
}
var [containerPosition, setContainerPosition] = React.useState({
offset: 0,
width: 0
});
useLayoutEffect(() => {
var {
current
} = elementRef;
if (current !== null) {
var offset = 0;
var el = current;
do {
offset += el.offsetTop || 0;
el = el.offsetParent;
} while (el);
if (offset !== containerPosition.offset || current.offsetWidth !== containerPosition.width) {
setContainerPosition({
offset,
width: current.offsetWidth
});
}
} // eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
return containerPosition;
}
var emptyArr$1 = [];
/**
* This hook creates the grid cell positioner and cache required by `useMasonry()`. This is
* the meat of the grid's layout algorithm, determining which cells to render at a given scroll
* position, as well as where to place new items in the grid.
*
* @param options - Properties that determine the number of columns in the grid, as well
* as their widths.
* @param options.columnWidth
* @param options.width
* @param deps - This hook will create a new positioner, clearing all existing cached positions,
* whenever the dependencies in this list change.
* @param options.columnGutter
* @param options.rowGutter
* @param options.columnCount
* @param options.maxColumnCount
* @param options.maxColumnWidth
*/
function usePositioner(_ref, deps) {
var {
width,
columnWidth = 200,
columnGutter = 0,
rowGutter,
columnCount,
maxColumnCount,
maxColumnWidth
} = _ref;
if (deps === void 0) {
deps = emptyArr;
}
var initPositioner = () => {
var [computedColumnWidth, computedColumnCount] = getColumns(width, columnWidth, columnGutter, columnCount, maxColumnCount, maxColumnWidth);
return createPositioner(computedColumnCount, computedColumnWidth, columnGutter, rowGutter !== null && rowGutter !== void 0 ? rowGutter : columnGutter);
};
var positionerRef = React.useRef();
if (positionerRef.current === undefined) positionerRef.current = initPositioner();
var prevDeps = React.useRef(deps);
var opts = [width, columnWidth, columnGutter, rowGutter, columnCount, maxColumnCount, maxColumnWidth];
var prevOpts = React.useRef(opts);
var optsChanged = !opts.every((item, i) => prevOpts.current[i] === item);
if (typeof process !== "undefined" && "production" !== "production") {
if (deps.length !== prevDeps.current.length) {
throw new Error("usePositioner(): The length of your dependencies array changed.");
}
} // Create a new positioner when the dependencies or sizes change
// Thanks to https://github.com/khmm12 for pointing this out
// https://github.com/jaredLunde/masonic/pull/41
if (optsChanged || !deps.every((item, i) => prevDeps.current[i] === item)) {
var prevPositioner = positionerRef.current;
var positioner = initPositioner();
prevDeps.current = deps;
prevOpts.current = opts;
if (optsChanged) {
var cacheSize = prevPositioner.size();
for (var _index2 = 0; _index2 < cacheSize; _index2++) {
var pos = prevPositioner.get(_index2);
positioner.set(_index2, pos !== void 0 ? pos.height : 0);
}
}
positionerRef.current = positioner;
}
return positionerRef.current;
}
/**
* Creates a cell positioner for the `useMasonry()` hook. The `usePositioner()` hook uses
* this utility under the hood.
*
* @param columnCount - The number of columns in the grid
* @param columnWidth - The width of each column in the grid
* @param columnGutter - The amount of horizontal space between columns in pixels.
* @param rowGutter - The amount of vertical space between cells within a column in pixels (falls back
* to `columnGutter`).
*/
var createPositioner = function createPositioner(columnCount, columnWidth, columnGutter, rowGutter) {
if (columnGutter === void 0) {
columnGutter = 0;
}
if (rowGutter === void 0) {
rowGutter = columnGutter;
}
// O(log(n)) lookup of cells to render for a given viewport size
// Store tops and bottoms of each cell for fast intersection lookup.
var intervalTree = createIntervalTree(); // Track the height of each column.
// Layout algorithm below always inserts into the shortest column.
var columnHeights = new Array(columnCount); // Used for O(1) item access
var items = []; // Tracks the item indexes within an individual column
var columnItems = new Array(columnCount);
for (var i = 0; i < columnCount; i++) {
columnHeights[i] = 0;
columnItems[i] = [];
}
return {
columnCount,
columnWidth,
set: function set(index, height) {
if (height === void 0) {
height = 0;
}
var column = 0; // finds the shortest column and uses it
for (var _i2 = 1; _i2 < columnHeights.length; _i2++) {
if (columnHeights[_i2] < columnHeights[column]) column = _i2;
}
var top = columnHeights[column] || 0;
columnHeights[column] = top + height + rowGutter;
columnItems[column].push(index);
items[index] = {
left: column * (columnWidth + columnGutter),
top,
height,
column
};
intervalTree.insert(top, top + height, index);
},
get: index => items[index],
// This only updates items in the specific columns that have changed, on and after the
// specific items that have changed
update: updates => {
var columns = new Array(columnCount);
var i = 0,
j = 0; // determines which columns have items that changed, as well as the minimum index
// changed in that column, as all items after that index will have their positions
// affected by the change
for (; i < updates.length - 1; i++) {
var _index3 = updates[i];
var item = items[_index3];
item.height = updates[++i];
intervalTree.remove(_index3);
intervalTree.insert(item.top, item.top + item.height, _index3);
columns[item.column] = columns[item.column] === void 0 ? _index3 : Math.min(_index3, columns[item.column]);
}
for (i = 0; i < columns.length; i++) {
// bails out if the column didn't change
if (columns[i] === void 0) continue;
var itemsInColumn = columnItems[i]; // the index order is sorted with certainty so binary search is a great solution
// here as opposed to Array.indexOf()
var startIndex = binarySearch(itemsInColumn, columns[i]);
var _index4 = columnItems[i][startIndex];
var startItem = items[_index4];
columnHeights[i] = startItem.top + startItem.height + rowGutter;
for (j = startIndex + 1; j < itemsInColumn.length; j++) {
var _index5 = itemsInColumn[j];
var _item = items[_index5];
_item.top = columnHeights[i];
columnHeights[i] = _item.top + _item.height + rowGutter;
intervalTree.remove(_index5);
intervalTree.insert(_item.top, _item.top + _item.height, _index5);
}
}
},
// Render all cells visible within the viewport range defined.
range: (lo, hi, renderCallback) => intervalTree.search(lo, hi, (index, top) => renderCallback(index, items[index].left, top)),
estimateHeight: (itemCount, defaultItemHeight) => {
var tallestColumn = Math.max(0, Math.max.apply(null, columnHeights));
return itemCount === intervalTree.size ? tallestColumn : tallestColumn + Math.ceil((itemCount - intervalTree.size) / columnCount) * defaultItemHeight;
},
shortestColumn: () => {
if (columnHeights.length > 1) return Math.min.apply(null, columnHeights);
return columnHeights[0] || 0;
},
size() {
return intervalTree.size;
},
all() {
return items;
}
};
};
/* istanbul ignore next */
var binarySearch = (a, y) => {
var l = 0;
var h = a.length - 1;
while (l <= h) {
var m = l + h >>> 1;
var x = a[m];
if (x === y) return m;else if (x <= y) l = m + 1;else h = m - 1;
}
return -1;
};
var getColumns = function getColumns(width, minimumWidth, gutter, columnCount, maxColumnCount, maxColumnWidth) {
if (width === void 0) {
width = 0;
}
if (minimumWidth === void 0) {
minimumWidth = 0;
}
if (gutter === void 0) {
gutter = 8;
}
columnCount = columnCount || Math.min(Math.floor((width + gutter) / (minimumWidth + gutter)), maxColumnCount || Infinity) || 1;
var columnWidth = Math.floor((width - gutter * (columnCount - 1)) / columnCount); // Cap the column width if maxColumnWidth is specified
if (maxColumnWidth !== undefined && columnWidth > maxColumnWidth) {
columnWidth = maxColumnWidth;
}
return [columnWidth, columnCount];
};
var emptyArr = [];
/**
* Creates a resize observer that forces updates to the grid cell positions when mutations are
* made to cells affecting their height.
*
* @param positioner - The masonry cell positioner created by the `usePositioner()` hook.
*/
function useResizeObserver(positioner) {
var forceUpdate = useForceUpdate();
var resizeObserver = createResizeObserver(positioner, forceUpdate); // Cleans up the resize observers when they change or the
// component unmounts
function _ref() {
return resizeObserver.disconnect();
}
React.useEffect(() => _ref, [resizeObserver]);
return resizeObserver;
}
function _ref2(handler) {
handler.cancel();
}
/**
* Creates a resize observer that fires an `updater` callback whenever the height of
* one or many cells change. The `useResizeObserver()` hook is using this under the hood.
*
* @param positioner - A cell positioner created by the `usePositioner()` hook or the `createPositioner()` utility
* @param updater - A callback that fires whenever one or many cell heights change.
*/
var createResizeObserver = /*#__PURE__*/trieMemoize([WeakMap], // TODO: figure out a way to test this
/* istanbul ignore next */
(positioner, updater) => {
var updates = [];
var update = rafSchd(() => {
if (updates.length > 0) {
// Updates the size/positions of the cell with the resize
// observer updates
positioner.update(updates);
updater(updates);
}
updates.length = 0;
});
var commonHandler = target => {
var height = target.offsetHeight;
if (height > 0) {
var index = elementsCache.get(target);
if (index !== void 0) {
var position = positioner.get(index);
if (position !== void 0 && height !== position.height) updates.push(index, height);
}
}
update();
};
var handlers = new Map();
var handleEntries = entries => {
var i = 0;
for (; i < entries.length; i++) {
var entry = entries[i];
var index = elementsCache.get(entry.target);
if (index === void 0) continue;
var handler = handlers.get(index);
if (!handler) {
handler = rafSchd(commonHandler);
handlers.set(index, handler);
}
handler(entry.target);
}
};
var ro = new ResizeObserver(handleEntries); // Overrides the original disconnect to include cancelling handling the entries.
// Ideally this would be its own method but that would result in a breaking
// change.
var disconnect = ro.disconnect.bind(ro);
ro.disconnect = () => {
disconnect();
handlers.forEach(_ref2);
};
return ro;
});
/**
* A hook that creates a callback for scrolling to a specific index in
* the "items" array.
*
* @param positioner - A positioner created by the `usePositioner()` hook
* @param options - Configuration options
*/
function useScrollToIndex(positioner, options) {
var _latestOptions$curren;
var {
align = "top",
element = typeof window !== "undefined" && window,
offset = 0,
height = typeof window !== "undefined" ? window.innerHeight : 0
} = options;
var latestOptions = useLatest({
positioner,
element,
align,
offset,
height
});
var getTarget = React.useRef(() => {
var latestElement = latestOptions.current.element;
return latestElement && "current" in latestElement ? latestElement.current : latestElement;
}).current;
var [state, dispatch] = React.useReducer((state, action) => {
var nextState = {
position: state.position,
index: state.index,
prevTop: state.prevTop
};
/* istanbul ignore next */
if (action.type === "scrollToIndex") {
var _action$value;
return {
position: latestOptions.current.positioner.get((_action$value = action.value) !== null && _action$value !== void 0 ? _action$value : -1),
index: action.value,
prevTop: void 0
};
} else if (action.type === "setPosition") {
nextState.position = action.value;
} else if (action.type === "setPrevTop") {
nextState.prevTop = action.value;
} else if (action.type === "reset") {
return defaultState;
}
return nextState;
}, defaultState);
var throttledDispatch = useThrottleCallback(dispatch, 15); // If we find the position along the way we can immediately take off
// to the correct spot.
useEvent(getTarget(), "scroll", () => {
if (!state.position && state.index) {
var position = latestOptions.current.positioner.get(state.index);
if (position) {
dispatch({
type: "setPosition",
value: position
});
}
}
}); // If the top changes out from under us in the case of dynamic cells, we
// want to keep following it.
var currentTop = state.index !== void 0 && ((_latestOptions$curren = latestOptions.current.positioner.get(state.index)) === null || _latestOptions$curren === void 0 ? void 0 : _latestOptions$curren.top);
React.useEffect(() => {
var target = getTarget();
if (!target) return;
var {
height,
align,
offset,
positioner
} = latestOptions.current;
function _ref() {
return !didUnsubscribe && dispatch({
type: "reset"
});
}
function _ref2() {
didUnsubscribe = true;
clearTimeout(timeout);
}
if (state.position) {
var scrollTop = state.position.top;
if (align === "bottom") {
scrollTop = scrollTop - height + state.position.height;
} else if (align === "center") {
scrollTop -= (height - state.position.height) / 2;
}
target.scrollTo(0, Math.max(0, scrollTop += offset)); // Resets state after 400ms, an arbitrary time I determined to be
// still visually pleasing if there is a slow network reply in dynamic
// cells
var didUnsubscribe = false;
var timeout = setTimeout(_ref, 400);
return _ref2;
} else if (state.index !== void 0) {
// Estimates the top based upon the average height of current cells
var estimatedTop = positioner.shortestColumn() / positioner.size() * state.index;
if (state.prevTop) estimatedTop = Math.max(estimatedTop, state.prevTop + height);
target.scrollTo(0, estimatedTop);
throttledDispatch({
type: "setPrevTop",
value: estimatedTop
});
}
}, [currentTop, state, latestOptions, getTarget, throttledDispatch]);
return React.useRef(index => {
dispatch({
type: "scrollToIndex",
value: index
});
}).current;
}
var defaultState = {
index: void 0,
position: void 0,
prevTop: void 0
};
var __reactCreateElement__$1 = React.createElement;
/**
* A "batteries included" masonry grid which includes all of the implementation details below. This component is the
* easiest way to get off and running in your app, before switching to more advanced implementations, if necessary.
* It will change its column count to fit its container's width and will decide how many rows to render based upon
* the height of the browser `window`.
*
* @param props
*/
function Masonry(props) {
var containerRef = React.useRef(null);
var windowSize = useWindowSize({
initialWidth: props.ssrWidth,
initialHeight: props.ssrHeight
});
var containerPos = useContainerPosition(containerRef, windowSize);
var nextProps = Object.assign({
offset: containerPos.offset,
width: containerPos.width || windowSize[0],
height: windowSize[1],
containerRef
}, props);
nextProps.positioner = usePositioner(nextProps);
nextProps.resizeObserver = useResizeObserver(nextProps.positioner);
var scrollToIndex = useScrollToIndex(nextProps.positioner, {
height: nextProps.height,
offset: containerPos.offset,
align: typeof props.scrollToIndex === "object" ? props.scrollToIndex.align : void 0
});
var index = props.scrollToIndex && (typeof props.scrollToIndex === "number" ? props.scrollToIndex : props.scrollToIndex.index);
React.useEffect(() => {
if (index !== void 0) scrollToIndex(index);
}, [index, scrollToIndex]);
return __reactCreateElement__$1(MasonryScroller, nextProps);
}
if (typeof process !== "undefined" && "production" !== "production") {
Masonry.displayName = "Masonry";
}
var __reactCreateElement__ = React.createElement;
/**
* This is just a single-column `<Masonry>` component without column-specific props.
*
* @param props
*/
function List(props) {
return /*#__PURE__*/__reactCreateElement__(Masonry, _extends({
role: "list",
rowGutter: props.rowGutter,
columnCount: 1,
columnWidth: 1
}, props));
}
if (typeof process !== "undefined" && "production" !== "production") {
List.displayName = "List";
}
/**
* A utility hook for seamlessly adding infinite scroll behavior to the `useMasonry()` hook. This
* hook invokes a callback each time the last rendered index surpasses the total number of items
* in your items array or the number defined in the `totalItems` option.
*
* @param loadMoreItems - This callback is invoked when more rows must be loaded. It will be used to
* determine when to refresh the list with the newly-loaded data. This callback may be called multiple
* times in reaction to a single scroll event, so it's important to memoize its arguments. If you're
* creating this callback inside of a functional component, make sure you wrap it in `React.useCallback()`,
* as well.
* @param options
*/
function useInfiniteLoader(loadMoreItems, options) {
if (options === void 0) {
options = emptyObj;
}
var {
isItemLoaded,
minimumBatchSize = 16,
threshold = 16,
totalItems = 9e9
} = options;
var storedLoadMoreItems = useLatest(loadMoreItems);
var storedIsItemLoaded = useLatest(isItemLoaded);
return React.useCallback((startIndex, stopIndex, items) => {
var unloadedRanges = scanForUnloadedRanges(storedIsItemLoaded.current, minimumBatchSize, items, totalItems, Math.max(0, startIndex - threshold), Math.min(totalItems - 1, (stopIndex || 0) + threshold)); // The user is responsible for memoizing their loadMoreItems() function
// because we don't want to make assumptions about how they want to deal
// with `items`
for (var i = 0; i < unloadedRanges.length - 1; ++i) storedLoadMoreItems.current(unloadedRanges[i], unloadedRanges[++i], items);
}, [totalItems, minimumBatchSize, threshold, storedLoadMoreItems, storedIsItemLoaded]);
}
/**
* Returns all of the ranges within a larger range that contain unloaded rows.
*
* @param isItemLoaded
* @param minimumBatchSize
* @param items
* @param totalItems
* @param startIndex
* @param stopIndex
*/
function scanForUnloadedRanges(isItemLoaded, minimumBatchSize, items, totalItems, startIndex, stopIndex) {
if (isItemLoaded === void 0) {
isItemLoaded = defaultIsItemLoaded;
}
if (minimumBatchSize === void 0) {
minimumBatchSize = 16;
}
if (totalItems === void 0) {
totalItems = 9e9;
}
var unloadedRanges = [];
var rangeStartIndex,
rangeStopIndex,
index = startIndex;
/* istanbul ignore next */
for (; index <= stopIndex; index++) {
if (!isItemLoaded(index, items)) {
rangeStopIndex = index;
if (rangeStartIndex === void 0) rangeStartIndex = index;
} else if (rangeStartIndex !== void 0 && rangeStopIndex !== void 0) {
unloadedRanges.push(rangeStartIndex, rangeStopIndex);
rangeStartIndex = rangeStopIndex = void 0;
}
} // If :rangeStopIndex is not null it means we haven't run out of unloaded rows.
// Scan forward to try filling our :minimumBatchSize.
if (rangeStartIndex !== void 0 && rangeStopIndex !== void 0) {
var potentialStopIndex = Math.min(Math.max(rangeStopIndex, rangeStartIndex + minimumBatchSize - 1), totalItems - 1);
/* istanbul ignore next */
for (index = rangeStopIndex + 1; index <= potentialStopIndex; index++) {
if (!isItemLoaded(index, items)) {
rangeStopIndex = index;
} else {
break;
}
}
unloadedRanges.push(rangeStartIndex, rangeStopIndex);
} // Check to see if our first range ended prematurely.
// In this case we should scan backwards to try filling our :minimumBatchSize.
/* istanbul ignore next */
if (unloadedRanges.length) {
var firstUnloadedStart = unloadedRanges[0];
var firstUnloadedStop = unloadedRanges[1];
while (firstUnloadedStop - firstUnloadedStart + 1 < minimumBatchSize && firstUnloadedStart > 0) {
var _index = firstUnloadedStart - 1;
if (!isItemLoaded(_index, items)) {
unloadedRanges[0] = firstUnloadedStart = _index;
} else {
break;
}
}
}
return unloadedRanges;
}
var defaultIsItemLoaded = (index, items) => items[index] !== void 0;
var emptyObj = {};
export { List, Masonry, MasonryScroller, createIntervalTree, createPositioner, createResizeObserver, useContainerPosition, useInfiniteLoader, useMasonry, usePositioner, useResizeObserver, useScrollToIndex, useScroller };
//# sourceMappingURL=index.dev.mjs.map