cogsbox-state 0.5.430 → 0.5.432
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CogsState.jsx +642 -606
- package/dist/CogsState.jsx.map +1 -1
- package/package.json +1 -1
- package/src/CogsState.tsx +159 -77
package/package.json
CHANGED
package/src/CogsState.tsx
CHANGED
|
@@ -1735,7 +1735,6 @@ function createProxyHandler<T>(
|
|
|
1735
1735
|
return (
|
|
1736
1736
|
options: VirtualViewOptions
|
|
1737
1737
|
): VirtualStateObjectResult<any[]> => {
|
|
1738
|
-
// --- All this setup is from your original, working code ---
|
|
1739
1738
|
const {
|
|
1740
1739
|
itemHeight = 50,
|
|
1741
1740
|
overscan = 6,
|
|
@@ -1753,8 +1752,8 @@ function createProxyHandler<T>(
|
|
|
1753
1752
|
const userHasScrolledAwayRef = useRef(false);
|
|
1754
1753
|
const previousCountRef = useRef(0);
|
|
1755
1754
|
const lastRangeRef = useRef(range);
|
|
1756
|
-
|
|
1757
|
-
//
|
|
1755
|
+
const orderedIds = getOrderedIds(path);
|
|
1756
|
+
// Subscribe to shadow state updates
|
|
1758
1757
|
useEffect(() => {
|
|
1759
1758
|
const unsubscribe = getGlobalStore
|
|
1760
1759
|
.getState()
|
|
@@ -1770,36 +1769,26 @@ function createProxyHandler<T>(
|
|
|
1770
1769
|
) as any[];
|
|
1771
1770
|
const totalCount = sourceArray.length;
|
|
1772
1771
|
|
|
1773
|
-
// --- START OF IMPROVEMENT ---
|
|
1774
|
-
// Get the canonical order of item IDs for this array. This is the most
|
|
1775
|
-
// reliable way to link an item's index to its metadata.
|
|
1776
|
-
const orderedIds = getOrderedIds(path);
|
|
1777
|
-
// --- END OF IMPROVEMENT ---
|
|
1778
|
-
|
|
1779
1772
|
// Calculate heights and positions
|
|
1780
1773
|
const { totalHeight, positions } = useMemo(() => {
|
|
1774
|
+
const arrayMeta = getGlobalStore
|
|
1775
|
+
.getState()
|
|
1776
|
+
.getShadowMetadata(stateKey, path);
|
|
1777
|
+
const orderedIds = arrayMeta?.arrayKeys || []; // Get the ordered IDs
|
|
1781
1778
|
let height = 0;
|
|
1782
1779
|
const pos: number[] = [];
|
|
1783
1780
|
for (let i = 0; i < totalCount; i++) {
|
|
1784
1781
|
pos[i] = height;
|
|
1785
|
-
|
|
1786
|
-
// --- START OF IMPROVEMENT ---
|
|
1787
|
-
// Use the ordered ID to look up the correct metadata for the item at this index.
|
|
1788
|
-
// This is much more reliable than numeric indexing into a metadata store.
|
|
1789
|
-
const itemId = orderedIds?.[i];
|
|
1782
|
+
const itemId = orderedIds[i]; // Get the ID for the item at this index
|
|
1790
1783
|
let measuredHeight = itemHeight; // Default height
|
|
1791
|
-
|
|
1792
1784
|
if (itemId) {
|
|
1793
|
-
|
|
1785
|
+
// Get metadata for the specific item using its full path
|
|
1794
1786
|
const itemMeta = getGlobalStore
|
|
1795
1787
|
.getState()
|
|
1796
|
-
.getShadowMetadata(stateKey,
|
|
1797
|
-
// Get the measured height from the shadow state if it exists.
|
|
1788
|
+
.getShadowMetadata(stateKey, [...path, itemId]);
|
|
1798
1789
|
measuredHeight =
|
|
1799
1790
|
itemMeta?.virtualizer?.itemHeight || itemHeight;
|
|
1800
1791
|
}
|
|
1801
|
-
// --- END OF IMPROVEMENT ---
|
|
1802
|
-
|
|
1803
1792
|
height += measuredHeight;
|
|
1804
1793
|
}
|
|
1805
1794
|
return { totalHeight: height, positions: pos };
|
|
@@ -1809,57 +1798,99 @@ function createProxyHandler<T>(
|
|
|
1809
1798
|
path.join("."),
|
|
1810
1799
|
itemHeight,
|
|
1811
1800
|
shadowUpdateTrigger,
|
|
1812
|
-
orderedIds, // Add `orderedIds` to the dependency array
|
|
1813
1801
|
]);
|
|
1814
1802
|
|
|
1815
|
-
// Create virtual state
|
|
1803
|
+
// Create virtual state
|
|
1816
1804
|
const virtualState = useMemo(() => {
|
|
1817
1805
|
const start = Math.max(0, range.startIndex);
|
|
1818
1806
|
const end = Math.min(totalCount, range.endIndex);
|
|
1819
1807
|
|
|
1820
|
-
//
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
const
|
|
1825
|
-
const sourceMap = new Map(
|
|
1826
|
-
sourceArray.map((item: any) => [`id:${item.id}`, item])
|
|
1827
|
-
);
|
|
1828
|
-
const slicedArray =
|
|
1829
|
-
slicedIds?.map((id) => sourceMap.get(id)).filter(Boolean) ||
|
|
1830
|
-
[];
|
|
1808
|
+
// Get the ordered IDs from the parent array's metadata
|
|
1809
|
+
const arrayMeta = getGlobalStore
|
|
1810
|
+
.getState()
|
|
1811
|
+
.getShadowMetadata(stateKey, path);
|
|
1812
|
+
const orderedIds = arrayMeta?.arrayKeys || [];
|
|
1831
1813
|
|
|
1814
|
+
// Slice the array of data and the array of IDs to match the virtual range
|
|
1815
|
+
const slicedArray = sourceArray.slice(start, end);
|
|
1816
|
+
const slicedIds = orderedIds.slice(start, end);
|
|
1817
|
+
|
|
1818
|
+
// Create the new proxy, passing the sliced IDs as its `validIds` metadata.
|
|
1832
1819
|
return rebuildStateShape(slicedArray as any, path, {
|
|
1833
1820
|
...meta,
|
|
1834
|
-
validIds: slicedIds,
|
|
1821
|
+
validIds: slicedIds,
|
|
1835
1822
|
});
|
|
1836
|
-
|
|
1837
|
-
}, [
|
|
1838
|
-
range.startIndex,
|
|
1839
|
-
range.endIndex,
|
|
1840
|
-
sourceArray,
|
|
1841
|
-
totalCount,
|
|
1842
|
-
orderedIds,
|
|
1843
|
-
]);
|
|
1823
|
+
}, [range.startIndex, range.endIndex, sourceArray, totalCount]);
|
|
1844
1824
|
|
|
1845
|
-
//
|
|
1846
|
-
|
|
1825
|
+
// Helper to scroll to last item using stored ref
|
|
1826
|
+
const scrollToLastItem = useCallback(() => {
|
|
1827
|
+
const shadowArray = getGlobalStore
|
|
1828
|
+
.getState()
|
|
1829
|
+
.getShadowMetadata(stateKey, path);
|
|
1830
|
+
if (
|
|
1831
|
+
!shadowArray ||
|
|
1832
|
+
(shadowArray && shadowArray?.arrayKeys?.length === 0)
|
|
1833
|
+
) {
|
|
1834
|
+
return false;
|
|
1835
|
+
}
|
|
1836
|
+
const lastIndex = totalCount - 1;
|
|
1837
|
+
const lastKkey = [...path, shadowArray.arrayKeys![lastIndex]!];
|
|
1838
|
+
if (lastIndex >= 0) {
|
|
1839
|
+
const lastItemData = getGlobalStore
|
|
1840
|
+
.getState()
|
|
1841
|
+
.getShadowMetadata(stateKey, lastKkey);
|
|
1842
|
+
if (lastItemData?.virtualizer?.domRef) {
|
|
1843
|
+
const element = lastItemData.virtualizer.domRef;
|
|
1844
|
+
if (element && element.scrollIntoView) {
|
|
1845
|
+
element.scrollIntoView({
|
|
1846
|
+
behavior: "auto",
|
|
1847
|
+
block: "end",
|
|
1848
|
+
inline: "nearest",
|
|
1849
|
+
});
|
|
1850
|
+
return true;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
return false;
|
|
1855
|
+
}, [stateKey, path, totalCount]);
|
|
1847
1856
|
|
|
1848
|
-
// Handle new items when at bottom
|
|
1857
|
+
// Handle new items when at bottom
|
|
1849
1858
|
useEffect(() => {
|
|
1850
1859
|
if (!stickToBottom || totalCount === 0) return;
|
|
1860
|
+
|
|
1851
1861
|
const hasNewItems = totalCount > previousCountRef.current;
|
|
1862
|
+
const isInitialLoad =
|
|
1863
|
+
previousCountRef.current === 0 && totalCount > 0;
|
|
1864
|
+
|
|
1865
|
+
// Only auto-scroll if user hasn't scrolled away
|
|
1852
1866
|
if (
|
|
1853
|
-
hasNewItems &&
|
|
1867
|
+
(hasNewItems || isInitialLoad) &&
|
|
1854
1868
|
wasAtBottomRef.current &&
|
|
1855
1869
|
!userHasScrolledAwayRef.current
|
|
1856
1870
|
) {
|
|
1857
|
-
|
|
1871
|
+
const visibleCount = Math.ceil(
|
|
1872
|
+
(containerRef.current?.clientHeight || 0) / itemHeight
|
|
1873
|
+
);
|
|
1874
|
+
const newRange = {
|
|
1875
|
+
startIndex: Math.max(
|
|
1876
|
+
0,
|
|
1877
|
+
totalCount - visibleCount - overscan
|
|
1878
|
+
),
|
|
1879
|
+
endIndex: totalCount,
|
|
1880
|
+
};
|
|
1881
|
+
|
|
1882
|
+
setRange(newRange);
|
|
1883
|
+
|
|
1884
|
+
const timeoutId = setTimeout(() => {
|
|
1885
|
+
scrollToIndex(totalCount - 1, "smooth");
|
|
1886
|
+
}, 50);
|
|
1887
|
+
return () => clearTimeout(timeoutId);
|
|
1858
1888
|
}
|
|
1889
|
+
|
|
1859
1890
|
previousCountRef.current = totalCount;
|
|
1860
|
-
}, [totalCount,
|
|
1891
|
+
}, [totalCount, itemHeight, overscan]);
|
|
1861
1892
|
|
|
1862
|
-
// Handle scroll events
|
|
1893
|
+
// Handle scroll events
|
|
1863
1894
|
useEffect(() => {
|
|
1864
1895
|
const container = containerRef.current;
|
|
1865
1896
|
if (!container) return;
|
|
@@ -1868,12 +1899,21 @@ function createProxyHandler<T>(
|
|
|
1868
1899
|
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
1869
1900
|
const distanceFromBottom =
|
|
1870
1901
|
scrollHeight - scrollTop - clientHeight;
|
|
1902
|
+
|
|
1903
|
+
// Track if we're at bottom
|
|
1871
1904
|
wasAtBottomRef.current = distanceFromBottom < 5;
|
|
1872
|
-
|
|
1905
|
+
|
|
1906
|
+
// If user scrolls away from bottom past threshold, set flag
|
|
1907
|
+
if (distanceFromBottom > 100) {
|
|
1873
1908
|
userHasScrolledAwayRef.current = true;
|
|
1874
|
-
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
// If user scrolls back to bottom, cle
|
|
1912
|
+
if (distanceFromBottom < 5) {
|
|
1875
1913
|
userHasScrolledAwayRef.current = false;
|
|
1914
|
+
}
|
|
1876
1915
|
|
|
1916
|
+
// Update visible range based on scroll position
|
|
1877
1917
|
let startIndex = 0;
|
|
1878
1918
|
for (let i = 0; i < positions.length; i++) {
|
|
1879
1919
|
if (positions[i]! > scrollTop - itemHeight * overscan) {
|
|
@@ -1881,6 +1921,7 @@ function createProxyHandler<T>(
|
|
|
1881
1921
|
break;
|
|
1882
1922
|
}
|
|
1883
1923
|
}
|
|
1924
|
+
|
|
1884
1925
|
let endIndex = startIndex;
|
|
1885
1926
|
const viewportEnd = scrollTop + clientHeight;
|
|
1886
1927
|
for (let i = startIndex; i < positions.length; i++) {
|
|
@@ -1889,12 +1930,14 @@ function createProxyHandler<T>(
|
|
|
1889
1930
|
}
|
|
1890
1931
|
endIndex = i;
|
|
1891
1932
|
}
|
|
1933
|
+
|
|
1892
1934
|
const newStartIndex = Math.max(0, startIndex);
|
|
1893
1935
|
const newEndIndex = Math.min(
|
|
1894
1936
|
totalCount,
|
|
1895
1937
|
endIndex + 1 + overscan
|
|
1896
1938
|
);
|
|
1897
1939
|
|
|
1940
|
+
// THE FIX: Only update state if the visible range of items has changed.
|
|
1898
1941
|
if (
|
|
1899
1942
|
newStartIndex !== lastRangeRef.current.startIndex ||
|
|
1900
1943
|
newEndIndex !== lastRangeRef.current.endIndex
|
|
@@ -1913,59 +1956,98 @@ function createProxyHandler<T>(
|
|
|
1913
1956
|
container.addEventListener("scroll", handleScroll, {
|
|
1914
1957
|
passive: true,
|
|
1915
1958
|
});
|
|
1959
|
+
|
|
1960
|
+
// Only auto-scroll on initial load when user hasn't scrolled away
|
|
1961
|
+
if (
|
|
1962
|
+
stickToBottom &&
|
|
1963
|
+
totalCount > 0 &&
|
|
1964
|
+
!userHasScrolledAwayRef.current
|
|
1965
|
+
) {
|
|
1966
|
+
const { scrollTop } = container;
|
|
1967
|
+
// Only if we're at the very top (initial load)
|
|
1968
|
+
if (scrollTop === 0) {
|
|
1969
|
+
container.scrollTop = container.scrollHeight;
|
|
1970
|
+
wasAtBottomRef.current = true;
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1916
1974
|
handleScroll();
|
|
1917
|
-
|
|
1975
|
+
|
|
1976
|
+
return () => {
|
|
1918
1977
|
container.removeEventListener("scroll", handleScroll);
|
|
1919
|
-
|
|
1978
|
+
};
|
|
1979
|
+
}, [positions, totalCount, itemHeight, overscan, stickToBottom]);
|
|
1920
1980
|
|
|
1921
1981
|
const scrollToBottom = useCallback(() => {
|
|
1922
1982
|
wasAtBottomRef.current = true;
|
|
1923
1983
|
userHasScrolledAwayRef.current = false;
|
|
1924
|
-
|
|
1984
|
+
const scrolled = scrollToLastItem();
|
|
1985
|
+
if (!scrolled && containerRef.current) {
|
|
1925
1986
|
containerRef.current.scrollTop =
|
|
1926
1987
|
containerRef.current.scrollHeight;
|
|
1927
1988
|
}
|
|
1928
|
-
}, []);
|
|
1929
|
-
|
|
1989
|
+
}, [scrollToLastItem]);
|
|
1930
1990
|
const scrollToIndex = useCallback(
|
|
1931
1991
|
(index: number, behavior: ScrollBehavior = "smooth") => {
|
|
1932
1992
|
const container = containerRef.current;
|
|
1933
1993
|
if (!container) return;
|
|
1934
1994
|
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1995
|
+
const isLastItem = index === totalCount - 1;
|
|
1996
|
+
|
|
1997
|
+
// --- Special Case: The Last Item ---
|
|
1998
|
+
if (isLastItem) {
|
|
1999
|
+
// For the last item, scrollIntoView can fail. The most reliable method
|
|
2000
|
+
// is to scroll the parent container to its maximum scroll height.
|
|
2001
|
+
container.scrollTo({
|
|
2002
|
+
top: container.scrollHeight,
|
|
2003
|
+
behavior: behavior,
|
|
2004
|
+
});
|
|
2005
|
+
return; // We're done.
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// --- Standard Case: All Other Items ---
|
|
2009
|
+
// For all other items, we find the ref and use scrollIntoView.
|
|
2010
|
+
const arrayMeta = getGlobalStore
|
|
2011
|
+
.getState()
|
|
2012
|
+
.getShadowMetadata(stateKey, path);
|
|
2013
|
+
const orderedIds = arrayMeta?.arrayKeys || [];
|
|
2014
|
+
const itemId = orderedIds[index];
|
|
2015
|
+
let element: HTMLElement | null | undefined = undefined;
|
|
2016
|
+
|
|
1938
2017
|
if (itemId) {
|
|
1939
|
-
const itemPathWithId = [...path, itemId];
|
|
1940
2018
|
const itemMeta = getGlobalStore
|
|
1941
2019
|
.getState()
|
|
1942
|
-
.getShadowMetadata(stateKey,
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
// If we have a direct ref to the DOM element from CogsItemWrapper, use it! It's the most reliable.
|
|
1946
|
-
if (element?.scrollIntoView) {
|
|
1947
|
-
element.scrollIntoView({ behavior, block: "nearest" });
|
|
1948
|
-
return;
|
|
1949
|
-
}
|
|
2020
|
+
.getShadowMetadata(stateKey, [...path, itemId]);
|
|
2021
|
+
element = itemMeta?.virtualizer?.domRef;
|
|
1950
2022
|
}
|
|
1951
|
-
// --- END OF IMPROVEMENT ---
|
|
1952
2023
|
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
2024
|
+
if (element) {
|
|
2025
|
+
// 'center' gives a better user experience for items in the middle of the list.
|
|
2026
|
+
element.scrollIntoView({
|
|
2027
|
+
behavior: behavior,
|
|
2028
|
+
block: "center",
|
|
2029
|
+
});
|
|
2030
|
+
} else if (positions[index] !== undefined) {
|
|
2031
|
+
// Fallback if the ref isn't available for some reason.
|
|
2032
|
+
container.scrollTo({
|
|
2033
|
+
top: positions[index],
|
|
2034
|
+
behavior,
|
|
2035
|
+
});
|
|
1957
2036
|
}
|
|
1958
2037
|
},
|
|
1959
|
-
[positions, stateKey, path,
|
|
2038
|
+
[positions, stateKey, path, totalCount] // Add totalCount to the dependencies
|
|
1960
2039
|
);
|
|
1961
2040
|
|
|
1962
2041
|
const virtualizerProps = {
|
|
1963
2042
|
outer: {
|
|
1964
2043
|
ref: containerRef,
|
|
1965
|
-
style: { overflowY: "auto", height: "100%" },
|
|
2044
|
+
style: { overflowY: "auto" as const, height: "100%" },
|
|
1966
2045
|
},
|
|
1967
2046
|
inner: {
|
|
1968
|
-
style: {
|
|
2047
|
+
style: {
|
|
2048
|
+
height: `${totalHeight}px`,
|
|
2049
|
+
position: "relative" as const,
|
|
2050
|
+
},
|
|
1969
2051
|
},
|
|
1970
2052
|
list: {
|
|
1971
2053
|
style: {
|
|
@@ -1976,7 +2058,7 @@ function createProxyHandler<T>(
|
|
|
1976
2058
|
|
|
1977
2059
|
return {
|
|
1978
2060
|
virtualState,
|
|
1979
|
-
virtualizerProps
|
|
2061
|
+
virtualizerProps,
|
|
1980
2062
|
scrollToBottom,
|
|
1981
2063
|
scrollToIndex,
|
|
1982
2064
|
};
|