@dot-present/virtual-list 1.0.2-alpha.0 → 1.0.4-alpha.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/dist/index.js +1 -0
- package/package.json +8 -3
- package/src/App.tsx +0 -395
- package/src/index.tsx +0 -3
- package/src/types.d.ts +0 -2
- package/tsconfig.app.json +0 -29
- package/tsconfig.json +0 -15
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{jsx as t}from"react/jsx-runtime";import{forwardRef as e,useRef as n,useState as r,useReducer as o,useMemo as c,useEffect as i,useCallback as s,useLayoutEffect as h,useImperativeHandle as l}from"react";const u=e((e,u)=>{const{data:f,itemKey:a,estimatedItemHeight:g=50,itemHeight:d,containerHeight:m="100%",bufferSize:p=3,overscanCount:x,onScroll:v,onReachEnd:M,renderItem:I}=e,H=x??p,b=void 0!==d,w=n(null),y=n(new Map),R=n(new Map),T=n(!1),[S,z]=r(0),[C,$]=r(0),[E,O]=o(t=>t+1,0),Y=c(()=>{if("number"==typeof m)return m;if("string"==typeof m){const t=m.trim().match(/^(\d+(?:\.\d+)?)px$/i);if(t)return Number(t[1])}return 0},[m]),j=n([]);i(()=>{if(b)return;f.length;let t=0;j.current=f.map((e,n)=>{const r={index:n,height:g,offset:t};return t+=g,r})},[f,g,b]),i(()=>()=>{R.current.forEach(t=>t.disconnect()),R.current.clear()},[]);const B=s((t,e)=>{if(b)return;const n=j.current[t];if(!n||n.height===e)return;const r=n.height;n.height=e;let o=e-r;for(let e=t+1;e<j.current.length;e++){const t=j.current[e];t&&(t.offset+=o)}O()},[b]);h(()=>{if(!w.current)return;const t=w.current.clientHeight;t>0&&$(t)},[]),i(()=>{if(!w.current)return;const t=new ResizeObserver(t=>{if(t.length>0){const e=t[0].contentRect.height;$(e)}});return t.observe(w.current),()=>t.disconnect()},[]);const{startIndex:K,endIndex:N,totalHeight:_}=c(()=>{if(0===f.length)return{startIndex:0,endIndex:-1,totalHeight:0};const t=C>0?C:Y>0?Y:8*g;if(b){const e=d,n=Math.max(0,Math.floor(S/e)-H),r=Math.max(1,Math.ceil(t/e));return{startIndex:n,endIndex:Math.min(f.length-1,n+r+H),totalHeight:f.length*e}}{const e=(t=>{let e=0,n=f.length-1,r=0;for(;e<=n;){const o=Math.floor((e+n)/2);(j.current[o]?.offset??0)<=t?(r=o,e=o+1):n=o-1}return r})(S);let n=e;const r=j.current[e];let o=(r?.offset??0)+(r?.height??0);for(;n<f.length-1&&o-S<t;){n++;const t=j.current[n];o=(t?.offset??0)+(t?.height??0)}return{startIndex:Math.max(0,e-H),endIndex:Math.min(f.length-1,n+H),totalHeight:j.current.length>0?(()=>{const t=j.current[j.current.length-1];return(t?.offset??0)+(t?.height??0)})():0}}},[S,C,f.length,H,d,b,E,Y,g]),k=s(t=>{const e=t.currentTarget,n=e.scrollTop;z(n),v?.(t);const r=e.scrollHeight-e.scrollTop-e.clientHeight<5;M&&r&&!T.current?(T.current=!0,M()):r||(T.current=!1)},[v,M]);l(u,()=>({scrollTo:t=>{if(!w.current)return;const e=Math.max(0,t);w.current.scrollTop=e,z(e)},scrollToIndex:(t,e="start")=>{if(!w.current||0===f.length)return;const n=Math.min(Math.max(0,t),f.length-1);let r=0;r=b?n*d:j.current[n]?.offset??0;const o=b?d:j.current[n]?.height??g;"center"===e?r-=(C-o)/2:"end"===e&&(r-=C-o);const c=Math.max(0,r);w.current.scrollTop=c,z(c)},getCurrentRange:()=>({start:K,end:N})}),[K,N,C,d,b,f.length,g]);const q=s((t,e)=>"function"==typeof a?a(t,e):t[a]?.toString()??`item_${e}`,[a]),A=c(()=>{const e=[];for(let n=K;n<=N&&n<f.length;n++){const r=f[n];if(void 0===r)continue;const o=q(r,n);let c=0;c=b?n*d:j.current[n]?.offset??0,e.push(t("div",{ref:t=>{if(b)return;if(!t){const t=R.current.get(o);return t&&(t.disconnect(),R.current.delete(o)),void y.current.delete(o)}const e=t;y.current.set(o,e);const r=R.current.get(o);r&&r.disconnect();const c=new ResizeObserver(t=>{const e=t[0]?.contentRect.height;e&&e>0&&B(n,e)});c.observe(t),R.current.set(o,c);const i=e.getBoundingClientRect().height;i&&j.current[n]?.height!==i&&B(n,i)},style:{position:"absolute",top:0,left:0,width:"100%",transform:`translateY(${c}px)`},children:I(r,n)},o))}return e},[K,N,f,q,b,d,I,B,E]);return t("div",{ref:w,style:{height:m,overflowY:"auto",position:"relative"},onScroll:k,children:t("div",{style:{position:"relative",height:_,width:"100%"},children:A})})});export{u as VirtualList};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dot-present/virtual-list",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4-alpha.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./src/index.tsx",
|
|
@@ -8,16 +8,21 @@
|
|
|
8
8
|
"scripts": {
|
|
9
9
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
10
10
|
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
11
17
|
"publishConfig": {
|
|
12
18
|
"access": "public"
|
|
13
19
|
},
|
|
14
20
|
"keywords": [],
|
|
15
21
|
"author": "",
|
|
16
|
-
"license": "ISC",
|
|
17
22
|
"packageManager": "pnpm@10.26.2",
|
|
18
23
|
"peerDependencies": {
|
|
19
24
|
"react": ">=18",
|
|
20
25
|
"react-dom": ">=18"
|
|
21
26
|
},
|
|
22
|
-
"gitHead": "
|
|
27
|
+
"gitHead": "33126b0d5ef1834105552afc9f693e31d1a7b5b5"
|
|
23
28
|
}
|
package/src/App.tsx
DELETED
|
@@ -1,395 +0,0 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
useRef,
|
|
3
|
-
useState,
|
|
4
|
-
useEffect,
|
|
5
|
-
useLayoutEffect,
|
|
6
|
-
useMemo,
|
|
7
|
-
useCallback,
|
|
8
|
-
forwardRef,
|
|
9
|
-
useImperativeHandle,
|
|
10
|
-
useReducer,
|
|
11
|
-
} from "react";
|
|
12
|
-
|
|
13
|
-
// 位置缓存项
|
|
14
|
-
interface Position {
|
|
15
|
-
index: number;
|
|
16
|
-
height: number;
|
|
17
|
-
offset: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface VirtualListRef {
|
|
21
|
-
scrollTo: (scrollTop: number) => void;
|
|
22
|
-
scrollToIndex: (index: number, align?: "start" | "center" | "end") => void;
|
|
23
|
-
getCurrentRange: () => { start: number; end: number };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface VirtualListProps<T> {
|
|
27
|
-
data: T[];
|
|
28
|
-
itemKey: keyof T | ((item: T, index: number) => string);
|
|
29
|
-
estimatedItemHeight?: number; // 预估高度,动态高度模式必须
|
|
30
|
-
itemHeight?: number; // 若提供,则启用固定高度模式
|
|
31
|
-
containerHeight?: number | string;
|
|
32
|
-
bufferSize?: number;
|
|
33
|
-
overscanCount?: number; // 与 bufferSize 同义,可选
|
|
34
|
-
onScroll?: (e: React.UIEvent<HTMLDivElement>) => void;
|
|
35
|
-
onReachEnd?: () => void;
|
|
36
|
-
renderItem: (item: T, index: number) => React.ReactNode;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const VirtualList = forwardRef(
|
|
40
|
-
<T,>(props: VirtualListProps<T>, ref: React.Ref<VirtualListRef>) => {
|
|
41
|
-
const {
|
|
42
|
-
data,
|
|
43
|
-
itemKey,
|
|
44
|
-
estimatedItemHeight = 50,
|
|
45
|
-
itemHeight: fixedItemHeight,
|
|
46
|
-
containerHeight = "100%",
|
|
47
|
-
bufferSize = 3,
|
|
48
|
-
overscanCount,
|
|
49
|
-
onScroll,
|
|
50
|
-
onReachEnd,
|
|
51
|
-
renderItem,
|
|
52
|
-
} = props;
|
|
53
|
-
|
|
54
|
-
const buffer = overscanCount ?? bufferSize;
|
|
55
|
-
const isFixedHeight = fixedItemHeight !== undefined;
|
|
56
|
-
|
|
57
|
-
// DOM refs
|
|
58
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
59
|
-
const itemsRef = useRef<Map<string, HTMLElement>>(new Map());
|
|
60
|
-
const itemObserversRef = useRef<Map<string, ResizeObserver>>(new Map());
|
|
61
|
-
const isNearEndRef = useRef(false);
|
|
62
|
-
|
|
63
|
-
// 状态
|
|
64
|
-
const [scrollTop, setScrollTop] = useState(0);
|
|
65
|
-
const [containerHeightVal, setContainerHeightVal] = useState(0);
|
|
66
|
-
/** 动态高度测量后递增,驱动 range / visibleItems 的 memo 与真实 offset 同步 */
|
|
67
|
-
const [measureVersion, bumpMeasure] = useReducer((v: number) => v + 1, 0);
|
|
68
|
-
|
|
69
|
-
const parsedContainerHeight = useMemo(() => {
|
|
70
|
-
if (typeof containerHeight === "number") return containerHeight;
|
|
71
|
-
if (typeof containerHeight === "string") {
|
|
72
|
-
const matched = containerHeight.trim().match(/^(\d+(?:\.\d+)?)px$/i);
|
|
73
|
-
if (matched) return Number(matched[1]);
|
|
74
|
-
}
|
|
75
|
-
return 0;
|
|
76
|
-
}, [containerHeight]);
|
|
77
|
-
|
|
78
|
-
// ---------- 动态高度模式的位置缓存 ----------
|
|
79
|
-
const positions = useRef<Position[]>([]);
|
|
80
|
-
|
|
81
|
-
// 初始化 / 数据变化时重建位置缓存
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
if (isFixedHeight) return;
|
|
84
|
-
const total = data.length;
|
|
85
|
-
let offset = 0;
|
|
86
|
-
positions.current = data.map((_, idx) => {
|
|
87
|
-
const height = estimatedItemHeight;
|
|
88
|
-
const pos = { index: idx, height, offset };
|
|
89
|
-
offset += height;
|
|
90
|
-
return pos;
|
|
91
|
-
});
|
|
92
|
-
}, [data, estimatedItemHeight, isFixedHeight]);
|
|
93
|
-
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
return () => {
|
|
96
|
-
itemObserversRef.current.forEach((ro) => ro.disconnect());
|
|
97
|
-
itemObserversRef.current.clear();
|
|
98
|
-
};
|
|
99
|
-
}, []);
|
|
100
|
-
|
|
101
|
-
// 更新某个索引的高度,并修正后续偏移
|
|
102
|
-
const updateItemHeight = useCallback(
|
|
103
|
-
(index: number, newHeight: number) => {
|
|
104
|
-
if (isFixedHeight) return;
|
|
105
|
-
const pos = positions.current[index];
|
|
106
|
-
if (!pos || pos.height === newHeight) return;
|
|
107
|
-
const oldHeight = pos.height;
|
|
108
|
-
pos.height = newHeight;
|
|
109
|
-
// 修正从 index+1 开始的所有偏移量
|
|
110
|
-
let diff = newHeight - oldHeight;
|
|
111
|
-
for (let i = index + 1; i < positions.current.length; i++) {
|
|
112
|
-
const pos = positions.current[i];
|
|
113
|
-
if (pos) {
|
|
114
|
-
pos.offset += diff;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
bumpMeasure();
|
|
118
|
-
},
|
|
119
|
-
[isFixedHeight],
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
// 监听容器高度变化
|
|
123
|
-
useLayoutEffect(() => {
|
|
124
|
-
if (!containerRef.current) return;
|
|
125
|
-
const initialHeight = containerRef.current.clientHeight;
|
|
126
|
-
if (initialHeight > 0) {
|
|
127
|
-
setContainerHeightVal(initialHeight);
|
|
128
|
-
}
|
|
129
|
-
}, []);
|
|
130
|
-
|
|
131
|
-
useEffect(() => {
|
|
132
|
-
if (!containerRef.current) return;
|
|
133
|
-
const observer = new ResizeObserver((entries) => {
|
|
134
|
-
if (entries.length > 0) {
|
|
135
|
-
const height = entries[0].contentRect.height;
|
|
136
|
-
setContainerHeightVal(height);
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
observer.observe(containerRef.current);
|
|
140
|
-
return () => observer.disconnect();
|
|
141
|
-
}, []);
|
|
142
|
-
|
|
143
|
-
// 计算当前可视区的起始/结束索引
|
|
144
|
-
const { startIndex, endIndex, totalHeight } = useMemo(() => {
|
|
145
|
-
if (data.length === 0) {
|
|
146
|
-
return { startIndex: 0, endIndex: -1, totalHeight: 0 };
|
|
147
|
-
}
|
|
148
|
-
const effectiveContainerHeight =
|
|
149
|
-
containerHeightVal > 0
|
|
150
|
-
? containerHeightVal
|
|
151
|
-
: parsedContainerHeight > 0
|
|
152
|
-
? parsedContainerHeight
|
|
153
|
-
: estimatedItemHeight * 8;
|
|
154
|
-
|
|
155
|
-
if (isFixedHeight) {
|
|
156
|
-
const itemHeight = fixedItemHeight!;
|
|
157
|
-
const start = Math.max(0, Math.floor(scrollTop / itemHeight) - buffer);
|
|
158
|
-
const visibleCount = Math.max(
|
|
159
|
-
1,
|
|
160
|
-
Math.ceil(effectiveContainerHeight / itemHeight),
|
|
161
|
-
);
|
|
162
|
-
const end = Math.min(data.length - 1, start + visibleCount + buffer);
|
|
163
|
-
const total = data.length * itemHeight;
|
|
164
|
-
return { startIndex: start, endIndex: end, totalHeight: total };
|
|
165
|
-
} else {
|
|
166
|
-
// 二分:最大 i 满足 offset[i] <= scrollTop(首项可见行的上沿)
|
|
167
|
-
const findStartIndex = (st: number) => {
|
|
168
|
-
let low = 0;
|
|
169
|
-
let high = data.length - 1;
|
|
170
|
-
let ans = 0;
|
|
171
|
-
while (low <= high) {
|
|
172
|
-
const mid = Math.floor((low + high) / 2);
|
|
173
|
-
const off = positions.current[mid]?.offset ?? 0;
|
|
174
|
-
if (off <= st) {
|
|
175
|
-
ans = mid;
|
|
176
|
-
low = mid + 1;
|
|
177
|
-
} else {
|
|
178
|
-
high = mid - 1;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
// 返回包含当前 scrollTop 的最后一个起始 offset <= scrollTop 的元素
|
|
182
|
-
return ans;
|
|
183
|
-
};
|
|
184
|
-
const start = findStartIndex(scrollTop);
|
|
185
|
-
let end = start;
|
|
186
|
-
const startPos = positions.current[start];
|
|
187
|
-
let offsetEnd = (startPos?.offset ?? 0) + (startPos?.height ?? 0);
|
|
188
|
-
while (
|
|
189
|
-
end < data.length - 1 &&
|
|
190
|
-
offsetEnd - scrollTop < effectiveContainerHeight
|
|
191
|
-
) {
|
|
192
|
-
end++;
|
|
193
|
-
const p = positions.current[end];
|
|
194
|
-
offsetEnd = (p?.offset ?? 0) + (p?.height ?? 0);
|
|
195
|
-
}
|
|
196
|
-
// 增加缓冲区
|
|
197
|
-
const startWithBuffer = Math.max(0, start - buffer);
|
|
198
|
-
const endWithBuffer = Math.min(data.length - 1, end + buffer);
|
|
199
|
-
const total =
|
|
200
|
-
positions.current.length > 0
|
|
201
|
-
? (() => {
|
|
202
|
-
const lastPos = positions.current[positions.current.length - 1];
|
|
203
|
-
return (lastPos?.offset ?? 0) + (lastPos?.height ?? 0);
|
|
204
|
-
})()
|
|
205
|
-
: 0;
|
|
206
|
-
return {
|
|
207
|
-
startIndex: startWithBuffer,
|
|
208
|
-
endIndex: endWithBuffer,
|
|
209
|
-
totalHeight: total,
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
}, [
|
|
213
|
-
scrollTop,
|
|
214
|
-
containerHeightVal,
|
|
215
|
-
data.length,
|
|
216
|
-
buffer,
|
|
217
|
-
fixedItemHeight,
|
|
218
|
-
isFixedHeight,
|
|
219
|
-
measureVersion,
|
|
220
|
-
parsedContainerHeight,
|
|
221
|
-
estimatedItemHeight,
|
|
222
|
-
]);
|
|
223
|
-
|
|
224
|
-
// 滚动事件处理(含节流)
|
|
225
|
-
const handleScroll = useCallback(
|
|
226
|
-
(e: React.UIEvent<HTMLDivElement>) => {
|
|
227
|
-
const target = e.currentTarget;
|
|
228
|
-
const newScrollTop = target.scrollTop;
|
|
229
|
-
setScrollTop(newScrollTop);
|
|
230
|
-
onScroll?.(e);
|
|
231
|
-
// 触底检测
|
|
232
|
-
const nearEnd =
|
|
233
|
-
target.scrollHeight - target.scrollTop - target.clientHeight < 5;
|
|
234
|
-
if (onReachEnd && nearEnd && !isNearEndRef.current) {
|
|
235
|
-
isNearEndRef.current = true;
|
|
236
|
-
onReachEnd();
|
|
237
|
-
} else if (!nearEnd) {
|
|
238
|
-
isNearEndRef.current = false;
|
|
239
|
-
}
|
|
240
|
-
},
|
|
241
|
-
[onScroll, onReachEnd],
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
// 暴露方法
|
|
245
|
-
useImperativeHandle(
|
|
246
|
-
ref,
|
|
247
|
-
() => ({
|
|
248
|
-
scrollTo: (top: number) => {
|
|
249
|
-
if (!containerRef.current) return;
|
|
250
|
-
const safeTop = Math.max(0, top);
|
|
251
|
-
containerRef.current.scrollTop = safeTop;
|
|
252
|
-
setScrollTop(safeTop);
|
|
253
|
-
},
|
|
254
|
-
scrollToIndex: (
|
|
255
|
-
index: number,
|
|
256
|
-
align: "start" | "center" | "end" = "start",
|
|
257
|
-
) => {
|
|
258
|
-
if (!containerRef.current || data.length === 0) return;
|
|
259
|
-
const safeIndex = Math.min(Math.max(0, index), data.length - 1);
|
|
260
|
-
let targetTop = 0;
|
|
261
|
-
if (isFixedHeight) {
|
|
262
|
-
targetTop = safeIndex * fixedItemHeight!;
|
|
263
|
-
} else {
|
|
264
|
-
targetTop = positions.current[safeIndex]?.offset ?? 0;
|
|
265
|
-
}
|
|
266
|
-
const targetHeight = isFixedHeight
|
|
267
|
-
? fixedItemHeight!
|
|
268
|
-
: (positions.current[safeIndex]?.height ?? estimatedItemHeight);
|
|
269
|
-
if (align === "center") {
|
|
270
|
-
targetTop -= (containerHeightVal - targetHeight) / 2;
|
|
271
|
-
} else if (align === "end") {
|
|
272
|
-
targetTop -= containerHeightVal - targetHeight;
|
|
273
|
-
}
|
|
274
|
-
const safeTop = Math.max(0, targetTop);
|
|
275
|
-
containerRef.current.scrollTop = safeTop;
|
|
276
|
-
setScrollTop(safeTop);
|
|
277
|
-
},
|
|
278
|
-
getCurrentRange: () => ({ start: startIndex, end: endIndex }),
|
|
279
|
-
}),
|
|
280
|
-
[
|
|
281
|
-
startIndex,
|
|
282
|
-
endIndex,
|
|
283
|
-
containerHeightVal,
|
|
284
|
-
fixedItemHeight,
|
|
285
|
-
isFixedHeight,
|
|
286
|
-
data.length,
|
|
287
|
-
estimatedItemHeight,
|
|
288
|
-
],
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
// 获取唯一 key
|
|
292
|
-
const getKey = useCallback(
|
|
293
|
-
(item: T, idx: number): string => {
|
|
294
|
-
if (typeof itemKey === "function") return itemKey(item, idx);
|
|
295
|
-
return (item[itemKey] as any)?.toString() ?? `item_${idx}`;
|
|
296
|
-
},
|
|
297
|
-
[itemKey],
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
// 渲染可视区内的项
|
|
301
|
-
const visibleItems = useMemo(() => {
|
|
302
|
-
const items = [];
|
|
303
|
-
for (let i = startIndex; i <= endIndex && i < data.length; i++) {
|
|
304
|
-
const item = data[i];
|
|
305
|
-
if (item === undefined) continue;
|
|
306
|
-
|
|
307
|
-
const key = getKey(item, i);
|
|
308
|
-
let top = 0;
|
|
309
|
-
if (isFixedHeight) {
|
|
310
|
-
top = i * fixedItemHeight!;
|
|
311
|
-
} else {
|
|
312
|
-
top = positions.current[i]?.offset ?? 0;
|
|
313
|
-
}
|
|
314
|
-
items.push(
|
|
315
|
-
<div
|
|
316
|
-
key={key}
|
|
317
|
-
ref={(el) => {
|
|
318
|
-
if (isFixedHeight) return;
|
|
319
|
-
if (!el) {
|
|
320
|
-
const prev = itemObserversRef.current.get(key);
|
|
321
|
-
if (prev) {
|
|
322
|
-
prev.disconnect();
|
|
323
|
-
itemObserversRef.current.delete(key);
|
|
324
|
-
}
|
|
325
|
-
itemsRef.current.delete(key);
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const htmlEl = el as unknown as HTMLElement;
|
|
330
|
-
itemsRef.current.set(key, htmlEl);
|
|
331
|
-
const prevObs = itemObserversRef.current.get(key);
|
|
332
|
-
if (prevObs) prevObs.disconnect();
|
|
333
|
-
const ro = new ResizeObserver((entries) => {
|
|
334
|
-
const h = entries[0]?.contentRect.height;
|
|
335
|
-
if (h && h > 0) updateItemHeight(i, h);
|
|
336
|
-
});
|
|
337
|
-
ro.observe(el);
|
|
338
|
-
itemObserversRef.current.set(key, ro);
|
|
339
|
-
|
|
340
|
-
// 测量真实高度(初次渲染或内容变化)
|
|
341
|
-
const actualHeight = htmlEl.getBoundingClientRect().height;
|
|
342
|
-
if (
|
|
343
|
-
actualHeight &&
|
|
344
|
-
positions.current[i]?.height !== actualHeight
|
|
345
|
-
) {
|
|
346
|
-
updateItemHeight(i, actualHeight);
|
|
347
|
-
}
|
|
348
|
-
}}
|
|
349
|
-
style={{
|
|
350
|
-
position: "absolute",
|
|
351
|
-
top: 0,
|
|
352
|
-
left: 0,
|
|
353
|
-
width: "100%",
|
|
354
|
-
transform: `translateY(${top}px)`,
|
|
355
|
-
}}
|
|
356
|
-
>
|
|
357
|
-
{renderItem(item, i)}
|
|
358
|
-
</div>,
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
return items;
|
|
362
|
-
}, [
|
|
363
|
-
startIndex,
|
|
364
|
-
endIndex,
|
|
365
|
-
data,
|
|
366
|
-
getKey,
|
|
367
|
-
isFixedHeight,
|
|
368
|
-
fixedItemHeight,
|
|
369
|
-
renderItem,
|
|
370
|
-
updateItemHeight,
|
|
371
|
-
measureVersion,
|
|
372
|
-
]);
|
|
373
|
-
|
|
374
|
-
// 容器样式
|
|
375
|
-
const containerStyle: React.CSSProperties = {
|
|
376
|
-
height: containerHeight,
|
|
377
|
-
overflowY: "auto",
|
|
378
|
-
position: "relative",
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
const innerStyle: React.CSSProperties = {
|
|
382
|
-
position: "relative",
|
|
383
|
-
height: totalHeight,
|
|
384
|
-
width: "100%",
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
return (
|
|
388
|
-
// 可视区域容器
|
|
389
|
-
<div ref={containerRef} style={containerStyle} onScroll={handleScroll}>
|
|
390
|
-
{/* 容器占位,高度是总列表的高度,用于撑起容器的高度,出现滚动条*/}
|
|
391
|
-
<div style={innerStyle}>{visibleItems}</div>
|
|
392
|
-
</div>
|
|
393
|
-
);
|
|
394
|
-
},
|
|
395
|
-
);
|
package/src/index.tsx
DELETED
package/src/types.d.ts
DELETED
package/tsconfig.app.json
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "ES2020",
|
|
5
|
-
"lib": ["ES2020", "DOM"],
|
|
6
|
-
"jsx": "react-jsx",
|
|
7
|
-
"jsxImportSource": "react",
|
|
8
|
-
"moduleResolution": "bundler",
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"forceConsistentCasingInFileNames": true,
|
|
11
|
-
"strict": true,
|
|
12
|
-
"skipLibCheck": false,
|
|
13
|
-
"composite": true,
|
|
14
|
-
"declaration": true,
|
|
15
|
-
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
16
|
-
"noEmit": false,
|
|
17
|
-
"outDir": "../../dist/virtual-list"
|
|
18
|
-
},
|
|
19
|
-
"include": ["src/**/*.tsx", "src/**/*.ts"],
|
|
20
|
-
"exclude": [
|
|
21
|
-
"src/**/*.test.ts",
|
|
22
|
-
"node_modules",
|
|
23
|
-
"**/*.vue",
|
|
24
|
-
"../vue-shim.d.ts",
|
|
25
|
-
"../../vue-shim.d.ts",
|
|
26
|
-
"../../node_modules/@vue",
|
|
27
|
-
"../../node_modules/vue"
|
|
28
|
-
]
|
|
29
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "ES2020",
|
|
5
|
-
"lib": ["ES2020", "DOM"],
|
|
6
|
-
"jsx": "react-jsx",
|
|
7
|
-
"moduleResolution": "node",
|
|
8
|
-
"esModuleInterop": true,
|
|
9
|
-
"forceConsistentCasingInFileNames": true,
|
|
10
|
-
"strict": true,
|
|
11
|
-
"declaration": true,
|
|
12
|
-
"emitDeclarationOnly": true,
|
|
13
|
-
"skipLibCheck": true
|
|
14
|
-
}
|
|
15
|
-
}
|