lazy-render-virtual-scroll 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -2
- package/examples/chat-ui/Chat.jsx +0 -158
- package/examples/infinite-feed/Feed.jsx +0 -97
- package/examples/react-basic/App.jsx +0 -64
- package/rollup.config.js +0 -39
- package/src/adapters/react/LazyList.tsx +0 -92
- package/src/adapters/react/useLazyList.ts +0 -87
- package/src/core/Engine.ts +0 -134
- package/src/core/PrefetchManager.ts +0 -22
- package/src/core/RequestQueue.ts +0 -69
- package/src/core/WindowManager.ts +0 -49
- package/src/core/types.ts +0 -24
- package/src/index.ts +0 -17
- package/src/platform/browser/ScrollObserver.ts +0 -86
- package/src/utils/debounce.ts +0 -19
- package/src/utils/throttle.ts +0 -19
- package/test/engine.test.ts +0 -136
- package/test/prefetchManager.test.ts +0 -99
- package/test/reactAdapter.test.ts +0 -26
- package/test/requestQueue.test.ts +0 -88
- package/test/testRunner.ts +0 -8
- package/test/windowManager.test.ts +0 -98
- package/tsconfig.json +0 -33
package/package.json
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lazy-render-virtual-scroll",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "A framework-agnostic virtual scrolling and lazy rendering solution",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"type": "module",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
9
14
|
"scripts": {
|
|
10
15
|
"build": "rollup -c",
|
|
11
16
|
"dev": "rollup -c -w",
|
|
@@ -52,4 +57,4 @@
|
|
|
52
57
|
"dependencies": {
|
|
53
58
|
"tsx": "^4.0.0"
|
|
54
59
|
}
|
|
55
|
-
}
|
|
60
|
+
}
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import React, { useState, useRef } from 'react';
|
|
2
|
-
import { LazyList, useLazyList } from 'lazy-render';
|
|
3
|
-
|
|
4
|
-
const ChatUI = () => {
|
|
5
|
-
const [messages, setMessages] = useState([]);
|
|
6
|
-
const [inputText, setInputText] = useState('');
|
|
7
|
-
const [userId] = useState('user_' + Math.random().toString(36).substr(2, 9));
|
|
8
|
-
const messagesEndRef = useRef(null);
|
|
9
|
-
|
|
10
|
-
// Initialize with some messages
|
|
11
|
-
React.useEffect(() => {
|
|
12
|
-
const initialMessages = Array.from({ length: 100 }, (_, i) => ({
|
|
13
|
-
id: i,
|
|
14
|
-
text: `Initial message ${i} - Lorem ipsum dolor sit amet, consectetur adipiscing elit.`,
|
|
15
|
-
sender: i % 3 === 0 ? 'other' : 'self',
|
|
16
|
-
timestamp: new Date(Date.now() - (100 - i) * 60000).toLocaleTimeString(),
|
|
17
|
-
avatar: `https://i.pravatar.cc/32?img=${i % 5 + 1}`
|
|
18
|
-
}));
|
|
19
|
-
setMessages(initialMessages);
|
|
20
|
-
}, []);
|
|
21
|
-
|
|
22
|
-
const sendMessage = () => {
|
|
23
|
-
if (inputText.trim() === '') return;
|
|
24
|
-
|
|
25
|
-
const newMessage = {
|
|
26
|
-
id: messages.length,
|
|
27
|
-
text: inputText,
|
|
28
|
-
sender: 'self',
|
|
29
|
-
timestamp: new Date().toLocaleTimeString(),
|
|
30
|
-
avatar: `https://i.pravatar.cc/32?img=6`
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
setMessages(prev => [...prev, newMessage]);
|
|
34
|
-
setInputText('');
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const handleKeyPress = (e) => {
|
|
38
|
-
if (e.key === 'Enter' && !e.shiftKey) {
|
|
39
|
-
e.preventDefault();
|
|
40
|
-
sendMessage();
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
// Custom hook usage for advanced control
|
|
45
|
-
const {
|
|
46
|
-
visibleRange,
|
|
47
|
-
setContainerRef,
|
|
48
|
-
scrollToIndex
|
|
49
|
-
} = useLazyList({
|
|
50
|
-
itemHeight: 80, // Average message height
|
|
51
|
-
viewportHeight: 500,
|
|
52
|
-
bufferSize: 5,
|
|
53
|
-
overscan: 3,
|
|
54
|
-
fetchMore: async () => {
|
|
55
|
-
// In a real app, this would fetch older messages
|
|
56
|
-
return [];
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const renderMessage = (msg, index) => (
|
|
61
|
-
<div
|
|
62
|
-
key={msg.id}
|
|
63
|
-
style={{
|
|
64
|
-
minHeight: '60px',
|
|
65
|
-
borderBottom: '1px solid #eee',
|
|
66
|
-
padding: '12px 16px',
|
|
67
|
-
display: 'flex',
|
|
68
|
-
alignItems: 'flex-start',
|
|
69
|
-
backgroundColor: msg.sender === 'self' ? '#e3f2fd' : '#fff'
|
|
70
|
-
}}
|
|
71
|
-
>
|
|
72
|
-
<img
|
|
73
|
-
src={msg.avatar}
|
|
74
|
-
alt="Avatar"
|
|
75
|
-
style={{
|
|
76
|
-
width: '32px',
|
|
77
|
-
height: '32px',
|
|
78
|
-
borderRadius: '50%',
|
|
79
|
-
marginRight: '12px',
|
|
80
|
-
flexShrink: 0
|
|
81
|
-
}}
|
|
82
|
-
/>
|
|
83
|
-
<div style={{ flex: 1 }}>
|
|
84
|
-
<div style={{ fontWeight: 'bold', fontSize: '12px', marginBottom: '4px' }}>
|
|
85
|
-
{msg.sender === 'self' ? 'You' : 'Other User'} • {msg.timestamp}
|
|
86
|
-
</div>
|
|
87
|
-
<div style={{ fontSize: '14px' }}>
|
|
88
|
-
{msg.text}
|
|
89
|
-
</div>
|
|
90
|
-
</div>
|
|
91
|
-
</div>
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
return (
|
|
95
|
-
<div style={{ width: '100%', maxWidth: '600px', margin: '0 auto', height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|
96
|
-
<h1 style={{ textAlign: 'center', margin: '10px 0' }}>Chat UI Example</h1>
|
|
97
|
-
|
|
98
|
-
{/* Messages container */}
|
|
99
|
-
<div
|
|
100
|
-
ref={setContainerRef}
|
|
101
|
-
style={{
|
|
102
|
-
flex: 1,
|
|
103
|
-
overflowY: 'auto',
|
|
104
|
-
border: '1px solid #ddd',
|
|
105
|
-
borderRadius: '8px',
|
|
106
|
-
marginBottom: '10px'
|
|
107
|
-
}}
|
|
108
|
-
>
|
|
109
|
-
<div style={{ height: `${visibleRange.start * 80}px` }} />
|
|
110
|
-
|
|
111
|
-
{messages
|
|
112
|
-
.slice(visibleRange.start, visibleRange.end)
|
|
113
|
-
.map((msg, idx) => renderMessage(msg, visibleRange.start + idx))}
|
|
114
|
-
|
|
115
|
-
<div style={{ height: `${Math.max(0, (messages.length - visibleRange.end) * 80)}px` }} />
|
|
116
|
-
</div>
|
|
117
|
-
|
|
118
|
-
{/* Input area */}
|
|
119
|
-
<div style={{ display: 'flex', gap: '8px', padding: '8px' }}>
|
|
120
|
-
<textarea
|
|
121
|
-
value={inputText}
|
|
122
|
-
onChange={(e) => setInputText(e.target.value)}
|
|
123
|
-
onKeyPress={handleKeyPress}
|
|
124
|
-
placeholder="Type a message..."
|
|
125
|
-
style={{
|
|
126
|
-
flex: 1,
|
|
127
|
-
padding: '10px',
|
|
128
|
-
border: '1px solid #ddd',
|
|
129
|
-
borderRadius: '4px',
|
|
130
|
-
resize: 'none',
|
|
131
|
-
minHeight: '40px',
|
|
132
|
-
maxHeight: '100px'
|
|
133
|
-
}}
|
|
134
|
-
/>
|
|
135
|
-
<button
|
|
136
|
-
onClick={sendMessage}
|
|
137
|
-
disabled={!inputText.trim()}
|
|
138
|
-
style={{
|
|
139
|
-
padding: '10px 16px',
|
|
140
|
-
backgroundColor: inputText.trim() ? '#2196f3' : '#ccc',
|
|
141
|
-
color: 'white',
|
|
142
|
-
border: 'none',
|
|
143
|
-
borderRadius: '4px',
|
|
144
|
-
cursor: inputText.trim() ? 'pointer' : 'not-allowed'
|
|
145
|
-
}}
|
|
146
|
-
>
|
|
147
|
-
Send
|
|
148
|
-
</button>
|
|
149
|
-
</div>
|
|
150
|
-
|
|
151
|
-
<div style={{ fontSize: '12px', textAlign: 'center', color: '#666' }}>
|
|
152
|
-
Showing {visibleRange.start}-{visibleRange.end} of {messages.length} messages
|
|
153
|
-
</div>
|
|
154
|
-
</div>
|
|
155
|
-
);
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
export default ChatUI;
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { LazyList } from 'lazy-render';
|
|
3
|
-
|
|
4
|
-
const InfiniteFeed = () => {
|
|
5
|
-
const [posts, setPosts] = useState([]);
|
|
6
|
-
const [page, setPage] = useState(1);
|
|
7
|
-
const [hasMore, setHasMore] = useState(true);
|
|
8
|
-
const [loading, setLoading] = useState(false);
|
|
9
|
-
|
|
10
|
-
const fetchMorePosts = async () => {
|
|
11
|
-
if (loading || !hasMore) return;
|
|
12
|
-
|
|
13
|
-
setLoading(true);
|
|
14
|
-
|
|
15
|
-
// Simulate API call
|
|
16
|
-
try {
|
|
17
|
-
await new Promise(resolve => setTimeout(resolve, 800)); // Simulate network delay
|
|
18
|
-
|
|
19
|
-
// Generate new posts
|
|
20
|
-
const newPosts = Array.from({ length: 10 }, (_, i) => ({
|
|
21
|
-
id: (page * 10) + i,
|
|
22
|
-
title: `Post Title ${page * 10 + i}`,
|
|
23
|
-
content: `This is the content for post number ${page * 10 + i}. It contains some interesting information.`,
|
|
24
|
-
author: `Author ${(page * 10) + i % 5}`,
|
|
25
|
-
timestamp: new Date(Date.now() - Math.random() * 10000000000).toISOString(),
|
|
26
|
-
likes: Math.floor(Math.random() * 100),
|
|
27
|
-
comments: Math.floor(Math.random() * 50)
|
|
28
|
-
}));
|
|
29
|
-
|
|
30
|
-
setPosts(prev => [...prev, ...newPosts]);
|
|
31
|
-
setPage(prev => prev + 1);
|
|
32
|
-
|
|
33
|
-
// Stop after 100 posts for demo purposes
|
|
34
|
-
if (page >= 10) {
|
|
35
|
-
setHasMore(false);
|
|
36
|
-
}
|
|
37
|
-
} catch (error) {
|
|
38
|
-
console.error('Error fetching posts:', error);
|
|
39
|
-
} finally {
|
|
40
|
-
setLoading(false);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return newPosts;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const renderPost = (post, index) => (
|
|
47
|
-
<div
|
|
48
|
-
key={post.id}
|
|
49
|
-
style={{
|
|
50
|
-
height: '120px',
|
|
51
|
-
borderBottom: '1px solid #eee',
|
|
52
|
-
padding: '16px',
|
|
53
|
-
backgroundColor: '#fafafa'
|
|
54
|
-
}}
|
|
55
|
-
>
|
|
56
|
-
<h3>{post.title}</h3>
|
|
57
|
-
<p style={{ fontSize: '14px', color: '#666', margin: '4px 0' }}>
|
|
58
|
-
by {post.author} • {post.timestamp.substring(0, 10)}
|
|
59
|
-
</p>
|
|
60
|
-
<p style={{ margin: '8px 0' }}>{post.content}</p>
|
|
61
|
-
<div style={{ fontSize: '12px', color: '#888' }}>
|
|
62
|
-
{post.likes} likes • {post.comments} comments
|
|
63
|
-
</div>
|
|
64
|
-
</div>
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
return (
|
|
68
|
-
<div style={{ width: '100%', maxWidth: '600px', margin: '0 auto' }}>
|
|
69
|
-
<h1>Infinite Feed Example</h1>
|
|
70
|
-
<p>{posts.length} posts loaded</p>
|
|
71
|
-
|
|
72
|
-
<LazyList
|
|
73
|
-
items={posts}
|
|
74
|
-
itemHeight={120}
|
|
75
|
-
viewportHeight={500}
|
|
76
|
-
fetchMore={fetchMorePosts}
|
|
77
|
-
renderItem={renderPost}
|
|
78
|
-
bufferSize={3}
|
|
79
|
-
overscan={2}
|
|
80
|
-
/>
|
|
81
|
-
|
|
82
|
-
{loading && (
|
|
83
|
-
<div style={{ textAlign: 'center', padding: '20px', color: '#666' }}>
|
|
84
|
-
Loading more posts...
|
|
85
|
-
</div>
|
|
86
|
-
)}
|
|
87
|
-
|
|
88
|
-
{!hasMore && (
|
|
89
|
-
<div style={{ textAlign: 'center', padding: '20px', color: '#666' }}>
|
|
90
|
-
You've reached the end!
|
|
91
|
-
</div>
|
|
92
|
-
)}
|
|
93
|
-
</div>
|
|
94
|
-
);
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
export default InfiniteFeed;
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { LazyList } from 'lazy-render';
|
|
3
|
-
|
|
4
|
-
const App = () => {
|
|
5
|
-
// Generate 10000 sample items
|
|
6
|
-
const [items] = useState(() =>
|
|
7
|
-
Array.from({ length: 10000 }, (_, i) => ({
|
|
8
|
-
id: i,
|
|
9
|
-
text: `Item ${i}`,
|
|
10
|
-
timestamp: new Date(Date.now() - Math.random() * 10000000000).toISOString()
|
|
11
|
-
}))
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
const [loadedItems, setLoadedItems] = useState(items.slice(0, 100)); // Initially load 100 items
|
|
15
|
-
const [hasMore, setHasMore] = useState(true);
|
|
16
|
-
|
|
17
|
-
const fetchMore = async () => {
|
|
18
|
-
if (loadedItems.length >= items.length) {
|
|
19
|
-
setHasMore(false);
|
|
20
|
-
return [];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Simulate API delay
|
|
24
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
25
|
-
|
|
26
|
-
const nextBatch = items.slice(loadedItems.length, loadedItems.length + 50);
|
|
27
|
-
setLoadedItems(prev => [...prev, ...nextBatch]);
|
|
28
|
-
return nextBatch;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const renderItem = (item, index) => (
|
|
32
|
-
<div
|
|
33
|
-
key={item.id}
|
|
34
|
-
style={{
|
|
35
|
-
height: '60px',
|
|
36
|
-
borderBottom: '1px solid #eee',
|
|
37
|
-
display: 'flex',
|
|
38
|
-
alignItems: 'center',
|
|
39
|
-
paddingLeft: '16px'
|
|
40
|
-
}}
|
|
41
|
-
>
|
|
42
|
-
{item.text} - {item.timestamp.substring(0, 10)}
|
|
43
|
-
</div>
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<div style={{ width: '100%', height: '100vh' }}>
|
|
48
|
-
<h1>Basic Lazy Render Example</h1>
|
|
49
|
-
<p>Loading {loadedItems.length} of {items.length} items</p>
|
|
50
|
-
|
|
51
|
-
<LazyList
|
|
52
|
-
items={loadedItems}
|
|
53
|
-
itemHeight={60}
|
|
54
|
-
viewportHeight={400}
|
|
55
|
-
fetchMore={fetchMore}
|
|
56
|
-
renderItem={renderItem}
|
|
57
|
-
bufferSize={5}
|
|
58
|
-
overscan={2}
|
|
59
|
-
/>
|
|
60
|
-
</div>
|
|
61
|
-
);
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export default App;
|
package/rollup.config.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import resolve from '@rollup/plugin-node-resolve';
|
|
2
|
-
import typescript from '@rollup/plugin-typescript';
|
|
3
|
-
import dts from 'rollup-plugin-dts';
|
|
4
|
-
import fs from 'fs';
|
|
5
|
-
|
|
6
|
-
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
|
7
|
-
|
|
8
|
-
export default [
|
|
9
|
-
{
|
|
10
|
-
input: 'src/index.ts',
|
|
11
|
-
output: [
|
|
12
|
-
{
|
|
13
|
-
file: packageJson.main,
|
|
14
|
-
format: 'cjs',
|
|
15
|
-
sourcemap: true
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
file: packageJson.module,
|
|
19
|
-
format: 'esm',
|
|
20
|
-
sourcemap: true
|
|
21
|
-
}
|
|
22
|
-
],
|
|
23
|
-
plugins: [
|
|
24
|
-
resolve({
|
|
25
|
-
browser: true,
|
|
26
|
-
}),
|
|
27
|
-
typescript({
|
|
28
|
-
tsconfig: './tsconfig.json',
|
|
29
|
-
jsx: 'react' // Use classic JSX transform
|
|
30
|
-
})
|
|
31
|
-
],
|
|
32
|
-
external: ['react', 'react-dom']
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
input: 'src/index.ts',
|
|
36
|
-
output: [{ file: 'dist/index.d.ts', format: 'es' }],
|
|
37
|
-
plugins: [dts()],
|
|
38
|
-
}
|
|
39
|
-
];
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import React, { forwardRef } from 'react';
|
|
2
|
-
import { useLazyList } from './useLazyList';
|
|
3
|
-
import { EngineConfig, FetchMoreCallback } from '../../core/types';
|
|
4
|
-
|
|
5
|
-
interface LazyListProps extends EngineConfig {
|
|
6
|
-
fetchMore: FetchMoreCallback;
|
|
7
|
-
renderItem: (item: any, index: number) => React.ReactNode;
|
|
8
|
-
items: any[];
|
|
9
|
-
className?: string;
|
|
10
|
-
style?: React.CSSProperties;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const LazyList = forwardRef<HTMLDivElement, LazyListProps>((props, ref) => {
|
|
14
|
-
const {
|
|
15
|
-
fetchMore,
|
|
16
|
-
renderItem,
|
|
17
|
-
items,
|
|
18
|
-
itemHeight,
|
|
19
|
-
viewportHeight,
|
|
20
|
-
bufferSize,
|
|
21
|
-
className = '',
|
|
22
|
-
style = {},
|
|
23
|
-
...rest
|
|
24
|
-
} = props;
|
|
25
|
-
|
|
26
|
-
const { visibleRange, setContainerRef, isLoading } = useLazyList({
|
|
27
|
-
fetchMore,
|
|
28
|
-
itemHeight,
|
|
29
|
-
viewportHeight,
|
|
30
|
-
bufferSize,
|
|
31
|
-
...rest
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
// Calculate container height to simulate infinite scroll
|
|
35
|
-
const containerHeight = items.length * itemHeight;
|
|
36
|
-
const visibleItems = items.slice(visibleRange.start, visibleRange.end);
|
|
37
|
-
|
|
38
|
-
// Calculate top padding to maintain scroll position
|
|
39
|
-
const paddingTop = visibleRange.start * itemHeight;
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<div
|
|
43
|
-
ref={(el) => {
|
|
44
|
-
setContainerRef(el);
|
|
45
|
-
if (ref) {
|
|
46
|
-
if (typeof ref === 'function') {
|
|
47
|
-
ref(el);
|
|
48
|
-
} else {
|
|
49
|
-
ref.current = el;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}}
|
|
53
|
-
className={`lazy-list ${className}`}
|
|
54
|
-
style={{
|
|
55
|
-
height: `${viewportHeight}px`,
|
|
56
|
-
overflowY: 'auto',
|
|
57
|
-
...style
|
|
58
|
-
}}
|
|
59
|
-
{...rest}
|
|
60
|
-
>
|
|
61
|
-
{/* Top padding to maintain scroll position */}
|
|
62
|
-
<div style={{ height: `${paddingTop}px` }} />
|
|
63
|
-
|
|
64
|
-
{/* Visible items */}
|
|
65
|
-
{visibleItems.map((item, index) => (
|
|
66
|
-
<div
|
|
67
|
-
key={visibleRange.start + index}
|
|
68
|
-
style={{ height: `${itemHeight}px` }}
|
|
69
|
-
className="lazy-item"
|
|
70
|
-
>
|
|
71
|
-
{renderItem(item, visibleRange.start + index)}
|
|
72
|
-
</div>
|
|
73
|
-
))}
|
|
74
|
-
|
|
75
|
-
{/* Bottom padding */}
|
|
76
|
-
<div
|
|
77
|
-
style={{
|
|
78
|
-
height: `${Math.max(0, containerHeight - (visibleRange.end * itemHeight))}px`
|
|
79
|
-
}}
|
|
80
|
-
/>
|
|
81
|
-
|
|
82
|
-
{/* Loading indicator */}
|
|
83
|
-
{isLoading && (
|
|
84
|
-
<div className="lazy-loading">
|
|
85
|
-
Loading more items...
|
|
86
|
-
</div>
|
|
87
|
-
)}
|
|
88
|
-
</div>
|
|
89
|
-
);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
LazyList.displayName = 'LazyList';
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
|
-
import { Engine } from '../../core/Engine';
|
|
3
|
-
import { EngineConfig, VisibleRange, FetchMoreCallback } from '../../core/types';
|
|
4
|
-
|
|
5
|
-
interface LazyListConfig extends EngineConfig {
|
|
6
|
-
fetchMore: FetchMoreCallback;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const useLazyList = (config: LazyListConfig) => {
|
|
10
|
-
const { fetchMore, ...engineConfig } = config;
|
|
11
|
-
const engineRef = useRef<Engine | null>(null);
|
|
12
|
-
const containerRef = useRef<HTMLElement | null>(null);
|
|
13
|
-
|
|
14
|
-
const [visibleRange, setVisibleRange] = useState<VisibleRange>({ start: 0, end: 0 });
|
|
15
|
-
const [loadedItems, setLoadedItems] = useState<any[]>([]);
|
|
16
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
17
|
-
|
|
18
|
-
// Initialize engine
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
engineRef.current = new Engine(engineConfig);
|
|
21
|
-
engineRef.current.setFetchMoreCallback(fetchMore);
|
|
22
|
-
|
|
23
|
-
return () => {
|
|
24
|
-
if (engineRef.current) {
|
|
25
|
-
engineRef.current.cleanup();
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
}, []);
|
|
29
|
-
|
|
30
|
-
// Update engine when config changes
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
if (engineRef.current) {
|
|
33
|
-
engineRef.current.updateDimensions(
|
|
34
|
-
engineConfig.viewportHeight,
|
|
35
|
-
engineConfig.itemHeight
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
}, [engineConfig.viewportHeight, engineConfig.itemHeight]);
|
|
39
|
-
|
|
40
|
-
// Handle scroll events
|
|
41
|
-
const handleScroll = useCallback((scrollTop: number) => {
|
|
42
|
-
if (engineRef.current) {
|
|
43
|
-
engineRef.current.updateScrollPosition(scrollTop);
|
|
44
|
-
|
|
45
|
-
// Update state based on engine
|
|
46
|
-
const state = engineRef.current.getState();
|
|
47
|
-
setVisibleRange(state.visibleRange);
|
|
48
|
-
setIsLoading(state.isLoading);
|
|
49
|
-
}
|
|
50
|
-
}, []);
|
|
51
|
-
|
|
52
|
-
// Set container reference
|
|
53
|
-
const setContainerRef = (element: HTMLElement | null) => {
|
|
54
|
-
if (element) {
|
|
55
|
-
containerRef.current = element;
|
|
56
|
-
|
|
57
|
-
// Initialize scroll observer when container is available
|
|
58
|
-
if (typeof window !== 'undefined' && element) {
|
|
59
|
-
// In a real implementation, we would use ScrollObserver here
|
|
60
|
-
// For now, we'll just attach a basic scroll listener
|
|
61
|
-
const handleScrollEvent = () => {
|
|
62
|
-
handleScroll(element.scrollTop);
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
element.addEventListener('scroll', handleScrollEvent, { passive: true });
|
|
66
|
-
|
|
67
|
-
// Cleanup
|
|
68
|
-
return () => {
|
|
69
|
-
element.removeEventListener('scroll', handleScrollEvent);
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
visibleRange,
|
|
77
|
-
loadedItems,
|
|
78
|
-
isLoading,
|
|
79
|
-
setContainerRef,
|
|
80
|
-
// Helper function to trigger manual refresh
|
|
81
|
-
refresh: () => {
|
|
82
|
-
if (engineRef.current) {
|
|
83
|
-
engineRef.current.updateScrollPosition(containerRef.current?.scrollTop || 0);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
};
|
package/src/core/Engine.ts
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { EngineConfig, VisibleRange, FetchMoreCallback, EngineState } from './types';
|
|
2
|
-
import { WindowManager } from './WindowManager';
|
|
3
|
-
import { PrefetchManager } from './PrefetchManager';
|
|
4
|
-
import { RequestQueue } from './RequestQueue';
|
|
5
|
-
|
|
6
|
-
export class Engine {
|
|
7
|
-
private config: EngineConfig;
|
|
8
|
-
private windowManager: WindowManager;
|
|
9
|
-
private prefetchManager: PrefetchManager;
|
|
10
|
-
private requestQueue: RequestQueue;
|
|
11
|
-
|
|
12
|
-
private state: EngineState;
|
|
13
|
-
private fetchMoreCallback: FetchMoreCallback | null = null;
|
|
14
|
-
private totalItems: number;
|
|
15
|
-
|
|
16
|
-
constructor(config: EngineConfig) {
|
|
17
|
-
this.config = {
|
|
18
|
-
...config,
|
|
19
|
-
bufferSize: config.bufferSize || 5
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
this.windowManager = new WindowManager(
|
|
23
|
-
this.config.itemHeight,
|
|
24
|
-
this.config.viewportHeight,
|
|
25
|
-
this.config.bufferSize
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
this.prefetchManager = new PrefetchManager(this.config.bufferSize);
|
|
29
|
-
this.requestQueue = new RequestQueue(1); // Single request at a time
|
|
30
|
-
|
|
31
|
-
this.totalItems = this.config.totalItems || Number.MAX_SAFE_INTEGER;
|
|
32
|
-
|
|
33
|
-
this.state = {
|
|
34
|
-
scrollTop: 0,
|
|
35
|
-
visibleRange: { start: 0, end: 0 },
|
|
36
|
-
loadedItems: 0,
|
|
37
|
-
isLoading: false
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Update scroll position and recalculate visible range
|
|
43
|
-
*/
|
|
44
|
-
updateScrollPosition(scrollTop: number): void {
|
|
45
|
-
this.state.scrollTop = scrollTop;
|
|
46
|
-
this.state.visibleRange = this.windowManager.calculateVisibleRange(scrollTop);
|
|
47
|
-
|
|
48
|
-
// Check if we need to fetch more items
|
|
49
|
-
if (this.shouldFetchMore()) {
|
|
50
|
-
this.fetchMore();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Get the current visible range
|
|
56
|
-
*/
|
|
57
|
-
getVisibleRange(): VisibleRange {
|
|
58
|
-
return this.state.visibleRange;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Check if more items should be fetched
|
|
63
|
-
*/
|
|
64
|
-
shouldFetchMore(): boolean {
|
|
65
|
-
if (!this.fetchMoreCallback) return false;
|
|
66
|
-
if (this.state.isLoading) return false;
|
|
67
|
-
if (this.state.loadedItems >= this.totalItems) return false;
|
|
68
|
-
|
|
69
|
-
return this.prefetchManager.shouldPrefetch(
|
|
70
|
-
this.state.visibleRange.end,
|
|
71
|
-
this.state.loadedItems
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Fetch more items
|
|
77
|
-
*/
|
|
78
|
-
async fetchMore(): Promise<void> {
|
|
79
|
-
if (!this.fetchMoreCallback || this.state.isLoading) return;
|
|
80
|
-
|
|
81
|
-
this.state.isLoading = true;
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
const result = await this.requestQueue.add(this.fetchMoreCallback);
|
|
85
|
-
// Assuming the result contains new items
|
|
86
|
-
// In a real implementation, this would update the loaded items count
|
|
87
|
-
this.state.loadedItems += Array.isArray(result) ? result.length : 1;
|
|
88
|
-
} catch (error) {
|
|
89
|
-
console.error('Error fetching more items:', error);
|
|
90
|
-
} finally {
|
|
91
|
-
this.state.isLoading = false;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Set the fetchMore callback function
|
|
97
|
-
*/
|
|
98
|
-
setFetchMoreCallback(callback: FetchMoreCallback): void {
|
|
99
|
-
this.fetchMoreCallback = callback;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Update total items count
|
|
104
|
-
*/
|
|
105
|
-
updateTotalItems(count: number): void {
|
|
106
|
-
this.totalItems = count;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Get current engine state
|
|
111
|
-
*/
|
|
112
|
-
getState(): EngineState {
|
|
113
|
-
return { ...this.state };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Update viewport dimensions
|
|
118
|
-
*/
|
|
119
|
-
updateDimensions(viewportHeight: number, itemHeight: number): void {
|
|
120
|
-
this.windowManager.updateViewportHeight(viewportHeight);
|
|
121
|
-
this.windowManager.updateItemHeight(itemHeight);
|
|
122
|
-
|
|
123
|
-
// Recalculate visible range with new dimensions
|
|
124
|
-
this.state.visibleRange = this.windowManager.calculateVisibleRange(this.state.scrollTop);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Cleanup resources
|
|
129
|
-
*/
|
|
130
|
-
cleanup(): void {
|
|
131
|
-
this.requestQueue.clear();
|
|
132
|
-
this.fetchMoreCallback = null;
|
|
133
|
-
}
|
|
134
|
-
}
|