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