@tanstack/virtual-core
Advanced tools
Comparing version 3.3.0 to 3.4.0
@@ -34,4 +34,4 @@ export * from './utils.js'; | ||
export declare const observeWindowRect: (instance: Virtualizer<Window, any>, cb: (rect: Rect) => void) => (() => void) | undefined; | ||
export declare const observeElementOffset: <T extends Element>(instance: Virtualizer<T, any>, cb: (offset: number) => void) => (() => void) | undefined; | ||
export declare const observeWindowOffset: (instance: Virtualizer<Window, any>, cb: (offset: number) => void) => (() => void) | undefined; | ||
export declare const observeElementOffset: <T extends Element>(instance: Virtualizer<T, any>, cb: (offset: number, isScrolling: boolean) => void) => (() => void) | undefined; | ||
export declare const observeWindowOffset: (instance: Virtualizer<Window, any>, cb: (offset: number, isScrolling: boolean) => void) => (() => void) | undefined; | ||
export declare const measureElement: <TItemElement extends Element>(element: TItemElement, entry: ResizeObserverEntry | undefined, instance: Virtualizer<any, TItemElement>) => number; | ||
@@ -55,3 +55,3 @@ export declare const windowScroll: <T extends Window>(offset: number, { adjustments, behavior, }: { | ||
observeElementRect: (instance: Virtualizer<TScrollElement, TItemElement>, cb: (rect: Rect) => void) => void | (() => void); | ||
observeElementOffset: (instance: Virtualizer<TScrollElement, TItemElement>, cb: (offset: number) => void) => void | (() => void); | ||
observeElementOffset: (instance: Virtualizer<TScrollElement, TItemElement>, cb: (offset: number, isScrolling: boolean) => void) => void | (() => void); | ||
debug?: any; | ||
@@ -72,3 +72,2 @@ initialRect?: Rect; | ||
gap?: number; | ||
scrollingDelay?: number; | ||
indexAttribute?: string; | ||
@@ -83,3 +82,2 @@ initialMeasurementsCache?: VirtualItem[]; | ||
isScrolling: boolean; | ||
private isScrollingTimeoutId; | ||
private scrollToIndexTimeoutId; | ||
@@ -103,3 +101,2 @@ measurementsCache: VirtualItem[]; | ||
private notify; | ||
private maybeNotify; | ||
private cleanup; | ||
@@ -109,3 +106,3 @@ _didMount: () => () => void; | ||
private getSize; | ||
private memoOptions; | ||
private getMeasurementOptions; | ||
private getFurthestMeasurement; | ||
@@ -112,0 +109,0 @@ private getMeasurements; |
@@ -1,2 +0,2 @@ | ||
import { memo, notUndefined, approxEqual } from "./utils.js"; | ||
import { memo, notUndefined, approxEqual, debounce } from "./utils.js"; | ||
const defaultKeyExtractor = (index) => index; | ||
@@ -42,2 +42,5 @@ const defaultRangeExtractor = (range) => { | ||
}; | ||
const addEventListenerOptions = { | ||
passive: true | ||
}; | ||
const observeWindowRect = (instance, cb) => { | ||
@@ -52,5 +55,3 @@ const element = instance.scrollElement; | ||
handler(); | ||
element.addEventListener("resize", handler, { | ||
passive: true | ||
}); | ||
element.addEventListener("resize", handler, addEventListenerOptions); | ||
return () => { | ||
@@ -60,2 +61,3 @@ element.removeEventListener("resize", handler); | ||
}; | ||
const supportsScrollend = typeof window == "undefined" ? true : "onscrollend" in window; | ||
const observeElementOffset = (instance, cb) => { | ||
@@ -66,11 +68,19 @@ const element = instance.scrollElement; | ||
} | ||
const handler = () => { | ||
cb(element[instance.options.horizontal ? "scrollLeft" : "scrollTop"]); | ||
let offset = 0; | ||
const fallback = supportsScrollend ? () => void 0 : debounce(() => { | ||
cb(offset, false); | ||
}, 150); | ||
const createHandler = (isScrolling) => () => { | ||
offset = element[instance.options.horizontal ? "scrollLeft" : "scrollTop"]; | ||
fallback(); | ||
cb(offset, isScrolling); | ||
}; | ||
handler(); | ||
element.addEventListener("scroll", handler, { | ||
passive: true | ||
}); | ||
const handler = createHandler(true); | ||
const endHandler = createHandler(false); | ||
endHandler(); | ||
element.addEventListener("scroll", handler, addEventListenerOptions); | ||
element.addEventListener("scrollend", endHandler, addEventListenerOptions); | ||
return () => { | ||
element.removeEventListener("scroll", handler); | ||
element.removeEventListener("scrollend", endHandler); | ||
}; | ||
@@ -83,11 +93,19 @@ }; | ||
} | ||
const handler = () => { | ||
cb(element[instance.options.horizontal ? "scrollX" : "scrollY"]); | ||
let offset = 0; | ||
const fallback = supportsScrollend ? () => void 0 : debounce(() => { | ||
cb(offset, false); | ||
}, 150); | ||
const createHandler = (isScrolling) => () => { | ||
offset = element[instance.options.horizontal ? "scrollX" : "scrollY"]; | ||
fallback(); | ||
cb(offset, isScrolling); | ||
}; | ||
handler(); | ||
element.addEventListener("scroll", handler, { | ||
passive: true | ||
}); | ||
const handler = createHandler(true); | ||
const endHandler = createHandler(false); | ||
endHandler(); | ||
element.addEventListener("scroll", handler, addEventListenerOptions); | ||
element.addEventListener("scrollend", endHandler, addEventListenerOptions); | ||
return () => { | ||
element.removeEventListener("scroll", handler); | ||
element.removeEventListener("scrollend", endHandler); | ||
}; | ||
@@ -136,3 +154,2 @@ }; | ||
this.isScrolling = false; | ||
this.isScrollingTimeoutId = null; | ||
this.scrollToIndexTimeoutId = null; | ||
@@ -198,3 +215,2 @@ this.measurementsCache = []; | ||
gap: 0, | ||
scrollingDelay: 150, | ||
indexAttribute: "data-index", | ||
@@ -206,28 +222,13 @@ initialMeasurementsCache: [], | ||
}; | ||
this.notify = (sync) => { | ||
this.notify = (force, sync) => { | ||
var _a, _b; | ||
(_b = (_a = this.options).onChange) == null ? void 0 : _b.call(_a, this, sync); | ||
const { startIndex, endIndex } = this.range ?? { | ||
startIndex: void 0, | ||
endIndex: void 0 | ||
}; | ||
const range = this.calculateRange(); | ||
if (force || startIndex !== (range == null ? void 0 : range.startIndex) || endIndex !== (range == null ? void 0 : range.endIndex)) { | ||
(_b = (_a = this.options).onChange) == null ? void 0 : _b.call(_a, this, sync); | ||
} | ||
}; | ||
this.maybeNotify = memo( | ||
() => { | ||
this.calculateRange(); | ||
return [ | ||
this.isScrolling, | ||
this.range ? this.range.startIndex : null, | ||
this.range ? this.range.endIndex : null | ||
]; | ||
}, | ||
(isScrolling) => { | ||
this.notify(isScrolling); | ||
}, | ||
{ | ||
key: process.env.NODE_ENV !== "production" && "maybeNotify", | ||
debug: () => this.options.debug, | ||
initialDeps: [ | ||
this.isScrolling, | ||
this.range ? this.range.startIndex : null, | ||
this.range ? this.range.endIndex : null | ||
] | ||
} | ||
); | ||
this.cleanup = () => { | ||
@@ -257,25 +258,13 @@ this.unsubs.filter(Boolean).forEach((d) => d()); | ||
this.scrollRect = rect; | ||
this.maybeNotify(); | ||
this.notify(false, false); | ||
}) | ||
); | ||
this.unsubs.push( | ||
this.options.observeElementOffset(this, (offset) => { | ||
this.options.observeElementOffset(this, (offset, isScrolling) => { | ||
this.scrollAdjustments = 0; | ||
if (this.scrollOffset === offset) { | ||
return; | ||
} | ||
if (this.isScrollingTimeoutId !== null) { | ||
clearTimeout(this.isScrollingTimeoutId); | ||
this.isScrollingTimeoutId = null; | ||
} | ||
this.isScrolling = true; | ||
this.scrollDirection = this.scrollOffset < offset ? "forward" : "backward"; | ||
this.scrollDirection = isScrolling ? this.scrollOffset < offset ? "forward" : "backward" : null; | ||
this.scrollOffset = offset; | ||
this.maybeNotify(); | ||
this.isScrollingTimeoutId = setTimeout(() => { | ||
this.isScrollingTimeoutId = null; | ||
this.isScrolling = false; | ||
this.scrollDirection = null; | ||
this.maybeNotify(); | ||
}, this.options.scrollingDelay); | ||
const prevIsScrolling = this.isScrolling; | ||
this.isScrolling = isScrolling; | ||
this.notify(prevIsScrolling !== isScrolling, isScrolling); | ||
}) | ||
@@ -288,3 +277,3 @@ ); | ||
}; | ||
this.memoOptions = memo( | ||
this.getMeasurementOptions = memo( | ||
() => [ | ||
@@ -337,3 +326,3 @@ this.options.count, | ||
this.getMeasurements = memo( | ||
() => [this.memoOptions(), this.itemSizeCache], | ||
() => [this.getMeasurementOptions(), this.itemSizeCache], | ||
({ count, paddingStart, scrollMargin, getItemKey }, itemSizeCache) => { | ||
@@ -391,3 +380,4 @@ const min = this.pendingMeasuredCacheIndexes.length > 0 ? Math.min(...this.pendingMeasuredCacheIndexes) : 0; | ||
return range === null ? [] : rangeExtractor({ | ||
...range, | ||
startIndex: range.startIndex, | ||
endIndex: range.endIndex, | ||
overscan, | ||
@@ -450,3 +440,3 @@ count | ||
this.itemSizeCache = new Map(this.itemSizeCache.set(item.key, size)); | ||
this.notify(false); | ||
this.notify(true, false); | ||
} | ||
@@ -603,4 +593,5 @@ }; | ||
this.measure = () => { | ||
var _a, _b; | ||
this.itemSizeCache = /* @__PURE__ */ new Map(); | ||
this.notify(false); | ||
(_b = (_a = this.options).onChange) == null ? void 0 : _b.call(_a, this, false); | ||
}; | ||
@@ -614,3 +605,3 @@ this.setOptions(opts); | ||
}); | ||
this.maybeNotify(); | ||
this.notify(false, false); | ||
} | ||
@@ -653,2 +644,3 @@ } | ||
approxEqual, | ||
debounce, | ||
defaultKeyExtractor, | ||
@@ -655,0 +647,0 @@ defaultRangeExtractor, |
@@ -11,1 +11,2 @@ export type NoInfer<A extends any> = [A][A extends any ? 0 : never]; | ||
export declare const approxEqual: (a: number, b: number) => boolean; | ||
export declare const debounce: (fn: Function, ms: number) => (this: any, ...args: any[]) => void; |
@@ -54,4 +54,12 @@ function memo(getDeps, fn, opts) { | ||
const approxEqual = (a, b) => Math.abs(a - b) < 1; | ||
const debounce = (fn, ms) => { | ||
let timeoutId; | ||
return function(...args) { | ||
clearTimeout(timeoutId); | ||
timeoutId = setTimeout(() => fn.apply(this, args), ms); | ||
}; | ||
}; | ||
export { | ||
approxEqual, | ||
debounce, | ||
memo, | ||
@@ -58,0 +66,0 @@ notUndefined |
{ | ||
"name": "@tanstack/virtual-core", | ||
"version": "3.3.0", | ||
"version": "3.4.0", | ||
"description": "Headless UI for virtualizing scrollable elements in TS/JS + Frameworks", | ||
@@ -5,0 +5,0 @@ "author": "Tanner Linsley", |
154
src/index.ts
@@ -1,2 +0,2 @@ | ||
import { approxEqual, memo, notUndefined } from './utils' | ||
import { approxEqual, memo, notUndefined, debounce } from './utils' | ||
@@ -101,2 +101,6 @@ export * from './utils' | ||
const addEventListenerOptions = { | ||
passive: true, | ||
} | ||
export const observeWindowRect = ( | ||
@@ -116,5 +120,3 @@ instance: Virtualizer<Window, any>, | ||
element.addEventListener('resize', handler, { | ||
passive: true, | ||
}) | ||
element.addEventListener('resize', handler, addEventListenerOptions) | ||
@@ -126,5 +128,8 @@ return () => { | ||
const supportsScrollend = | ||
typeof window == 'undefined' ? true : 'onscrollend' in window | ||
export const observeElementOffset = <T extends Element>( | ||
instance: Virtualizer<T, any>, | ||
cb: (offset: number) => void, | ||
cb: (offset: number, isScrolling: boolean) => void, | ||
) => { | ||
@@ -136,13 +141,24 @@ const element = instance.scrollElement | ||
const handler = () => { | ||
cb(element[instance.options.horizontal ? 'scrollLeft' : 'scrollTop']) | ||
let offset = 0 | ||
const fallback = supportsScrollend | ||
? () => undefined | ||
: debounce(() => { | ||
cb(offset, false) | ||
}, 150) | ||
const createHandler = (isScrolling: boolean) => () => { | ||
offset = element[instance.options.horizontal ? 'scrollLeft' : 'scrollTop'] | ||
fallback() | ||
cb(offset, isScrolling) | ||
} | ||
handler() | ||
const handler = createHandler(true) | ||
const endHandler = createHandler(false) | ||
endHandler() | ||
element.addEventListener('scroll', handler, { | ||
passive: true, | ||
}) | ||
element.addEventListener('scroll', handler, addEventListenerOptions) | ||
element.addEventListener('scrollend', endHandler, addEventListenerOptions) | ||
return () => { | ||
element.removeEventListener('scroll', handler) | ||
element.removeEventListener('scrollend', endHandler) | ||
} | ||
@@ -153,3 +169,3 @@ } | ||
instance: Virtualizer<Window, any>, | ||
cb: (offset: number) => void, | ||
cb: (offset: number, isScrolling: boolean) => void, | ||
) => { | ||
@@ -161,13 +177,24 @@ const element = instance.scrollElement | ||
const handler = () => { | ||
cb(element[instance.options.horizontal ? 'scrollX' : 'scrollY']) | ||
let offset = 0 | ||
const fallback = supportsScrollend | ||
? () => undefined | ||
: debounce(() => { | ||
cb(offset, false) | ||
}, 150) | ||
const createHandler = (isScrolling: boolean) => () => { | ||
offset = element[instance.options.horizontal ? 'scrollX' : 'scrollY'] | ||
fallback() | ||
cb(offset, isScrolling) | ||
} | ||
handler() | ||
const handler = createHandler(true) | ||
const endHandler = createHandler(false) | ||
endHandler() | ||
element.addEventListener('scroll', handler, { | ||
passive: true, | ||
}) | ||
element.addEventListener('scroll', handler, addEventListenerOptions) | ||
element.addEventListener('scrollend', endHandler, addEventListenerOptions) | ||
return () => { | ||
element.removeEventListener('scroll', handler) | ||
element.removeEventListener('scrollend', endHandler) | ||
} | ||
@@ -250,3 +277,3 @@ } | ||
instance: Virtualizer<TScrollElement, TItemElement>, | ||
cb: (offset: number) => void, | ||
cb: (offset: number, isScrolling: boolean) => void, | ||
) => void | (() => void) | ||
@@ -277,3 +304,2 @@ | ||
gap?: number | ||
scrollingDelay?: number | ||
indexAttribute?: string | ||
@@ -292,3 +318,2 @@ initialMeasurementsCache?: VirtualItem[] | ||
isScrolling: boolean = false | ||
private isScrollingTimeoutId: ReturnType<typeof setTimeout> | null = null | ||
private scrollToIndexTimeoutId: ReturnType<typeof setTimeout> | null = null | ||
@@ -348,3 +373,3 @@ measurementsCache: VirtualItem[] = [] | ||
this.maybeNotify() | ||
this.notify(false, false) | ||
} | ||
@@ -373,3 +398,2 @@ | ||
gap: 0, | ||
scrollingDelay: 150, | ||
indexAttribute: 'data-index', | ||
@@ -382,30 +406,18 @@ initialMeasurementsCache: [], | ||
private notify = (sync: boolean) => { | ||
this.options.onChange?.(this, sync) | ||
private notify = (force: boolean, sync: boolean) => { | ||
const { startIndex, endIndex } = this.range ?? { | ||
startIndex: undefined, | ||
endIndex: undefined, | ||
} | ||
const range = this.calculateRange() | ||
if ( | ||
force || | ||
startIndex !== range?.startIndex || | ||
endIndex !== range?.endIndex | ||
) { | ||
this.options.onChange?.(this, sync) | ||
} | ||
} | ||
private maybeNotify = memo( | ||
() => { | ||
this.calculateRange() | ||
return [ | ||
this.isScrolling, | ||
this.range ? this.range.startIndex : null, | ||
this.range ? this.range.endIndex : null, | ||
] | ||
}, | ||
(isScrolling) => { | ||
this.notify(isScrolling) | ||
}, | ||
{ | ||
key: process.env.NODE_ENV !== 'production' && 'maybeNotify', | ||
debug: () => this.options.debug, | ||
initialDeps: [ | ||
this.isScrolling, | ||
this.range ? this.range.startIndex : null, | ||
this.range ? this.range.endIndex : null, | ||
] as [boolean, number | null, number | null], | ||
}, | ||
) | ||
private cleanup = () => { | ||
@@ -441,3 +453,3 @@ this.unsubs.filter(Boolean).forEach((d) => d!()) | ||
this.scrollRect = rect | ||
this.maybeNotify() | ||
this.notify(false, false) | ||
}), | ||
@@ -447,28 +459,15 @@ ) | ||
this.unsubs.push( | ||
this.options.observeElementOffset(this, (offset) => { | ||
this.options.observeElementOffset(this, (offset, isScrolling) => { | ||
this.scrollAdjustments = 0 | ||
if (this.scrollOffset === offset) { | ||
return | ||
} | ||
if (this.isScrollingTimeoutId !== null) { | ||
clearTimeout(this.isScrollingTimeoutId) | ||
this.isScrollingTimeoutId = null | ||
} | ||
this.isScrolling = true | ||
this.scrollDirection = | ||
this.scrollOffset < offset ? 'forward' : 'backward' | ||
this.scrollDirection = isScrolling | ||
? this.scrollOffset < offset | ||
? 'forward' | ||
: 'backward' | ||
: null | ||
this.scrollOffset = offset | ||
this.maybeNotify() | ||
const prevIsScrolling = this.isScrolling | ||
this.isScrolling = isScrolling | ||
this.isScrollingTimeoutId = setTimeout(() => { | ||
this.isScrollingTimeoutId = null | ||
this.isScrolling = false | ||
this.scrollDirection = null | ||
this.maybeNotify() | ||
}, this.options.scrollingDelay) | ||
this.notify(prevIsScrolling !== isScrolling, isScrolling) | ||
}), | ||
@@ -483,3 +482,3 @@ ) | ||
private memoOptions = memo( | ||
private getMeasurementOptions = memo( | ||
() => [ | ||
@@ -547,3 +546,3 @@ this.options.count, | ||
private getMeasurements = memo( | ||
() => [this.memoOptions(), this.itemSizeCache], | ||
() => [this.getMeasurementOptions(), this.itemSizeCache], | ||
({ count, paddingStart, scrollMargin, getItemKey }, itemSizeCache) => { | ||
@@ -631,3 +630,4 @@ const min = | ||
: rangeExtractor({ | ||
...range, | ||
startIndex: range.startIndex, | ||
endIndex: range.endIndex, | ||
overscan, | ||
@@ -711,3 +711,3 @@ count, | ||
this.notify(false) | ||
this.notify(true, false) | ||
} | ||
@@ -939,3 +939,3 @@ } | ||
this.itemSizeCache = new Map() | ||
this.notify(false) | ||
this.options.onChange?.(this, false) | ||
} | ||
@@ -942,0 +942,0 @@ } |
@@ -80,1 +80,9 @@ export type NoInfer<A extends any> = [A][A extends any ? 0 : never] | ||
export const approxEqual = (a: number, b: number) => Math.abs(a - b) < 1 | ||
export const debounce = (fn: Function, ms: number) => { | ||
let timeoutId: ReturnType<typeof setTimeout> | ||
return function (this: any, ...args: any[]) { | ||
clearTimeout(timeoutId) | ||
timeoutId = setTimeout(() => fn.apply(this, args), ms) | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
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
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
178616
2447
10