@@ -9,68 +9,94 @@ export async function useListBox(config) {
99 const itemBuffer = useItemBuffer ( config ) ;
1010 const listContainer = document . getElementById ( "listBox" ) ;
1111 const list = listContainer . querySelector ( "#listItems" ) ;
12- const spinner = document . getElementById ( "spinner" ) ; // Spinner element
12+ const spinner = document . getElementById ( "spinner" ) ;
1313 const { itemHeight, itemsPerPage } = config ;
14- let eol = false ;
15- let totalItems = 0 ;
14+
1615 let items = [ ] ;
1716 let eofIndex = Number . MAX_SAFE_INTEGER ;
17+ let isLoading = false ;
18+ let scrollTimeout = null ;
19+
20+ // Add intersection observer for infinite scroll
21+ const observerOptions = {
22+ root : listContainer ,
23+ rootMargin : '100px' ,
24+ threshold : 0.1
25+ } ;
26+
27+ const loadMoreCallback = ( entries ) => {
28+ const target = entries [ 0 ] ;
29+ if ( target . isIntersecting && ! isLoading ) {
30+ const scrollTop = Math . round ( listContainer . scrollTop / itemHeight ) ;
31+ handleScroll ( scrollTop ) ;
32+ }
33+ } ;
34+
35+ const observer = new IntersectionObserver ( loadMoreCallback , observerOptions ) ;
1836
19- let isRendering = false ;
20- let pendingRender = null ;
37+ // Create sentinel element for infinite scroll
38+ const sentinel = document . createElement ( 'div' ) ;
39+ sentinel . className = 'scroll-sentinel' ;
40+ sentinel . style . height = '1px' ;
41+ list . appendChild ( sentinel ) ;
42+ observer . observe ( sentinel ) ;
2143
2244 listContainer . style . height = `${ itemHeight * itemsPerPage } px` ;
2345
24- function debounce ( func , wait ) {
25- let timeout ;
26- return function executedFunction ( ...args ) {
27- const later = ( ) => {
28- clearTimeout ( timeout ) ;
29- func ( ...args ) ;
30- } ;
31- clearTimeout ( timeout ) ;
32- timeout = setTimeout ( later , wait ) ;
46+ function throttleScroll ( callback ) {
47+ return function ( ) {
48+ if ( scrollTimeout ) {
49+ return ;
50+ }
51+
52+ scrollTimeout = requestAnimationFrame ( ( ) => {
53+ const scrollTop = Math . round ( listContainer . scrollTop / itemHeight ) ;
54+ callback ( scrollTop ) ;
55+ scrollTimeout = null ;
56+ } ) ;
3357 } ;
3458 }
3559
36- const debouncedHandleScroll = debounce ( handleScroll , 100 ) ;
60+ const throttledHandleScroll = throttleScroll ( handleScroll ) ;
3761
3862 if ( listContainer . _scrollHandler ) {
3963 listContainer . removeEventListener ( "scroll" , listContainer . _scrollHandler ) ;
4064 }
4165
42- listContainer . _scrollHandler = debouncedHandleScroll ;
66+ listContainer . _scrollHandler = throttledHandleScroll ;
4367 listContainer . addEventListener ( "scroll" , listContainer . _scrollHandler ) ;
4468
4569 function showSpinner ( ) {
46- spinner . classList . remove ( "hidden" ) ;
70+ if ( ! spinner . classList . contains ( 'visible' ) ) {
71+ spinner . classList . remove ( "hidden" ) ;
72+ spinner . classList . add ( "visible" ) ;
73+ }
4774 }
4875
4976 function hideSpinner ( ) {
50- spinner . classList . add ( "hidden" ) ;
77+ if ( ! spinner . classList . contains ( 'hidden' ) ) {
78+ spinner . classList . add ( "hidden" ) ;
79+ spinner . classList . remove ( "visible" ) ;
80+ }
5181 }
5282
53- hideSpinner ( ) ;
54-
55- // Initialize by getting items and rendering them
56- await getItems ( 0 , config . itemsPerPage )
57- . then ( itemz => {
58- logger . info ( "Initial items" , itemz ) ;
59- items = itemz ;
60- return getItemCount ( ) ;
61- } )
62- . then ( async count => {
63- totalItems = count ;
64- logger . info ( `Set list height to ${ totalItems * itemHeight } px` ) ;
65- await renderItems ( 0 ) ;
66- } ) ;
83+ // Initialize
84+ await initializeList ( ) ;
6785
68- listContainer . scrollTop = 0 ;
86+ async function initializeList ( ) {
87+ try {
88+ const initialItems = await getItems ( 0 , config . itemsPerPage ) ;
89+ items = initialItems ;
90+ totalItems = await getItemCount ( ) ;
91+ await renderItems ( 0 ) ;
92+ listContainer . scrollTop = 0 ;
93+ } catch ( error ) {
94+ logger . error ( "Error initializing list:" , error ) ;
95+ }
96+ }
6997
7098 async function getItems ( startIndex , count ) {
71- logger . debug (
72- `Getting items from ${ startIndex } to ${ startIndex + count - 1 } `
73- ) ;
99+ logger . debug ( `Getting items from ${ startIndex } to ${ startIndex + count - 1 } ` ) ;
74100 return await itemBuffer . getItems ( startIndex , count ) ;
75101 }
76102
@@ -80,84 +106,82 @@ export async function useListBox(config) {
80106 return count ;
81107 }
82108
83- async function handleScroll ( ) {
84- let scrollTop = Math . round ( listContainer . scrollTop / itemHeight ) ;
109+ async function handleScroll ( scrollTop ) {
110+ if ( isLoading ) return ;
85111
86- while ( true ) {
112+ try {
113+ isLoading = true ;
87114 await renderItems ( scrollTop ) ;
88- if ( ! pendingRender ) {
89- break ;
90- }
91- scrollTop = pendingRender ;
92- }
93- if ( scrollTop >= eofIndex - itemsPerPage ) {
94- list . style . marginTop = `${ ( eofIndex - itemsPerPage ) * itemHeight } px` ;
95115
96- listContainer . scrollTop = Math . round (
97- ( eofIndex - itemsPerPage ) * itemHeight
98- ) ;
116+ // Update sentinel position
117+ sentinel . style . transform = `translateY(${ ( scrollTop + itemsPerPage ) * itemHeight } px)` ;
118+
119+ if ( scrollTop >= eofIndex - itemsPerPage ) {
120+ list . style . transform = `translateY(${ ( eofIndex - itemsPerPage ) * itemHeight } px)` ;
121+ listContainer . scrollTop = ( eofIndex - itemsPerPage ) * itemHeight ;
122+ }
123+ } finally {
124+ isLoading = false ;
99125 }
100126 }
101127
102128 async function renderItems ( startIndex ) {
129+ if ( startIndex + itemsPerPage > eofIndex ) {
130+ logger . debug ( "Reached end of list. Skipping render." ) ;
131+ return ;
132+ }
133+
134+ showSpinner ( ) ;
135+
103136 try {
104- return new Promise ( async resolve => {
105- if ( startIndex + itemsPerPage > eofIndex ) {
106- logger . debug ( "Reached end of list. Skipping render." ) ;
107- resolve ( ) ;
108- }
109- items = [ ] ;
110- showSpinner ( ) ;
111- items = await getItems ( startIndex , itemsPerPage ) ;
112- if ( items . length === 0 ) {
113- const n = await getItemCount ( ) ;
114- eofIndex = startIndex = n ;
115- list . style . marginTop = `${ ( startIndex + 1 ) * itemHeight } px` ;
116- listContainer . scrollTop = ( n - itemsPerPage ) * itemHeight ;
117- resolve ( ) ;
118- }
119- logger . info ( "Rendering items" , items ) ;
120- list . innerHTML = "" ;
121- // debugger;
122- // list.style.transform = `translateY(${startIndex * itemHeight}px)`;
123- list . style . marginTop = `${ startIndex * itemHeight } px` ;
124- items . forEach ( item => {
125- const div = document . createElement ( "div" ) ;
126- div . className = "list-item" ;
127- div . textContent = item . text ;
128- list . appendChild ( div ) ;
129- } ) ;
130- hideSpinner ( ) ;
131- resolve ( ) ;
137+ // Use DocumentFragment for better performance
138+ const fragment = document . createDocumentFragment ( ) ;
139+
140+ // Fetch next batch of items
141+ const newItems = await getItems ( startIndex , itemsPerPage ) ;
142+
143+ if ( newItems . length === 0 ) {
144+ const n = await getItemCount ( ) ;
145+ eofIndex = startIndex = n ;
146+ list . style . transform = `translateY(${ ( startIndex + 1 ) * itemHeight } px)` ;
147+ listContainer . scrollTop = ( n - itemsPerPage ) * itemHeight ;
148+ return ;
149+ }
150+
151+ items = newItems ;
152+
153+ // Clear existing items
154+ list . innerHTML = '' ;
155+
156+ // Create and append new items to fragment
157+ items . forEach ( item => {
158+ const div = document . createElement ( "div" ) ;
159+ div . className = "list-item" ;
160+ div . textContent = item . text ;
161+ fragment . appendChild ( div ) ;
132162 } ) ;
163+
164+ // Single DOM operation to append all items
165+ list . appendChild ( fragment ) ;
166+
167+ // Use transform instead of margin for better performance
168+ list . style . transform = `translateY(${ startIndex * itemHeight } px)` ;
169+
133170 } catch ( error ) {
134171 logger . error ( "Error during rendering:" , error ) ;
135172 } finally {
136- isRendering = false ;
173+ hideSpinner ( ) ;
137174 }
138175 }
139- }
140-
141- document . addEventListener ( "DOMContentLoaded" , ( ) => {
142- // Function to log the position of the listBox and move the spinner
143- function updateSpinnerPosition ( ) {
144- const listBox = document . getElementById ( "listBox" ) ;
145- const spinner = document . getElementById ( "spinner" ) ;
146176
147- if ( listBox && spinner ) {
148- const rect = listBox . getBoundingClientRect ( ) ;
149-
150- // Move spinner to the upper left corner of the listBox
151- spinner . style . top = `${ rect . top + 50 } px` ;
152- spinner . style . left = `${ rect . left + 100 } px` ;
153- } else {
154- console . error ( "listBox or spinner element not found" ) ;
177+ // Cleanup function
178+ return ( ) => {
179+ observer . disconnect ( ) ;
180+ if ( listContainer . _scrollHandler ) {
181+ listContainer . removeEventListener ( "scroll" , listContainer . _scrollHandler ) ;
155182 }
156- }
157-
158- // Add event listener for window resize
159- window . addEventListener ( "resize" , updateSpinnerPosition ) ;
160-
161- // Initial update to position spinner on page load
162- updateSpinnerPosition ( ) ;
163- } ) ;
183+ if ( scrollTimeout ) {
184+ cancelAnimationFrame ( scrollTimeout ) ;
185+ }
186+ } ;
187+ }
0 commit comments