@@ -25,6 +25,9 @@ import lowerCase from 'licia/lowerCase'
25
25
import clamp from 'licia/clamp'
26
26
import max from 'licia/max'
27
27
import min from 'licia/min'
28
+ import isOdd from 'licia/isOdd'
29
+ import now from 'licia/now'
30
+ import remove from 'licia/remove'
28
31
import pointerEvent from 'licia/pointerEvent'
29
32
import { exportCjs , eventClient , pxToNum } from '../share/util'
30
33
@@ -69,7 +72,7 @@ export interface IDataGridNodeOptions {
69
72
selectable ?: boolean
70
73
}
71
74
72
- const MIN_APPEND_INTERVAL = 100
75
+ const ROW_HEIGHT = 20
73
76
74
77
/**
75
78
* Grid for displaying datasets.
@@ -117,9 +120,19 @@ export default class DataGrid extends Component<IOptions> {
117
120
private sortId ?: string
118
121
private selectedNode : DataGridNode | null = null
119
122
private isAscending = true
120
- private appendTimer : NodeJS . Timeout | null = null
121
- private frag : DocumentFragment = document . createDocumentFragment ( )
122
123
private colWidths : number [ ] = [ ]
124
+ private $space : $ . $
125
+ private $data : $ . $
126
+ private data : HTMLElement
127
+ private space : HTMLElement
128
+ private spaceHeight = 0
129
+ private topSpaceHeight = 0
130
+ private lastScrollTop = 0
131
+ private lastTimestamp = 0
132
+ private speedToleranceFactor = 100
133
+ private maxSpeedTolerance = 2000
134
+ private minSpeedTolerance = 100
135
+ private scrollTimer : NodeJS . Timeout | null = null
123
136
constructor ( container : HTMLElement , options : IOptions ) {
124
137
super ( container , { compName : 'data-grid' } , options )
125
138
this . $container . attr ( 'tabindex' , '0' )
@@ -154,11 +167,15 @@ export default class DataGrid extends Component<IOptions> {
154
167
this . $headerRow = this . find ( '.header' ) . find ( 'tr' )
155
168
this . $fillerRow = this . find ( '.filler-row' )
156
169
this . fillerRow = this . $fillerRow . get ( 0 ) as HTMLElement
157
- this . $tableBody = this . find ( '.data' ) . find ( 'tbody' )
170
+ this . $data = this . find ( '.data' )
171
+ this . data = this . $data . get ( 0 ) as HTMLElement
172
+ this . $tableBody = this . $data . find ( 'tbody' )
158
173
this . tableBody = this . $tableBody . get ( 0 ) as HTMLElement
159
174
this . $colgroup = this . $container . find ( 'colgroup' )
160
175
this . $dataContainer = this . find ( '.data-container' )
161
176
this . dataContainer = this . $dataContainer . get ( 0 ) as HTMLDivElement
177
+ this . $space = this . find ( '.data-space' )
178
+ this . space = this . $space . get ( 0 ) as HTMLElement
162
179
163
180
this . renderHeader ( )
164
181
this . renderResizers ( )
@@ -174,16 +191,14 @@ export default class DataGrid extends Component<IOptions> {
174
191
}
175
192
/** Remove row data. */
176
193
remove ( node : DataGridNode ) {
177
- const { nodes } = this
178
- const pos = nodes . indexOf ( node )
179
- if ( pos > - 1 ) {
180
- node . detach ( )
181
- nodes . splice ( pos , 1 )
182
- if ( node === this . selectedNode ) {
183
- this . selectNode ( nodes [ pos ] || nodes [ pos - 1 ] || null )
184
- }
185
- this . updateHeight ( )
194
+ const { nodes, displayNodes } = this
195
+ remove ( nodes , ( n ) => n === node )
196
+ remove ( displayNodes , ( n ) => n === node )
197
+ if ( node === this . selectedNode ) {
198
+ this . selectNode ( null )
186
199
}
200
+ this . renderData ( )
201
+ this . updateHeight ( )
187
202
}
188
203
/** Append row data. */
189
204
append ( data : NodeData , options : IDataGridNodeOptions = { } ) {
@@ -202,19 +217,13 @@ export default class DataGrid extends Component<IOptions> {
202
217
this . sortNodes ( this . sortId , this . isAscending )
203
218
} else {
204
219
if ( isVisible ) {
205
- this . frag . appendChild ( node . container )
206
- if ( ! this . appendTimer ) {
207
- this . appendTimer = setTimeout ( this . _append , MIN_APPEND_INTERVAL )
208
- }
220
+ this . renderData ( )
209
221
}
210
222
}
211
223
212
- return node
213
- }
214
- private _append = ( ) => {
215
- this . tableBody . insertBefore ( this . frag , this . fillerRow )
216
- this . appendTimer = null
217
224
this . updateHeight ( )
225
+
226
+ return node
218
227
}
219
228
/** Set data. */
220
229
setData (
@@ -287,19 +296,17 @@ export default class DataGrid extends Component<IOptions> {
287
296
}
288
297
/** Clear all data. */
289
298
clear ( ) {
290
- this . detachAll ( )
291
299
this . nodes = [ ]
292
300
this . displayNodes = [ ]
293
301
this . selectNode ( null )
294
302
303
+ this . renderData ( )
295
304
this . updateHeight ( )
296
305
}
297
306
private updateHeight ( ) {
298
- const { $fillerRow, c , $container } = this
307
+ const { $fillerRow, $container } = this
299
308
let { maxHeight, minHeight } = this . options
300
309
301
- this . $dataContainer . css ( { height : 'auto' } )
302
-
303
310
const headerHeight = this . $headerRow . offset ( ) . height
304
311
const borderTopWidth = pxToNum ( $container . css ( 'border-top-width' ) )
305
312
const borderBottomWidth = pxToNum ( $container . css ( 'border-bottom-width' ) )
@@ -311,12 +318,10 @@ export default class DataGrid extends Component<IOptions> {
311
318
}
312
319
maxHeight -= minusHeight
313
320
314
- const $tr = this . $dataContainer . find ( c ( '.node' ) )
315
- const len = ( $tr as any ) . length
321
+ const len = this . displayNodes . length
316
322
let height = 0
317
323
if ( len > 0 ) {
318
- const rowHeight = $tr . offset ( ) . height
319
- height = rowHeight * len
324
+ height = ROW_HEIGHT * len
320
325
}
321
326
322
327
if ( height > minHeight ) {
@@ -411,10 +416,12 @@ export default class DataGrid extends Component<IOptions> {
411
416
$document . off ( pointerEvent ( 'up' ) , this . onResizeColEnd )
412
417
}
413
418
private bindEvent ( ) {
414
- const { c, $headerRow, $tableBody, $resizers } = this
419
+ const { c, $headerRow, $tableBody, $resizers, $dataContainer } = this
415
420
416
421
this . resizeSensor . addListener ( this . onResize )
417
422
423
+ $dataContainer . on ( 'scroll' , this . onScroll )
424
+
418
425
const self = this
419
426
420
427
$tableBody
@@ -593,26 +600,120 @@ export default class DataGrid extends Component<IOptions> {
593
600
this . $resizers . eq ( i ) . css ( 'left' , resizerLeft [ i ] + 'px' )
594
601
}
595
602
}
596
- private renderData ( ) {
597
- const { tableBody, displayNodes, fillerRow, dataContainer } = this
603
+ private onScroll = ( ) => {
604
+ const { scrollHeight, clientHeight, scrollTop } = this
605
+ . dataContainer as HTMLElement
606
+ // safari bounce effect
607
+ if ( scrollTop <= 0 ) return
608
+ if ( clientHeight + scrollTop > scrollHeight ) return
609
+
610
+ const lastScrollTop = this . lastScrollTop
611
+ const lastTimestamp = this . lastTimestamp
612
+
613
+ const timestamp = now ( )
614
+ const duration = timestamp - lastTimestamp
615
+ const distance = scrollTop - lastScrollTop
616
+ const speed = Math . abs ( distance / duration )
617
+ let speedTolerance = speed * this . speedToleranceFactor
618
+ if ( duration > 1000 ) {
619
+ speedTolerance = 1000
620
+ }
621
+ if ( speedTolerance > this . maxSpeedTolerance ) {
622
+ speedTolerance = this . maxSpeedTolerance
623
+ }
624
+ if ( speedTolerance < this . minSpeedTolerance ) {
625
+ speedTolerance = this . minSpeedTolerance
626
+ }
627
+ this . lastScrollTop = scrollTop
628
+ this . lastTimestamp = timestamp
629
+
630
+ let topTolerance = 0
631
+ let bottomTolerance = 0
632
+ if ( lastScrollTop < scrollTop ) {
633
+ topTolerance = this . minSpeedTolerance
634
+ bottomTolerance = speedTolerance
635
+ } else {
636
+ topTolerance = speedTolerance
637
+ bottomTolerance = this . minSpeedTolerance
638
+ }
598
639
599
- const scrollTop = dataContainer . scrollTop
640
+ if (
641
+ this . topSpaceHeight < scrollTop - topTolerance &&
642
+ this . topSpaceHeight + this . data . offsetHeight >
643
+ scrollTop + clientHeight + bottomTolerance
644
+ ) {
645
+ return
646
+ }
600
647
601
- this . detachAll ( )
602
- const frag = document . createDocumentFragment ( )
603
- each ( displayNodes , ( node ) => {
604
- frag . appendChild ( node . container )
648
+ this . renderData ( {
649
+ topTolerance : topTolerance * 2 ,
650
+ bottomTolerance : bottomTolerance * 2 ,
605
651
} )
606
- tableBody . insertBefore ( frag , fillerRow )
607
652
608
- this . updateHeight ( )
653
+ if ( this . scrollTimer ) {
654
+ clearTimeout ( this . scrollTimer )
655
+ }
656
+ this . scrollTimer = setTimeout ( ( ) => {
657
+ this . renderData ( )
658
+ } , 100 )
659
+ }
660
+ private renderData = throttle (
661
+ ( { topTolerance = 500 , bottomTolerance = 500 } = { } ) => {
662
+ const { dataContainer, displayNodes, tableBody } = this
663
+ const { scrollTop, clientHeight } = dataContainer
664
+ const top = scrollTop - topTolerance
665
+ const bottom = scrollTop + clientHeight + bottomTolerance
666
+
667
+ let topSpaceHeight = 0
668
+ let currentHeight = 0
669
+
670
+ const len = displayNodes . length
671
+
672
+ const renderNodes = [ ]
673
+ const height = ROW_HEIGHT
674
+
675
+ for ( let i = 0 ; i < len ; i ++ ) {
676
+ const node = displayNodes [ i ]
609
677
610
- dataContainer . scrollTop = scrollTop
678
+ if ( currentHeight <= bottom ) {
679
+ if ( currentHeight + height > top ) {
680
+ if ( renderNodes . length === 0 && isOdd ( i ) ) {
681
+ renderNodes . push ( displayNodes [ i - 1 ] )
682
+ topSpaceHeight -= height
683
+ }
684
+ renderNodes . push ( node )
685
+ } else if ( currentHeight < top ) {
686
+ topSpaceHeight += height
687
+ }
688
+ }
689
+
690
+ currentHeight += height
691
+ }
692
+
693
+ this . updateSpace ( currentHeight )
694
+ this . updateTopSpace ( topSpaceHeight )
695
+
696
+ const frag = document . createDocumentFragment ( )
697
+ for ( let i = 0 , len = renderNodes . length ; i < len ; i ++ ) {
698
+ frag . appendChild ( renderNodes [ i ] . container )
699
+ }
700
+ frag . appendChild ( this . fillerRow )
701
+
702
+ tableBody . textContent = ''
703
+ tableBody . appendChild ( frag )
704
+ } ,
705
+ 16
706
+ )
707
+ private updateTopSpace ( height : number ) {
708
+ this . topSpaceHeight = height
709
+ this . data . style . top = height + 'px'
611
710
}
612
- private detachAll ( ) {
613
- const { tableBody } = this
614
- tableBody . innerHTML = ''
615
- tableBody . appendChild ( this . fillerRow )
711
+ private updateSpace ( height : number ) {
712
+ if ( this . spaceHeight === height ) {
713
+ return
714
+ }
715
+ this . spaceHeight = height
716
+ this . space . style . height = height + 'px'
616
717
}
617
718
private filterNode ( node : DataGridNode ) {
618
719
let { filter } = this . options
@@ -675,12 +776,14 @@ export default class DataGrid extends Component<IOptions> {
675
776
</table>
676
777
</div>
677
778
<div class="data-container">
678
- <table class="data">
679
- <colgroup></colgroup>
680
- <tbody>
681
- <tr class="filler-row"></tr>
682
- </tbody>
683
- </table>
779
+ <div class="data-space">
780
+ <table class="data">
781
+ <colgroup></colgroup>
782
+ <tbody>
783
+ <tr class="filler-row"></tr>
784
+ </tbody>
785
+ </table>
786
+ </div>
684
787
</div>
685
788
` )
686
789
)
@@ -714,9 +817,6 @@ export class DataGridNode {
714
817
text ( ) {
715
818
return this . $container . text ( )
716
819
}
717
- detach ( ) {
718
- this . $container . remove ( )
719
- }
720
820
select ( ) {
721
821
this . $container . addClass ( this . dataGrid . c ( 'selected' ) )
722
822
}
0 commit comments