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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogsbox-state",
3
- "version": "0.5.430",
3
+ "version": "0.5.432",
4
4
  "description": "React state management library with form controls and server sync",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
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
- // This is still the correct way to trigger re-calculations when item heights change.
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
- const itemPathWithId = [...path, itemId];
1785
+ // Get metadata for the specific item using its full path
1794
1786
  const itemMeta = getGlobalStore
1795
1787
  .getState()
1796
- .getShadowMetadata(stateKey, itemPathWithId);
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 (This part of your original code looks fine)
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
- // --- START OF IMPROVEMENT ---
1821
- // We use `orderedIds` here as well to create a `validIds` list for the
1822
- // virtualized proxy. This ensures that any subsequent operations on `virtualState`
1823
- // (like `.index()` or `.getSelected()`) will have the correct context.
1824
- const slicedIds = orderedIds?.slice(start, end);
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, // Pass the sliced IDs as the new `validIds`
1821
+ validIds: slicedIds,
1835
1822
  });
1836
- // --- END OF IMPROVEMENT ---
1837
- }, [
1838
- range.startIndex,
1839
- range.endIndex,
1840
- sourceArray,
1841
- totalCount,
1842
- orderedIds,
1843
- ]);
1823
+ }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1844
1824
 
1845
- // --- All the following logic for scrolling and event handling is from your original code. ---
1846
- // It is preserved, but we will improve `scrollToIndex` to use the shadow refs.
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 (original logic)
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
- setTimeout(() => scrollToIndex(totalCount - 1, "smooth"), 50);
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, stickToBottom]);
1891
+ }, [totalCount, itemHeight, overscan]);
1861
1892
 
1862
- // Handle scroll events (original logic)
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
- if (distanceFromBottom > 100)
1905
+
1906
+ // If user scrolls away from bottom past threshold, set flag
1907
+ if (distanceFromBottom > 100) {
1873
1908
  userHasScrolledAwayRef.current = true;
1874
- if (distanceFromBottom < 5)
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
- return () =>
1975
+
1976
+ return () => {
1918
1977
  container.removeEventListener("scroll", handleScroll);
1919
- }, [positions, totalCount, itemHeight, overscan]);
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
- if (containerRef.current) {
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
- // --- START OF IMPROVEMENT ---
1936
- // Use the orderedId to reliably get the item's metadata and DOM ref.
1937
- const itemId = orderedIds?.[index];
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, itemPathWithId);
1943
- const element = itemMeta?.virtualizer?.domRef;
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
- // Fallback to position-based scrolling if the ref isn't available for any reason.
1954
- const top = positions[index];
1955
- if (top !== undefined) {
1956
- container.scrollTo({ top, behavior });
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, orderedIds] // Add `orderedIds` to dependency array
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: { height: `${totalHeight}px`, position: "relative" },
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: virtualizerProps as any,
2061
+ virtualizerProps,
1980
2062
  scrollToBottom,
1981
2063
  scrollToIndex,
1982
2064
  };