cogsbox-state 0.5.410 → 0.5.412
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 +797 -826
- package/dist/CogsState.jsx.map +1 -1
- package/package.json +1 -1
- package/src/CogsState.tsx +118 -226
package/package.json
CHANGED
package/src/CogsState.tsx
CHANGED
|
@@ -1821,11 +1821,9 @@ function createProxyHandler<T>(
|
|
|
1821
1821
|
endIndex: 10,
|
|
1822
1822
|
});
|
|
1823
1823
|
const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
|
|
1824
|
-
const
|
|
1825
|
-
const
|
|
1826
|
-
const
|
|
1827
|
-
null
|
|
1828
|
-
);
|
|
1824
|
+
const wasAtBottomRef = useRef(false);
|
|
1825
|
+
const previousCountRef = useRef(0);
|
|
1826
|
+
const hasInitializedRef = useRef(false); // Track if we've done initial scroll
|
|
1829
1827
|
|
|
1830
1828
|
// Subscribe to shadow state updates
|
|
1831
1829
|
useEffect(() => {
|
|
@@ -1880,104 +1878,99 @@ function createProxyHandler<T>(
|
|
|
1880
1878
|
});
|
|
1881
1879
|
}, [range.startIndex, range.endIndex, sourceArray, totalCount]);
|
|
1882
1880
|
|
|
1883
|
-
//
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
if (
|
|
1891
|
-
|
|
1881
|
+
// Helper to scroll to last item using stored ref
|
|
1882
|
+
const scrollToLastItem = useCallback(() => {
|
|
1883
|
+
const shadowArray =
|
|
1884
|
+
getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
|
|
1885
|
+
[];
|
|
1886
|
+
const lastIndex = totalCount - 1;
|
|
1887
|
+
|
|
1888
|
+
if (lastIndex >= 0) {
|
|
1889
|
+
const lastItemData = shadowArray[lastIndex];
|
|
1890
|
+
if (lastItemData?.virtualizer?.domRef) {
|
|
1891
|
+
const element = lastItemData.virtualizer.domRef;
|
|
1892
|
+
if (element && element.scrollIntoView) {
|
|
1893
|
+
element.scrollIntoView({
|
|
1894
|
+
behavior: "auto",
|
|
1895
|
+
block: "end",
|
|
1896
|
+
inline: "nearest",
|
|
1897
|
+
});
|
|
1898
|
+
return true;
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1892
1901
|
}
|
|
1902
|
+
return false;
|
|
1903
|
+
}, [stateKey, path, totalCount]);
|
|
1893
1904
|
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
const isBigJump = totalCount > range.endIndex + jumpThreshold;
|
|
1905
|
+
// Handle ONLY new items - not height changes
|
|
1906
|
+
useEffect(() => {
|
|
1907
|
+
if (!stickToBottom || totalCount === 0) return;
|
|
1898
1908
|
|
|
1899
|
-
|
|
1900
|
-
// Set programmatic scroll flag BEFORE changing range
|
|
1901
|
-
isProgrammaticScrollRef.current = true;
|
|
1909
|
+
const hasNewItems = totalCount > previousCountRef.current;
|
|
1902
1910
|
|
|
1903
|
-
|
|
1904
|
-
|
|
1911
|
+
// Initial load
|
|
1912
|
+
if (
|
|
1913
|
+
previousCountRef.current === 0 &&
|
|
1914
|
+
totalCount > 0 &&
|
|
1915
|
+
!hasInitializedRef.current
|
|
1916
|
+
) {
|
|
1917
|
+
hasInitializedRef.current = true;
|
|
1918
|
+
const visibleCount = Math.ceil(
|
|
1919
|
+
(containerRef.current?.clientHeight || 600) / itemHeight
|
|
1920
|
+
);
|
|
1921
|
+
setRange({
|
|
1922
|
+
startIndex: Math.max(
|
|
1923
|
+
0,
|
|
1924
|
+
totalCount - visibleCount - overscan
|
|
1925
|
+
),
|
|
1905
1926
|
endIndex: totalCount,
|
|
1906
|
-
};
|
|
1907
|
-
setRange(newRange);
|
|
1927
|
+
});
|
|
1908
1928
|
|
|
1909
|
-
// Reset flag after a delay to ensure scroll events are ignored
|
|
1910
1929
|
setTimeout(() => {
|
|
1911
|
-
|
|
1930
|
+
if (containerRef.current) {
|
|
1931
|
+
containerRef.current.scrollTop =
|
|
1932
|
+
containerRef.current.scrollHeight;
|
|
1933
|
+
wasAtBottomRef.current = true;
|
|
1934
|
+
}
|
|
1912
1935
|
}, 100);
|
|
1913
1936
|
}
|
|
1937
|
+
// New items added AFTER initial load
|
|
1938
|
+
else if (hasNewItems && wasAtBottomRef.current) {
|
|
1939
|
+
const visibleCount = Math.ceil(
|
|
1940
|
+
(containerRef.current?.clientHeight || 600) / itemHeight
|
|
1941
|
+
);
|
|
1942
|
+
const newRange = {
|
|
1943
|
+
startIndex: Math.max(
|
|
1944
|
+
0,
|
|
1945
|
+
totalCount - visibleCount - overscan
|
|
1946
|
+
),
|
|
1947
|
+
endIndex: totalCount,
|
|
1948
|
+
};
|
|
1914
1949
|
|
|
1915
|
-
|
|
1916
|
-
let attempts = 0;
|
|
1917
|
-
const maxAttempts = 50; // 5 seconds max
|
|
1918
|
-
|
|
1919
|
-
scrollToBottomIntervalRef.current = setInterval(() => {
|
|
1920
|
-
const container = containerRef.current;
|
|
1921
|
-
if (!container) return;
|
|
1922
|
-
|
|
1923
|
-
attempts++;
|
|
1924
|
-
|
|
1925
|
-
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
1926
|
-
const currentBottom = scrollTop + clientHeight;
|
|
1927
|
-
const actualBottom = scrollHeight;
|
|
1928
|
-
const isAtBottom = actualBottom - currentBottom < 10; // Increased tolerance
|
|
1950
|
+
setRange(newRange);
|
|
1929
1951
|
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
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);
|
|
1952
|
+
setTimeout(() => {
|
|
1953
|
+
scrollToLastItem();
|
|
1954
|
+
}, 50);
|
|
1955
|
+
}
|
|
1944
1956
|
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
if (scrollToBottomIntervalRef.current) {
|
|
1948
|
-
clearInterval(scrollToBottomIntervalRef.current);
|
|
1949
|
-
scrollToBottomIntervalRef.current = null;
|
|
1950
|
-
}
|
|
1951
|
-
};
|
|
1952
|
-
}, [totalCount, stickToBottom, range.startIndex, range.endIndex]);
|
|
1957
|
+
previousCountRef.current = totalCount;
|
|
1958
|
+
}, [totalCount]); // ONLY depend on totalCount, not positions!
|
|
1953
1959
|
|
|
1954
|
-
// Handle
|
|
1960
|
+
// Handle scroll events
|
|
1955
1961
|
useEffect(() => {
|
|
1956
1962
|
const container = containerRef.current;
|
|
1957
1963
|
if (!container) return;
|
|
1958
1964
|
|
|
1959
1965
|
const handleScroll = () => {
|
|
1960
|
-
// Ignore programmatic scrolls
|
|
1961
|
-
if (isProgrammaticScrollRef.current) {
|
|
1962
|
-
return;
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
// This is a real user scroll
|
|
1966
1966
|
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
1967
1967
|
const distanceFromBottom =
|
|
1968
1968
|
scrollHeight - scrollTop - clientHeight;
|
|
1969
|
-
const isAtBottom = distanceFromBottom < 50; // Increased tolerance
|
|
1970
|
-
|
|
1971
|
-
// Stop any auto-scrolling if user scrolls
|
|
1972
|
-
if (scrollToBottomIntervalRef.current) {
|
|
1973
|
-
clearInterval(scrollToBottomIntervalRef.current);
|
|
1974
|
-
scrollToBottomIntervalRef.current = null;
|
|
1975
|
-
}
|
|
1976
1969
|
|
|
1977
|
-
//
|
|
1978
|
-
|
|
1970
|
+
// Track if we're at bottom
|
|
1971
|
+
wasAtBottomRef.current = distanceFromBottom < 100;
|
|
1979
1972
|
|
|
1980
|
-
// Update visible range
|
|
1973
|
+
// Update visible range based on scroll position
|
|
1981
1974
|
let startIndex = 0;
|
|
1982
1975
|
for (let i = 0; i < positions.length; i++) {
|
|
1983
1976
|
if (positions[i]! > scrollTop - itemHeight * overscan) {
|
|
@@ -2004,42 +1997,49 @@ function createProxyHandler<T>(
|
|
|
2004
1997
|
container.addEventListener("scroll", handleScroll, {
|
|
2005
1998
|
passive: true,
|
|
2006
1999
|
});
|
|
2007
|
-
|
|
2000
|
+
|
|
2001
|
+
// Just calculate visible range, no scrolling
|
|
2002
|
+
handleScroll();
|
|
2008
2003
|
|
|
2009
2004
|
return () => {
|
|
2010
2005
|
container.removeEventListener("scroll", handleScroll);
|
|
2011
2006
|
};
|
|
2012
2007
|
}, [positions, totalCount, itemHeight, overscan]);
|
|
2013
2008
|
|
|
2014
|
-
const scrollToBottom = useCallback(
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
containerRef.current.
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
setTimeout(() => {
|
|
2023
|
-
isProgrammaticScrollRef.current = false;
|
|
2024
|
-
}, 100);
|
|
2025
|
-
},
|
|
2026
|
-
[]
|
|
2027
|
-
);
|
|
2009
|
+
const scrollToBottom = useCallback(() => {
|
|
2010
|
+
wasAtBottomRef.current = true;
|
|
2011
|
+
const scrolled = scrollToLastItem();
|
|
2012
|
+
if (!scrolled && containerRef.current) {
|
|
2013
|
+
containerRef.current.scrollTop =
|
|
2014
|
+
containerRef.current.scrollHeight;
|
|
2015
|
+
}
|
|
2016
|
+
}, [scrollToLastItem]);
|
|
2028
2017
|
|
|
2029
2018
|
const scrollToIndex = useCallback(
|
|
2030
2019
|
(index: number, behavior: ScrollBehavior = "smooth") => {
|
|
2031
|
-
|
|
2020
|
+
const shadowArray =
|
|
2021
|
+
getGlobalStore
|
|
2022
|
+
.getState()
|
|
2023
|
+
.getShadowMetadata(stateKey, path) || [];
|
|
2024
|
+
const itemData = shadowArray[index];
|
|
2025
|
+
|
|
2026
|
+
if (itemData?.virtualizer?.domRef) {
|
|
2027
|
+
const element = itemData.virtualizer.domRef;
|
|
2028
|
+
if (element && element.scrollIntoView) {
|
|
2029
|
+
element.scrollIntoView({ behavior, block: "center" });
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// Fallback to position-based scrolling
|
|
2032
2035
|
if (containerRef.current && positions[index] !== undefined) {
|
|
2033
2036
|
containerRef.current.scrollTo({
|
|
2034
2037
|
top: positions[index],
|
|
2035
2038
|
behavior,
|
|
2036
2039
|
});
|
|
2037
2040
|
}
|
|
2038
|
-
setTimeout(() => {
|
|
2039
|
-
isProgrammaticScrollRef.current = false;
|
|
2040
|
-
}, 100);
|
|
2041
2041
|
},
|
|
2042
|
-
[positions]
|
|
2042
|
+
[positions, stateKey, path]
|
|
2043
2043
|
);
|
|
2044
2044
|
|
|
2045
2045
|
const virtualizerProps = {
|
|
@@ -2068,128 +2068,6 @@ function createProxyHandler<T>(
|
|
|
2068
2068
|
};
|
|
2069
2069
|
};
|
|
2070
2070
|
}
|
|
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
2071
|
if (prop === "stateMapNoRender") {
|
|
2194
2072
|
return (
|
|
2195
2073
|
callbackfn: (
|
|
@@ -3031,6 +2909,8 @@ export function $cogsSignalStore(proxy: {
|
|
|
3031
2909
|
);
|
|
3032
2910
|
return createElement("text", {}, String(value));
|
|
3033
2911
|
}
|
|
2912
|
+
|
|
2913
|
+
// Modified CogsItemWrapper that stores the DOM ref
|
|
3034
2914
|
function CogsItemWrapper({
|
|
3035
2915
|
stateKey,
|
|
3036
2916
|
itemComponentId,
|
|
@@ -3045,10 +2925,21 @@ function CogsItemWrapper({
|
|
|
3045
2925
|
// This hook handles the re-rendering when the item's own data changes.
|
|
3046
2926
|
const [, forceUpdate] = useState({});
|
|
3047
2927
|
// This hook measures the element.
|
|
3048
|
-
const [
|
|
2928
|
+
const [measureRef, bounds] = useMeasure();
|
|
2929
|
+
// Store the actual DOM element
|
|
2930
|
+
const elementRef = useRef<HTMLDivElement | null>(null);
|
|
3049
2931
|
// This ref prevents sending the same height update repeatedly.
|
|
3050
2932
|
const lastReportedHeight = useRef<number | null>(null);
|
|
3051
2933
|
|
|
2934
|
+
// Combine both refs
|
|
2935
|
+
const setRefs = useCallback(
|
|
2936
|
+
(element: HTMLDivElement | null) => {
|
|
2937
|
+
measureRef(element);
|
|
2938
|
+
elementRef.current = element;
|
|
2939
|
+
},
|
|
2940
|
+
[measureRef]
|
|
2941
|
+
);
|
|
2942
|
+
|
|
3052
2943
|
// This is the primary effect for this component.
|
|
3053
2944
|
useEffect(() => {
|
|
3054
2945
|
// We only report a height if it's a valid number AND it's different
|
|
@@ -3057,14 +2948,15 @@ function CogsItemWrapper({
|
|
|
3057
2948
|
// Store the new height so we don't report it again.
|
|
3058
2949
|
lastReportedHeight.current = bounds.height;
|
|
3059
2950
|
|
|
3060
|
-
// Call the store function to save the height
|
|
2951
|
+
// Call the store function to save the height AND the ref
|
|
3061
2952
|
getGlobalStore.getState().setShadowMetadata(stateKey, itemPath, {
|
|
3062
2953
|
virtualizer: {
|
|
3063
2954
|
itemHeight: bounds.height,
|
|
2955
|
+
domRef: elementRef.current, // Store the actual DOM element reference
|
|
3064
2956
|
},
|
|
3065
2957
|
});
|
|
3066
2958
|
}
|
|
3067
|
-
}, [bounds.height, stateKey, itemPath]); //
|
|
2959
|
+
}, [bounds.height, stateKey, itemPath]); // Removed ref.current as dependency
|
|
3068
2960
|
|
|
3069
2961
|
// This effect handles subscribing the item to its own data path for updates.
|
|
3070
2962
|
useLayoutEffect(() => {
|
|
@@ -3093,5 +2985,5 @@ function CogsItemWrapper({
|
|
|
3093
2985
|
}, [stateKey, itemComponentId, itemPath.join(".")]);
|
|
3094
2986
|
|
|
3095
2987
|
// The rendered output is a simple div that gets measured.
|
|
3096
|
-
return <div ref={
|
|
2988
|
+
return <div ref={setRefs}>{children}</div>;
|
|
3097
2989
|
}
|