@tamagui/react-native-web-lite
Version:
React Native for Web
970 lines (969 loc) • 43.7 kB
JavaScript
import { invariant, StyleSheet } from "@tamagui/react-native-web-internals";
import ViewabilityHelper from "../ViewabilityHelper.mjs";
import { CellRenderMask } from "./CellRenderMask.mjs";
import StateSafePureComponent from "./StateSafePureComponent.mjs";
import { VirtualizedListCellContextProvider, VirtualizedListContext, VirtualizedListContextProvider } from "./VirtualizedListContext.mjs";
import ScrollView from "../../../ScrollView/ScrollViewBase.mjs";
import View from "../../../View";
import CellRenderer from "./VirtualizedListCellRenderer.mjs";
import FillRateHelper from "../FillRateHelper";
import ChildListCollection from "./ChildListCollection.mjs";
import React from "react";
import clamp from "../Utilities/clamp.mjs";
import { keyExtractor as defaultKeyExtractor } from "../VirtualizeUtils";
import Batchinator from "../../../Batchinator";
import RefreshControl from "../../../RefreshControl";
import { jsx, jsxs } from "react/jsx-runtime";
const __DEV__ = process.env.NODE_ENV !== "production",
ON_EDGE_REACHED_EPSILON = 1e-3;
let _usedIndexForKey = !1,
_keylessItemComponentName = "";
function horizontalOrDefault(horizontal) {
return horizontal ?? !1;
}
function initialNumToRenderOrDefault(initialNumToRender) {
return initialNumToRender ?? 10;
}
function maxToRenderPerBatchOrDefault(maxToRenderPerBatch) {
return maxToRenderPerBatch ?? 10;
}
function onStartReachedThresholdOrDefault(onStartReachedThreshold) {
return onStartReachedThreshold ?? 2;
}
function onEndReachedThresholdOrDefault(onEndReachedThreshold) {
return onEndReachedThreshold ?? 2;
}
function getScrollingThreshold(threshold, visibleLength) {
return threshold * visibleLength / 2;
}
function scrollEventThrottleOrDefault(scrollEventThrottle) {
return scrollEventThrottle ?? 50;
}
function windowSizeOrDefault(windowSize) {
return windowSize ?? 21;
}
function findLastWhere(arr, predicate) {
for (let i = arr.length - 1; i >= 0; i--) if (predicate(arr[i])) return arr[i];
return null;
}
class VirtualizedList extends StateSafePureComponent {
static contextType = VirtualizedListContext;
// scrollToEnd may be janky without getItemLayout prop
scrollToEnd(params) {
const animated = params ? params.animated : !0,
veryLast = this.props.getItemCount(this.props.data) - 1;
if (veryLast < 0) return;
const frame = this.__getFrameMetricsApprox(veryLast, this.props),
offset = Math.max(0, frame.offset + frame.length + this._footerLength - this._scrollMetrics.visibleLength);
if (this._scrollRef != null) {
if (this._scrollRef.scrollTo == null) {
console.warn("No scrollTo method provided. This may be because you have two nested VirtualizedLists with the same orientation, or because you are using a custom component that does not implement scrollTo.");
return;
}
this._scrollRef.scrollTo(horizontalOrDefault(this.props.horizontal) ? {
x: offset,
animated
} : {
y: offset,
animated
});
}
}
// scrollToIndex may be janky without getItemLayout prop
scrollToIndex(params) {
const {
data,
horizontal,
getItemCount,
getItemLayout,
onScrollToIndexFailed
} = this.props,
{
animated,
index,
viewOffset,
viewPosition
} = params;
if (invariant(index >= 0, `scrollToIndex out of range: requested index ${index} but minimum is 0`), invariant(getItemCount(data) >= 1, `scrollToIndex out of range: item length ${getItemCount(data)} but minimum is 1`), invariant(index < getItemCount(data), `scrollToIndex out of range: requested index ${index} is out of 0 to ${getItemCount(data) - 1}`), !getItemLayout && index > this._highestMeasuredFrameIndex) {
invariant(!!onScrollToIndexFailed, "scrollToIndex should be used in conjunction with getItemLayout or onScrollToIndexFailed, otherwise there is no way to know the location of offscreen indices or handle failures."), onScrollToIndexFailed({
averageItemLength: this._averageCellLength,
highestMeasuredFrameIndex: this._highestMeasuredFrameIndex,
index
});
return;
}
const frame = this.__getFrameMetricsApprox(Math.floor(index), this.props),
offset = Math.max(0,
// @ts-ignore
this._getOffsetApprox(index, this.props) - (viewPosition || 0) * (this._scrollMetrics.visibleLength - frame.length)) - (viewOffset || 0);
if (this._scrollRef != null) {
if (this._scrollRef.scrollTo == null) {
console.warn("No scrollTo method provided. This may be because you have two nested VirtualizedLists with the same orientation, or because you are using a custom component that does not implement scrollTo.");
return;
}
this._scrollRef.scrollTo(horizontal ? {
x: offset,
animated
} : {
y: offset,
animated
});
}
}
// scrollToItem may be janky without getItemLayout prop. Required linear scan through items -
// use scrollToIndex instead if possible.
scrollToItem(params) {
const {
item
} = params,
{
data,
getItem,
getItemCount
} = this.props,
itemCount = getItemCount(data);
for (let index = 0; index < itemCount; index++) if (getItem(data, index) === item) {
this.scrollToIndex({
...params,
index
});
break;
}
}
/**
* Scroll to a specific content pixel offset in the list.
*
* Param `offset` expects the offset to scroll to.
* In case of `horizontal` is true, the offset is the x-value,
* in any other case the offset is the y-value.
*
* Param `animated` (`true` by default) defines whether the list
* should do an animation while scrolling.
*/
scrollToOffset(params) {
const {
animated,
offset
} = params;
if (this._scrollRef != null) {
if (this._scrollRef.scrollTo == null) {
console.warn("No scrollTo method provided. This may be because you have two nested VirtualizedLists with the same orientation, or because you are using a custom component that does not implement scrollTo.");
return;
}
this._scrollRef.scrollTo(horizontalOrDefault(this.props.horizontal) ? {
x: offset,
animated
} : {
y: offset,
animated
});
}
}
recordInteraction() {
this._nestedChildLists.forEach(childList => {
childList.recordInteraction();
}), this._viewabilityTuples.forEach(t => {
t.viewabilityHelper.recordInteraction();
}), this._updateViewableItems(this.props, this.state.cellsAroundViewport);
}
flashScrollIndicators() {
this._scrollRef != null && this._scrollRef.flashScrollIndicators();
}
/**
* Provides a handle to the underlying scroll responder.
* Note that `this._scrollRef` might not be a `ScrollView`, so we
* need to check that it responds to `getScrollResponder` before calling it.
*/
getScrollResponder() {
return this._scrollRef && this._scrollRef.getScrollResponder ? this._scrollRef.getScrollResponder() : null;
}
getScrollableNode() {
return this._scrollRef && this._scrollRef.getScrollableNode ? this._scrollRef.getScrollableNode() : this._scrollRef;
}
getScrollRef() {
return this._scrollRef && this._scrollRef.getScrollRef ? this._scrollRef.getScrollRef() : this._scrollRef;
}
_getCellKey() {
return this.context?.cellKey || "rootList";
}
_getScrollMetrics = () => this._scrollMetrics;
hasMore() {
return this._hasMore;
}
_getOutermostParentListRef = () => this._isNestedWithSameOrientation() ? this.context.getOutermostParentListRef() : this;
_registerAsNestedChild = childList => {
this._nestedChildLists.add(childList.ref, childList.cellKey), this._hasInteracted && childList.ref.recordInteraction();
};
_unregisterAsNestedChild = childList => {
this._nestedChildLists.remove(childList.ref);
};
invertedWheelEventHandler;
constructor(props) {
if (super(props), this._checkProps(props), this._fillRateHelper = new FillRateHelper(this._getFrameMetrics), this._updateCellsToRenderBatcher = new Batchinator(this._updateCellsToRender, this.props.updateCellsBatchingPeriod ?? 50), this.props.viewabilityConfigCallbackPairs) this._viewabilityTuples = this.props.viewabilityConfigCallbackPairs.map(pair => ({
viewabilityHelper: new ViewabilityHelper(pair.viewabilityConfig),
onViewableItemsChanged: pair.onViewableItemsChanged
}));else {
const {
onViewableItemsChanged,
viewabilityConfig
} = this.props;
onViewableItemsChanged && this._viewabilityTuples.push({
viewabilityHelper: new ViewabilityHelper(viewabilityConfig),
onViewableItemsChanged
});
}
const initialRenderRegion = VirtualizedList._initialRenderRegion(props);
this.state = {
cellsAroundViewport: initialRenderRegion,
renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion)
}, this.invertedWheelEventHandler = ev => {
const scrollOffset = this.props.horizontal ? ev.target.scrollLeft : ev.target.scrollTop,
scrollLength = this.props.horizontal ? ev.target.scrollWidth : ev.target.scrollHeight,
clientLength = this.props.horizontal ? ev.target.clientWidth : ev.target.clientHeight,
isEventTargetScrollable = scrollLength > clientLength,
delta = this.props.horizontal ? ev.deltaX || ev.wheelDeltaX : ev.deltaY || ev.wheelDeltaY;
let leftoverDelta = delta;
isEventTargetScrollable && (leftoverDelta = delta < 0 ? Math.min(delta + scrollOffset, 0) : Math.max(delta - (scrollLength - clientLength - scrollOffset), 0));
const targetDelta = delta - leftoverDelta;
if (this.props.inverted && this._scrollRef && this._scrollRef.getScrollableNode) {
const node = this._scrollRef.getScrollableNode();
if (this.props.horizontal) {
ev.target.scrollLeft += targetDelta;
const nextScrollLeft = node.scrollLeft - leftoverDelta;
node.scrollLeft = this.props.getItemLayout ? nextScrollLeft : Math.min(nextScrollLeft, this._totalCellLength);
} else {
ev.target.scrollTop += targetDelta;
const nextScrollTop = node.scrollTop - leftoverDelta;
node.scrollTop = this.props.getItemLayout ? nextScrollTop : Math.min(nextScrollTop, this._totalCellLength);
}
ev.preventDefault();
}
};
}
_checkProps(props) {
const {
onScroll,
windowSize,
getItemCount,
data,
initialScrollIndex
} = props;
invariant(!(onScroll && onScroll.__isNative), "Components based on VirtualizedList must be wrapped with Animated.createAnimatedComponent to support native onScroll events with useNativeDriver"), invariant(windowSizeOrDefault(windowSize) > 0, "VirtualizedList: The windowSize prop must be present and set to a value greater than 0."), invariant(getItemCount, 'VirtualizedList: The "getItemCount" prop must be provided');
const itemCount = getItemCount(data);
if (initialScrollIndex != null && !this._hasTriggeredInitialScrollToIndex && (initialScrollIndex < 0 || itemCount > 0 && initialScrollIndex >= itemCount) && !this._hasWarned.initialScrollIndex && (console.warn(`initialScrollIndex "${initialScrollIndex}" is not valid (list has ${itemCount} items)`), this._hasWarned.initialScrollIndex = !0), __DEV__ && !this._hasWarned.flexWrap) {
const flatStyles = StyleSheet.flatten(this.props.contentContainerStyle);
flatStyles != null && flatStyles.flexWrap === "wrap" && (console.warn("`flexWrap: `wrap`` is not supported with the `VirtualizedList` components.Consider using `numColumns` with `FlatList` instead."), this._hasWarned.flexWrap = !0);
}
}
static _createRenderMask(props, cellsAroundViewport, additionalRegions) {
const itemCount = props.getItemCount(props.data);
invariant(cellsAroundViewport.first >= 0 && cellsAroundViewport.last >= cellsAroundViewport.first - 1 && cellsAroundViewport.last < itemCount, `Invalid cells around viewport "[${cellsAroundViewport.first}, ${cellsAroundViewport.last}]" was passed to VirtualizedList._createRenderMask`);
const renderMask = new CellRenderMask(itemCount);
if (itemCount > 0) {
const allRegions = [cellsAroundViewport, ...(additionalRegions ?? [])];
for (const region of allRegions) renderMask.addCells(region);
if (props.initialScrollIndex == null || props.initialScrollIndex <= 0) {
const initialRegion = VirtualizedList._initialRenderRegion(props);
renderMask.addCells(initialRegion);
}
const stickyIndicesSet = new Set(props.stickyHeaderIndices);
VirtualizedList._ensureClosestStickyHeader(props, stickyIndicesSet, renderMask, cellsAroundViewport.first);
}
return renderMask;
}
static _initialRenderRegion(props) {
const itemCount = props.getItemCount(props.data),
firstCellIndex = Math.max(0, Math.min(itemCount - 1, Math.floor(props.initialScrollIndex ?? 0))),
lastCellIndex = Math.min(itemCount, firstCellIndex + initialNumToRenderOrDefault(props.initialNumToRender)) - 1;
return {
first: firstCellIndex,
last: lastCellIndex
};
}
static _ensureClosestStickyHeader(props, stickyIndicesSet, renderMask, cellIdx) {
const stickyOffset = props.ListHeaderComponent ? 1 : 0;
for (let itemIdx = cellIdx - 1; itemIdx >= 0; itemIdx--) if (stickyIndicesSet.has(itemIdx + stickyOffset)) {
renderMask.addCells({
first: itemIdx,
last: itemIdx
});
break;
}
}
_adjustCellsAroundViewport(props, cellsAroundViewport) {
const {
data,
getItemCount
} = props,
onEndReachedThreshold = onEndReachedThresholdOrDefault(props.onEndReachedThreshold),
{
contentLength,
offset,
visibleLength
} = this._scrollMetrics,
distanceFromEnd = contentLength - visibleLength - offset;
if (visibleLength <= 0 || contentLength <= 0) return cellsAroundViewport.last >= getItemCount(data) ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) : cellsAroundViewport;
let newCellsAroundViewport;
if (props.disableVirtualization) {
const renderAhead = distanceFromEnd < onEndReachedThreshold * visibleLength ? maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch) : 0;
newCellsAroundViewport = {
first: 0,
last: Math.min(cellsAroundViewport.last + renderAhead, getItemCount(data) - 1)
};
} else {
if (props.initialScrollIndex && !this._scrollMetrics.offset && Math.abs(distanceFromEnd) >= Number.EPSILON) return cellsAroundViewport.last >= getItemCount(data) ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) : cellsAroundViewport;
newCellsAroundViewport = computeWindowedRenderLimits(props, maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), windowSizeOrDefault(props.windowSize), cellsAroundViewport, this.__getFrameMetricsApprox, this._scrollMetrics), invariant(newCellsAroundViewport.last < getItemCount(data), "computeWindowedRenderLimits() should return range in-bounds");
}
if (this._nestedChildLists.size() > 0) {
const childIdx = this._findFirstChildWithMore(newCellsAroundViewport.first, newCellsAroundViewport.last);
newCellsAroundViewport.last = childIdx ?? newCellsAroundViewport.last;
}
return newCellsAroundViewport;
}
_findFirstChildWithMore(first, last) {
for (let ii = first; ii <= last; ii++) {
const cellKeyForIndex = this._indicesToKeys.get(ii);
if (cellKeyForIndex != null && this._nestedChildLists.anyInCell(cellKeyForIndex, childList => childList.hasMore())) return ii;
}
return null;
}
componentDidMount() {
this._isNestedWithSameOrientation() && this.context.registerAsNestedChild({
ref: this,
cellKey: this.context.cellKey
}), this.setupWebWheelHandler();
}
componentWillUnmount() {
this._isNestedWithSameOrientation() && this.context.unregisterAsNestedChild({
ref: this
}), this._updateCellsToRenderBatcher.dispose({
abort: !0
}), this._viewabilityTuples.forEach(tuple => {
tuple.viewabilityHelper.dispose();
}), this._fillRateHelper.deactivateAndFlush(), this.teardownWebWheelHandler();
}
setupWebWheelHandler() {
if (this._scrollRef && this._scrollRef.getScrollableNode) this._scrollRef.getScrollableNode().addEventListener("wheel", this.invertedWheelEventHandler);else {
setTimeout(() => this.setupWebWheelHandler(), 50);
return;
}
}
teardownWebWheelHandler() {
this._scrollRef && this._scrollRef.getScrollableNode && this._scrollRef.getScrollableNode().removeEventListener("wheel", this.invertedWheelEventHandler);
}
static getDerivedStateFromProps(newProps, prevState) {
if (newProps.getItemCount(newProps.data) === prevState.renderMask.numCells()) return prevState;
const constrainedCells = VirtualizedList._constrainToItemCount(prevState.cellsAroundViewport, newProps);
return {
cellsAroundViewport: constrainedCells,
renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells)
};
}
_pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, first, last, inversionStyle) {
const {
CellRendererComponent,
ItemSeparatorComponent,
ListHeaderComponent,
ListItemComponent,
data,
debug,
getItem,
getItemCount,
getItemLayout,
horizontal,
renderItem
} = this.props,
stickyOffset = ListHeaderComponent ? 1 : 0,
end = getItemCount(data) - 1;
let prevCellKey;
last = Math.min(end, last);
for (let ii = first; ii <= last; ii++) {
const item = getItem(data, ii),
key = this._keyExtractor(item, ii, this.props);
this._indicesToKeys.set(ii, key), stickyIndicesFromProps.has(ii + stickyOffset) && stickyHeaderIndices.push(cells.length);
const shouldListenForLayout = getItemLayout == null || debug || this._fillRateHelper.enabled();
cells.push(/* @__PURE__ */jsx(CellRenderer, {
CellRendererComponent,
ItemSeparatorComponent: ii < end ? ItemSeparatorComponent : void 0,
ListItemComponent,
cellKey: key,
horizontal,
index: ii,
inversionStyle,
item,
prevCellKey,
onUpdateSeparators: this._onUpdateSeparators,
onCellFocusCapture: e => this._onCellFocusCapture(key),
onUnmount: this._onCellUnmount,
ref: ref => {
this._cellRefs[key] = ref;
},
renderItem,
...(shouldListenForLayout && {
onCellLayout: this._onCellLayout
})
}, key)), prevCellKey = key;
}
}
static _constrainToItemCount(cells, props) {
const itemCount = props.getItemCount(props.data),
last = Math.min(itemCount - 1, cells.last),
maxToRenderPerBatch = maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch);
return {
first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first),
last
};
}
_onUpdateSeparators = (keys, newProps) => {
keys.forEach(key => {
const ref = key != null && this._cellRefs[key];
ref && ref.updateSeparatorProps(newProps);
});
};
_isNestedWithSameOrientation() {
const nestedContext = this.context;
return !!(nestedContext && !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal));
}
_getSpacerKey = isVertical => isVertical ? "height" : "width";
_keyExtractor(item, index, props) {
if (props.keyExtractor != null) return props.keyExtractor(item, index);
const key = defaultKeyExtractor(item, index);
return key === String(index) && (_usedIndexForKey = !0, item.type && item.type.displayName && (_keylessItemComponentName = item.type.displayName)), key;
}
render() {
this._checkProps(this.props);
const {
ListEmptyComponent,
ListFooterComponent,
ListHeaderComponent
} = this.props,
{
data,
horizontal
} = this.props,
inversionStyle = this.props.inverted ? horizontalOrDefault(this.props.horizontal) ? styles.horizontallyInverted : styles.verticallyInverted : null,
cells = [],
stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices),
stickyHeaderIndices = [];
if (ListHeaderComponent) {
stickyIndicesFromProps.has(0) && stickyHeaderIndices.push(0);
const element = React.isValidElement(ListHeaderComponent) ? ListHeaderComponent : /* @__PURE__ */jsx(ListHeaderComponent, {});
cells.push(/* @__PURE__ */jsx(VirtualizedListCellContextProvider, {
cellKey: this._getCellKey() + "-header",
children: /* @__PURE__ */jsx(View, {
onLayout: this._onLayoutHeader,
style: [inversionStyle, this.props.ListHeaderComponentStyle],
children: element
})
}, "$header"));
}
const itemCount = this.props.getItemCount(data);
if (itemCount === 0 && ListEmptyComponent) {
const element = React.isValidElement(ListEmptyComponent) ? ListEmptyComponent : /* @__PURE__ */jsx(ListEmptyComponent, {});
cells.push(/* @__PURE__ */jsx(VirtualizedListCellContextProvider, {
cellKey: this._getCellKey() + "-empty",
children: React.cloneElement(element, {
onLayout: event => {
this._onLayoutEmpty(event), element.props.onLayout && element.props.onLayout(event);
},
style: [inversionStyle, element.props.style]
})
}, "$empty"));
}
if (itemCount > 0) {
_usedIndexForKey = !1, _keylessItemComponentName = "";
const spacerKey = this._getSpacerKey(!horizontal),
renderRegions = this.state.renderMask.enumerateRegions(),
lastSpacer = findLastWhere(renderRegions, r => r.isSpacer);
for (const section of renderRegions) if (section.isSpacer) {
if (this.props.disableVirtualization) continue;
const last = section === lastSpacer && !this.props.getItemLayout ? clamp(section.first - 1, section.last, this._highestMeasuredFrameIndex) : section.last,
firstMetrics = this.__getFrameMetricsApprox(section.first, this.props),
lastMetrics = this.__getFrameMetricsApprox(last, this.props),
spacerSize = lastMetrics.offset + lastMetrics.length - firstMetrics.offset;
cells.push(/* @__PURE__ */jsx(View, {
style: {
[spacerKey]: spacerSize
}
}, `$spacer-${section.first}`));
} else this._pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, section.first, section.last, inversionStyle);
!this._hasWarned.keys && _usedIndexForKey && (console.warn("VirtualizedList: missing keys for items, make sure to specify a key or id property on each item or provide a custom keyExtractor.", _keylessItemComponentName), this._hasWarned.keys = !0);
}
if (ListFooterComponent) {
const element = React.isValidElement(ListFooterComponent) ? ListFooterComponent : /* @__PURE__ */jsx(ListFooterComponent, {});
cells.push(/* @__PURE__ */jsx(VirtualizedListCellContextProvider, {
cellKey: this._getFooterCellKey(),
children: /* @__PURE__ */jsx(View, {
onLayout: this._onLayoutFooter,
style: [inversionStyle, this.props.ListFooterComponentStyle],
children: element
})
}, "$footer"));
}
const scrollProps = {
...this.props,
onContentSizeChange: this._onContentSizeChange,
onLayout: this._onLayout,
onScroll: this._onScroll,
onScrollBeginDrag: this._onScrollBeginDrag,
onScrollEndDrag: this._onScrollEndDrag,
onMomentumScrollBegin: this._onMomentumScrollBegin,
onMomentumScrollEnd: this._onMomentumScrollEnd,
scrollEventThrottle: scrollEventThrottleOrDefault(this.props.scrollEventThrottle),
invertStickyHeaders: this.props.invertStickyHeaders !== void 0 ? this.props.invertStickyHeaders : this.props.inverted,
stickyHeaderIndices,
style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style
};
return this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1, /* @__PURE__ */jsx(VirtualizedListContextProvider, {
value: {
cellKey: null,
getScrollMetrics: this._getScrollMetrics,
horizontal: horizontalOrDefault(this.props.horizontal),
getOutermostParentListRef: this._getOutermostParentListRef,
registerAsNestedChild: this._registerAsNestedChild,
unregisterAsNestedChild: this._unregisterAsNestedChild
},
children: React.cloneElement((this.props.renderScrollComponent || this._defaultRenderScrollComponent)(scrollProps), {
ref: this._captureScrollRef
}, cells)
});
}
componentDidUpdate(prevProps) {
const {
data,
extraData
} = this.props;
(data !== prevProps.data || extraData !== prevProps.extraData) && this._viewabilityTuples.forEach(tuple => {
tuple.viewabilityHelper.resetViewableIndices();
});
const hiPriInProgress = this._hiPriInProgress;
this._scheduleCellsToRenderUpdate(), hiPriInProgress && (this._hiPriInProgress = !1);
}
_averageCellLength = 0;
_cellRefs = {};
_fillRateHelper;
_frames = {};
_footerLength = 0;
_hasTriggeredInitialScrollToIndex = !1;
_hasInteracted = !1;
_hasMore = !1;
_hasWarned = {};
_headerLength = 0;
_hiPriInProgress = !1;
_highestMeasuredFrameIndex = 0;
_indicesToKeys = /* @__PURE__ */new Map();
_lastFocusedCellKey = null;
_nestedChildLists = new ChildListCollection();
_offsetFromParentVirtualizedList = 0;
_prevParentOffset = 0;
_scrollMetrics = {
contentLength: 0,
dOffset: 0,
dt: 10,
offset: 0,
timestamp: 0,
velocity: 0,
visibleLength: 0,
zoomScale: 1
};
_scrollRef = null;
_sentStartForContentLength = 0;
_sentEndForContentLength = 0;
_totalCellLength = 0;
_totalCellsMeasured = 0;
_updateCellsToRenderBatcher;
_viewabilityTuples = [];
_captureScrollRef = ref => {
this._scrollRef = ref;
};
_computeBlankness() {
this._fillRateHelper.computeBlankness(
// @ts-ignore
this.props, this.state.cellsAroundViewport, this._scrollMetrics);
}
_defaultRenderScrollComponent = props => {
const onRefresh = props.onRefresh;
return this._isNestedWithSameOrientation() ? /* @__PURE__ */jsx(View, {
...props
}) : onRefresh ? (invariant(typeof props.refreshing == "boolean", "`refreshing` prop must be set as a boolean in order to use `onRefresh`, but got `" + JSON.stringify(props.refreshing ?? "undefined") + "`"), /* @__PURE__ */jsx(ScrollView, {
...props,
refreshControl: props.refreshControl == null ? /* @__PURE__ */jsx(RefreshControl, {
refreshing: props.refreshing,
onRefresh,
progressViewOffset: props.progressViewOffset
}) : props.refreshControl
})) : /* @__PURE__ */jsx(ScrollView, {
...props
});
};
_onCellLayout = (e, cellKey, index) => {
const layout = e.nativeEvent.layout,
next = {
offset: this._selectOffset(layout),
length: this._selectLength(layout),
index,
inLayout: !0
},
curr = this._frames[cellKey];
!curr || next.offset !== curr.offset || next.length !== curr.length || index !== curr.index ? (this._totalCellLength += next.length - (curr ? curr.length : 0), this._totalCellsMeasured += curr ? 0 : 1, this._averageCellLength = this._totalCellLength / this._totalCellsMeasured, this._frames[cellKey] = next, this._highestMeasuredFrameIndex = Math.max(this._highestMeasuredFrameIndex, index), this._scheduleCellsToRenderUpdate()) : this._frames[cellKey].inLayout = !0, this._triggerRemeasureForChildListsInCell(cellKey), this._computeBlankness(), this._updateViewableItems(
// @ts-ignore
this.props, this.state.cellsAroundViewport);
};
_onCellFocusCapture(cellKey) {
this._lastFocusedCellKey = cellKey, this._updateCellsToRender();
}
_onCellUnmount = cellKey => {
delete this._cellRefs[cellKey];
const curr = this._frames[cellKey];
curr && (this._frames[cellKey] = {
...curr,
inLayout: !1
});
};
_triggerRemeasureForChildListsInCell(cellKey) {
this._nestedChildLists.forEachInCell(cellKey, childList => {
childList.measureLayoutRelativeToContainingList();
});
}
measureLayoutRelativeToContainingList() {
try {
if (!this._scrollRef) return;
this._scrollRef.measureLayout(this.context.getOutermostParentListRef().getScrollRef(), (x, y, width, height) => {
this._offsetFromParentVirtualizedList = this._selectOffset({
x,
y
}), this._scrollMetrics.contentLength = this._selectLength({
width,
height
});
const scrollMetrics = this._convertParentScrollMetrics(this.context.getScrollMetrics());
(this._scrollMetrics.visibleLength !== scrollMetrics.visibleLength || this._scrollMetrics.offset !== scrollMetrics.offset) && (this._scrollMetrics.visibleLength = scrollMetrics.visibleLength, this._scrollMetrics.offset = scrollMetrics.offset, this._nestedChildLists.forEach(childList => {
childList.measureLayoutRelativeToContainingList();
}));
}, error => {
console.warn("VirtualizedList: Encountered an error while measuring a list's offset from its containing VirtualizedList.");
});
} catch (error) {
console.warn("measureLayoutRelativeToContainingList threw an error", error.stack);
}
}
_onLayout = e => {
this._isNestedWithSameOrientation() ? this.measureLayoutRelativeToContainingList() : this._scrollMetrics.visibleLength = this._selectLength(e.nativeEvent.layout), this.props.onLayout && this.props.onLayout(e), this._scheduleCellsToRenderUpdate(), this._maybeCallOnEdgeReached();
};
_onLayoutEmpty = e => {
this.props.onLayout && this.props.onLayout(e);
};
_getFooterCellKey() {
return this._getCellKey() + "-footer";
}
_onLayoutFooter = e => {
this._triggerRemeasureForChildListsInCell(this._getFooterCellKey()), this._footerLength = this._selectLength(e.nativeEvent.layout);
};
_onLayoutHeader = e => {
this._headerLength = this._selectLength(e.nativeEvent.layout);
};
_renderDebugOverlay() {
const normalize = this._scrollMetrics.visibleLength / (this._scrollMetrics.contentLength || 1),
framesInLayout = [],
itemCount = this.props.getItemCount(this.props.data);
for (let ii = 0; ii < itemCount; ii++) {
const frame = this.__getFrameMetricsApprox(ii, this.props);
frame.inLayout && framesInLayout.push(frame);
}
const windowTop = this.__getFrameMetricsApprox(this.state.cellsAroundViewport.first, this.props).offset,
frameLast = this.__getFrameMetricsApprox(this.state.cellsAroundViewport.last, this.props),
windowLen = frameLast.offset + frameLast.length - windowTop,
visTop = this._scrollMetrics.offset,
visLen = this._scrollMetrics.visibleLength;
return /* @__PURE__ */jsxs(View, {
style: [styles.debugOverlayBase, styles.debugOverlay],
children: [framesInLayout.map((f, ii) => /* @__PURE__ */jsx(View, {
style: [styles.debugOverlayBase, styles.debugOverlayFrame, {
top: f.offset * normalize,
height: f.length * normalize
}]
}, "f" + ii)), /* @__PURE__ */jsx(View, {
style: [styles.debugOverlayBase, styles.debugOverlayFrameLast, {
top: windowTop * normalize,
height: windowLen * normalize
}]
}), /* @__PURE__ */jsx(View, {
style: [styles.debugOverlayBase, styles.debugOverlayFrameVis, {
top: visTop * normalize,
height: visLen * normalize
}]
})]
});
}
_selectLength(metrics) {
return horizontalOrDefault(this.props.horizontal) ? metrics.width : metrics.height;
}
_selectOffset(metrics) {
return horizontalOrDefault(this.props.horizontal) ? metrics.x : metrics.y;
}
_maybeCallOnEdgeReached() {
const {
data,
getItemCount,
onStartReached,
onStartReachedThreshold,
onEndReached,
onEndReachedThreshold,
initialScrollIndex
} = this.props,
{
contentLength,
visibleLength,
offset
} = this._scrollMetrics;
let distanceFromStart = offset,
distanceFromEnd = contentLength - visibleLength - offset;
distanceFromStart < ON_EDGE_REACHED_EPSILON && (distanceFromStart = 0), distanceFromEnd < ON_EDGE_REACHED_EPSILON && (distanceFromEnd = 0);
const DEFAULT_THRESHOLD_PX = 2,
startThreshold = onStartReachedThreshold != null ? onStartReachedThreshold * visibleLength : DEFAULT_THRESHOLD_PX,
endThreshold = onEndReachedThreshold != null ? onEndReachedThreshold * visibleLength : DEFAULT_THRESHOLD_PX,
isWithinStartThreshold = distanceFromStart <= startThreshold,
isWithinEndThreshold = distanceFromEnd <= endThreshold;
onEndReached && this.state.cellsAroundViewport.last === getItemCount(data) - 1 && isWithinEndThreshold && this._scrollMetrics.contentLength !== this._sentEndForContentLength ? (this._sentEndForContentLength = this._scrollMetrics.contentLength, onEndReached({
distanceFromEnd
})) : onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this._scrollMetrics.contentLength !== this._sentStartForContentLength ? (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) && (this._sentStartForContentLength = this._scrollMetrics.contentLength, onStartReached({
distanceFromStart
})) : (this._sentStartForContentLength = isWithinStartThreshold ? this._sentStartForContentLength : 0, this._sentEndForContentLength = isWithinEndThreshold ? this._sentEndForContentLength : 0);
}
_onContentSizeChange = (width, height) => {
width > 0 && height > 0 && this.props.initialScrollIndex != null && this.props.initialScrollIndex > 0 && !this._hasTriggeredInitialScrollToIndex && (this.props.contentOffset == null && (this.props.initialScrollIndex < this.props.getItemCount(this.props.data) ? this.scrollToIndex({
animated: !1,
index: nullthrows(this.props.initialScrollIndex)
}) : this.scrollToEnd({
animated: !1
})), this._hasTriggeredInitialScrollToIndex = !0), this.props.onContentSizeChange && this.props.onContentSizeChange(width, height), this._scrollMetrics.contentLength = this._selectLength({
height,
width
}), this._scheduleCellsToRenderUpdate(), this._maybeCallOnEdgeReached();
};
_convertParentScrollMetrics = metrics => {
const offset = metrics.offset - this._offsetFromParentVirtualizedList,
visibleLength = metrics.visibleLength,
dOffset = offset - this._scrollMetrics.offset,
contentLength = this._scrollMetrics.contentLength;
return {
visibleLength,
contentLength,
offset,
dOffset
};
};
_onScroll = e => {
this._nestedChildLists.forEach(childList => {
childList._onScroll(e);
}), this.props.onScroll && this.props.onScroll(e);
const timestamp = e.timeStamp;
let visibleLength = this._selectLength(e.nativeEvent.layoutMeasurement),
contentLength = this._selectLength(e.nativeEvent.contentSize),
offset = this._selectOffset(e.nativeEvent.contentOffset),
dOffset = offset - this._scrollMetrics.offset;
if (this._isNestedWithSameOrientation()) {
if (this._scrollMetrics.contentLength === 0) return;
({
visibleLength,
contentLength,
offset,
dOffset
} = this._convertParentScrollMetrics({
visibleLength,
offset
}));
}
const dt = this._scrollMetrics.timestamp ? Math.max(1, timestamp - this._scrollMetrics.timestamp) : 1,
velocity = dOffset / dt;
dt > 500 && this._scrollMetrics.dt > 500 && contentLength > 5 * visibleLength && !this._hasWarned.perf && (infoLog("VirtualizedList: You have a large list that is slow to update - make sure your renderItem function renders components that follow React performance best practices like PureComponent, shouldComponentUpdate, etc.", {
dt,
prevDt: this._scrollMetrics.dt,
contentLength
}), this._hasWarned.perf = !0);
const zoomScale = e.nativeEvent.zoomScale < 0 ? 1 : e.nativeEvent.zoomScale;
this._scrollMetrics = {
contentLength,
dt,
dOffset,
offset,
timestamp,
velocity,
visibleLength,
zoomScale
}, this._updateViewableItems(this.props, this.state.cellsAroundViewport), this.props && (this._maybeCallOnEdgeReached(), velocity !== 0 && this._fillRateHelper.activate(), this._computeBlankness(), this._scheduleCellsToRenderUpdate());
};
_scheduleCellsToRenderUpdate() {
const {
first,
last
} = this.state.cellsAroundViewport,
{
offset,
visibleLength,
velocity
} = this._scrollMetrics,
itemCount = this.props.getItemCount(this.props.data);
let hiPri = !1;
const onStartReachedThreshold = onStartReachedThresholdOrDefault(this.props.onStartReachedThreshold),
onEndReachedThreshold = onEndReachedThresholdOrDefault(this.props.onEndReachedThreshold);
if (first > 0) {
const distTop = offset - this.__getFrameMetricsApprox(first, this.props).offset;
hiPri = distTop < 0 || velocity < -2 && distTop < getScrollingThreshold(onStartReachedThreshold, visibleLength);
}
if (!hiPri && last >= 0 && last < itemCount - 1) {
const distBottom = this.__getFrameMetricsApprox(last, this.props).offset - (offset + visibleLength);
hiPri = distBottom < 0 || velocity > 2 && distBottom < getScrollingThreshold(onEndReachedThreshold, visibleLength);
}
if (hiPri && (this._averageCellLength || this.props.getItemLayout) && !this._hiPriInProgress) {
this._hiPriInProgress = !0, this._updateCellsToRenderBatcher.dispose({
abort: !0
}), this._updateCellsToRender();
return;
} else this._updateCellsToRenderBatcher.schedule();
}
_onScrollBeginDrag = e => {
this._nestedChildLists.forEach(childList => {
childList._onScrollBeginDrag(e);
}), this._viewabilityTuples.forEach(tuple => {
tuple.viewabilityHelper.recordInteraction();
}), this._hasInteracted = !0, this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e);
};
_onScrollEndDrag = e => {
this._nestedChildLists.forEach(childList => {
childList._onScrollEndDrag(e);
});
const {
velocity
} = e.nativeEvent;
velocity && (this._scrollMetrics.velocity = this._selectOffset(velocity)), this._computeBlankness(), this.props.onScrollEndDrag && this.props.onScrollEndDrag(e);
};
_onMomentumScrollBegin = e => {
this._nestedChildLists.forEach(childList => {
childList._onMomentumScrollBegin(e);
}), this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e);
};
_onMomentumScrollEnd = e => {
this._nestedChildLists.forEach(childList => {
childList._onMomentumScrollEnd(e);
}), this._scrollMetrics.velocity = 0, this._computeBlankness(), this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e);
};
_updateCellsToRender = () => {
this._updateViewableItems(this.props, this.state.cellsAroundViewport), this.setState((state, props) => {
const cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport),
renderMask = VirtualizedList._createRenderMask(props, cellsAroundViewport, this._getNonViewportRenderRegions(props));
return cellsAroundViewport.first === state.cellsAroundViewport.first && cellsAroundViewport.last === state.cellsAroundViewport.last && renderMask.equals(state.renderMask) ? null : {
cellsAroundViewport,
renderMask
};
});
};
_createViewToken = (index, isViewable, props) => {
const {
data,
getItem
} = props,
item = getItem(data, index);
return {
index,
item,
key: this._keyExtractor(item, index, props),
isViewable
};
};
_getOffsetApprox = (index, props) => {
if (Number.isInteger(index)) return this.__getFrameMetricsApprox(index, props).offset;
{
const frameMetrics = this.__getFrameMetricsApprox(Math.floor(index), props),
remainder = index - Math.floor(index);
return frameMetrics.offset + remainder * frameMetrics.length;
}
};
__getFrameMetricsApprox = (index, props) => {
const frame = this._getFrameMetrics(index, props);
if (frame && frame.index === index) return frame;
{
const {
data,
getItemCount,
getItemLayout
} = props;
return invariant(index >= 0 && index < getItemCount(data), "Tried to get frame for out of range index " + index), invariant(!getItemLayout, "Should not have to estimate frames when a measurement metrics function is provided"), {
length: this._averageCellLength,
offset: this._averageCellLength * index
};
}
};
_getFrameMetrics = (index, props) => {
const {
data,
getItem,
getItemCount,
getItemLayout
} = props;
invariant(index >= 0 && index < getItemCount(data), "Tried to get frame for out of range index " + index);
const item = getItem(data, index),
frame = this._frames[this._keyExtractor(item, index, props)];
return (!frame || frame.index !== index) && getItemLayout ? getItemLayout(data, index) : frame;
};
_getNonViewportRenderRegions = props => {
if (!(this._lastFocusedCellKey && this._cellRefs[this._lastFocusedCellKey])) return [];
const focusedCellIndex = this._cellRefs[this._lastFocusedCellKey].props.index,
itemCount = props.getItemCount(props.data);
if (focusedCellIndex >= itemCount || this._keyExtractor(props.getItem(props.data, focusedCellIndex), focusedCellIndex, props) !== this._lastFocusedCellKey) return [];
let first = focusedCellIndex,
heightOfCellsBeforeFocused = 0;
for (let i = first - 1; i >= 0 && heightOfCellsBeforeFocused < this._scrollMetrics.visibleLength; i--) first--, heightOfCellsBeforeFocused += this.__getFrameMetricsApprox(i, props).length;
let last = focusedCellIndex,
heightOfCellsAfterFocused = 0;
for (let i = last + 1; i < itemCount && heightOfCellsAfterFocused < this._scrollMetrics.visibleLength; i++) last++, heightOfCellsAfterFocused += this.__getFrameMetricsApprox(i, props).length;
return [{
first,
last
}];
};
_updateViewableItems(props, cellsAroundViewport) {
this._viewabilityTuples.forEach(tuple => {
tuple.viewabilityHelper.onUpdate(props, this._scrollMetrics.offset, this._scrollMetrics.visibleLength, this._getFrameMetrics, this._createViewToken, tuple.onViewableItemsChanged, cellsAroundViewport);
});
}
}
const styles = StyleSheet.create({
verticallyInverted: {
transform: "scaleY(-1)"
},
horizontallyInverted: {
transform: "scaleX(-1)"
},
debug: {
flex: 1
},
debugOverlayBase: {
position: "absolute",
top: 0,
right: 0
},
debugOverlay: {
bottom: 0,
width: 20,
borderColor: "blue",
borderWidth: 1
},
debugOverlayFrame: {
left: 0,
backgroundColor: "orange"
},
debugOverlayFrameLast: {
left: 0,
borderColor: "green",
borderWidth: 2
},
debugOverlayFrameVis: {
left: 0,
borderColor: "red",
borderWidth: 2
}
});
var VirtualizedList_default = VirtualizedList;
export { VirtualizedList_default as default };
//# sourceMappingURL=index.mjs.map