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
JavaScript
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