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 CHANGED
@@ -1,11 +1,16 @@
1
1
  {
2
2
  "name": "lazy-render-virtual-scroll",
3
- "version": "1.0.1",
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
- };
@@ -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
- }