Skip to content

Commit e3f7451

Browse files
committed
release(data-grid): v1.4.0
1 parent 0d56481 commit e3f7451

File tree

7 files changed

+197
-71
lines changed

7 files changed

+197
-71
lines changed

index.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
"data-grid": {
7575
"react": true,
7676
"icon": true,
77-
"version": "1.3.2",
77+
"version": "1.4.0",
7878
"style": true,
7979
"test": true,
8080
"install": false,

src/data-grid/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 1.4.0 (17 Mar 2025)
2+
3+
* feat: virtual list
4+
15
## 1.3.2 (2 Mar 2025)
26

37
* refactor: theme

src/data-grid/index.ts

+154-54
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import lowerCase from 'licia/lowerCase'
2525
import clamp from 'licia/clamp'
2626
import max from 'licia/max'
2727
import min from 'licia/min'
28+
import isOdd from 'licia/isOdd'
29+
import now from 'licia/now'
30+
import remove from 'licia/remove'
2831
import pointerEvent from 'licia/pointerEvent'
2932
import { exportCjs, eventClient, pxToNum } from '../share/util'
3033

@@ -69,7 +72,7 @@ export interface IDataGridNodeOptions {
6972
selectable?: boolean
7073
}
7174

72-
const MIN_APPEND_INTERVAL = 100
75+
const ROW_HEIGHT = 20
7376

7477
/**
7578
* Grid for displaying datasets.
@@ -117,9 +120,19 @@ export default class DataGrid extends Component<IOptions> {
117120
private sortId?: string
118121
private selectedNode: DataGridNode | null = null
119122
private isAscending = true
120-
private appendTimer: NodeJS.Timeout | null = null
121-
private frag: DocumentFragment = document.createDocumentFragment()
122123
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
123136
constructor(container: HTMLElement, options: IOptions) {
124137
super(container, { compName: 'data-grid' }, options)
125138
this.$container.attr('tabindex', '0')
@@ -154,11 +167,15 @@ export default class DataGrid extends Component<IOptions> {
154167
this.$headerRow = this.find('.header').find('tr')
155168
this.$fillerRow = this.find('.filler-row')
156169
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')
158173
this.tableBody = this.$tableBody.get(0) as HTMLElement
159174
this.$colgroup = this.$container.find('colgroup')
160175
this.$dataContainer = this.find('.data-container')
161176
this.dataContainer = this.$dataContainer.get(0) as HTMLDivElement
177+
this.$space = this.find('.data-space')
178+
this.space = this.$space.get(0) as HTMLElement
162179

163180
this.renderHeader()
164181
this.renderResizers()
@@ -174,16 +191,14 @@ export default class DataGrid extends Component<IOptions> {
174191
}
175192
/** Remove row data. */
176193
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)
186199
}
200+
this.renderData()
201+
this.updateHeight()
187202
}
188203
/** Append row data. */
189204
append(data: NodeData, options: IDataGridNodeOptions = {}) {
@@ -202,19 +217,13 @@ export default class DataGrid extends Component<IOptions> {
202217
this.sortNodes(this.sortId, this.isAscending)
203218
} else {
204219
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()
209221
}
210222
}
211223

