UNPKG

react-procedural-scroller

Version:

A headless React hook for building infinite scroller components that display procedurally generated data, perfect for use cases like date scrollers, where row data is generated dynamically rather than fetched from a dataset. Notable features include: full

1,401 lines (1,371 loc) 42.7 kB
"use strict"; var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropNames = Object.getOwnPropertyNames; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { useProceduralScroller: () => useProceduralScroller }); module.exports = __toCommonJS(index_exports); // src/hooks/use-procedural-scroller.ts var import_react8 = require("react"); // src/types/items.ts var itemRangeKeys = [ "startPadding", "startContent", "content", "endContent", "endPadding" ]; // src/lib/error.ts var ProceduralScrollerError = class extends Error { constructor(message, data) { super(message); this.name = this.constructor.name; Object.setPrototypeOf(this, new.target.prototype); if (data) { this.data = data; } } }; // src/validation/number/integer.ts function asInteger(n) { if (!isInteger(n)) { throw new ProceduralScrollerError( `Expected an integer number, received n=${n}`, { n } ); } return n; } function isInteger(input) { return !(!Number.isInteger(input) || !isFinite(input) || isNaN(input)); } // src/validation/number/non-negative-integer.ts function asNonNegativeInteger(n) { if (!isNonNegativeInteger(n)) { throw new ProceduralScrollerError( `Expected a non-negative integer number, received n=${n}`, { n } ); } return n; } function isNonNegativeInteger(n) { return !(!Number.isInteger(n) || Number(n) < 0 || !isFinite(n)); } // src/validation/items.ts function asItemsRangePointers(input) { const errorPrefix = `Invalid rangePointers object:`; if (typeof input !== "object" || input === null || Object.keys(input).length !== itemRangeKeys.length) { throw new ProceduralScrollerError( `${errorPrefix} Expected an object with ${itemRangeKeys.length} keys`, { input } ); } for (const key of itemRangeKeys) { const inputAsRangePointers = input; if (!Array.isArray(inputAsRangePointers[key]) || inputAsRangePointers[key].length !== 2 || !isNonNegativeInteger(inputAsRangePointers[key][0]) || !isNonNegativeInteger(inputAsRangePointers[key][1])) { throw new ProceduralScrollerError( `${errorPrefix} Invalid pointer array for key=${key}`, { input } ); } } return input; } function asItems(input) { const errorPrefix = "Invalid items object:"; const expectedKeys = 2; if (typeof input !== "object" || input === null || Object.keys(input).length !== expectedKeys) { throw new ProceduralScrollerError( `${errorPrefix} Expected an object with ${expectedKeys} keys`, { input } ); } const inputAsItems = input; asItemsRangePointers(inputAsItems.rangePointers); if (!Array.isArray(inputAsItems.indexes)) { throw new ProceduralScrollerError( `${errorPrefix} Expected items.indexes to be an array`, { input } ); } for (let i = 0; i < inputAsItems.indexes.length; i++) { if (!isInteger(inputAsItems.indexes[i])) { throw new ProceduralScrollerError( `${errorPrefix} items.indexes[${i}] is not an integer`, { input } ); } if (i > 0 && i < inputAsItems.indexes.length - 1) { if (inputAsItems.indexes[i - 1] !== inputAsItems.indexes[i] - 1) { throw new ProceduralScrollerError( `${errorPrefix} items.indexes[${i - 1}] and items.indexes[${i}] are not consecutive`, { input } ); } if (inputAsItems.indexes[i + 1] !== inputAsItems.indexes[i] + 1) { throw new ProceduralScrollerError( `${errorPrefix} items.indexes[${i}] and items.indexes[${i + 1}] are not consecutive`, { input } ); } } } return input; } // src/validation/number/non-negative-real.ts function asNonNegativeReal(n) { if (!isNonNegativeReal(n)) { throw new ProceduralScrollerError( `Expected a non-negative real number, received n=${n}`, { n } ); } return n; } function isNonNegativeReal(n) { return !(typeof n !== "number" || isNaN(n) || !isFinite(n) || Number(n) < 0); } // src/validation/range-scaled-sizes.ts function asRangeScaledSizes(input) { const errorPrefix = "Received invalid rangeScaledSizes value:"; if (typeof input !== "object" || Object.keys(input).length !== itemRangeKeys.length) { throw new ProceduralScrollerError( `${errorPrefix} Expected object with ${itemRangeKeys.length} keys.`, { input, itemRangeKeys } ); } itemRangeKeys.forEach((key) => { if (!isNonNegativeReal(input[key])) { throw new ProceduralScrollerError( `${errorPrefix} Expected rangeScaledSizes[${key}] to be a non-negative real number.`, { input, itemRangeKeys } ); } }); return input; } // src/types/scroll.ts var scrollBlocks = ["start", "center", "end"]; // src/validation/scroll.ts function asScroll(input) { function throwError(message) { throw new ProceduralScrollerError(`Invalid scroll object: ${message}`, { input }); } const expectedKeys = 2; if (typeof input !== "object" || input === null || Object.keys(input).length !== expectedKeys) { throwError(`Expected an object with ${expectedKeys} keys`); } if (scrollBlocks.indexOf(input == null ? void 0 : input.block) === -1) { throwError(`Invalid scroll.block value`); } if (!isInteger(input == null ? void 0 : input.index)) { throwError(`Expected scroll.index to be an integer`); } return input; } // src/lib/items.ts function throwIndexLimitDistanceError(minIndex, maxIndex) { throw new ProceduralScrollerError( "Invalid configuration: the specified minIndex and maxIndex are too close together. There isn\u2019t enough room between them to render the required items.", { minIndex, maxIndex } ); } function indexesExceedLimits(minIndexLimit, maxIndexLimit, minIndex, maxIndex) { const minLimitExceeded = typeof minIndexLimit === "number" && minIndex < minIndexLimit; const maxLimitExceeded = typeof maxIndexLimit === "number" && maxIndex > maxIndexLimit; if (minLimitExceeded && maxLimitExceeded) throwIndexLimitDistanceError(minIndex, maxIndex); if (minLimitExceeded) return { minLimitExceeded: true, maxLimitExceeded: false }; if (maxLimitExceeded) return { minLimitExceeded: false, maxLimitExceeded: true }; return { minLimitExceeded: false, maxLimitExceeded: false }; } function getItems(containerSize, rangeScaledSizes, scroll, getMinItemSize, minIndex = null, maxIndex = null) { asNonNegativeReal(containerSize); asRangeScaledSizes(rangeScaledSizes); asScroll(scroll); let contentIndexes = []; if (scroll.block === "start") { contentIndexes = getRangeIndexes( rangeScaledSizes["content"], scroll.index, 1, containerSize, getMinItemSize ); } else if (scroll.block === "end") { contentIndexes = getRangeIndexes( rangeScaledSizes["content"], scroll.index, -1, containerSize, getMinItemSize ); } else if (scroll.block === "center") { contentIndexes = [ ...getRangeIndexes( asNonNegativeReal(rangeScaledSizes["content"] * 0.5), asInteger(scroll.index - 1), -1, containerSize, getMinItemSize ), scroll.index, ...getRangeIndexes( asNonNegativeReal(rangeScaledSizes["content"] * 0.5), asInteger(scroll.index + 1), 1, containerSize, getMinItemSize ) ]; } else { throw new ProceduralScrollerError( `Invalid: scroll.block = ${scroll.block}`, scroll ); } const startContentIndexes = getRangeIndexes( rangeScaledSizes["startContent"], asInteger(contentIndexes[0] - 1), -1, containerSize, getMinItemSize ); const startPaddingIndexes = getRangeIndexes( rangeScaledSizes["startPadding"], asInteger(startContentIndexes[0] - 1), -1, containerSize, getMinItemSize ); const endContentIndexes = getRangeIndexes( rangeScaledSizes["endContent"], asInteger(contentIndexes[contentIndexes.length - 1] + 1), 1, containerSize, getMinItemSize ); const endPaddingIndexes = getRangeIndexes( rangeScaledSizes["endPadding"], asInteger(endContentIndexes[endContentIndexes.length - 1] + 1), 1, containerSize, getMinItemSize ); const { minLimitExceeded, maxLimitExceeded } = indexesExceedLimits( minIndex, maxIndex, startPaddingIndexes[0], endPaddingIndexes[endPaddingIndexes.length - 1] ); if (minLimitExceeded) { const minLimitHitItems = asItems( orderedRangeIndexesToItems( getIndexLimitItems( containerSize, rangeScaledSizes, getMinItemSize, asInteger(Number(minIndex)), 1 ) ) ); const { minLimitExceeded: minLimitExceeded2, maxLimitExceeded: maxLimitExceeded2 } = indexesExceedLimits( minIndex, maxIndex, minLimitHitItems.indexes[0], minLimitHitItems.indexes[minLimitHitItems.indexes.length - 1] ); if (minLimitExceeded2 || maxLimitExceeded2) { throwIndexLimitDistanceError( asInteger(Number(minIndex)), asInteger(Number(maxIndex)) ); } return minLimitHitItems; } if (maxLimitExceeded) { const maxLimitHitItems = asItems( orderedRangeIndexesToItems( getIndexLimitItems( containerSize, rangeScaledSizes, getMinItemSize, asInteger(Number(maxIndex)), -1 ) ) ); const { minLimitExceeded: minLimitExceeded2, maxLimitExceeded: maxLimitExceeded2 } = indexesExceedLimits( minIndex, maxIndex, maxLimitHitItems.indexes[0], maxLimitHitItems.indexes[maxLimitHitItems.indexes.length - 1] ); if (minLimitExceeded2 || maxLimitExceeded2) { throwIndexLimitDistanceError( asInteger(Number(minIndex)), asInteger(Number(maxIndex)) ); } return maxLimitHitItems; } return asItems( orderedRangeIndexesToItems([ startPaddingIndexes, startContentIndexes, contentIndexes, endContentIndexes, endPaddingIndexes ]) ); } function itemsAreEqual(itemsA, itemsB) { asItems(itemsA); asItems(itemsB); if (itemsA.indexes.length !== itemsB.indexes.length) { return false; } for (let i = 0; i < itemsA.indexes.length; i++) { if (itemsA.indexes[i] !== itemsB.indexes[i]) { return false; } } if (Object.keys(itemsA.rangePointers).length !== Object.keys(itemsB.rangePointers).length) { return false; } for (const pointer of Object.keys( itemsA.rangePointers )) { if (itemsA.rangePointers[pointer].length !== itemsB.rangePointers[pointer].length) { return false; } for (let i = 0; i < itemsA.rangePointers[pointer].length; i++) { if (itemsA.rangePointers[pointer][i] !== itemsB.rangePointers[pointer][i]) { return false; } } } return true; } function getRangeIndexes(targetScaledHeight, startIndex, increment, containerSize, getMinItemSize) { asNonNegativeReal(targetScaledHeight); asInteger(startIndex); if (increment !== 1 && increment !== -1) { throw new ProceduralScrollerError(`Invalid increment: ${increment}`, { increment }); } asNonNegativeReal(containerSize); const targetHeight = asNonNegativeReal( targetScaledHeight * containerSize ); const indexes = [startIndex]; let totalHeight = asNonNegativeReal(0); while (totalHeight < targetHeight) { const newIndex = asInteger( indexes[increment > 0 ? indexes.length - 1 : 0] + increment ); if (increment > 0) { indexes.push(newIndex); } else { indexes.unshift(newIndex); } totalHeight = asNonNegativeReal(totalHeight + getMinItemSize(newIndex)); } return indexes; } function orderedRangeIndexesToItems(orderedRangeIndexes) { if (itemRangeKeys.length !== orderedRangeIndexes.length) { throw new ProceduralScrollerError( `Array length mismatch: ${itemRangeKeys.length} !== ${orderedRangeIndexes.length}`, { itemRangeKeys, orderedRangeIndexes } ); } let indexes = []; orderedRangeIndexes.forEach((rangeIndexes) => { indexes = [...indexes, ...rangeIndexes]; }); const pointers = []; orderedRangeIndexes.forEach((rangeIndexes) => { if (pointers.length < 1) { pointers.push([ asNonNegativeInteger(0), asNonNegativeInteger(rangeIndexes.length - 1) ]); } else { pointers.push([ asNonNegativeInteger(pointers[pointers.length - 1][1] + 1), asNonNegativeInteger( pointers[pointers.length - 1][1] + rangeIndexes.length ) ]); } }); return asItems({ indexes, rangePointers: { startPadding: pointers[0], startContent: pointers[1], content: pointers[2], endContent: pointers[3], endPadding: pointers[4] } }); } function getIndexLimitItems(containerSize, rangeScaledSizes, getMinItemSize, indexLimit, increment) { const ranges = increment === 1 ? [...itemRangeKeys] : [...itemRangeKeys].reverse(); let indexTracker = indexLimit; const result = ranges.map((range) => { const rangeIndexes = getRangeIndexes( rangeScaledSizes[range], indexTracker, increment, containerSize, getMinItemSize ); indexTracker = asInteger( (increment === 1 ? rangeIndexes[rangeIndexes.length - 1] : rangeIndexes[0]) + increment ); return rangeIndexes; }); return increment === 1 ? result : result.reverse(); } // src/hooks/use-scroll-handler.ts var import_react = require("react"); function useScrollHandler({ elementRef, handler }) { (0, import_react.useEffect)(() => { function safeHandler(ev) { const element2 = elementRef == null ? void 0 : elementRef.current; if (!element2) return; handler(element2, ev); } const element = elementRef.current; if (!element) return; element.addEventListener("scroll", safeHandler); return () => { element.removeEventListener("scroll", safeHandler); }; }, [elementRef, handler]); } // src/hooks/use-deferred-scroll-reset.ts var import_react2 = require("react"); // src/lib/dimensions.ts function getElementSize(element, axis, options = {}) { const { includePadding = true, includeBorder = false, includeMargin = false } = options; const isHorizontal = axis === "horizontal"; const style = window.getComputedStyle(element); let size; size = isHorizontal ? element.clientWidth : element.clientHeight; if (!includePadding) { const paddingStart = parseFloat( isHorizontal ? style.paddingLeft : style.paddingTop ); const paddingEnd = parseFloat( isHorizontal ? style.paddingRight : style.paddingBottom ); size -= paddingStart + paddingEnd; } if (includeBorder) { const borderStart = parseFloat( isHorizontal ? style.borderLeftWidth : style.borderTopWidth ); const borderEnd = parseFloat( isHorizontal ? style.borderRightWidth : style.borderBottomWidth ); size += borderStart + borderEnd; } if (includeMargin) { const marginStart = parseFloat( isHorizontal ? style.marginLeft : style.marginTop ); const marginEnd = parseFloat( isHorizontal ? style.marginRight : style.marginBottom ); size += marginStart + marginEnd; } return size; } // src/lib/scroll.ts function getScrollLength(block, container, item, scrollDirection) { if (scrollBlocks.indexOf(block) === -1) { throw new ProceduralScrollerError("Invalid scroll block", { block }); } const containerSize = getElementSize(container, scrollDirection, { includePadding: true, includeBorder: false, includeMargin: false }); const itemSize = getElementSize(item, scrollDirection, { includePadding: true, includeBorder: true, includeMargin: false }); const relativeOffset = computeRelativeOffset( container, item, scrollDirection ); if (block === "start") { return relativeOffset; } else if (block === "end") { return relativeOffset - (containerSize - itemSize); } else if (block === "center") { return relativeOffset - (containerSize - itemSize) / 2; } else { throw new ProceduralScrollerError(`Invalid scroll block`, { block }); } } function scrollToIndexInputToScroll(input) { if (!input) { return null; } return { block: input.block, index: asInteger(input.index) }; } function computeRelativeOffset(container, item, scrollDirection) { const isHorizontal = scrollDirection === "horizontal"; const containerScroll = container[isHorizontal ? "scrollLeft" : "scrollTop"]; const itemViewportOffset = item.getBoundingClientRect()[isHorizontal ? "left" : "top"] + containerScroll; const containerViewportOffset = container.getBoundingClientRect()[isHorizontal ? "left" : "top"] + (isHorizontal ? container.clientLeft : container.clientTop); return itemViewportOffset - containerViewportOffset; } // src/hooks/use-deferred-scroll-reset.ts function useDeferredScrollReset({ scroll, onScrollReset, containerRef, items, getRef, suppress, scrollDirection }) { const scrollTo = (0, import_react2.useCallback)( (scroll2, retry = false) => { var _a; const item = (_a = getRef(scroll2.index)) == null ? void 0 : _a.current; const container = containerRef.current; if (!item || !container) { if (!retry) { requestAnimationFrame(() => { scrollTo(scroll2, true); }); } return; } container[scrollDirection === "horizontal" ? "scrollLeft" : "scrollTop"] = getScrollLength(scroll2.block, container, item, scrollDirection); onScrollReset(); }, [containerRef, getRef, onScrollReset, scrollDirection] ); (0, import_react2.useLayoutEffect)(() => { if (suppress) return; if (!(scroll == null ? void 0 : scroll.current)) return; const scrollReference = __spreadValues({}, scroll.current); requestAnimationFrame(() => { if (scrollReference) { scrollTo(scrollReference); } }); }, [scroll, scrollTo, items, suppress]); } // src/hooks/use-item-stack.ts var import_react5 = require("react"); // src/hooks/use-element-ref-map.ts var import_react3 = require("react"); // src/validation/hooks/use-element-ref-map.ts function asElementRefMapKey(input) { function throwError(message) { throw new ProceduralScrollerError( `Invalid element ref map key: ${message}`, { input } ); } if (typeof input !== "string" && typeof input !== "number") { throwError(`Expected key to be a number or a string`); } return String(input); } // src/validation/number/positive-integer.ts function asPositiveInteger(n) { if (!Number.isInteger(n) || Number(n) < 1 || !isFinite(n)) { throw new ProceduralScrollerError( `Expected a positive integer number, received n=${n}`, { n } ); } return n; } // src/lib/map.ts function mapToObject(map) { const result = {}; map.forEach((value, key) => { result[key] = value; }); return result; } // src/hooks/use-element-ref-map.ts var useElementRefMap = ({ cacheLimit = asPositiveInteger(1) }) => { const elementRefMap = (0, import_react3.useRef)( /* @__PURE__ */ new Map() ); const getRef = (0, import_react3.useCallback)( (key) => { return elementRefMap.current.get(asElementRefMapKey(key)); }, [elementRefMap] ); const getRefOrError = (0, import_react3.useCallback)( (key, requireNonNull) => { const ref = elementRefMap.current.get(asElementRefMapKey(key)); if (ref && !requireNonNull) { return ref; } if ((ref == null ? void 0 : ref.current) !== null && requireNonNull) { return ref; } throw new ProceduralScrollerError( `A ref with key=${key} does not exist in elementRefMap`, mapToObject(elementRefMap.current) ); }, [elementRefMap] ); const setRef = (0, import_react3.useCallback)( (key, ref) => { asPositiveInteger(cacheLimit); const stringKey = asElementRefMapKey(key); const map = elementRefMap.current; map.delete(stringKey); map.set(stringKey, ref); while (map.size > cacheLimit) { map.delete(map.keys().next().value); } return getRefOrError(key, false); }, [cacheLimit, getRefOrError, elementRefMap] ); const getAllRefs = (0, import_react3.useCallback)(() => { return mapToObject(elementRefMap.current); }, [elementRefMap]); const clearRefs = (0, import_react3.useCallback)(() => { elementRefMap.current.clear(); }, [elementRefMap]); return { setRef, getRef, getRefOrError, getAllRefs, clearRefs }; }; // src/hooks/use-dimension-observer.ts var import_react4 = require("react"); function useDimensionObserver({ dimensions, elementRef, resizeHandler }) { const observedDimensions = (0, import_react4.useRef)( null ); const guardedResizeHandler = (0, import_react4.useCallback)(() => { function htmlPropertyIsNumeric(value) { if (typeof value !== "number") { throw new ProceduralScrollerError( `${value} must be a number property of a HTMLElement, received: ${value} (${typeof value})`, { value } ); } return true; } const element = elementRef.current; if (!element) return; if (observedDimensions.current) { let didResize = false; for (const dim of Object.keys( observedDimensions.current )) { if (element[dim] !== observedDimensions.current[dim]) { didResize = true; } if (htmlPropertyIsNumeric(element[dim])) { observedDimensions.current[dim] = element[dim]; } } if (!didResize) return; } else { const initialDimensions = {}; dimensions.forEach((dim) => { if (htmlPropertyIsNumeric(element[dim])) { initialDimensions[dim] = element[dim]; } }); observedDimensions.current = initialDimensions; } resizeHandler(element); }, [dimensions, elementRef, resizeHandler]); (0, import_react4.useEffect)(() => { const element = elementRef.current; if (!element) return; const resizeObserver = new ResizeObserver(guardedResizeHandler); resizeObserver.observe(element); return () => { resizeObserver.disconnect(); }; }, [elementRef, guardedResizeHandler]); } // src/hooks/use-item-stack.ts function useItemStack({ scrollDirection, rangeScaledSizes, containerRef, scroll, getMinItemSize, minIndex, maxIndex, initialContainerSize }) { var _a; const [items, setItems] = (0, import_react5.useState)(() => { const container = containerRef == null ? void 0 : containerRef.current; let containerSize = typeof initialContainerSize === "number" ? asNonNegativeReal(initialContainerSize) : null; if (container) { containerSize = asNonNegativeReal( getElementSize(container, scrollDirection, { includePadding: false, includeBorder: false, includeMargin: false }) ); } return scroll && typeof containerSize === "number" ? getItems( containerSize, rangeScaledSizes, scroll, getMinItemSize, minIndex, maxIndex ) : null; }); const { getRef, setRef, getRefOrError } = useElementRefMap({ cacheLimit: asPositiveInteger( typeof ((_a = items == null ? void 0 : items.indexes) == null ? void 0 : _a.length) === "number" ? items.indexes.length * 2 : 1 ) }); if (items == null ? void 0 : items.indexes) { items.indexes.forEach((index) => { setRef(index, getRef(index) || (0, import_react5.createRef)()); }); } const containerResizeHandler = (0, import_react5.useCallback)( (container) => { if (!scroll) { return; } setItems((prevItems) => { const newItems = getItems( asNonNegativeReal( getElementSize(container, scrollDirection, { includePadding: false, includeBorder: false, includeMargin: false }) ), rangeScaledSizes, scroll, getMinItemSize, minIndex, maxIndex ); return prevItems !== null && itemsAreEqual(newItems, prevItems) ? prevItems : newItems; }); }, [ scroll, scrollDirection, rangeScaledSizes, getMinItemSize, minIndex, maxIndex ] ); useDimensionObserver({ dimensions: [ scrollDirection === "horizontal" ? "clientWidth" : "clientHeight" ], elementRef: containerRef, resizeHandler: containerResizeHandler }); return { items, setItems, getRef, getRefOrError }; } // src/validation/array.ts function asArrayOfConsecutiveIntegers(array, dir = "asc") { function throwError(message) { throw new ProceduralScrollerError( `Invalid array of consecutive integers: ${message}`, { array, dir } ); } if (!Array.isArray(array)) { throwError("Input is not an array"); } let increment = 1; if (dir === "asc") { increment = 1; } else if (dir === "desc") { increment = -1; } else { throwError("dir is invalid"); } array.forEach((currentValue, index) => { asInteger(currentValue); if (index < array.length - 1) { const nextValue = asInteger(array[index + 1]); if (currentValue + increment !== nextValue) { throwError( `Non-consecutive values found at indexes ${index} and ${index + 1}.` ); } } }); return array.map((value) => asInteger(value)); } // src/lib/array.ts function mergeConsecutiveIntegerArrays(...arrays) { const sortedArrays = arrays.slice().filter((a) => a.length > 0).sort((a, b) => { return a[0] - b[0]; }).map((array) => asArrayOfConsecutiveIntegers(array)); const result = []; sortedArrays.forEach((array) => { array.forEach((value) => { if (result.length === 0 || result[result.length - 1] < value) { result.push(value); } }); }); return result; } // src/hooks/use-item-size-check.ts var import_react6 = require("react"); function useItemSizeCheck({ items, getMinItemSize, scrollDirection, enabled }) { (0, import_react6.useEffect)(() => { if (!items || !enabled) { return; } items.forEach((itemObj) => { var _a; const item = (_a = itemObj == null ? void 0 : itemObj.ref) == null ? void 0 : _a.current; if (!item) { return; } const itemSize = getElementSize(item, scrollDirection, { includePadding: true, includeBorder: true, includeMargin: true }); const expectedMinimum = getMinItemSize(itemObj.index); if (expectedMinimum > itemSize) { throw new ProceduralScrollerError( `Invalid item size: Item at index ${itemObj.index} has a height of ${itemSize}px, which is smaller than the expected minimum height of ${expectedMinimum}px (as returned by getMinItemSize(${itemObj.index})).` ); } }); }, [items, getMinItemSize, scrollDirection, enabled]); } // src/hooks/use-unbounded-height-check.ts var import_react7 = require("react"); var checks = 2; function useUnboundedHeightCheck({ items, containerRef, scrollDirection, enabled }) { const count = (0, import_react7.useRef)(asNonNegativeInteger(0)); (0, import_react7.useEffect)(() => { const container = containerRef.current; if (!container || !enabled) { return; } const scrollSizeAccessor = scrollDirection === "horizontal" ? "scrollWidth" : "scrollHeight"; const clientSizeAccessor = scrollDirection === "horizontal" ? "clientWidth" : "clientHeight"; if (container[scrollSizeAccessor] === container[clientSizeAccessor]) { count.current = asNonNegativeInteger(count.current + 1); } else { count.current = asNonNegativeInteger(0); } if (count.current >= checks) { throw new ProceduralScrollerError( `Unbounded container detected: The container\u2019s ${scrollSizeAccessor} and ${clientSizeAccessor} were equal for ${checks} consecutive renders. This suggests the container\u2019s size is not constrained, so scrolling cannot occur. This check can be disabled by passing \`validateLayouts = { container: false }\` into useProceduralScroller.` ); } }, [containerRef, enabled, items, scrollDirection]); } // src/hooks/use-procedural-scroller.ts var scrollToIndexDebounceDelay = 100; var useProceduralScroller = ({ getMinItemSize, scrollAreaScale = 3, paddingAreaScale = { start: 1, end: 1 }, initialScroll = { block: "center", index: 0 }, scrollDirection = "vertical", minIndex: minIndexInput, maxIndex: maxIndexInput, initialContainerSize: initialContainerSizeInput, validateLayouts = { container: true, items: true } }) => { const initialContainerSize = (0, import_react8.useMemo)(() => { if (isNonNegativeReal(initialContainerSizeInput)) { return asNonNegativeReal(initialContainerSizeInput); } return null; }, [initialContainerSizeInput]); const minIndex = (0, import_react8.useMemo)(() => { if (typeof minIndexInput === "number") { if (typeof maxIndexInput === "number" && minIndexInput > maxIndexInput) { throw new ProceduralScrollerError( "minIndex should not be greater than maxIndex", { minIndexInput, maxIndexInput } ); } return asInteger(minIndexInput); } return null; }, [minIndexInput, maxIndexInput]); const maxIndex = (0, import_react8.useMemo)(() => { if (typeof maxIndexInput === "number") { if (typeof minIndexInput === "number" && minIndexInput > maxIndexInput) { throw new ProceduralScrollerError( "minIndex should not be greater than maxIndex", { minIndexInput, maxIndexInput } ); } return asInteger(maxIndexInput); } return null; }, [minIndexInput, maxIndexInput]); const rangeScaledSizes = (0, import_react8.useMemo)(() => { return asRangeScaledSizes({ startPadding: asNonNegativeReal(paddingAreaScale.start), startContent: asNonNegativeReal((scrollAreaScale - 1) / 2), content: asNonNegativeReal(1), endContent: asNonNegativeReal((scrollAreaScale - 1) / 2), endPadding: asNonNegativeReal(paddingAreaScale.end) }); }, [scrollAreaScale, paddingAreaScale]); const scroll = (0, import_react8.useRef)( asScroll(__spreadProps(__spreadValues({}, initialScroll), { index: asInteger(initialScroll.index) })) ); const scrollResetting = (0, import_react8.useRef)(true); const containerRef = (0, import_react8.useRef)(null); const scrollToIndexDebounceRef = (0, import_react8.useRef)( null ); const scrollToIndexInProgressRef = (0, import_react8.useRef)(false); const [scrollToIndexInput, setScrollToIndexInput] = (0, import_react8.useState)(null); const [itemStackPointer, setItemStackPointer] = (0, import_react8.useState)(0); const itemStackA = useItemStack({ scrollDirection, rangeScaledSizes, containerRef, scroll: [scroll.current, scrollToIndexInputToScroll(scrollToIndexInput)][itemStackPointer], getMinItemSize, minIndex, maxIndex, initialContainerSize }); const itemStackB = useItemStack({ scrollDirection, rangeScaledSizes, containerRef, scroll: [scroll.current, scrollToIndexInputToScroll(scrollToIndexInput)][Number(!itemStackPointer)], getMinItemSize, minIndex, maxIndex, initialContainerSize }); const itemStacks = (0, import_react8.useMemo)( () => [itemStackA, itemStackB], [itemStackA, itemStackB] ); const items = (0, import_react8.useMemo)( () => itemStacks[itemStackPointer].items, [itemStackPointer, itemStacks] ); const setItems = (0, import_react8.useMemo)( () => itemStacks[itemStackPointer].setItems, [itemStackPointer, itemStacks] ); const getPrimaryRef = (0, import_react8.useMemo)( () => itemStacks[itemStackPointer].getRef, [itemStackPointer, itemStacks] ); const secondaryItems = (0, import_react8.useMemo)( () => itemStacks[Number(!itemStackPointer)].items, [itemStackPointer, itemStacks] ); const setSecondaryItems = (0, import_react8.useMemo)( () => itemStacks[Number(!itemStackPointer)].setItems, [itemStackPointer, itemStacks] ); const getSecondaryRef = (0, import_react8.useMemo)( () => itemStacks[Number(!itemStackPointer)].getRef, [itemStackPointer, itemStacks] ); const mergedIndexes = (0, import_react8.useMemo)(() => { return mergeConsecutiveIntegerArrays( (items == null ? void 0 : items.indexes) || [], (secondaryItems == null ? void 0 : secondaryItems.indexes) || [] ); }, [items, secondaryItems]); const getRef = (0, import_react8.useCallback)( (index) => { const primaryRef = getPrimaryRef(index); const secondaryRef = getSecondaryRef(index); if (primaryRef == null ? void 0 : primaryRef.current) { return primaryRef; } if (secondaryRef == null ? void 0 : secondaryRef.current) { return secondaryRef; } return primaryRef || secondaryRef; }, [getPrimaryRef, getSecondaryRef] ); const getRefOrError = (0, import_react8.useCallback)( (index, requireNonNull) => { const ref = getRef(index); if (ref && !requireNonNull) { return ref; } if ((ref == null ? void 0 : ref.current) !== null && requireNonNull) { return ref; } throw new ProceduralScrollerError("Could not find ref", { index, ref, requireNonNull }); }, [getRef] ); const updateItems = (0, import_react8.useCallback)( (newScroll, container) => { scroll.current = __spreadValues({}, newScroll); scrollResetting.current = true; setItems( getItems( asNonNegativeReal( getElementSize(container, scrollDirection, { includePadding: false, includeBorder: false, includeMargin: false }) ), rangeScaledSizes, newScroll, getMinItemSize, minIndex, maxIndex ) ); }, [ setItems, scrollDirection, rangeScaledSizes, getMinItemSize, minIndex, maxIndex ] ); const completeScrollToIndex = (0, import_react8.useCallback)(() => { if (scrollToIndexInProgressRef.current) { scrollResetting.current = true; setItemStackPointer((prev) => { return Number(!prev); }); } }, [setItemStackPointer, scrollToIndexInProgressRef]); (0, import_react8.useEffect)(() => { if (scrollToIndexInProgressRef.current) { setSecondaryItems(null); } }, [setSecondaryItems, itemStackPointer]); (0, import_react8.useEffect)(() => { if (secondaryItems === null && scrollToIndexInProgressRef.current) { setScrollToIndexInput(null); scrollToIndexInProgressRef.current = false; scrollToIndexDebounceRef.current = null; } }, [ setScrollToIndexInput, scrollResetting, scrollToIndexInProgressRef, secondaryItems ]); const containerScrollHandler = (0, import_react8.useCallback)( (container, ev, isRetry = false) => { var _a, _b; if (secondaryItems) { if (typeof scrollToIndexDebounceRef.current === "number") clearTimeout(scrollToIndexDebounceRef.current); scrollToIndexDebounceRef.current = setTimeout( completeScrollToIndex, scrollToIndexDebounceDelay ); return; } if (scrollResetting.current || !items) return; const startContentIndex = items.rangePointers["startContent"][0]; const endContentIndex = items.rangePointers["endContent"][1]; const startContentItem = (_a = getRef( items.indexes[startContentIndex] )) == null ? void 0 : _a.current; const endContentItem = (_b = getRef(items.indexes[endContentIndex])) == null ? void 0 : _b.current; if (!startContentItem || !endContentItem) { if (!isRetry) { requestAnimationFrame( () => containerScrollHandler(container, ev, true) ); } return; } const containerScroll = scrollDirection === "horizontal" ? container.scrollLeft : container.scrollTop; if ((typeof minIndex !== "number" || mergedIndexes[0] > minIndex) && containerScroll < getScrollLength("start", container, startContentItem, scrollDirection)) { updateItems( { block: "start", index: items.indexes[startContentIndex] }, container ); } else if ((typeof maxIndex !== "number" || mergedIndexes[mergedIndexes.length - 1] < maxIndex) && containerScroll > getScrollLength("end", container, endContentItem, scrollDirection)) { updateItems( { block: "end", index: items.indexes[endContentIndex] }, container ); } }, [ secondaryItems, items, getRef, scrollDirection, minIndex, mergedIndexes, maxIndex, completeScrollToIndex, updateItems ] ); useScrollHandler({ elementRef: containerRef, handler: containerScrollHandler }); useDeferredScrollReset({ scroll, onScrollReset: () => { scrollResetting.current = false; }, containerRef, items, getRef, suppress: scrollToIndexInProgressRef.current, scrollDirection }); const scrollToIndex = (0, import_react8.useCallback)( (input) => { const container = containerRef == null ? void 0 : containerRef.current; if (!container) { throw new ProceduralScrollerError("Could not find container", { container }); } let targetIndex = asInteger(input.index); if (typeof minIndex === "number") { targetIndex = asInteger(Math.max(targetIndex, minIndex)); } if (typeof maxIndex === "number") { targetIndex = asInteger(Math.min(targetIndex, maxIndex)); } const targetScroll = asScroll({ block: input.block, index: targetIndex }); scroll.current = targetScroll; scrollResetting.current = true; scrollToIndexInProgressRef.current = true; setScrollToIndexInput(__spreadValues(__spreadValues({}, input), targetScroll)); setSecondaryItems( getItems( asNonNegativeReal( getElementSize(container, scrollDirection, { includePadding: false, includeBorder: false, includeMargin: false }) ), rangeScaledSizes, targetScroll, getMinItemSize, minIndex, maxIndex ) ); }, [ getMinItemSize, maxIndex, minIndex, rangeScaledSizes, scrollDirection, setSecondaryItems ] ); (0, import_react8.useEffect)(() => { if (!secondaryItems || !scrollToIndexInput) return; const itemRef = getRefOrError(scrollToIndexInput.index, true); const item = itemRef == null ? void 0 : itemRef.current; const container = containerRef == null ? void 0 : containerRef.current; if (!item || !container) { throw new ProceduralScrollerError( `Tried to scroll to index = ${scrollToIndexInput.index} but the element/container has no mounted ref`, { container, item } ); } const scrollPos = getScrollLength( scrollToIndexInput.block, container, item, scrollDirection ); container.scrollTo({ behavior: scrollToIndexInput.behavior || "auto", [scrollDirection === "horizontal" ? "left" : "top"]: scrollPos }); scrollToIndexDebounceRef.current = setTimeout( completeScrollToIndex, scrollToIndexDebounceDelay ); }, [ getRefOrError, secondaryItems, scrollToIndexInput, maxIndex, minIndex, completeScrollToIndex, scrollDirection ]); const itemsResult = (0, import_react8.useMemo)(() => { if (items) { return mergedIndexes.map((index) => { return { index, ref: getRefOrError(index, false) }; }); } else { return null; } }, [getRefOrError, items, mergedIndexes]); useItemSizeCheck({ items: itemsResult, getMinItemSize, scrollDirection, enabled: (validateLayouts == null ? void 0 : validateLayouts.items) !== false }); useUnboundedHeightCheck({ items: itemsResult, containerRef, scrollDirection, enabled: (validateLayouts == null ? void 0 : validateLayouts.container) !== false }); return { scrollToIndex, container: { ref: containerRef }, items: itemsResult }; }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { useProceduralScroller }); //# sourceMappingURL=index.cjs.map