cogsbox-state 0.5.410 → 0.5.411
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 +725 -760
- package/dist/CogsState.jsx.map +1 -1
- package/package.json +1 -1
- package/src/CogsState.tsx +115 -228
package/package.json
CHANGED
package/src/CogsState.tsx
CHANGED
|
@@ -1821,11 +1821,8 @@ function createProxyHandler<T>(
|
|
|
1821
1821
|
endIndex: 10,
|
|
1822
1822
|
});
|
|
1823
1823
|
const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
|
|
1824
|
-
const
|
|
1825
|
-
const
|
|
1826
|
-
const scrollToBottomIntervalRef = useRef<NodeJS.Timeout | null>(
|
|
1827
|
-
null
|
|
1828
|
-
);
|
|
1824
|
+
const wasAtBottomRef = useRef(true);
|
|
1825
|
+
const previousCountRef = useRef(0);
|
|
1829
1826
|
|
|
1830
1827
|
// Subscribe to shadow state updates
|
|
1831
1828
|
useEffect(() => {
|
|
@@ -1880,104 +1877,91 @@ function createProxyHandler<T>(
|
|
|
1880
1877
|
});
|
|
1881
1878
|
}, [range.startIndex, range.endIndex, sourceArray, totalCount]);
|
|
1882
1879
|
|
|
1883
|
-
//
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
if (
|
|
1891
|
-
|
|
1880
|
+
// Helper to scroll to last item using stored ref
|
|
1881
|
+
const scrollToLastItem = useCallback(() => {
|
|
1882
|
+
const shadowArray =
|
|
1883
|
+
getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
|
|
1884
|
+
[];
|
|
1885
|
+
const lastIndex = totalCount - 1;
|
|
1886
|
+
|
|
1887
|
+
if (lastIndex >= 0) {
|
|
1888
|
+
const lastItemData = shadowArray[lastIndex];
|
|
1889
|
+
if (lastItemData?.virtualizer?.domRef) {
|
|
1890
|
+
const element = lastItemData.virtualizer.domRef;
|
|
1891
|
+
if (element && element.scrollIntoView) {
|
|
1892
|
+
element.scrollIntoView({
|
|
1893
|
+
behavior: "auto",
|
|
1894
|
+
block: "end",
|
|
1895
|
+
inline: "nearest",
|
|
1896
|
+
});
|
|
1897
|
+
return true;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1892
1900
|
}
|
|
1901
|
+
return false;
|
|
1902
|
+
}, [stateKey, path, totalCount]);
|
|
1893
1903
|
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
const isBigJump = totalCount > range.endIndex + jumpThreshold;
|
|
1904
|
+
// Handle new items when at bottom
|
|
1905
|
+
useEffect(() => {
|
|
1906
|
+
if (!stickToBottom || totalCount === 0) return;
|
|
1898
1907
|
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1908
|
+
const hasNewItems = totalCount > previousCountRef.current;
|
|
1909
|
+
const isInitialLoad =
|
|
1910
|
+
previousCountRef.current === 0 && totalCount > 0;
|
|
1902
1911
|
|
|
1912
|
+
if ((hasNewItems || isInitialLoad) && wasAtBottomRef.current) {
|
|
1913
|
+
// First, ensure the last items are in range
|
|
1914
|
+
const visibleCount = Math.ceil(
|
|
1915
|
+
containerRef.current?.clientHeight || 0 / itemHeight
|
|
1916
|
+
);
|
|
1903
1917
|
const newRange = {
|
|
1904
|
-
startIndex: Math.max(
|
|
1918
|
+
startIndex: Math.max(
|
|
1919
|
+
0,
|
|
1920
|
+
totalCount - visibleCount - overscan
|
|
1921
|
+
),
|
|
1905
1922
|
endIndex: totalCount,
|
|
1906
1923
|
};
|
|
1907
|
-
setRange(newRange);
|
|
1908
1924
|
|
|
1909
|
-
|
|
1910
|
-
setTimeout(() => {
|
|
1911
|
-
isProgrammaticScrollRef.current = false;
|
|
1912
|
-
}, 100);
|
|
1913
|
-
}
|
|
1914
|
-
|
|
1915
|
-
// Keep scrolling to bottom until we're actually there
|
|
1916
|
-
let attempts = 0;
|
|
1917
|
-
const maxAttempts = 50; // 5 seconds max
|
|
1925
|
+
setRange(newRange);
|
|
1918
1926
|
|
|
1919
|
-
|
|
1920
|
-
const
|
|
1921
|
-
|
|
1927
|
+
// Then scroll to the last item after it renders
|
|
1928
|
+
const timeoutId = setTimeout(() => {
|
|
1929
|
+
const scrolled = scrollToLastItem();
|
|
1930
|
+
if (!scrolled && containerRef.current) {
|
|
1931
|
+
// Fallback if ref not available yet
|
|
1932
|
+
containerRef.current.scrollTop =
|
|
1933
|
+
containerRef.current.scrollHeight;
|
|
1934
|
+
}
|
|
1935
|
+
}, 50);
|
|
1922
1936
|
|
|
1923
|
-
|
|
1937
|
+
previousCountRef.current = totalCount;
|
|
1924
1938
|
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
const actualBottom = scrollHeight;
|
|
1928
|
-
const isAtBottom = actualBottom - currentBottom < 10; // Increased tolerance
|
|
1929
|
-
|
|
1930
|
-
if (isAtBottom || attempts >= maxAttempts) {
|
|
1931
|
-
clearInterval(scrollToBottomIntervalRef.current!);
|
|
1932
|
-
scrollToBottomIntervalRef.current = null;
|
|
1933
|
-
} else {
|
|
1934
|
-
// Set flag before scrolling
|
|
1935
|
-
isProgrammaticScrollRef.current = true;
|
|
1936
|
-
container.scrollTop = container.scrollHeight;
|
|
1937
|
-
|
|
1938
|
-
// Reset flag after a short delay
|
|
1939
|
-
setTimeout(() => {
|
|
1940
|
-
isProgrammaticScrollRef.current = false;
|
|
1941
|
-
}, 50);
|
|
1942
|
-
}
|
|
1943
|
-
}, 100);
|
|
1939
|
+
return () => clearTimeout(timeoutId);
|
|
1940
|
+
}
|
|
1944
1941
|
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1942
|
+
previousCountRef.current = totalCount;
|
|
1943
|
+
}, [
|
|
1944
|
+
totalCount,
|
|
1945
|
+
stickToBottom,
|
|
1946
|
+
itemHeight,
|
|
1947
|
+
overscan,
|
|
1948
|
+
scrollToLastItem,
|
|
1949
|
+
]);
|
|
1953
1950
|
|
|
1954
|
-
// Handle
|
|
1951
|
+
// Handle scroll events
|
|
1955
1952
|
useEffect(() => {
|
|
1956
1953
|
const container = containerRef.current;
|
|
1957
1954
|
if (!container) return;
|
|
1958
1955
|
|
|
1959
1956
|
const handleScroll = () => {
|
|
1960
|
-
// Ignore programmatic scrolls
|
|
1961
|
-
if (isProgrammaticScrollRef.current) {
|
|
1962
|
-
return;
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
// This is a real user scroll
|
|
1966
1957
|
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
1967
1958
|
const distanceFromBottom =
|
|
1968
1959
|
scrollHeight - scrollTop - clientHeight;
|
|
1969
|
-
const isAtBottom = distanceFromBottom < 50; // Increased tolerance
|
|
1970
1960
|
|
|
1971
|
-
//
|
|
1972
|
-
|
|
1973
|
-
clearInterval(scrollToBottomIntervalRef.current);
|
|
1974
|
-
scrollToBottomIntervalRef.current = null;
|
|
1975
|
-
}
|
|
1976
|
-
|
|
1977
|
-
// Only update this for real user scrolls
|
|
1978
|
-
shouldStickToBottomRef.current = isAtBottom;
|
|
1961
|
+
// Track if we're at bottom (with tolerance)
|
|
1962
|
+
wasAtBottomRef.current = distanceFromBottom < 100;
|
|
1979
1963
|
|
|
1980
|
-
// Update visible range
|
|
1964
|
+
// Update visible range based on scroll position
|
|
1981
1965
|
let startIndex = 0;
|
|
1982
1966
|
for (let i = 0; i < positions.length; i++) {
|
|
1983
1967
|
if (positions[i]! > scrollTop - itemHeight * overscan) {
|
|
@@ -2004,42 +1988,53 @@ function createProxyHandler<T>(
|
|
|
2004
1988
|
container.addEventListener("scroll", handleScroll, {
|
|
2005
1989
|
passive: true,
|
|
2006
1990
|
});
|
|
2007
|
-
|
|
1991
|
+
|
|
1992
|
+
// Initial setup
|
|
1993
|
+
if (stickToBottom && totalCount > 0) {
|
|
1994
|
+
// For initial load, jump to bottom
|
|
1995
|
+
container.scrollTop = container.scrollHeight;
|
|
1996
|
+
}
|
|
1997
|
+
handleScroll();
|
|
2008
1998
|
|
|
2009
1999
|
return () => {
|
|
2010
2000
|
container.removeEventListener("scroll", handleScroll);
|
|
2011
2001
|
};
|
|
2012
|
-
}, [positions, totalCount, itemHeight, overscan]);
|
|
2013
|
-
|
|
2014
|
-
const scrollToBottom = useCallback(
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
containerRef.current.
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
setTimeout(() => {
|
|
2023
|
-
isProgrammaticScrollRef.current = false;
|
|
2024
|
-
}, 100);
|
|
2025
|
-
},
|
|
2026
|
-
[]
|
|
2027
|
-
);
|
|
2002
|
+
}, [positions, totalCount, itemHeight, overscan, stickToBottom]);
|
|
2003
|
+
|
|
2004
|
+
const scrollToBottom = useCallback(() => {
|
|
2005
|
+
wasAtBottomRef.current = true;
|
|
2006
|
+
const scrolled = scrollToLastItem();
|
|
2007
|
+
if (!scrolled && containerRef.current) {
|
|
2008
|
+
containerRef.current.scrollTop =
|
|
2009
|
+
containerRef.current.scrollHeight;
|
|
2010
|
+
}
|
|
2011
|
+
}, [scrollToLastItem]);
|
|
2028
2012
|
|
|
2029
2013
|
const scrollToIndex = useCallback(
|
|
2030
2014
|
(index: number, behavior: ScrollBehavior = "smooth") => {
|
|
2031
|
-
|
|
2015
|
+
const shadowArray =
|
|
2016
|
+
getGlobalStore
|
|
2017
|
+
.getState()
|
|
2018
|
+
.getShadowMetadata(stateKey, path) || [];
|
|
2019
|
+
const itemData = shadowArray[index];
|
|
2020
|
+
|
|
2021
|
+
if (itemData?.virtualizer?.domRef) {
|
|
2022
|
+
const element = itemData.virtualizer.domRef;
|
|
2023
|
+
if (element && element.scrollIntoView) {
|
|
2024
|
+
element.scrollIntoView({ behavior, block: "center" });
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
// Fallback to position-based scrolling
|
|
2032
2030
|
if (containerRef.current && positions[index] !== undefined) {
|
|
2033
2031
|
containerRef.current.scrollTo({
|
|
2034
2032
|
top: positions[index],
|
|
2035
2033
|
behavior,
|
|
2036
2034
|
});
|
|
2037
2035
|
}
|
|
2038
|
-
setTimeout(() => {
|
|
2039
|
-
isProgrammaticScrollRef.current = false;
|
|
2040
|
-
}, 100);
|
|
2041
2036
|
},
|
|
2042
|
-
[positions]
|
|
2037
|
+
[positions, stateKey, path]
|
|
2043
2038
|
);
|
|
2044
2039
|
|
|
2045
2040
|
const virtualizerProps = {
|
|
@@ -2068,128 +2063,6 @@ function createProxyHandler<T>(
|
|
|
2068
2063
|
};
|
|
2069
2064
|
};
|
|
2070
2065
|
}
|
|
2071
|
-
if (prop === "stateSort") {
|
|
2072
|
-
return (
|
|
2073
|
-
compareFn: (
|
|
2074
|
-
a: InferArrayElement<T>,
|
|
2075
|
-
b: InferArrayElement<T>
|
|
2076
|
-
) => number
|
|
2077
|
-
) => {
|
|
2078
|
-
const sourceWithIndices = getSourceArrayAndIndices();
|
|
2079
|
-
const sortedResult = [...sourceWithIndices].sort((a, b) =>
|
|
2080
|
-
compareFn(a.item, b.item)
|
|
2081
|
-
);
|
|
2082
|
-
const newCurrentState = sortedResult.map(({ item }) => item);
|
|
2083
|
-
// We construct the meta object with the CORRECT property name: `validIndices`.
|
|
2084
|
-
const newMeta = {
|
|
2085
|
-
...meta,
|
|
2086
|
-
validIndices: sortedResult.map(
|
|
2087
|
-
({ originalIndex }) => originalIndex
|
|
2088
|
-
),
|
|
2089
|
-
};
|
|
2090
|
-
return rebuildStateShape(newCurrentState as any, path, newMeta);
|
|
2091
|
-
};
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
|
-
if (prop === "stateFilter") {
|
|
2095
|
-
return (
|
|
2096
|
-
callbackfn: (
|
|
2097
|
-
value: InferArrayElement<T>,
|
|
2098
|
-
index: number
|
|
2099
|
-
) => boolean
|
|
2100
|
-
) => {
|
|
2101
|
-
const sourceWithIndices = getSourceArrayAndIndices();
|
|
2102
|
-
const filteredResult = sourceWithIndices.filter(
|
|
2103
|
-
({ item }, index) => callbackfn(item, index)
|
|
2104
|
-
);
|
|
2105
|
-
const newCurrentState = filteredResult.map(({ item }) => item);
|
|
2106
|
-
// We construct the meta object with the CORRECT property name: `validIndices`.
|
|
2107
|
-
const newMeta = {
|
|
2108
|
-
...meta,
|
|
2109
|
-
validIndices: filteredResult.map(
|
|
2110
|
-
({ originalIndex }) => originalIndex
|
|
2111
|
-
),
|
|
2112
|
-
};
|
|
2113
|
-
return rebuildStateShape(newCurrentState as any, path, newMeta);
|
|
2114
|
-
};
|
|
2115
|
-
}
|
|
2116
|
-
|
|
2117
|
-
if (prop === "stateMap") {
|
|
2118
|
-
return (
|
|
2119
|
-
callbackfn: (
|
|
2120
|
-
value: InferArrayElement<T>,
|
|
2121
|
-
setter: StateObject<InferArrayElement<T>>,
|
|
2122
|
-
info: {
|
|
2123
|
-
register: () => void;
|
|
2124
|
-
index: number;
|
|
2125
|
-
originalIndex: number;
|
|
2126
|
-
}
|
|
2127
|
-
) => any
|
|
2128
|
-
) => {
|
|
2129
|
-
const arrayToMap = getGlobalStore
|
|
2130
|
-
.getState()
|
|
2131
|
-
.getNestedState(stateKey, path) as any[];
|
|
2132
|
-
|
|
2133
|
-
// Defensive check to make sure we are mapping over an array
|
|
2134
|
-
if (!Array.isArray(arrayToMap)) {
|
|
2135
|
-
console.warn(
|
|
2136
|
-
`stateMap called on a non-array value at path: ${path.join(".")}. The current value is:`,
|
|
2137
|
-
arrayToMap
|
|
2138
|
-
);
|
|
2139
|
-
return null;
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
// If we have validIndices, only map those items
|
|
2143
|
-
const indicesToMap =
|
|
2144
|
-
meta?.validIndices ||
|
|
2145
|
-
Array.from({ length: arrayToMap.length }, (_, i) => i);
|
|
2146
|
-
|
|
2147
|
-
return indicesToMap.map((originalIndex, localIndex) => {
|
|
2148
|
-
const item = arrayToMap[originalIndex];
|
|
2149
|
-
const finalPath = [...path, originalIndex.toString()];
|
|
2150
|
-
const setter = rebuildStateShape(item, finalPath, meta);
|
|
2151
|
-
|
|
2152
|
-
// Create the register function right here. It closes over the necessary variables.
|
|
2153
|
-
const register = () => {
|
|
2154
|
-
const [, forceUpdate] = useState({});
|
|
2155
|
-
const itemComponentId = `${componentId}-${path.join(".")}-${originalIndex}`;
|
|
2156
|
-
|
|
2157
|
-
useLayoutEffect(() => {
|
|
2158
|
-
const fullComponentId = `${stateKey}////${itemComponentId}`;
|
|
2159
|
-
const stateEntry = getGlobalStore
|
|
2160
|
-
.getState()
|
|
2161
|
-
.stateComponents.get(stateKey) || {
|
|
2162
|
-
components: new Map(),
|
|
2163
|
-
};
|
|
2164
|
-
|
|
2165
|
-
stateEntry.components.set(fullComponentId, {
|
|
2166
|
-
forceUpdate: () => forceUpdate({}),
|
|
2167
|
-
paths: new Set([finalPath.join(".")]),
|
|
2168
|
-
});
|
|
2169
|
-
|
|
2170
|
-
getGlobalStore
|
|
2171
|
-
.getState()
|
|
2172
|
-
.stateComponents.set(stateKey, stateEntry);
|
|
2173
|
-
|
|
2174
|
-
return () => {
|
|
2175
|
-
const currentEntry = getGlobalStore
|
|
2176
|
-
.getState()
|
|
2177
|
-
.stateComponents.get(stateKey);
|
|
2178
|
-
if (currentEntry) {
|
|
2179
|
-
currentEntry.components.delete(fullComponentId);
|
|
2180
|
-
}
|
|
2181
|
-
};
|
|
2182
|
-
}, [stateKey, itemComponentId]);
|
|
2183
|
-
};
|
|
2184
|
-
|
|
2185
|
-
return callbackfn(item, setter, {
|
|
2186
|
-
register,
|
|
2187
|
-
index: localIndex,
|
|
2188
|
-
originalIndex,
|
|
2189
|
-
});
|
|
2190
|
-
});
|
|
2191
|
-
};
|
|
2192
|
-
}
|
|
2193
2066
|
if (prop === "stateMapNoRender") {
|
|
2194
2067
|
return (
|
|
2195
2068
|
callbackfn: (
|
|
@@ -3031,6 +2904,8 @@ export function $cogsSignalStore(proxy: {
|
|
|
3031
2904
|
);
|
|
3032
2905
|
return createElement("text", {}, String(value));
|
|
3033
2906
|
}
|
|
2907
|
+
|
|
2908
|
+
// Modified CogsItemWrapper that stores the DOM ref
|
|
3034
2909
|
function CogsItemWrapper({
|
|
3035
2910
|
stateKey,
|
|
3036
2911
|
itemComponentId,
|
|
@@ -3045,10 +2920,21 @@ function CogsItemWrapper({
|
|
|
3045
2920
|
// This hook handles the re-rendering when the item's own data changes.
|
|
3046
2921
|
const [, forceUpdate] = useState({});
|
|
3047
2922
|
// This hook measures the element.
|
|
3048
|
-
const [
|
|
2923
|
+
const [measureRef, bounds] = useMeasure();
|
|
2924
|
+
// Store the actual DOM element
|
|
2925
|
+
const elementRef = useRef<HTMLDivElement | null>(null);
|
|
3049
2926
|
// This ref prevents sending the same height update repeatedly.
|
|
3050
2927
|
const lastReportedHeight = useRef<number | null>(null);
|
|
3051
2928
|
|
|
2929
|
+
// Combine both refs
|
|
2930
|
+
const setRefs = useCallback(
|
|
2931
|
+
(element: HTMLDivElement | null) => {
|
|
2932
|
+
measureRef(element);
|
|
2933
|
+
elementRef.current = element;
|
|
2934
|
+
},
|
|
2935
|
+
[measureRef]
|
|
2936
|
+
);
|
|
2937
|
+
|
|
3052
2938
|
// This is the primary effect for this component.
|
|
3053
2939
|
useEffect(() => {
|
|
3054
2940
|
// We only report a height if it's a valid number AND it's different
|
|
@@ -3057,14 +2943,15 @@ function CogsItemWrapper({
|
|
|
3057
2943
|
// Store the new height so we don't report it again.
|
|
3058
2944
|
lastReportedHeight.current = bounds.height;
|
|
3059
2945
|
|
|
3060
|
-
// Call the store function to save the height
|
|
2946
|
+
// Call the store function to save the height AND the ref
|
|
3061
2947
|
getGlobalStore.getState().setShadowMetadata(stateKey, itemPath, {
|
|
3062
2948
|
virtualizer: {
|
|
3063
2949
|
itemHeight: bounds.height,
|
|
2950
|
+
domRef: elementRef.current, // Store the actual DOM element reference
|
|
3064
2951
|
},
|
|
3065
2952
|
});
|
|
3066
2953
|
}
|
|
3067
|
-
}, [bounds.height, stateKey, itemPath]); //
|
|
2954
|
+
}, [bounds.height, stateKey, itemPath]); // Removed ref.current as dependency
|
|
3068
2955
|
|
|
3069
2956
|
// This effect handles subscribing the item to its own data path for updates.
|
|
3070
2957
|
useLayoutEffect(() => {
|
|
@@ -3093,5 +2980,5 @@ function CogsItemWrapper({
|
|
|
3093
2980
|
}, [stateKey, itemComponentId, itemPath.join(".")]);
|
|
3094
2981
|
|
|
3095
2982
|
// The rendered output is a simple div that gets measured.
|
|
3096
|
-
return <div ref={
|
|
2983
|
+
return <div ref={setRefs}>{children}</div>;
|
|
3097
2984
|
}
|