@af-utils/react-virtual-headless
Version:
React components for rendering large scrollable data
376 lines (370 loc) • 14.3 kB
JavaScript
import { useRef as t, useEffect as s, useMemo as i } from "react";
import { useSyncExternalStore as e } from "use-sync-external-store/shim";
const h = (t, s, i) => {
const e = [];
let {from: h} = t;
const {to: o} = t;
if (i) for (let i = t.getOffset(h); o > h; ) e.push(s(h, i)), i += t.getSize(h++); else for (;o > h; ) e.push(s(h++, 0));
return e;
}, o = 2147483647, r = [ 0 /* Event.RANGE */ , 1 /* Event.SCROLL_SIZE */ , 2 /* Event.SIZES */ ], n = Uint32Array, l = class {
constructor(t) {}
observe() {}
unobserve() {}
disconnect() {}
}, c = t => t(), a = (t, s) => {
if (!t) throw Error(s);
}, u = t => t instanceof Element, f = (t, s) => {
if (u(t)) {
const i = new l(s);
return i.observe(t), () => i.disconnect();
}
// resizeObserver has required 1st call
return s(), t.addEventListener("resize", s), () => t.removeEventListener("resize", s);
}, _ = (t, s, i) => {
const e = new n(s);
return e.set(t), e.fill(i, t.length), e;
}, m = t => {
const s = t.length + 1, i = new n(s);
i.set(t, 1);
for (let t, e = 1; s > e; e++) t = e + (e & -e), s > t && (i[t] += i[e]);
return i;
}, d = (t, s, i, e) => {
for (;e > s; s += s & -s) t[s] += i;
}, S = (t, s, i) => {
for (;i > s; s += s & -s) ;
return Math.min(s, t.length);
}, z = (t, s) => t.getBoundingClientRect()[s], p = (t, s, i, e) => s && t && t !== s ? t[i] + Math.round(z(s, e) - (u(t) ? z(t, e) : 0)) : 0, b = new Set, v = {
t: 0,
i() {
this.t++;
},
h() {
0 == --this.t && (b.forEach(c), b.clear());
},
o: t => b.add(t)
}, x = {
box: "border-box"
}, y = {
passive: !0
}, T = new n(0), g = [ "offsetHeight" /* ScrollElementSizeKey.ELEMENT_VERTICAL */ , "offsetWidth" /* ScrollElementSizeKey.ELEMENT_HORIZONTAL */ , "innerHeight" /* ScrollElementSizeKey.WINDOW_VERTICAL */ , "innerWidth" /* ScrollElementSizeKey.WINDOW_HORIZONTAL */ ], E = [ "scrollTop" /* ScrollKey.ELEMENT_VERTICAL */ , "scrollLeft" /* ScrollKey.ELEMENT_HORIZONTAL */ , "scrollY" /* ScrollKey.WINDOW_VERTICAL */ , "scrollX" /* ScrollKey.WINDOW_HORIZONTAL */ ], w = [ "blockSize" /* ResizeObserverSizeKey.VERTICAL */ , "inlineSize" /* ResizeObserverSizeKey.HORIZONTAL */ ], M = [ "top" /* ScrollToKey.VERTICAL */ , "left" /* ScrollToKey.HORIZONTAL */ ], O = (t, s) => Math.round(t[0][s]);
class k {
l=g[0];
u=E[0];
_=w[0];
m=M[0];
/*
When using window scroll mode, some blocks may go before & after virtual container.
[ ---- window start ---- ] |.|
Some header |s|
Another header |c|
<Virtual> |r|
item 1 [o]
item 2 [l]
item 3 [l]
... [b]
</Virtual> |a|
Some footer |r|
[ ---- window end ---- ] |.|
Actually any div may be used instead of window, but offset should be calculated.
*/
/* It is more useful to store scrollPos - scrollElementOffset in one variable for future calculations */
S=0;
p=0;
v=0;
T=0;
M=0;
O=0;
k=0;
C=3;
I=40;
R=null;
F=null;
K=T;
H=T;
/**
* Most significant bit of this._itemCount;
* caching it to avoid Math.clz32 calculations on every getIndex call
*/
P=0;
/** Scroll direction */
horizontal=!1;
/** Sum of all item sizes */
scrollSize=0;
/** Items range start */
from=0;
/** Items range end */
to=0;
/** Hash of item sizes. Changed when at least one visible item is resized. */
sizesHash=0;
W=new Map;
B=new Map;
/* header and footer; lengths are hardcoded */
L=[ null, null ];
q=[ 0, 0 ];
A=new l((t => {
let s = 0, i = 0, e = 0;
for (const {target: h, borderBoxSize: o} of t) s = this.L.indexOf(h), -1 !== s && (i = O(o, this._) - this.q[s],
i && (this.q[s] += i, e += i));
this.G(e);
}));
U=new l((t => {
let s = 0, i = !1;
const e = /*@__NOINLINE__*/ S(this.H, this.from + 1, this.to);
/*
TODO: check perf of borderBoxSize vs offsetWidth/offsetHeight
*/ for (const {target: h, borderBoxSize: o} of t) {
// cannot be undefined, because element is being added to this map before getting into ResizeObserver
const t = this.W.get(h);
/*
ResizeObserver may give us elements, which are not in visible range => will be unmounted soon.
Should not take them into account.
This is done for performance + updateItemHeight hack would not work without it
*/ if (e > t) {
const h = O(o, this._) - this.K[t];
h && (i = !0, this.K[t] += h, s += h, d(this.H, t + 1, h, e));
}
}
i && (v.i(), 0 !== s && (d(this.H, e, s, this.H.length), this.v += s, this.scrollSize += s,
this.X(1 /* Event.SCROLL_SIZE */), 0 > s &&
/*
If visible item sizes reduced - holes may appear, so rerender is a must.
No holes possible if item sizes increased => no need to rerender.
*/
this.Y())
/*
Modulo is used to prevent sizesHash from growing too much.
Using bitwise hack to optimize modulo.
5 % 2 === 5 & 1 && 9 % 4 === 9 & 3
*/ , this.sizesHash = this.sizesHash + 1 & 2147483647, this.X(2 /* Event.SIZES */),
v.h());
}));
$=r.map((() => []));
/**
* Update property names for resize events, dimensions and scroll position extraction
*
* @remarks
*
* `window.resize` event must be used for window scroller, `ResizeObserver` must be used in other cases.
* `offsetWidth` is used as item size in horizontal mode, `offsetHeight` - in vertical.
*/
j() {
const t = this.horizontal ? 1 : 0, s = t + 2 * (u(this.R) ? 0 : 1);
this.l = g[s], this.u = E[s], this._ = w[t], this.m = M[t];
}
D=() => {
const t = (s =
// casting type here because this stuff is used only as scrollElement resize event handler
this.R, i = this.l, e = this.T, s[i] - e);
var s, i, e;
t !== this.O && (this.O = t, this.Y());
};
G(t) {
t && (v.i(), this.T += t, this.O -= t, this.scrollSize += t, this.X(1 /* Event.SCROLL_SIZE */),
this.Y(), v.h());
}
J=() => {};
constructor(t) {
t && (this.horizontal = !!t.horizontal,
// stickyOffset is included;
this.p = t.estimatedScrollElementOffset || 0, this.O = t.estimatedWidgetSize ?? 200,
this.set(t));
}
/**
* Subscribe to model events
*
* @param callBack - event to be triggered
* @param events - events to subscribe
*
* @returns unsubscribe function
*/ on(t, s) {
return s.forEach((s => this.$[s].push(t))), () => s.forEach((s => this.$[s].splice(this.$[s].indexOf(t), 1)));
}
/**
* Call all `event` subscribers
* @param event - event to emit
*/ X(t) {
this.$[t].forEach(0 === v.t ? c : v.o);
}
/**
* Get item index by pixel offset
*
* @param offset - pixel offset
* @returns item index
*/ getIndex(t) {
if (0 >= t) return 0;
if (t >= this.v) return this.M - 1;
let s = 0;
for (let i = this.P, e = 0; i > 0; i >>= 1) e = s + i, e <= this.M && t > this.H[e] && (s = e,
t -= this.H[e]);
return s;
}
/**
* Get pixel offset by item index
*
* @param index - item index
* @returns pixel offset
*/ getOffset(t) {
"production" !== process.env.NODE_ENV && a(t <= this.M, "index must not be > itemCount");
let s = 0;
for (;t > 0; t -= t & -t) s += this.H[t];
return s;
}
/**
* @param itemIndex - item index
* @returns last cached item size
*/ getSize(t) {
return "production" !== process.env.NODE_ENV && a(t < this.K.length, "itemIndex must be < itemCount in getSize"),
this.K[t];
}
/**
* Get snapshot of current scroll position.
*
* @remarks
*
* For example `5.3` stands for item at index `5` + `30%` of its size.
* Used to remember scroll position before prepending elements.
*
* @returns visible item index (double number)
*/ get visibleFrom() {
const t = this.N;
return t + (this.S - this.getOffset(t)) / this.K[t];
}
/**
* Synchronize current scroll position with visible range
*/ V=() => {
/*
scrollElement may not be null here.
Math.round, because scrollY/scrollX may be float on Safari
*/
const t = this.S, s = Math.round(this.R[this.u]) - this.p;
s !== t && (this.S = s, s > t ? this.Y() : this.Z());
};
/*
Performs as destructor when null is passed
will ne used as callback, so using =>
*/
setScroller=t => {
t !== this.R && (this.J(), this.R?.removeEventListener("scroll", this.V), this.R = t,
t ? (this.j(), this.J = /*@__NOINLINE__*/ f(t, this.D), t.addEventListener("scroll", this.V, y),
this.tt(), this.V()) : (this.U.disconnect(), this.A.disconnect(), clearTimeout(this.k)));
};
setContainer=t => {
t !== this.F && (this.F = t, this.updateScrollerOffset());
};
tt() {
const t = /*@__NOINLINE__*/ p(this.R, this.F, this.u, this.m) - this.p;
return this.p += t, this.S -= t, t;
}
updateScrollerOffset() {
this.tt() && this.R && this.V();
}
/**
* Start observing size of `element` at `index`. Observing is finished if element is falsy.
* @param index - item index
* @param element - element for item
*/ el(t, s) {
const i = this.B.get(t);
i && (this.B.delete(t), this.W.delete(i), this.U.unobserve(i)), s && (this.W.set(s, t),
this.B.set(t, s), this.U.observe(s, x));
}
st(t, s) {
const i = this.L[t];
i && (this.A.unobserve(i), this.G(-this.q[t]), this.L[t] = null, this.q[t] = 0),
s && this.A.observe(this.L[t] = s, x);
}
/**
* Start observing size of sticky header `element`. Observing is finished if element is falsy.
* @param element - header element
*/ setStickyHeader(t) {
this.st(0, t);
}
/**
* Start observing size of sticky footer `element`. Observing is finished if element is falsy.
* @param element - footer element
*/ setStickyFooter(t) {
this.st(1, t);
}
/**
* Get first visible item index (without overscan)
* @returns first visible item index
*/ get N() {
return this.getIndex(this.S);
}
/**
* Get last visible item index (without overscan)
* @returns last visible item index
*/ get it() {
return this.M && 1 + this.getIndex(this.S + this.O);
}
/**
* Used to update current visible items range when scrolling down/right;
* adds overscan reserve forward to reduce rerenders quantity
*/ Y() {
const {it: t} = this;
t > this.to && (this.to = Math.min(this.M, t + this.C), this.from = this.N, this.X(0 /* Event.RANGE */));
}
/**
* Used to update current visible items range when scrolling up/left;
* adds overscan reserve backward to reduce rerenders quantity
*/ Z() {
const {N: t} = this;
t < this.from && (this.from = Math.max(0, t - this.C), this.to = this.it, this.X(0 /* Event.RANGE */));
}
/**
* Scroll to pixel offset
*
* @param offset - offset to scroll to
* @param smooth - should smooth scroll be used
*/ scrollToOffset(t, s) {
this.R?.scroll({
[this.m]: this.p + t,
behavior: s ? "smooth" : void 0
});
}
et(t, s, i) {
clearTimeout(this.k);
const e = 0 | s, h = Math.min(this.scrollSize - this.O, this.getOffset(e) + Math.round(this.K[e] * (s - e)));
h !== this.S && this.R && --t && (this.scrollToOffset(h, i), this.k = setTimeout((() => this.et(t, s, i)), i ? 512 : 32));
}
/**
* Scroll to item index
*
* @param index - item index to scroll to
* @param smooth - should smooth scroll be used
*/ scrollToIndex(t, s) {
this.et(16, t, s);
}
/**
* Notify model about items quantity change
* @param itemCount - new items quantity
*/ setItemCount(t) {
this.M !== t && (v.i(), a(o >= t, `itemCount must be <= 2147483647. Got: ${t}.`),
this.M = t, this.P = t && 1 << 31 - Math.clz32(t), t > this.K.length && (this.K = /*@__NOINLINE__*/ _(this.K, Math.min(t + 32, o), this.I || 40),
this.H = /*@__NOINLINE__*/ m(this.K)), this.v = this.getOffset(t), this.scrollSize = this.v + this.T,
this.X(1 /* Event.SCROLL_SIZE */), this.to > t && (
// after this range would be 100% updated
this.to = -1), this.Y(), v.h());
}
/**
* Synchronize runtime parameters
* @param runtimeParams - runtime parameters
*/ set(t) {
let {overscanCount: s, itemCount: i, estimatedItemSize: e} = t;
e && (
// must not be falsy, so not checking for undefined here.
this.I = e), void 0 !== s && (this.C = s), void 0 !== i && this.setItemCount(i);
}
}
const C = s => t().current ||= new k(s), I = s, R = t => {
const s = C(t);
return I((() => s.set(t))), s;
}, F = (t, i, e) => s((() => {
if (e) return e(), t.on(e, i);
}), [ t, e, i ]), K = function(t, s) {
void 0 === s && (s = r);
const [h, o] = i((() => [ i => t.on(i, s), () => s.reduce(((s, i) => s + "_" + (0 /* Event.RANGE */ === i ? t.to ** 2 + t.from : 1 /* Event.SCROLL_SIZE */ === i ? t.scrollSize : t.sizesHash)), "") ]), [ t, s ]);
e(h, o, o);
}, H = (t, s) => I((() => (t.setScroller(s), () => t.setScroller(null))), [ t, s ]), P = t => (K(t.model, t.events),
t.children()), W = 0 /* Event.RANGE */ , B = 1 /* Event.SCROLL_SIZE */ , L = 2 /* Event.SIZES */;
export { r as EVT_ALL, W as EVT_RANGE, B as EVT_SCROLL_SIZE, L as EVT_SIZES, P as Subscription, k as VirtualScroller, h as mapVisibleRange, K as useComponentSubscription, H as useScroller, F as useSubscription, R as useVirtual, C as useVirtualModel };
//# sourceMappingURL=index.server.js.map