UNPKG

@legendapp/list

Version:

Legend List is a drop-in replacement for FlatList with much better performance and supporting dynamically sized items.

1,462 lines (1,450 loc) 90.6 kB
import * as React2 from 'react'; import React2__default, { useReducer, useEffect, createContext, useMemo, useRef, useCallback, useLayoutEffect, useImperativeHandle, useContext, useState, forwardRef, memo } from 'react'; import { View, Text, Platform, Animated, ScrollView, StyleSheet, Dimensions, RefreshControl } from 'react-native'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; import { LegendList as LegendList$1 } from '@legendapp/list'; // src/LegendList.tsx var ContextState = React2.createContext(null); function StateProvider({ children }) { const [value] = React2.useState(() => ({ listeners: /* @__PURE__ */ new Map(), values: /* @__PURE__ */ new Map([ ["paddingTop", 0], ["alignItemsPaddingTop", 0], ["stylePaddingTop", 0], ["headerSize", 0] ]), mapViewabilityCallbacks: /* @__PURE__ */ new Map(), mapViewabilityValues: /* @__PURE__ */ new Map(), mapViewabilityAmountCallbacks: /* @__PURE__ */ new Map(), mapViewabilityAmountValues: /* @__PURE__ */ new Map(), columnWrapperStyle: void 0, viewRefs: /* @__PURE__ */ new Map() })); return /* @__PURE__ */ React2.createElement(ContextState.Provider, { value }, children); } function useStateContext() { return React2.useContext(ContextState); } function createSelectorFunctionsArr(ctx, signalNames) { let lastValues = []; let lastSignalValues = []; return { subscribe: (cb) => { const listeners = []; for (const signalName of signalNames) { listeners.push(listen$(ctx, signalName, cb)); } return () => { for (const listener of listeners) { listener(); } }; }, get: () => { const currentValues = []; let hasChanged = false; for (let i = 0; i < signalNames.length; i++) { const value = peek$(ctx, signalNames[i]); currentValues.push(value); if (value !== lastSignalValues[i]) { hasChanged = true; } } lastSignalValues = currentValues; if (hasChanged) { lastValues = currentValues; } return lastValues; } }; } function listen$(ctx, signalName, cb) { const { listeners } = ctx; let setListeners = listeners.get(signalName); if (!setListeners) { setListeners = /* @__PURE__ */ new Set(); listeners.set(signalName, setListeners); } setListeners.add(cb); return () => setListeners.delete(cb); } function peek$(ctx, signalName) { const { values } = ctx; return values.get(signalName); } function set$(ctx, signalName, value) { const { listeners, values } = ctx; if (values.get(signalName) !== value) { values.set(signalName, value); const setListeners = listeners.get(signalName); if (setListeners) { for (const listener of setListeners) { listener(value); } } } } function getContentSize(ctx) { const { values } = ctx; const stylePaddingTop = values.get("stylePaddingTop") || 0; const headerSize = values.get("headerSize") || 0; const footerSize = values.get("footerSize") || 0; const totalSize = values.get("totalSize") || 0; return headerSize + footerSize + totalSize + stylePaddingTop; } function useArr$(signalNames) { const ctx = React2.useContext(ContextState); const { subscribe, get } = React2.useMemo(() => createSelectorFunctionsArr(ctx, signalNames), [ctx, signalNames]); const value = useSyncExternalStore(subscribe, get); return value; } function useSelector$(signalName, selector) { const ctx = React2.useContext(ContextState); const { subscribe, get } = React2.useMemo(() => createSelectorFunctionsArr(ctx, [signalName]), [ctx, signalName]); const value = useSyncExternalStore(subscribe, () => selector(get()[0])); return value; } // src/DebugView.tsx var DebugRow = ({ children }) => { return /* @__PURE__ */ React2.createElement(View, { style: { flexDirection: "row", alignItems: "center", justifyContent: "space-between" } }, children); }; var DebugView = React2.memo(function DebugView2({ state }) { const ctx = useStateContext(); const [ totalSize = 0, totalSizeWithScrollAdjust = 0, scrollAdjust = 0, rawScroll = 0, scroll = 0, numContainers = 0, numContainersPooled = 0 ] = useArr$([ "totalSize", "totalSizeWithScrollAdjust", "scrollAdjust", "debugRawScroll", "debugComputedScroll", "numContainers", "numContainersPooled" ]); const contentSize = getContentSize(ctx); const [, forceUpdate] = useReducer((x) => x + 1, 0); useInterval(() => { forceUpdate(); }, 100); return /* @__PURE__ */ React2.createElement( View, { style: { position: "absolute", top: 0, right: 0, paddingLeft: 4, paddingBottom: 4, // height: 100, backgroundColor: "#FFFFFFCC", padding: 4, borderRadius: 4 }, pointerEvents: "none" }, /* @__PURE__ */ React2.createElement(DebugRow, null, /* @__PURE__ */ React2.createElement(Text, null, "TotalSize:"), /* @__PURE__ */ React2.createElement(Text, null, totalSize.toFixed(2))), /* @__PURE__ */ React2.createElement(DebugRow, null, /* @__PURE__ */ React2.createElement(Text, null, "ContentSize:"), /* @__PURE__ */ React2.createElement(Text, null, contentSize.toFixed(2))), /* @__PURE__ */ React2.createElement(DebugRow, null, /* @__PURE__ */ React2.createElement(Text, null, "At end:"), /* @__PURE__ */ React2.createElement(Text, null, String(state.isAtEnd))), /* @__PURE__ */ React2.createElement(Text, null), /* @__PURE__ */ React2.createElement(DebugRow, null, /* @__PURE__ */ React2.createElement(Text, null, "ScrollAdjust:"), /* @__PURE__ */ React2.createElement(Text, null, scrollAdjust.toFixed(2))), /* @__PURE__ */ React2.createElement(DebugRow, null, /* @__PURE__ */ React2.createElement(Text, null, "TotalSizeReal: "), /* @__PURE__ */ React2.createElement(Text, null, totalSizeWithScrollAdjust.toFixed(2))), /* @__PURE__ */ React2.createElement(Text, null), /* @__PURE__ */ React2.createElement(DebugRow, null, /* @__PURE__ */ React2.createElement(Text, null, "RawScroll: "), /* @__PURE__ */ React2.createElement(Text, null, rawScroll.toFixed(2))), /* @__PURE__ */ React2.createElement(DebugRow, null, /* @__PURE__ */ React2.createElement(Text, null, "ComputedScroll: "), /* @__PURE__ */ React2.createElement(Text, null, scroll.toFixed(2))) ); }); function useInterval(callback, delay) { useEffect(() => { const interval = setInterval(callback, delay); return () => clearInterval(interval); }, [delay]); } // src/helpers.ts function isFunction(obj) { return typeof obj === "function"; } function isArray(obj) { return Array.isArray(obj); } var warned = /* @__PURE__ */ new Set(); function warnDevOnce(id, text) { if (__DEV__ && !warned.has(id)) { warned.add(id); console.warn(`[legend-list] ${text}`); } } function isNullOrUndefined(value) { return value === null || value === void 0; } function comparatorByDistance(a, b) { return b.distance - a.distance; } function comparatorDefault(a, b) { return a - b; } function getPadding(s, type) { var _a, _b, _c; return (_c = (_b = (_a = s[`padding${type}`]) != null ? _a : s.paddingVertical) != null ? _b : s.padding) != null ? _c : 0; } function extractPadding(style, contentContainerStyle, type) { return getPadding(style, type) + getPadding(contentContainerStyle, type); } var symbolFirst = Symbol(); function useInit(cb) { const refValue = useRef(symbolFirst); if (refValue.current === symbolFirst) { refValue.current = cb(); } return refValue.current; } // src/ContextContainer.ts var ContextContainer = createContext(null); function useViewability(callback, configId) { const ctx = useStateContext(); const { containerId } = useContext(ContextContainer); const key = containerId + (configId != null ? configId : ""); useInit(() => { const value = ctx.mapViewabilityValues.get(key); if (value) { callback(value); } }); ctx.mapViewabilityCallbacks.set(key, callback); useEffect( () => () => { ctx.mapViewabilityCallbacks.delete(key); }, [] ); } function useViewabilityAmount(callback) { const ctx = useStateContext(); const { containerId } = useContext(ContextContainer); useInit(() => { const value = ctx.mapViewabilityAmountValues.get(containerId); if (value) { callback(value); } }); ctx.mapViewabilityAmountCallbacks.set(containerId, callback); useEffect( () => () => { ctx.mapViewabilityAmountCallbacks.delete(containerId); }, [] ); } function useRecyclingEffect(effect) { const { index, value } = useContext(ContextContainer); const prevValues = useRef({ prevIndex: void 0, prevItem: void 0 }); useEffect(() => { let ret = void 0; if (prevValues.current.prevIndex !== void 0 && prevValues.current.prevItem !== void 0) { ret = effect({ index, item: value, prevIndex: prevValues.current.prevIndex, prevItem: prevValues.current.prevItem }); } prevValues.current = { prevIndex: index, prevItem: value }; return ret; }, [index, value]); } function useRecyclingState(valueOrFun) { const { index, value, itemKey, triggerLayout } = useContext(ContextContainer); const refState = useRef({ itemKey: null, value: null }); const [_, setRenderNum] = useState(0); if (refState.current.itemKey !== itemKey) { refState.current.itemKey = itemKey; refState.current.value = isFunction(valueOrFun) ? valueOrFun({ index, item: value, prevIndex: void 0, prevItem: void 0 }) : valueOrFun; } const setState = useCallback( (newState) => { refState.current.value = isFunction(newState) ? newState(refState.current.value) : newState; setRenderNum((v) => v + 1); triggerLayout(); }, [triggerLayout] ); return [refState.current.value, setState]; } function useIsLastItem() { const { itemKey } = useContext(ContextContainer); const isLast = useSelector$("lastItemKeys", (lastItemKeys) => (lastItemKeys == null ? void 0 : lastItemKeys.includes(itemKey)) || false); return isLast; } function useListScrollSize() { const [scrollSize] = useArr$(["scrollSize"]); return scrollSize; } var LeanViewComponent = React2.forwardRef((props, ref) => { return React2.createElement("RCTView", { ...props, ref }); }); LeanViewComponent.displayName = "RCTView"; var LeanView = Platform.OS === "android" || Platform.OS === "ios" ? LeanViewComponent : View; // src/constants.ts var POSITION_OUT_OF_VIEW = -1e7; var ANCHORED_POSITION_OUT_OF_VIEW = { type: "top", relativeCoordinate: POSITION_OUT_OF_VIEW, top: POSITION_OUT_OF_VIEW }; var ENABLE_DEVMODE = __DEV__ && false; var ENABLE_DEBUG_VIEW = __DEV__ && false; var IsNewArchitecture = global.nativeFabricUIManager != null; // src/Container.tsx var Container = ({ id, recycleItems, horizontal, getRenderedItem, updateItemSize, ItemSeparatorComponent }) => { const ctx = useStateContext(); const columnWrapperStyle = ctx.columnWrapperStyle; const [ maintainVisibleContentPosition, position = ANCHORED_POSITION_OUT_OF_VIEW, column = 0, numColumns, lastItemKeys, itemKey, data, extraData ] = useArr$([ "maintainVisibleContentPosition", `containerPosition${id}`, `containerColumn${id}`, "numColumns", "lastItemKeys", `containerItemKey${id}`, `containerItemData${id}`, "extraData" ]); const refLastSize = useRef(); const ref = useRef(null); const [layoutRenderCount, forceLayoutRender] = useState(0); const otherAxisPos = numColumns > 1 ? `${(column - 1) / numColumns * 100}%` : 0; const otherAxisSize = numColumns > 1 ? `${1 / numColumns * 100}%` : void 0; const isALastItem = lastItemKeys.includes(itemKey); let paddingStyles; if (columnWrapperStyle) { const { columnGap, rowGap, gap } = columnWrapperStyle; if (horizontal) { paddingStyles = { paddingRight: !isALastItem ? columnGap || gap || void 0 : void 0, paddingVertical: numColumns > 1 ? (rowGap || gap || 0) / 2 : void 0 }; } else { paddingStyles = { paddingBottom: !isALastItem ? rowGap || gap || void 0 : void 0, paddingHorizontal: numColumns > 1 ? (columnGap || gap || 0) / 2 : void 0 }; } } const style = horizontal ? { flexDirection: ItemSeparatorComponent ? "row" : void 0, position: "absolute", top: otherAxisPos, height: otherAxisSize, left: position.relativeCoordinate, ...paddingStyles || {} } : { position: "absolute", left: otherAxisPos, right: numColumns > 1 ? null : 0, width: otherAxisSize, top: position.relativeCoordinate, ...paddingStyles || {} }; const renderedItemInfo = useMemo( () => itemKey !== void 0 ? getRenderedItem(itemKey) : null, [itemKey, data, extraData] ); const { index, renderedItem } = renderedItemInfo || {}; const triggerLayout = useCallback(() => { forceLayoutRender((v) => v + 1); }, []); const onLayout = (event) => { var _a, _b; if (!isNullOrUndefined(itemKey)) { let layout = event.nativeEvent.layout; const size = layout[horizontal ? "width" : "height"]; const doUpdate = () => { refLastSize.current = { width: layout.width, height: layout.height }; updateItemSize(itemKey, layout); }; if (IsNewArchitecture || size > 0) { doUpdate(); } else { (_b = (_a = ref.current) == null ? void 0 : _a.measure) == null ? void 0 : _b.call(_a, (x, y, width, height) => { layout = { width, height }; doUpdate(); }); } } }; if (IsNewArchitecture) { useLayoutEffect(() => { var _a, _b; if (!isNullOrUndefined(itemKey)) { const measured = (_b = (_a = ref.current) == null ? void 0 : _a.unstable_getBoundingClientRect) == null ? void 0 : _b.call(_a); if (measured) { const size = Math.floor(measured[horizontal ? "width" : "height"] * 8) / 8; if (size) { updateItemSize(itemKey, measured); } } } }, [itemKey, layoutRenderCount, isALastItem]); } else { useEffect(() => { if (!isNullOrUndefined(itemKey)) { const timeout = setTimeout(() => { if (refLastSize.current) { updateItemSize(itemKey, refLastSize.current); } }, 16); return () => { clearTimeout(timeout); }; } }, [itemKey]); } const contextValue = useMemo(() => { ctx.viewRefs.set(id, ref); return { containerId: id, itemKey, index, value: data, triggerLayout }; }, [id, itemKey, index, data]); const contentFragment = /* @__PURE__ */ React2__default.createElement(React2__default.Fragment, { key: recycleItems ? void 0 : itemKey }, /* @__PURE__ */ React2__default.createElement(ContextContainer.Provider, { value: contextValue }, renderedItem, renderedItemInfo && ItemSeparatorComponent && !isALastItem && /* @__PURE__ */ React2__default.createElement(ItemSeparatorComponent, { leadingItem: renderedItemInfo.item }))); if (maintainVisibleContentPosition) { const anchorStyle = horizontal ? position.type === "top" ? { position: "absolute", left: 0, top: 0, bottom: 0, flexDirection: "row", alignItems: "stretch" } : { position: "absolute", right: 0, top: 0, bottom: 0, flexDirection: "row", alignItems: "stretch" } : position.type === "top" ? { position: "absolute", top: 0, left: 0, right: 0 } : { position: "absolute", bottom: 0, left: 0, right: 0 }; if (__DEV__ && ENABLE_DEVMODE) { anchorStyle.borderColor = position.type === "top" ? "red" : "blue"; anchorStyle.borderWidth = 1; } return /* @__PURE__ */ React2__default.createElement(LeanView, { style }, /* @__PURE__ */ React2__default.createElement(LeanView, { style: [anchorStyle, paddingStyles], onLayout, ref }, contentFragment, __DEV__ && ENABLE_DEVMODE && /* @__PURE__ */ React2__default.createElement(Text, { style: { position: "absolute", top: 0, left: 0, zIndex: 1e3 } }, position.top))); } return /* @__PURE__ */ React2__default.createElement(LeanView, { style, onLayout, ref }, contentFragment); }; var typedForwardRef = forwardRef; var typedMemo = memo; var useAnimatedValue = (initialValue) => { return useRef(new Animated.Value(initialValue)).current; }; // src/useValue$.ts function useValue$(key, getValue, useMicrotask) { var _a; const ctx = useStateContext(); const animValue = useAnimatedValue((_a = getValue ? getValue(peek$(ctx, key)) : peek$(ctx, key)) != null ? _a : 0); useMemo(() => { let newValue = void 0; listen$(ctx, key, (v) => { if (useMicrotask && newValue === void 0) { queueMicrotask(() => { animValue.setValue(newValue); newValue = void 0; }); } newValue = getValue ? getValue(v) : v; if (!useMicrotask) { animValue.setValue(newValue); } }); }, []); return animValue; } // src/Containers.tsx var Containers = typedMemo(function Containers2({ horizontal, recycleItems, ItemSeparatorComponent, waitForInitialLayout, updateItemSize, getRenderedItem }) { const ctx = useStateContext(); const columnWrapperStyle = ctx.columnWrapperStyle; const [numContainers, numColumns] = useArr$(["numContainersPooled", "numColumns"]); const animSize = useValue$( "totalSizeWithScrollAdjust", void 0, /*useMicrotask*/ true ); const animOpacity = waitForInitialLayout ? useValue$("containersDidLayout", (value) => value ? 1 : 0) : void 0; const otherAxisSize = useValue$( "otherAxisSize", void 0, /*useMicrotask*/ true ); const containers = []; for (let i = 0; i < numContainers; i++) { containers.push( /* @__PURE__ */ React2.createElement( Container, { id: i, key: i, recycleItems, horizontal, getRenderedItem, updateItemSize, ItemSeparatorComponent } ) ); } const style = horizontal ? { width: animSize, opacity: animOpacity, minHeight: otherAxisSize } : { height: animSize, opacity: animOpacity, minWidth: otherAxisSize }; if (columnWrapperStyle && numColumns > 1) { const { columnGap, rowGap, gap } = columnWrapperStyle; if (horizontal) { const my = (rowGap || gap || 0) / 2; if (my) { style.marginVertical = -my; } } else { const mx = (columnGap || gap || 0) / 2; if (mx) { style.marginHorizontal = -mx; } } } return /* @__PURE__ */ React2.createElement(Animated.View, { style }, containers); }); function ListHeaderComponentContainer({ children, style, ctx, horizontal, waitForInitialLayout }) { var _a; const hasData = ((_a = peek$(ctx, "lastItemKeys")) == null ? void 0 : _a.length) > 0; const scrollAdjust = useValue$("scrollAdjust", (v) => v != null ? v : 0, true); const animOpacity = waitForInitialLayout ? useValue$("containersDidLayout", (value) => value ? 1 : 0) : void 0; const additionalSize = { transform: [{ translateY: Animated.multiply(scrollAdjust, -1) }], // Header should show if there's no data yet, but containersDidLayout will be false until it has some data opacity: hasData ? animOpacity : 1 }; return /* @__PURE__ */ React2.createElement( Animated.View, { style: [style, additionalSize], onLayout: (event) => { const size = event.nativeEvent.layout[horizontal ? "width" : "height"]; set$(ctx, "headerSize", size); } }, children ); } // src/ListComponent.tsx var getComponent = (Component) => { if (React2.isValidElement(Component)) { return Component; } if (Component) { return /* @__PURE__ */ React2.createElement(Component, null); } return null; }; var PaddingAndAdjust = () => { const animPaddingTop = useValue$("paddingTop", (v) => v, true); const animScrollAdjust = useValue$("scrollAdjust", (v) => v, true); const additionalSize = { marginTop: animScrollAdjust, paddingTop: animPaddingTop }; return /* @__PURE__ */ React2.createElement(Animated.View, { style: additionalSize }); }; var PaddingAndAdjustDevMode = () => { const animPaddingTop = useValue$("paddingTop", (v) => v, true); const animScrollAdjust = useValue$("scrollAdjust", (v) => v, true); return /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(Animated.View, { style: { marginTop: animScrollAdjust } }), /* @__PURE__ */ React2.createElement(Animated.View, { style: { paddingTop: animPaddingTop } }), /* @__PURE__ */ React2.createElement( Animated.View, { style: { position: "absolute", top: Animated.add(animScrollAdjust, Animated.multiply(animScrollAdjust, -1)), height: animPaddingTop, left: 0, right: 0, backgroundColor: "green" } } ), /* @__PURE__ */ React2.createElement( Animated.View, { style: { position: "absolute", top: animPaddingTop, height: animScrollAdjust, left: -16, right: -16, backgroundColor: "lightblue" } } ), /* @__PURE__ */ React2.createElement( Animated.View, { style: { position: "absolute", top: animPaddingTop, height: Animated.multiply(animScrollAdjust, -1), width: 8, right: 4, borderStyle: "dashed", borderColor: "blue", borderWidth: 1, backgroundColor: "lightblue" //backgroundColor: "blue", } } )); }; var ListComponent = typedMemo(function ListComponent2({ style, contentContainerStyle, horizontal, initialContentOffset, recycleItems, ItemSeparatorComponent, alignItemsAtEnd, waitForInitialLayout, handleScroll, onLayout, ListHeaderComponent, ListHeaderComponentStyle, ListFooterComponent, ListFooterComponentStyle, ListEmptyComponent, getRenderedItem, updateItemSize, refScrollView, maintainVisibleContentPosition, renderScrollComponent, ...rest }) { const ctx = useStateContext(); const ScrollComponent = renderScrollComponent ? useMemo( () => React2.forwardRef((props, ref) => renderScrollComponent({ ...props, ref })), [renderScrollComponent] ) : ScrollView; return /* @__PURE__ */ React2.createElement( ScrollComponent, { ...rest, style, maintainVisibleContentPosition: maintainVisibleContentPosition && !ListEmptyComponent ? { minIndexForVisible: 0 } : void 0, contentContainerStyle: [ contentContainerStyle, horizontal ? { height: "100%" } : {} ], onScroll: handleScroll, onLayout, horizontal, contentOffset: initialContentOffset ? horizontal ? { x: initialContentOffset, y: 0 } : { x: 0, y: initialContentOffset } : void 0, ref: refScrollView }, ENABLE_DEVMODE ? /* @__PURE__ */ React2.createElement(PaddingAndAdjustDevMode, null) : /* @__PURE__ */ React2.createElement(PaddingAndAdjust, null), ListHeaderComponent && /* @__PURE__ */ React2.createElement( ListHeaderComponentContainer, { style: ListHeaderComponentStyle, ctx, horizontal, waitForInitialLayout }, getComponent(ListHeaderComponent) ), ListEmptyComponent && getComponent(ListEmptyComponent), /* @__PURE__ */ React2.createElement( Containers, { horizontal, recycleItems, waitForInitialLayout, getRenderedItem, ItemSeparatorComponent, updateItemSize } ), ListFooterComponent && /* @__PURE__ */ React2.createElement( View, { style: ListFooterComponentStyle, onLayout: (event) => { const size = event.nativeEvent.layout[horizontal ? "width" : "height"]; set$(ctx, "footerSize", size); } }, getComponent(ListFooterComponent) ) ); }); // src/ScrollAdjustHandler.ts var ScrollAdjustHandler = class { constructor(ctx) { this.ctx = ctx; this.appliedAdjust = 0; this.busy = false; this.isPaused = false; this.isDisabled = false; this.context = ctx; } doAjdust() { set$(this.context, "scrollAdjust", this.appliedAdjust); this.busy = false; } requestAdjust(adjust, onAdjusted) { if (this.isDisabled) { return; } const oldAdjustTop = peek$(this.context, "scrollAdjust"); if (oldAdjustTop === adjust) { return; } this.appliedAdjust = adjust; if (!this.busy && !this.isPaused) { this.busy = true; this.doAjdust(); onAdjusted(oldAdjustTop - adjust); } } getAppliedAdjust() { return this.appliedAdjust; } pauseAdjust() { this.isPaused = true; } setDisableAdjust(disable) { this.isDisabled = disable; } // return true if it was paused unPauseAdjust() { if (this.isPaused) { this.isPaused = false; this.doAjdust(); return true; } return false; } }; var useCombinedRef = (...refs) => { const callback = useCallback((element) => { for (const ref of refs) { if (!ref) { continue; } if (isFunction(ref)) { ref(element); } else { ref.current = element; } } }, refs); return callback; }; // src/viewability.ts var mapViewabilityConfigCallbackPairs = /* @__PURE__ */ new Map(); function setupViewability(props) { let { viewabilityConfig, viewabilityConfigCallbackPairs, onViewableItemsChanged } = props; if (viewabilityConfig || onViewableItemsChanged) { viewabilityConfigCallbackPairs = [ ...viewabilityConfigCallbackPairs || [], { viewabilityConfig: viewabilityConfig || { viewAreaCoveragePercentThreshold: 0 }, onViewableItemsChanged } ]; } if (viewabilityConfigCallbackPairs) { for (const pair of viewabilityConfigCallbackPairs) { mapViewabilityConfigCallbackPairs.set(pair.viewabilityConfig.id, { viewableItems: [], start: -1, end: -1, previousStart: -1, previousEnd: -1 }); } } return viewabilityConfigCallbackPairs; } function updateViewableItems(state, ctx, viewabilityConfigCallbackPairs, getId, scrollSize, start, end) { for (const viewabilityConfigCallbackPair of viewabilityConfigCallbackPairs) { const viewabilityState = mapViewabilityConfigCallbackPairs.get( viewabilityConfigCallbackPair.viewabilityConfig.id ); viewabilityState.start = start; viewabilityState.end = end; if (viewabilityConfigCallbackPair.viewabilityConfig.minimumViewTime) { const timer = setTimeout(() => { state.timeouts.delete(timer); updateViewableItemsWithConfig(state.data, viewabilityConfigCallbackPair, getId, state, ctx, scrollSize); }, viewabilityConfigCallbackPair.viewabilityConfig.minimumViewTime); state.timeouts.add(timer); } else { updateViewableItemsWithConfig(state.data, viewabilityConfigCallbackPair, getId, state, ctx, scrollSize); } } } function updateViewableItemsWithConfig(data, viewabilityConfigCallbackPair, getId, state, ctx, scrollSize) { const { viewabilityConfig, onViewableItemsChanged } = viewabilityConfigCallbackPair; const configId = viewabilityConfig.id; const viewabilityState = mapViewabilityConfigCallbackPairs.get(configId); const { viewableItems: previousViewableItems, start, end } = viewabilityState; const viewabilityTokens = /* @__PURE__ */ new Map(); for (const [containerId, value] of ctx.mapViewabilityAmountValues) { viewabilityTokens.set( containerId, computeViewability( state, ctx, viewabilityConfig, containerId, value.key, scrollSize, value.item, value.index ) ); } const changed = []; if (previousViewableItems) { for (const viewToken of previousViewableItems) { const containerId = findContainerId(ctx, viewToken.key); if (!isViewable( state, ctx, viewabilityConfig, containerId, viewToken.key, scrollSize, viewToken.item, viewToken.index )) { viewToken.isViewable = false; changed.push(viewToken); } } } const viewableItems = []; for (let i = start; i <= end; i++) { const item = data[i]; if (item) { const key = getId(i); const containerId = findContainerId(ctx, key); if (isViewable(state, ctx, viewabilityConfig, containerId, key, scrollSize, item, i)) { const viewToken = { item, key, index: i, isViewable: true, containerId }; viewableItems.push(viewToken); if (!(previousViewableItems == null ? void 0 : previousViewableItems.find((v) => v.key === viewToken.key))) { changed.push(viewToken); } } } } Object.assign(viewabilityState, { viewableItems, previousStart: start, previousEnd: end }); if (changed.length > 0) { viewabilityState.viewableItems = viewableItems; for (let i = 0; i < changed.length; i++) { const change = changed[i]; maybeUpdateViewabilityCallback(ctx, configId, change.containerId, change); } if (onViewableItemsChanged) { onViewableItemsChanged({ viewableItems, changed }); } } for (const [containerId, value] of ctx.mapViewabilityAmountValues) { if (value.sizeVisible < 0) { ctx.mapViewabilityAmountValues.delete(containerId); } } } function computeViewability(state, ctx, viewabilityConfig, containerId, key, scrollSize, item, index) { const { sizes, positions, scroll: scrollState, scrollAdjustHandler } = state; const topPad = (peek$(ctx, "stylePaddingTop") || 0) + (peek$(ctx, "headerSize") || 0); const { itemVisiblePercentThreshold, viewAreaCoveragePercentThreshold } = viewabilityConfig; const viewAreaMode = viewAreaCoveragePercentThreshold != null; const viewablePercentThreshold = viewAreaMode ? viewAreaCoveragePercentThreshold : itemVisiblePercentThreshold; const previousScrollAdjust = scrollAdjustHandler.getAppliedAdjust(); const scroll = scrollState - previousScrollAdjust - topPad; const top = positions.get(key) - scroll; const size = sizes.get(key) || 0; const bottom = top + size; const isEntirelyVisible = top >= 0 && bottom <= scrollSize && bottom > top; const sizeVisible = isEntirelyVisible ? size : Math.min(bottom, scrollSize) - Math.max(top, 0); const percentVisible = size ? isEntirelyVisible ? 100 : 100 * (sizeVisible / size) : 0; const percentOfScroller = size ? 100 * (sizeVisible / scrollSize) : 0; const percent = isEntirelyVisible ? 100 : viewAreaMode ? percentOfScroller : percentVisible; const isViewable2 = percent >= viewablePercentThreshold; const value = { index, isViewable: isViewable2, item, key, percentVisible, percentOfScroller, sizeVisible, size, scrollSize, containerId }; if (JSON.stringify(value) !== JSON.stringify(ctx.mapViewabilityAmountValues.get(containerId))) { ctx.mapViewabilityAmountValues.set(containerId, value); const cb = ctx.mapViewabilityAmountCallbacks.get(containerId); if (cb) { cb(value); } } return value; } function isViewable(state, ctx, viewabilityConfig, containerId, key, scrollSize, item, index) { const value = ctx.mapViewabilityAmountValues.get(containerId) || computeViewability(state, ctx, viewabilityConfig, containerId, key, scrollSize, item, index); return value.isViewable; } function findContainerId(ctx, key) { const numContainers = peek$(ctx, "numContainers"); for (let i = 0; i < numContainers; i++) { const itemKey = peek$(ctx, `containerItemKey${i}`); if (itemKey === key) { return i; } } return -1; } function maybeUpdateViewabilityCallback(ctx, configId, containerId, viewToken) { const key = containerId + configId; ctx.mapViewabilityValues.set(key, viewToken); const cb = ctx.mapViewabilityCallbacks.get(key); cb == null ? void 0 : cb(viewToken); } // src/LegendList.tsx var DEFAULT_DRAW_DISTANCE = 250; var DEFAULT_ITEM_SIZE = 100; function createColumnWrapperStyle(contentContainerStyle) { const { gap, columnGap, rowGap } = contentContainerStyle; if (gap || columnGap || rowGap) { contentContainerStyle.gap = void 0; contentContainerStyle.columnGap = void 0; contentContainerStyle.rowGap = void 0; return { gap, columnGap, rowGap }; } } var LegendList = typedForwardRef(function LegendList2(props, forwardedRef) { return /* @__PURE__ */ React2.createElement(StateProvider, null, /* @__PURE__ */ React2.createElement(LegendListInner, { ...props, ref: forwardedRef })); }); var LegendListInner = typedForwardRef(function LegendListInner2(props, forwardedRef) { const { data: dataProp = [], initialScrollIndex: initialScrollIndexProp, initialScrollOffset, horizontal, drawDistance = 250, recycleItems = false, onEndReachedThreshold = 0.5, onStartReachedThreshold = 0.5, maintainScrollAtEnd = false, maintainScrollAtEndThreshold = 0.1, alignItemsAtEnd = false, maintainVisibleContentPosition = false, onScroll: onScrollProp, onMomentumScrollEnd, numColumns: numColumnsProp = 1, columnWrapperStyle, keyExtractor: keyExtractorProp, renderItem: renderItem2, estimatedListSize, estimatedItemSize: estimatedItemSizeProp, getEstimatedItemSize, suggestEstimatedItemSize, ListHeaderComponent, ListEmptyComponent, onItemSizeChanged, refScrollView, waitForInitialLayout = true, extraData, contentContainerStyle: contentContainerStyleProp, style: styleProp, onLayout: onLayoutProp, onRefresh, refreshing, progressViewOffset, refreshControl, initialContainerPoolRatio = 2, viewabilityConfig, viewabilityConfigCallbackPairs, onViewableItemsChanged, ...rest } = props; const initialScroll = typeof initialScrollIndexProp === "number" ? { index: initialScrollIndexProp } : initialScrollIndexProp; const initialScrollIndex = initialScroll == null ? void 0 : initialScroll.index; const refLoadStartTime = useRef(Date.now()); const callbacks = useRef({ onStartReached: rest.onStartReached, onEndReached: rest.onEndReached }); callbacks.current.onStartReached = rest.onStartReached; callbacks.current.onEndReached = rest.onEndReached; const contentContainerStyle = { ...StyleSheet.flatten(contentContainerStyleProp) }; const style = { ...StyleSheet.flatten(styleProp) }; const stylePaddingTopState = extractPadding(style, contentContainerStyle, "Top"); const stylePaddingBottomState = extractPadding(style, contentContainerStyle, "Bottom"); if (style == null ? void 0 : style.paddingTop) { style.paddingTop = void 0; } if (contentContainerStyle == null ? void 0 : contentContainerStyle.paddingTop) { contentContainerStyle.paddingTop = void 0; } const ctx = useStateContext(); ctx.columnWrapperStyle = columnWrapperStyle || (contentContainerStyle ? createColumnWrapperStyle(contentContainerStyle) : void 0); const refScroller = useRef(null); const combinedRef = useCombinedRef(refScroller, refScrollView); const estimatedItemSize = estimatedItemSizeProp != null ? estimatedItemSizeProp : DEFAULT_ITEM_SIZE; const scrollBuffer = (drawDistance != null ? drawDistance : DEFAULT_DRAW_DISTANCE) || 1; const keyExtractor = keyExtractorProp != null ? keyExtractorProp : (item, index) => index.toString(); const refState = useRef(); const getId = (index) => { var _a; const data = (_a = refState.current) == null ? void 0 : _a.data; if (!data) { return ""; } const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; return `${ret}`; }; const getItemSize = (key, index, data, useAverageSize = false) => { const state = refState.current; state.sizesKnown.get(key); const sizePrevious = state.sizes.get(key); let size; peek$(ctx, "numColumns"); if (size === void 0 && sizePrevious !== void 0) { return sizePrevious; } if (size === void 0) { size = getEstimatedItemSize ? getEstimatedItemSize(index, data) : estimatedItemSize; } state.sizes.set(key, size); return size; }; const calculateOffsetForIndex = (indexParam) => { var _a; const isFromInit = indexParam === void 0; const index = isFromInit ? initialScrollIndex : indexParam; const data = dataProp; if (index !== void 0) { let offset = 0; const canGetSize = !!refState.current; if (canGetSize || getEstimatedItemSize) { const sizeFn = (index2) => { if (canGetSize) { return getItemSize(getId(index2), index2, data[index2], true); } return getEstimatedItemSize(index2, data[index2]); }; for (let i = 0; i < index; i++) { offset += sizeFn(i); } } else { offset = index * estimatedItemSize; } const adjust = peek$(ctx, "containersDidLayout") ? ((_a = refState.current) == null ? void 0 : _a.scrollAdjustHandler.getAppliedAdjust()) || 0 : 0; const stylePaddingTop = isFromInit ? stylePaddingTopState : peek$(ctx, "stylePaddingTop"); const topPad = (stylePaddingTop != null ? stylePaddingTop : 0) + peek$(ctx, "headerSize"); return offset / numColumnsProp - adjust + topPad; } return 0; }; const calculateOffsetWithOffsetPosition = (offsetParam, params) => { const { index, viewOffset, viewPosition } = params; let offset = offsetParam; const state = refState.current; if (viewOffset) { offset -= viewOffset; } if (viewPosition !== void 0 && index !== void 0) { offset -= viewPosition * (state.scrollLength - getItemSize(getId(index), index, state.data[index])); } return offset; }; const initialContentOffset = initialScrollOffset != null ? initialScrollOffset : useMemo(() => calculateOffsetForIndex(void 0), []); if (!refState.current) { const initialScrollLength = (estimatedListSize != null ? estimatedListSize : Dimensions.get("window"))[horizontal ? "width" : "height"]; refState.current = { sizes: /* @__PURE__ */ new Map(), positions: /* @__PURE__ */ new Map(), columns: /* @__PURE__ */ new Map(), pendingAdjust: 0, isStartReached: initialContentOffset < initialScrollLength * onStartReachedThreshold, isEndReached: false, isAtEnd: false, isAtStart: false, data: dataProp, scrollLength: initialScrollLength, startBuffered: -1, startNoBuffer: -1, endBuffered: -1, endNoBuffer: -1, scroll: initialContentOffset || 0, totalSize: 0, totalSizeBelowAnchor: 0, timeouts: /* @__PURE__ */ new Set(), viewabilityConfigCallbackPairs: void 0, renderItem: void 0, scrollAdjustHandler: new ScrollAdjustHandler(ctx), nativeMarginTop: 0, scrollPrev: 0, scrollPrevTime: 0, scrollTime: 0, scrollPending: 0, indexByKey: /* @__PURE__ */ new Map(), scrollHistory: [], scrollVelocity: 0, sizesKnown: /* @__PURE__ */ new Map(), timeoutSizeMessage: 0, scrollTimer: void 0, belowAnchorElementPositions: void 0, rowHeights: /* @__PURE__ */ new Map(), startReachedBlockedByTimer: false, endReachedBlockedByTimer: false, scrollForNextCalculateItemsInView: void 0, enableScrollForNextCalculateItemsInView: true, minIndexSizeChanged: 0, queuedCalculateItemsInView: 0, lastBatchingAction: Date.now(), averageSizes: {}, onScroll: onScrollProp }; const dataLength = dataProp.length; if (maintainVisibleContentPosition && dataLength > 0) { if (initialScrollIndex && initialScrollIndex < dataLength) { refState.current.anchorElement = { coordinate: initialContentOffset, id: getId(initialScrollIndex) }; } else if (dataLength > 0) { refState.current.anchorElement = { coordinate: initialContentOffset, id: getId(0) }; } else { __DEV__ && warnDevOnce( "maintainVisibleContentPosition", "[legend-list] maintainVisibleContentPosition was not able to find an anchor element" ); } } set$(ctx, "scrollAdjust", 0); set$(ctx, "maintainVisibleContentPosition", maintainVisibleContentPosition); set$(ctx, "extraData", extraData); } const didDataChange = refState.current.data !== dataProp; refState.current.data = dataProp; refState.current.onScroll = onScrollProp; const getAnchorElementIndex = () => { const state = refState.current; if (state.anchorElement) { const el = state.indexByKey.get(state.anchorElement.id); return el; } return void 0; }; const scrollToIndex = ({ index, viewOffset = 0, animated = true, viewPosition }) => { var _a; const state = refState.current; if (index >= state.data.length) { index = state.data.length - 1; } else if (index < 0) { index = 0; } const firstIndexOffset = calculateOffsetForIndex(index); const isLast = index === state.data.length - 1; if (isLast && viewPosition !== void 0) { viewPosition = 1; } let firstIndexScrollPostion = firstIndexOffset - viewOffset; const diff = Math.abs(state.scroll - firstIndexScrollPostion); const topPad = peek$(ctx, "stylePaddingTop") + peek$(ctx, "headerSize"); const needsReanchoring = maintainVisibleContentPosition && diff > 100; state.scrollForNextCalculateItemsInView = void 0; if (needsReanchoring) { const id = getId(index); state.anchorElement = { id, coordinate: firstIndexOffset - topPad }; (_a = state.belowAnchorElementPositions) == null ? void 0 : _a.clear(); state.positions.clear(); calcTotalSizesAndPositions({ forgetPositions: true }); state.startBufferedId = id; state.minIndexSizeChanged = index; firstIndexScrollPostion = firstIndexOffset - viewOffset + state.scrollAdjustHandler.getAppliedAdjust(); } scrollTo({ offset: firstIndexScrollPostion, animated, index, viewPosition: viewPosition != null ? viewPosition : 0, viewOffset }); }; const setDidLayout = () => { refState.current.queuedInitialLayout = true; checkAtBottom(); const setIt = () => { set$(ctx, "containersDidLayout", true); if (props.onLoad) { props.onLoad({ elapsedTimeInMs: Date.now() - refLoadStartTime.current }); } }; if (initialScroll) { queueMicrotask(() => { scrollToIndex({ ...initialScroll, animated: false }); requestAnimationFrame(() => { if (!IsNewArchitecture) { scrollToIndex({ ...initialScroll, animated: false }); } setIt(); }); }); } else { queueMicrotask(setIt); } }; const addTotalSize = useCallback((key, add, totalSizeBelowAnchor) => { const state = refState.current; const { indexByKey, anchorElement } = state; const index = key === null ? 0 : indexByKey.get(key); let isAboveAnchor = false; if (maintainVisibleContentPosition) { if (anchorElement && index < getAnchorElementIndex()) { isAboveAnchor = true; } } if (key === null) { state.totalSize = add; state.totalSizeBelowAnchor = totalSizeBelowAnchor; } else { state.totalSize += add; if (isAboveAnchor) { state.totalSizeBelowAnchor += add; } } let applyAdjustValue = 0; let resultSize = state.totalSize; if (maintainVisibleContentPosition && anchorElement !== void 0) { const newAdjust = anchorElement.coordinate - state.totalSizeBelowAnchor; applyAdjustValue = -newAdjust; state.belowAnchorElementPositions = buildElementPositionsBelowAnchor(); state.rowHeights.clear(); if (applyAdjustValue !== void 0) { resultSize -= applyAdjustValue; state.scrollAdjustHandler.requestAdjust(applyAdjustValue, (diff) => { state.scroll -= diff; }); } } set$(ctx, "totalSize", state.totalSize); set$(ctx, "totalSizeWithScrollAdjust", resultSize); if (alignItemsAtEnd) { updateAlignItemsPaddingTop(); } }, []); const getRowHeight = (n) => { const { rowHeights, data } = refState.current; const numColumns = peek$(ctx, "numColumns"); if (numColumns === 1) { const id = getId(n); return getItemSize(id, n, data[n]); } if (rowHeights.has(n)) { return rowHeights.get(n) || 0; } let rowHeight = 0; const startEl = n * numColumns; for (let i = startEl; i < startEl + numColumns && i < data.length; i++) { const id = getId(i); const size = getItemSize(id, i, data[i]); rowHeight = Math.max(rowHeight, size); } rowHeights.set(n, rowHeight); return rowHeight; }; const buildElementPositionsBelowAnchor = () => { const state = refState.current; if (!state.anchorElement) { return /* @__PURE__ */ new Map(); } const anchorIndex = state.indexByKey.get(state.anchorElement.id); if (anchorIndex === 0) { return /* @__PURE__ */ new Map(); } const map = state.belowAnchorElementPositions || /* @__PURE__ */ new Map(); const numColumns = peek$(ctx, "numColumns"); let top = state.anchorElement.coordinate; for (let i = anchorIndex - 1; i >= 0; i--) { const id = getId(i); const rowNumber = Math.floor(i / numColumns); if (i % numColumns === 0) { top -= getRowHeight(rowNumber); } map.set(id, top); } return map; }; const disableScrollJumps = (timeout) => { const state = refState.current; if (state.scrollingTo === void 0) { state.disableScrollJumpsFrom = state.scroll - state.scrollAdjustHandler.getAppliedAdjust(); state.scrollHistory.length = 0; setTimeout(() => { state.disableScrollJumpsFrom = void 0; if (state.scrollPending !== void 0 && state.scrollPending !== state.scroll) { updateScroll(state.scrollPending); } }, timeout); } }; const getElementPositionBelowAchor = (id) => { var _a; const state = refState.current; if (!refState.current.belowAnchorElementPositions) { state.belowAnchorElementPositions = buildElementPositionsBelowAnchor(); } const res = state.belowAnchorElementPositions.get(id); if (res === void 0) { console.warn(`Undefined position below anchor ${id} ${(_a = state.anchorElement) == null ? void 0 : _a.id}`); return 0; } return res; }; const fixGaps = useCallback(() => { var _a; const state = refState.current; const { data, scrollLength, positions, startBuffered, endBuffered } = state; const numColumns = peek$(ctx, "numColumns"); if (!data || scrollLength === 0 || numColumns > 1) { return; } const numContainers = ctx.values.get("numContainers"); let numMeasurements = 0; for (let i = 0; i < numContainers; i++) { const itemKey = peek$(ctx, `containerItemKey${i}`); const isSizeKnown = state.sizesKnown.get(itemKey); if (itemKey && !isSizeKnown) { const containerRef = ctx.viewRefs.get(i); if (containerRef) { let measured; (_a = containerRef.current) == null ? void 0 : _a.measure((x, y, width, height) => { measured = { width, height }; }); numMeasurements++; if (measured) { updateItemSize( itemKey, measured, /*fromFixGaps*/ true ); } } } } if (numMeasurements > 0) { let top; const diffs = /* @__PURE__ */ new Map(); for (let i = startBuffered; i <= endBuffered; i++) { const id = getId(i); if (top === void 0) { top = positions.get(id); } if (positions.get(id) !== top) { diffs.set(id, top - positions.get(id)); positions.set(id, top); } const size = getItemSize(id, i, data[i]); const bottom = top + size; top = bottom; } for (let i = 0; i < numContainers; i++) { const itemKey = peek$(ctx, `containerItemKey${i}`); const diff = diffs.get(itemKey); if (diff) { const prevPos = peek$(ctx, `containerPosition${i}`); const newPos = prevPos.top + diff; if (prevPos.top !== newPos) { const pos = { ...prevPos }; pos.relativeCoordinate += diff; pos.top += diff; set$(ctx, `containerPosition${i}`, pos); } } } } }, []); const checkAllSizesKnown = useCallback(() => { const { startBuffered, endBuffered, sizesKnown } = refState.current; if (endBuffered !== null) { let areAllKnown = true; for (let i = startBuffered; areAllKnown && i <= endBuffered; i++) { const key = getId(i); areAllKnown && (areAllKnown = sizesKnown.has(key)); } return areAllKnown; } return false; }, []); const calculateItemsInView = useCallback((isReset) => { var _a; const state = refState.current; const { data, scrollLength, startBufferedId: startBufferedIdOrig, positions, columns, scrollAdjustHandler, scrollVelocity: speed } = state; if (!data || scrollLength === 0) { return; } const totalSize = peek$(ctx, "totalSizeWithScrollAdjust"); const t