@tipolox/litgrid-renderer 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +6 -2
- package/src/index.ts +20 -0
- package/src/utils/math.ts +13 -0
- package/src/viewport/scrollMapping.ts +83 -0
- package/src/virtualization/createVariableVirtualizer.ts +78 -0
- package/src/virtualization/createVirtualizer.ts +65 -0
- package/src/virtualization/types.ts +57 -0
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ npm install @tipolox/litgrid-renderer
|
|
|
16
16
|
- [Renderer utilities API](https://github.com/tipolox/litgrid/blob/main/docs/api-reference.md#renderer-utilities)
|
|
17
17
|
- [Installation guide](https://github.com/tipolox/litgrid/blob/main/docs/installation.md)
|
|
18
18
|
- [Repository](https://github.com/tipolox/litgrid)
|
|
19
|
+
- [v1.1.0 upgrade guide](https://github.com/tipolox/litgrid/blob/main/docs/installation.md#upgrading-to-v110)
|
|
19
20
|
- [Live demo](https://tipolox.com/litgrid/demo)
|
|
20
21
|
|
|
21
22
|
Licensed under the [MIT License](https://github.com/tipolox/litgrid/blob/main/LICENSE).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tipolox/litgrid-renderer",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Virtualization and scroll-mapping utilities for LitGrid.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
"./package.json": "./package.json"
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
|
-
"dist"
|
|
18
|
+
"dist",
|
|
19
|
+
"src/**/*.ts",
|
|
20
|
+
"!src/**/*.d.ts",
|
|
21
|
+
"!src/**/*.test.ts",
|
|
22
|
+
"!src/**/*.bench.ts"
|
|
19
23
|
],
|
|
20
24
|
"keywords": [
|
|
21
25
|
"data-grid",
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { createVirtualizer } from './virtualization/createVirtualizer.js'
|
|
2
|
+
export { createVariableVirtualizer } from './virtualization/createVariableVirtualizer.js'
|
|
3
|
+
export { calculateDisplayLayout, mapDisplayScrollOffset } from './viewport/scrollMapping.js'
|
|
4
|
+
export type {
|
|
5
|
+
FixedVirtualizer,
|
|
6
|
+
FixedVirtualRange,
|
|
7
|
+
VariableVirtualizerOptions,
|
|
8
|
+
VariableVirtualizerState,
|
|
9
|
+
VariableVirtualizer,
|
|
10
|
+
VariableVirtualRange,
|
|
11
|
+
VirtualRange,
|
|
12
|
+
VirtualizerOptions,
|
|
13
|
+
VirtualizerState
|
|
14
|
+
} from './virtualization/types.js'
|
|
15
|
+
export type {
|
|
16
|
+
DisplayLayout,
|
|
17
|
+
DisplayLayoutOptions,
|
|
18
|
+
ScrollMappingOptions,
|
|
19
|
+
ScrollMappingResult
|
|
20
|
+
} from './viewport/scrollMapping.js'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function clamp(value: number, min: number, max: number) {
|
|
2
|
+
return Math.min(Math.max(value, min), max)
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function sumSizes(sizes: number[], startIndex: number, endIndex: number) {
|
|
6
|
+
let total = 0
|
|
7
|
+
|
|
8
|
+
for (let index = startIndex; index < endIndex; index += 1) {
|
|
9
|
+
total += Math.max(1, sizes[index] ?? 0)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return total
|
|
13
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { clamp } from '../utils/math.js'
|
|
2
|
+
|
|
3
|
+
export type ScrollMappingOptions = {
|
|
4
|
+
displayScrollOffset: number
|
|
5
|
+
previousDisplayScrollOffset: number
|
|
6
|
+
displaySize: number
|
|
7
|
+
totalSize: number
|
|
8
|
+
viewportSize: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type ScrollMappingResult = {
|
|
12
|
+
availableDisplaySize: number
|
|
13
|
+
availableVirtualSize: number
|
|
14
|
+
scrollOffset: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function mapDisplayScrollOffset({
|
|
18
|
+
displayScrollOffset,
|
|
19
|
+
previousDisplayScrollOffset,
|
|
20
|
+
displaySize,
|
|
21
|
+
totalSize,
|
|
22
|
+
viewportSize
|
|
23
|
+
}: ScrollMappingOptions): ScrollMappingResult {
|
|
24
|
+
const availableDisplaySize = Math.max(0, displaySize - viewportSize)
|
|
25
|
+
const availableVirtualSize = Math.max(0, totalSize - viewportSize)
|
|
26
|
+
|
|
27
|
+
if (totalSize > 0 && availableDisplaySize > 0 && availableVirtualSize > 0) {
|
|
28
|
+
const ratio = displayScrollOffset / availableDisplaySize
|
|
29
|
+
const desiredScrollOffset = ratio * availableVirtualSize
|
|
30
|
+
let scrollOffset = clamp(
|
|
31
|
+
Number.isFinite(desiredScrollOffset) ? desiredScrollOffset : 0,
|
|
32
|
+
0,
|
|
33
|
+
availableVirtualSize
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const bottomThreshold = 1
|
|
37
|
+
const isScrollingDown = displayScrollOffset >= previousDisplayScrollOffset
|
|
38
|
+
if (isScrollingDown && displayScrollOffset >= Math.max(0, availableDisplaySize - bottomThreshold)) {
|
|
39
|
+
scrollOffset = availableVirtualSize
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { availableDisplaySize, availableVirtualSize, scrollOffset }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
availableDisplaySize,
|
|
47
|
+
availableVirtualSize,
|
|
48
|
+
scrollOffset: clamp(displayScrollOffset, 0, availableVirtualSize)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type DisplayLayoutOptions = {
|
|
53
|
+
totalSize: number
|
|
54
|
+
displaySize: number
|
|
55
|
+
virtualOffsetTop: number
|
|
56
|
+
visibleSize: number
|
|
57
|
+
bottomPadding: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type DisplayLayout = {
|
|
61
|
+
displayScale: number
|
|
62
|
+
displayOffsetTop: number
|
|
63
|
+
displayBottomPadding: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function calculateDisplayLayout({
|
|
67
|
+
totalSize,
|
|
68
|
+
displaySize,
|
|
69
|
+
virtualOffsetTop,
|
|
70
|
+
visibleSize,
|
|
71
|
+
bottomPadding
|
|
72
|
+
}: DisplayLayoutOptions): DisplayLayout {
|
|
73
|
+
const displayScale = totalSize > 0 ? displaySize / totalSize : 1
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
displayScale,
|
|
77
|
+
displayOffsetTop: Math.min(
|
|
78
|
+
virtualOffsetTop * displayScale,
|
|
79
|
+
Math.max(0, displaySize - visibleSize)
|
|
80
|
+
),
|
|
81
|
+
displayBottomPadding: bottomPadding * displayScale
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { clamp, sumSizes } from '../utils/math.js'
|
|
2
|
+
import type {
|
|
3
|
+
VariableVirtualizer,
|
|
4
|
+
VariableVirtualizerOptions,
|
|
5
|
+
VariableVirtualizerState
|
|
6
|
+
} from './types.js'
|
|
7
|
+
|
|
8
|
+
export function createVariableVirtualizer({
|
|
9
|
+
sizes,
|
|
10
|
+
viewportSize,
|
|
11
|
+
overscan = 2
|
|
12
|
+
}: VariableVirtualizerOptions): VariableVirtualizer {
|
|
13
|
+
let state: VariableVirtualizerState = {
|
|
14
|
+
sizes,
|
|
15
|
+
viewportSize,
|
|
16
|
+
overscan,
|
|
17
|
+
scrollOffset: 0
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
setOptions(next: Partial<VariableVirtualizerState>) {
|
|
22
|
+
state = {
|
|
23
|
+
...state,
|
|
24
|
+
...next
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
setScrollOffset(scrollOffset: number) {
|
|
28
|
+
state.scrollOffset = scrollOffset
|
|
29
|
+
},
|
|
30
|
+
getState() {
|
|
31
|
+
const sizes = state.sizes.map((size) => Math.max(1, size))
|
|
32
|
+
const count = sizes.length
|
|
33
|
+
const totalSize = sumSizes(sizes, 0, count)
|
|
34
|
+
const safeViewportSize = Math.max(0, state.viewportSize)
|
|
35
|
+
const safeOverscan = Math.max(0, state.overscan)
|
|
36
|
+
const safeScrollOffset = clamp(state.scrollOffset, 0, totalSize)
|
|
37
|
+
|
|
38
|
+
let baseStartIndex = 0
|
|
39
|
+
let baseStartOffset = 0
|
|
40
|
+
|
|
41
|
+
while (
|
|
42
|
+
baseStartIndex < count &&
|
|
43
|
+
baseStartOffset + sizes[baseStartIndex] <= safeScrollOffset
|
|
44
|
+
) {
|
|
45
|
+
baseStartOffset += sizes[baseStartIndex]
|
|
46
|
+
baseStartIndex += 1
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let visibleEndIndex = baseStartIndex
|
|
50
|
+
let visibleEndOffset = baseStartOffset
|
|
51
|
+
|
|
52
|
+
while (
|
|
53
|
+
visibleEndIndex < count &&
|
|
54
|
+
visibleEndOffset < safeScrollOffset + safeViewportSize
|
|
55
|
+
) {
|
|
56
|
+
visibleEndOffset += sizes[visibleEndIndex]
|
|
57
|
+
visibleEndIndex += 1
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const startIndex = clamp(baseStartIndex - safeOverscan, 0, count)
|
|
61
|
+
const endIndex = clamp(visibleEndIndex + safeOverscan, startIndex, count)
|
|
62
|
+
const leftPadding = sumSizes(sizes, 0, startIndex)
|
|
63
|
+
const visibleSize = sumSizes(sizes, startIndex, endIndex)
|
|
64
|
+
const rightPadding = Math.max(0, totalSize - leftPadding - visibleSize)
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
startIndex,
|
|
68
|
+
endIndex,
|
|
69
|
+
leftPadding,
|
|
70
|
+
rightPadding,
|
|
71
|
+
offsetTop: leftPadding,
|
|
72
|
+
bottomPadding: rightPadding,
|
|
73
|
+
totalSize,
|
|
74
|
+
visibleSize
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { clamp } from '../utils/math.js'
|
|
2
|
+
import type { FixedVirtualizer, VirtualizerOptions, VirtualizerState } from './types.js'
|
|
3
|
+
|
|
4
|
+
export function createVirtualizer({
|
|
5
|
+
rowHeight,
|
|
6
|
+
viewportHeight,
|
|
7
|
+
overscan = 4
|
|
8
|
+
}: VirtualizerOptions): FixedVirtualizer {
|
|
9
|
+
let state: VirtualizerState = {
|
|
10
|
+
rowHeight,
|
|
11
|
+
viewportHeight,
|
|
12
|
+
overscan,
|
|
13
|
+
scrollTop: 0
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
setOptions(next: Partial<VirtualizerState>) {
|
|
18
|
+
state = {
|
|
19
|
+
...state,
|
|
20
|
+
...next
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
setScrollTop(scrollTop: number) {
|
|
24
|
+
state.scrollTop = scrollTop
|
|
25
|
+
},
|
|
26
|
+
setScrollOffset(scrollTop: number) {
|
|
27
|
+
state.scrollTop = scrollTop
|
|
28
|
+
},
|
|
29
|
+
getState(count: number) {
|
|
30
|
+
const safeCount = Math.max(0, count)
|
|
31
|
+
const safeRowHeight = Math.max(1, state.rowHeight)
|
|
32
|
+
const safeViewportHeight = Math.max(0, state.viewportHeight)
|
|
33
|
+
const safeOverscan = Math.max(0, state.overscan)
|
|
34
|
+
const visibleCount = Math.max(1, Math.ceil(safeViewportHeight / safeRowHeight))
|
|
35
|
+
const maxStartIndex = Math.max(0, safeCount - visibleCount)
|
|
36
|
+
|
|
37
|
+
const baseStartIndex = clamp(
|
|
38
|
+
Math.floor(state.scrollTop / safeRowHeight),
|
|
39
|
+
0,
|
|
40
|
+
maxStartIndex
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const startIndex = clamp(baseStartIndex - safeOverscan, 0, safeCount)
|
|
44
|
+
const endIndex = clamp(
|
|
45
|
+
baseStartIndex + visibleCount + safeOverscan,
|
|
46
|
+
startIndex,
|
|
47
|
+
safeCount
|
|
48
|
+
)
|
|
49
|
+
const offsetTop = startIndex * safeRowHeight
|
|
50
|
+
const totalHeight = safeCount * safeRowHeight
|
|
51
|
+
const visibleSize = (endIndex - startIndex) * safeRowHeight
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
startIndex,
|
|
55
|
+
endIndex,
|
|
56
|
+
offsetTop,
|
|
57
|
+
bottomPadding: Math.max(0, totalHeight - endIndex * safeRowHeight),
|
|
58
|
+
totalHeight,
|
|
59
|
+
totalSize: totalHeight,
|
|
60
|
+
visibleCount,
|
|
61
|
+
visibleSize
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type VirtualizerOptions = {
|
|
2
|
+
rowHeight: number
|
|
3
|
+
viewportHeight: number
|
|
4
|
+
overscan?: number
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type VirtualizerState = {
|
|
8
|
+
rowHeight: number
|
|
9
|
+
viewportHeight: number
|
|
10
|
+
overscan: number
|
|
11
|
+
scrollTop: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type VirtualRange = {
|
|
15
|
+
startIndex: number
|
|
16
|
+
endIndex: number
|
|
17
|
+
offsetTop: number
|
|
18
|
+
bottomPadding: number
|
|
19
|
+
totalSize: number
|
|
20
|
+
visibleSize: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type FixedVirtualRange = VirtualRange & {
|
|
24
|
+
totalHeight: number
|
|
25
|
+
visibleCount: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type FixedVirtualizer = {
|
|
29
|
+
setOptions: (next: Partial<VirtualizerState>) => void
|
|
30
|
+
setScrollTop: (scrollTop: number) => void
|
|
31
|
+
setScrollOffset: (scrollTop: number) => void
|
|
32
|
+
getState: (count: number) => FixedVirtualRange
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type VariableVirtualizerOptions = {
|
|
36
|
+
sizes: number[]
|
|
37
|
+
viewportSize: number
|
|
38
|
+
overscan?: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type VariableVirtualizerState = {
|
|
42
|
+
sizes: number[]
|
|
43
|
+
viewportSize: number
|
|
44
|
+
overscan: number
|
|
45
|
+
scrollOffset: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type VariableVirtualRange = VirtualRange & {
|
|
49
|
+
leftPadding: number
|
|
50
|
+
rightPadding: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type VariableVirtualizer = {
|
|
54
|
+
setOptions: (next: Partial<VariableVirtualizerState>) => void
|
|
55
|
+
setScrollOffset: (scrollOffset: number) => void
|
|
56
|
+
getState: () => VariableVirtualRange
|
|
57
|
+
}
|