1
1
import Component , { IComponentOptions } from '../share/Component'
2
2
import $ from 'licia/$'
3
- import types from 'licia/types'
4
3
import throttle from 'licia/throttle'
5
4
import isHidden from 'licia/isHidden'
6
5
import now from 'licia/now'
6
+ import ResizeSensor from 'licia/ResizeSensor'
7
+ import isEmpty from 'licia/isEmpty'
8
+ import unique from 'licia/unique'
9
+ import map from 'licia/map'
10
+ import debounce from 'licia/debounce'
11
+ import each from 'licia/each'
7
12
8
13
/** IOptions */
9
14
export interface IOptions extends IComponentOptions {
10
15
/** Auto scroll if at bottom. */
11
16
autoScroll ?: boolean
12
17
}
13
18
14
- interface IItem {
15
- el : HTMLElement
16
- height : number
17
- width : number
18
- }
19
-
20
19
/**
21
20
* Vertical list with virtual scrolling.
22
21
*
@@ -27,8 +26,7 @@ interface IItem {
27
26
* virtualList.append(document.createElement('div'))
28
27
*/
29
28
export default class VirtualList extends Component < IOptions > {
30
- render : types . AnyFn
31
- private items : IItem [ ] = [ ]
29
+ private items : Item [ ] = [ ]
32
30
private $el : $ . $
33
31
private el : HTMLElement
34
32
private $fakeEl : $ . $
@@ -45,6 +43,10 @@ export default class VirtualList extends Component<IOptions> {
45
43
private maxSpeedTolerance = 2000
46
44
private minSpeedTolerance = 100
47
45
private isAtBottom = true
46
+ private updateTimer : NodeJS . Timeout | null = null
47
+ private updateItems : Item [ ] = [ ]
48
+ private resizeSensor : ResizeSensor
49
+ private scrollTimer : NodeJS . Timeout | null = null
48
50
constructor ( container : HTMLElement , options : IOptions = { } ) {
49
51
super ( container , { compName : 'virtual-list' } , options )
50
52
@@ -61,21 +63,63 @@ export default class VirtualList extends Component<IOptions> {
61
63
this . $space = this . find ( '.items-space' )
62
64
this . space = this . $space . get ( 0 ) as HTMLElement
63
65
64
- this . render = throttle ( ( options : any ) => this . _render ( options ) , 16 )
66
+ this . resizeSensor = new ResizeSensor ( this . space )
65
67
66
68
this . bindEvent ( )
67
69
}
68
70
clear ( ) {
69
71
this . items = [ ]
70
72
this . render ( )
71
73
}
72
- append ( item : HTMLElement ) {
73
- this . items . push ( {
74
- el : item ,
75
- height : 0 ,
76
- width : 0 ,
77
- } )
74
+ append ( el : HTMLElement ) {
75
+ const item = new Item ( el , this . el )
76
+ this . items . push ( item )
77
+ this . updateSize ( item )
78
+ }
79
+ setItems ( els : HTMLElement [ ] ) {
80
+ each ( this . items , ( item ) => item . destroy ( ) )
81
+ this . items = map ( els , ( el ) => new Item ( el , this . el ) )
82
+ this . updateItems = [ ]
83
+ this . updateAllSize ( )
84
+ }
85
+ private updateAllSize = debounce ( ( ) => {
86
+ this . updateItems . push ( ...this . items )
87
+ this . updateItems = unique ( this . updateItems )
88
+ if ( ! this . updateTimer ) {
89
+ this . _updateSize ( )
90
+ }
91
+ } , 1000 )
92
+ private updateSize ( item : Item ) {
93
+ this . updateItems . push ( item )
94
+ if ( ! this . updateTimer ) {
95
+ this . _updateSize ( )
96
+ }
97
+ }
98
+ private _updateSize = ( ) => {
99
+ const items = this . updateItems . splice ( 0 , 1000 )
100
+ if ( isEmpty ( items ) ) {
101
+ return
102
+ }
103
+
104
+ const len = items . length
105
+ const { fakeEl } = this
106
+ const fakeFrag = document . createDocumentFragment ( )
107
+ for ( let i = 0 ; i < len ; i ++ ) {
108
+ fakeFrag . appendChild ( items [ i ] . el )
109
+ }
110
+ fakeEl . appendChild ( fakeFrag )
111
+ for ( let i = 0 ; i < len ; i ++ ) {
112
+ items [ i ] . updateSize ( )
113
+ }
114
+ fakeEl . textContent = ''
115
+
78
116
this . render ( )
117
+
118
+ if ( ! isEmpty ( this . updateItems ) ) {
119
+ this . updateTimer = setTimeout ( ( ) => this . _updateSize ( ) , 100 )
120
+ } else {
121
+ this . updateTimer = null
122
+ }
79
123
}
80
124
private initTpl ( ) {
81
125
this . $container . html (
@@ -97,77 +141,64 @@ export default class VirtualList extends Component<IOptions> {
97
141
this . space . style . height = height + 'px'
98
142
}
99
143
private bindEvent ( ) {
144
+ this . resizeSensor . addListener (
145
+ throttle ( ( ) => {
146
+ this . updateAllSize ( )
147
+ } , 100 )
148
+ )
100
149
this . $container . on ( 'scroll' , this . onScroll )
101
150
}
102
- private _render ( { topTolerance = 500 , bottomTolerance = 500 } = { } ) {
103
- const { el, container, space } = this
104
- if ( isHidden ( container ) ) return
105
- const { scrollTop, offsetHeight } = container as HTMLElement
106
- const containerWidth = space . getBoundingClientRect ( ) . width
107
- const top = scrollTop - topTolerance
108
- const bottom = scrollTop + offsetHeight + bottomTolerance
109
-
110
- const { items } = this
151
+ private render = throttle (
152
+ ( { topTolerance = 500 , bottomTolerance = 500 } = { } ) => {
153
+ const { el, container } = this
154
+ if ( isHidden ( container ) ) {
155
+ return
156
+ }
111
157
112
- let topSpaceHeight = 0
113
- let bottomSpaceHeight = 0
114
- let currentHeight = 0
158
+ const { scrollTop , offsetHeight } = container as HTMLElement
159
+ const top = scrollTop - topTolerance
160
+ const bottom = scrollTop + offsetHeight + bottomTolerance
115
161
116
- const len = items . length
162
+ const { items } = this
117
163
118
- const { fakeEl } = this
119
- const fakeFrag = document . createDocumentFragment ( )
120
- const updateItems = [ ]
121
- for ( let i = 0 ; i < len ; i ++ ) {
122
- const item = items [ i ]
123
- const { width, height } = item
124
- if ( height === 0 || width !== containerWidth ) {
125
- fakeFrag . appendChild ( item . el )
126
- updateItems . push ( item )
127
- }
128
- }
129
- if ( updateItems . length > 0 ) {
130
- fakeEl . appendChild ( fakeFrag )
131
- for ( let i = 0 , len = updateItems . length ; i < len ; i ++ ) {
132
- this . updateItemSize ( updateItems [ i ] )
133
- }
134
- fakeEl . textContent = ''
135
- }
164
+ let topSpaceHeight = 0
165
+ let bottomSpaceHeight = 0
166
+ let currentHeight = 0
136
167
137
- const frag = document . createDocumentFragment ( )
138
- for ( let i = 0 ; i < len ; i ++ ) {
139
- const item = items [ i ]
140
- const { el, height } = item
141
-
142
- if ( currentHeight > bottom ) {
143
- bottomSpaceHeight += height
144
- } else if ( currentHeight + height > top ) {
145
- frag . appendChild ( el )
146
- } else if ( currentHeight < top ) {
147
- topSpaceHeight += height
148
- }
168
+ const len = items . length
149
169
150
- currentHeight += height
151
- }
170
+ const frag = document . createDocumentFragment ( )
171
+ for ( let i = 0 ; i < len ; i ++ ) {
172
+ const item = items [ i ]
173
+ const { el, height } = item
152
174
153
- this . updateSpace ( currentHeight )
154
- this . updateTopSpace ( topSpaceHeight )
155
- this . updateBottomSpace ( bottomSpaceHeight )
175
+ if ( currentHeight > bottom ) {
176
+ bottomSpaceHeight += height
177
+ } else if ( currentHeight + height > top ) {
178
+ frag . appendChild ( el )
179
+ } else if ( currentHeight < top ) {
180
+ topSpaceHeight += height
181
+ }
156
182
157
- while ( el . firstChild ) {
158
- if ( el . lastChild ) {
159
- el . removeChild ( el . lastChild )
183
+ currentHeight += height
160
184
}
161
- }
162
- el . appendChild ( frag )
163
185
164
- if ( this . options . autoScroll ) {
165
- const { scrollHeight } = container
166
- if ( this . isAtBottom && scrollTop <= scrollHeight - offsetHeight ) {
167
- container . scrollTop = 10000000
186
+ this . updateSpace ( currentHeight )
187
+ this . updateTopSpace ( topSpaceHeight )
188
+ this . updateBottomSpace ( bottomSpaceHeight )
189
+
190
+ el . textContent = ''
191
+ el . appendChild ( frag )
192
+
193
+ if ( this . options . autoScroll ) {
194
+ const { scrollHeight } = container
195
+ if ( this . isAtBottom && scrollTop <= scrollHeight - offsetHeight ) {
196
+ container . scrollTop = 10000000
197
+ }
168
198
}
169
- }
170
- }
199
+ } ,
200
+ 16
201
+ )
171
202
private onScroll = ( ) => {
172
203
const { scrollHeight, offsetHeight, scrollTop } = this
173
204
. container as HTMLElement
@@ -225,14 +256,39 @@ export default class VirtualList extends Component<IOptions> {
225
256
topTolerance : topTolerance * 2 ,
226
257
bottomTolerance : bottomTolerance * 2 ,
227
258
} )
228
- }
229
- private updateItemSize ( item : IItem ) {
230
- const { width, height } = item . el . getBoundingClientRect ( )
231
- if ( item . height !== height ) {
232
- item . height = height
233
- }
234
- if ( item . width !== width ) {
235
- item . width = width
259
+
260
+ if ( this . scrollTimer ) {
261
+ clearTimeout ( this . scrollTimer )
236
262
}
263
+ this . scrollTimer = setTimeout ( ( ) => {
264
+ this . render ( )
265
+ } , 100 )
266
+ }
267
+ }
268
+
269
+ class Item {
270
+ el : HTMLElement
271
+ width : number
272
+ height : number
273
+ private resizeSensor : ResizeSensor
274
+ constructor ( el : HTMLElement , container : HTMLElement ) {
275
+ this . el = el
276
+ this . width = 0
277
+ this . height = 0
278
+
279
+ this . resizeSensor = new ResizeSensor ( el )
280
+ this . resizeSensor . addListener ( ( ) => {
281
+ if ( el . parentNode === container && ! isHidden ( el ) ) {
282
+ this . updateSize ( )
283
+ }
284
+ } )
285
+ }
286
+ destroy ( ) {
287
+ this . resizeSensor . destroy ( )
288
+ }
289
+ updateSize ( ) {
290
+ const { width, height } = this . el . getBoundingClientRect ( )
291
+ this . width = width
292
+ this . height = height
237
293
}
238
294
}
0 commit comments