react-simple-virtualize 1.0.0 → 2.0.0
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/README.md +53 -18
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.modern.mjs +1 -1
- package/dist/index.modern.mjs.map +1 -1
- package/dist/index.module.js +1 -1
- package/dist/index.module.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/package.json +44 -38
- package/src/index.ts +223 -0
- package/tsconfig.json +24 -0
- package/src/index.js +0 -50
package/README.md
CHANGED
|
@@ -51,6 +51,7 @@ const MyLargeList = () => {
|
|
|
51
51
|
// Outer Container: Needs fixed height & overflow-y: auto
|
|
52
52
|
<div
|
|
53
53
|
onScroll={onScroll}
|
|
54
|
+
ref={scrollRef}
|
|
54
55
|
style={{
|
|
55
56
|
height: containerHeight,
|
|
56
57
|
overflowY: 'auto',
|
|
@@ -65,6 +66,7 @@ const MyLargeList = () => {
|
|
|
65
66
|
{virtualItems.map(({ index, offsetTop }) => (
|
|
66
67
|
<div
|
|
67
68
|
key={index}
|
|
69
|
+
ref={virtualItem.measureRef}
|
|
68
70
|
style={{
|
|
69
71
|
position: 'absolute',
|
|
70
72
|
top: 0,
|
|
@@ -91,31 +93,64 @@ export default MyLargeList;
|
|
|
91
93
|
|
|
92
94
|
## ⚙️ API Reference
|
|
93
95
|
|
|
94
|
-
|
|
96
|
+
| Prop | Type | Default | Description |
|
|
97
|
+
|---|---|---|---|
|
|
98
|
+
| `itemCount` | `number` | **Required** | Total number of items in the list. |
|
|
99
|
+
| `itemHeight` | `number` | **Required** | Estimated height of an item. Used for initial rendering and unmeasured items. |
|
|
100
|
+
| `overscan` | `number` | `3` | Number of extra items to render outside the visible viewport. |
|
|
101
|
+
| `containerHeight` | `number` | `undefined` | Optional fixed height. If omitted, the hook uses `ResizeObserver` on `scrollRef` to measure it automatically. |
|
|
95
102
|
|
|
96
|
-
|
|
97
|
-
| :--- | :--- | :--- | :--- |
|
|
98
|
-
| `itemCount` | `number` | Yes | Total number of items in the list. |
|
|
99
|
-
| `itemHeight` | `number` | Yes | Height of a single item in pixels. |
|
|
100
|
-
| `containerHeight` | `number` | Yes | Height of the visible scroll area (viewport). |
|
|
101
|
-
| `overscan` | `number` | No | Number of extra items to render above/below the visible area (Default: 3). |
|
|
102
|
-
|
|
103
|
-
### Returns
|
|
103
|
+
### Return Values
|
|
104
104
|
|
|
105
105
|
| Value | Type | Description |
|
|
106
|
-
|
|
107
|
-
| `virtualItems` | `
|
|
108
|
-
| `totalHeight` | `number` | Total height of the list (
|
|
109
|
-
| `
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `virtualItems` | `VirtualItem[]` | Array of items to be rendered in the current viewport. |
|
|
108
|
+
| `totalHeight` | `number` | Total calculated height of the list (apply this to the inner container). |
|
|
109
|
+
| `scrollRef` | `RefObject` | Ref to be attached to the scrollable container element (for auto-sizing). |
|
|
110
|
+
| `onScroll` | `function` | Scroll handler to be attached to the scrollable container. |
|
|
111
|
+
|
|
112
|
+
#### `VirtualItem` Interface
|
|
113
|
+
```ts
|
|
114
|
+
interface VirtualItem {
|
|
115
|
+
index: number;
|
|
116
|
+
offsetTop: number;
|
|
117
|
+
measureRef: (el: HTMLElement | null) => void; // Must be attached to the rendered element
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Migration from v1 to v2
|
|
122
|
+
|
|
123
|
+
v2 introduces support for **Dynamic Heights**. This is a breaking change.
|
|
124
|
+
|
|
125
|
+
**Changes:**
|
|
126
|
+
- You must now attach the `measureRef` to your rendered element.
|
|
127
|
+
- `itemHeight` prop is now used as an **estimate** for unrendered items.
|
|
128
|
+
|
|
129
|
+
```jsx
|
|
130
|
+
// v1 (Old)
|
|
131
|
+
<div style={{ transform: `translateY(${virtualItem.offsetTop}px)` }}>...</div>
|
|
132
|
+
|
|
133
|
+
// v2 (New)
|
|
134
|
+
<div
|
|
135
|
+
ref={virtualItem.measureRef} // <--- Required!
|
|
136
|
+
style={{ transform: `translateY(${virtualItem.offsetTop}px)` }}
|
|
137
|
+
>
|
|
138
|
+
...
|
|
139
|
+
</div>
|
|
140
|
+
```
|
|
110
141
|
|
|
111
142
|
## 🗺️ Roadmap & Support
|
|
112
143
|
|
|
113
|
-
|
|
114
|
-
|
|
144
|
+
Here's what's planned for the future of `react-simple-virtualize`.
|
|
145
|
+
|
|
146
|
+
- [x] **Fixed Height Lists:** Initial release with core virtualization logic. (v1.0)
|
|
147
|
+
- [x] **Dynamic Height Lists:** Support for items with variable heights using a measurement ref. (v2.0) 🚀
|
|
148
|
+
- [ ] **Horizontal Virtualization:** Support for row-based scrolling.
|
|
149
|
+
- [ ] **Grid Virtualization:** Virtualize both rows and columns (spreadsheet style).
|
|
150
|
+
- [ ] **Window Scrolling:** Support for using the browser's native scrollbar instead of a container.
|
|
151
|
+
- [ ] **Infinite Loader:** Built-in support for loading more data as you scroll (Pagination).
|
|
115
152
|
|
|
116
|
-
|
|
117
|
-
- [ ] **Grid Virtualization:** Virtualizing both rows and columns.
|
|
118
|
-
- [ ] **Horizontal Scrolling:** Support for X-axis virtualization.
|
|
153
|
+
> **Note:** If you have a specific feature request, feel free to open an issue!
|
|
119
154
|
|
|
120
155
|
### ☕ Support the Development
|
|
121
156
|
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { UIEvent } from "react";
|
|
2
|
+
export interface UseVirtualizeProps {
|
|
3
|
+
/** Total number of items in the list */
|
|
4
|
+
itemCount: number;
|
|
5
|
+
/** Estimated height of a single item (used for unmeasured items) */
|
|
6
|
+
itemHeight: number;
|
|
7
|
+
/** Number of extra items to render outside the visible view (default: 3) */
|
|
8
|
+
overscan?: number;
|
|
9
|
+
/** Fixed container height. If not provided, it will be measured automatically via ResizeObserver */
|
|
10
|
+
containerHeight?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface VirtualItem {
|
|
13
|
+
/** The index of the item in the original data array */
|
|
14
|
+
index: number;
|
|
15
|
+
/** The calculated top position (px) for absolute positioning */
|
|
16
|
+
offsetTop: number;
|
|
17
|
+
/** Callback ref to measure the actual DOM height of the item */
|
|
18
|
+
measureRef: (el: HTMLElement | null) => void;
|
|
19
|
+
}
|
|
20
|
+
export declare function useVirtualize({ itemCount, itemHeight: estimatedItemHeight, overscan, containerHeight: propsContainerHeight, }: UseVirtualizeProps): {
|
|
21
|
+
virtualItems: VirtualItem[];
|
|
22
|
+
totalHeight: number;
|
|
23
|
+
scrollRef: import("react").RefObject<HTMLDivElement | null>;
|
|
24
|
+
measurementVersion: number;
|
|
25
|
+
onScroll: (e: UIEvent<HTMLElement>) => void;
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,OAAO,EACR,MAAM,OAAO,CAAC;AAEf,MAAM,WAAW,kBAAkB;IACjC,wCAAwC;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,UAAU,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oGAAoG;IACpG,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,UAAU,EAAE,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;CAC9C;AAED,wBAAgB,aAAa,CAAC,EAC5B,SAAS,EACT,UAAU,EAAE,mBAAmB,EAC/B,QAAY,EACZ,eAAe,EAAE,oBAAoB,GACtC,EAAE,kBAAkB;;;;;kBAyLH,QAAQ,WAAW,CAAC;EAGrC"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var
|
|
1
|
+
var e=require("react");exports.useVirtualize=function(r){var t=r.itemCount,n=r.itemHeight,u=r.overscan,c=void 0===u?3:u,i=r.containerHeight,a=e.useState(0),o=a[0],f=a[1],s=e.useState(0),l=s[0],v=s[1],h=null!=i?i:l,R=e.useRef({}),m=e.useRef({}),g=e.useRef(-1),b=e.useRef(null),d=e.useRef(0),C=e.useRef(0),M=e.useState(0),k=M[0],p=M[1],x=e.useCallback(function(){return C.current>0?d.current/C.current:n},[n]);e.useLayoutEffect(function(){if(void 0===i){var e=b.current;if(e){var r=new ResizeObserver(function(e){var r=e[0];r.contentRect.height!==l&&v(r.contentRect.height)});return r.observe(e),function(){return r.disconnect()}}}},[i,l]);var S=e.useCallback(function(e){if(e<=g.current)return m.current[e]||0;var r=g.current;return(r>=0?m.current[r]:0)+(r>=0?R.current[r]:0)+(e-r-1)*x()},[n,x]),H=e.useCallback(function(e){for(var r=0,n=t-1;r<=n;){var u=Math.floor((r+n)/2),c=S(u),i=R.current[u]||x();if(c<=e&&c+i>e)return u;c<e?r=u+1:n=u-1}return Math.max(0,r-1)},[S,t,n]),T=e.useCallback(function(e){return function(r){if(r){var t=r.getBoundingClientRect().height,n=R.current[e];if(n!==t){if(void 0===n?(d.current+=t,C.current+=1):d.current+=t-n,R.current[e]=t,e>g.current){var u=S(e);m.current[e]=u,g.current=e}else g.current=Math.min(g.current,e-1);p(function(e){return e+1})}}}},[S]),z=H(o),V=Math.min(t-1,H(o+h)+c);return{virtualItems:e.useMemo(function(){for(var e=[],r=Math.max(0,z-c);r<=V;r++)e.push({index:r,offsetTop:S(r),measureRef:T(r)});return e},[z,V,S,T,c]),totalHeight:S(t),scrollRef:b,measurementVersion:k,onScroll:function(e){return f(e.currentTarget.scrollTop)}}};
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/index.js"],"sourcesContent":["import { useState, useMemo } from 'react';\n\nexport const useVirtualize = ({\n itemCount,\n itemHeight,\n containerHeight,\n overscan = 3\n}) => {\n // Store the current scroll position\n const [scrollTop, setScrollTop] = useState(0);\n\n // Update scroll position on scroll event\n const onScroll = (e) => {\n setScrollTop(e.currentTarget.scrollTop);\n };\n\n // Memoize calculations to prevent unnecessary re-renders\n const { virtualItems, totalHeight } = useMemo(() => {\n // 1. Calculate the visible range based on scroll position\n const rangeStart = Math.floor(scrollTop / itemHeight);\n const rangeEnd = Math.min(\n itemCount - 1,\n Math.floor((scrollTop + containerHeight) / itemHeight)\n );\n\n // 2. Add overscan (buffer) to the range for smoother scrolling\n const startIndex = Math.max(0, rangeStart - overscan);\n const endIndex = Math.min(itemCount - 1, rangeEnd + overscan);\n\n // 3. Generate the array of items to be rendered\n const virtualItems = [];\n for (let i = startIndex; i <= endIndex; i++) {\n virtualItems.push({\n index: i,\n offsetTop: i * itemHeight, // Calculate absolute position for each item\n });\n }\n\n // 4. Calculate total phantom height to maintain scrollbar size\n const totalHeight = itemCount * itemHeight;\n\n return { virtualItems, totalHeight };\n }, [scrollTop, itemCount, itemHeight, containerHeight, overscan]);\n\n return {\n virtualItems,\n totalHeight,\n onScroll,\n };\n};"],"names":["_ref","itemCount","itemHeight","containerHeight","_ref$overscan","overscan","_useState","useState","scrollTop","setScrollTop","_useMemo","useMemo","rangeStart","Math","floor","rangeEnd","min","startIndex","max","endIndex","virtualItems","i","push","index","offsetTop","totalHeight","onScroll","e","currentTarget"],"mappings":"6CAE6B,SAAHA,GAKpB,IAJJC,EAASD,EAATC,UACAC,EAAUF,EAAVE,WACAC,EAAeH,EAAfG,gBAAeC,EAAAJ,EACfK,SAAAA,OAAQ,IAAAD,EAAG,EAACA,EAGZE,EAAkCC,EAAQA,SAAC,GAApCC,EAASF,EAAEG,GAAAA,EAAYH,EAG9B,GAKAI,EAAsCC,EAAAA,QAAQ,WAc5C,IAZA,IAAMC,EAAaC,KAAKC,MAAMN,EAAYN,GACpCa,EAAWF,KAAKG,IACpBf,EAAY,EACZY,KAAKC,OAAON,EAAYL,GAAmBD,IAIvCe,EAAaJ,KAAKK,IAAI,EAAGN,EAAaP,GACtCc,EAAWN,KAAKG,IAAIf,EAAY,EAAGc,EAAWV,GAG9Ce,EAAe,GACZC,EAAIJ,EAAYI,GAAKF,EAAUE,IACtCD,EAAaE,KAAK,CAChBC,MAAOF,EACPG,UAAWH,EAAInB,IAOnB,MAAO,CAAEkB,aAAAA,EAAcK,YAFHxB,EAAYC,EAGlC,EAAG,CAACM,EAAWP,EAAWC,EAAYC,EAAiBE,IAEvD,MAAO,CACLe,aA5BkBV,EAAZU,aA6BNK,YA7B+Bf,EAAXe,YA8BpBC,SAnCe,SAACC,GAChBlB,EAAakB,EAAEC,cAAcpB,UAC/B,EAmCF"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import {\n useState,\n useRef,\n useMemo,\n useCallback,\n useLayoutEffect,\n UIEvent,\n} from \"react\";\n\nexport interface UseVirtualizeProps {\n /** Total number of items in the list */\n itemCount: number;\n /** Estimated height of a single item (used for unmeasured items) */\n itemHeight: number;\n /** Number of extra items to render outside the visible view (default: 3) */\n overscan?: number;\n /** Fixed container height. If not provided, it will be measured automatically via ResizeObserver */\n containerHeight?: number;\n}\n\nexport interface VirtualItem {\n /** The index of the item in the original data array */\n index: number;\n /** The calculated top position (px) for absolute positioning */\n offsetTop: number;\n /** Callback ref to measure the actual DOM height of the item */\n measureRef: (el: HTMLElement | null) => void;\n}\n\nexport function useVirtualize({\n itemCount,\n itemHeight: estimatedItemHeight,\n overscan = 3,\n containerHeight: propsContainerHeight,\n}: UseVirtualizeProps) {\n const [scrollTop, setScrollTop] = useState(0);\n const [observedHeight, setObservedHeight] = useState(0);\n\n // Determine the effective container height:\n // Use the prop if provided; otherwise, fall back to the measured height.\n const height = propsContainerHeight ?? observedHeight;\n\n // --- REFS (State that doesn't trigger re-renders) ---\n\n // Stores the exact height of each measured item: { index: height }\n const measurementCache = useRef({} as Record<number, number>);\n\n // Stores the calculated total offset for each index: { index: offset }\n // This acts as a Prefix Sum array to avoid recalculating from 0 every time.\n const offsetCache = useRef({} as Record<number, number>);\n\n // Tracks the last index that has a valid calculated offset in the cache.\n const lastMeasuredIndex = useRef(-1);\n\n // Reference to the scrollable container (used for ResizeObserver)\n const scrollRef = useRef<HTMLDivElement>(null);\n\n // Total measured height (Pixel)\n const totalMeasuredSize = useRef(0);\n // Total measured element?\n const totalMeasuredCount = useRef(0);\n\n const [measurementVersion, setMeasurementVersion] = useState(0);\n\n // --- HELPER: GET ESTIMATED HEIGHT ---\n // Zeka burada: Eğer hiç ölçüm yoksa senin verdiğin 'itemHeight'ı kullan.\n // Ama ölçüm yaptıysak, gerçek ortalamayı kullan.\n const getEstimatedItemHeight = useCallback(() => {\n return totalMeasuredCount.current > 0\n ? totalMeasuredSize.current / totalMeasuredCount.current\n : estimatedItemHeight;\n }, [estimatedItemHeight]);\n\n // --- 1. AUTO-SIZER LOGIC ---\n useLayoutEffect(() => {\n // If the user provided a fixed height, skip observation to save resources.\n if (propsContainerHeight !== undefined) return;\n\n const scrollElement = scrollRef.current;\n if (!scrollElement) return;\n\n const observer = new ResizeObserver(([entry]) => {\n // Update state only if dimensions actually changed\n if (entry.contentRect.height !== observedHeight) {\n setObservedHeight(entry.contentRect.height);\n }\n });\n\n observer.observe(scrollElement);\n\n return () => observer.disconnect();\n }, [propsContainerHeight, observedHeight]);\n\n // --- 2. OFFSET CALCULATOR (Core Logic) ---\n // Returns the vertical position (px) of an item.\n const getItemOffset = useCallback(\n (index: number) => {\n // If the offset for this index is already cached, return it directly.\n if (index <= lastMeasuredIndex.current) {\n return offsetCache.current[index] || 0;\n }\n\n // 1. Retrieve reference data from the last measured item.\n const lastIndex = lastMeasuredIndex.current;\n\n // If no items have been measured yet (-1), the starting offset is 0.\n // Otherwise, retrieve the offset of the last measured item.\n const lastOffset = lastIndex >= 0 ? offsetCache.current[lastIndex] : 0;\n\n // Retrieve the height of the last measured item (default to 0 if none measured).\n const lastHeight =\n lastIndex >= 0 ? measurementCache.current[lastIndex] : 0;\n\n // 2. Calculate the bottom position of the last measured item.\n // This serves as the anchor point for estimating subsequent offsets.\n const lastBottom = lastOffset + lastHeight;\n\n // 3. Calculate how many unmeasured items exist between the last measured index and the target index.\n const unmeasuredCount = index - lastIndex - 1;\n\n // 4. Result: Anchor Point + (Number of Unmeasured Items * Estimated Height)\n return lastBottom + unmeasuredCount * getEstimatedItemHeight();\n },\n [estimatedItemHeight, getEstimatedItemHeight]\n );\n\n // --- 3. BINARY SEARCH (Find Visible Index) ---\n // Finds the index corresponding to the current scroll position.\n // Performance: O(log N) - Crucial for large lists.\n const getStartIndexForOffset = useCallback(\n (offset: number) => {\n let low = 0;\n let high = itemCount - 1;\n\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n const currentOffset = getItemOffset(mid);\n const currentHeight =\n measurementCache.current[mid] || getEstimatedItemHeight();\n\n if (currentOffset <= offset && currentOffset + currentHeight > offset) {\n return mid;\n } else if (currentOffset < offset) {\n low = mid + 1;\n } else {\n high = mid - 1;\n }\n }\n\n // Fallback to 0 if not found\n return Math.max(0, low - 1);\n },\n [getItemOffset, itemCount, estimatedItemHeight]\n );\n\n // --- 4. MEASUREMENT CALLBACK (The \"Ref\" logic) ---\n // This function is passed to the consumer to attach to their DOM elements.\n const measureElement = useCallback(\n (index: number) => (el: HTMLElement | null) => {\n if (!el) return;\n\n const measuredHeight = el.getBoundingClientRect().height;\n const prevHeight = measurementCache.current[index];\n\n if (prevHeight !== measuredHeight) {\n if (prevHeight === undefined) {\n totalMeasuredSize.current += measuredHeight;\n totalMeasuredCount.current += 1;\n } else {\n totalMeasuredSize.current += measuredHeight - prevHeight;\n }\n\n measurementCache.current[index] = measuredHeight;\n\n if (index > lastMeasuredIndex.current) {\n const prevOffset = getItemOffset(index);\n offsetCache.current[index] = prevOffset;\n lastMeasuredIndex.current = index;\n } else {\n lastMeasuredIndex.current = Math.min(\n lastMeasuredIndex.current,\n index - 1\n );\n }\n\n setMeasurementVersion(v => v + 1);\n }\n },\n [getItemOffset]\n );\n\n // --- 5. RENDER RANGE CALCULATION ---\n const startIndex = getStartIndexForOffset(scrollTop);\n const endIndex = Math.min(\n itemCount - 1,\n getStartIndexForOffset(scrollTop + height) + overscan\n );\n\n // Generate the virtual items array to be rendered\n const virtualItems = useMemo(() => {\n const items: VirtualItem[] = [];\n for (let i = Math.max(0, startIndex - overscan); i <= endIndex; i++) {\n items.push({\n index: i,\n offsetTop: getItemOffset(i),\n measureRef: measureElement(i),\n });\n }\n return items;\n }, [startIndex, endIndex, getItemOffset, measureElement, overscan]);\n\n // Calculate total list height for the scrollbar\n const totalHeight = getItemOffset(itemCount);\n\n return {\n virtualItems,\n totalHeight,\n scrollRef,\n measurementVersion,\n onScroll: (e: UIEvent<HTMLElement>) =>\n setScrollTop(e.currentTarget.scrollTop),\n };\n}\n"],"names":["_ref","itemCount","estimatedItemHeight","itemHeight","_ref$overscan","overscan","propsContainerHeight","containerHeight","_useState","useState","scrollTop","setScrollTop","_useState2","observedHeight","setObservedHeight","height","measurementCache","useRef","offsetCache","lastMeasuredIndex","scrollRef","totalMeasuredSize","totalMeasuredCount","_useState3","measurementVersion","setMeasurementVersion","getEstimatedItemHeight","useCallback","current","useLayoutEffect","undefined","scrollElement","observer","ResizeObserver","_ref2","entry","contentRect","observe","disconnect","getItemOffset","index","lastIndex","getStartIndexForOffset","offset","low","high","mid","Math","floor","currentOffset","currentHeight","max","measureElement","el","measuredHeight","getBoundingClientRect","prevHeight","prevOffset","min","v","startIndex","endIndex","virtualItems","useMemo","items","i","push","offsetTop","measureRef","totalHeight","onScroll","e","currentTarget"],"mappings":"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"}
|
package/dist/index.modern.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{useState as t,useMemo as
|
|
1
|
+
import{useState as r,useRef as t,useCallback as e,useLayoutEffect as n,useMemo as c}from"react";function u({itemCount:u,itemHeight:o,overscan:i=3,containerHeight:s}){const[l,f]=r(0),[a,h]=r(0),m=null!=s?s:a,g=t({}),v=t({}),R=t(-1),d=t(null),p=t(0),M=t(0),[x,H]=r(0),T=e(()=>M.current>0?p.current/M.current:o,[o]);n(()=>{if(void 0!==s)return;const r=d.current;if(!r)return;const t=new ResizeObserver(([r])=>{r.contentRect.height!==a&&h(r.contentRect.height)});return t.observe(r),()=>t.disconnect()},[s,a]);const b=e(r=>{if(r<=R.current)return v.current[r]||0;const t=R.current;return(t>=0?v.current[t]:0)+(t>=0?g.current[t]:0)+(r-t-1)*T()},[o,T]),C=e(r=>{let t=0,e=u-1;for(;t<=e;){const n=Math.floor((t+e)/2),c=b(n),u=g.current[n]||T();if(c<=r&&c+u>r)return n;c<r?t=n+1:e=n-1}return Math.max(0,t-1)},[b,u,o]),w=e(r=>t=>{if(!t)return;const e=t.getBoundingClientRect().height,n=g.current[r];if(n!==e){if(void 0===n?(p.current+=e,M.current+=1):p.current+=e-n,g.current[r]=e,r>R.current){const t=b(r);v.current[r]=t,R.current=r}else R.current=Math.min(R.current,r-1);H(r=>r+1)}},[b]),z=C(l),B=Math.min(u-1,C(l+m)+i);return{virtualItems:c(()=>{const r=[];for(let t=Math.max(0,z-i);t<=B;t++)r.push({index:t,offsetTop:b(t),measureRef:w(t)});return r},[z,B,b,w,i]),totalHeight:b(u),scrollRef:d,measurementVersion:x,onScroll:r=>f(r.currentTarget.scrollTop)}}export{u as useVirtualize};
|
|
2
2
|
//# sourceMappingURL=index.modern.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.modern.mjs","sources":["../src/index.js"],"sourcesContent":["import { useState, useMemo } from 'react';\n\nexport const useVirtualize = ({\n itemCount,\n itemHeight,\n containerHeight,\n overscan = 3\n}) => {\n // Store the current scroll position\n const [scrollTop, setScrollTop] = useState(0);\n\n // Update scroll position on scroll event\n const onScroll = (e) => {\n setScrollTop(e.currentTarget.scrollTop);\n };\n\n // Memoize calculations to prevent unnecessary re-renders\n const { virtualItems, totalHeight } = useMemo(() => {\n // 1. Calculate the visible range based on scroll position\n const rangeStart = Math.floor(scrollTop / itemHeight);\n const rangeEnd = Math.min(\n itemCount - 1,\n Math.floor((scrollTop + containerHeight) / itemHeight)\n );\n\n // 2. Add overscan (buffer) to the range for smoother scrolling\n const startIndex = Math.max(0, rangeStart - overscan);\n const endIndex = Math.min(itemCount - 1, rangeEnd + overscan);\n\n // 3. Generate the array of items to be rendered\n const virtualItems = [];\n for (let i = startIndex; i <= endIndex; i++) {\n virtualItems.push({\n index: i,\n offsetTop: i * itemHeight, // Calculate absolute position for each item\n });\n }\n\n // 4. Calculate total phantom height to maintain scrollbar size\n const totalHeight = itemCount * itemHeight;\n\n return { virtualItems, totalHeight };\n }, [scrollTop, itemCount, itemHeight, containerHeight, overscan]);\n\n return {\n virtualItems,\n totalHeight,\n onScroll,\n };\n};"],"names":["useVirtualize","itemCount","itemHeight","containerHeight","overscan","scrollTop","setScrollTop","useState","virtualItems","totalHeight","useMemo","rangeStart","Math","floor","rangeEnd","min","startIndex","max","endIndex","i","push","index","offsetTop","onScroll","e","currentTarget"],"mappings":"8CAEa,MAAAA,EAAgBA,EAC3BC,YACAC,aACAC,kBACAC,SAAAA,EAAW,MAGX,MAAOC,EAAWC,GAAgBC,EAAS,IAQrCC,aAAEA,EAAYC,YAAEA,GAAgBC,EAAQ,KAE5C,MAAMC,EAAaC,KAAKC,MAAMR,EAAYH,GACpCY,EAAWF,KAAKG,IACpBd,EAAY,EACZW,KAAKC,OAAOR,EAAYF,GAAmBD,IAIvCc,EAAaJ,KAAKK,IAAI,EAAGN,EAAaP,GACtCc,EAAWN,KAAKG,IAAId,EAAY,EAAGa,EAAWV,GAG9CI,EAAe,GACrB,IAAK,IAAIW,EAAIH,EAAYG,GAAKD,EAAUC,IACtCX,EAAaY,KAAK,CAChBC,MAAOF,EACPG,UAAWH,EAAIjB,IAOnB,MAAO,CAAEM,eAAcC,YAFHR,EAAYC,IAG/B,CAACG,EAAWJ,EAAWC,EAAYC,EAAiBC,IAEvD,MAAO,CACLI,eACAC,cACAc,SAnCgBC,IAChBlB,EAAakB,EAAEC,cAAcpB"}
|
|
1
|
+
{"version":3,"file":"index.modern.mjs","sources":["../src/index.ts"],"sourcesContent":["import {\n useState,\n useRef,\n useMemo,\n useCallback,\n useLayoutEffect,\n UIEvent,\n} from \"react\";\n\nexport interface UseVirtualizeProps {\n /** Total number of items in the list */\n itemCount: number;\n /** Estimated height of a single item (used for unmeasured items) */\n itemHeight: number;\n /** Number of extra items to render outside the visible view (default: 3) */\n overscan?: number;\n /** Fixed container height. If not provided, it will be measured automatically via ResizeObserver */\n containerHeight?: number;\n}\n\nexport interface VirtualItem {\n /** The index of the item in the original data array */\n index: number;\n /** The calculated top position (px) for absolute positioning */\n offsetTop: number;\n /** Callback ref to measure the actual DOM height of the item */\n measureRef: (el: HTMLElement | null) => void;\n}\n\nexport function useVirtualize({\n itemCount,\n itemHeight: estimatedItemHeight,\n overscan = 3,\n containerHeight: propsContainerHeight,\n}: UseVirtualizeProps) {\n const [scrollTop, setScrollTop] = useState(0);\n const [observedHeight, setObservedHeight] = useState(0);\n\n // Determine the effective container height:\n // Use the prop if provided; otherwise, fall back to the measured height.\n const height = propsContainerHeight ?? observedHeight;\n\n // --- REFS (State that doesn't trigger re-renders) ---\n\n // Stores the exact height of each measured item: { index: height }\n const measurementCache = useRef({} as Record<number, number>);\n\n // Stores the calculated total offset for each index: { index: offset }\n // This acts as a Prefix Sum array to avoid recalculating from 0 every time.\n const offsetCache = useRef({} as Record<number, number>);\n\n // Tracks the last index that has a valid calculated offset in the cache.\n const lastMeasuredIndex = useRef(-1);\n\n // Reference to the scrollable container (used for ResizeObserver)\n const scrollRef = useRef<HTMLDivElement>(null);\n\n // Total measured height (Pixel)\n const totalMeasuredSize = useRef(0);\n // Total measured element?\n const totalMeasuredCount = useRef(0);\n\n const [measurementVersion, setMeasurementVersion] = useState(0);\n\n // --- HELPER: GET ESTIMATED HEIGHT ---\n // Zeka burada: Eğer hiç ölçüm yoksa senin verdiğin 'itemHeight'ı kullan.\n // Ama ölçüm yaptıysak, gerçek ortalamayı kullan.\n const getEstimatedItemHeight = useCallback(() => {\n return totalMeasuredCount.current > 0\n ? totalMeasuredSize.current / totalMeasuredCount.current\n : estimatedItemHeight;\n }, [estimatedItemHeight]);\n\n // --- 1. AUTO-SIZER LOGIC ---\n useLayoutEffect(() => {\n // If the user provided a fixed height, skip observation to save resources.\n if (propsContainerHeight !== undefined) return;\n\n const scrollElement = scrollRef.current;\n if (!scrollElement) return;\n\n const observer = new ResizeObserver(([entry]) => {\n // Update state only if dimensions actually changed\n if (entry.contentRect.height !== observedHeight) {\n setObservedHeight(entry.contentRect.height);\n }\n });\n\n observer.observe(scrollElement);\n\n return () => observer.disconnect();\n }, [propsContainerHeight, observedHeight]);\n\n // --- 2. OFFSET CALCULATOR (Core Logic) ---\n // Returns the vertical position (px) of an item.\n const getItemOffset = useCallback(\n (index: number) => {\n // If the offset for this index is already cached, return it directly.\n if (index <= lastMeasuredIndex.current) {\n return offsetCache.current[index] || 0;\n }\n\n // 1. Retrieve reference data from the last measured item.\n const lastIndex = lastMeasuredIndex.current;\n\n // If no items have been measured yet (-1), the starting offset is 0.\n // Otherwise, retrieve the offset of the last measured item.\n const lastOffset = lastIndex >= 0 ? offsetCache.current[lastIndex] : 0;\n\n // Retrieve the height of the last measured item (default to 0 if none measured).\n const lastHeight =\n lastIndex >= 0 ? measurementCache.current[lastIndex] : 0;\n\n // 2. Calculate the bottom position of the last measured item.\n // This serves as the anchor point for estimating subsequent offsets.\n const lastBottom = lastOffset + lastHeight;\n\n // 3. Calculate how many unmeasured items exist between the last measured index and the target index.\n const unmeasuredCount = index - lastIndex - 1;\n\n // 4. Result: Anchor Point + (Number of Unmeasured Items * Estimated Height)\n return lastBottom + unmeasuredCount * getEstimatedItemHeight();\n },\n [estimatedItemHeight, getEstimatedItemHeight]\n );\n\n // --- 3. BINARY SEARCH (Find Visible Index) ---\n // Finds the index corresponding to the current scroll position.\n // Performance: O(log N) - Crucial for large lists.\n const getStartIndexForOffset = useCallback(\n (offset: number) => {\n let low = 0;\n let high = itemCount - 1;\n\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n const currentOffset = getItemOffset(mid);\n const currentHeight =\n measurementCache.current[mid] || getEstimatedItemHeight();\n\n if (currentOffset <= offset && currentOffset + currentHeight > offset) {\n return mid;\n } else if (currentOffset < offset) {\n low = mid + 1;\n } else {\n high = mid - 1;\n }\n }\n\n // Fallback to 0 if not found\n return Math.max(0, low - 1);\n },\n [getItemOffset, itemCount, estimatedItemHeight]\n );\n\n // --- 4. MEASUREMENT CALLBACK (The \"Ref\" logic) ---\n // This function is passed to the consumer to attach to their DOM elements.\n const measureElement = useCallback(\n (index: number) => (el: HTMLElement | null) => {\n if (!el) return;\n\n const measuredHeight = el.getBoundingClientRect().height;\n const prevHeight = measurementCache.current[index];\n\n if (prevHeight !== measuredHeight) {\n if (prevHeight === undefined) {\n totalMeasuredSize.current += measuredHeight;\n totalMeasuredCount.current += 1;\n } else {\n totalMeasuredSize.current += measuredHeight - prevHeight;\n }\n\n measurementCache.current[index] = measuredHeight;\n\n if (index > lastMeasuredIndex.current) {\n const prevOffset = getItemOffset(index);\n offsetCache.current[index] = prevOffset;\n lastMeasuredIndex.current = index;\n } else {\n lastMeasuredIndex.current = Math.min(\n lastMeasuredIndex.current,\n index - 1\n );\n }\n\n setMeasurementVersion(v => v + 1);\n }\n },\n [getItemOffset]\n );\n\n // --- 5. RENDER RANGE CALCULATION ---\n const startIndex = getStartIndexForOffset(scrollTop);\n const endIndex = Math.min(\n itemCount - 1,\n getStartIndexForOffset(scrollTop + height) + overscan\n );\n\n // Generate the virtual items array to be rendered\n const virtualItems = useMemo(() => {\n const items: VirtualItem[] = [];\n for (let i = Math.max(0, startIndex - overscan); i <= endIndex; i++) {\n items.push({\n index: i,\n offsetTop: getItemOffset(i),\n measureRef: measureElement(i),\n });\n }\n return items;\n }, [startIndex, endIndex, getItemOffset, measureElement, overscan]);\n\n // Calculate total list height for the scrollbar\n const totalHeight = getItemOffset(itemCount);\n\n return {\n virtualItems,\n totalHeight,\n scrollRef,\n measurementVersion,\n onScroll: (e: UIEvent<HTMLElement>) =>\n setScrollTop(e.currentTarget.scrollTop),\n };\n}\n"],"names":["useVirtualize","itemCount","itemHeight","estimatedItemHeight","overscan","containerHeight","propsContainerHeight","scrollTop","setScrollTop","useState","observedHeight","setObservedHeight","height","measurementCache","useRef","offsetCache","lastMeasuredIndex","scrollRef","totalMeasuredSize","totalMeasuredCount","measurementVersion","setMeasurementVersion","getEstimatedItemHeight","useCallback","current","useLayoutEffect","undefined","scrollElement","observer","ResizeObserver","entry","contentRect","observe","disconnect","getItemOffset","index","lastIndex","getStartIndexForOffset","offset","low","high","mid","Math","floor","currentOffset","currentHeight","max","measureElement","el","measuredHeight","getBoundingClientRect","prevHeight","prevOffset","min","v","startIndex","endIndex","virtualItems","useMemo","items","i","push","offsetTop","measureRef","totalHeight","onScroll","e","currentTarget"],"mappings":"yGA6BgBA,GAAcC,UAC5BA,EACAC,WAAYC,EAAmBC,SAC/BA,EAAW,EACXC,gBAAiBC,IAEjB,MAAOC,EAAWC,GAAgBC,EAAS,IACpCC,EAAgBC,GAAqBF,EAAS,GAI/CG,EAA6B,MAApBN,EAAAA,EAAwBI,EAKjCG,EAAmBC,EAAO,IAI1BC,EAAcD,EAAO,IAGrBE,EAAoBF,GAAQ,GAG5BG,EAAYH,EAAuB,MAGnCI,EAAoBJ,EAAO,GAE3BK,EAAqBL,EAAO,IAE3BM,EAAoBC,GAAyBZ,EAAS,GAKvDa,EAAyBC,EAAY,IAClCJ,EAAmBK,QAAU,EAChCN,EAAkBM,QAAUL,EAAmBK,QAC/CrB,EACH,CAACA,IAGJsB,EAAgB,KAEd,QAA6BC,IAAzBpB,EAAoC,OAExC,MAAMqB,EAAgBV,EAAUO,QAChC,IAAKG,EAAe,OAEpB,MAAMC,EAAW,IAAIC,eAAe,EAAEC,MAEhCA,EAAMC,YAAYnB,SAAWF,GAC/BC,EAAkBmB,EAAMC,YAAYnB,UAMxC,OAFAgB,EAASI,QAAQL,GAEV,IAAMC,EAASK,cACrB,CAAC3B,EAAsBI,IAI1B,MAAMwB,EAAgBX,EACnBY,IAEC,GAAIA,GAASnB,EAAkBQ,QAC7B,OAAOT,EAAYS,QAAQW,IAAU,EAIvC,MAAMC,EAAYpB,EAAkBQ,QAkBpC,OAdmBY,GAAa,EAAIrB,EAAYS,QAAQY,GAAa,IAInEA,GAAa,EAAIvB,EAAiBW,QAAQY,GAAa,IAOjCD,EAAQC,EAAY,GAGNd,KAExC,CAACnB,EAAqBmB,IAMlBe,EAAyBd,EAC5Be,IACC,IAAIC,EAAM,EACNC,EAAOvC,EAAY,EAEvB,KAAOsC,GAAOC,GAAM,CAClB,MAAMC,EAAMC,KAAKC,OAAOJ,EAAMC,GAAQ,GAChCI,EAAgBV,EAAcO,GAC9BI,EACJhC,EAAiBW,QAAQiB,IAAQnB,IAEnC,GAAIsB,GAAiBN,GAAUM,EAAgBC,EAAgBP,EAC7D,OAAOG,EACEG,EAAgBN,EACzBC,EAAME,EAAM,EAEZD,EAAOC,EAAM,CAEhB,CAGD,OAAOC,KAAKI,IAAI,EAAGP,EAAM,IAE3B,CAACL,EAAejC,EAAWE,IAKvB4C,EAAiBxB,EACpBY,GAAmBa,IAClB,IAAKA,EAAI,OAET,MAAMC,EAAiBD,EAAGE,wBAAwBtC,OAC5CuC,EAAatC,EAAiBW,QAAQW,GAE5C,GAAIgB,IAAeF,EAAgB,CAUjC,QATmBvB,IAAfyB,GACFjC,EAAkBM,SAAWyB,EAC7B9B,EAAmBK,SAAW,GAE9BN,EAAkBM,SAAWyB,EAAiBE,EAGhDtC,EAAiBW,QAAQW,GAASc,EAE9Bd,EAAQnB,EAAkBQ,QAAS,CACrC,MAAM4B,EAAalB,EAAcC,GACjCpB,EAAYS,QAAQW,GAASiB,EAC7BpC,EAAkBQ,QAAUW,CAC7B,MACCnB,EAAkBQ,QAAUkB,KAAKW,IAC/BrC,EAAkBQ,QAClBW,EAAQ,GAIZd,EAAsBiC,GAAKA,EAAI,EAChC,GAEH,CAACpB,IAIGqB,EAAalB,EAAuB9B,GACpCiD,EAAWd,KAAKW,IACpBpD,EAAY,EACZoC,EAAuB9B,EAAYK,GAAUR,GAmB/C,MAAO,CACLqD,aAhBmBC,EAAQ,KAC3B,MAAMC,EAAuB,GAC7B,IAAK,IAAIC,EAAIlB,KAAKI,IAAI,EAAGS,EAAanD,GAAWwD,GAAKJ,EAAUI,IAC9DD,EAAME,KAAK,CACT1B,MAAOyB,EACPE,UAAW5B,EAAc0B,GACzBG,WAAYhB,EAAea,KAG/B,OAAOD,GACN,CAACJ,EAAYC,EAAUtB,EAAea,EAAgB3C,IAOvD4D,YAJkB9B,EAAcjC,GAKhCgB,YACAG,qBACA6C,SAAWC,GACT1D,EAAa0D,EAAEC,cAAc5D,WAEnC"}
|
package/dist/index.module.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{useState as t,useMemo as
|
|
1
|
+
import{useState as r,useRef as t,useCallback as n,useLayoutEffect as e,useMemo as u}from"react";function c(c){var i=c.itemCount,o=c.itemHeight,f=c.overscan,a=void 0===f?3:f,v=c.containerHeight,h=r(0),l=h[0],s=h[1],m=r(0),g=m[0],d=m[1],R=null!=v?v:g,p=t({}),M=t({}),x=t(-1),H=t(null),T=t(0),b=t(0),C=r(0),w=C[0],z=C[1],B=n(function(){return b.current>0?T.current/b.current:o},[o]);e(function(){if(void 0===v){var r=H.current;if(r){var t=new ResizeObserver(function(r){var t=r[0];t.contentRect.height!==g&&d(t.contentRect.height)});return t.observe(r),function(){return t.disconnect()}}}},[v,g]);var I=n(function(r){if(r<=x.current)return M.current[r]||0;var t=x.current;return(t>=0?M.current[t]:0)+(t>=0?p.current[t]:0)+(r-t-1)*B()},[o,B]),O=n(function(r){for(var t=0,n=i-1;t<=n;){var e=Math.floor((t+n)/2),u=I(e),c=p.current[e]||B();if(u<=r&&u+c>r)return e;u<r?t=e+1:n=e-1}return Math.max(0,t-1)},[I,i,o]),S=n(function(r){return function(t){if(t){var n=t.getBoundingClientRect().height,e=p.current[r];if(e!==n){if(void 0===e?(T.current+=n,b.current+=1):T.current+=n-e,p.current[r]=n,r>x.current){var u=I(r);M.current[r]=u,x.current=r}else x.current=Math.min(x.current,r-1);z(function(r){return r+1})}}}},[I]),V=O(l),j=Math.min(i-1,O(l+R)+a);return{virtualItems:u(function(){for(var r=[],t=Math.max(0,V-a);t<=j;t++)r.push({index:t,offsetTop:I(t),measureRef:S(t)});return r},[V,j,I,S,a]),totalHeight:I(i),scrollRef:H,measurementVersion:w,onScroll:function(r){return s(r.currentTarget.scrollTop)}}}export{c as useVirtualize};
|
|
2
2
|
//# sourceMappingURL=index.module.js.map
|
package/dist/index.module.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.module.js","sources":["../src/index.js"],"sourcesContent":["import { useState, useMemo } from 'react';\n\nexport const useVirtualize = ({\n itemCount,\n itemHeight,\n containerHeight,\n overscan = 3\n}) => {\n // Store the current scroll position\n const [scrollTop, setScrollTop] = useState(0);\n\n // Update scroll position on scroll event\n const onScroll = (e) => {\n setScrollTop(e.currentTarget.scrollTop);\n };\n\n // Memoize calculations to prevent unnecessary re-renders\n const { virtualItems, totalHeight } = useMemo(() => {\n // 1. Calculate the visible range based on scroll position\n const rangeStart = Math.floor(scrollTop / itemHeight);\n const rangeEnd = Math.min(\n itemCount - 1,\n Math.floor((scrollTop + containerHeight) / itemHeight)\n );\n\n // 2. Add overscan (buffer) to the range for smoother scrolling\n const startIndex = Math.max(0, rangeStart - overscan);\n const endIndex = Math.min(itemCount - 1, rangeEnd + overscan);\n\n // 3. Generate the array of items to be rendered\n const virtualItems = [];\n for (let i = startIndex; i <= endIndex; i++) {\n virtualItems.push({\n index: i,\n offsetTop: i * itemHeight, // Calculate absolute position for each item\n });\n }\n\n // 4. Calculate total phantom height to maintain scrollbar size\n const totalHeight = itemCount * itemHeight;\n\n return { virtualItems, totalHeight };\n }, [scrollTop, itemCount, itemHeight, containerHeight, overscan]);\n\n return {\n virtualItems,\n totalHeight,\n onScroll,\n };\n};"],"names":["useVirtualize","_ref","itemCount","itemHeight","containerHeight","_ref$overscan","overscan","_useState","useState","scrollTop","setScrollTop","_useMemo","useMemo","rangeStart","Math","floor","rangeEnd","min","startIndex","max","endIndex","virtualItems","i","push","index","offsetTop","totalHeight","onScroll","e","currentTarget"],"mappings":"8CAEa,IAAAA,EAAgB,SAAHC,GAKpB,IAJJC,EAASD,EAATC,UACAC,EAAUF,EAAVE,WACAC,EAAeH,EAAfG,gBAAeC,EAAAJ,EACfK,SAAAA,OAAQ,IAAAD,EAAG,EAACA,EAGZE,EAAkCC,EAAS,GAApCC,EAASF,EAAEG,GAAAA,EAAYH,EAG9B,GAKAI,EAAsCC,EAAQ,WAc5C,IAZA,IAAMC,EAAaC,KAAKC,MAAMN,EAAYN,GACpCa,EAAWF,KAAKG,IACpBf,EAAY,EACZY,KAAKC,OAAON,EAAYL,GAAmBD,IAIvCe,EAAaJ,KAAKK,IAAI,EAAGN,EAAaP,GACtCc,EAAWN,KAAKG,IAAIf,EAAY,EAAGc,EAAWV,GAG9Ce,EAAe,GACZC,EAAIJ,EAAYI,GAAKF,EAAUE,IACtCD,EAAaE,KAAK,CAChBC,MAAOF,EACPG,UAAWH,EAAInB,IAOnB,MAAO,CAAEkB,aAAAA,EAAcK,YAFHxB,EAAYC,EAGlC,EAAG,CAACM,EAAWP,EAAWC,EAAYC,EAAiBE,IAEvD,MAAO,CACLe,aA5BkBV,EAAZU,aA6BNK,YA7B+Bf,EAAXe,YA8BpBC,SAnCe,SAACC,GAChBlB,EAAakB,EAAEC,cAAcpB,UAC/B,EAmCF"}
|
|
1
|
+
{"version":3,"file":"index.module.js","sources":["../src/index.ts"],"sourcesContent":["import {\n useState,\n useRef,\n useMemo,\n useCallback,\n useLayoutEffect,\n UIEvent,\n} from \"react\";\n\nexport interface UseVirtualizeProps {\n /** Total number of items in the list */\n itemCount: number;\n /** Estimated height of a single item (used for unmeasured items) */\n itemHeight: number;\n /** Number of extra items to render outside the visible view (default: 3) */\n overscan?: number;\n /** Fixed container height. If not provided, it will be measured automatically via ResizeObserver */\n containerHeight?: number;\n}\n\nexport interface VirtualItem {\n /** The index of the item in the original data array */\n index: number;\n /** The calculated top position (px) for absolute positioning */\n offsetTop: number;\n /** Callback ref to measure the actual DOM height of the item */\n measureRef: (el: HTMLElement | null) => void;\n}\n\nexport function useVirtualize({\n itemCount,\n itemHeight: estimatedItemHeight,\n overscan = 3,\n containerHeight: propsContainerHeight,\n}: UseVirtualizeProps) {\n const [scrollTop, setScrollTop] = useState(0);\n const [observedHeight, setObservedHeight] = useState(0);\n\n // Determine the effective container height:\n // Use the prop if provided; otherwise, fall back to the measured height.\n const height = propsContainerHeight ?? observedHeight;\n\n // --- REFS (State that doesn't trigger re-renders) ---\n\n // Stores the exact height of each measured item: { index: height }\n const measurementCache = useRef({} as Record<number, number>);\n\n // Stores the calculated total offset for each index: { index: offset }\n // This acts as a Prefix Sum array to avoid recalculating from 0 every time.\n const offsetCache = useRef({} as Record<number, number>);\n\n // Tracks the last index that has a valid calculated offset in the cache.\n const lastMeasuredIndex = useRef(-1);\n\n // Reference to the scrollable container (used for ResizeObserver)\n const scrollRef = useRef<HTMLDivElement>(null);\n\n // Total measured height (Pixel)\n const totalMeasuredSize = useRef(0);\n // Total measured element?\n const totalMeasuredCount = useRef(0);\n\n const [measurementVersion, setMeasurementVersion] = useState(0);\n\n // --- HELPER: GET ESTIMATED HEIGHT ---\n // Zeka burada: Eğer hiç ölçüm yoksa senin verdiğin 'itemHeight'ı kullan.\n // Ama ölçüm yaptıysak, gerçek ortalamayı kullan.\n const getEstimatedItemHeight = useCallback(() => {\n return totalMeasuredCount.current > 0\n ? totalMeasuredSize.current / totalMeasuredCount.current\n : estimatedItemHeight;\n }, [estimatedItemHeight]);\n\n // --- 1. AUTO-SIZER LOGIC ---\n useLayoutEffect(() => {\n // If the user provided a fixed height, skip observation to save resources.\n if (propsContainerHeight !== undefined) return;\n\n const scrollElement = scrollRef.current;\n if (!scrollElement) return;\n\n const observer = new ResizeObserver(([entry]) => {\n // Update state only if dimensions actually changed\n if (entry.contentRect.height !== observedHeight) {\n setObservedHeight(entry.contentRect.height);\n }\n });\n\n observer.observe(scrollElement);\n\n return () => observer.disconnect();\n }, [propsContainerHeight, observedHeight]);\n\n // --- 2. OFFSET CALCULATOR (Core Logic) ---\n // Returns the vertical position (px) of an item.\n const getItemOffset = useCallback(\n (index: number) => {\n // If the offset for this index is already cached, return it directly.\n if (index <= lastMeasuredIndex.current) {\n return offsetCache.current[index] || 0;\n }\n\n // 1. Retrieve reference data from the last measured item.\n const lastIndex = lastMeasuredIndex.current;\n\n // If no items have been measured yet (-1), the starting offset is 0.\n // Otherwise, retrieve the offset of the last measured item.\n const lastOffset = lastIndex >= 0 ? offsetCache.current[lastIndex] : 0;\n\n // Retrieve the height of the last measured item (default to 0 if none measured).\n const lastHeight =\n lastIndex >= 0 ? measurementCache.current[lastIndex] : 0;\n\n // 2. Calculate the bottom position of the last measured item.\n // This serves as the anchor point for estimating subsequent offsets.\n const lastBottom = lastOffset + lastHeight;\n\n // 3. Calculate how many unmeasured items exist between the last measured index and the target index.\n const unmeasuredCount = index - lastIndex - 1;\n\n // 4. Result: Anchor Point + (Number of Unmeasured Items * Estimated Height)\n return lastBottom + unmeasuredCount * getEstimatedItemHeight();\n },\n [estimatedItemHeight, getEstimatedItemHeight]\n );\n\n // --- 3. BINARY SEARCH (Find Visible Index) ---\n // Finds the index corresponding to the current scroll position.\n // Performance: O(log N) - Crucial for large lists.\n const getStartIndexForOffset = useCallback(\n (offset: number) => {\n let low = 0;\n let high = itemCount - 1;\n\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n const currentOffset = getItemOffset(mid);\n const currentHeight =\n measurementCache.current[mid] || getEstimatedItemHeight();\n\n if (currentOffset <= offset && currentOffset + currentHeight > offset) {\n return mid;\n } else if (currentOffset < offset) {\n low = mid + 1;\n } else {\n high = mid - 1;\n }\n }\n\n // Fallback to 0 if not found\n return Math.max(0, low - 1);\n },\n [getItemOffset, itemCount, estimatedItemHeight]\n );\n\n // --- 4. MEASUREMENT CALLBACK (The \"Ref\" logic) ---\n // This function is passed to the consumer to attach to their DOM elements.\n const measureElement = useCallback(\n (index: number) => (el: HTMLElement | null) => {\n if (!el) return;\n\n const measuredHeight = el.getBoundingClientRect().height;\n const prevHeight = measurementCache.current[index];\n\n if (prevHeight !== measuredHeight) {\n if (prevHeight === undefined) {\n totalMeasuredSize.current += measuredHeight;\n totalMeasuredCount.current += 1;\n } else {\n totalMeasuredSize.current += measuredHeight - prevHeight;\n }\n\n measurementCache.current[index] = measuredHeight;\n\n if (index > lastMeasuredIndex.current) {\n const prevOffset = getItemOffset(index);\n offsetCache.current[index] = prevOffset;\n lastMeasuredIndex.current = index;\n } else {\n lastMeasuredIndex.current = Math.min(\n lastMeasuredIndex.current,\n index - 1\n );\n }\n\n setMeasurementVersion(v => v + 1);\n }\n },\n [getItemOffset]\n );\n\n // --- 5. RENDER RANGE CALCULATION ---\n const startIndex = getStartIndexForOffset(scrollTop);\n const endIndex = Math.min(\n itemCount - 1,\n getStartIndexForOffset(scrollTop + height) + overscan\n );\n\n // Generate the virtual items array to be rendered\n const virtualItems = useMemo(() => {\n const items: VirtualItem[] = [];\n for (let i = Math.max(0, startIndex - overscan); i <= endIndex; i++) {\n items.push({\n index: i,\n offsetTop: getItemOffset(i),\n measureRef: measureElement(i),\n });\n }\n return items;\n }, [startIndex, endIndex, getItemOffset, measureElement, overscan]);\n\n // Calculate total list height for the scrollbar\n const totalHeight = getItemOffset(itemCount);\n\n return {\n virtualItems,\n totalHeight,\n scrollRef,\n measurementVersion,\n onScroll: (e: UIEvent<HTMLElement>) =>\n setScrollTop(e.currentTarget.scrollTop),\n };\n}\n"],"names":["useVirtualize","_ref","itemCount","estimatedItemHeight","itemHeight","_ref$overscan","overscan","propsContainerHeight","containerHeight","_useState","useState","scrollTop","setScrollTop","_useState2","observedHeight","setObservedHeight","height","measurementCache","useRef","offsetCache","lastMeasuredIndex","scrollRef","totalMeasuredSize","totalMeasuredCount","_useState3","measurementVersion","setMeasurementVersion","getEstimatedItemHeight","useCallback","current","useLayoutEffect","undefined","scrollElement","observer","ResizeObserver","_ref2","entry","contentRect","observe","disconnect","getItemOffset","index","lastIndex","getStartIndexForOffset","offset","low","high","mid","Math","floor","currentOffset","currentHeight","max","measureElement","el","measuredHeight","getBoundingClientRect","prevHeight","prevOffset","min","v","startIndex","endIndex","virtualItems","useMemo","items","i","push","offsetTop","measureRef","totalHeight","onScroll","e","currentTarget"],"mappings":"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"}
|
package/dist/index.umd.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
!function(
|
|
1
|
+
!function(e,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports,require("react")):"function"==typeof define&&define.amd?define(["exports","react"],r):r((e||self).reactSimpleVirtualize={},e.react)}(this,function(e,r){e.useVirtualize=function(e){var t=e.itemCount,n=e.itemHeight,u=e.overscan,c=void 0===u?3:u,i=e.containerHeight,o=r.useState(0),f=o[0],a=o[1],s=r.useState(0),l=s[0],h=s[1],v=null!=i?i:l,d=r.useRef({}),m=r.useRef({}),R=r.useRef(-1),g=r.useRef(null),p=r.useRef(0),b=r.useRef(0),x=r.useState(0),C=x[0],M=x[1],y=r.useCallback(function(){return b.current>0?p.current/b.current:n},[n]);r.useLayoutEffect(function(){if(void 0===i){var e=g.current;if(e){var r=new ResizeObserver(function(e){var r=e[0];r.contentRect.height!==l&&h(r.contentRect.height)});return r.observe(e),function(){return r.disconnect()}}}},[i,l]);var S=r.useCallback(function(e){if(e<=R.current)return m.current[e]||0;var r=R.current;return(r>=0?m.current[r]:0)+(r>=0?d.current[r]:0)+(e-r-1)*y()},[n,y]),T=r.useCallback(function(e){for(var r=0,n=t-1;r<=n;){var u=Math.floor((r+n)/2),c=S(u),i=d.current[u]||y();if(c<=e&&c+i>e)return u;c<e?r=u+1:n=u-1}return Math.max(0,r-1)},[S,t,n]),k=r.useCallback(function(e){return function(r){if(r){var t=r.getBoundingClientRect().height,n=d.current[e];if(n!==t){if(void 0===n?(p.current+=t,b.current+=1):p.current+=t-n,d.current[e]=t,e>R.current){var u=S(e);m.current[e]=u,R.current=e}else R.current=Math.min(R.current,e-1);M(function(e){return e+1})}}}},[S]),z=T(f),H=Math.min(t-1,T(f+v)+c);return{virtualItems:r.useMemo(function(){for(var e=[],r=Math.max(0,z-c);r<=H;r++)e.push({index:r,offsetTop:S(r),measureRef:k(r)});return e},[z,H,S,k,c]),totalHeight:S(t),scrollRef:g,measurementVersion:C,onScroll:function(e){return a(e.currentTarget.scrollTop)}}}});
|
|
2
2
|
//# sourceMappingURL=index.umd.js.map
|
package/dist/index.umd.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.umd.js","sources":["../src/index.js"],"sourcesContent":["import { useState, useMemo } from 'react';\n\nexport const useVirtualize = ({\n itemCount,\n itemHeight,\n containerHeight,\n overscan = 3\n}) => {\n // Store the current scroll position\n const [scrollTop, setScrollTop] = useState(0);\n\n // Update scroll position on scroll event\n const onScroll = (e) => {\n setScrollTop(e.currentTarget.scrollTop);\n };\n\n // Memoize calculations to prevent unnecessary re-renders\n const { virtualItems, totalHeight } = useMemo(() => {\n // 1. Calculate the visible range based on scroll position\n const rangeStart = Math.floor(scrollTop / itemHeight);\n const rangeEnd = Math.min(\n itemCount - 1,\n Math.floor((scrollTop + containerHeight) / itemHeight)\n );\n\n // 2. Add overscan (buffer) to the range for smoother scrolling\n const startIndex = Math.max(0, rangeStart - overscan);\n const endIndex = Math.min(itemCount - 1, rangeEnd + overscan);\n\n // 3. Generate the array of items to be rendered\n const virtualItems = [];\n for (let i = startIndex; i <= endIndex; i++) {\n virtualItems.push({\n index: i,\n offsetTop: i * itemHeight, // Calculate absolute position for each item\n });\n }\n\n // 4. Calculate total phantom height to maintain scrollbar size\n const totalHeight = itemCount * itemHeight;\n\n return { virtualItems, totalHeight };\n }, [scrollTop, itemCount, itemHeight, containerHeight, overscan]);\n\n return {\n virtualItems,\n totalHeight,\n onScroll,\n };\n};"],"names":["_ref","itemCount","itemHeight","containerHeight","_ref$overscan","overscan","_useState","useState","scrollTop","setScrollTop","_useMemo","useMemo","rangeStart","Math","floor","rangeEnd","min","startIndex","max","endIndex","virtualItems","i","push","index","offsetTop","totalHeight","onScroll","e","currentTarget"],"mappings":"kSAE6B,SAAHA,GAKpB,IAJJC,EAASD,EAATC,UACAC,EAAUF,EAAVE,WACAC,EAAeH,EAAfG,gBAAeC,EAAAJ,EACfK,SAAAA,OAAQ,IAAAD,EAAG,EAACA,EAGZE,EAAkCC,EAAQA,SAAC,GAApCC,EAASF,EAAEG,GAAAA,EAAYH,EAG9B,GAKAI,EAAsCC,EAAAA,QAAQ,WAc5C,IAZA,IAAMC,EAAaC,KAAKC,MAAMN,EAAYN,GACpCa,EAAWF,KAAKG,IACpBf,EAAY,EACZY,KAAKC,OAAON,EAAYL,GAAmBD,IAIvCe,EAAaJ,KAAKK,IAAI,EAAGN,EAAaP,GACtCc,EAAWN,KAAKG,IAAIf,EAAY,EAAGc,EAAWV,GAG9Ce,EAAe,GACZC,EAAIJ,EAAYI,GAAKF,EAAUE,IACtCD,EAAaE,KAAK,CAChBC,MAAOF,EACPG,UAAWH,EAAInB,IAOnB,MAAO,CAAEkB,aAAAA,EAAcK,YAFHxB,EAAYC,EAGlC,EAAG,CAACM,EAAWP,EAAWC,EAAYC,EAAiBE,IAEvD,MAAO,CACLe,aA5BkBV,EAAZU,aA6BNK,YA7B+Bf,EAAXe,YA8BpBC,SAnCe,SAACC,GAChBlB,EAAakB,EAAEC,cAAcpB,UAC/B,EAmCF"}
|
|
1
|
+
{"version":3,"file":"index.umd.js","sources":["../src/index.ts"],"sourcesContent":["import {\n useState,\n useRef,\n useMemo,\n useCallback,\n useLayoutEffect,\n UIEvent,\n} from \"react\";\n\nexport interface UseVirtualizeProps {\n /** Total number of items in the list */\n itemCount: number;\n /** Estimated height of a single item (used for unmeasured items) */\n itemHeight: number;\n /** Number of extra items to render outside the visible view (default: 3) */\n overscan?: number;\n /** Fixed container height. If not provided, it will be measured automatically via ResizeObserver */\n containerHeight?: number;\n}\n\nexport interface VirtualItem {\n /** The index of the item in the original data array */\n index: number;\n /** The calculated top position (px) for absolute positioning */\n offsetTop: number;\n /** Callback ref to measure the actual DOM height of the item */\n measureRef: (el: HTMLElement | null) => void;\n}\n\nexport function useVirtualize({\n itemCount,\n itemHeight: estimatedItemHeight,\n overscan = 3,\n containerHeight: propsContainerHeight,\n}: UseVirtualizeProps) {\n const [scrollTop, setScrollTop] = useState(0);\n const [observedHeight, setObservedHeight] = useState(0);\n\n // Determine the effective container height:\n // Use the prop if provided; otherwise, fall back to the measured height.\n const height = propsContainerHeight ?? observedHeight;\n\n // --- REFS (State that doesn't trigger re-renders) ---\n\n // Stores the exact height of each measured item: { index: height }\n const measurementCache = useRef({} as Record<number, number>);\n\n // Stores the calculated total offset for each index: { index: offset }\n // This acts as a Prefix Sum array to avoid recalculating from 0 every time.\n const offsetCache = useRef({} as Record<number, number>);\n\n // Tracks the last index that has a valid calculated offset in the cache.\n const lastMeasuredIndex = useRef(-1);\n\n // Reference to the scrollable container (used for ResizeObserver)\n const scrollRef = useRef<HTMLDivElement>(null);\n\n // Total measured height (Pixel)\n const totalMeasuredSize = useRef(0);\n // Total measured element?\n const totalMeasuredCount = useRef(0);\n\n const [measurementVersion, setMeasurementVersion] = useState(0);\n\n // --- HELPER: GET ESTIMATED HEIGHT ---\n // Zeka burada: Eğer hiç ölçüm yoksa senin verdiğin 'itemHeight'ı kullan.\n // Ama ölçüm yaptıysak, gerçek ortalamayı kullan.\n const getEstimatedItemHeight = useCallback(() => {\n return totalMeasuredCount.current > 0\n ? totalMeasuredSize.current / totalMeasuredCount.current\n : estimatedItemHeight;\n }, [estimatedItemHeight]);\n\n // --- 1. AUTO-SIZER LOGIC ---\n useLayoutEffect(() => {\n // If the user provided a fixed height, skip observation to save resources.\n if (propsContainerHeight !== undefined) return;\n\n const scrollElement = scrollRef.current;\n if (!scrollElement) return;\n\n const observer = new ResizeObserver(([entry]) => {\n // Update state only if dimensions actually changed\n if (entry.contentRect.height !== observedHeight) {\n setObservedHeight(entry.contentRect.height);\n }\n });\n\n observer.observe(scrollElement);\n\n return () => observer.disconnect();\n }, [propsContainerHeight, observedHeight]);\n\n // --- 2. OFFSET CALCULATOR (Core Logic) ---\n // Returns the vertical position (px) of an item.\n const getItemOffset = useCallback(\n (index: number) => {\n // If the offset for this index is already cached, return it directly.\n if (index <= lastMeasuredIndex.current) {\n return offsetCache.current[index] || 0;\n }\n\n // 1. Retrieve reference data from the last measured item.\n const lastIndex = lastMeasuredIndex.current;\n\n // If no items have been measured yet (-1), the starting offset is 0.\n // Otherwise, retrieve the offset of the last measured item.\n const lastOffset = lastIndex >= 0 ? offsetCache.current[lastIndex] : 0;\n\n // Retrieve the height of the last measured item (default to 0 if none measured).\n const lastHeight =\n lastIndex >= 0 ? measurementCache.current[lastIndex] : 0;\n\n // 2. Calculate the bottom position of the last measured item.\n // This serves as the anchor point for estimating subsequent offsets.\n const lastBottom = lastOffset + lastHeight;\n\n // 3. Calculate how many unmeasured items exist between the last measured index and the target index.\n const unmeasuredCount = index - lastIndex - 1;\n\n // 4. Result: Anchor Point + (Number of Unmeasured Items * Estimated Height)\n return lastBottom + unmeasuredCount * getEstimatedItemHeight();\n },\n [estimatedItemHeight, getEstimatedItemHeight]\n );\n\n // --- 3. BINARY SEARCH (Find Visible Index) ---\n // Finds the index corresponding to the current scroll position.\n // Performance: O(log N) - Crucial for large lists.\n const getStartIndexForOffset = useCallback(\n (offset: number) => {\n let low = 0;\n let high = itemCount - 1;\n\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n const currentOffset = getItemOffset(mid);\n const currentHeight =\n measurementCache.current[mid] || getEstimatedItemHeight();\n\n if (currentOffset <= offset && currentOffset + currentHeight > offset) {\n return mid;\n } else if (currentOffset < offset) {\n low = mid + 1;\n } else {\n high = mid - 1;\n }\n }\n\n // Fallback to 0 if not found\n return Math.max(0, low - 1);\n },\n [getItemOffset, itemCount, estimatedItemHeight]\n );\n\n // --- 4. MEASUREMENT CALLBACK (The \"Ref\" logic) ---\n // This function is passed to the consumer to attach to their DOM elements.\n const measureElement = useCallback(\n (index: number) => (el: HTMLElement | null) => {\n if (!el) return;\n\n const measuredHeight = el.getBoundingClientRect().height;\n const prevHeight = measurementCache.current[index];\n\n if (prevHeight !== measuredHeight) {\n if (prevHeight === undefined) {\n totalMeasuredSize.current += measuredHeight;\n totalMeasuredCount.current += 1;\n } else {\n totalMeasuredSize.current += measuredHeight - prevHeight;\n }\n\n measurementCache.current[index] = measuredHeight;\n\n if (index > lastMeasuredIndex.current) {\n const prevOffset = getItemOffset(index);\n offsetCache.current[index] = prevOffset;\n lastMeasuredIndex.current = index;\n } else {\n lastMeasuredIndex.current = Math.min(\n lastMeasuredIndex.current,\n index - 1\n );\n }\n\n setMeasurementVersion(v => v + 1);\n }\n },\n [getItemOffset]\n );\n\n // --- 5. RENDER RANGE CALCULATION ---\n const startIndex = getStartIndexForOffset(scrollTop);\n const endIndex = Math.min(\n itemCount - 1,\n getStartIndexForOffset(scrollTop + height) + overscan\n );\n\n // Generate the virtual items array to be rendered\n const virtualItems = useMemo(() => {\n const items: VirtualItem[] = [];\n for (let i = Math.max(0, startIndex - overscan); i <= endIndex; i++) {\n items.push({\n index: i,\n offsetTop: getItemOffset(i),\n measureRef: measureElement(i),\n });\n }\n return items;\n }, [startIndex, endIndex, getItemOffset, measureElement, overscan]);\n\n // Calculate total list height for the scrollbar\n const totalHeight = getItemOffset(itemCount);\n\n return {\n virtualItems,\n totalHeight,\n scrollRef,\n measurementVersion,\n onScroll: (e: UIEvent<HTMLElement>) =>\n setScrollTop(e.currentTarget.scrollTop),\n };\n}\n"],"names":["_ref","itemCount","estimatedItemHeight","itemHeight","_ref$overscan","overscan","propsContainerHeight","containerHeight","_useState","useState","scrollTop","setScrollTop","_useState2","observedHeight","setObservedHeight","height","measurementCache","useRef","offsetCache","lastMeasuredIndex","scrollRef","totalMeasuredSize","totalMeasuredCount","_useState3","measurementVersion","setMeasurementVersion","getEstimatedItemHeight","useCallback","current","useLayoutEffect","undefined","scrollElement","observer","ResizeObserver","_ref2","entry","contentRect","observe","disconnect","getItemOffset","index","lastIndex","getStartIndexForOffset","offset","low","high","mid","Math","floor","currentOffset","currentHeight","max","measureElement","el","measuredHeight","getBoundingClientRect","prevHeight","prevOffset","min","v","startIndex","endIndex","virtualItems","useMemo","items","i","push","offsetTop","measureRef","totalHeight","onScroll","e","currentTarget"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,39 +1,45 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
2
|
+
"name": "react-simple-virtualize",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "A lightweight, dependency-free virtualization hook for React.",
|
|
5
|
+
"source": "src/index.ts",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "dist/index.module.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.module.js"
|
|
13
|
+
},
|
|
14
|
+
"unpkg": "dist/index.umd.js",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "microbundle",
|
|
17
|
+
"dev": "microbundle watch",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"react",
|
|
22
|
+
"virtual-list",
|
|
23
|
+
"virtualization",
|
|
24
|
+
"scroll",
|
|
25
|
+
"performance",
|
|
26
|
+
"hook"
|
|
27
|
+
],
|
|
28
|
+
"author": "Ozan Batuhan Ceylan",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/ozanbatuhanceylan/react-simple-virtualize"
|
|
33
|
+
},
|
|
34
|
+
"funding": {
|
|
35
|
+
"type": "buymeacoffee",
|
|
36
|
+
"url": "https://www.buymeacoffee.com/obceylan"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"react": ">=16.8.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/react": "^19.2.8",
|
|
43
|
+
"microbundle": "^0.15.1"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useState,
|
|
3
|
+
useRef,
|
|
4
|
+
useMemo,
|
|
5
|
+
useCallback,
|
|
6
|
+
useLayoutEffect,
|
|
7
|
+
UIEvent,
|
|
8
|
+
} from "react";
|
|
9
|
+
|
|
10
|
+
export interface UseVirtualizeProps {
|
|
11
|
+
/** Total number of items in the list */
|
|
12
|
+
itemCount: number;
|
|
13
|
+
/** Estimated height of a single item (used for unmeasured items) */
|
|
14
|
+
itemHeight: number;
|
|
15
|
+
/** Number of extra items to render outside the visible view (default: 3) */
|
|
16
|
+
overscan?: number;
|
|
17
|
+
/** Fixed container height. If not provided, it will be measured automatically via ResizeObserver */
|
|
18
|
+
containerHeight?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface VirtualItem {
|
|
22
|
+
/** The index of the item in the original data array */
|
|
23
|
+
index: number;
|
|
24
|
+
/** The calculated top position (px) for absolute positioning */
|
|
25
|
+
offsetTop: number;
|
|
26
|
+
/** Callback ref to measure the actual DOM height of the item */
|
|
27
|
+
measureRef: (el: HTMLElement | null) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useVirtualize({
|
|
31
|
+
itemCount,
|
|
32
|
+
itemHeight: estimatedItemHeight,
|
|
33
|
+
overscan = 3,
|
|
34
|
+
containerHeight: propsContainerHeight,
|
|
35
|
+
}: UseVirtualizeProps) {
|
|
36
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
37
|
+
const [observedHeight, setObservedHeight] = useState(0);
|
|
38
|
+
|
|
39
|
+
// Determine the effective container height:
|
|
40
|
+
// Use the prop if provided; otherwise, fall back to the measured height.
|
|
41
|
+
const height = propsContainerHeight ?? observedHeight;
|
|
42
|
+
|
|
43
|
+
// --- REFS (State that doesn't trigger re-renders) ---
|
|
44
|
+
|
|
45
|
+
// Stores the exact height of each measured item: { index: height }
|
|
46
|
+
const measurementCache = useRef({} as Record<number, number>);
|
|
47
|
+
|
|
48
|
+
// Stores the calculated total offset for each index: { index: offset }
|
|
49
|
+
// This acts as a Prefix Sum array to avoid recalculating from 0 every time.
|
|
50
|
+
const offsetCache = useRef({} as Record<number, number>);
|
|
51
|
+
|
|
52
|
+
// Tracks the last index that has a valid calculated offset in the cache.
|
|
53
|
+
const lastMeasuredIndex = useRef(-1);
|
|
54
|
+
|
|
55
|
+
// Reference to the scrollable container (used for ResizeObserver)
|
|
56
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
57
|
+
|
|
58
|
+
// Total measured height (Pixel)
|
|
59
|
+
const totalMeasuredSize = useRef(0);
|
|
60
|
+
// Total measured element?
|
|
61
|
+
const totalMeasuredCount = useRef(0);
|
|
62
|
+
|
|
63
|
+
const [measurementVersion, setMeasurementVersion] = useState(0);
|
|
64
|
+
|
|
65
|
+
// --- HELPER: GET ESTIMATED HEIGHT ---
|
|
66
|
+
// Zeka burada: Eğer hiç ölçüm yoksa senin verdiğin 'itemHeight'ı kullan.
|
|
67
|
+
// Ama ölçüm yaptıysak, gerçek ortalamayı kullan.
|
|
68
|
+
const getEstimatedItemHeight = useCallback(() => {
|
|
69
|
+
return totalMeasuredCount.current > 0
|
|
70
|
+
? totalMeasuredSize.current / totalMeasuredCount.current
|
|
71
|
+
: estimatedItemHeight;
|
|
72
|
+
}, [estimatedItemHeight]);
|
|
73
|
+
|
|
74
|
+
// --- 1. AUTO-SIZER LOGIC ---
|
|
75
|
+
useLayoutEffect(() => {
|
|
76
|
+
// If the user provided a fixed height, skip observation to save resources.
|
|
77
|
+
if (propsContainerHeight !== undefined) return;
|
|
78
|
+
|
|
79
|
+
const scrollElement = scrollRef.current;
|
|
80
|
+
if (!scrollElement) return;
|
|
81
|
+
|
|
82
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
83
|
+
// Update state only if dimensions actually changed
|
|
84
|
+
if (entry.contentRect.height !== observedHeight) {
|
|
85
|
+
setObservedHeight(entry.contentRect.height);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
observer.observe(scrollElement);
|
|
90
|
+
|
|
91
|
+
return () => observer.disconnect();
|
|
92
|
+
}, [propsContainerHeight, observedHeight]);
|
|
93
|
+
|
|
94
|
+
// --- 2. OFFSET CALCULATOR (Core Logic) ---
|
|
95
|
+
// Returns the vertical position (px) of an item.
|
|
96
|
+
const getItemOffset = useCallback(
|
|
97
|
+
(index: number) => {
|
|
98
|
+
// If the offset for this index is already cached, return it directly.
|
|
99
|
+
if (index <= lastMeasuredIndex.current) {
|
|
100
|
+
return offsetCache.current[index] || 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 1. Retrieve reference data from the last measured item.
|
|
104
|
+
const lastIndex = lastMeasuredIndex.current;
|
|
105
|
+
|
|
106
|
+
// If no items have been measured yet (-1), the starting offset is 0.
|
|
107
|
+
// Otherwise, retrieve the offset of the last measured item.
|
|
108
|
+
const lastOffset = lastIndex >= 0 ? offsetCache.current[lastIndex] : 0;
|
|
109
|
+
|
|
110
|
+
// Retrieve the height of the last measured item (default to 0 if none measured).
|
|
111
|
+
const lastHeight =
|
|
112
|
+
lastIndex >= 0 ? measurementCache.current[lastIndex] : 0;
|
|
113
|
+
|
|
114
|
+
// 2. Calculate the bottom position of the last measured item.
|
|
115
|
+
// This serves as the anchor point for estimating subsequent offsets.
|
|
116
|
+
const lastBottom = lastOffset + lastHeight;
|
|
117
|
+
|
|
118
|
+
// 3. Calculate how many unmeasured items exist between the last measured index and the target index.
|
|
119
|
+
const unmeasuredCount = index - lastIndex - 1;
|
|
120
|
+
|
|
121
|
+
// 4. Result: Anchor Point + (Number of Unmeasured Items * Estimated Height)
|
|
122
|
+
return lastBottom + unmeasuredCount * getEstimatedItemHeight();
|
|
123
|
+
},
|
|
124
|
+
[estimatedItemHeight, getEstimatedItemHeight]
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// --- 3. BINARY SEARCH (Find Visible Index) ---
|
|
128
|
+
// Finds the index corresponding to the current scroll position.
|
|
129
|
+
// Performance: O(log N) - Crucial for large lists.
|
|
130
|
+
const getStartIndexForOffset = useCallback(
|
|
131
|
+
(offset: number) => {
|
|
132
|
+
let low = 0;
|
|
133
|
+
let high = itemCount - 1;
|
|
134
|
+
|
|
135
|
+
while (low <= high) {
|
|
136
|
+
const mid = Math.floor((low + high) / 2);
|
|
137
|
+
const currentOffset = getItemOffset(mid);
|
|
138
|
+
const currentHeight =
|
|
139
|
+
measurementCache.current[mid] || getEstimatedItemHeight();
|
|
140
|
+
|
|
141
|
+
if (currentOffset <= offset && currentOffset + currentHeight > offset) {
|
|
142
|
+
return mid;
|
|
143
|
+
} else if (currentOffset < offset) {
|
|
144
|
+
low = mid + 1;
|
|
145
|
+
} else {
|
|
146
|
+
high = mid - 1;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Fallback to 0 if not found
|
|
151
|
+
return Math.max(0, low - 1);
|
|
152
|
+
},
|
|
153
|
+
[getItemOffset, itemCount, estimatedItemHeight]
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// --- 4. MEASUREMENT CALLBACK (The "Ref" logic) ---
|
|
157
|
+
// This function is passed to the consumer to attach to their DOM elements.
|
|
158
|
+
const measureElement = useCallback(
|
|
159
|
+
(index: number) => (el: HTMLElement | null) => {
|
|
160
|
+
if (!el) return;
|
|
161
|
+
|
|
162
|
+
const measuredHeight = el.getBoundingClientRect().height;
|
|
163
|
+
const prevHeight = measurementCache.current[index];
|
|
164
|
+
|
|
165
|
+
if (prevHeight !== measuredHeight) {
|
|
166
|
+
if (prevHeight === undefined) {
|
|
167
|
+
totalMeasuredSize.current += measuredHeight;
|
|
168
|
+
totalMeasuredCount.current += 1;
|
|
169
|
+
} else {
|
|
170
|
+
totalMeasuredSize.current += measuredHeight - prevHeight;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
measurementCache.current[index] = measuredHeight;
|
|
174
|
+
|
|
175
|
+
if (index > lastMeasuredIndex.current) {
|
|
176
|
+
const prevOffset = getItemOffset(index);
|
|
177
|
+
offsetCache.current[index] = prevOffset;
|
|
178
|
+
lastMeasuredIndex.current = index;
|
|
179
|
+
} else {
|
|
180
|
+
lastMeasuredIndex.current = Math.min(
|
|
181
|
+
lastMeasuredIndex.current,
|
|
182
|
+
index - 1
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
setMeasurementVersion(v => v + 1);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
[getItemOffset]
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// --- 5. RENDER RANGE CALCULATION ---
|
|
193
|
+
const startIndex = getStartIndexForOffset(scrollTop);
|
|
194
|
+
const endIndex = Math.min(
|
|
195
|
+
itemCount - 1,
|
|
196
|
+
getStartIndexForOffset(scrollTop + height) + overscan
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// Generate the virtual items array to be rendered
|
|
200
|
+
const virtualItems = useMemo(() => {
|
|
201
|
+
const items: VirtualItem[] = [];
|
|
202
|
+
for (let i = Math.max(0, startIndex - overscan); i <= endIndex; i++) {
|
|
203
|
+
items.push({
|
|
204
|
+
index: i,
|
|
205
|
+
offsetTop: getItemOffset(i),
|
|
206
|
+
measureRef: measureElement(i),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return items;
|
|
210
|
+
}, [startIndex, endIndex, getItemOffset, measureElement, overscan]);
|
|
211
|
+
|
|
212
|
+
// Calculate total list height for the scrollbar
|
|
213
|
+
const totalHeight = getItemOffset(itemCount);
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
virtualItems,
|
|
217
|
+
totalHeight,
|
|
218
|
+
scrollRef,
|
|
219
|
+
measurementVersion,
|
|
220
|
+
onScroll: (e: UIEvent<HTMLElement>) =>
|
|
221
|
+
setScrollTop(e.currentTarget.scrollTop),
|
|
222
|
+
};
|
|
223
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["DOM", "ESNext"],
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationMap": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"noImplicitAny": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"forceConsistentCasingInFileNames": true
|
|
16
|
+
},
|
|
17
|
+
"include": [
|
|
18
|
+
"src/**/*"
|
|
19
|
+
],
|
|
20
|
+
"exclude": [
|
|
21
|
+
"node_modules",
|
|
22
|
+
"dist"
|
|
23
|
+
]
|
|
24
|
+
}
|
package/src/index.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { useState, useMemo } from 'react';
|
|
2
|
-
|
|
3
|
-
export const useVirtualize = ({
|
|
4
|
-
itemCount,
|
|
5
|
-
itemHeight,
|
|
6
|
-
containerHeight,
|
|
7
|
-
overscan = 3
|
|
8
|
-
}) => {
|
|
9
|
-
// Store the current scroll position
|
|
10
|
-
const [scrollTop, setScrollTop] = useState(0);
|
|
11
|
-
|
|
12
|
-
// Update scroll position on scroll event
|
|
13
|
-
const onScroll = (e) => {
|
|
14
|
-
setScrollTop(e.currentTarget.scrollTop);
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
// Memoize calculations to prevent unnecessary re-renders
|
|
18
|
-
const { virtualItems, totalHeight } = useMemo(() => {
|
|
19
|
-
// 1. Calculate the visible range based on scroll position
|
|
20
|
-
const rangeStart = Math.floor(scrollTop / itemHeight);
|
|
21
|
-
const rangeEnd = Math.min(
|
|
22
|
-
itemCount - 1,
|
|
23
|
-
Math.floor((scrollTop + containerHeight) / itemHeight)
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
// 2. Add overscan (buffer) to the range for smoother scrolling
|
|
27
|
-
const startIndex = Math.max(0, rangeStart - overscan);
|
|
28
|
-
const endIndex = Math.min(itemCount - 1, rangeEnd + overscan);
|
|
29
|
-
|
|
30
|
-
// 3. Generate the array of items to be rendered
|
|
31
|
-
const virtualItems = [];
|
|
32
|
-
for (let i = startIndex; i <= endIndex; i++) {
|
|
33
|
-
virtualItems.push({
|
|
34
|
-
index: i,
|
|
35
|
-
offsetTop: i * itemHeight, // Calculate absolute position for each item
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// 4. Calculate total phantom height to maintain scrollbar size
|
|
40
|
-
const totalHeight = itemCount * itemHeight;
|
|
41
|
-
|
|
42
|
-
return { virtualItems, totalHeight };
|
|
43
|
-
}, [scrollTop, itemCount, itemHeight, containerHeight, overscan]);
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
virtualItems,
|
|
47
|
-
totalHeight,
|
|
48
|
-
onScroll,
|
|
49
|
-
};
|
|
50
|
-
};
|