cogsbox-state 0.5.331 → 0.5.332
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 +720 -700
- package/dist/CogsState.jsx.map +1 -1
- package/package.json +1 -1
- package/src/CogsState.tsx +119 -65
package/package.json
CHANGED
package/src/CogsState.tsx
CHANGED
|
@@ -1813,9 +1813,13 @@ function createProxyHandler<T>(
|
|
|
1813
1813
|
startIndex: 0,
|
|
1814
1814
|
endIndex: 10,
|
|
1815
1815
|
});
|
|
1816
|
+
|
|
1817
|
+
// This ref tracks if the user is locked to the bottom.
|
|
1818
|
+
const isLockedToBottomRef = useRef(stickToBottom);
|
|
1819
|
+
|
|
1820
|
+
// This state triggers a re-render when item heights change.
|
|
1816
1821
|
const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
|
|
1817
1822
|
|
|
1818
|
-
// Subscribe to shadow updates
|
|
1819
1823
|
useEffect(() => {
|
|
1820
1824
|
const unsubscribe = getGlobalStore
|
|
1821
1825
|
.getState()
|
|
@@ -1831,18 +1835,20 @@ function createProxyHandler<T>(
|
|
|
1831
1835
|
) as any[];
|
|
1832
1836
|
const totalCount = sourceArray.length;
|
|
1833
1837
|
|
|
1834
|
-
// Calculate
|
|
1835
|
-
const totalHeight = useMemo(() => {
|
|
1838
|
+
// Calculate heights from shadow state. This runs when data or measurements change.
|
|
1839
|
+
const { totalHeight, positions } = useMemo(() => {
|
|
1836
1840
|
const shadowArray =
|
|
1837
1841
|
getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
|
|
1838
1842
|
[];
|
|
1839
1843
|
let height = 0;
|
|
1844
|
+
const pos: number[] = [];
|
|
1840
1845
|
for (let i = 0; i < totalCount; i++) {
|
|
1846
|
+
pos[i] = height;
|
|
1841
1847
|
const measuredHeight =
|
|
1842
1848
|
shadowArray[i]?.virtualizer?.itemHeight;
|
|
1843
1849
|
height += measuredHeight || itemHeight;
|
|
1844
1850
|
}
|
|
1845
|
-
return height;
|
|
1851
|
+
return { totalHeight: height, positions: pos };
|
|
1846
1852
|
}, [
|
|
1847
1853
|
totalCount,
|
|
1848
1854
|
stateKey,
|
|
@@ -1851,7 +1857,7 @@ function createProxyHandler<T>(
|
|
|
1851
1857
|
shadowUpdateTrigger,
|
|
1852
1858
|
]);
|
|
1853
1859
|
|
|
1854
|
-
//
|
|
1860
|
+
// Memoize the virtualized slice of data.
|
|
1855
1861
|
const virtualState = useMemo(() => {
|
|
1856
1862
|
const start = Math.max(0, range.startIndex);
|
|
1857
1863
|
const end = Math.min(totalCount, range.endIndex);
|
|
@@ -1865,47 +1871,123 @@ function createProxyHandler<T>(
|
|
|
1865
1871
|
validIndices,
|
|
1866
1872
|
});
|
|
1867
1873
|
}, [range.startIndex, range.endIndex, sourceArray, totalCount]);
|
|
1868
|
-
|
|
1869
|
-
// Handle scroll
|
|
1870
1874
|
useEffect(() => {
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
const visibleCount = Math.ceil(clientHeight / itemHeight);
|
|
1875
|
+
if (stickToBottom && totalCount > 0 && containerRef.current) {
|
|
1876
|
+
// When count increases, immediately adjust range to show bottom
|
|
1877
|
+
const container = containerRef.current;
|
|
1878
|
+
const visibleCount = Math.ceil(
|
|
1879
|
+
container.clientHeight / itemHeight
|
|
1880
|
+
);
|
|
1878
1881
|
|
|
1882
|
+
// Set range to show the last items including the new one
|
|
1879
1883
|
setRange({
|
|
1880
|
-
startIndex: Math.max(
|
|
1881
|
-
|
|
1882
|
-
totalCount
|
|
1883
|
-
startIndex + visibleCount + overscan
|
|
1884
|
+
startIndex: Math.max(
|
|
1885
|
+
0,
|
|
1886
|
+
totalCount - visibleCount - overscan
|
|
1884
1887
|
),
|
|
1888
|
+
endIndex: totalCount,
|
|
1885
1889
|
});
|
|
1890
|
+
|
|
1891
|
+
// Then scroll to bottom after a short delay
|
|
1892
|
+
setTimeout(() => {
|
|
1893
|
+
container.scrollTop = container.scrollHeight;
|
|
1894
|
+
}, 100);
|
|
1895
|
+
}
|
|
1896
|
+
}, [totalCount]);
|
|
1897
|
+
// This is the main effect that handles all scrolling and updates.
|
|
1898
|
+
useLayoutEffect(() => {
|
|
1899
|
+
const container = containerRef.current;
|
|
1900
|
+
if (!container) return;
|
|
1901
|
+
|
|
1902
|
+
let scrollTimeoutId: NodeJS.Timeout;
|
|
1903
|
+
|
|
1904
|
+
// This function determines what's visible in the viewport.
|
|
1905
|
+
const updateVirtualRange = () => {
|
|
1906
|
+
if (!container) return;
|
|
1907
|
+
const { scrollTop } = container;
|
|
1908
|
+
let low = 0,
|
|
1909
|
+
high = totalCount - 1;
|
|
1910
|
+
while (low <= high) {
|
|
1911
|
+
const mid = Math.floor((low + high) / 2);
|
|
1912
|
+
if (positions[mid]! < scrollTop) low = mid + 1;
|
|
1913
|
+
else high = mid - 1;
|
|
1914
|
+
}
|
|
1915
|
+
const startIndex = Math.max(0, high - overscan);
|
|
1916
|
+
let endIndex = startIndex;
|
|
1917
|
+
const visibleEnd = scrollTop + container.clientHeight;
|
|
1918
|
+
while (
|
|
1919
|
+
endIndex < totalCount &&
|
|
1920
|
+
positions[endIndex]! < visibleEnd
|
|
1921
|
+
) {
|
|
1922
|
+
endIndex++;
|
|
1923
|
+
}
|
|
1924
|
+
endIndex = Math.min(totalCount, endIndex + overscan);
|
|
1925
|
+
setRange({ startIndex, endIndex });
|
|
1886
1926
|
};
|
|
1887
1927
|
|
|
1888
|
-
|
|
1889
|
-
|
|
1928
|
+
// This function handles ONLY user-initiated scrolls.
|
|
1929
|
+
const handleUserScroll = () => {
|
|
1930
|
+
isLockedToBottomRef.current =
|
|
1931
|
+
container.scrollHeight -
|
|
1932
|
+
container.scrollTop -
|
|
1933
|
+
container.clientHeight <
|
|
1934
|
+
1;
|
|
1935
|
+
updateVirtualRange();
|
|
1936
|
+
};
|
|
1890
1937
|
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1938
|
+
container.addEventListener("scroll", handleUserScroll, {
|
|
1939
|
+
passive: true,
|
|
1940
|
+
});
|
|
1894
1941
|
|
|
1895
|
-
|
|
1896
|
-
if (
|
|
1897
|
-
|
|
1898
|
-
|
|
1942
|
+
// --- THE CORE FIX ---
|
|
1943
|
+
if (stickToBottom && isLockedToBottomRef.current) {
|
|
1944
|
+
// We use a timeout to wait for React to render AND for useMeasure to update heights.
|
|
1945
|
+
// This is the CRUCIAL part that fixes the race condition.
|
|
1946
|
+
scrollTimeoutId = setTimeout(() => {
|
|
1947
|
+
console.log("totalHeight", totalHeight);
|
|
1948
|
+
if (isLockedToBottomRef.current) {
|
|
1949
|
+
container.scrollTo({
|
|
1950
|
+
top: 999999999,
|
|
1951
|
+
behavior: "smooth", // ALWAYS 'auto' for an instant, correct jump.
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
}, 200); // A small 50ms delay is a robust buffer.
|
|
1899
1955
|
}
|
|
1900
|
-
}, []);
|
|
1901
1956
|
|
|
1902
|
-
|
|
1903
|
-
|
|
1957
|
+
updateVirtualRange();
|
|
1958
|
+
|
|
1959
|
+
// Cleanup function is vital to prevent memory leaks.
|
|
1960
|
+
return () => {
|
|
1961
|
+
clearTimeout(scrollTimeoutId);
|
|
1962
|
+
container.removeEventListener("scroll", handleUserScroll);
|
|
1963
|
+
};
|
|
1964
|
+
// This effect re-runs whenever the list size or item heights change.
|
|
1965
|
+
}, [totalCount, positions, totalHeight, stickToBottom]);
|
|
1966
|
+
|
|
1967
|
+
const scrollToBottom = useCallback(
|
|
1968
|
+
(behavior: ScrollBehavior = "smooth") => {
|
|
1904
1969
|
if (containerRef.current) {
|
|
1905
|
-
|
|
1970
|
+
isLockedToBottomRef.current = true;
|
|
1971
|
+
containerRef.current.scrollTo({
|
|
1972
|
+
top: containerRef.current.scrollHeight,
|
|
1973
|
+
behavior,
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
},
|
|
1977
|
+
[]
|
|
1978
|
+
);
|
|
1979
|
+
|
|
1980
|
+
const scrollToIndex = useCallback(
|
|
1981
|
+
(index: number, behavior: ScrollBehavior = "smooth") => {
|
|
1982
|
+
if (containerRef.current && positions[index] !== undefined) {
|
|
1983
|
+
isLockedToBottomRef.current = false;
|
|
1984
|
+
containerRef.current.scrollTo({
|
|
1985
|
+
top: positions[index],
|
|
1986
|
+
behavior,
|
|
1987
|
+
});
|
|
1906
1988
|
}
|
|
1907
1989
|
},
|
|
1908
|
-
[
|
|
1990
|
+
[positions]
|
|
1909
1991
|
);
|
|
1910
1992
|
|
|
1911
1993
|
const virtualizerProps = {
|
|
@@ -1921,10 +2003,7 @@ function createProxyHandler<T>(
|
|
|
1921
2003
|
},
|
|
1922
2004
|
list: {
|
|
1923
2005
|
style: {
|
|
1924
|
-
|
|
1925
|
-
top: `${range.startIndex * itemHeight}px`,
|
|
1926
|
-
left: 0,
|
|
1927
|
-
right: 0,
|
|
2006
|
+
transform: `translateY(${positions[range.startIndex] || 0}px)`,
|
|
1928
2007
|
},
|
|
1929
2008
|
},
|
|
1930
2009
|
};
|
|
@@ -2136,24 +2215,21 @@ function createProxyHandler<T>(
|
|
|
2136
2215
|
return null;
|
|
2137
2216
|
}
|
|
2138
2217
|
|
|
2139
|
-
const arrayLength = arrayToMap.length;
|
|
2140
2218
|
const indicesToMap =
|
|
2141
2219
|
meta?.validIndices ||
|
|
2142
|
-
Array.from({ length:
|
|
2220
|
+
Array.from({ length: arrayToMap.length }, (_, i) => i);
|
|
2143
2221
|
|
|
2144
2222
|
return indicesToMap.map((originalIndex, localIndex) => {
|
|
2145
2223
|
const item = arrayToMap[originalIndex];
|
|
2146
2224
|
const finalPath = [...path, originalIndex.toString()];
|
|
2147
2225
|
const setter = rebuildStateShape(item, finalPath, meta);
|
|
2148
2226
|
const itemComponentId = `${componentId}-${path.join(".")}-${originalIndex}`;
|
|
2149
|
-
const isLastItem = originalIndex === arrayLength - 1;
|
|
2150
2227
|
|
|
2151
2228
|
return createElement(CogsItemWrapper, {
|
|
2152
2229
|
key: originalIndex,
|
|
2153
2230
|
stateKey,
|
|
2154
2231
|
itemComponentId,
|
|
2155
2232
|
itemPath: finalPath,
|
|
2156
|
-
isLastItem, // Pass it here!
|
|
2157
2233
|
children: callbackfn(
|
|
2158
2234
|
item,
|
|
2159
2235
|
setter,
|
|
@@ -2883,25 +2959,21 @@ export function $cogsSignalStore(proxy: {
|
|
|
2883
2959
|
);
|
|
2884
2960
|
return createElement("text", {}, String(value));
|
|
2885
2961
|
}
|
|
2886
|
-
|
|
2887
2962
|
function CogsItemWrapper({
|
|
2888
2963
|
stateKey,
|
|
2889
2964
|
itemComponentId,
|
|
2890
2965
|
itemPath,
|
|
2891
|
-
isLastItem,
|
|
2892
2966
|
children,
|
|
2893
2967
|
}: {
|
|
2894
2968
|
stateKey: string;
|
|
2895
2969
|
itemComponentId: string;
|
|
2896
2970
|
itemPath: string[];
|
|
2897
|
-
isLastItem: boolean;
|
|
2898
2971
|
children: React.ReactNode;
|
|
2899
2972
|
}) {
|
|
2900
2973
|
// This hook handles the re-rendering when the item's own data changes.
|
|
2901
2974
|
const [, forceUpdate] = useState({});
|
|
2902
2975
|
// This hook measures the element.
|
|
2903
|
-
const [
|
|
2904
|
-
const scrollRef = useRef<HTMLDivElement | null>(null);
|
|
2976
|
+
const [ref, bounds] = useMeasure();
|
|
2905
2977
|
// This ref prevents sending the same height update repeatedly.
|
|
2906
2978
|
const lastReportedHeight = useRef<number | null>(null);
|
|
2907
2979
|
|
|
@@ -2947,25 +3019,7 @@ function CogsItemWrapper({
|
|
|
2947
3019
|
}
|
|
2948
3020
|
};
|
|
2949
3021
|
}, [stateKey, itemComponentId, itemPath.join(".")]);
|
|
2950
|
-
|
|
2951
|
-
if (isLastItem && scrollRef.current) {
|
|
2952
|
-
setTimeout(() => {
|
|
2953
|
-
scrollRef.current?.scrollIntoView({
|
|
2954
|
-
behavior: "smooth",
|
|
2955
|
-
block: "end",
|
|
2956
|
-
});
|
|
2957
|
-
}, 50);
|
|
2958
|
-
}
|
|
2959
|
-
}, [isLastItem]);
|
|
3022
|
+
|
|
2960
3023
|
// The rendered output is a simple div that gets measured.
|
|
2961
|
-
return
|
|
2962
|
-
<div
|
|
2963
|
-
ref={(el) => {
|
|
2964
|
-
measureRef(el);
|
|
2965
|
-
scrollRef.current = el;
|
|
2966
|
-
}}
|
|
2967
|
-
>
|
|
2968
|
-
{children}
|
|
2969
|
-
</div>
|
|
2970
|
-
);
|
|
3024
|
+
return <div ref={ref}>{children}</div>;
|
|
2971
3025
|
}
|