@af-utils/virtual-core
Advanced tools
Comparing version 0.0.22 to 0.0.23
818
lib/index.js
@@ -1,223 +0,653 @@ | ||
const t = { | ||
RANGE: 0, | ||
SCROLL_SIZE: 1, | ||
SIZES: 2 | ||
}, s = t => t(), i = (t, s) => { | ||
if (!t) throw Error(s); | ||
}, e = (t, s, i) => { | ||
const e = new Uint32Array(s); | ||
return e.set(t), e.fill(i, t.length), e; | ||
}, h = t => t instanceof HTMLElement, r = (t, s) => t.getBoundingClientRect()[s], o = (t, s, i, e) => s && t && t !== s ? t[i] + Math.round(r(s, e) - (h(t) ? r(t, e) : 0)) : 0, n = (t, s) => { | ||
t.set(s, 1); | ||
for (let s, i = 1, e = t.length; e > i; i++) s = i + (i & -i), e > s && (t[s] += t[i]); | ||
}, l = (t, s, i, e) => { | ||
for (;e > s; s += s & -s) t[s] += i; | ||
}, c = (t, s, i) => { | ||
for (;i > s; s += s & -s) ; | ||
return Math.min(s, t.length); | ||
}, a = new Set; | ||
// src/constants/index.ts | ||
var VirtualScrollerEvent = { | ||
RANGE: 0, | ||
SCROLL_SIZE: 1, | ||
SIZES: 2 | ||
}; | ||
let f = 0; | ||
// src/utils/misc/index.ts | ||
var call = (fn) => fn(); | ||
var growTypedArray = (sourceArray, newLength, fillValue) => { | ||
const resultArray = new Uint32Array(newLength); | ||
resultArray.set(sourceArray); | ||
resultArray.fill(fillValue, sourceArray.length); | ||
return resultArray; | ||
}; | ||
var isElement = (el) => el instanceof HTMLElement; | ||
var getElementOffset = (element, scrollToKey) => element.getBoundingClientRect()[scrollToKey]; | ||
var getDistanceBetween = (scrollElement, containerElement, scrollKey, scrollToKey) => { | ||
if (!containerElement || !scrollElement || scrollElement === containerElement) { | ||
return 0; | ||
} | ||
return scrollElement[scrollKey] + Math.round( | ||
getElementOffset(containerElement, scrollToKey) - (isElement(scrollElement) ? getElementOffset(scrollElement, scrollToKey) : 0) | ||
); | ||
}; | ||
const _ = () => { | ||
--f || (a.forEach(s), a.clear()); | ||
}, u = t => a.add(t), m = { | ||
box: "border-box" | ||
}, d = new Uint32Array(0), S = 2147483647, v = [ "offsetHeight", "offsetWidth", "innerHeight", "innerWidth" ], E = [ "scrollTop", "scrollLeft", "scrollY", "scrollX" ], z = [ "blockSize", "inlineSize" ], b = [ "top", "left" ], y = (t, s) => Math.round(t.borderBoxSize[0][s]); | ||
// src/utils/fTree/index.ts | ||
var syncWithArray = (fTree, sourceArray) => { | ||
fTree.set(sourceArray, 1); | ||
for (let i = 1, fTreeLength = fTree.length, j; i < fTreeLength; i++) { | ||
j = i + (i & -i); | ||
if (j < fTreeLength) { | ||
fTree[j] += fTree[i]; | ||
} | ||
} | ||
}; | ||
var update = (fTree, i, delta, limitTreeLiftingIndex) => { | ||
for (; i < limitTreeLiftingIndex; i += i & -i) { | ||
fTree[i] += delta; | ||
} | ||
}; | ||
var getLiftingLimit = (fTree, from, to) => { | ||
for (; from < to; from += from & -from) | ||
; | ||
return Math.min(from, fTree.length); | ||
}; | ||
class p { | ||
t=v[0]; | ||
i=E[0]; | ||
h=z[0]; | ||
o=b[0]; | ||
l=0; | ||
_=0; | ||
u=0; | ||
m=0; | ||
S=0; | ||
v=0; | ||
p=0; | ||
T=0; | ||
M=6; | ||
O=40; | ||
I=null; | ||
R=null; | ||
k=d; | ||
C=d; | ||
L=0; | ||
horizontal=!1; | ||
scrollSize=0; | ||
from=0; | ||
to=0; | ||
sizesHash=0; | ||
F=new Map; | ||
H=new Map; | ||
K=[ null, null ]; | ||
A=[ 0, 0 ]; | ||
P=new ResizeObserver((t => { | ||
let s = 0; | ||
for (const i of t) { | ||
const t = this.K.indexOf(i.target); | ||
if (-1 !== t) { | ||
const e = y(i, this.h) - this.A[t]; | ||
this.A[t] += e, s += e; | ||
} | ||
// src/models/VirtualScroller.ts | ||
var BatchQueue = /* @__PURE__ */ new Set(); | ||
var batchLevel = 0; | ||
var batchEnd = () => { | ||
if (!--batchLevel) { | ||
BatchQueue.forEach(call); | ||
BatchQueue.clear(); | ||
} | ||
}; | ||
var addToBatchQueue = (fn) => BatchQueue.add(fn); | ||
var OBSERVE_OPTIONS = { | ||
box: "border-box" | ||
}; | ||
var ITEMS_ROOM = 32; | ||
var DEFAULT_OVERSCAN_COUNT = 6; | ||
var DEFAULT_ESTIMATED_WIDGET_SIZE = 200; | ||
var DEFAULT_ESTIMATED_ITEM_SIZE = 40; | ||
var EMPTY_TYPED_ARRAY = new Uint32Array(0); | ||
var MAX_INT_32 = 2147483647; | ||
var STICKY_HEADER_INDEX = 0; | ||
var STICKY_FOOTER_INDEX = 1; | ||
var ScrollElementSizeKeysOrdered = [ | ||
"offsetHeight" /* ELEMENT_VERTICAL */, | ||
"offsetWidth" /* ELEMENT_HORIZONTAL */, | ||
"innerHeight" /* WINDOW_VERTICAL */, | ||
"innerWidth" /* WINDOW_HORIZONTAL */ | ||
]; | ||
var ScrollKeysOrdered = [ | ||
"scrollTop" /* ELEMENT_VERTICAL */, | ||
"scrollLeft" /* ELEMENT_HORIZONTAL */, | ||
"scrollY" /* WINDOW_VERTICAL */, | ||
"scrollX" /* WINDOW_HORIZONTAL */ | ||
]; | ||
var ResizeObserverSizeKeysOrdered = [ | ||
"blockSize" /* VERTICAL */, | ||
"inlineSize" /* HORIZONTAL */ | ||
]; | ||
var ScrollToKeysOrdered = [ | ||
"top" /* VERTICAL */, | ||
"left" /* HORIZONTAL */ | ||
]; | ||
var getEntrySize = (resizeObserverEntry, sizeKey) => Math.round(resizeObserverEntry.borderBoxSize[0][sizeKey]); | ||
var VirtualScroller = class { | ||
z = ScrollElementSizeKeysOrdered[0]; | ||
o = ScrollKeysOrdered[0]; | ||
p = ResizeObserverSizeKeysOrdered[0]; | ||
q = ScrollToKeysOrdered[0]; | ||
A = 0; | ||
j = 0; | ||
r = 0; | ||
/* It is more useful to store scrollPos - scrollElementOffset in one variable for future calculations */ | ||
d = 0; | ||
g = 0; | ||
B = 0; | ||
/** {@inheritDoc VirtualScrollerRuntimeParams.itemCount} */ | ||
e = 0; | ||
f = 0; | ||
/** {@inheritDoc VirtualScrollerRuntimeParams.overscanCount} */ | ||
s = DEFAULT_OVERSCAN_COUNT; | ||
/** {@inheritDoc VirtualScrollerRuntimeParams.estimatedItemSize} */ | ||
C = DEFAULT_ESTIMATED_ITEM_SIZE; | ||
b = null; | ||
t = null; | ||
a = EMPTY_TYPED_ARRAY; | ||
c = EMPTY_TYPED_ARRAY; | ||
/** | ||
* Most significant bit of this._itemCount; | ||
* caching it to avoid Math.clz32 calculations on every getIndex call | ||
*/ | ||
D = 0; | ||
/** @readonly {@inheritDoc VirtualScrollerInitialParams.horizontal} */ | ||
horizontal = false; | ||
/** | ||
* @readonly | ||
* Sum of all item sizes */ | ||
scrollSize = 0; | ||
/** | ||
* @readonly | ||
* Items range start with {@link VirtualScrollerRuntimeParams.overscanCount | overscanCount} included */ | ||
from = 0; | ||
/** | ||
* @readonly | ||
* Items range end with {@link VirtualScrollerRuntimeParams.overscanCount | overscanCount} included */ | ||
to = 0; | ||
/** | ||
* @readonly | ||
* Hash of item sizes. Changed when at least one visible item is resized */ | ||
sizesHash = 0; | ||
u = /* @__PURE__ */ new Map(); | ||
v = /* @__PURE__ */ new Map(); | ||
/* header and footer; lengths are hardcoded */ | ||
k = [null, null]; | ||
l = [0, 0]; | ||
E = new ResizeObserver((entries) => { | ||
let buff = 0; | ||
for (const entry of entries) { | ||
const index = this.k.indexOf(entry.target); | ||
if (index !== -1) { | ||
const diff = getEntrySize(entry, this.p) - this.l[index]; | ||
this.l[index] += diff; | ||
buff += diff; | ||
} | ||
} | ||
this.F(buff); | ||
}); | ||
G = new ResizeObserver((entries) => { | ||
let buff = 0, wasAtLeastOneSizeChanged = false; | ||
const lim = ( | ||
/*#__NOINLINE__*/ | ||
getLiftingLimit( | ||
this.c, | ||
this.from + 1, | ||
this.to | ||
) | ||
); | ||
for (const entry of entries) { | ||
const index = this.u.get(entry.target); | ||
if (index < lim) { | ||
const diff = getEntrySize(entry, this.p) - this.a[index]; | ||
if (diff) { | ||
wasAtLeastOneSizeChanged = true; | ||
this.a[index] += diff; | ||
buff += diff; | ||
update(this.c, index + 1, diff, lim); | ||
} | ||
this.U(s); | ||
})); | ||
W=new ResizeObserver((t => { | ||
let s = 0, i = !1; | ||
const e = /*#__NOINLINE__*/ c(this.C, this.from + 1, this.to); | ||
for (const h of t) { | ||
const t = this.F.get(h.target); | ||
if (e > t) { | ||
const r = y(h, this.h) - this.k[t]; | ||
r && (i = !0, this.k[t] += r, s += r, l(this.C, t + 1, r, e)); | ||
} | ||
} | ||
} | ||
if (wasAtLeastOneSizeChanged) { | ||
++batchLevel; | ||
if (buff !== 0) { | ||
update(this.c, lim, buff, this.c.length); | ||
this.scrollSize += buff; | ||
this.h(1 /* SCROLL_SIZE */); | ||
if (buff < 0) { | ||
this.i(); | ||
} | ||
i && (++f, 0 !== s && (l(this.C, e, s, this.C.length), this.scrollSize += s, this.Z(1), | ||
0 > s && this.G()), this.sizesHash = this.sizesHash + 1 & S, this.Z(2), _()); | ||
})); | ||
N=[ [], [], [] ]; | ||
X() { | ||
const t = this.horizontal ? 1 : 0, s = t + 2 * (h(this.I) ? 0 : 1); | ||
this.t = v[s], this.i = E[s], this.h = z[t], this.o = b[t]; | ||
} | ||
this.sizesHash = this.sizesHash + 1 & MAX_INT_32; | ||
this.h(2 /* SIZES */); | ||
batchEnd(); | ||
} | ||
Y=() => { | ||
const t = this.I[this.t] - this.v; | ||
t !== this.T && (this.T = t, this.updateScrollerOffset(), this.G()); | ||
}; | ||
U(t) { | ||
t && (this.v += t, this.T -= t, this.G()); | ||
}); | ||
/** | ||
* Providing exact type here with 2 purposes: | ||
* - forbid using it as an array (.map, .filter, etc.) | ||
* - if events quantity does not match - type error would be shown | ||
*/ | ||
m = [ | ||
[], | ||
[], | ||
[] | ||
]; | ||
/** | ||
* 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. | ||
*/ | ||
K() { | ||
const h = this.horizontal ? 1 : 0; | ||
const w = isElement(this.b) ? 0 : 1; | ||
const i = h + 2 * w; | ||
this.z = ScrollElementSizeKeysOrdered[i]; | ||
this.o = ScrollKeysOrdered[i]; | ||
this.p = ResizeObserverSizeKeysOrdered[h]; | ||
this.q = ScrollToKeysOrdered[h]; | ||
} | ||
n = () => { | ||
const availableWidgetSize = this.b[this.z] - this.B; | ||
if (availableWidgetSize !== this.f) { | ||
this.f = availableWidgetSize; | ||
this.updateScrollerOffset(); | ||
this.i(); | ||
} | ||
j=() => {}; | ||
constructor(t) { | ||
t && (this.horizontal = !!t.horizontal, this.S = t.estimatedScrollElementOffset || 0, | ||
this.T = t.estimatedWidgetSize ?? 200, this.set(t)); | ||
}; | ||
F(relativeOffset) { | ||
if (relativeOffset) { | ||
this.B += relativeOffset; | ||
this.f -= relativeOffset; | ||
this.i(); | ||
} | ||
on(t, s) { | ||
return s.forEach((s => this.N[s].push(t))), () => s.forEach((s => this.N[s].splice(this.N[s].indexOf(t) >>> 0, 1))); | ||
} | ||
w = () => { | ||
}; | ||
constructor(params) { | ||
if (params) { | ||
this.horizontal = !!params.horizontal; | ||
this.g = params.estimatedScrollElementOffset || 0; | ||
this.f = params.estimatedWidgetSize ?? DEFAULT_ESTIMATED_WIDGET_SIZE; | ||
this.set(params); | ||
} | ||
Z(t) { | ||
this.N[t].forEach(f ? u : s); | ||
} | ||
/** | ||
* Subscribe to model events | ||
* @returns unsubscribe function | ||
* @param callBack - event to be triggered | ||
* @param events - events to subscribe | ||
*/ | ||
on(callBack, events) { | ||
events.forEach((evt) => this.m[evt].push(callBack)); | ||
return () => events.forEach( | ||
(evt) => this.m[evt].splice( | ||
// >>> 0 - protection against -1 | ||
this.m[evt].indexOf(callBack) >>> 0, | ||
1 | ||
) | ||
); | ||
} | ||
/** | ||
* Call all `event` subscribers | ||
* @param event - event to emit | ||
*/ | ||
h(event) { | ||
this.m[event].forEach(batchLevel ? addToBatchQueue : call); | ||
} | ||
/** | ||
* Get item index by pixel offset; | ||
* @param offset - pixel offset | ||
* @returns item index; | ||
* | ||
* @remarks | ||
* Time complexity: `O(log2(itemCount))` | ||
*/ | ||
getIndex(offset) { | ||
if (offset <= 0) { | ||
return 0; | ||
} | ||
getIndex(t) { | ||
if (0 >= t) return 0; | ||
if (t >= this.scrollSize) return this.p - 1; | ||
let s = 0; | ||
for (let i = this.L, e = 0; i > 0; i >>= 1) e = s + i, e <= this.p && t > this.C[e] && (s = e, | ||
t -= this.C[e]); | ||
return s; | ||
if (offset >= this.scrollSize) { | ||
return this.e - 1; | ||
} | ||
getOffset(t) { | ||
"production" !== process.env.NODE_ENV && i(t <= this.p, "index must not be > itemCount"); | ||
let s = 0; | ||
for (;t > 0; t -= t & -t) s += this.C[t]; | ||
return s; | ||
let index = 0; | ||
for (let bitMask = this.D, tempIndex = 0; bitMask > 0; bitMask >>= 1) { | ||
tempIndex = index + bitMask; | ||
if (tempIndex <= this.e && offset > this.c[tempIndex]) { | ||
index = tempIndex; | ||
offset -= this.c[tempIndex]; | ||
} | ||
} | ||
getSize(t) { | ||
return "production" !== process.env.NODE_ENV && i(t < this.k.length, "itemIndex must be < itemCount in getSize"), | ||
this.k[t]; | ||
return index; | ||
} | ||
/** | ||
* Get pixel offset by item index; | ||
* @param index - item index | ||
* @returns pixel offset | ||
* | ||
* @remarks | ||
* Time complexity: `O(log2(itemCount))` | ||
*/ | ||
getOffset(index) { | ||
if (process.env.NODE_ENV !== "production") { | ||
if (index >= this.e) { | ||
throw Error("index must not be > itemCount"); | ||
} | ||
} | ||
get visibleFrom() { | ||
const t = this.q(); | ||
return t + (this.m - this.getOffset(t)) / this.k[t]; | ||
let result = 0; | ||
for (; index > 0; index -= index & -index) { | ||
result += this.c[index]; | ||
} | ||
B() { | ||
const t = Math.round(this.I[this.i]) - this.S; | ||
t > this.m ? (this.m = t, this.G()) : t < this.m && (this.m = t, this.D()); | ||
return result; | ||
} | ||
/** | ||
* Get last cached item size by item index | ||
* @param itemIndex - item index; | ||
* @returns last cached item size | ||
* | ||
* @remarks | ||
* Time complexity: `O(1)` | ||
*/ | ||
getSize(itemIndex) { | ||
if (process.env.NODE_ENV !== "production") { | ||
if (itemIndex >= this.a.length) { | ||
throw Error("itemIndex must be < itemCount in getSize"); | ||
} | ||
} | ||
J=t => { | ||
this.l = t.timeStamp, this.B(); | ||
}; | ||
setScroller(t) { | ||
if (this.I && (clearInterval(this._), clearTimeout(this.u), this.j(), this.I.removeEventListener("scroll", this.J)), | ||
this.I = t, t) { | ||
if (this.X(), h(t)) { | ||
const s = new ResizeObserver(this.Y); | ||
s.observe(t), this.j = () => s.disconnect(); | ||
} else this.Y(), addEventListener("resize", this.Y), this.j = () => removeEventListener("resize", this.Y); | ||
t.addEventListener("scroll", this.J, { | ||
passive: !0 | ||
}), this.updateScrollerOffset(), this.B(); | ||
} | ||
return this.a[itemIndex]; | ||
} | ||
/** | ||
* Returns snapshot of current scroll position. | ||
* | ||
* @remarks | ||
* {@link VirtualScrollerExactPosition} | ||
* | ||
* @privateRemarks | ||
* "returns" tag is missed by api-extractor for getters (for now). | ||
* So using Regular description + type link. | ||
*/ | ||
get visibleFrom() { | ||
const firstVisibleIndex = this.x(); | ||
return firstVisibleIndex + (this.d - this.getOffset(firstVisibleIndex)) / this.a[firstVisibleIndex]; | ||
} | ||
/** | ||
* Synchronize current scroll position with visible range | ||
*/ | ||
y() { | ||
const newAlignedScrollPos = Math.round(this.b[this.o]) - this.g; | ||
if (newAlignedScrollPos > this.d) { | ||
this.d = newAlignedScrollPos; | ||
this.i(); | ||
} else if (newAlignedScrollPos < this.d) { | ||
this.d = newAlignedScrollPos; | ||
this.L(); | ||
} | ||
setContainer(t) { | ||
t !== this.R && (this.R = t, this.updateScrollerOffset()); | ||
} | ||
H = (e) => { | ||
this.A = e.timeStamp; | ||
this.y(); | ||
}; | ||
/** | ||
* Informs model about scrollable element. | ||
* @param element - scroller element | ||
* | ||
* @remarks | ||
* Must be called with `null` before killing the instance. | ||
*/ | ||
setScroller(element) { | ||
if (this.b) { | ||
clearInterval(this.j); | ||
clearTimeout(this.r); | ||
this.w(); | ||
this.b.removeEventListener( | ||
"scroll", | ||
this.H | ||
); | ||
} | ||
updateScrollerOffset() { | ||
clearTimeout(this.u), this.u = setTimeout((() => { | ||
if (this.I) { | ||
const t = | ||
/*#__NOINLINE__*/ o(this.I, this.R, this.i, this.o), s = t - this.S; | ||
s && (this.S = t, this.m -= s, this.B()); | ||
} | ||
}), 256); | ||
this.b = element; | ||
if (element) { | ||
this.K(); | ||
if (isElement(element)) { | ||
const RO = new ResizeObserver(this.n); | ||
RO.observe(element); | ||
this.w = () => RO.disconnect(); | ||
} else { | ||
this.n(); | ||
addEventListener("resize", this.n); | ||
this.w = () => removeEventListener( | ||
"resize", | ||
this.n | ||
); | ||
} | ||
element.addEventListener("scroll", this.H, { | ||
passive: true | ||
}); | ||
this.updateScrollerOffset(); | ||
this.y(); | ||
} | ||
el(t, s) { | ||
const i = this.H.get(t); | ||
i && (this.H.delete(t), this.F.delete(i), this.W.unobserve(i)), s && (this.F.set(s, t), | ||
this.H.set(t, s), this.W.observe(s, m)); | ||
} | ||
/** | ||
* Informs model about items container element. Usually not needed. | ||
* | ||
* @param element - container element | ||
* | ||
* @remarks | ||
* By default top/left offset between scroll container and first scrollable item is `0`. | ||
* In this case just {@link VirtualScroller.setScroller} is needed. | ||
* But extra element is needed when something "foreign" stands between scroll container and first scrollable item to measure distance between them. | ||
* That extra element is represented as `ItemsContainer` on this schema: | ||
* | ||
* ```plaintext | ||
* <ScrollContainer> |.| | ||
* Some header |s| | ||
* Another header |c| | ||
* <ItemsContainer> |r| | ||
* item 1 [o] | ||
* item 2 [l] | ||
* item 3 [l] | ||
* ... [b] | ||
* </ItemsContainer> |a| | ||
* Some footer |r| | ||
* </ScrollContainer> |.| | ||
* ``` | ||
* | ||
* Must be called with `null` before killing the instance. | ||
*/ | ||
setContainer(element) { | ||
if (element !== this.t) { | ||
this.t = element; | ||
this.updateScrollerOffset(); | ||
} | ||
V(t, s) { | ||
const i = this.K[t]; | ||
i && (this.P.unobserve(i), this.U(-this.A[t]), this.K[t] = null, this.A[t] = 0), | ||
s && (this.K[t] = s, this.P.observe(s, m)); | ||
} | ||
/** | ||
* Recalculates the offset between | ||
* {@link VirtualScroller.setScroller | scroller element} and {@link VirtualScroller.setContainer | container element}. | ||
* | ||
* @remarks | ||
* By default debounced at `256` milliseconds and called automatically when: | ||
* | ||
* - {@link VirtualScroller.setScroller | setScroller} was called; | ||
* | ||
* - {@link VirtualScroller.setContainer | setContainer} was called; | ||
* | ||
* - {@link VirtualScroller.setScroller | scroller element} was resized. | ||
* | ||
* Normally this is enough, needed only if something else would trigger this offset change. | ||
*/ | ||
updateScrollerOffset() { | ||
clearTimeout(this.r); | ||
this.r = setTimeout(() => { | ||
if (this.b) { | ||
const newScrollElementOffset = ( | ||
/*#__NOINLINE__*/ | ||
getDistanceBetween( | ||
this.b, | ||
this.t, | ||
this.o, | ||
this.q | ||
) | ||
); | ||
const diff = newScrollElementOffset - this.g; | ||
if (diff) { | ||
this.g = newScrollElementOffset; | ||
this.d -= diff; | ||
this.y(); | ||
} | ||
} | ||
}, 256); | ||
} | ||
/** | ||
* Start/finish observing size of `element` at `index`. Observing is finished if element is `null`. | ||
* @param index - item index | ||
* @param element - element for item | ||
* | ||
* @remarks | ||
* If an item was registered like `el(5, HTMLElement)` - it must be killed with `el(5, null)` before killing the instance. | ||
*/ | ||
el(index, element) { | ||
const oldElement = this.v.get(index); | ||
if (oldElement) { | ||
this.v.delete(index); | ||
this.u.delete(oldElement); | ||
this.G.unobserve(oldElement); | ||
} | ||
setStickyHeader(t) { | ||
this.V(0, t); | ||
if (element) { | ||
this.u.set(element, index); | ||
this.v.set(index, element); | ||
this.G.observe(element, OBSERVE_OPTIONS); | ||
} | ||
setStickyFooter(t) { | ||
this.V(1, t); | ||
} | ||
I(i, element) { | ||
const oldElement = this.k[i]; | ||
if (oldElement) { | ||
this.E.unobserve(oldElement); | ||
this.F(-this.l[i]); | ||
this.k[i] = null; | ||
this.l[i] = 0; | ||
} | ||
q() { | ||
return this.getIndex(this.m); | ||
if (element) { | ||
this.k[i] = element; | ||
this.E.observe(element, OBSERVE_OPTIONS); | ||
} | ||
$() { | ||
return this.p && 1 + this.getIndex(this.m + this.T); | ||
} | ||
/** | ||
* Start observing size of sticky header `element`. Observing is finished if element is `null`. | ||
* @param element - header element | ||
* | ||
* @remarks | ||
* Must be called with `null` before killing the instance. | ||
*/ | ||
setStickyHeader(element) { | ||
this.I(STICKY_HEADER_INDEX, element); | ||
} | ||
/** | ||
* Start observing size of sticky footer `element`. Observing is finished if element is `null`. | ||
* @param element - footer element | ||
* | ||
* @remarks | ||
* Must be called with `null` before killing the instance. | ||
*/ | ||
setStickyFooter(element) { | ||
this.I(STICKY_FOOTER_INDEX, element); | ||
} | ||
/** | ||
* Get first visible item index (without overscan) | ||
* @returns first visible item index | ||
*/ | ||
x() { | ||
return this.getIndex(this.d); | ||
} | ||
/** | ||
* Get last visible item index (without overscan) | ||
* @returns last visible item index | ||
*/ | ||
J() { | ||
return this.e && 1 + this.getIndex( | ||
this.d + this.f | ||
); | ||
} | ||
/** | ||
* Used to update current visible items range when scrolling down/right; | ||
* adds overscan reserve forward to reduce rerenders quantity | ||
*/ | ||
i() { | ||
const exactTo = this.J(); | ||
if (exactTo > this.to) { | ||
this.to = Math.min(this.e, exactTo + this.s); | ||
this.from = this.x(); | ||
this.h(0 /* RANGE */); | ||
} | ||
G() { | ||
const t = this.$(); | ||
t > this.to && (this.to = Math.min(this.p, t + this.M), this.from = this.q(), this.Z(0)); | ||
} | ||
/** | ||
* Used to update current visible items range when scrolling up/left; | ||
* adds overscan reserve backward to reduce rerenders quantity | ||
*/ | ||
L() { | ||
const exactFrom = this.x(); | ||
if (exactFrom < this.from) { | ||
this.from = Math.max(0, exactFrom - this.s); | ||
this.to = this.J(); | ||
this.h(0 /* RANGE */); | ||
} | ||
D() { | ||
const t = this.q(); | ||
t < this.from && (this.from = Math.max(0, t - this.M), this.to = this.$(), this.Z(0)); | ||
} | ||
/** | ||
* Scroll to pixel offset | ||
* | ||
* @param offset - offset to scroll to | ||
* @param smooth - should smooth scroll be used | ||
*/ | ||
scrollToOffset(offset, smooth) { | ||
this.b?.scroll({ | ||
[this.q]: this.g + offset, | ||
behavior: smooth ? "smooth" : "instant" | ||
}); | ||
} | ||
/** | ||
* Scroll to item index | ||
* | ||
* @param index - item index to scroll to | ||
* @param smooth - should smooth scroll be used | ||
* | ||
* @remarks | ||
* Calls {@link VirtualScroller.scrollToOffset | scrollToOffset} with calcuated offset until desired scroll position is reached. | ||
*/ | ||
scrollToIndex(index, smooth) { | ||
clearInterval(this.j); | ||
let attempts = 5; | ||
this.j = setInterval( | ||
() => { | ||
const finishedScrolling = !smooth || performance.now() - this.A > 128; | ||
if (finishedScrolling) { | ||
if (!--attempts) { | ||
clearInterval(this.j); | ||
} | ||
const whole = Math.trunc(index); | ||
const desiredScrollPos = Math.min( | ||
this.scrollSize - this.f, | ||
this.getOffset(whole) + Math.round(this.a[whole] * (index - whole)) | ||
); | ||
this.scrollToOffset(desiredScrollPos, smooth); | ||
} | ||
}, | ||
smooth ? 50 : 16 | ||
); | ||
} | ||
/** | ||
* Notify model about items quantity change | ||
* @param itemCount - new items quantity. {@link VirtualScrollerRuntimeParams.itemCount} | ||
*/ | ||
setItemCount(itemCount) { | ||
if (this.e !== itemCount) { | ||
++batchLevel; | ||
if (itemCount > MAX_INT_32) { | ||
throw Error("itemCount must be <= " + MAX_INT_32); | ||
} | ||
this.e = itemCount; | ||
this.D = itemCount && 1 << 31 - Math.clz32(itemCount); | ||
if (itemCount > this.a.length) { | ||
const newLen = Math.min(itemCount + ITEMS_ROOM, MAX_INT_32); | ||
this.a = /*#__NOINLINE__*/ | ||
growTypedArray( | ||
this.a, | ||
newLen, | ||
this.C || DEFAULT_ESTIMATED_ITEM_SIZE | ||
); | ||
this.c = new Uint32Array(newLen + 1); | ||
syncWithArray(this.c, this.a); | ||
} | ||
this.scrollSize = this.getOffset(itemCount); | ||
this.h(1 /* SCROLL_SIZE */); | ||
if (this.to > itemCount) { | ||
this.to = -1; | ||
} | ||
this.i(); | ||
batchEnd(); | ||
} | ||
scrollToOffset(t, s) { | ||
this.I?.scroll({ | ||
[this.o]: this.S + t, | ||
behavior: s ? "smooth" : "instant" | ||
}); | ||
} | ||
/** | ||
* Synchronize runtime parameters | ||
* @param runtimeParams - runtime parameters | ||
*/ | ||
set(runtimeParams) { | ||
if (runtimeParams.estimatedItemSize) { | ||
this.C = runtimeParams.estimatedItemSize; | ||
} | ||
scrollToIndex(t, s) { | ||
clearInterval(this._); | ||
let i = 5; | ||
this._ = setInterval((() => { | ||
if (!s || performance.now() - this.l > 128) { | ||
--i || clearInterval(this._); | ||
const e = Math.trunc(t), h = Math.min(this.scrollSize - this.T, this.getOffset(e) + Math.round(this.k[e] * (t - e))); | ||
this.scrollToOffset(h, s); | ||
} | ||
}), s ? 50 : 16); | ||
if (runtimeParams.overscanCount !== void 0) { | ||
this.s = runtimeParams.overscanCount; | ||
} | ||
setItemCount(t) { | ||
if (this.p !== t) { | ||
if (++f, i(S >= t, "itemCount must be <= " + S), this.p = t, this.L = t && 1 << 31 - Math.clz32(t), | ||
t > this.k.length) { | ||
const s = Math.min(t + 32, S); | ||
this.k = /*#__NOINLINE__*/ e(this.k, s, this.O || 40), this.C = new Uint32Array(s + 1), | ||
/*#__NOINLINE__*/ n(this.C, this.k); | ||
} | ||
this.scrollSize = this.getOffset(t), this.Z(1), this.to > t && (this.to = -1), this.G(), | ||
_(); | ||
} | ||
if (runtimeParams.itemCount !== void 0) { | ||
this.setItemCount(runtimeParams.itemCount); | ||
} | ||
set(t) { | ||
t.estimatedItemSize && (this.O = t.estimatedItemSize), void 0 !== t.overscanCount && (this.M = t.overscanCount), | ||
void 0 !== t.itemCount && this.setItemCount(t.itemCount); | ||
} | ||
} | ||
export { p as VirtualScroller, t as VirtualScrollerEvent }; | ||
//# sourceMappingURL=index.js.map | ||
} | ||
}; | ||
var VirtualScroller_default = VirtualScroller; | ||
export { | ||
VirtualScroller_default as VirtualScroller, | ||
VirtualScrollerEvent | ||
}; |
global.ResizeObserver ||= class { | ||
observe() {} | ||
unobserve() {} | ||
disconnect() {} | ||
observe(){} | ||
unobserve(){} | ||
disconnect(){} | ||
} | ||
// src/constants/index.ts | ||
var VirtualScrollerEvent = { | ||
RANGE: 0, | ||
SCROLL_SIZE: 1, | ||
SIZES: 2 | ||
}; | ||
const t = { | ||
RANGE: 0, | ||
SCROLL_SIZE: 1, | ||
SIZES: 2 | ||
}, s = t => t(), i = (t, s) => { | ||
if (!t) throw Error(s); | ||
}, e = (t, s, i) => { | ||
const e = new Uint32Array(s); | ||
return e.set(t), e.fill(i, t.length), e; | ||
}, h = t => t instanceof HTMLElement, r = (t, s) => t.getBoundingClientRect()[s], o = (t, s, i, e) => s && t && t !== s ? t[i] + Math.round(r(s, e) - (h(t) ? r(t, e) : 0)) : 0, n = (t, s) => { | ||
t.set(s, 1); | ||
for (let s, i = 1, e = t.length; e > i; i++) s = i + (i & -i), e > s && (t[s] += t[i]); | ||
}, l = (t, s, i, e) => { | ||
for (;e > s; s += s & -s) t[s] += i; | ||
}, c = (t, s, i) => { | ||
for (;i > s; s += s & -s) ; | ||
return Math.min(s, t.length); | ||
}, a = new Set; | ||
// src/utils/misc/index.ts | ||
var call = (fn) => fn(); | ||
var growTypedArray = (sourceArray, newLength, fillValue) => { | ||
const resultArray = new Uint32Array(newLength); | ||
resultArray.set(sourceArray); | ||
resultArray.fill(fillValue, sourceArray.length); | ||
return resultArray; | ||
}; | ||
var isElement = (el) => el instanceof HTMLElement; | ||
var getElementOffset = (element, scrollToKey) => element.getBoundingClientRect()[scrollToKey]; | ||
var getDistanceBetween = (scrollElement, containerElement, scrollKey, scrollToKey) => { | ||
if (!containerElement || !scrollElement || scrollElement === containerElement) { | ||
return 0; | ||
} | ||
return scrollElement[scrollKey] + Math.round( | ||
getElementOffset(containerElement, scrollToKey) - (isElement(scrollElement) ? getElementOffset(scrollElement, scrollToKey) : 0) | ||
); | ||
}; | ||
let f = 0; | ||
// src/utils/fTree/index.ts | ||
var syncWithArray = (fTree, sourceArray) => { | ||
fTree.set(sourceArray, 1); | ||
for (let i = 1, fTreeLength = fTree.length, j; i < fTreeLength; i++) { | ||
j = i + (i & -i); | ||
if (j < fTreeLength) { | ||
fTree[j] += fTree[i]; | ||
} | ||
} | ||
}; | ||
var update = (fTree, i, delta, limitTreeLiftingIndex) => { | ||
for (; i < limitTreeLiftingIndex; i += i & -i) { | ||
fTree[i] += delta; | ||
} | ||
}; | ||
var getLiftingLimit = (fTree, from, to) => { | ||
for (; from < to; from += from & -from) | ||
; | ||
return Math.min(from, fTree.length); | ||
}; | ||
const u = () => { | ||
--f || (a.forEach(s), a.clear()); | ||
}, _ = t => a.add(t), m = { | ||
box: "border-box" | ||
}, d = new Uint32Array(0), S = 2147483647, v = [ "offsetHeight", "offsetWidth", "innerHeight", "innerWidth" ], E = [ "scrollTop", "scrollLeft", "scrollY", "scrollX" ], z = [ "blockSize", "inlineSize" ], b = [ "top", "left" ], y = (t, s) => Math.round(t.borderBoxSize[0][s]); | ||
class p { | ||
t=v[0]; | ||
i=E[0]; | ||
h=z[0]; | ||
o=b[0]; | ||
l=0; | ||
u=0; | ||
_=0; | ||
m=0; | ||
S=0; | ||
v=0; | ||
p=0; | ||
T=0; | ||
M=6; | ||
O=40; | ||
I=null; | ||
R=null; | ||
k=d; | ||
C=d; | ||
L=0; | ||
horizontal=!1; | ||
scrollSize=0; | ||
from=0; | ||
to=0; | ||
sizesHash=0; | ||
F=new Map; | ||
H=new Map; | ||
K=[ null, null ]; | ||
A=[ 0, 0 ]; | ||
P=new ResizeObserver((t => { | ||
let s = 0; | ||
for (const i of t) { | ||
const t = this.K.indexOf(i.target); | ||
if (-1 !== t) { | ||
const e = y(i, this.h) - this.A[t]; | ||
this.A[t] += e, s += e; | ||
} | ||
// src/models/VirtualScroller.ts | ||
var BatchQueue = /* @__PURE__ */ new Set(); | ||
var batchLevel = 0; | ||
var batchEnd = () => { | ||
if (!--batchLevel) { | ||
BatchQueue.forEach(call); | ||
BatchQueue.clear(); | ||
} | ||
}; | ||
var addToBatchQueue = (fn) => BatchQueue.add(fn); | ||
var OBSERVE_OPTIONS = { | ||
box: "border-box" | ||
}; | ||
var ITEMS_ROOM = 32; | ||
var DEFAULT_OVERSCAN_COUNT = 6; | ||
var DEFAULT_ESTIMATED_WIDGET_SIZE = 200; | ||
var DEFAULT_ESTIMATED_ITEM_SIZE = 40; | ||
var EMPTY_TYPED_ARRAY = new Uint32Array(0); | ||
var MAX_INT_32 = 2147483647; | ||
var STICKY_HEADER_INDEX = 0; | ||
var STICKY_FOOTER_INDEX = 1; | ||
var ScrollElementSizeKeysOrdered = [ | ||
"offsetHeight" /* ELEMENT_VERTICAL */, | ||
"offsetWidth" /* ELEMENT_HORIZONTAL */, | ||
"innerHeight" /* WINDOW_VERTICAL */, | ||
"innerWidth" /* WINDOW_HORIZONTAL */ | ||
]; | ||
var ScrollKeysOrdered = [ | ||
"scrollTop" /* ELEMENT_VERTICAL */, | ||
"scrollLeft" /* ELEMENT_HORIZONTAL */, | ||
"scrollY" /* WINDOW_VERTICAL */, | ||
"scrollX" /* WINDOW_HORIZONTAL */ | ||
]; | ||
var ResizeObserverSizeKeysOrdered = [ | ||
"blockSize" /* VERTICAL */, | ||
"inlineSize" /* HORIZONTAL */ | ||
]; | ||
var ScrollToKeysOrdered = [ | ||
"top" /* VERTICAL */, | ||
"left" /* HORIZONTAL */ | ||
]; | ||
var getEntrySize = (resizeObserverEntry, sizeKey) => Math.round(resizeObserverEntry.borderBoxSize[0][sizeKey]); | ||
var VirtualScroller = class { | ||
z = ScrollElementSizeKeysOrdered[0]; | ||
o = ScrollKeysOrdered[0]; | ||
p = ResizeObserverSizeKeysOrdered[0]; | ||
q = ScrollToKeysOrdered[0]; | ||
A = 0; | ||
j = 0; | ||
r = 0; | ||
/* It is more useful to store scrollPos - scrollElementOffset in one variable for future calculations */ | ||
d = 0; | ||
g = 0; | ||
B = 0; | ||
/** {@inheritDoc VirtualScrollerRuntimeParams.itemCount} */ | ||
e = 0; | ||
f = 0; | ||
/** {@inheritDoc VirtualScrollerRuntimeParams.overscanCount} */ | ||
s = DEFAULT_OVERSCAN_COUNT; | ||
/** {@inheritDoc VirtualScrollerRuntimeParams.estimatedItemSize} */ | ||
C = DEFAULT_ESTIMATED_ITEM_SIZE; | ||
b = null; | ||
t = null; | ||
a = EMPTY_TYPED_ARRAY; | ||
c = EMPTY_TYPED_ARRAY; | ||
/** | ||
* Most significant bit of this._itemCount; | ||
* caching it to avoid Math.clz32 calculations on every getIndex call | ||
*/ | ||
D = 0; | ||
/** @readonly {@inheritDoc VirtualScrollerInitialParams.horizontal} */ | ||
horizontal = false; | ||
/** | ||
* @readonly | ||
* Sum of all item sizes */ | ||
scrollSize = 0; | ||
/** | ||
* @readonly | ||
* Items range start with {@link VirtualScrollerRuntimeParams.overscanCount | overscanCount} included */ | ||
from = 0; | ||
/** | ||
* @readonly | ||
* Items range end with {@link VirtualScrollerRuntimeParams.overscanCount | overscanCount} included */ | ||
to = 0; | ||
/** | ||
* @readonly | ||
* Hash of item sizes. Changed when at least one visible item is resized */ | ||
sizesHash = 0; | ||
u = /* @__PURE__ */ new Map(); | ||
v = /* @__PURE__ */ new Map(); | ||
/* header and footer; lengths are hardcoded */ | ||
k = [null, null]; | ||
l = [0, 0]; | ||
E = new ResizeObserver((entries) => { | ||
let buff = 0; | ||
for (const entry of entries) { | ||
const index = this.k.indexOf(entry.target); | ||
if (index !== -1) { | ||
const diff = getEntrySize(entry, this.p) - this.l[index]; | ||
this.l[index] += diff; | ||
buff += diff; | ||
} | ||
} | ||
this.F(buff); | ||
}); | ||
G = new ResizeObserver((entries) => { | ||
let buff = 0, wasAtLeastOneSizeChanged = false; | ||
const lim = ( | ||
/*#__NOINLINE__*/ | ||
getLiftingLimit( | ||
this.c, | ||
this.from + 1, | ||
this.to | ||
) | ||
); | ||
for (const entry of entries) { | ||
const index = this.u.get(entry.target); | ||
if (index < lim) { | ||
const diff = getEntrySize(entry, this.p) - this.a[index]; | ||
if (diff) { | ||
wasAtLeastOneSizeChanged = true; | ||
this.a[index] += diff; | ||
buff += diff; | ||
update(this.c, index + 1, diff, lim); | ||
} | ||
this.U(s); | ||
})); | ||
W=new ResizeObserver((t => { | ||
let s = 0, i = !1; | ||
const e = /*#__NOINLINE__*/ c(this.C, this.from + 1, this.to); | ||
for (const h of t) { | ||
const t = this.F.get(h.target); | ||
if (e > t) { | ||
const r = y(h, this.h) - this.k[t]; | ||
r && (i = !0, this.k[t] += r, s += r, l(this.C, t + 1, r, e)); | ||
} | ||
} | ||
} | ||
if (wasAtLeastOneSizeChanged) { | ||
++batchLevel; | ||
if (buff !== 0) { | ||
update(this.c, lim, buff, this.c.length); | ||
this.scrollSize += buff; | ||
this.h(1 /* SCROLL_SIZE */); | ||
if (buff < 0) { | ||
this.i(); | ||
} | ||
i && (++f, 0 !== s && (l(this.C, e, s, this.C.length), this.scrollSize += s, this.Z(1), | ||
0 > s && this.G()), this.sizesHash = this.sizesHash + 1 & S, this.Z(2), u()); | ||
})); | ||
N=[ [], [], [] ]; | ||
X() { | ||
const t = this.horizontal ? 1 : 0, s = t + 2 * (h(this.I) ? 0 : 1); | ||
this.t = v[s], this.i = E[s], this.h = z[t], this.o = b[t]; | ||
} | ||
this.sizesHash = this.sizesHash + 1 & MAX_INT_32; | ||
this.h(2 /* SIZES */); | ||
batchEnd(); | ||
} | ||
Y=() => { | ||
const t = this.I[this.t] - this.v; | ||
t !== this.T && (this.T = t, this.updateScrollerOffset(), this.G()); | ||
}; | ||
U(t) { | ||
t && (this.v += t, this.T -= t, this.G()); | ||
}); | ||
/** | ||
* Providing exact type here with 2 purposes: | ||
* - forbid using it as an array (.map, .filter, etc.) | ||
* - if events quantity does not match - type error would be shown | ||
*/ | ||
m = [ | ||
[], | ||
[], | ||
[] | ||
]; | ||
/** | ||
* 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. | ||
*/ | ||
K() { | ||
const h = this.horizontal ? 1 : 0; | ||
const w = isElement(this.b) ? 0 : 1; | ||
const i = h + 2 * w; | ||
this.z = ScrollElementSizeKeysOrdered[i]; | ||
this.o = ScrollKeysOrdered[i]; | ||
this.p = ResizeObserverSizeKeysOrdered[h]; | ||
this.q = ScrollToKeysOrdered[h]; | ||
} | ||
n = () => { | ||
const availableWidgetSize = this.b[this.z] - this.B; | ||
if (availableWidgetSize !== this.f) { | ||
this.f = availableWidgetSize; | ||
this.updateScrollerOffset(); | ||
this.i(); | ||
} | ||
j=() => {}; | ||
constructor(t) { | ||
t && (this.horizontal = !!t.horizontal, this.S = t.estimatedScrollElementOffset || 0, | ||
this.T = t.estimatedWidgetSize ?? 200, this.set(t)); | ||
}; | ||
F(relativeOffset) { | ||
if (relativeOffset) { | ||
this.B += relativeOffset; | ||
this.f -= relativeOffset; | ||
this.i(); | ||
} | ||
on(t, s) { | ||
return s.forEach((s => this.N[s].push(t))), () => s.forEach((s => this.N[s].splice(this.N[s].indexOf(t) >>> 0, 1))); | ||
} | ||
w = () => { | ||
}; | ||
constructor(params) { | ||
if (params) { | ||
this.horizontal = !!params.horizontal; | ||
this.g = params.estimatedScrollElementOffset || 0; | ||
this.f = params.estimatedWidgetSize ?? DEFAULT_ESTIMATED_WIDGET_SIZE; | ||
this.set(params); | ||
} | ||
Z(t) { | ||
this.N[t].forEach(f ? _ : s); | ||
} | ||
/** | ||
* Subscribe to model events | ||
* @returns unsubscribe function | ||
* @param callBack - event to be triggered | ||
* @param events - events to subscribe | ||
*/ | ||
on(callBack, events) { | ||
events.forEach((evt) => this.m[evt].push(callBack)); | ||
return () => events.forEach( | ||
(evt) => this.m[evt].splice( | ||
// >>> 0 - protection against -1 | ||
this.m[evt].indexOf(callBack) >>> 0, | ||
1 | ||
) | ||
); | ||
} | ||
/** | ||
* Call all `event` subscribers | ||
* @param event - event to emit | ||
*/ | ||
h(event) { | ||
this.m[event].forEach(batchLevel ? addToBatchQueue : call); | ||
} | ||
/** | ||
* Get item index by pixel offset; | ||
* @param offset - pixel offset | ||
* @returns item index; | ||
* | ||
* @remarks | ||
* Time complexity: `O(log2(itemCount))` | ||
*/ | ||
getIndex(offset) { | ||
if (offset <= 0) { | ||
return 0; | ||
} | ||
getIndex(t) { | ||
if (0 >= t) return 0; | ||
if (t >= this.scrollSize) return this.p - 1; | ||
let s = 0; | ||
for (let i = this.L, e = 0; i > 0; i >>= 1) e = s + i, e <= this.p && t > this.C[e] && (s = e, | ||
t -= this.C[e]); | ||
return s; | ||
if (offset >= this.scrollSize) { | ||
return this.e - 1; | ||
} | ||
getOffset(t) { | ||
"production" !== process.env.NODE_ENV && i(t <= this.p, "index must not be > itemCount"); | ||
let s = 0; | ||
for (;t > 0; t -= t & -t) s += this.C[t]; | ||
return s; | ||
let index = 0; | ||
for (let bitMask = this.D, tempIndex = 0; bitMask > 0; bitMask >>= 1) { | ||
tempIndex = index + bitMask; | ||
if (tempIndex <= this.e && offset > this.c[tempIndex]) { | ||
index = tempIndex; | ||
offset -= this.c[tempIndex]; | ||
} | ||
} | ||
getSize(t) { | ||
return "production" !== process.env.NODE_ENV && i(t < this.k.length, "itemIndex must be < itemCount in getSize"), | ||
this.k[t]; | ||
return index; | ||
} | ||
/** | ||
* Get pixel offset by item index; | ||
* @param index - item index | ||
* @returns pixel offset | ||
* | ||
* @remarks | ||
* Time complexity: `O(log2(itemCount))` | ||
*/ | ||
getOffset(index) { | ||
if (process.env.NODE_ENV !== "production") { | ||
if (index >= this.e) { | ||
throw Error("index must not be > itemCount"); | ||
} | ||
} | ||
get visibleFrom() { | ||
const t = this.q(); | ||
return t + (this.m - this.getOffset(t)) / this.k[t]; | ||
let result = 0; | ||
for (; index > 0; index -= index & -index) { | ||
result += this.c[index]; | ||
} | ||
B() { | ||
const t = Math.round(this.I[this.i]) - this.S; | ||
t > this.m ? (this.m = t, this.G()) : t < this.m && (this.m = t, this.D()); | ||
return result; | ||
} | ||
/** | ||
* Get last cached item size by item index | ||
* @param itemIndex - item index; | ||
* @returns last cached item size | ||
* | ||
* @remarks | ||
* Time complexity: `O(1)` | ||
*/ | ||
getSize(itemIndex) { | ||
if (process.env.NODE_ENV !== "production") { | ||
if (itemIndex >= this.a.length) { | ||
throw Error("itemIndex must be < itemCount in getSize"); | ||
} | ||
} | ||
J=t => { | ||
this.l = t.timeStamp, this.B(); | ||
}; | ||
setScroller(t) { | ||
if (this.I && (clearInterval(this.u), clearTimeout(this._), this.j(), this.I.removeEventListener("scroll", this.J)), | ||
this.I = t, t) { | ||
if (this.X(), h(t)) { | ||
const s = new ResizeObserver(this.Y); | ||
s.observe(t), this.j = () => s.disconnect(); | ||
} else this.Y(), addEventListener("resize", this.Y), this.j = () => removeEventListener("resize", this.Y); | ||
t.addEventListener("scroll", this.J, { | ||
passive: !0 | ||
}), this.updateScrollerOffset(), this.B(); | ||
} | ||
return this.a[itemIndex]; | ||
} | ||
/** | ||
* Returns snapshot of current scroll position. | ||
* | ||
* @remarks | ||
* {@link VirtualScrollerExactPosition} | ||
* | ||
* @privateRemarks | ||
* "returns" tag is missed by api-extractor for getters (for now). | ||
* So using Regular description + type link. | ||
*/ | ||
get visibleFrom() { | ||
const firstVisibleIndex = this.x(); | ||
return firstVisibleIndex + (this.d - this.getOffset(firstVisibleIndex)) / this.a[firstVisibleIndex]; | ||
} | ||
/** | ||
* Synchronize current scroll position with visible range | ||
*/ | ||
y() { | ||
const newAlignedScrollPos = Math.round(this.b[this.o]) - this.g; | ||
if (newAlignedScrollPos > this.d) { | ||
this.d = newAlignedScrollPos; | ||
this.i(); | ||
} else if (newAlignedScrollPos < this.d) { | ||
this.d = newAlignedScrollPos; | ||
this.L(); | ||
} | ||
setContainer(t) { | ||
t !== this.R && (this.R = t, this.updateScrollerOffset()); | ||
} | ||
H = (e) => { | ||
this.A = e.timeStamp; | ||
this.y(); | ||
}; | ||
/** | ||
* Informs model about scrollable element. | ||
* @param element - scroller element | ||
* | ||
* @remarks | ||
* Must be called with `null` before killing the instance. | ||
*/ | ||
setScroller(element) { | ||
if (this.b) { | ||
clearInterval(this.j); | ||
clearTimeout(this.r); | ||
this.w(); | ||
this.b.removeEventListener( | ||
"scroll", | ||
this.H | ||
); | ||
} | ||
updateScrollerOffset() { | ||
clearTimeout(this._), this._ = setTimeout((() => { | ||
if (this.I) { | ||
const t = | ||
/*#__NOINLINE__*/ o(this.I, this.R, this.i, this.o), s = t - this.S; | ||
s && (this.S = t, this.m -= s, this.B()); | ||
} | ||
}), 256); | ||
this.b = element; | ||
if (element) { | ||
this.K(); | ||
if (isElement(element)) { | ||
const RO = new ResizeObserver(this.n); | ||
RO.observe(element); | ||
this.w = () => RO.disconnect(); | ||
} else { | ||
this.n(); | ||
addEventListener("resize", this.n); | ||
this.w = () => removeEventListener( | ||
"resize", | ||
this.n | ||
); | ||
} | ||
element.addEventListener("scroll", this.H, { | ||
passive: true | ||
}); | ||
this.updateScrollerOffset(); | ||
this.y(); | ||
} | ||
el(t, s) { | ||
const i = this.H.get(t); | ||
i && (this.H.delete(t), this.F.delete(i), this.W.unobserve(i)), s && (this.F.set(s, t), | ||
this.H.set(t, s), this.W.observe(s, m)); | ||
} | ||
/** | ||
* Informs model about items container element. Usually not needed. | ||
* | ||
* @param element - container element | ||
* | ||
* @remarks | ||
* By default top/left offset between scroll container and first scrollable item is `0`. | ||
* In this case just {@link VirtualScroller.setScroller} is needed. | ||
* But extra element is needed when something "foreign" stands between scroll container and first scrollable item to measure distance between them. | ||
* That extra element is represented as `ItemsContainer` on this schema: | ||
* | ||
* ```plaintext | ||
* <ScrollContainer> |.| | ||
* Some header |s| | ||
* Another header |c| | ||
* <ItemsContainer> |r| | ||
* item 1 [o] | ||
* item 2 [l] | ||
* item 3 [l] | ||
* ... [b] | ||
* </ItemsContainer> |a| | ||
* Some footer |r| | ||
* </ScrollContainer> |.| | ||
* ``` | ||
* | ||
* Must be called with `null` before killing the instance. | ||
*/ | ||
setContainer(element) { | ||
if (element !== this.t) { | ||
this.t = element; | ||
this.updateScrollerOffset(); | ||
} | ||
V(t, s) { | ||
const i = this.K[t]; | ||
i && (this.P.unobserve(i), this.U(-this.A[t]), this.K[t] = null, this.A[t] = 0), | ||
s && (this.K[t] = s, this.P.observe(s, m)); | ||
} | ||
/** | ||
* Recalculates the offset between | ||
* {@link VirtualScroller.setScroller | scroller element} and {@link VirtualScroller.setContainer | container element}. | ||
* | ||
* @remarks | ||
* By default debounced at `256` milliseconds and called automatically when: | ||
* | ||
* - {@link VirtualScroller.setScroller | setScroller} was called; | ||
* | ||
* - {@link VirtualScroller.setContainer | setContainer} was called; | ||
* | ||
* - {@link VirtualScroller.setScroller | scroller element} was resized. | ||
* | ||
* Normally this is enough, needed only if something else would trigger this offset change. | ||
*/ | ||
updateScrollerOffset() { | ||
clearTimeout(this.r); | ||
this.r = setTimeout(() => { | ||
if (this.b) { | ||
const newScrollElementOffset = ( | ||
/*#__NOINLINE__*/ | ||
getDistanceBetween( | ||
this.b, | ||
this.t, | ||
this.o, | ||
this.q | ||
) | ||
); | ||
const diff = newScrollElementOffset - this.g; | ||
if (diff) { | ||
this.g = newScrollElementOffset; | ||
this.d -= diff; | ||
this.y(); | ||
} | ||
} | ||
}, 256); | ||
} | ||
/** | ||
* Start/finish observing size of `element` at `index`. Observing is finished if element is `null`. | ||
* @param index - item index | ||
* @param element - element for item | ||
* | ||
* @remarks | ||
* If an item was registered like `el(5, HTMLElement)` - it must be killed with `el(5, null)` before killing the instance. | ||
*/ | ||
el(index, element) { | ||
const oldElement = this.v.get(index); | ||
if (oldElement) { | ||
this.v.delete(index); | ||
this.u.delete(oldElement); | ||
this.G.unobserve(oldElement); | ||
} | ||
setStickyHeader(t) { | ||
this.V(0, t); | ||
if (element) { | ||
this.u.set(element, index); | ||
this.v.set(index, element); | ||
this.G.observe(element, OBSERVE_OPTIONS); | ||
} | ||
setStickyFooter(t) { | ||
this.V(1, t); | ||
} | ||
I(i, element) { | ||
const oldElement = this.k[i]; | ||
if (oldElement) { | ||
this.E.unobserve(oldElement); | ||
this.F(-this.l[i]); | ||
this.k[i] = null; | ||
this.l[i] = 0; | ||
} | ||
q() { | ||
return this.getIndex(this.m); | ||
if (element) { | ||
this.k[i] = element; | ||
this.E.observe(element, OBSERVE_OPTIONS); | ||
} | ||
$() { | ||
return this.p && 1 + this.getIndex(this.m + this.T); | ||
} | ||
/** | ||
* Start observing size of sticky header `element`. Observing is finished if element is `null`. | ||
* @param element - header element | ||
* | ||
* @remarks | ||
* Must be called with `null` before killing the instance. | ||
*/ | ||
setStickyHeader(element) { | ||
this.I(STICKY_HEADER_INDEX, element); | ||
} | ||
/** | ||
* Start observing size of sticky footer `element`. Observing is finished if element is `null`. | ||
* @param element - footer element | ||
* | ||
* @remarks | ||
* Must be called with `null` before killing the instance. | ||
*/ | ||
setStickyFooter(element) { | ||
this.I(STICKY_FOOTER_INDEX, element); | ||
} | ||
/** | ||
* Get first visible item index (without overscan) | ||
* @returns first visible item index | ||
*/ | ||
x() { | ||
return this.getIndex(this.d); | ||
} | ||
/** | ||
* Get last visible item index (without overscan) | ||
* @returns last visible item index | ||
*/ | ||
J() { | ||
return this.e && 1 + this.getIndex( | ||
this.d + this.f | ||
); | ||
} | ||
/** | ||
* Used to update current visible items range when scrolling down/right; | ||
* adds overscan reserve forward to reduce rerenders quantity | ||
*/ | ||
i() { | ||
const exactTo = this.J(); | ||
if (exactTo > this.to) { | ||
this.to = Math.min(this.e, exactTo + this.s); | ||
this.from = this.x(); | ||
this.h(0 /* RANGE */); | ||
} | ||
G() { | ||
const t = this.$(); | ||
t > this.to && (this.to = Math.min(this.p, t + this.M), this.from = this.q(), this.Z(0)); | ||
} | ||
/** | ||
* Used to update current visible items range when scrolling up/left; | ||
* adds overscan reserve backward to reduce rerenders quantity | ||
*/ | ||
L() { | ||
const exactFrom = this.x(); | ||
if (exactFrom < this.from) { | ||
this.from = Math.max(0, exactFrom - this.s); | ||
this.to = this.J(); | ||
this.h(0 /* RANGE */); | ||
} | ||
D() { | ||
const t = this.q(); | ||
t < this.from && (this.from = Math.max(0, t - this.M), this.to = this.$(), this.Z(0)); | ||
} | ||
/** | ||
* Scroll to pixel offset | ||
* | ||
* @param offset - offset to scroll to | ||
* @param smooth - should smooth scroll be used | ||
*/ | ||
scrollToOffset(offset, smooth) { | ||
this.b?.scroll({ | ||
[this.q]: this.g + offset, | ||
behavior: smooth ? "smooth" : "instant" | ||
}); | ||
} | ||
/** | ||
* Scroll to item index | ||
* | ||
* @param index - item index to scroll to | ||
* @param smooth - should smooth scroll be used | ||
* | ||
* @remarks | ||
* Calls {@link VirtualScroller.scrollToOffset | scrollToOffset} with calcuated offset until desired scroll position is reached. | ||
*/ | ||
scrollToIndex(index, smooth) { | ||
clearInterval(this.j); | ||
let attempts = 5; | ||
this.j = setInterval( | ||
() => { | ||
const finishedScrolling = !smooth || performance.now() - this.A > 128; | ||
if (finishedScrolling) { | ||
if (!--attempts) { | ||
clearInterval(this.j); | ||
} | ||
const whole = Math.trunc(index); | ||
const desiredScrollPos = Math.min( | ||
this.scrollSize - this.f, | ||
this.getOffset(whole) + Math.round(this.a[whole] * (index - whole)) | ||
); | ||
this.scrollToOffset(desiredScrollPos, smooth); | ||
} | ||
}, | ||
smooth ? 50 : 16 | ||
); | ||
} | ||
/** | ||
* Notify model about items quantity change | ||
* @param itemCount - new items quantity. {@link VirtualScrollerRuntimeParams.itemCount} | ||
*/ | ||
setItemCount(itemCount) { | ||
if (this.e !== itemCount) { | ||
++batchLevel; | ||
if (itemCount > MAX_INT_32) { | ||
throw Error("itemCount must be <= " + MAX_INT_32); | ||
} | ||
this.e = itemCount; | ||
this.D = itemCount && 1 << 31 - Math.clz32(itemCount); | ||
if (itemCount > this.a.length) { | ||
const newLen = Math.min(itemCount + ITEMS_ROOM, MAX_INT_32); | ||
this.a = /*#__NOINLINE__*/ | ||
growTypedArray( | ||
this.a, | ||
newLen, | ||
this.C || DEFAULT_ESTIMATED_ITEM_SIZE | ||
); | ||
this.c = new Uint32Array(newLen + 1); | ||
syncWithArray(this.c, this.a); | ||
} | ||
this.scrollSize = this.getOffset(itemCount); | ||
this.h(1 /* SCROLL_SIZE */); | ||
if (this.to > itemCount) { | ||
this.to = -1; | ||
} | ||
this.i(); | ||
batchEnd(); | ||
} | ||
scrollToOffset(t, s) { | ||
this.I?.scroll({ | ||
[this.o]: this.S + t, | ||
behavior: s ? "smooth" : "instant" | ||
}); | ||
} | ||
/** | ||
* Synchronize runtime parameters | ||
* @param runtimeParams - runtime parameters | ||
*/ | ||
set(runtimeParams) { | ||
if (runtimeParams.estimatedItemSize) { | ||
this.C = runtimeParams.estimatedItemSize; | ||
} | ||
scrollToIndex(t, s) { | ||
clearInterval(this.u); | ||
let i = 5; | ||
this.u = setInterval((() => { | ||
if (!s || performance.now() - this.l > 128) { | ||
--i || clearInterval(this.u); | ||
const e = Math.trunc(t), h = Math.min(this.scrollSize - this.T, this.getOffset(e) + Math.round(this.k[e] * (t - e))); | ||
this.scrollToOffset(h, s); | ||
} | ||
}), s ? 50 : 16); | ||
if (runtimeParams.overscanCount !== void 0) { | ||
this.s = runtimeParams.overscanCount; | ||
} | ||
setItemCount(t) { | ||
if (this.p !== t) { | ||
if (++f, i(S >= t, "itemCount must be <= " + S), this.p = t, this.L = t && 1 << 31 - Math.clz32(t), | ||
t > this.k.length) { | ||
const s = Math.min(t + 32, S); | ||
this.k = /*#__NOINLINE__*/ e(this.k, s, this.O || 40), this.C = new Uint32Array(s + 1), | ||
/*#__NOINLINE__*/ n(this.C, this.k); | ||
} | ||
this.scrollSize = this.getOffset(t), this.Z(1), this.to > t && (this.to = -1), this.G(), | ||
u(); | ||
} | ||
if (runtimeParams.itemCount !== void 0) { | ||
this.setItemCount(runtimeParams.itemCount); | ||
} | ||
set(t) { | ||
t.estimatedItemSize && (this.O = t.estimatedItemSize), void 0 !== t.overscanCount && (this.M = t.overscanCount), | ||
void 0 !== t.itemCount && this.setItemCount(t.itemCount); | ||
} | ||
} | ||
export { p as VirtualScroller, t as VirtualScrollerEvent }; | ||
} | ||
}; | ||
var VirtualScroller_default = VirtualScroller; | ||
export { | ||
VirtualScroller_default as VirtualScroller, | ||
VirtualScrollerEvent | ||
}; |
{ | ||
"name": "@af-utils/virtual-core", | ||
"private": false, | ||
"version": "0.0.22", | ||
"version": "0.0.23", | ||
"description": "Model for rendering large scrollable data", | ||
@@ -11,5 +11,9 @@ "repository": { | ||
}, | ||
"homepage": "https://af-utils.vercel.app/virtual", | ||
"homepage": "https://af-utils.com/virtual", | ||
"bugs": "https://github.com/nowaalex/af-utils/issues", | ||
"author": "Alex Fomin <nowaalex@gmail.com> (https://github.com/nowaalex/)", | ||
"author": { | ||
"name": "Alex Fomin", | ||
"email": "nowaalex@gmail.com", | ||
"url": "https://github.com/nowaalex/" | ||
}, | ||
"license": "MIT", | ||
@@ -24,3 +28,6 @@ "sideEffects": false, | ||
"exports": { | ||
"./lib/bundlesize.index.js": "./lib/bundlesize.index.js", | ||
"./bundlesizes": { | ||
"types": "./lib/bundlesizes.d.ts", | ||
"default": "./lib/bundlesizes.js" | ||
}, | ||
".": { | ||
@@ -51,7 +58,6 @@ "types": "./lib/index.d.ts", | ||
"devDependencies": { | ||
"@rollup/plugin-terser": "^0.4.4", | ||
"@rollup/plugin-typescript": "^11.1.6", | ||
"rollup": "^4.13.0", | ||
"esbuild": "^0.20.2", | ||
"typescript": "^5.4.5", | ||
"ts-jest": "^29.1.2", | ||
"@af-utils/rollup-plugin-export-bundle-size": "0.0.5" | ||
"@af-utils/export-bundle-size": "0.0.5" | ||
}, | ||
@@ -62,6 +68,11 @@ "publishConfig": { | ||
"scripts": { | ||
"dev": "rollup -w -c rollup.config.ts --configPlugin typescript & tsc-alias -w", | ||
"build": "rollup -c rollup.config.ts --configPlugin typescript && tsc-alias && api-extractor run && rm -r lib/types", | ||
"dev": "pnpm build:types && pnpm esbuild:dev", | ||
"build": "pnpm build:types && pnpm esbuild:build && pnpm export-bundle-size && pnpm api-extractor", | ||
"build:types": "tsc && tsc-alias", | ||
"esbuild:build": "node build.mjs prod", | ||
"esbuild:dev": "node build.mjs", | ||
"api-extractor": "api-extractor run && rm -r lib/types", | ||
"export-bundle-size": "export-bundle-size -i ./lib/index.js -o ./lib/bundlesizes.js", | ||
"test": "jest" | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
4
1739
54352
7
1