@tamagui/react-native-web-lite
Version:
React Native for Web
150 lines (149 loc) • 5.36 kB
JavaScript
import { invariant } from "@tamagui/react-native-web-internals";
class ViewabilityHelper {
_config;
_hasInteracted = false;
_timers = /* @__PURE__ */new Set();
_viewableIndices = [];
_viewableItems = /* @__PURE__ */new Map();
constructor(config = {
viewAreaCoveragePercentThreshold: 0
}) {
this._config = config;
}
/**
* Cleanup, e.g. on unmount. Clears any pending timers.
*/
dispose() {
this._timers.forEach(clearTimeout);
}
/**
* Determines which items are viewable based on the current metrics and config.
*/
computeViewableItems(props, scrollOffset, viewportHeight, getFrameMetrics, renderRange) {
const itemCount = props.getItemCount(props.data);
const {
itemVisiblePercentThreshold,
viewAreaCoveragePercentThreshold
} = this._config;
const viewAreaMode = viewAreaCoveragePercentThreshold != null;
const viewablePercentThreshold = viewAreaMode ? viewAreaCoveragePercentThreshold : itemVisiblePercentThreshold;
invariant(viewablePercentThreshold != null && itemVisiblePercentThreshold != null !== (viewAreaCoveragePercentThreshold != null), "Must set exactly one of itemVisiblePercentThreshold or viewAreaCoveragePercentThreshold");
const viewableIndices = [];
if (itemCount === 0) {
return viewableIndices;
}
let firstVisible = -1;
const {
first,
last
} = renderRange || {
first: 0,
last: itemCount - 1
};
if (last >= itemCount) {
console.warn("Invalid render range computing viewability " + JSON.stringify({
renderRange,
itemCount
}));
return [];
}
for (let idx = first; idx <= last; idx++) {
const metrics = getFrameMetrics(idx, props);
if (!metrics) {
continue;
}
const top = metrics.offset - scrollOffset;
const bottom = top + metrics.length;
if (top < viewportHeight && bottom > 0) {
firstVisible = idx;
if (_isViewable(viewAreaMode, viewablePercentThreshold, top, bottom, viewportHeight, metrics.length)) {
viewableIndices.push(idx);
}
} else if (firstVisible >= 0) {
break;
}
}
return viewableIndices;
}
/**
* Figures out which items are viewable and how that has changed from before and calls
* `onViewableItemsChanged` as appropriate.
*/
onUpdate(props, scrollOffset, viewportHeight, getFrameMetrics, createViewToken, onViewableItemsChanged, renderRange) {
const itemCount = props.getItemCount(props.data);
if (this._config.waitForInteraction && !this._hasInteracted || itemCount === 0 || !getFrameMetrics(0, props)) {
return;
}
let viewableIndices = [];
if (itemCount) {
viewableIndices = this.computeViewableItems(props, scrollOffset, viewportHeight, getFrameMetrics, renderRange);
}
if (this._viewableIndices.length === viewableIndices.length && this._viewableIndices.every((v, ii) => v === viewableIndices[ii])) {
return;
}
this._viewableIndices = viewableIndices;
if (this._config.minimumViewTime) {
const handle = setTimeout(() => {
this._timers.delete(handle);
this._onUpdateSync(props, viewableIndices, onViewableItemsChanged, createViewToken);
}, this._config.minimumViewTime);
this._timers.add(handle);
} else {
this._onUpdateSync(props, viewableIndices, onViewableItemsChanged, createViewToken);
}
}
resetViewableIndices() {
this._viewableIndices = [];
}
recordInteraction() {
this._hasInteracted = true;
}
_onUpdateSync(props, viewableIndicesToCheck, onViewableItemsChanged, createViewToken) {
viewableIndicesToCheck = viewableIndicesToCheck.filter(ii => this._viewableIndices.includes(ii));
const prevItems = this._viewableItems;
const nextItems = new Map(viewableIndicesToCheck.map(ii => {
const viewable = createViewToken(ii, true, props);
return [viewable.key, viewable];
}));
const changed = [];
for (const [key, viewable] of nextItems) {
if (!prevItems.has(key)) {
changed.push(viewable);
}
}
for (const [key, viewable] of prevItems) {
if (!nextItems.has(key)) {
changed.push({
...viewable,
isViewable: false
});
}
}
if (changed.length > 0) {
this._viewableItems = nextItems;
onViewableItemsChanged({
viewableItems: Array.from(nextItems.values()),
changed
});
}
}
}
function _isViewable(viewAreaMode, viewablePercentThreshold, top, bottom, viewportHeight, itemLength) {
if (_isEntirelyVisible(top, bottom, viewportHeight)) {
return true;
} else {
const pixels = _getPixelsVisible(top, bottom, viewportHeight);
const percent = 100 * (viewAreaMode ? pixels / viewportHeight : pixels / itemLength);
return percent >= viewablePercentThreshold;
}
}
function _getPixelsVisible(top, bottom, viewportHeight) {
const visibleHeight = Math.min(bottom, viewportHeight) - Math.max(top, 0);
return Math.max(0, visibleHeight);
}
function _isEntirelyVisible(top, bottom, viewportHeight) {
return top >= 0 && bottom <= viewportHeight && bottom > top;
}
var ViewabilityHelper_default = ViewabilityHelper;
export { ViewabilityHelper, ViewabilityHelper_default as default };
//# sourceMappingURL=ViewabilityHelper.mjs.map