UNPKG

masonic

Version:

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

1,632 lines (1,375 loc) • 60.3 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react')) : typeof define === 'function' && define.amd ? define(['exports', 'react'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Masonic = {}, global.React)); })(this, (function (exports, React) { 'use strict'; function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return Object.freeze(n); } var React__namespace = /*#__PURE__*/_interopNamespace(React); var React__default = /*#__PURE__*/_interopDefaultLegacy(React); 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: index, high: high, next: node }; if (prevNode) prevNode.next = { index: index, high: 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: function 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: low, high: high, max: high, C: RED, P: y, L: NULL_NODE, R: NULL_NODE, list: { index: index, high: 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: function 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: function 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 useLatest = function useLatest(current) { var storedValue = React__namespace.useRef(current); React__namespace.useEffect(function () { storedValue.current = current; }); return storedValue; }; var useLatest$1 = useLatest; var useDebounceCallback = function useDebounceCallback(callback, wait, leading) { if (wait === void 0) { wait = 100; } if (leading === void 0) { leading = false; } var storedCallback = useLatest$1(callback); var timeout = React__namespace.useRef(); var deps = [wait, leading, storedCallback]; // Cleans up pending timeouts when the deps change function _ref() { timeout.current && clearTimeout(timeout.current); timeout.current = void 0; } React__namespace.useEffect(function () { return _ref; }, deps); function _ref2() { timeout.current = void 0; } return React__namespace.useCallback(function () { // eslint-disable-next-line prefer-rest-params var args = arguments; var current = timeout.current; // Calls on leading edge if (current === void 0 && leading) { timeout.current = setTimeout(_ref2, wait); // eslint-disable-next-line prefer-spread return storedCallback.current.apply(null, args); } // Clear the timeout every call and start waiting again current && clearTimeout(current); // Waits for `wait` before invoking the callback timeout.current = setTimeout(function () { timeout.current = void 0; storedCallback.current.apply(null, args); }, wait); }, deps); }; var useDebounce = function useDebounce(initialState, wait, leading) { var state = React__namespace.useState(initialState); return [state[0], useDebounceCallback(state[1], wait, leading)]; }; function useEvent(target, type, listener, cleanup) { var storedListener = React__namespace.useRef(listener); var storedCleanup = React__namespace.useRef(cleanup); React__namespace.useEffect(function () { storedListener.current = listener; storedCleanup.current = cleanup; }); React__namespace.useEffect(function () { var targetEl = target && 'current' in target ? target.current : target; if (!targetEl) return; var didUnsubscribe = 0; function listener() { if (didUnsubscribe) return; for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } storedListener.current.apply(this, args); } targetEl.addEventListener(type, listener); var cleanup = storedCleanup.current; return function () { didUnsubscribe = 1; targetEl.removeEventListener(type, listener); cleanup && cleanup(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [target, type]); } var emptyObj$2 = {}; var win$2 = typeof window === 'undefined' ? null : window; var wv = win$2 && typeof win$2.visualViewport !== 'undefined' ? win$2.visualViewport : null; var getSize = function getSize() { return [document.documentElement.clientWidth, document.documentElement.clientHeight]; }; var useWindowSize = function useWindowSize(options) { if (options === void 0) { options = emptyObj$2; } var _options = options, wait = _options.wait, leading = _options.leading, _options$initialWidth = _options.initialWidth, initialWidth = _options$initialWidth === void 0 ? 0 : _options$initialWidth, _options$initialHeigh = _options.initialHeight, initialHeight = _options$initialHeigh === void 0 ? 0 : _options$initialHeigh; var _useDebounce = useDebounce( /* istanbul ignore next */ typeof document === 'undefined' ? [initialWidth, initialHeight] : getSize, wait, leading), size = _useDebounce[0], setDebouncedSize = _useDebounce[1]; var setSize = function setSize() { return setDebouncedSize(getSize); }; useEvent(win$2, 'resize', setSize); // @ts-expect-error useEvent(wv, 'resize', setSize); useEvent(win$2, 'orientationchange', setSize); return size; }; var memoOne = function memoOne(fn, areEqual) { var equal = areEqual || defaultAreEqual; var args, value; return function () { return !!args && equal(arguments, args) ? value : value = fn.apply(null, args = arguments); }; }; var memoizeOne = memoOne; var defaultAreEqual = function defaultAreEqual(current, prev) { return current[0] === prev[0] && current[1] === prev[1] && current[2] === prev[2] && current[3] === prev[3]; }; var OneKeyMap = function OneKeyMap() { this.set = void 0; this.get = void 0; var key, val; this.get = function (k) { return k === key ? val : void 0; }; this.set = function (k, v) { key = k; val = v; }; }; var OneKeyMap$1 = OneKeyMap; var createCache = function createCache(obj) { function _set(k, v) { cache[k] = v; } function _get(k) { return cache[k]; } try { // @ts-ignore return new obj(); } catch (e) { var cache = {}; return { set: _set, get: _get }; } }; var memo = function memo(constructors) { var depth = constructors.length, baseCache = createCache(constructors[0]); var base; var map; var i; var node; var one = depth === 1; // quicker access for one and two-argument functions var g1 = function g1(args) { return (base = baseCache.get(args[0])) === void 0 || one ? base : base.get(args[1]); }; var s1 = function s1(args, value) { if (one) baseCache.set(args[0], value);else { if ((base = baseCache.get(args[0])) === void 0) { map = createCache(constructors[1]); map.set(args[1], value); baseCache.set(args[0], map); } else { base.set(args[1], value); } } return value; }; var g2 = function g2(args) { node = baseCache; for (i = 0; i < depth; i++) if ((node = node.get(args[i])) === void 0) return; return node; }; var s2 = function s2(args, value) { node = baseCache; for (i = 0; i < depth - 1; i++) { if ((map = node.get(args[i])) === void 0) { map = createCache(constructors[i + 1]); node.set(args[i], map); node = map; } else { node = map; } } node.set(args[depth - 1], value); return value; }; return depth < 3 ? { g: g1, s: s1 } : { g: g2, s: s2 }; }; var memoize = function memoize(mapConstructors, fn) { var item; var _memo = memo(mapConstructors), g = _memo.g, s = _memo.s; return function () { return (item = g(arguments)) === void 0 ? s(arguments, fn.apply(null, arguments)) : item; }; }; var trieMemoize = memoize; var elementsCache = /*#__PURE__*/new WeakMap(); function useForceUpdate() { var setState = React__namespace.useState(emptyObj$1)[1]; return React__namespace.useRef(function () { return setState({}); }).current; } var emptyObj$1 = {}; var __reactCreateElement__$2 = React__namespace.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 positioner = _ref.positioner, resizeObserver = _ref.resizeObserver, items = _ref.items, _ref$as = _ref.as, ContainerComponent = _ref$as === void 0 ? "div" : _ref$as, id = _ref.id, className = _ref.className, style = _ref.style, _ref$role = _ref.role, role = _ref$role === void 0 ? "grid" : _ref$role, _ref$tabIndex = _ref.tabIndex, tabIndex = _ref$tabIndex === void 0 ? 0 : _ref$tabIndex, containerRef = _ref.containerRef, _ref$itemAs = _ref.itemAs, ItemComponent = _ref$itemAs === void 0 ? "div" : _ref$itemAs, itemStyle = _ref.itemStyle, _ref$itemHeightEstima = _ref.itemHeightEstimate, itemHeightEstimate = _ref$itemHeightEstima === void 0 ? 300 : _ref$itemHeightEstima, _ref$itemKey = _ref.itemKey, itemKey = _ref$itemKey === void 0 ? defaultGetItemKey : _ref$itemKey, _ref$overscanBy = _ref.overscanBy, overscanBy = _ref$overscanBy === void 0 ? 2 : _ref$overscanBy, scrollTop = _ref.scrollTop, isScrolling = _ref.isScrolling, height = _ref.height, RenderComponent = _ref.render, onRender = _ref.onRender; var startIndex = 0; var stopIndex; var forceUpdate = useForceUpdate(); var setItemRef = getRefSetter(positioner, resizeObserver); var itemCount = items.length; var columnWidth = positioner.columnWidth, columnCount = positioner.columnCount, range = positioner.range, estimateHeight = positioner.estimateHeight, size = positioner.size, shortestColumn = positioner.shortestColumn; var measuredCount = size(); var shortestColumnSize = shortestColumn(); var children = []; var itemRole = role === "list" ? "listitem" : role === "grid" ? "gridcell" : undefined; var storedOnRender = useLatest$1(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, function (index, left, top) { var data = items[index]; var key = itemKey(data, index); var phaseTwoStyle = { top: top, left: 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 ? _extends({}, 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" ? _extends({}, phaseOneStyle, itemStyle) : phaseOneStyle }, createRenderElement(RenderComponent, _index, _data, columnWidth))); } } // Calls the onRender callback if the rendered indices changed React__namespace.useEffect(function () { 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__namespace.useEffect(function () { 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$1, {}, WeakMap, OneKeyMap$1], function (RenderComponent, index, data, columnWidth) { return /*#__PURE__*/__reactCreateElement__$2(RenderComponent, { index: index, data: data, width: columnWidth }); }); var getContainerStyle = /*#__PURE__*/memoizeOne(function (isScrolling, estimateHeight) { return { 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 = function cmp2(args, pargs) { return args[0] === pargs[0] && args[1] === pargs[1]; }; var assignUserStyle = /*#__PURE__*/memoizeOne(function (containerStyle, userStyle) { return _extends({}, 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(function (width) { return { width: width, zIndex: -1000, visibility: "hidden", position: "absolute", writingMode: "horizontal-tb" }; }, function (args, pargs) { return args[0] === pargs[0]; }); var getRefSetter = /*#__PURE__*/memoizeOne(function (positioner, resizeObserver) { return function (index) { return function (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); var u = 'undefined', win$1 = typeof window !== u ? window : {}, p = typeof performance !== u ? performance : Date, now$1 = function now() { return p.now(); }, af = 'AnimationFrame', Caf = 'cancel' + af, Raf = 'request' + af, raf = win$1[Raf] && /*#__PURE__*/win$1[Raf].bind(win$1), caf = win$1[Caf] && /*#__PURE__*/win$1[Caf].bind(win$1); function _raf(callback) { var curr = now$1(), next = Math.max(lastTime + 1000 / 60, curr); return setTimeout(function () { callback(lastTime = next); }, next - curr); } function _caf(h) { return clearTimeout(h); } if (!raf || !caf) { var lastTime = 0; raf = _raf; caf = _caf; } /** * Copyright 2011, Joe Lambert. * Free to use under the MIT license. * http://www.opensource.org/licenses/mit-license.php **/ var clearRequestTimeout = function clearRequestTimeout(handle) { caf(handle.v || -1); }; var requestTimeout = function requestTimeout(fn, ms) { var start = now$1(), handle = {}; var loop = function loop() { now$1() - start >= ms ? fn.call(null) : handle.v = raf(loop); }; handle.v = raf(loop); return handle; }; var perf = typeof performance !== 'undefined' ? performance : Date; var now = function now() { return perf.now(); }; function useThrottleCallback(callback, fps, leading) { if (fps === void 0) { fps = 30; } if (leading === void 0) { leading = false; } var storedCallback = useLatest$1(callback); var ms = 1000 / fps; var prev = React__namespace.useRef(0); var trailingTimeout = React__namespace.useRef(); var clearTrailing = function clearTrailing() { return trailingTimeout.current && clearTimeout(trailingTimeout.current); }; var deps = [fps, leading, storedCallback]; // Reset any time the deps change function _ref() { prev.current = 0; clearTrailing(); } React__namespace.useEffect(function () { return _ref; }, deps); return React__namespace.useCallback(function () { // eslint-disable-next-line prefer-rest-params var args = arguments; var rightNow = now(); var call = function call() { prev.current = rightNow; clearTrailing(); storedCallback.current.apply(null, args); }; var current = prev.current; // leading if (leading && current === 0) return call(); // body if (rightNow - current > ms) { if (current > 0) return call(); prev.current = rightNow; } // trailing clearTrailing(); trailingTimeout.current = setTimeout(function () { call(); prev.current = 0; }, ms); }, deps); } function useThrottle(initialState, fps, leading) { var state = React__namespace.useState(initialState); return [state[0], useThrottleCallback(state[1], fps, leading)]; } var win = typeof window === 'undefined' ? null : window; var getScrollY = function getScrollY() { return win.scrollY !== void 0 ? win.scrollY : win.pageYOffset === void 0 ? 0 : win.pageYOffset; }; var useWindowScroll = function useWindowScroll(fps) { if (fps === void 0) { fps = 30; } var state = useThrottle(typeof window === 'undefined' ? 0 : getScrollY, fps, true); useEvent(win, 'scroll', function () { return state[1](getScrollY()); }); return state[0]; }; var useScrollPosition = useWindowScroll; /** * 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 _React$useState = React__namespace.useState(false), isScrolling = _React$useState[0], setIsScrolling = _React$useState[1]; var didMount = React__namespace.useRef(0); React__namespace.useEffect(function () { if (didMount.current === 1) setIsScrolling(true); var didUnsubscribe = false; var to = requestTimeout(function () { 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 function () { didUnsubscribe = true; clearRequestTimeout(to); }; }, [fps, scrollTop]); return { scrollTop: Math.max(0, scrollTop - offset), isScrolling: 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 _useScroller = useScroller(props.offset, props.scrollFps), scrollTop = _useScroller.scrollTop, isScrolling = _useScroller.isScrolling; // 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: scrollTop, isScrolling: 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"; } var usePassiveLayoutEffect = React__default["default"][typeof document !== 'undefined' && document.createElement !== void 0 ? 'useLayoutEffect' : 'useEffect']; var useLayoutEffect = usePassiveLayoutEffect; /** * 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 _React$useState = React__namespace.useState({ offset: 0, width: 0 }), containerPosition = _React$useState[0], setContainerPosition = _React$useState[1]; useLayoutEffect(function () { var current = elementRef.current; 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: 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 = _ref.width, _ref$columnWidth = _ref.columnWidth, columnWidth = _ref$columnWidth === void 0 ? 200 : _ref$columnWidth, _ref$columnGutter = _ref.columnGutter, columnGutter = _ref$columnGutter === void 0 ? 0 : _ref$columnGutter, rowGutter = _ref.rowGutter, columnCount = _ref.columnCount, maxColumnCount = _ref.maxColumnCount, maxColumnWidth = _ref.maxColumnWidth; if (deps === void 0) { deps = emptyArr; } var initPositioner = function initPositioner() { var _getColumns = getColumns(width, columnWidth, columnGutter, columnCount, maxColumnCount, maxColumnWidth), computedColumnWidth = _getColumns[0], computedColumnCount = _getColumns[1]; return createPositioner(computedColumnCount, computedColumnWidth, columnGutter, rowGutter !== null && rowGutter !== void 0 ? rowGutter : columnGutter); }; var positionerRef = React__namespace.useRef(); if (positionerRef.current === undefined) positionerRef.current = initPositioner(); var prevDeps = React__namespace.useRef(deps); var opts = [width, columnWidth, columnGutter, rowGutter, columnCount, maxColumnCount, maxColumnWidth]; var prevOpts = React__namespace.useRef(opts); var optsChanged = !opts.every(function (item, i) { return 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(function (item, i) { return 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: columnCount, columnWidth: 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: top, height: height, column: column }; intervalTree.insert(top, top + height, index); }, get: function get(index) { return items[index]; }, // This only updates items in the specific columns that have changed, on and after the // specific items that have changed update: function 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: function range(lo, hi, renderCallback) { return intervalTree.search(lo, hi, function (index, top) { return renderCallback(index, items[index].left, top); }); }, estimateHeight: function 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: function shortestColumn() { if (columnHeights.length > 1) return Math.min.apply(null, columnHeights); return columnHeights[0] || 0; }, size: function size() { return intervalTree.size; }, all: function all() { return items; } }; }; /* istanbul ignore next */ var binarySearch = function 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 = []; var rafSchd = function rafSchd(fn) { var lastArgs = []; var frameId = null; function _ref() { frameId = null; fn.apply(void 0, lastArgs); } var wrapperFn = function wrapperFn() { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } lastArgs = args; if (frameId) { return; } frameId = requestAnimationFrame(_ref); }; wrapperFn.cancel = function () { if (!frameId) { return; } cancelAnimationFrame(frameId); frameId = null; }; return wrapperFn; }; var rafSchd$1 = rafSchd; /** * 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__namespace.useEffect(function () { return _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 */ function (positioner, updater) { var updates = []; var update = rafSchd$1(function () { 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 = function 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 = function 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$1(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 = function () { 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 _options$align = options.align, align = _options$align === void 0 ? "top" : _options$align, _options$element = options.element, element = _options$element === void 0 ? typeof window !== "undefined" && window : _options$element, _options$offset = options.offset, offset = _options$offset === void 0 ? 0 : _options$offset, _options$height = options.height, height = _options$height === void 0 ? typeof window !== "undefined" ? window.innerHeight : 0 : _options$height; var latestOptions = useLatest$1({ positioner: positioner, element: element, align: align, offset: offset, height: height }); var getTarget = React__namespace.useRef(function () { var latestElement = latestOptions.current.element; return latestElement && "current" in latestElement ? latestElement.current : latestElement; }).current; var _React$useReducer = React__namespace.useReducer(function (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), state = _React$useReducer[0], dispatch = _React$useReducer[1]; var throttledDispatch = useThrottleCallback(dispatch, 15); // If we find the pos