UNPKG

react-virtuoso

Version:

<img src="https://user-images.githubusercontent.com/13347/101237112-ec4c6000-36de-11eb-936d-4b6b7ec94976.png" width="229" />

1,636 lines 146 kB
import React from "react"; import ReactDOM from "react-dom"; const PUBLISH = 0; const SUBSCRIBE = 1; const RESET = 2; const VALUE = 4; function compose(a, b) { return (arg) => a(b(arg)); } function thrush(arg, proc) { return proc(arg); } function curry2to1(proc, arg1) { return (arg2) => proc(arg1, arg2); } function curry1to0(proc, arg) { return () => proc(arg); } function tap(arg, proc) { proc(arg); return arg; } function tup(...args) { return args; } function call(proc) { proc(); } function always(value) { return () => value; } function joinProc(...procs) { return () => { procs.map(call); }; } function isDefined(arg) { return arg !== void 0; } function noop() { } function subscribe(emitter, subscription) { return emitter(SUBSCRIBE, subscription); } function publish(publisher, value) { publisher(PUBLISH, value); } function reset(emitter) { emitter(RESET); } function getValue(depot) { return depot(VALUE); } function connect(emitter, publisher) { return subscribe(emitter, curry2to1(publisher, PUBLISH)); } function handleNext(emitter, subscription) { const unsub = emitter(SUBSCRIBE, (value) => { unsub(); subscription(value); }); return unsub; } function stream() { const subscriptions = []; return (action, arg) => { switch (action) { case RESET: subscriptions.splice(0, subscriptions.length); return; case SUBSCRIBE: subscriptions.push(arg); return () => { const indexOf = subscriptions.indexOf(arg); if (indexOf > -1) { subscriptions.splice(indexOf, 1); } }; case PUBLISH: subscriptions.slice().forEach((subscription) => { subscription(arg); }); return; default: throw new Error(`unrecognized action ${action}`); } }; } function statefulStream(initial) { let value = initial; const innerSubject = stream(); return (action, arg) => { switch (action) { case SUBSCRIBE: const subscription = arg; subscription(value); break; case PUBLISH: value = arg; break; case VALUE: return value; } return innerSubject(action, arg); }; } function eventHandler(emitter) { let unsub; let currentSubscription; const cleanup = () => unsub && unsub(); return function(action, subscription) { switch (action) { case SUBSCRIBE: if (subscription) { if (currentSubscription === subscription) { return; } cleanup(); currentSubscription = subscription; unsub = subscribe(emitter, subscription); return unsub; } else { cleanup(); return noop; } case RESET: cleanup(); currentSubscription = null; return; default: throw new Error(`unrecognized action ${action}`); } }; } function streamFromEmitter(emitter) { return tap(stream(), (stream2) => connect(emitter, stream2)); } function statefulStreamFromEmitter(emitter, initial) { return tap(statefulStream(initial), (stream2) => connect(emitter, stream2)); } function combineOperators(...operators) { return (subscriber) => { return operators.reduceRight(thrush, subscriber); }; } function pipe(source, ...operators) { const project = combineOperators(...operators); return (action, subscription) => { switch (action) { case SUBSCRIBE: return subscribe(source, project(subscription)); case RESET: reset(source); return; } }; } function defaultComparator(previous, next) { return previous === next; } function distinctUntilChanged(comparator = defaultComparator) { let current; return (done) => (next) => { if (!comparator(current, next)) { current = next; done(next); } }; } function filter(predicate) { return (done) => (value) => { predicate(value) && done(value); }; } function map(project) { return (done) => compose(done, project); } function mapTo(value) { return (done) => () => done(value); } function scan(scanner, initial) { return (done) => (value) => done(initial = scanner(initial, value)); } function skip(times) { return (done) => (value) => { times > 0 ? times-- : done(value); }; } function throttleTime(interval) { let currentValue = null; let timeout; return (done) => (value) => { currentValue = value; if (timeout) { return; } timeout = setTimeout(() => { timeout = void 0; done(currentValue); }, interval); }; } function debounceTime(interval) { let currentValue; let timeout; return (done) => (value) => { currentValue = value; if (timeout) { clearTimeout(timeout); } timeout = setTimeout(() => { done(currentValue); }, interval); }; } function withLatestFrom(...sources) { const values = new Array(sources.length); let called = 0; let pendingCall = null; const allCalled = Math.pow(2, sources.length) - 1; sources.forEach((source, index) => { const bit = Math.pow(2, index); subscribe(source, (value) => { const prevCalled = called; called = called | bit; values[index] = value; if (prevCalled !== allCalled && called === allCalled && pendingCall) { pendingCall(); pendingCall = null; } }); }); return (done) => (value) => { const call2 = () => done([value].concat(values)); if (called === allCalled) { call2(); } else { pendingCall = call2; } }; } function merge(...sources) { return function(action, subscription) { switch (action) { case SUBSCRIBE: return joinProc(...sources.map((source) => subscribe(source, subscription))); case RESET: return; default: throw new Error(`unrecognized action ${action}`); } }; } function duc(source, comparator = defaultComparator) { return pipe(source, distinctUntilChanged(comparator)); } function combineLatest(...emitters) { const innerSubject = stream(); const values = new Array(emitters.length); let called = 0; const allCalled = Math.pow(2, emitters.length) - 1; emitters.forEach((source, index) => { const bit = Math.pow(2, index); subscribe(source, (value) => { values[index] = value; called = called | bit; if (called === allCalled) { publish(innerSubject, values); } }); }); return function(action, subscription) { switch (action) { case SUBSCRIBE: if (called === allCalled) { subscription(values); } return subscribe(innerSubject, subscription); case RESET: return reset(innerSubject); default: throw new Error(`unrecognized action ${action}`); } }; } function system(constructor, dependencies = [], { singleton } = { singleton: true }) { return { id: id(), constructor, dependencies, singleton }; } const id = () => Symbol(); function init(systemSpec) { const singletons = /* @__PURE__ */ new Map(); const _init = ({ id: id2, constructor, dependencies, singleton }) => { if (singleton && singletons.has(id2)) { return singletons.get(id2); } const system2 = constructor(dependencies.map((e) => _init(e))); if (singleton) { singletons.set(id2, system2); } return system2; }; return _init(systemSpec); } function omit(keys, obj) { const result = {}; const index = {}; let idx = 0; const len = keys.length; while (idx < len) { index[keys[idx]] = 1; idx += 1; } for (const prop in obj) { if (!index.hasOwnProperty(prop)) { result[prop] = obj[prop]; } } return result; } const useIsomorphicLayoutEffect$2 = typeof document !== "undefined" ? React.useLayoutEffect : React.useEffect; function systemToComponent(systemSpec, map2, Root) { const requiredPropNames = Object.keys(map2.required || {}); const optionalPropNames = Object.keys(map2.optional || {}); const methodNames = Object.keys(map2.methods || {}); const eventNames = Object.keys(map2.events || {}); const Context = React.createContext({}); function applyPropsToSystem(system2, props) { if (system2["propsReady"]) { publish(system2["propsReady"], false); } for (const requiredPropName of requiredPropNames) { const stream2 = system2[map2.required[requiredPropName]]; publish(stream2, props[requiredPropName]); } for (const optionalPropName of optionalPropNames) { if (optionalPropName in props) { const stream2 = system2[map2.optional[optionalPropName]]; publish(stream2, props[optionalPropName]); } } if (system2["propsReady"]) { publish(system2["propsReady"], true); } } function buildMethods(system2) { return methodNames.reduce((acc, methodName) => { acc[methodName] = (value) => { const stream2 = system2[map2.methods[methodName]]; publish(stream2, value); }; return acc; }, {}); } function buildEventHandlers(system2) { return eventNames.reduce((handlers, eventName) => { handlers[eventName] = eventHandler(system2[map2.events[eventName]]); return handlers; }, {}); } const Component = React.forwardRef((propsWithChildren, ref) => { const { children, ...props } = propsWithChildren; const [system2] = React.useState(() => { return tap(init(systemSpec), (system22) => applyPropsToSystem(system22, props)); }); const [handlers] = React.useState(curry1to0(buildEventHandlers, system2)); useIsomorphicLayoutEffect$2(() => { for (const eventName of eventNames) { if (eventName in props) { subscribe(handlers[eventName], props[eventName]); } } return () => { Object.values(handlers).map(reset); }; }, [props, handlers, system2]); useIsomorphicLayoutEffect$2(() => { applyPropsToSystem(system2, props); }); React.useImperativeHandle(ref, always(buildMethods(system2))); return React.createElement( Context.Provider, { value: system2 }, Root ? React.createElement( Root, omit([...requiredPropNames, ...optionalPropNames, ...eventNames], props), children ) : children ); }); const usePublisher2 = (key) => { return React.useCallback(curry2to1(publish, React.useContext(Context)[key]), [key]); }; const useEmitterValue18 = (key) => { const system2 = React.useContext(Context); const source = system2[key]; const cb = React.useCallback( (c) => { return subscribe(source, c); }, [source] ); return React.useSyncExternalStore( cb, () => getValue(source), () => getValue(source) ); }; const useEmitterValueLegacy = (key) => { const system2 = React.useContext(Context); const source = system2[key]; const [value, setValue] = React.useState(curry1to0(getValue, source)); useIsomorphicLayoutEffect$2( () => subscribe(source, (next) => { if (next !== value) { setValue(always(next)); } }), [source, value] ); return value; }; const useEmitterValue2 = React.version.startsWith("18") ? useEmitterValue18 : useEmitterValueLegacy; const useEmitter2 = (key, callback) => { const context = React.useContext(Context); const source = context[key]; useIsomorphicLayoutEffect$2(() => subscribe(source, callback), [callback, source]); }; return { Component, usePublisher: usePublisher2, useEmitterValue: useEmitterValue2, useEmitter: useEmitter2 }; } const useIsomorphicLayoutEffect = typeof document !== "undefined" ? React.useLayoutEffect : React.useEffect; const useIsomorphicLayoutEffect$1 = useIsomorphicLayoutEffect; var LogLevel = /* @__PURE__ */ ((LogLevel2) => { LogLevel2[LogLevel2["DEBUG"] = 0] = "DEBUG"; LogLevel2[LogLevel2["INFO"] = 1] = "INFO"; LogLevel2[LogLevel2["WARN"] = 2] = "WARN"; LogLevel2[LogLevel2["ERROR"] = 3] = "ERROR"; return LogLevel2; })(LogLevel || {}); const CONSOLE_METHOD_MAP = { [ 0 /* DEBUG */ ]: "debug", [ 1 /* INFO */ ]: "log", [ 2 /* WARN */ ]: "warn", [ 3 /* ERROR */ ]: "error" }; const getGlobalThis = () => typeof globalThis === "undefined" ? window : globalThis; const loggerSystem = system( () => { const logLevel = statefulStream( 3 /* ERROR */ ); const log = statefulStream((label, message, level = 1) => { var _a; const currentLevel = (_a = getGlobalThis()["VIRTUOSO_LOG_LEVEL"]) != null ? _a : getValue(logLevel); if (level >= currentLevel) { console[CONSOLE_METHOD_MAP[level]]( "%creact-virtuoso: %c%s %o", "color: #0253b3; font-weight: bold", "color: initial", label, message ); } }); return { log, logLevel }; }, [], { singleton: true } ); function useSizeWithElRef(callback, enabled = true) { const ref = React.useRef(null); let callbackRef = (_el) => { }; if (typeof ResizeObserver !== "undefined") { const observer = React.useMemo(() => { return new ResizeObserver((entries) => { requestAnimationFrame(() => { const element = entries[0].target; if (element.offsetParent !== null) { callback(element); } }); }); }, [callback]); callbackRef = (elRef) => { if (elRef && enabled) { observer.observe(elRef); ref.current = elRef; } else { if (ref.current) { observer.unobserve(ref.current); } ref.current = null; } }; } return { ref, callbackRef }; } function useSize(callback, enabled = true) { return useSizeWithElRef(callback, enabled).callbackRef; } function useChangedListContentsSizes(callback, itemSize, enabled, scrollContainerStateCallback, log, gap, customScrollParent) { const memoedCallback = React.useCallback( (el) => { const ranges = getChangedChildSizes(el.children, itemSize, "offsetHeight", log); let scrollableElement = el.parentElement; while (!scrollableElement.dataset["virtuosoScroller"]) { scrollableElement = scrollableElement.parentElement; } const windowScrolling = scrollableElement.lastElementChild.dataset["viewportType"] === "window"; const scrollTop = customScrollParent ? customScrollParent.scrollTop : windowScrolling ? window.pageYOffset || document.documentElement.scrollTop : scrollableElement.scrollTop; const scrollHeight = customScrollParent ? customScrollParent.scrollHeight : windowScrolling ? document.documentElement.scrollHeight : scrollableElement.scrollHeight; const viewportHeight = customScrollParent ? customScrollParent.offsetHeight : windowScrolling ? window.innerHeight : scrollableElement.offsetHeight; scrollContainerStateCallback({ scrollTop: Math.max(scrollTop, 0), scrollHeight, viewportHeight }); gap == null ? void 0 : gap(resolveGapValue$1("row-gap", getComputedStyle(el).rowGap, log)); if (ranges !== null) { callback(ranges); } }, [callback, itemSize, log, gap, customScrollParent, scrollContainerStateCallback] ); return useSizeWithElRef(memoedCallback, enabled); } function getChangedChildSizes(children, itemSize, field, log) { const length = children.length; if (length === 0) { return null; } const results = []; for (let i = 0; i < length; i++) { const child = children.item(i); if (!child || child.dataset.index === void 0) { continue; } const index = parseInt(child.dataset.index); const knownSize = parseFloat(child.dataset.knownSize); const size = itemSize(child, field); if (size === 0) { log("Zero-sized element, this should not happen", { child }, LogLevel.ERROR); } if (size === knownSize) { continue; } const lastResult = results[results.length - 1]; if (results.length === 0 || lastResult.size !== size || lastResult.endIndex !== index - 1) { results.push({ startIndex: index, endIndex: index, size }); } else { results[results.length - 1].endIndex++; } } return results; } function resolveGapValue$1(property, value, log) { if (value !== "normal" && !(value == null ? void 0 : value.endsWith("px"))) { log(`${property} was not resolved to pixel value correctly`, value, LogLevel.WARN); } if (value === "normal") { return 0; } return parseInt(value != null ? value : "0", 10); } function correctItemSize(el, dimension) { return Math.round(el.getBoundingClientRect()[dimension]); } function approximatelyEqual(num1, num2) { return Math.abs(num1 - num2) < 1.01; } function useScrollTop(scrollContainerStateCallback, smoothScrollTargetReached, scrollerElement, scrollerRefCallback = noop, customScrollParent) { const scrollerRef = React.useRef(null); const scrollTopTarget = React.useRef(null); const timeoutRef = React.useRef(null); const handler = React.useCallback( (ev) => { const el = ev.target; const windowScroll = el === window || el === document; const scrollTop = windowScroll ? window.pageYOffset || document.documentElement.scrollTop : el.scrollTop; const scrollHeight = windowScroll ? document.documentElement.scrollHeight : el.scrollHeight; const viewportHeight = windowScroll ? window.innerHeight : el.offsetHeight; const call2 = () => { scrollContainerStateCallback({ scrollTop: Math.max(scrollTop, 0), scrollHeight, viewportHeight }); }; if (ev.suppressFlushSync) { call2(); } else { ReactDOM.flushSync(call2); } if (scrollTopTarget.current !== null) { if (scrollTop === scrollTopTarget.current || scrollTop <= 0 || scrollTop === scrollHeight - viewportHeight) { scrollTopTarget.current = null; smoothScrollTargetReached(true); if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } } } }, [scrollContainerStateCallback, smoothScrollTargetReached] ); React.useEffect(() => { const localRef = customScrollParent ? customScrollParent : scrollerRef.current; scrollerRefCallback(customScrollParent ? customScrollParent : scrollerRef.current); handler({ target: localRef, suppressFlushSync: true }); localRef.addEventListener("scroll", handler, { passive: true }); return () => { scrollerRefCallback(null); localRef.removeEventListener("scroll", handler); }; }, [scrollerRef, handler, scrollerElement, scrollerRefCallback, customScrollParent]); function scrollToCallback(location) { const scrollerElement2 = scrollerRef.current; if (!scrollerElement2 || "offsetHeight" in scrollerElement2 && scrollerElement2.offsetHeight === 0) { return; } const isSmooth = location.behavior === "smooth"; let offsetHeight; let scrollHeight; let scrollTop; if (scrollerElement2 === window) { scrollHeight = Math.max(correctItemSize(document.documentElement, "height"), document.documentElement.scrollHeight); offsetHeight = window.innerHeight; scrollTop = document.documentElement.scrollTop; } else { scrollHeight = scrollerElement2.scrollHeight; offsetHeight = correctItemSize(scrollerElement2, "height"); scrollTop = scrollerElement2.scrollTop; } const maxScrollTop = scrollHeight - offsetHeight; location.top = Math.ceil(Math.max(Math.min(maxScrollTop, location.top), 0)); if (approximatelyEqual(offsetHeight, scrollHeight) || location.top === scrollTop) { scrollContainerStateCallback({ scrollTop, scrollHeight, viewportHeight: offsetHeight }); if (isSmooth) { smoothScrollTargetReached(true); } return; } if (isSmooth) { scrollTopTarget.current = location.top; if (timeoutRef.current) { clearTimeout(timeoutRef.current); } timeoutRef.current = setTimeout(() => { timeoutRef.current = null; scrollTopTarget.current = null; smoothScrollTargetReached(true); }, 1e3); } else { scrollTopTarget.current = null; } scrollerElement2.scrollTo(location); } function scrollByCallback(location) { scrollerRef.current.scrollBy(location); } return { scrollerRef, scrollByCallback, scrollToCallback }; } const domIOSystem = system( () => { const scrollContainerState = stream(); const scrollTop = stream(); const deviation = statefulStream(0); const smoothScrollTargetReached = stream(); const statefulScrollTop = statefulStream(0); const viewportHeight = stream(); const scrollHeight = stream(); const headerHeight = statefulStream(0); const fixedHeaderHeight = statefulStream(0); const fixedFooterHeight = statefulStream(0); const footerHeight = statefulStream(0); const scrollTo = stream(); const scrollBy = stream(); const scrollingInProgress = statefulStream(false); connect( pipe( scrollContainerState, map(({ scrollTop: scrollTop2 }) => scrollTop2) ), scrollTop ); connect( pipe( scrollContainerState, map(({ scrollHeight: scrollHeight2 }) => scrollHeight2) ), scrollHeight ); connect(scrollTop, statefulScrollTop); return { // input scrollContainerState, scrollTop, viewportHeight, headerHeight, fixedHeaderHeight, fixedFooterHeight, footerHeight, scrollHeight, smoothScrollTargetReached, // signals scrollTo, scrollBy, // state statefulScrollTop, deviation, scrollingInProgress }; }, [], { singleton: true } ); const NIL_NODE = { lvl: 0 }; function newAANode(k, v, lvl, l = NIL_NODE, r = NIL_NODE) { return { k, v, lvl, l, r }; } function empty(node) { return node === NIL_NODE; } function newTree() { return NIL_NODE; } function remove(node, key) { if (empty(node)) return NIL_NODE; const { k, l, r } = node; if (key === k) { if (empty(l)) { return r; } else if (empty(r)) { return l; } else { const [lastKey, lastValue] = last(l); return adjust(clone(node, { k: lastKey, v: lastValue, l: deleteLast(l) })); } } else if (key < k) { return adjust(clone(node, { l: remove(l, key) })); } else { return adjust(clone(node, { r: remove(r, key) })); } } function find(node, key) { if (empty(node)) { return; } if (key === node.k) { return node.v; } else if (key < node.k) { return find(node.l, key); } else { return find(node.r, key); } } function findMaxKeyValue(node, value, field = "k") { if (empty(node)) { return [-Infinity, void 0]; } if (Number(node[field]) === value) { return [node.k, node.v]; } if (Number(node[field]) < value) { const r = findMaxKeyValue(node.r, value, field); if (r[0] === -Infinity) { return [node.k, node.v]; } else { return r; } } return findMaxKeyValue(node.l, value, field); } function insert(node, k, v) { if (empty(node)) { return newAANode(k, v, 1); } if (k === node.k) { return clone(node, { k, v }); } else if (k < node.k) { return rebalance(clone(node, { l: insert(node.l, k, v) })); } else { return rebalance(clone(node, { r: insert(node.r, k, v) })); } } function walkWithin(node, start, end) { if (empty(node)) { return []; } const { k, v, l, r } = node; let result = []; if (k > start) { result = result.concat(walkWithin(l, start, end)); } if (k >= start && k <= end) { result.push({ k, v }); } if (k <= end) { result = result.concat(walkWithin(r, start, end)); } return result; } function walk(node) { if (empty(node)) { return []; } return [...walk(node.l), { k: node.k, v: node.v }, ...walk(node.r)]; } function last(node) { return empty(node.r) ? [node.k, node.v] : last(node.r); } function deleteLast(node) { return empty(node.r) ? node.l : adjust(clone(node, { r: deleteLast(node.r) })); } function clone(node, args) { return newAANode( args.k !== void 0 ? args.k : node.k, args.v !== void 0 ? args.v : node.v, args.lvl !== void 0 ? args.lvl : node.lvl, args.l !== void 0 ? args.l : node.l, args.r !== void 0 ? args.r : node.r ); } function isSingle(node) { return empty(node) || node.lvl > node.r.lvl; } function rebalance(node) { return split(skew(node)); } function adjust(node) { const { l, r, lvl } = node; if (r.lvl >= lvl - 1 && l.lvl >= lvl - 1) { return node; } else if (lvl > r.lvl + 1) { if (isSingle(l)) { return skew(clone(node, { lvl: lvl - 1 })); } else { if (!empty(l) && !empty(l.r)) { return clone(l.r, { l: clone(l, { r: l.r.l }), r: clone(node, { l: l.r.r, lvl: lvl - 1 }), lvl }); } else { throw new Error("Unexpected empty nodes"); } } } else { if (isSingle(node)) { return split(clone(node, { lvl: lvl - 1 })); } else { if (!empty(r) && !empty(r.l)) { const rl = r.l; const rlvl = isSingle(rl) ? r.lvl - 1 : r.lvl; return clone(rl, { l: clone(node, { r: rl.l, lvl: lvl - 1 }), r: split(clone(r, { l: rl.r, lvl: rlvl })), lvl: rl.lvl + 1 }); } else { throw new Error("Unexpected empty nodes"); } } } } function rangesWithin(node, startIndex, endIndex) { if (empty(node)) { return []; } const adjustedStart = findMaxKeyValue(node, startIndex)[0]; return toRanges(walkWithin(node, adjustedStart, endIndex)); } function arrayToRanges(items, parser) { const length = items.length; if (length === 0) { return []; } let { index: start, value } = parser(items[0]); const result = []; for (let i = 1; i < length; i++) { const { index: nextIndex, value: nextValue } = parser(items[i]); result.push({ start, end: nextIndex - 1, value }); start = nextIndex; value = nextValue; } result.push({ start, end: Infinity, value }); return result; } function toRanges(nodes) { return arrayToRanges(nodes, ({ k: index, v: value }) => ({ index, value })); } function split(node) { const { r, lvl } = node; return !empty(r) && !empty(r.r) && r.lvl === lvl && r.r.lvl === lvl ? clone(r, { l: clone(node, { r: r.l }), lvl: lvl + 1 }) : node; } function skew(node) { const { l } = node; return !empty(l) && l.lvl === node.lvl ? clone(l, { r: clone(node, { l: l.r }) }) : node; } function findIndexOfClosestSmallerOrEqual(items, value, comparator, start = 0) { let end = items.length - 1; while (start <= end) { const index = Math.floor((start + end) / 2); const item = items[index]; const match = comparator(item, value); if (match === 0) { return index; } if (match === -1) { if (end - start < 2) { return index - 1; } end = index - 1; } else { if (end === start) { return index; } start = index + 1; } } throw new Error(`Failed binary finding record in array - ${items.join(",")}, searched for ${value}`); } function findClosestSmallerOrEqual(items, value, comparator) { return items[findIndexOfClosestSmallerOrEqual(items, value, comparator)]; } function findRange(items, startValue, endValue, comparator) { const startIndex = findIndexOfClosestSmallerOrEqual(items, startValue, comparator); const endIndex = findIndexOfClosestSmallerOrEqual(items, endValue, comparator, startIndex); return items.slice(startIndex, endIndex + 1); } const recalcSystem = system( () => { const recalcInProgress = statefulStream(false); return { recalcInProgress }; }, [], { singleton: true } ); function rangeIncludes(refRange) { const { size, startIndex, endIndex } = refRange; return (range) => { return range.start === startIndex && (range.end === endIndex || range.end === Infinity) && range.value === size; }; } function affectedGroupCount(offset, groupIndices) { let recognizedOffsetItems = 0; let groupIndex = 0; while (recognizedOffsetItems < offset) { recognizedOffsetItems += groupIndices[groupIndex + 1] - groupIndices[groupIndex] - 1; groupIndex++; } const offsetIsExact = recognizedOffsetItems === offset; return groupIndex - (offsetIsExact ? 0 : 1); } function insertRanges(sizeTree, ranges) { let syncStart = empty(sizeTree) ? 0 : Infinity; for (const range of ranges) { const { size, startIndex, endIndex } = range; syncStart = Math.min(syncStart, startIndex); if (empty(sizeTree)) { sizeTree = insert(sizeTree, 0, size); continue; } const overlappingRanges = rangesWithin(sizeTree, startIndex - 1, endIndex + 1); if (overlappingRanges.some(rangeIncludes(range))) { continue; } let firstPassDone = false; let shouldInsert = false; for (const { start: rangeStart, end: rangeEnd, value: rangeValue } of overlappingRanges) { if (!firstPassDone) { shouldInsert = rangeValue !== size; firstPassDone = true; } else { if (endIndex >= rangeStart || size === rangeValue) { sizeTree = remove(sizeTree, rangeStart); } } if (rangeEnd > endIndex && endIndex >= rangeStart) { if (rangeValue !== size) { sizeTree = insert(sizeTree, endIndex + 1, rangeValue); } } } if (shouldInsert) { sizeTree = insert(sizeTree, startIndex, size); } } return [sizeTree, syncStart]; } function initialSizeState() { return { offsetTree: [], sizeTree: newTree(), groupOffsetTree: newTree(), lastIndex: 0, lastOffset: 0, lastSize: 0, groupIndices: [] }; } function indexComparator({ index: itemIndex }, index) { return index === itemIndex ? 0 : index < itemIndex ? -1 : 1; } function offsetComparator({ offset: itemOffset }, offset) { return offset === itemOffset ? 0 : offset < itemOffset ? -1 : 1; } function offsetPointParser(point) { return { index: point.index, value: point }; } function rangesWithinOffsets(tree, startOffset, endOffset, minStartIndex = 0) { if (minStartIndex > 0) { startOffset = Math.max(startOffset, findClosestSmallerOrEqual(tree, minStartIndex, indexComparator).offset); } return arrayToRanges(findRange(tree, startOffset, endOffset, offsetComparator), offsetPointParser); } function createOffsetTree(prevOffsetTree, syncStart, sizeTree, gap) { let offsetTree = prevOffsetTree; let prevIndex = 0; let prevSize = 0; let prevOffset = 0; let startIndex = 0; if (syncStart !== 0) { startIndex = findIndexOfClosestSmallerOrEqual(offsetTree, syncStart - 1, indexComparator); const offsetInfo = offsetTree[startIndex]; prevOffset = offsetInfo.offset; const kv = findMaxKeyValue(sizeTree, syncStart - 1); prevIndex = kv[0]; prevSize = kv[1]; if (offsetTree.length && offsetTree[startIndex].size === findMaxKeyValue(sizeTree, syncStart)[1]) { startIndex -= 1; } offsetTree = offsetTree.slice(0, startIndex + 1); } else { offsetTree = []; } for (const { start: startIndex2, value } of rangesWithin(sizeTree, syncStart, Infinity)) { const indexOffset = startIndex2 - prevIndex; const aOffset = indexOffset * prevSize + prevOffset + indexOffset * gap; offsetTree.push({ offset: aOffset, size: value, index: startIndex2 }); prevIndex = startIndex2; prevOffset = aOffset; prevSize = value; } return { offsetTree, lastIndex: prevIndex, lastOffset: prevOffset, lastSize: prevSize }; } function sizeStateReducer(state, [ranges, groupIndices, log, gap]) { if (ranges.length > 0) { log("received item sizes", ranges, LogLevel.DEBUG); } const sizeTree = state.sizeTree; let newSizeTree = sizeTree; let syncStart = 0; if (groupIndices.length > 0 && empty(sizeTree) && ranges.length === 2) { const groupSize = ranges[0].size; const itemSize = ranges[1].size; newSizeTree = groupIndices.reduce((tree, groupIndex) => { return insert(insert(tree, groupIndex, groupSize), groupIndex + 1, itemSize); }, newSizeTree); } else { [newSizeTree, syncStart] = insertRanges(newSizeTree, ranges); } if (newSizeTree === sizeTree) { return state; } const { offsetTree: newOffsetTree, lastIndex, lastSize, lastOffset } = createOffsetTree(state.offsetTree, syncStart, newSizeTree, gap); return { sizeTree: newSizeTree, offsetTree: newOffsetTree, lastIndex, lastOffset, lastSize, groupOffsetTree: groupIndices.reduce((tree, index) => { return insert(tree, index, offsetOf(index, newOffsetTree, gap)); }, newTree()), groupIndices }; } function offsetOf(index, tree, gap) { if (tree.length === 0) { return 0; } const { offset, index: startIndex, size } = findClosestSmallerOrEqual(tree, index, indexComparator); const itemCount = index - startIndex; const top = size * itemCount + (itemCount - 1) * gap + offset; return top > 0 ? top + gap : top; } function isGroupLocation(location) { return typeof location.groupIndex !== "undefined"; } function originalIndexFromLocation(location, sizes, lastIndex) { if (isGroupLocation(location)) { return sizes.groupIndices[location.groupIndex] + 1; } else { const numericIndex = location.index === "LAST" ? lastIndex : location.index; let result = originalIndexFromItemIndex(numericIndex, sizes); result = Math.max(0, result, Math.min(lastIndex, result)); return result; } } function originalIndexFromItemIndex(itemIndex, sizes) { if (!hasGroups(sizes)) { return itemIndex; } let groupOffset = 0; while (sizes.groupIndices[groupOffset] <= itemIndex + groupOffset) { groupOffset++; } return itemIndex + groupOffset; } function hasGroups(sizes) { return !empty(sizes.groupOffsetTree); } function sizeTreeToRanges(sizeTree) { return walk(sizeTree).map(({ k: startIndex, v: size }, index, sizeArray) => { const nextSize = sizeArray[index + 1]; const endIndex = nextSize ? nextSize.k - 1 : Infinity; return { startIndex, endIndex, size }; }); } const SIZE_MAP = { offsetHeight: "height", offsetWidth: "width" }; const sizeSystem = system( ([{ log }, { recalcInProgress }]) => { const sizeRanges = stream(); const totalCount = stream(); const statefulTotalCount = statefulStreamFromEmitter(totalCount, 0); const unshiftWith = stream(); const shiftWith = stream(); const firstItemIndex = statefulStream(0); const groupIndices = statefulStream([]); const fixedItemSize = statefulStream(void 0); const defaultItemSize = statefulStream(void 0); const itemSize = statefulStream((el, field) => correctItemSize(el, SIZE_MAP[field])); const data = statefulStream(void 0); const gap = statefulStream(0); const initial = initialSizeState(); const sizes = statefulStreamFromEmitter( pipe(sizeRanges, withLatestFrom(groupIndices, log, gap), scan(sizeStateReducer, initial), distinctUntilChanged()), initial ); const prevGroupIndices = statefulStreamFromEmitter( pipe( groupIndices, distinctUntilChanged(), scan((prev, curr) => ({ prev: prev.current, current: curr }), { prev: [], current: [] }), map(({ prev }) => prev) ), [] ); connect( pipe( groupIndices, filter((indexes) => indexes.length > 0), withLatestFrom(sizes, gap), map(([groupIndices2, sizes2, gap2]) => { const groupOffsetTree = groupIndices2.reduce((tree, index, idx) => { return insert(tree, index, offsetOf(index, sizes2.offsetTree, gap2) || idx); }, newTree()); return { ...sizes2, groupIndices: groupIndices2, groupOffsetTree }; }) ), sizes ); connect( pipe( totalCount, withLatestFrom(sizes), filter(([totalCount2, { lastIndex }]) => { return totalCount2 < lastIndex; }), map(([totalCount2, { lastIndex, lastSize }]) => { return [ { startIndex: totalCount2, endIndex: lastIndex, size: lastSize } ]; }) ), sizeRanges ); connect(fixedItemSize, defaultItemSize); const trackItemSizes = statefulStreamFromEmitter( pipe( fixedItemSize, map((size) => size === void 0) ), true ); connect( pipe( defaultItemSize, filter((value) => { return value !== void 0 && empty(getValue(sizes).sizeTree); }), map((size) => [{ startIndex: 0, endIndex: 0, size }]) ), sizeRanges ); const listRefresh = streamFromEmitter( pipe( sizeRanges, withLatestFrom(sizes), scan( ({ sizes: oldSizes }, [_, newSizes]) => { return { changed: newSizes !== oldSizes, sizes: newSizes }; }, { changed: false, sizes: initial } ), map((value) => value.changed) ) ); subscribe( pipe( firstItemIndex, scan( (prev, next) => { return { diff: prev.prev - next, prev: next }; }, { diff: 0, prev: 0 } ), map((val) => val.diff) ), (offset) => { const { groupIndices: groupIndices2 } = getValue(sizes); if (offset > 0) { publish(recalcInProgress, true); publish(unshiftWith, offset + affectedGroupCount(offset, groupIndices2)); } else if (offset < 0) { const prevGroupIndicesValue = getValue(prevGroupIndices); if (prevGroupIndicesValue.length > 0) { offset -= affectedGroupCount(-offset, prevGroupIndicesValue); } publish(shiftWith, offset); } } ); subscribe(pipe(firstItemIndex, withLatestFrom(log)), ([index, log2]) => { if (index < 0) { log2( "`firstItemIndex` prop should not be set to less than zero. If you don't know the total count, just use a very high value", { firstItemIndex }, LogLevel.ERROR ); } }); const beforeUnshiftWith = streamFromEmitter(unshiftWith); connect( pipe( unshiftWith, withLatestFrom(sizes), map(([unshiftWith2, sizes2]) => { const groupedMode = sizes2.groupIndices.length > 0; const initialRanges = []; const defaultSize = sizes2.lastSize; if (groupedMode) { const firstGroupSize = find(sizes2.sizeTree, 0); let prependedGroupItemsCount = 0; let groupIndex = 0; while (prependedGroupItemsCount < unshiftWith2) { const theGroupIndex = sizes2.groupIndices[groupIndex]; const groupItemCount = sizes2.groupIndices.length === groupIndex + 1 ? Infinity : sizes2.groupIndices[groupIndex + 1] - theGroupIndex - 1; initialRanges.push({ startIndex: theGroupIndex, endIndex: theGroupIndex, size: firstGroupSize }); initialRanges.push({ startIndex: theGroupIndex + 1, endIndex: theGroupIndex + 1 + groupItemCount - 1, size: defaultSize }); groupIndex++; prependedGroupItemsCount += groupItemCount + 1; } const sizeTreeKV = walk(sizes2.sizeTree); const firstGroupIsExpanded = prependedGroupItemsCount !== unshiftWith2; if (firstGroupIsExpanded) { sizeTreeKV.shift(); } return sizeTreeKV.reduce( (acc, { k: index, v: size }) => { let ranges = acc.ranges; if (acc.prevSize !== 0) { ranges = [ ...acc.ranges, { startIndex: acc.prevIndex, endIndex: index + unshiftWith2 - 1, size: acc.prevSize } ]; } return { ranges, prevIndex: index + unshiftWith2, prevSize: size }; }, { ranges: initialRanges, prevIndex: unshiftWith2, prevSize: 0 } ).ranges; } return walk(sizes2.sizeTree).reduce( (acc, { k: index, v: size }) => { return { ranges: [...acc.ranges, { startIndex: acc.prevIndex, endIndex: index + unshiftWith2 - 1, size: acc.prevSize }], prevIndex: index + unshiftWith2, prevSize: size }; }, { ranges: [], prevIndex: 0, prevSize: defaultSize } ).ranges; }) ), sizeRanges ); const shiftWithOffset = streamFromEmitter( pipe( shiftWith, withLatestFrom(sizes, gap), map(([shiftWith2, { offsetTree }, gap2]) => { const newFirstItemIndex = -shiftWith2; return offsetOf(newFirstItemIndex, offsetTree, gap2); }) ) ); connect( pipe( shiftWith, withLatestFrom(sizes, gap), map(([shiftWith2, sizes2, gap2]) => { const groupedMode = sizes2.groupIndices.length > 0; if (groupedMode) { if (empty(sizes2.sizeTree)) { return sizes2; } let newSizeTree = newTree(); const prevGroupIndicesValue = getValue(prevGroupIndices); let removedItemsCount = 0; let groupIndex = 0; let groupOffset = 0; while (removedItemsCount < -shiftWith2) { groupOffset = prevGroupIndicesValue[groupIndex]; const groupItemCount = prevGroupIndicesValue[groupIndex + 1] - groupOffset - 1; groupIndex++; removedItemsCount += groupItemCount + 1; } newSizeTree = walk(sizes2.sizeTree).reduce((acc, { k, v }) => { return insert(acc, Math.max(0, k + shiftWith2), v); }, newSizeTree); const aGroupIsShrunk = removedItemsCount !== -shiftWith2; if (aGroupIsShrunk) { const firstGroupSize = find(sizes2.sizeTree, groupOffset); newSizeTree = insert(newSizeTree, 0, firstGroupSize); const nextItemSize = findMaxKeyValue(sizes2.sizeTree, -shiftWith2 + 1)[1]; newSizeTree = insert(newSizeTree, 1, nextItemSize); } return { ...sizes2, sizeTree: newSizeTree, ...createOffsetTree(sizes2.offsetTree, 0, newSizeTree, gap2) }; } else { const newSizeTree = walk(sizes2.sizeTree).reduce((acc, { k, v }) => { return insert(acc, Math.max(0, k + shiftWith2), v); }, newTree()); return { ...sizes2, sizeTree: newSizeTree, ...createOffsetTree(sizes2.offsetTree, 0, newSizeTree, gap2) }; } }) ), sizes ); return { // input data, totalCount, sizeRanges, groupIndices, defaultItemSize, fixedItemSize, unshiftWith, shiftWith, shiftWithOffset, beforeUnshiftWith, firstItemIndex, gap, // output sizes, listRefresh, statefulTotalCount, trackItemSizes, itemSize }; }, tup(loggerSystem, recalcSystem), { singleton: true } ); const SUPPORTS_SCROLL_TO_OPTIONS = typeof document !== "undefined" && "scrollBehavior" in document.documentElement.style; function normalizeIndexLocation(location) { const result = typeof location === "number" ? { index: location } : location; if (!result.align) { result.align = "start"; } if (!result.behavior || !SUPPORTS_SCROLL_TO_OPTIONS) { result.behavior = "auto"; } if (!result.offset) { result.offset = 0; } return result; } const scrollToIndexSystem = system( ([ { sizes, totalCount, listRefresh, gap }, { scrollingInProgress, viewportHeight, scrollTo, smoothScrollTargetReached, headerHeight, footerHeight, fixedHeaderHeight, fixedFooterHeight }, { log } ]) => { const scrollToIndex = stream(); const scrollTargetReached = stream(); const topListHeight = statefulStream(0); let unsubscribeNextListRefresh = null; let cleartTimeoutRef = null; let unsubscribeListRefresh = null; function cleanup() { if (unsubscribeNextListRefresh) { unsubscribeNextListRefresh(); unsubscribeNextListRefresh = null; } if (unsubscribeListRefresh) { unsubscribeListRefresh(); unsubscribeListRefresh = null; } if (cleartTimeoutRef) { clearTimeout(cleartTimeoutRef); cleartTimeoutRef = null; } publish(scrollingInProgress, false); } connect( pipe( scrollToIndex, withLatestFrom(sizes, viewportHeight, totalCount, topListHeight, headerHeight, footerHeight, log), withLatestFrom(gap, fixedHeaderHeight, fixedFooterHeight), map( ([ [location, sizes2, viewportHeight2, totalCount2, topListHeight2, headerHeight2, footerHeight2, log2], gap2, fixedHeaderHeight2, fixedFooterHeight2 ]) => { const normalLocation = normalizeIndexLocation(location); const { align, behavior, offset } = normalLocation; const lastIndex = totalCount2 - 1; const index = originalIndexFromLocation(normalLocation, sizes2, lastIndex); let top = offsetOf(index, sizes2.offsetTree, gap2) + headerHeight2; if (align === "end") { top += fixedHeaderHeight2 + findMaxKeyValue(sizes2.sizeTree, index)[1] - viewportHeight2 + fixedFooterHeight2; if (index === lastIndex) { top += footerHeight2; } } else if (align === "center") { top += (fixedHeaderHeight2 + findMaxKeyValue(sizes2.sizeTree, index)[1] - viewportHeight2 + fixedFooterHeight2) / 2; } else { top -= topListHeight2; } if (offset) { top += offset; } const retry = (listChanged) => { cleanup(); if (listChanged) { log2("retrying to scroll to", { location }, LogLevel.DEBUG); publish(scrollToIndex, location); } else { publish(scrollTargetReached, true); log2("list did not change, scroll successful", {}, LogLevel.DEBUG); } }; cleanup(); if (behavior === "smooth") { let listChanged = false; unsubscribeListRefresh = subscribe(listRefresh, (changed) => { listChanged = listChanged || changed; }); unsubscribeNextListRefresh = handleNext(smoothScrollTargetReached, () => { retry(listChanged); }); } else { unsubscribeNextListRefresh = handleNext(pipe(listRefresh, watchChangesFor(150)), retry); } cleartTimeoutRef = setTimeout(() => { cleanup(); }, 1200); publish(scrollingInProgress, true); log2("scrolling from index to", { index, top, behavior }, LogLevel.DEBUG); return { top, behavior }; } ) ), scrollTo ); return { scrollToIndex, scrollTargetReached, topListHeight }; }, tup(sizeSystem, domIOSystem, loggerSystem), { singleton: true } ); function watchChangesFor(limit) { return (done) => { const timeoutRef = setTimeout(() => { done(false); }, limit); return (value) => { if (value) { done(true); clearTimeout(timeoutRef); } }; }; } const UP = "up"; const DOWN = "down"; const NONE$1 = "none"; const INITIAL_BOTTOM_STATE = { atBottom: false, notAtBottomBecause: "NOT_SHOWING_LAST_ITEM", state: { offsetBottom: 0, scrollTop: 0, viewportHeight: 0, scrollHeight: 0 } }; const DEFAULT_AT_TOP_THRESHOLD = 0; const stateFlagsSystem = system(([{ scrollContainerState, scrollTop, viewportHeight, headerHeight, footerHeight, scrollBy }]) => { const isAtBottom = statefulStream(false); const isAtTop = statefulStream(true); const atBottomStateChange = stream(); const atTopStateChange = stream(); const atBottomThreshold = statefulStream(4); const atTopThreshold = statefulStream(DEFAULT_AT_TOP_THRESHOLD); const isScrolling = statefulStreamFromEmitter( pipe( merge(pipe(duc(scrollTop), skip(1), mapTo(true)), pipe(duc(scrollTop), skip(1), mapTo(false), debounceTime(100))), distinctUntilChanged() ), false ); const isScrollingBy = statefulStreamFromEmitter( pipe(merge(pipe(scrollBy, mapTo(true)), pipe(scrollBy, mapTo(false), debounceTime(200))), distinctUntilChanged()), false ); connect( pipe( combineLatest(d