UNPKG

@legendapp/list

Version:

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

1,507 lines (1,478 loc) 268 kB
import * as React3 from 'react'; import { forwardRef, useReducer, useEffect, createContext, useRef, useState, useMemo, useCallback, useImperativeHandle, useLayoutEffect, useContext } from 'react'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; import * as ReactDOM from 'react-dom'; import { flushSync } from 'react-dom'; // src/components/LegendList.tsx forwardRef(function AnimatedView2(props, ref) { return /* @__PURE__ */ React3.createElement("div", { ref, ...props }); }); var View = forwardRef(function View2(props, ref) { return /* @__PURE__ */ React3.createElement("div", { ref, ...props }); }); var Text = View; // src/platform/Platform.ts var Platform = { // Widen the type to avoid unreachable-branch lints in cross-platform code that compares against other OSes OS: "web" }; // src/utils/rtl.ts function clampHorizontalOffset(offset, maxOffset) { if (maxOffset === void 0) { return offset; } return Math.max(0, Math.min(maxOffset, offset)); } function getHorizontalMaxOffset(state, contentWidth) { if (contentWidth === void 0 || !Number.isFinite(contentWidth) || !Number.isFinite(state.scrollLength) || contentWidth <= state.scrollLength) { return contentWidth !== void 0 && Number.isFinite(contentWidth) && Number.isFinite(state.scrollLength) ? 0 : void 0; } return Math.max(0, contentWidth - state.scrollLength); } function getDefaultHorizontalRTLScrollType() { return "normal" ; } function getNativeHorizontalRTLScrollType(state) { var _a3; return (_a3 = state == null ? void 0 : state.horizontalRTLScrollType) != null ? _a3 : getDefaultHorizontalRTLScrollType(); } function isRTLProps(props) { var _a3; return (_a3 = props == null ? void 0 : props.rtl) != null ? _a3 : false; } function isHorizontalRTL(state) { return isHorizontalRTLProps(state == null ? void 0 : state.props); } function isHorizontalRTLProps(props) { return !!(props == null ? void 0 : props.horizontal) && isRTLProps(props); } function getLogicalHorizontalMaxOffset(state, contentWidth) { var _a3; return (_a3 = getHorizontalMaxOffset(state, contentWidth)) != null ? _a3 : 0; } function getHorizontalInsetEnd(state, inset) { if (!inset) { return 0; } return (isHorizontalRTL(state) ? inset.left : inset.right) || 0; } function toPhysicalHorizontalItemPosition(state, logicalPosition, itemSize, listSize) { if (!isHorizontalRTL(state) || listSize === void 0 || !Number.isFinite(listSize)) { return logicalPosition; } return Math.max(0, listSize - logicalPosition - itemSize); } function toNativeHorizontalOffset(state, logicalOffset, contentWidth) { if (!state || !isHorizontalRTL(state)) { return logicalOffset; } const maxOffset = getHorizontalMaxOffset(state, contentWidth); const clampedLogicalOffset = clampHorizontalOffset(logicalOffset, maxOffset); const mode = getNativeHorizontalRTLScrollType(state); if (mode === "negative") { return clampedLogicalOffset === 0 ? 0 : -clampedLogicalOffset; } if (mode === "inverted") { if (maxOffset === void 0) { return clampedLogicalOffset; } return clampHorizontalOffset(maxOffset - clampedLogicalOffset, maxOffset); } return clampedLogicalOffset; } function toLogicalHorizontalOffset(state, rawOffset, contentWidth) { if (!isHorizontalRTL(state)) { state.horizontalRTLScrollType = void 0; return rawOffset; } const maxOffset = getHorizontalMaxOffset(state, contentWidth); if (rawOffset < 0) { state.horizontalRTLScrollType = "negative"; return clampHorizontalOffset(-rawOffset, maxOffset); } if (maxOffset === void 0) { return rawOffset; } const normalOffset = rawOffset; const invertedOffset = maxOffset - rawOffset; if (!Number.isFinite(invertedOffset)) { state.horizontalRTLScrollType = "normal"; return normalOffset; } const previousMode = state.horizontalRTLScrollType; if (previousMode === "inverted") { return clampHorizontalOffset(invertedOffset, maxOffset); } if (previousMode === "normal") { return clampHorizontalOffset(normalOffset, maxOffset); } if (!state.hasScrolled) { const defaultMode = getDefaultHorizontalRTLScrollType(); state.horizontalRTLScrollType = defaultMode; return clampHorizontalOffset(normalOffset, maxOffset); } const referenceScroll = state.scroll; const distanceNormal = Math.abs(normalOffset - referenceScroll); const distanceInverted = Math.abs(invertedOffset - referenceScroll); const useInverted = distanceInverted + 0.5 < distanceNormal; state.horizontalRTLScrollType = useInverted ? "inverted" : "normal"; return clampHorizontalOffset(useInverted ? invertedOffset : normalOffset, maxOffset); } // src/platform/Animated.tsx var createAnimatedValue = (value) => value; // src/state/state.tsx var ContextState = React3.createContext(null); var contextNum = 0; function StateProvider({ children }) { const [value] = React3.useState(() => ({ animatedScrollY: createAnimatedValue(0), columnWrapperStyle: void 0, contextNum: contextNum++, listeners: /* @__PURE__ */ new Map(), mapViewabilityAmountCallbacks: /* @__PURE__ */ new Map(), mapViewabilityAmountValues: /* @__PURE__ */ new Map(), mapViewabilityCallbacks: /* @__PURE__ */ new Map(), mapViewabilityConfigStates: /* @__PURE__ */ new Map(), mapViewabilityValues: /* @__PURE__ */ new Map(), positionListeners: /* @__PURE__ */ new Map(), state: void 0, values: /* @__PURE__ */ new Map([ ["stylePaddingTop", 0], ["headerSize", 0], ["numContainers", 0], ["activeStickyIndex", -1], ["isAtEnd", false], ["isAtStart", false], ["isNearEnd", false], ["isNearStart", false], ["isWithinMaintainScrollAtEndThreshold", false], ["totalSize", 0], ["scrollAdjustPending", 0] ]), viewRefs: /* @__PURE__ */ new Map() })); return /* @__PURE__ */ React3.createElement(ContextState.Provider, { value }, children); } function useStateContext() { return React3.useContext(ContextState); } function createSelectorFunctionsArr(ctx, signalNames) { let lastValues = []; let lastSignalValues = []; return { 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; }, subscribe: (cb) => { const listeners = []; for (const signalName of signalNames) { listeners.push(listen$(ctx, signalName, cb)); } return () => { for (const listener of listeners) { listener(); } }; } }; } 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 listenPosition$(ctx, key, cb) { const { positionListeners } = ctx; let setListeners = positionListeners.get(key); if (!setListeners) { setListeners = /* @__PURE__ */ new Set(); positionListeners.set(key, setListeners); } setListeners.add(cb); return () => setListeners.delete(cb); } function notifyPosition$(ctx, key, value) { const { positionListeners } = ctx; const setListeners = positionListeners.get(key); if (setListeners) { for (const listener of setListeners) { listener(value); } } } function useArr$(signalNames) { const ctx = React3.useContext(ContextState); const { subscribe, get } = React3.useMemo(() => createSelectorFunctionsArr(ctx, signalNames), [ctx, signalNames]); const value = useSyncExternalStore(subscribe, get, get); return value; } function useSelector$(signalName, selector) { const ctx = React3.useContext(ContextState); const { subscribe, get } = React3.useMemo(() => createSelectorFunctionsArr(ctx, [signalName]), [ctx, signalName]); const getSelectedValue = React3.useCallback(() => selector(get()[0]), [get, selector]); const value = useSyncExternalStore(subscribe, getSelectedValue, getSelectedValue); return value; } // src/state/getContentInsetEnd.ts function getContentInsetEndAdjustmentEnd(adjustment) { return Math.max(0, adjustment != null ? adjustment : 0); } function getContentInsetEnd(ctx, contentInsetEndAdjustmentOverride) { var _a3, _b; const state = ctx.state; const { props } = state; const horizontal = props.horizontal; const contentInset = props.contentInset; const baseInset = contentInset != null ? contentInset : state.nativeContentInset; const baseEndInset = (horizontal ? getHorizontalInsetEnd(state, baseInset) : baseInset == null ? void 0 : baseInset.bottom) || 0; const contentInsetEndAdjustment = getContentInsetEndAdjustmentEnd( contentInsetEndAdjustmentOverride != null ? contentInsetEndAdjustmentOverride : props.contentInsetEndAdjustment ); const anchoredEndSpaceSize = peek$(ctx, "anchoredEndSpaceSize"); const anchoredEndInset = ((_a3 = props.anchoredEndSpace) == null ? void 0 : _a3.includeInEndInset) && anchoredEndSpaceSize ? anchoredEndSpaceSize : 0; const overrideInset = (_b = state.contentInsetOverride) != null ? _b : void 0; const adjustedBaseEndInset = baseEndInset + contentInsetEndAdjustment; if (overrideInset) { const mergedInset = { bottom: 0, left: 0, right: 0, ...baseInset, ...overrideInset }; return Math.max( ((horizontal ? getHorizontalInsetEnd(state, mergedInset) : mergedInset.bottom) || 0) + contentInsetEndAdjustment, anchoredEndInset ); } return Math.max(adjustedBaseEndInset, anchoredEndInset); } // src/state/getContentSize.ts function getContentSize(ctx) { var _a3; const { values, state } = ctx; const stylePaddingTop = values.get("stylePaddingTop") || 0; const stylePaddingBottom = state.props.stylePaddingBottom || 0; const headerSize = values.get("headerSize") || 0; const footerSize = values.get("footerSize") || 0; const contentInsetBottom = getContentInsetEnd(ctx); const totalSize = (_a3 = state.pendingTotalSize) != null ? _a3 : values.get("totalSize"); return headerSize + footerSize + totalSize + stylePaddingTop + stylePaddingBottom + (contentInsetBottom || 0); } // src/components/DebugView.tsx var DebugRow = ({ children }) => { return /* @__PURE__ */ React3.createElement(View, { style: { alignItems: "center", flexDirection: "row", justifyContent: "space-between" } }, children); }; React3.memo(function DebugView2() { const ctx = useStateContext(); const [ totalSize = 0, scrollAdjust = 0, rawScroll = 0, scroll = 0, _numContainers = 0, _numContainersPooled = 0, isAtEnd = false ] = useArr$([ "totalSize", "scrollAdjust", "debugRawScroll", "debugComputedScroll", "numContainers", "numContainersPooled", "isAtEnd" ]); const contentSize = getContentSize(ctx); const [, forceUpdate] = useReducer((x) => x + 1, 0); useInterval(() => { forceUpdate(); }, 100); return /* @__PURE__ */ React3.createElement( View, { pointerEvents: "none", style: { // height: 100, backgroundColor: "#FFFFFFCC", borderRadius: 4, padding: 4, paddingBottom: 4, paddingLeft: 4, position: "absolute", right: 0, top: 0 } }, /* @__PURE__ */ React3.createElement(DebugRow, null, /* @__PURE__ */ React3.createElement(Text, null, "TotalSize:"), /* @__PURE__ */ React3.createElement(Text, null, totalSize.toFixed(2))), /* @__PURE__ */ React3.createElement(DebugRow, null, /* @__PURE__ */ React3.createElement(Text, null, "ContentSize:"), /* @__PURE__ */ React3.createElement(Text, null, contentSize.toFixed(2))), /* @__PURE__ */ React3.createElement(DebugRow, null, /* @__PURE__ */ React3.createElement(Text, null, "At end:"), /* @__PURE__ */ React3.createElement(Text, null, String(isAtEnd))), /* @__PURE__ */ React3.createElement(DebugRow, null, /* @__PURE__ */ React3.createElement(Text, null, "ScrollAdjust:"), /* @__PURE__ */ React3.createElement(Text, null, scrollAdjust.toFixed(2))), /* @__PURE__ */ React3.createElement(DebugRow, null, /* @__PURE__ */ React3.createElement(Text, null, "RawScroll: "), /* @__PURE__ */ React3.createElement(Text, null, rawScroll.toFixed(2))), /* @__PURE__ */ React3.createElement(DebugRow, null, /* @__PURE__ */ React3.createElement(Text, null, "ComputedScroll: "), /* @__PURE__ */ React3.createElement(Text, null, scroll.toFixed(2))) ); }); function useInterval(callback, delay) { useEffect(() => { const interval = setInterval(callback, delay); return () => clearInterval(interval); }, [delay]); } // src/utils/devEnvironment.ts var metroDev = typeof __DEV__ !== "undefined" ? __DEV__ : void 0; var _a; var envMode = typeof process !== "undefined" && typeof process.env === "object" && process.env ? (_a = process.env.NODE_ENV) != null ? _a : process.env.MODE : void 0; var processDev = typeof envMode === "string" ? envMode.toLowerCase() !== "production" : void 0; var _a2; var IS_DEV = (_a2 = processDev != null ? processDev : metroDev) != null ? _a2 : false; // src/constants.ts var POSITION_OUT_OF_VIEW = -1e7; var EDGE_POSITION_EPSILON = 1; var ENABLE_DEVMODE = IS_DEV && false; var ENABLE_DEBUG_VIEW = IS_DEV && false; var typedForwardRef = React3.forwardRef; var typedMemo = React3.memo; var getComponent = (Component) => { if (React3.isValidElement(Component)) { return Component; } if (Component) { return /* @__PURE__ */ React3.createElement(Component, null); } return null; }; // src/utils/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 (IS_DEV && !warned.has(id)) { warned.add(id); console.warn(`[legend-list] ${text}`); } } function roundSize(size) { return Math.floor(size * 8) / 8; } function isNullOrUndefined(value) { return value === null || value === void 0; } function comparatorDefault(a, b) { return a - b; } function getPadding(s, type) { var _a3, _b, _c; const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; } function extractPadding(style, contentContainerStyle, type) { return getPadding(style, type) + getPadding(contentContainerStyle, type); } function findContainerId(ctx, key) { var _a3, _b; const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); if (directMatch !== void 0) { return directMatch; } 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; } // src/components/PositionView.tsx var isRNWeb = typeof document !== "undefined" && !!document.getElementById("react-native-stylesheet"); var baseCss = { contain: "paint layout style", ...isRNWeb ? { display: "flex", flexDirection: "column" } : {} }; var PositionViewState = typedMemo(function PositionViewState2({ id, horizontal, style, refView, ...props }) { const [position = POSITION_OUT_OF_VIEW] = useArr$([`containerPosition${id}`]); const composed = isArray(style) ? Object.assign({}, ...style) : style; const combinedStyle = horizontal ? { ...baseCss, ...composed, left: position } : { ...baseCss, ...composed, top: position }; const { animatedScrollY: _animatedScrollY, index, onLayout: _onLayout, onLayoutChange: _onLayoutChange, stickyHeaderConfig: _stickyHeaderConfig, ...webProps } = props; return /* @__PURE__ */ React3.createElement("div", { "data-index": index, ref: refView, ...webProps, style: combinedStyle }); }); var PositionViewSticky = typedMemo(function PositionViewSticky2({ id, horizontal, style, refView, index, animatedScrollY: _animatedScrollY, stickyHeaderConfig, onLayout: _onLayout, onLayoutChange: _onLayoutChange, children, ...webProps }) { const [position = POSITION_OUT_OF_VIEW, activeStickyIndex] = useArr$([ `containerPosition${id}`, "activeStickyIndex" ]); const composed = React3.useMemo( () => { var _a3; return (_a3 = isArray(style) ? Object.assign({}, ...style) : style) != null ? _a3 : {}; }, [style] ); const viewStyle = React3.useMemo(() => { var _a3; const styleBase = { ...baseCss, ...composed }; delete styleBase.transform; const offset = (_a3 = stickyHeaderConfig == null ? void 0 : stickyHeaderConfig.offset) != null ? _a3 : 0; const isActive = activeStickyIndex === index; styleBase.position = isActive ? "sticky" : "absolute"; styleBase.zIndex = index + 1e3; if (horizontal) { styleBase.left = isActive ? offset : position; } else { styleBase.top = isActive ? offset : position; } return styleBase; }, [composed, horizontal, position, index, activeStickyIndex, stickyHeaderConfig == null ? void 0 : stickyHeaderConfig.offset]); const renderStickyHeaderBackdrop = React3.useMemo( () => (stickyHeaderConfig == null ? void 0 : stickyHeaderConfig.backdropComponent) ? /* @__PURE__ */ React3.createElement( "div", { style: { inset: 0, pointerEvents: "none", position: "absolute" } }, getComponent(stickyHeaderConfig.backdropComponent) ) : null, [stickyHeaderConfig == null ? void 0 : stickyHeaderConfig.backdropComponent] ); return /* @__PURE__ */ React3.createElement( "div", { "data-index": index, ref: refView, style: viewStyle, ...webProps }, renderStickyHeaderBackdrop, children ); }); var PositionView = PositionViewState; // src/constants-platform.ts var IsNewArchitecture = true; function useInit(cb) { useState(() => cb()); } // src/state/ContextContainer.ts var ContextContainer = createContext(null); function useContextContainer() { return useContext(ContextContainer); } function useViewability(callback, configId) { const ctx = useStateContext(); const containerContext = useContextContainer(); useInit(() => { if (!containerContext) { return; } const { containerId } = containerContext; const key = containerId + (configId != null ? configId : ""); const value = ctx.mapViewabilityValues.get(key); if (value) { callback(value); } }); useEffect(() => { if (!containerContext) { return; } const { containerId } = containerContext; const key = containerId + (configId != null ? configId : ""); ctx.mapViewabilityCallbacks.set(key, callback); return () => { ctx.mapViewabilityCallbacks.delete(key); }; }, [ctx, callback, configId, containerContext]); } function useViewabilityAmount(callback) { const ctx = useStateContext(); const containerContext = useContextContainer(); useInit(() => { if (!containerContext) { return; } const { containerId } = containerContext; const value = ctx.mapViewabilityAmountValues.get(containerId); if (value) { callback(value); } }); useEffect(() => { if (!containerContext) { return; } const { containerId } = containerContext; ctx.mapViewabilityAmountCallbacks.set(containerId, callback); return () => { ctx.mapViewabilityAmountCallbacks.delete(containerId); }; }, [ctx, callback, containerContext]); } function useRecyclingEffect(effect) { const containerContext = useContextContainer(); const prevValues = useRef({ prevIndex: void 0, prevItem: void 0 }); useEffect(() => { if (!containerContext) { return; } const { index, value } = containerContext; let ret; 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; }, [effect, containerContext]); } function useRecyclingState(valueOrFun) { var _a3, _b; const containerContext = useContextContainer(); const computeValue = (ctx) => { if (isFunction(valueOrFun)) { const initializer = valueOrFun; return ctx ? initializer({ index: ctx.index, item: ctx.value, prevIndex: void 0, prevItem: void 0 }) : initializer(); } return valueOrFun; }; const [stateValue, setStateValue] = useState(() => { return computeValue(containerContext); }); const prevItemKeyRef = useRef((_a3 = containerContext == null ? void 0 : containerContext.itemKey) != null ? _a3 : null); const currentItemKey = (_b = containerContext == null ? void 0 : containerContext.itemKey) != null ? _b : null; if (currentItemKey !== null && prevItemKeyRef.current !== currentItemKey) { prevItemKeyRef.current = currentItemKey; setStateValue(computeValue(containerContext)); } const triggerLayout = containerContext == null ? void 0 : containerContext.triggerLayout; const setState = useCallback( (newState) => { if (!triggerLayout) { return; } setStateValue((prevValue) => { return isFunction(newState) ? newState(prevValue) : newState; }); triggerLayout(); }, [triggerLayout] ); return [stateValue, setState]; } function useIsLastItem() { const containerContext = useContextContainer(); const isLast = useSelector$("lastItemKeys", (lastItemKeys) => { if (containerContext) { const { itemKey } = containerContext; if (!isNullOrUndefined(itemKey)) { return (lastItemKeys == null ? void 0 : lastItemKeys.includes(itemKey)) || false; } } return false; }); return isLast; } function useListScrollSize() { const [scrollSize] = useArr$(["scrollSize"]); return scrollSize; } var noop = () => { }; function useSyncLayout() { const containerContext = useContextContainer(); if (containerContext) { const { triggerLayout: syncLayout } = containerContext; return syncLayout; } else { return noop; } } // src/components/Separator.tsx function Separator({ ItemSeparatorComponent, leadingItem }) { const isLastItem = useIsLastItem(); return isLastItem ? null : /* @__PURE__ */ React3.createElement(ItemSeparatorComponent, { leadingItem }); } // src/hooks/createResizeObserver.ts var globalResizeObserver = null; function getGlobalResizeObserver() { if (!globalResizeObserver) { globalResizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { const callbacks = callbackMap.get(entry.target); if (callbacks) { for (const callback of callbacks) { callback(entry); } } } }); } return globalResizeObserver; } var callbackMap = /* @__PURE__ */ new WeakMap(); function createResizeObserver(element, callback) { if (typeof ResizeObserver === "undefined") { return () => { }; } if (!element) { return () => { }; } const observer = getGlobalResizeObserver(); let callbacks = callbackMap.get(element); if (!callbacks) { callbacks = /* @__PURE__ */ new Set(); callbackMap.set(element, callbacks); observer.observe(element); } callbacks.add(callback); return () => { if (callbacks) { callbacks.delete(callback); if (callbacks.size === 0) { callbackMap.delete(element); observer.unobserve(element); } } }; } // src/hooks/useOnLayoutSync.tsx function useOnLayoutSync({ ref, onLayoutProp, onLayoutChange, webLayoutResync }, deps) { useLayoutEffect(() => { var _a3, _b; const current = ref.current; const scrollableNode = (_b = (_a3 = current == null ? void 0 : current.getScrollableNode) == null ? void 0 : _a3.call(current)) != null ? _b : null; const element = scrollableNode || current; if (!element) { return; } const emit = (layout, fromLayoutEffect) => { if (layout.height === 0 && layout.width === 0) { return; } onLayoutChange(layout, fromLayoutEffect); onLayoutProp == null ? void 0 : onLayoutProp({ nativeEvent: { layout } }); }; const rect = element.getBoundingClientRect(); emit(toLayout(rect), true); let prevRect = rect; return createResizeObserver(element, (entry) => { var _a4; const target = entry.target instanceof HTMLElement ? entry.target : void 0; const rectObserved = (_a4 = entry.contentRect) != null ? _a4 : target == null ? void 0 : target.getBoundingClientRect(); const didSizeChange = rectObserved.width !== prevRect.width || rectObserved.height !== prevRect.height; const shouldResyncLayout = !!(webLayoutResync == null ? void 0 : webLayoutResync()); if (didSizeChange || shouldResyncLayout) { prevRect = rectObserved; emit(toLayout(rectObserved), false); } }); }, deps || []); return {}; } function toLayout(rect) { if (!rect) { return { height: 0, width: 0, x: 0, y: 0 }; } return { height: rect.height, width: rect.width, x: rect.left, y: rect.top }; } // src/utils/hasActiveMVCPAnchorLock.ts function hasActiveMVCPAnchorLock(state) { const lock = state.mvcpAnchorLock; if (!lock) { return false; } if (Date.now() > lock.expiresAt) { state.mvcpAnchorLock = void 0; return false; } return true; } // src/utils/isInMVCPActiveMode.ts function isInMVCPActiveMode(state) { return state.dataChangeNeedsScrollUpdate || hasActiveMVCPAnchorLock(state); } // src/components/Container.tsx function getContainerPositionStyle({ columnWrapperStyle, horizontal, hasItemSeparator, isHorizontalRTLList, numColumns, otherAxisPos, otherAxisSize }) { let paddingStyles; if (columnWrapperStyle) { const { columnGap, rowGap, gap } = columnWrapperStyle; if (horizontal) { paddingStyles = { paddingBottom: numColumns > 1 ? (rowGap || gap || 0) / 2 : void 0, paddingRight: columnGap || gap || void 0, paddingTop: numColumns > 1 ? (rowGap || gap || 0) / 2 : void 0 }; } else { paddingStyles = { paddingBottom: rowGap || gap || void 0, paddingLeft: numColumns > 1 ? (columnGap || gap || 0) / 2 : void 0, paddingRight: numColumns > 1 ? (columnGap || gap || 0) / 2 : void 0 }; } } return horizontal ? { boxSizing: paddingStyles ? "border-box" : void 0, direction: isHorizontalRTLList && Platform.OS === "web" ? "ltr" : void 0, flexDirection: hasItemSeparator ? "row" : void 0, height: otherAxisSize, left: 0, position: "absolute", top: otherAxisPos, ...paddingStyles || {} } : { boxSizing: paddingStyles ? "border-box" : void 0, left: otherAxisPos, position: "absolute", right: numColumns > 1 ? null : 0, top: 0, width: otherAxisSize, ...paddingStyles || {} }; } var Container = typedMemo(function Container2({ id, itemKey, recycleItems, horizontal, getRenderedItem: getRenderedItem2, updateItemSize: updateItemSize2, ItemSeparatorComponent, stickyHeaderConfig }) { const ctx = useStateContext(); const { columnWrapperStyle, animatedScrollY } = ctx; const isHorizontalRTLList = isHorizontalRTL(ctx.state); const positionComponentInternal = ctx.state.props.positionComponentInternal; const stickyPositionComponentInternal = ctx.state.props.stickyPositionComponentInternal; const [column = 0, span = 1, data, numColumns = 1, extraData, isSticky] = useArr$([ `containerColumn${id}`, `containerSpan${id}`, `containerItemData${id}`, "numColumns", "extraData", `containerSticky${id}` ]); const itemLayoutRef = useRef({ didLayout: false, horizontal, itemKey, pendingShrinkToken: 0, updateItemSize: updateItemSize2 }); itemLayoutRef.current.horizontal = horizontal; itemLayoutRef.current.itemKey = itemKey; itemLayoutRef.current.updateItemSize = updateItemSize2; const ref = useRef(null); const [layoutRenderCount, forceLayoutRender] = useState(0); const resolvedColumn = column > 0 ? column : 1; const resolvedSpan = Math.min(Math.max(span || 1, 1), numColumns); const otherAxisPos = numColumns > 1 ? `${(resolvedColumn - 1) / numColumns * 100}%` : 0; const otherAxisSize = numColumns > 1 ? `${resolvedSpan / numColumns * 100}%` : void 0; const style = useMemo( () => getContainerPositionStyle({ columnWrapperStyle, hasItemSeparator: !!ItemSeparatorComponent, horizontal, isHorizontalRTLList, numColumns, otherAxisPos, otherAxisSize }), [ horizontal, isHorizontalRTLList, otherAxisPos, otherAxisSize, columnWrapperStyle, numColumns, ItemSeparatorComponent ] ); const renderedItemInfo = useMemo( () => itemKey !== void 0 ? getRenderedItem2(itemKey) : null, [itemKey, data, extraData] ); const { index, renderedItem } = renderedItemInfo || {}; const contextValue = useMemo(() => { ctx.viewRefs.set(id, ref); return { containerId: id, index, itemKey, triggerLayout: () => { forceLayoutRender((v) => v + 1); }, value: data }; }, [id, itemKey, index, data]); const onLayoutChange = useCallback((rectangle) => { const { horizontal: currentHorizontal, itemKey: currentItemKey, updateItemSize: updateItemSizeFn, lastSize, pendingShrinkToken } = itemLayoutRef.current; if (isNullOrUndefined(currentItemKey)) { return; } itemLayoutRef.current.didLayout = true; let layout = rectangle; const axis = currentHorizontal ? "width" : "height"; const size = roundSize(rectangle[axis]); const prevSize = lastSize ? roundSize(lastSize[axis]) : void 0; const doUpdate = () => { itemLayoutRef.current.lastSize = layout; updateItemSizeFn(currentItemKey, layout); itemLayoutRef.current.didLayout = true; }; const shouldDeferWebShrinkLayoutUpdate = !isInMVCPActiveMode(ctx.state) && prevSize !== void 0 && size + 1 < prevSize; if (shouldDeferWebShrinkLayoutUpdate) { const token = pendingShrinkToken + 1; itemLayoutRef.current.pendingShrinkToken = token; requestAnimationFrame(() => { var _a4; if (itemLayoutRef.current.pendingShrinkToken !== token) { return; } const element = ref.current; const rect = (_a4 = element == null ? void 0 : element.getBoundingClientRect) == null ? void 0 : _a4.call(element); if (rect) { layout = { height: rect.height, width: rect.width }; } doUpdate(); }); return; } { doUpdate(); } }, []); const { onLayout } = useOnLayoutSync( { onLayoutChange, ref, webLayoutResync: () => isInMVCPActiveMode(ctx.state) }, [itemKey, layoutRenderCount] ); const PositionComponent = isSticky ? stickyPositionComponentInternal ? stickyPositionComponentInternal : PositionViewSticky : positionComponentInternal ? positionComponentInternal : PositionView; return /* @__PURE__ */ React3.createElement( PositionComponent, { animatedScrollY: isSticky ? animatedScrollY : void 0, horizontal, id, index, key: recycleItems ? void 0 : itemKey, onLayout, refView: ref, stickyHeaderConfig, style }, /* @__PURE__ */ React3.createElement(ContextContainer.Provider, { value: contextValue }, renderedItem, renderedItemInfo && ItemSeparatorComponent && /* @__PURE__ */ React3.createElement(Separator, { ItemSeparatorComponent, leadingItem: renderedItemInfo.item })) ); }); // src/components/ContainerSlot.tsx function ContainerSlotBase({ id, horizontal, recycleItems, ItemSeparatorComponent, updateItemSize: updateItemSize2, getRenderedItem: getRenderedItem2, stickyHeaderConfig, ContainerComponent = Container }) { const [itemKey] = useArr$([`containerItemKey${id}`]); if (itemKey === void 0) { return null; } return /* @__PURE__ */ React3.createElement( ContainerComponent, { getRenderedItem: getRenderedItem2, horizontal, ItemSeparatorComponent, id, itemKey, recycleItems, stickyHeaderConfig, updateItemSize: updateItemSize2 } ); } var ContainerSlot = typedMemo(function ContainerSlot2(props) { return /* @__PURE__ */ React3.createElement(ContainerSlotBase, { ...props }); }); // src/utils/reordering.ts var mapFn = (element) => { const indexStr = element.getAttribute("data-index"); if (indexStr === null) { return [element, null]; } const index = Number.parseInt(indexStr, 10); return [element, Number.isNaN(index) ? null : index]; }; function sortDOMElements(container) { const elements = Array.from(container.children); if (elements.length <= 1) return elements; const items = elements.map(mapFn); items.sort((a, b) => { const aKey = a[1]; const bKey = b[1]; if (aKey === null) { return 1; } if (bKey === null) { return -1; } return aKey - bKey; }); const targetPositions = /* @__PURE__ */ new Map(); items.forEach((item, index) => { targetPositions.set(item[0], index); }); const currentPositions = elements.map((el) => targetPositions.get(el)); const lis = findLIS(currentPositions); const stableIndices = new Set(lis); for (let targetPos = 0; targetPos < items.length; targetPos++) { const element = items[targetPos][0]; const currentPos = elements.indexOf(element); if (!stableIndices.has(currentPos)) { let nextStableElement = null; for (let i = targetPos + 1; i < items.length; i++) { const nextEl = items[i][0]; const nextCurrentPos = elements.indexOf(nextEl); if (stableIndices.has(nextCurrentPos)) { nextStableElement = nextEl; break; } } if (nextStableElement) { container.insertBefore(element, nextStableElement); } else { container.appendChild(element); } } } } function findLIS(arr) { const n = arr.length; const tails = []; const predecessors = new Array(n).fill(-1); const indices = []; for (let i = 0; i < n; i++) { const num = arr[i]; let left = 0, right = tails.length; while (left < right) { const mid = Math.floor((left + right) / 2); if (arr[indices[mid]] < num) { left = mid + 1; } else { right = mid; } } if (left === tails.length) { tails.push(num); indices.push(i); } else { tails[left] = num; indices[left] = i; } if (left > 0) { predecessors[i] = indices[left - 1]; } } const result = []; let k = indices[indices.length - 1]; while (k !== -1) { result.unshift(k); k = predecessors[k]; } return result; } // src/hooks/useDOMOrder.ts function useDOMOrder(ref) { const ctx = useStateContext(); const debounceRef = useRef(void 0); useEffect(() => { const unsubscribe = listen$(ctx, "lastPositionUpdate", () => { if (debounceRef.current !== void 0) { clearTimeout(debounceRef.current); } debounceRef.current = setTimeout(() => { const parent = ref.current; if (parent) { sortDOMElements(parent); } debounceRef.current = void 0; }, 500); }); return () => { unsubscribe(); if (debounceRef.current !== void 0) { clearTimeout(debounceRef.current); } }; }, [ctx]); } // src/components/Containers.tsx var ContainersInner = typedMemo(function ContainersInner2({ horizontal, numColumns, children }) { const ref = useRef(null); const ctx = useStateContext(); const columnWrapperStyle = ctx.columnWrapperStyle; const isHorizontalRTLList = isHorizontalRTL(ctx.state); const [otherAxisSize, readyToRender, totalSize] = useArr$(["otherAxisSize", "readyToRender", "totalSize"]); useDOMOrder(ref); const style = horizontal ? { direction: isHorizontalRTLList ? "ltr" : void 0, minHeight: otherAxisSize, opacity: readyToRender ? 1 : 0, position: "relative", width: totalSize } : { height: totalSize, minWidth: otherAxisSize, opacity: readyToRender ? 1 : 0, position: "relative" }; if (!readyToRender) { style.pointerEvents = "none"; } if (columnWrapperStyle && numColumns > 1) { const { columnGap, rowGap, gap } = columnWrapperStyle; const gapX = columnGap || gap || 0; const gapY = rowGap || gap || 0; if (horizontal) { if (gapY) { style.marginTop = style.marginBottom = -gapY / 2; } if (gapX) { style.marginRight = -gapX; } } else { if (gapY) { style.marginBottom = -gapY; } } } return /* @__PURE__ */ React3.createElement("div", { ref, style }, children); }); var Containers = typedMemo(function Containers2({ horizontal, recycleItems, ItemSeparatorComponent, updateItemSize: updateItemSize2, getRenderedItem: getRenderedItem2, stickyHeaderConfig }) { const [numContainersPooled, numColumns] = useArr$(["numContainersPooled", "numColumns"]); const containers = []; for (let i = 0; i < numContainersPooled; i++) { containers.push( /* @__PURE__ */ React3.createElement( ContainerSlot, { getRenderedItem: getRenderedItem2, horizontal, ItemSeparatorComponent, id: i, key: i, recycleItems, stickyHeaderConfig, updateItemSize: updateItemSize2 } ) ); } return /* @__PURE__ */ React3.createElement(ContainersInner, { horizontal, numColumns }, containers); }); // src/platform/StyleSheet.tsx function flattenStyles(styles) { if (isArray(styles)) { return Object.assign({}, ...styles.filter(Boolean)); } return styles; } var StyleSheet = { create: (styles) => styles, flatten: (style) => flattenStyles(style) }; function useLatestRef(value) { const ref = React3.useRef(value); ref.current = value; return ref; } // src/utils/useRafCoalescer.ts function useRafCoalescer(callback) { const callbackRef = useLatestRef(callback); const rafIdRef = useRef(void 0); const coalescer = useMemo( () => ({ cancel() { if (rafIdRef.current !== void 0) { cancelAnimationFrame(rafIdRef.current); rafIdRef.current = void 0; } }, flush() { coalescer.cancel(); callbackRef.current(); }, schedule() { if (rafIdRef.current !== void 0) { return false; } const rafId = requestAnimationFrame(() => { if (rafIdRef.current !== rafId) { return; } rafIdRef.current = void 0; callbackRef.current(); }); rafIdRef.current = rafId; return true; } }), [] ); useEffect( () => () => { coalescer.cancel(); }, [coalescer] ); return coalescer; } // src/components/webConstants.ts var LEGEND_LIST_CONTENT_CONTAINER_CLASS = "legend-list-content-container"; var LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS = "legend-list-scrollbar-x-hidden"; var LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS = "legend-list-scrollbar-y-hidden"; // src/components/webScrollUtils.ts function getDocumentScrollerNode() { if (typeof document === "undefined") { return null; } return document.scrollingElement || document.documentElement || document.body; } function getWindowScrollPosition() { var _a3, _b, _c, _d; if (typeof window === "undefined") { return { x: 0, y: 0 }; } return { x: (_b = (_a3 = window.scrollX) != null ? _a3 : window.pageXOffset) != null ? _b : 0, y: (_d = (_c = window.scrollY) != null ? _c : window.pageYOffset) != null ? _d : 0 }; } function getElementDocumentPosition(element, scroll) { var _a3, _b; const rect = element == null ? void 0 : element.getBoundingClientRect(); return { left: ((_a3 = rect == null ? void 0 : rect.left) != null ? _a3 : 0) + scroll.x, top: ((_b = rect == null ? void 0 : rect.top) != null ? _b : 0) + scroll.y }; } function getContentSize2(content) { var _a3, _b; return { height: (_a3 = content == null ? void 0 : content.scrollHeight) != null ? _a3 : 0, width: (_b = content == null ? void 0 : content.scrollWidth) != null ? _b : 0 }; } function getScrollContentSize(scrollElement, contentElement, isWindowScroll) { return getContentSize2(isWindowScroll ? contentElement : scrollElement); } function getLayoutMeasurement(scrollElement, isWindowScroll, horizontal) { var _a3, _b, _c, _d, _e, _f; if (isWindowScroll && typeof window !== "undefined") { const rect = scrollElement == null ? void 0 : scrollElement.getBoundingClientRect(); return { // In window-scroll mode, use viewport size on the scroll axis. height: horizontal ? (_b = (_a3 = rect == null ? void 0 : rect.height) != null ? _a3 : scrollElement == null ? void 0 : scrollElement.clientHeight) != null ? _b : window.innerHeight : window.innerHeight, // Keep the cross-axis size list-relative to avoid inflating container measurements. width: horizontal ? window.innerWidth : (_d = (_c = rect == null ? void 0 : rect.width) != null ? _c : scrollElement == null ? void 0 : scrollElement.clientWidth) != null ? _d : window.innerWidth }; } return { height: (_e = scrollElement == null ? void 0 : scrollElement.clientHeight) != null ? _e : 0, width: (_f = scrollElement == null ? void 0 : scrollElement.clientWidth) != null ? _f : 0 }; } function clampOffset(offset, maxOffset) { return Math.max(0, Math.min(offset, maxOffset)); } function getAxisSize(size, horizontal) { return horizontal ? size.width : size.height; } function getMaxOffset(contentSize, layoutMeasurement, horizontal) { return Math.max(0, getAxisSize(contentSize, horizontal) - getAxisSize(layoutMeasurement, horizontal)); } function resolveScrollableNode(scrollElement, isWindowScroll) { return isWindowScroll ? getDocumentScrollerNode() || scrollElement : scrollElement; } function resolveScrollEventTarget(scrollElement, isWindowScroll) { return isWindowScroll && typeof window !== "undefined" ? window : scrollElement; } function getLayoutRectangle(element, isWindowScroll, horizontal) { const rect = element.getBoundingClientRect(); const scroll = getWindowScrollPosition(); return { height: isWindowScroll && typeof window !== "undefined" && !horizontal ? window.innerHeight : rect.height, width: isWindowScroll && typeof window !== "undefined" && horizontal ? window.innerWidth : rect.width, x: isWindowScroll ? rect.left + scroll.x : rect.left, y: isWindowScroll ? rect.top + scroll.y : rect.top }; } function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll }) { return { left: horizontal ? listPos.left + clampedOffset : scroll.x, top: horizontal ? scroll.y : listPos.top + clampedOffset }; } // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { return; } const styleElement = document.createElement("style"); styleElement.id = SCROLLBAR_HIDDEN_STYLE_ID; styleElement.textContent = SCROLLBAR_HIDDEN_STYLE; document.head.appendChild(styleElement); } function getContentInsetEndAdjustmentEnd2(ctx) { var _a3, _b; const adjustment = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.props) == null ? void 0 : _b.contentInsetEndAdjustment; return Math.max(0, adjustment != null ? adjustment : 0); } function getFiniteSnapOffsets(snapToOffsets) { if (!(snapToOffsets == null ? void 0 : snapToOffsets.length)) { return []; } const snapOffsets = []; const seen = /* @__PURE__ */ new Set(); for (const offset of snapToOffsets) { if (Number.isFinite(offset) && !seen.has(offset)) { seen.add(offset); snapOffsets.push(offset); } } return snapOffsets; } function getSnapAnchorStyle(offset, horizontal) { return { height: horizontal ? "100%" : 1, left: horizontal ? offset : 0, pointerEvents: "none", position: "absolute", scrollSnapAlign: "start", top: horizontal ? 0 : offset, width: horizontal ? 1 : "100%" }; } var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ children, style, contentContainerClassName, contentContainerStyle, horizontal = false, contentOffset, maintainVisibleContentPosition, onScroll: onScroll2, onMomentumScrollEnd: _onMomentumScrollEnd, showsHorizontalScrollIndicator = true, showsVerticalScrollIndicator = true, refreshControl, useWindowScroll = false, onLayout, ...props }, ref) { var _a3, _b, _c, _d; const ctx = useStateContext(); const [anchoredEndSpaceSize] = useArr$(["anchoredEndSpaceSize"]); const scrollRef = useRef(null); const contentRef = useRef(null); const isWindowScroll = useWindowScroll; const getScrollTarget = useCallback( () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); const getMaxScrollOffset = useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); const layoutMeasurement = getLayoutMeasurement(scrollElement, isWindowScroll, horizontal); return getMaxOffset(contentSize, layoutMeasurement, horizontal); }, [horizontal, isWindowScroll]); const getCurrentScrollOffset = useCallback(() => { const scrollElement = scrollRef.current; if (isWindowScroll) { const maxOffset = getMaxScrollOffset(); const scroll = getWindowScrollPosition(); const listPos = getElementDocumentPosition(scrollElement, scroll); const rawOffset = horizontal ? scroll.x - listPos.left : scroll.y - listPos.top; return clampOffset(rawOffset, maxOffset); } if (!scrollElement) { return 0; } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); const scrollToLocalOffset = useCallback( (offset, animated) => { const scrollElement = scrollRef.current; const target = getScrollTarget(); if (!target || typeof target.scrollTo !== "function") { return; } const maxOffset = getMaxScrollOffset(); const clampedOffset = clampOffset(offset, maxOffset); const behavior = animated ? "smooth" : "auto"; const options = { behavior }; if (isWindowScroll) { const scroll = getWindowScrollPosition(); const listPos = getElementDocumentPosition(scrollElement, scroll); const { left, top } = resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll }); options.left = left; options.top = top; } else if (horizontal) { options.left = clampedOffset; } else { options.top = clampedOffset; } target.scrollTo(options); }, [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] ); useImperativeHandle(ref, () => { const api = { getBoundingClientRect: () => { var _a4; return (_a4 = scrollRef.current) == null ? void 0 : _a4.getBoundingClientRect(); }, getCurrentScrollOffset, getScrollableNode: () => resolveScrollableNode(scrollRef.current, isWindowScroll), getScrollEventTarget: () => getScrollTarget(), getScrollResponder: () => resolveScrollableNode(scrollRef.current, isWindowScroll), isWindowScroll: () => isWindowScroll, scrollBy: (x, y) => { const target = getScrollTarget(); if (!target || typeof target.scrollBy !== "function") { return; } target.scrollBy({ behavior: "auto", left: x, top: y }); }, scrollTo: (options) => { const { x = 0, y = 0, animated = true } = options; scrollToLocalOffset(horizontal ? x : y, animated); }, scrollToEnd: (options = {}) => { const { animated = true } = options; const endOffset = getMaxScrollOffset(); scrollToLocalOffset(endOffset, animated); }, scrollToOffset: (params) => { const { offset, animated = true } = params; scrollToLocalOffset(offset, animated); } }; return api; }, [getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollToLoc