Files
opencode/patches/@tanstack%2Fvirtual-core@3.17.8.patch
T

560 lines
25 KiB
Diff

diff --git a/dist/cjs/index.cjs b/dist/cjs/index.cjs
index e470032a9572b3ced764ca02238a8c6be435a9d4..4c25e03b5db221a78793083d97970b2d90e28efa 100644
--- a/dist/cjs/index.cjs
+++ b/dist/cjs/index.cjs
@@ -289,7 +289,7 @@ class Virtualizer {
let anchor = null;
let followOnAppend = null;
let edgeKeysChanged = false;
- if (prevOptions !== void 0 && prevOptions.enabled && merged.enabled && merged.anchorTo === "end" && this.scrollElement !== null) {
+ if (prevOptions !== void 0 && prevOptions.enabled && merged.enabled && this.scrollElement !== null) {
const prevCount = prevOptions.count;
const nextCount = merged.count;
const measurements = this.getMeasurements();
@@ -299,11 +299,20 @@ class Virtualizer {
const didEdgeKeysChange = didCountChange || prevCount > 0 && nextCount > 0 && (merged.getItemKey(0) !== prevFirstKey || merged.getItemKey(nextCount - 1) !== prevLastKey);
if (didEdgeKeysChange) {
edgeKeysChanged = true;
+ // A data change can legitimately re-key the rows around the current offset
+ // (e.g. a truncated leading chat turn regrouping once a prepended page loads
+ // its parent). Capture fallback anchors below the primary one so the scroll
+ // position survives even when the nearest keys disappear.
const item = prevCount > 0 ? this.getVirtualItemForOffset(this.getScrollOffset()) ?? measurements[0] : null;
if (item) {
- anchor = [item.key, this.getScrollOffset() - item.start];
+ anchor = [];
+ for (let i = item.index; i < prevCount && anchor.length < 100; i++) {
+ const candidate = measurements[i];
+ if (!candidate) break;
+ anchor.push([candidate.key, this.getScrollOffset() - candidate.start]);
+ }
}
- const behavior = merged.followOnAppend === true ? "auto" : merged.followOnAppend || null;
+ const behavior = merged.anchorTo === "end" ? merged.followOnAppend === true ? "auto" : merged.followOnAppend || null : null;
if (behavior && nextCount > prevCount && this.isAtEnd(prevOptions.scrollEndThreshold) && (prevCount === 0 || merged.getItemKey(nextCount - 1) !== prevLastKey)) {
followOnAppend = behavior;
}
@@ -316,30 +325,31 @@ class Virtualizer {
}
let anchorResolved = false;
let anchorDelta = 0;
- if (anchor && this.scrollOffset !== null) {
- const [anchorKey, anchorOffset] = anchor;
+ let resolvedAnchor = null;
+ if (anchor && anchor.length > 0 && this.scrollOffset !== null) {
const newMeasurements = this.getMeasurements();
const { count, getItemKey } = this.options;
- let idx = 0;
- while (idx < count && getItemKey(idx) !== anchorKey) {
- idx++;
- }
- if (idx < count) {
+ const indexByKey = new Map();
+ for (let i = 0; i < count; i++) indexByKey.set(getItemKey(i), i);
+ for (const [anchorKey, anchorOffset] of anchor) {
+ const idx = indexByKey.get(anchorKey);
+ if (idx === void 0) continue;
const anchorItem = newMeasurements[idx];
- if (anchorItem) {
- const newOffset = Math.max(0, anchorItem.start + anchorOffset);
- if (newOffset !== this.scrollOffset) {
- anchorDelta = newOffset - this.scrollOffset;
- this.scrollOffset = newOffset;
- anchorResolved = true;
- }
+ if (!anchorItem) continue;
+ resolvedAnchor = [anchorKey, anchorOffset];
+ const newOffset = Math.max(0, anchorItem.start + anchorOffset);
+ if (newOffset !== this.scrollOffset) {
+ anchorDelta = newOffset - this.scrollOffset;
+ this.scrollOffset = newOffset;
+ anchorResolved = true;
}
+ break;
}
}
if (anchorResolved || followOnAppend) {
this.pendingScrollAnchor = [
- anchorResolved ? anchor[0] : null,
- anchorResolved ? anchor[1] : 0,
+ anchorResolved ? resolvedAnchor[0] : null,
+ anchorResolved ? resolvedAnchor[1] : 0,
followOnAppend,
anchorDelta
];
@@ -440,7 +450,17 @@ class Virtualizer {
);
if ("addEventListener" in this.scrollElement) {
const scrollEl = this.scrollElement;
- const onTouchStart = () => {
+ let touchTarget = null;
+ const clearTouchTarget = () => {
+ touchTarget?.removeEventListener("touchend", onTouchEnd);
+ touchTarget?.removeEventListener("touchcancel", onTouchEnd);
+ touchTarget = null;
+ };
+ const onTouchStart = (event) => {
+ clearTouchTarget();
+ touchTarget = event.target ?? scrollEl;
+ touchTarget.addEventListener("touchend", onTouchEnd, addEventListenerOptions);
+ touchTarget.addEventListener("touchcancel", onTouchEnd, addEventListenerOptions);
this._iosTouching = true;
this._iosJustTouchEnded = false;
if (this._iosTouchEndTimerId !== null && this.targetWindow != null) {
@@ -449,6 +469,7 @@ class Virtualizer {
}
};
const onTouchEnd = () => {
+ clearTouchTarget();
this._iosTouching = false;
if (!isIOSWebKit() || this.targetWindow == null) {
return;
@@ -465,14 +486,9 @@ class Virtualizer {
onTouchStart,
addEventListenerOptions
);
- scrollEl.addEventListener(
- "touchend",
- onTouchEnd,
- addEventListenerOptions
- );
this.unsubs.push(() => {
scrollEl.removeEventListener("touchstart", onTouchStart);
- scrollEl.removeEventListener("touchend", onTouchEnd);
+ clearTouchTarget();
if (this._iosTouchEndTimerId !== null && this.targetWindow != null) {
this.targetWindow.clearTimeout(this._iosTouchEndTimerId);
this._iosTouchEndTimerId = null;
@@ -725,17 +741,20 @@ class Virtualizer {
this.getMeasurements(),
this.getSize(),
this.getScrollOffset(),
- this.options.lanes
+ this.options.lanes,
+ this.options.paddingEnd
],
- (measurements, outerSize, scrollOffset, lanes) => {
+ (measurements, outerSize, scrollOffset, lanes, _paddingEnd) => {
if (measurements.length === 0 || outerSize === 0) {
this.range = null;
return null;
}
+ const maxScrollOffset = Math.max(this.options.scrollMargin + this.getTotalSize() - outerSize, 0);
+ const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset);
this.range = calculateRangeImpl(
measurements,
outerSize,
- scrollOffset,
+ effectiveScrollOffset,
lanes,
// Pass the typed array so binary search + forward-walk can read
// start/end directly from Float64Array, skipping the Proxy traps.
@@ -1095,8 +1114,10 @@ class Virtualizer {
const snapshot = [];
if (this.itemSizeCache.size === 0) return snapshot;
const m = this.getMeasurements();
- for (const item of m) {
- if (item && this.itemSizeCache.has(item.key)) {
+ for (let index = 0; index < m.length; index++) {
+ if (!this.itemSizeCache.has(this.options.getItemKey(index))) continue;
+ const item = m[index];
+ if (item) {
snapshot.push({
index: item.index,
key: item.key,
diff --git a/dist/cjs/index.d.cts b/dist/cjs/index.d.cts
index cc1001b8a14d06aa73a3a9e8f59870557ee9e7d6..70b202ed436ce1f3283a8465a81faf2e6e2e7983 100644
--- a/dist/cjs/index.d.cts
+++ b/dist/cjs/index.d.cts
@@ -144,7 +144,7 @@ export declare class Virtualizer<TScrollElement extends Element | Window, TItemE
startIndex: number;
endIndex: number;
} | null;
- updateDeps(newDeps: [VirtualItem[], number, number, number]): void;
+ updateDeps(newDeps: [VirtualItem[], number, number, number, number]): void;
};
getVirtualIndexes: {
(): number[];
diff --git a/dist/esm/index.d.ts b/dist/esm/index.d.ts
index 6b43c0aea7ed9eeef75cbfb1351fcbd243913bdd..7be2680967934ddfbc4583a210a3d11ee2cddc94 100644
--- a/dist/esm/index.d.ts
+++ b/dist/esm/index.d.ts
@@ -144,7 +144,7 @@ export declare class Virtualizer<TScrollElement extends Element | Window, TItemE
startIndex: number;
endIndex: number;
} | null;
- updateDeps(newDeps: [VirtualItem[], number, number, number]): void;
+ updateDeps(newDeps: [VirtualItem[], number, number, number, number]): void;
};
getVirtualIndexes: {
(): number[];
diff --git a/dist/esm/index.js b/dist/esm/index.js
index 2495b26cf2c3589213546b3958eaadf2eb6b751d..1398bc7dd33690a8fe3280343b6aabdeaea6435a 100644
--- a/dist/esm/index.js
+++ b/dist/esm/index.js
@@ -287,7 +287,7 @@ class Virtualizer {
let anchor = null;
let followOnAppend = null;
let edgeKeysChanged = false;
- if (prevOptions !== void 0 && prevOptions.enabled && merged.enabled && merged.anchorTo === "end" && this.scrollElement !== null) {
+ if (prevOptions !== void 0 && prevOptions.enabled && merged.enabled && this.scrollElement !== null) {
const prevCount = prevOptions.count;
const nextCount = merged.count;
const measurements = this.getMeasurements();
@@ -297,11 +297,20 @@ class Virtualizer {
const didEdgeKeysChange = didCountChange || prevCount > 0 && nextCount > 0 && (merged.getItemKey(0) !== prevFirstKey || merged.getItemKey(nextCount - 1) !== prevLastKey);
if (didEdgeKeysChange) {
edgeKeysChanged = true;
+ // A data change can legitimately re-key the rows around the current offset
+ // (e.g. a truncated leading chat turn regrouping once a prepended page loads
+ // its parent). Capture fallback anchors below the primary one so the scroll
+ // position survives even when the nearest keys disappear.
const item = prevCount > 0 ? this.getVirtualItemForOffset(this.getScrollOffset()) ?? measurements[0] : null;
if (item) {
- anchor = [item.key, this.getScrollOffset() - item.start];
+ anchor = [];
+ for (let i = item.index; i < prevCount && anchor.length < 100; i++) {
+ const candidate = measurements[i];
+ if (!candidate) break;
+ anchor.push([candidate.key, this.getScrollOffset() - candidate.start]);
+ }
}
- const behavior = merged.followOnAppend === true ? "auto" : merged.followOnAppend || null;
+ const behavior = merged.anchorTo === "end" ? merged.followOnAppend === true ? "auto" : merged.followOnAppend || null : null;
if (behavior && nextCount > prevCount && this.isAtEnd(prevOptions.scrollEndThreshold) && (prevCount === 0 || merged.getItemKey(nextCount - 1) !== prevLastKey)) {
followOnAppend = behavior;
}
@@ -314,30 +323,31 @@ class Virtualizer {
}
let anchorResolved = false;
let anchorDelta = 0;
- if (anchor && this.scrollOffset !== null) {
- const [anchorKey, anchorOffset] = anchor;
+ let resolvedAnchor = null;
+ if (anchor && anchor.length > 0 && this.scrollOffset !== null) {
const newMeasurements = this.getMeasurements();
const { count, getItemKey } = this.options;
- let idx = 0;
- while (idx < count && getItemKey(idx) !== anchorKey) {
- idx++;
- }
- if (idx < count) {
+ const indexByKey = new Map();
+ for (let i = 0; i < count; i++) indexByKey.set(getItemKey(i), i);
+ for (const [anchorKey, anchorOffset] of anchor) {
+ const idx = indexByKey.get(anchorKey);
+ if (idx === void 0) continue;
const anchorItem = newMeasurements[idx];
- if (anchorItem) {
- const newOffset = Math.max(0, anchorItem.start + anchorOffset);
- if (newOffset !== this.scrollOffset) {
- anchorDelta = newOffset - this.scrollOffset;
- this.scrollOffset = newOffset;
- anchorResolved = true;
- }
+ if (!anchorItem) continue;
+ resolvedAnchor = [anchorKey, anchorOffset];
+ const newOffset = Math.max(0, anchorItem.start + anchorOffset);
+ if (newOffset !== this.scrollOffset) {
+ anchorDelta = newOffset - this.scrollOffset;
+ this.scrollOffset = newOffset;
+ anchorResolved = true;
}
+ break;
}
}
if (anchorResolved || followOnAppend) {
this.pendingScrollAnchor = [
- anchorResolved ? anchor[0] : null,
- anchorResolved ? anchor[1] : 0,
+ anchorResolved ? resolvedAnchor[0] : null,
+ anchorResolved ? resolvedAnchor[1] : 0,
followOnAppend,
anchorDelta
];
@@ -438,7 +448,17 @@ class Virtualizer {
);
if ("addEventListener" in this.scrollElement) {
const scrollEl = this.scrollElement;
- const onTouchStart = () => {
+ let touchTarget = null;
+ const clearTouchTarget = () => {
+ touchTarget?.removeEventListener("touchend", onTouchEnd);
+ touchTarget?.removeEventListener("touchcancel", onTouchEnd);
+ touchTarget = null;
+ };
+ const onTouchStart = (event) => {
+ clearTouchTarget();
+ touchTarget = event.target ?? scrollEl;
+ touchTarget.addEventListener("touchend", onTouchEnd, addEventListenerOptions);
+ touchTarget.addEventListener("touchcancel", onTouchEnd, addEventListenerOptions);
this._iosTouching = true;
this._iosJustTouchEnded = false;
if (this._iosTouchEndTimerId !== null && this.targetWindow != null) {
@@ -447,6 +467,7 @@ class Virtualizer {
}
};
const onTouchEnd = () => {
+ clearTouchTarget();
this._iosTouching = false;
if (!isIOSWebKit() || this.targetWindow == null) {
return;
@@ -463,14 +484,9 @@ class Virtualizer {
onTouchStart,
addEventListenerOptions
);
- scrollEl.addEventListener(
- "touchend",
- onTouchEnd,
- addEventListenerOptions
- );
this.unsubs.push(() => {
scrollEl.removeEventListener("touchstart", onTouchStart);
- scrollEl.removeEventListener("touchend", onTouchEnd);
+ clearTouchTarget();
if (this._iosTouchEndTimerId !== null && this.targetWindow != null) {
this.targetWindow.clearTimeout(this._iosTouchEndTimerId);
this._iosTouchEndTimerId = null;
@@ -723,17 +739,20 @@ class Virtualizer {
this.getMeasurements(),
this.getSize(),
this.getScrollOffset(),
- this.options.lanes
+ this.options.lanes,
+ this.options.paddingEnd
],
- (measurements, outerSize, scrollOffset, lanes) => {
+ (measurements, outerSize, scrollOffset, lanes, _paddingEnd) => {
if (measurements.length === 0 || outerSize === 0) {
this.range = null;
return null;
}
+ const maxScrollOffset = Math.max(this.options.scrollMargin + this.getTotalSize() - outerSize, 0);
+ const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset);
this.range = calculateRangeImpl(
measurements,
outerSize,
- scrollOffset,
+ effectiveScrollOffset,
lanes,
// Pass the typed array so binary search + forward-walk can read
// start/end directly from Float64Array, skipping the Proxy traps.
@@ -1093,8 +1112,10 @@ class Virtualizer {
const snapshot = [];
if (this.itemSizeCache.size === 0) return snapshot;
const m = this.getMeasurements();
- for (const item of m) {
- if (item && this.itemSizeCache.has(item.key)) {
+ for (let index = 0; index < m.length; index++) {
+ if (!this.itemSizeCache.has(this.options.getItemKey(index))) continue;
+ const item = m[index];
+ if (item) {
snapshot.push({
index: item.index,
key: item.key,
diff --git a/src/index.ts b/src/index.ts
index dc6f1010c4d4758de9c46fb8d69209e582e47171..9299287d72ce79d5a597cdc3440339ee148e6385 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -567,7 +567,7 @@ export class Virtualizer<
const prevOptions = this.options as
| Required<VirtualizerOptions<TScrollElement, TItemElement>>
| undefined
- let anchor: [Key, number] | null = null
+ let anchor: Array<[Key, number]> | null = null
let followOnAppend: ScrollBehavior | null = null
let edgeKeysChanged = false
@@ -575,7 +575,6 @@ export class Virtualizer<
prevOptions !== undefined &&
prevOptions.enabled &&
merged.enabled &&
- merged.anchorTo === 'end' &&
this.scrollElement !== null
) {
const prevCount = prevOptions.count
@@ -600,6 +599,10 @@ export class Virtualizer<
if (didEdgeKeysChange) {
edgeKeysChanged = true
+ // A data change can legitimately re-key the rows around the current offset
+ // (e.g. a truncated leading chat turn regrouping once a prepended page loads
+ // its parent). Capture fallback anchors below the primary one so the scroll
+ // position survives even when the nearest keys disappear.
const item =
prevCount > 0
? (this.getVirtualItemForOffset(this.getScrollOffset()) ??
@@ -607,13 +610,20 @@ export class Virtualizer<
: null
if (item) {
- anchor = [item.key, this.getScrollOffset() - item.start]
+ anchor = []
+ for (let i = item.index; i < prevCount && anchor.length < 100; i++) {
+ const candidate = measurements[i]
+ if (!candidate) break
+ anchor.push([candidate.key, this.getScrollOffset() - candidate.start])
+ }
}
const behavior =
- merged.followOnAppend === true
- ? 'auto'
- : merged.followOnAppend || null
+ merged.anchorTo === 'end'
+ ? merged.followOnAppend === true
+ ? 'auto'
+ : merged.followOnAppend || null
+ : null
if (
behavior &&
@@ -646,35 +656,36 @@ export class Virtualizer<
// frame, producing a visible "jump" on prepend with dynamic sizes.
let anchorResolved = false
let anchorDelta = 0
- if (anchor && this.scrollOffset !== null) {
- const [anchorKey, anchorOffset] = anchor
+ let resolvedAnchor: [Key, number] | null = null
+ if (anchor && anchor.length > 0 && this.scrollOffset !== null) {
const newMeasurements = this.getMeasurements()
const { count, getItemKey } = this.options
- let idx = 0
- while (idx < count && getItemKey(idx) !== anchorKey) {
- idx++
- }
- if (idx < count) {
+ const indexByKey = new Map<Key, number>()
+ for (let i = 0; i < count; i++) indexByKey.set(getItemKey(i), i)
+ for (const [anchorKey, anchorOffset] of anchor) {
+ const idx = indexByKey.get(anchorKey)
+ if (idx === undefined) continue
const anchorItem = newMeasurements[idx]
- if (anchorItem) {
- // Clamp to the reachable range's lower bound — anchorOffset may
- // have been derived from a transiently negative scrollOffset
- // (rubber-band), and a negative tracked offset never self-heals
- // when the element cannot scroll (#1229).
- const newOffset = Math.max(0, anchorItem.start + anchorOffset)
- if (newOffset !== this.scrollOffset) {
- anchorDelta = newOffset - this.scrollOffset
- this.scrollOffset = newOffset
- anchorResolved = true
- }
+ if (!anchorItem) continue
+ resolvedAnchor = [anchorKey, anchorOffset]
+ // Clamp to the reachable range's lower bound — anchorOffset may
+ // have been derived from a transiently negative scrollOffset
+ // (rubber-band), and a negative tracked offset never self-heals
+ // when the element cannot scroll (#1229).
+ const newOffset = Math.max(0, anchorItem.start + anchorOffset)
+ if (newOffset !== this.scrollOffset) {
+ anchorDelta = newOffset - this.scrollOffset
+ this.scrollOffset = newOffset
+ anchorResolved = true
}
+ break
}
}
if (anchorResolved || followOnAppend) {
this.pendingScrollAnchor = [
- anchorResolved ? anchor![0] : null,
- anchorResolved ? anchor![1] : 0,
+ anchorResolved ? resolvedAnchor![0] : null,
+ anchorResolved ? resolvedAnchor![1] : 0,
followOnAppend,
anchorDelta,
]
@@ -895,7 +906,18 @@ export class Virtualizer<
// and _flushIosDeferredIfReady so we only burn the path on iOS.
if ('addEventListener' in this.scrollElement) {
const scrollEl = this.scrollElement as unknown as EventTarget
- const onTouchStart = () => {
+ let touchTarget: EventTarget | null = null
+ const clearTouchTarget = () => {
+ touchTarget?.removeEventListener('touchend', onTouchEnd)
+ touchTarget?.removeEventListener('touchcancel', onTouchEnd)
+ touchTarget = null
+ }
+ const onTouchStart = (event: Event) => {
+ clearTouchTarget()
+ // The original target still receives release after its DOM removal.
+ touchTarget = event.target ?? scrollEl
+ touchTarget.addEventListener('touchend', onTouchEnd, addEventListenerOptions)
+ touchTarget.addEventListener('touchcancel', onTouchEnd, addEventListenerOptions)
this._iosTouching = true
this._iosJustTouchEnded = false
if (this._iosTouchEndTimerId !== null && this.targetWindow != null) {
@@ -904,6 +926,7 @@ export class Virtualizer<
}
}
const onTouchEnd = () => {
+ clearTouchTarget()
this._iosTouching = false
if (!isIOSWebKit() || this.targetWindow == null) {
// Non-iOS: nothing more to track. Just clear the touching flag.
@@ -924,14 +947,9 @@ export class Virtualizer<
onTouchStart,
addEventListenerOptions,
)
- scrollEl.addEventListener(
- 'touchend',
- onTouchEnd,
- addEventListenerOptions,
- )
this.unsubs.push(() => {
scrollEl.removeEventListener('touchstart', onTouchStart)
- scrollEl.removeEventListener('touchend', onTouchEnd)
+ clearTouchTarget()
if (this._iosTouchEndTimerId !== null && this.targetWindow != null) {
this.targetWindow.clearTimeout(this._iosTouchEndTimerId)
this._iosTouchEndTimerId = null
@@ -1410,16 +1428,25 @@ export class Virtualizer<
this.getSize(),
this.getScrollOffset(),
this.options.lanes,
+ this.options.paddingEnd,
],
- (measurements, outerSize, scrollOffset, lanes) => {
+ (measurements, outerSize, scrollOffset, lanes, _paddingEnd) => {
if (measurements.length === 0 || outerSize === 0) {
this.range = null
return null
}
+ const maxScrollOffset = Math.max(
+ this.options.scrollMargin + this.getTotalSize() - outerSize,
+ 0,
+ )
+ const effectiveScrollOffset = Math.min(
+ Math.max(scrollOffset, 0),
+ maxScrollOffset,
+ )
this.range = calculateRangeImpl(
measurements,
outerSize,
- scrollOffset,
+ effectiveScrollOffset,
lanes,
// Pass the typed array so binary search + forward-walk can read
// start/end directly from Float64Array, skipping the Proxy traps.
@@ -1937,12 +1964,13 @@ export class Virtualizer<
takeSnapshot = (): Array<VirtualItem> => {
const snapshot: Array<VirtualItem> = []
if (this.itemSizeCache.size === 0) return snapshot
- // Iterate measurementsCache only for indices whose key is in itemSizeCache
- // (i.e., have been measured). We build VirtualItem objects with the
- // current start/size/end so they can be persisted as plain data.
+ // Check the key before reading a lazy entry so unmeasured history is
+ // not materialized merely to discard it from the snapshot.
const m = this.getMeasurements()
- for (const item of m) {
- if (item && this.itemSizeCache.has(item.key)) {
+ for (let index = 0; index < m.length; index++) {
+ if (!this.itemSizeCache.has(this.options.getItemKey(index))) continue
+ const item = m[index]
+ if (item) {
// Force materialization (lazy path) and copy plain fields.
snapshot.push({
index: item.index,