212-
return node
213-
}
214-
private _append = () => {
215-
this.tableBody.insertBefore(this.frag, this.fillerRow)
216-
this.appendTimer = null
217224
this.updateHeight()
225+
226+
return node
218227
}
219228
/** Set data. */
220229
setData(
@@ -287,19 +296,17 @@ export default class DataGrid extends Component<IOptions> {
287296
}
288297
/** Clear all data. */
289298
clear() {
290-
this.detachAll()
291299
this.nodes = []
292300
this.displayNodes = []
293301
this.selectNode(null)
294302

303+
this.renderData()
295304
this.updateHeight()
296305
}
297306
private updateHeight() {
298-
const { $fillerRow, c, $container } = this
307+
const { $fillerRow, $container } = this
299308
let { maxHeight, minHeight } = this.options
300309

301-
this.$dataContainer.css({ height: 'auto' })
302-
303310
const headerHeight = this.$headerRow.offset().height
304311
const borderTopWidth = pxToNum($container.css('border-top-width'))
305312
const borderBottomWidth = pxToNum($container.css('border-bottom-width'))
@@ -311,12 +318,10 @@ export default class DataGrid extends Component<IOptions> {
311318
}
312319
maxHeight -= minusHeight
313320

314-
const $tr = this.$dataContainer.find(c('.node'))
315-
const len = ($tr as any).length
321+
const len = this.displayNodes.length
316322
let height = 0
317323
if (len > 0) {
318-
const rowHeight = $tr.offset().height
319-
height = rowHeight * len
324+
height = ROW_HEIGHT * len
320325
}
321326

322327
if (height > minHeight) {
@@ -411,10 +416,12 @@ export default class DataGrid extends Component<IOptions> {
411416
$document.off(pointerEvent('up'), this.onResizeColEnd)
412417
}
413418
private bindEvent() {
414-
const { c, $headerRow, $tableBody, $resizers } = this
419+
const { c, $headerRow, $tableBody, $resizers, $dataContainer } = this
415420

416421
this.resizeSensor.addListener(this.onResize)
417422

423+
$dataContainer.on('scroll', this.onScroll)
424+
418425
const self = this
419426

420427
$tableBody
@@ -593,26 +600,120 @@ export default class DataGrid extends Component<IOptions> {
593600
this.$resizers.eq(i).css('left', resizerLeft[i] + 'px')
594601
}
595602
}
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+
}
598639

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+
}
600647

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,
605651
})
606-
tableBody.insertBefore(frag, fillerRow)
607652

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]
609677

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'
611710
}
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'
616717
}
617718
private filterNode(node: DataGridNode) {
618719
let { filter } = this.options
@@ -675,12 +776,14 @@ export default class DataGrid extends Component<IOptions> {
675776
</table>
676777
</div>
677778
<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>
684787
</div>
685788
`)
686789
)
@@ -714,9 +817,6 @@ export class DataGridNode {
714817
text() {
715818
return this.$container.text()
716819
}
717-
detach() {
718-
this.$container.remove()
719-
}
720820
select() {
721821
this.$container.addClass(this.dataGrid.c('selected'))
722822
}

src/data-grid/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "data-grid",
3-
"version": "1.3.2",
3+
"version": "1.4.0",
44
"description": "Grid for displaying datasets",
55
"luna": {
66
"react": true,

src/data-grid/story.js

+18
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,24 @@ const def = story(
3434
dataGrid.on('click', (e, node) => console.log('click', node))
3535
dataGrid.on('dblclick', (e, node) => console.log('dblclick', node))
3636
dataGrid.on('contextmenu', (e, node) => console.log('contextmenu', node))
37+
38+
button('Append 10000 items', () => {
39+
for (let i = 0; i < 10000; i++) {
40+
dataGrid.append(
41+
{
42+
index: i,
43+
name: 'Luna',
44+
site: 'luna.liriliri.io',
45+
},
46+
{
47+
selectable: true,
48+
}
49+
)
50+
}
51+
52+
return false
53+
})
54+
3755
button('Remove Selected', () => {
3856
if (selectedNode) {
3957
dataGrid.remove(selectedNode)

src/data-grid/style.scss

+12-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
table {
1515
width: 100%;
16-
height: 100%;
16+
min-height: 100%;
1717
border-collapse: separate;
1818
border-spacing: 0;
1919
table-layout: fixed;
@@ -73,6 +73,17 @@
7373

7474
.data-container {
7575
overflow-y: auto;
76+
position: relative;
77+
}
78+
79+
.data-space {
80+
min-height: 100%;
81+
}
82+
83+
.data {
84+
position: absolute;
85+
left: 0;
86+
top: 0;
7687
}
7788

7889
.filler-row {

0 commit comments

Comments
 (0)