@@ -9,22 +9,34 @@ import React, {
99} from 'react' ;
1010import PropTypes from 'prop-types' ;
1111
12- import { slideItems } from './helpers/collections' ;
12+ import { slideItems , Page } from './helpers/collections' ;
1313
1414import './base.css' ;
1515
1616import { LazyPaginatedCollection } from './helpers/LazyPaginatedCollection' ;
1717import { Style , DataSource } from './helpers/types' ;
1818import { Container , Row , Col } from 'react-bootstrap' ;
1919
20+ /**
21+ * Represent the rectangular.
22+ */
23+ interface Rect {
24+ x : number ;
25+ y : number ;
26+ height : number ;
27+ width : number ;
28+ }
29+
2030interface State < Type > {
21- scrollTop : number ,
22- itemHeight : number ,
23- itemCount : number ,
24- items : Array < Type > ,
25- offset : number ,
31+ ready : boolean ;
32+ scrollTop : number ;
33+ itemHeight : number ;
34+ itemCount : number ;
35+ page : Page < Type > ;
36+ offset : number ;
2637 selected : number ;
2738 hovered : number ;
39+ rect : Rect ;
2840}
2941
3042interface Action < Type > {
@@ -39,6 +51,31 @@ interface Args<Type> {
3951 style ?: Style ;
4052}
4153
54+ function get_initial_state < T > ( ) : State < T > {
55+ return {
56+ ready : false ,
57+ scrollTop : 0 ,
58+ itemHeight : 0 ,
59+ itemCount : 0 ,
60+ page : {
61+ items : [ ] ,
62+ offset : 0 ,
63+ } ,
64+ offset : 0 ,
65+ selected : - 1 ,
66+ hovered : - 1 ,
67+ rect : {
68+ x : 0 ,
69+ y : 0 ,
70+ height : 0 ,
71+ width : 0 ,
72+ }
73+ }
74+ }
75+ function calculatePageCount ( pageHeight : number , itemHeight : number ) {
76+ return 2 * Math . floor ( pageHeight / itemHeight ) ;
77+ }
78+
4279/**
4380 * Reducer function for managing state changes.
4481 *
@@ -50,13 +87,9 @@ interface Args<Type> {
5087function reducer < Type > ( state : State < Type > , action : Action < Type > ) : State < Type > {
5188 switch ( action . type ) {
5289 case 'scroll' :
53- return { ...state , ...action . data } ;
5490 case 'render' :
55- return { ...state , ...action . data } ;
5691 case 'loaded' :
57- return { ...state , ...action . data } ;
5892 case 'click' :
59- return { ...state , ...action . data } ;
6093 case 'hover' :
6194 return { ...state , ...action . data } ;
6295 default :
@@ -79,16 +112,6 @@ function reducer<Type>(state: State<Type>, action: Action<Type>): State<Type> {
79112 * @property {DataSource } fetcher A datasource to fetch the data.
80113 */
81114
82- /**
83- * Represent the rectangular.
84- */
85- interface Rect {
86- x : number ;
87- y : number ;
88- height : number ;
89- width : number ;
90- }
91-
92115/**
93116 * @description VirtualTable component.
94117 *
@@ -99,89 +122,9 @@ interface Rect {
99122 */
100123export default function VirtualTable < Type > ( { height, renderer, fetcher, style } : Args < Type > ) : JSX . Element {
101124 const ref = useRef ( null ) ;
125+ const invisible = useRef ( null ) ;
102126 const [ collection , setCollection ] = useState < LazyPaginatedCollection < Type > > ( ( ) => new LazyPaginatedCollection < Type > ( 1 , fetcher ) ) ;
103- const [ rect , setRect ] = useState < Rect > ( {
104- x : 0 ,
105- y : 0 ,
106- height : 0 ,
107- width : 0 ,
108- } ) ;
109-
110- useEffect ( ( ) => {
111- setCollection ( new LazyPaginatedCollection < Type > ( collection . pageSize ( ) ? collection . pageSize ( ) : 1 , fetcher ) ) ;
112- } , [ fetcher ] ) ;
113-
114- const [ state , dispatch ] = useReducer ( reducer < Type > , {
115- scrollTop : 0 ,
116- itemHeight : 0 ,
117- itemCount : 0 ,
118- items : [ ] ,
119- offset : 0 ,
120- selected : - 1 ,
121- hovered : - 1 ,
122- } ) ;
123-
124- const [ currentOffset , setCurrentOffset ] = useState ( 0 ) ;
125-
126- const calculatePageCount = ( ) => 2 * Math . floor ( height / state . itemHeight ) ;
127-
128- useEffect ( ( ) => {
129- const handler = ( ) => {
130- if ( ref && ref . current ) {
131- setRect ( ref . current . getBoundingClientRect ( ) ) ;
132- }
133- } ;
134- window . addEventListener ( 'resize' , handler ) ;
135- return function cleanup ( ) {
136- window . removeEventListener ( 'resize' , handler , true ) ;
137- }
138- } , [ ] ) ;
139-
140- useEffect ( ( ) => {
141- if ( collection ) {
142- collection . slice ( 0 , collection . pageSize ( ) ) . then ( ( result ) => {
143- dispatch ( {
144- type : 'loaded' ,
145- data : {
146- scrollTop : 0 ,
147- itemHeight : 0 ,
148- items : [ ] ,
149- offset : 0 ,
150- selected : - 1 ,
151- hovered : - 1 ,
152- ...result ,
153- itemCount : collection . count ( ) ,
154- } ,
155- } ) ;
156- } ) ;
157- }
158- } , [ collection ] ) ;
159-
160- useEffect ( ( ) => {
161- if ( state . itemHeight ) {
162- const offset = Math . floor ( state . scrollTop / state . itemHeight ) ;
163- const c = calculatePageCount ( ) ;
164- if ( c !== collection . pageSize ( ) ) {
165- setCurrentOffset ( 0 ) ;
166- setCollection ( new LazyPaginatedCollection < Type > ( c , fetcher ) ) ;
167- } else {
168- setCurrentOffset ( offset ) ;
169- collection . slice ( offset , collection . pageSize ( ) ) . then ( ( result ) => {
170- if ( currentOffset !== result . offset ) {
171- dispatch ( {
172- type : 'loaded' ,
173- data : {
174- ...result ,
175- itemCount : collection . count ( ) ,
176- } ,
177- } ) ;
178- }
179- } ) ;
180- }
181- }
182- } , [
183- state ,
184- ] ) ;
127+ const [ state , dispatch ] = useReducer ( reducer < Type > , get_initial_state < Type > ( ) ) ;
185128
186129 const generate = ( offset : number , d : Array < Type > ) => {
187130 const ret = [ ] ;
@@ -201,26 +144,118 @@ export default function VirtualTable<Type>({ height, renderer, fetcher, style }:
201144 return ret ;
202145 } ;
203146
147+
148+ // A callback to update the table view in case of resize event.
149+ const handler = ( ) => {
150+ let itemHeight = state . itemHeight ;
151+ let rect = state . rect ;
152+ if ( invisible && invisible . current ) {
153+ itemHeight = invisible . current . clientHeight ;
154+ }
155+ if ( ref && ref . current ) {
156+ rect = ref . current . getBoundingClientRect ( ) ;
157+ }
158+
159+ // Update the size of the widget and the size of the items
160+ dispatch ( {
161+ type : 'render' ,
162+ data : {
163+ rect,
164+ itemHeight,
165+ scrollTop : 0 ,
166+ selected : - 1 ,
167+ hovered : - 1 ,
168+ page : {
169+ items : [ ] ,
170+ offset : 0 ,
171+ }
172+ } ,
173+ } ) ;
174+
175+ // If the item's height is already known, then update the lazy collection
176+ // and re-fetch the items.
177+ if ( itemHeight ) {
178+ const new_collection = new LazyPaginatedCollection < Type > ( calculatePageCount ( rect . height , itemHeight ) , fetcher ) ;
179+ new_collection . slice ( 0 , new_collection . pageSize ( ) ) . then ( ( result ) => {
180+ dispatch ( {
181+ type : 'loaded' ,
182+ data : {
183+ page : result ,
184+ itemCount : new_collection . count ( ) ,
185+ } ,
186+ } ) ;
187+ setCollection ( new_collection ) ;
188+ } ) ;
189+ }
190+ } ;
191+
192+ // Effect that updates the lazy collection in case fetcher gets updated
204193 useEffect ( ( ) => {
205- if ( ref . current ) {
206- ref . current . scrollTop = state . scrollTop % state . itemHeight ;
207- if ( ref . current . children && ref . current . children . length ) {
208- if ( ref . current . children [ 0 ] . clientHeight !== state . itemHeight ) {
209- setRect ( ref . current . getBoundingClientRect ( ) ) ;
194+ setCollection ( new LazyPaginatedCollection < Type > ( collection . pageSize ( ) ? collection . pageSize ( ) : 1 , fetcher ) ) ;
195+ } , [ fetcher ] ) ;
196+
197+ // Effect to fetch the first item (to draw a fake item to get the true size if the item)
198+ // and the total number of items.
199+ useEffect ( ( ) => {
200+ collection . slice ( 0 , collection . pageSize ( ) ) . then ( ( result ) => {
201+ dispatch ( {
202+ type : 'loaded' ,
203+ data : {
204+ ready : true ,
205+ page : result ,
206+ itemCount : collection . count ( ) ,
207+ } ,
208+ } ) ;
209+ } ) ;
210+
211+ window . addEventListener ( 'resize' , handler ) ;
212+ return function cleanup ( ) {
213+ window . removeEventListener ( 'resize' , handler , true ) ;
214+ }
215+ } , [ ] ) ;
216+
217+ // Effect to run on all state updates.
218+ useEffect ( ( ) => {
219+ if ( state . ready ) {
220+ if ( state . itemHeight ) {
221+ const offset = Math . floor ( state . scrollTop / state . itemHeight ) ;
222+ const c = calculatePageCount ( height , state . itemHeight ) ;
223+ if ( c === collection . pageSize ( ) && state . offset !== offset ) {
224+ // Update the offset first and then start fetching the necessary items.
225+ // This ensures a non-interruptive user experience, where all the
226+ // required data is already available.
210227 dispatch ( {
211- type : 'render ' ,
228+ type : 'loaded ' ,
212229 data : {
213- itemHeight : ref . current . children [ 0 ] . clientHeight ,
230+ offset ,
214231 } ,
215232 } ) ;
233+ collection . slice ( offset , collection . pageSize ( ) ) . then ( ( result ) => {
234+ if ( state . offset !== result . offset ) {
235+ dispatch ( {
236+ type : 'loaded' ,
237+ data : {
238+ page : result ,
239+ itemCount : collection . count ( ) ,
240+ } ,
241+ } ) ;
242+ }
243+ } ) ;
216244 }
245+ } else {
246+ handler ( ) ;
217247 }
218248 }
249+ } , [ state ] ) ;
250+
251+ // Effect to run on each render to make sure that the scrolltop of
252+ // the item container is up-to-date.
253+ useEffect ( ( ) => {
254+ if ( ref . current ) {
255+ ref . current . scrollTop = state . scrollTop % state . itemHeight ;
256+ }
219257 } ) ;
220258
221- if ( state . items . length === 0 ) {
222- return < div /> ;
223- }
224259
225260 return (
226261 < Container >
@@ -233,18 +268,23 @@ export default function VirtualTable<Type>({ height, renderer, fetcher, style }:
233268 height,
234269 } }
235270 >
236- { generate ( currentOffset , slideItems ( currentOffset , {
237- items : state . items ,
238- offset : state . offset ,
239- } ) ) }
271+ { state . ready && state . itemHeight === 0 &&
272+ < div ref = { invisible } style = { {
273+ 'visibility' : 'hidden' ,
274+ position : 'absolute' ,
275+ pointerEvents : 'none'
276+ } } >
277+ { renderer ( state . page . items [ 0 ] , '' ) }
278+ </ div > }
279+ { state . itemHeight !== 0 && generate ( state . offset , slideItems ( state . offset , state . page ) ) }
240280 </ div >
241281 < div
242282 className = 'overflow-scroll position-absolute'
243283 style = { {
244- top : rect . y ,
245- left : rect . x ,
246- width : rect . width ,
247- height : rect . height ,
284+ top : state . rect . y ,
285+ left : state . rect . x ,
286+ width : state . rect . width ,
287+ height : state . rect . height ,
248288 } }
249289 onMouseMove = { ( e ) => {
250290 const index = Math . floor ( ( e . clientY + state . scrollTop - ref . current . getBoundingClientRect ( ) . top ) / state . itemHeight ) ;
0 commit comments