@indielayer/ui 1.15.3 → 1.17.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/docs/components/menu/DocsMenu.vue +3 -0
- package/docs/pages/component/infiniteLoader/composable.vue +168 -0
- package/docs/pages/component/infiniteLoader/index.vue +36 -0
- package/docs/pages/component/infiniteLoader/usage.vue +161 -0
- package/docs/pages/component/table/usage.vue +13 -0
- package/docs/pages/component/virtualGrid/index.vue +29 -0
- package/docs/pages/component/virtualGrid/usage.vue +20 -0
- package/docs/pages/component/virtualList/dynamicHeight.vue +75 -0
- package/docs/pages/component/virtualList/index.vue +36 -0
- package/docs/pages/component/virtualList/usage.vue +17 -0
- package/docs/search/components.json +1 -1
- package/lib/components/select/Select.vue.js +35 -35
- package/lib/components/table/Table.vue.d.ts +9 -0
- package/lib/components/table/Table.vue.js +190 -160
- package/lib/components/tooltip/Tooltip.vue.js +64 -52
- package/lib/composables/useVirtualList.d.ts +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +88 -76
- package/lib/index.umd.js +4 -4
- package/lib/install.js +15 -7
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/virtual/components/infiniteLoader/InfiniteLoader.test.d.ts +1 -0
- package/lib/virtual/components/infiniteLoader/InfiniteLoader.vue.d.ts +49 -0
- package/lib/virtual/components/infiniteLoader/InfiniteLoader.vue.js +21 -0
- package/lib/virtual/components/infiniteLoader/InfiniteLoader.vue2.js +4 -0
- package/lib/virtual/components/virtualGrid/VirtualGrid.vue.d.ts +185 -0
- package/lib/virtual/components/virtualGrid/VirtualGrid.vue.js +241 -0
- package/lib/virtual/components/virtualGrid/VirtualGrid.vue2.js +4 -0
- package/lib/virtual/components/virtualGrid/types.d.ts +138 -0
- package/lib/virtual/components/virtualList/VirtualList.test.d.ts +1 -0
- package/lib/virtual/components/virtualList/VirtualList.vue.d.ts +135 -0
- package/lib/virtual/components/virtualList/VirtualList.vue.js +157 -0
- package/lib/virtual/components/virtualList/VirtualList.vue2.js +4 -0
- package/lib/virtual/components/virtualList/isDynamicRowHeight.d.ts +2 -0
- package/lib/virtual/components/virtualList/isDynamicRowHeight.js +6 -0
- package/lib/virtual/components/virtualList/types.d.ts +115 -0
- package/lib/virtual/components/virtualList/useDynamicRowHeight.d.ts +7 -0
- package/lib/virtual/components/virtualList/useDynamicRowHeight.js +69 -0
- package/lib/virtual/components/virtualList/useDynamicRowHeight.test.d.ts +1 -0
- package/lib/virtual/composables/infinite-loader/scanForUnloadedIndices.d.ts +8 -0
- package/lib/virtual/composables/infinite-loader/scanForUnloadedIndices.js +41 -0
- package/lib/virtual/composables/infinite-loader/scanForUnloadedIndices.test.d.ts +1 -0
- package/lib/virtual/composables/infinite-loader/types.d.ts +30 -0
- package/lib/virtual/composables/infinite-loader/useInfiniteLoader.d.ts +6 -0
- package/lib/virtual/composables/infinite-loader/useInfiniteLoader.js +42 -0
- package/lib/virtual/composables/infinite-loader/useInfiniteLoader.test.d.ts +1 -0
- package/lib/virtual/core/createCachedBounds.d.ts +6 -0
- package/lib/virtual/core/createCachedBounds.js +55 -0
- package/lib/virtual/core/getEstimatedSize.d.ts +6 -0
- package/lib/virtual/core/getEstimatedSize.js +22 -0
- package/lib/virtual/core/getOffsetForIndex.d.ts +11 -0
- package/lib/virtual/core/getOffsetForIndex.js +40 -0
- package/lib/virtual/core/getStartStopIndices.d.ts +13 -0
- package/lib/virtual/core/getStartStopIndices.js +31 -0
- package/lib/virtual/core/getStartStopIndices.test.d.ts +1 -0
- package/lib/virtual/core/types.d.ts +11 -0
- package/lib/virtual/core/useCachedBounds.d.ts +7 -0
- package/lib/virtual/core/useCachedBounds.js +18 -0
- package/lib/virtual/core/useIsRtl.d.ts +2 -0
- package/lib/virtual/core/useIsRtl.js +15 -0
- package/lib/virtual/core/useItemSize.d.ts +5 -0
- package/lib/virtual/core/useItemSize.js +27 -0
- package/lib/virtual/core/useVirtualizer.d.ts +33 -0
- package/lib/virtual/core/useVirtualizer.js +171 -0
- package/lib/virtual/index.d.ts +9 -0
- package/lib/virtual/test-utils/mockResizeObserver.d.ts +15 -0
- package/lib/virtual/types.d.ts +2 -0
- package/lib/virtual/utils/adjustScrollOffsetForRtl.d.ts +7 -0
- package/lib/virtual/utils/adjustScrollOffsetForRtl.js +24 -0
- package/lib/virtual/utils/areArraysEqual.d.ts +1 -0
- package/lib/virtual/utils/assert.d.ts +1 -0
- package/lib/virtual/utils/assert.js +7 -0
- package/lib/virtual/utils/getRTLOffsetType.d.ts +2 -0
- package/lib/virtual/utils/getRTLOffsetType.js +13 -0
- package/lib/virtual/utils/getScrollbarSize.d.ts +2 -0
- package/lib/virtual/utils/getScrollbarSize.js +11 -0
- package/lib/virtual/utils/isRtl.d.ts +1 -0
- package/lib/virtual/utils/isRtl.js +12 -0
- package/lib/virtual/utils/parseNumericStyleValue.d.ts +2 -0
- package/lib/virtual/utils/parseNumericStyleValue.js +15 -0
- package/lib/virtual/utils/shallowCompare.d.ts +1 -0
- package/lib/virtual/utils/shallowCompare.js +14 -0
- package/package.json +1 -1
- package/src/components/select/Select.vue +3 -2
- package/src/components/table/Table.vue +23 -2
- package/src/components/tooltip/Tooltip.vue +25 -5
- package/src/composables/useVirtualList.ts +1 -1
- package/src/index.ts +1 -0
- package/src/install.ts +9 -3
- package/src/version.ts +1 -1
- package/src/virtual/README.md +285 -0
- package/src/virtual/components/infiniteLoader/InfiniteLoader.test.ts +96 -0
- package/src/virtual/components/infiniteLoader/InfiniteLoader.vue +18 -0
- package/src/virtual/components/virtualGrid/VirtualGrid.vue +322 -0
- package/src/virtual/components/virtualGrid/types.ts +160 -0
- package/src/virtual/components/virtualList/VirtualList.test.ts +47 -0
- package/src/virtual/components/virtualList/VirtualList.vue +233 -0
- package/src/virtual/components/virtualList/isDynamicRowHeight.ts +13 -0
- package/src/virtual/components/virtualList/types.ts +127 -0
- package/src/virtual/components/virtualList/useDynamicRowHeight.test.ts +183 -0
- package/src/virtual/components/virtualList/useDynamicRowHeight.ts +147 -0
- package/src/virtual/composables/infinite-loader/scanForUnloadedIndices.test.ts +141 -0
- package/src/virtual/composables/infinite-loader/scanForUnloadedIndices.ts +82 -0
- package/src/virtual/composables/infinite-loader/types.ts +36 -0
- package/src/virtual/composables/infinite-loader/useInfiniteLoader.test.ts +236 -0
- package/src/virtual/composables/infinite-loader/useInfiniteLoader.ts +88 -0
- package/src/virtual/core/createCachedBounds.ts +72 -0
- package/src/virtual/core/getEstimatedSize.ts +29 -0
- package/src/virtual/core/getOffsetForIndex.ts +90 -0
- package/src/virtual/core/getStartStopIndices.test.ts +45 -0
- package/src/virtual/core/getStartStopIndices.ts +71 -0
- package/src/virtual/core/types.ts +17 -0
- package/src/virtual/core/useCachedBounds.ts +21 -0
- package/src/virtual/core/useIsRtl.ts +25 -0
- package/src/virtual/core/useItemSize.ts +34 -0
- package/src/virtual/core/useVirtualizer.ts +294 -0
- package/src/virtual/index.ts +25 -0
- package/src/virtual/test-utils/mockResizeObserver.ts +162 -0
- package/src/virtual/types.ts +3 -0
- package/src/virtual/utils/adjustScrollOffsetForRtl.ts +37 -0
- package/src/virtual/utils/areArraysEqual.ts +13 -0
- package/src/virtual/utils/assert.ts +10 -0
- package/src/virtual/utils/getRTLOffsetType.ts +51 -0
- package/src/virtual/utils/getScrollbarSize.ts +24 -0
- package/src/virtual/utils/isRtl.ts +13 -0
- package/src/virtual/utils/parseNumericStyleValue.ts +19 -0
- package/src/virtual/utils/shallowCompare.ts +29 -0
- package/volar.d.ts +3 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
2
|
+
import { scanForUnloadedIndices } from './scanForUnloadedIndices'
|
|
3
|
+
|
|
4
|
+
describe('scanForUnloadedIndices', () => {
|
|
5
|
+
test('should return an empty array for a range of rows that have all been loaded', () => {
|
|
6
|
+
const isRowLoaded = vi.fn((index: number) => {
|
|
7
|
+
expect(index).toBeGreaterThanOrEqual(0)
|
|
8
|
+
expect(index).toBeLessThanOrEqual(2)
|
|
9
|
+
|
|
10
|
+
return true
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
expect(
|
|
14
|
+
scanForUnloadedIndices({
|
|
15
|
+
minimumBatchSize: 0,
|
|
16
|
+
isRowLoaded,
|
|
17
|
+
rowCount: 3,
|
|
18
|
+
startIndex: 0,
|
|
19
|
+
stopIndex: 2,
|
|
20
|
+
}),
|
|
21
|
+
).toEqual([])
|
|
22
|
+
|
|
23
|
+
expect(isRowLoaded).toHaveBeenCalledTimes(3)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('return a range of only 1 unloaded row', () => {
|
|
27
|
+
const isRowLoaded = vi.fn((index: number) => {
|
|
28
|
+
expect(index).toBeGreaterThanOrEqual(0)
|
|
29
|
+
expect(index).toBeLessThanOrEqual(2)
|
|
30
|
+
|
|
31
|
+
return index !== 1
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
expect(
|
|
35
|
+
scanForUnloadedIndices({
|
|
36
|
+
minimumBatchSize: 0,
|
|
37
|
+
isRowLoaded,
|
|
38
|
+
rowCount: 3,
|
|
39
|
+
startIndex: 0,
|
|
40
|
+
stopIndex: 2,
|
|
41
|
+
}),
|
|
42
|
+
).toEqual([{ startIndex: 1, stopIndex: 1 }])
|
|
43
|
+
|
|
44
|
+
expect(isRowLoaded).toHaveBeenCalledTimes(3)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('return a range of multiple unloaded rows', () => {
|
|
48
|
+
const isRowLoaded = vi.fn((index: number) => {
|
|
49
|
+
expect(index).toBeGreaterThanOrEqual(0)
|
|
50
|
+
expect(index).toBeLessThanOrEqual(2)
|
|
51
|
+
|
|
52
|
+
return index === 2
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
expect(
|
|
56
|
+
scanForUnloadedIndices({
|
|
57
|
+
minimumBatchSize: 0,
|
|
58
|
+
isRowLoaded,
|
|
59
|
+
rowCount: 3,
|
|
60
|
+
startIndex: 0,
|
|
61
|
+
stopIndex: 2,
|
|
62
|
+
}),
|
|
63
|
+
).toEqual([{ startIndex: 0, stopIndex: 1 }])
|
|
64
|
+
|
|
65
|
+
expect(isRowLoaded).toHaveBeenCalledTimes(3)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('return multiple ranges of unloaded rows', () => {
|
|
69
|
+
const isRowLoaded = vi.fn((index: number) => {
|
|
70
|
+
expect(index).toBeGreaterThanOrEqual(0)
|
|
71
|
+
expect(index).toBeLessThanOrEqual(6)
|
|
72
|
+
switch (index) {
|
|
73
|
+
case 0:
|
|
74
|
+
case 3:
|
|
75
|
+
case 5: {
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return false
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
expect(
|
|
84
|
+
scanForUnloadedIndices({
|
|
85
|
+
minimumBatchSize: 0,
|
|
86
|
+
isRowLoaded,
|
|
87
|
+
rowCount: 7,
|
|
88
|
+
startIndex: 0,
|
|
89
|
+
stopIndex: 6,
|
|
90
|
+
}),
|
|
91
|
+
).toEqual([
|
|
92
|
+
{ startIndex: 1, stopIndex: 2 },
|
|
93
|
+
{ startIndex: 4, stopIndex: 4 },
|
|
94
|
+
{ startIndex: 6, stopIndex: 6 },
|
|
95
|
+
])
|
|
96
|
+
|
|
97
|
+
expect(isRowLoaded).toHaveBeenCalledTimes(7)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('return respect the minimum batch size param when fetching rows ahead', () => {
|
|
101
|
+
const isRowLoaded = vi.fn((index: number) => {
|
|
102
|
+
expect(index).toBeGreaterThanOrEqual(0)
|
|
103
|
+
expect(index).toBeLessThanOrEqual(9)
|
|
104
|
+
|
|
105
|
+
return index < 4
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
expect(
|
|
109
|
+
scanForUnloadedIndices({
|
|
110
|
+
minimumBatchSize: 4,
|
|
111
|
+
isRowLoaded,
|
|
112
|
+
rowCount: 10,
|
|
113
|
+
startIndex: 0,
|
|
114
|
+
stopIndex: 4,
|
|
115
|
+
}),
|
|
116
|
+
).toEqual([{ startIndex: 4, stopIndex: 7 }])
|
|
117
|
+
|
|
118
|
+
expect(isRowLoaded).toHaveBeenCalledTimes(8)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('return respect the minimum batch size param when fetching rows behind', () => {
|
|
122
|
+
const isRowLoaded = vi.fn((index: number) => {
|
|
123
|
+
expect(index).toBeGreaterThanOrEqual(0)
|
|
124
|
+
expect(index).toBeLessThanOrEqual(9)
|
|
125
|
+
|
|
126
|
+
return index > 6
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
expect(
|
|
130
|
+
scanForUnloadedIndices({
|
|
131
|
+
minimumBatchSize: 4,
|
|
132
|
+
isRowLoaded,
|
|
133
|
+
rowCount: 10,
|
|
134
|
+
startIndex: 6,
|
|
135
|
+
stopIndex: 9,
|
|
136
|
+
}),
|
|
137
|
+
).toEqual([{ startIndex: 3, stopIndex: 6 }])
|
|
138
|
+
|
|
139
|
+
expect(isRowLoaded).toHaveBeenCalledTimes(7)
|
|
140
|
+
})
|
|
141
|
+
})
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Indices } from './types'
|
|
2
|
+
|
|
3
|
+
export function scanForUnloadedIndices({
|
|
4
|
+
isRowLoaded,
|
|
5
|
+
minimumBatchSize,
|
|
6
|
+
rowCount,
|
|
7
|
+
startIndex,
|
|
8
|
+
stopIndex,
|
|
9
|
+
}: {
|
|
10
|
+
isRowLoaded: (index: number) => boolean;
|
|
11
|
+
minimumBatchSize: number;
|
|
12
|
+
rowCount: number;
|
|
13
|
+
startIndex: number;
|
|
14
|
+
stopIndex: number;
|
|
15
|
+
}): Indices[] {
|
|
16
|
+
const indices: Indices[] = []
|
|
17
|
+
|
|
18
|
+
let currentStartIndex = -1
|
|
19
|
+
let currentStopIndex = -1
|
|
20
|
+
|
|
21
|
+
for (let index = startIndex; index <= stopIndex; index++) {
|
|
22
|
+
if (!isRowLoaded(index)) {
|
|
23
|
+
currentStopIndex = index
|
|
24
|
+
if (currentStartIndex < 0) {
|
|
25
|
+
currentStartIndex = index
|
|
26
|
+
}
|
|
27
|
+
} else if (currentStopIndex >= 0) {
|
|
28
|
+
indices.push({
|
|
29
|
+
startIndex: currentStartIndex,
|
|
30
|
+
stopIndex: currentStopIndex,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
currentStartIndex = currentStopIndex = -1
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Scan forward to satisfy the minimum batch size.
|
|
38
|
+
if (currentStopIndex >= 0) {
|
|
39
|
+
const potentialStopIndex = Math.min(
|
|
40
|
+
Math.max(currentStopIndex, currentStartIndex + minimumBatchSize - 1),
|
|
41
|
+
rowCount - 1,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
for (
|
|
45
|
+
let index = currentStopIndex + 1;
|
|
46
|
+
index <= potentialStopIndex;
|
|
47
|
+
index++
|
|
48
|
+
) {
|
|
49
|
+
if (!isRowLoaded(index)) {
|
|
50
|
+
currentStopIndex = index
|
|
51
|
+
} else {
|
|
52
|
+
break
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
indices.push({
|
|
57
|
+
startIndex: currentStartIndex,
|
|
58
|
+
stopIndex: currentStopIndex,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check to see if our first range ended prematurely.
|
|
63
|
+
// In this case we should scan backwards to satisfy the minimum batch size.
|
|
64
|
+
if (indices.length) {
|
|
65
|
+
const firstIndices = indices[0]
|
|
66
|
+
|
|
67
|
+
while (
|
|
68
|
+
firstIndices.stopIndex - firstIndices.startIndex + 1 < minimumBatchSize &&
|
|
69
|
+
firstIndices.startIndex > 0
|
|
70
|
+
) {
|
|
71
|
+
const index = firstIndices.startIndex - 1
|
|
72
|
+
|
|
73
|
+
if (!isRowLoaded(index)) {
|
|
74
|
+
firstIndices.startIndex = index
|
|
75
|
+
} else {
|
|
76
|
+
break
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return indices
|
|
82
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type Indices = {
|
|
2
|
+
startIndex: number;
|
|
3
|
+
stopIndex: number;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type OnRowsRendered = (indices: Indices) => void;
|
|
7
|
+
|
|
8
|
+
export type InfiniteLoaderProps = {
|
|
9
|
+
/**
|
|
10
|
+
* Function responsible for tracking the loaded state of each item.
|
|
11
|
+
*/
|
|
12
|
+
isRowLoaded: (index: number) => boolean;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Callback to be invoked when more rows must be loaded.
|
|
16
|
+
* It should return a Promise that is resolved once all data has finished loading.
|
|
17
|
+
*/
|
|
18
|
+
loadMoreRows: (startIndex: number, stopIndex: number) => Promise<void>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Minimum number of rows to be loaded at a time; defaults to 10.
|
|
22
|
+
* This property can be used to batch requests to reduce HTTP requests.
|
|
23
|
+
*/
|
|
24
|
+
minimumBatchSize?: number;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Threshold at which to pre-fetch data; defaults to 15.
|
|
28
|
+
* A threshold of 15 means that data will start loading when a user scrolls within 15 rows.
|
|
29
|
+
*/
|
|
30
|
+
threshold?: number;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Number of rows in list; can be arbitrary high number if actual number is unknown.
|
|
34
|
+
*/
|
|
35
|
+
rowCount: number;
|
|
36
|
+
};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { useInfiniteLoader } from './useInfiniteLoader'
|
|
4
|
+
import type { InfiniteLoaderProps } from './types'
|
|
5
|
+
|
|
6
|
+
describe('useInfiniteLoader', () => {
|
|
7
|
+
test('should not load rows that have already been loaded', () => {
|
|
8
|
+
const isRowLoaded = vi.fn(() => true)
|
|
9
|
+
const loadMoreRows = vi.fn(() => Promise.resolve())
|
|
10
|
+
|
|
11
|
+
const props: InfiniteLoaderProps = {
|
|
12
|
+
isRowLoaded,
|
|
13
|
+
loadMoreRows,
|
|
14
|
+
rowCount: 10,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { onRowsRendered } = useInfiniteLoader(props)
|
|
18
|
+
|
|
19
|
+
expect(isRowLoaded).not.toHaveBeenCalled()
|
|
20
|
+
|
|
21
|
+
onRowsRendered({
|
|
22
|
+
startIndex: 0,
|
|
23
|
+
stopIndex: 9,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
expect(isRowLoaded).toHaveBeenCalled()
|
|
27
|
+
expect(loadMoreRows).not.toHaveBeenCalled()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('should call loadMoreRows when needed', () => {
|
|
31
|
+
const loadMoreRows = vi.fn(() => Promise.resolve())
|
|
32
|
+
|
|
33
|
+
const props: InfiniteLoaderProps = {
|
|
34
|
+
isRowLoaded: (index) => index <= 2,
|
|
35
|
+
loadMoreRows,
|
|
36
|
+
rowCount: 5,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { onRowsRendered } = useInfiniteLoader(props)
|
|
40
|
+
|
|
41
|
+
expect(loadMoreRows).not.toHaveBeenCalled()
|
|
42
|
+
|
|
43
|
+
onRowsRendered({
|
|
44
|
+
startIndex: 0,
|
|
45
|
+
stopIndex: 4,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
expect(loadMoreRows).toHaveBeenCalled()
|
|
49
|
+
expect(loadMoreRows).toHaveBeenLastCalledWith(3, 4)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('should work with reactive props', () => {
|
|
53
|
+
const loadMoreRows = vi.fn(() => Promise.resolve())
|
|
54
|
+
const rowCount = ref(5)
|
|
55
|
+
|
|
56
|
+
const props = ref<InfiniteLoaderProps>({
|
|
57
|
+
isRowLoaded: (index) => index <= 2,
|
|
58
|
+
loadMoreRows,
|
|
59
|
+
rowCount: rowCount.value,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const { onRowsRendered } = useInfiniteLoader(props)
|
|
63
|
+
|
|
64
|
+
onRowsRendered({
|
|
65
|
+
startIndex: 0,
|
|
66
|
+
stopIndex: 4,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
expect(loadMoreRows).toHaveBeenCalledWith(3, 4)
|
|
70
|
+
|
|
71
|
+
// Update rowCount
|
|
72
|
+
rowCount.value = 10
|
|
73
|
+
props.value = {
|
|
74
|
+
...props.value,
|
|
75
|
+
rowCount: rowCount.value,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
onRowsRendered({
|
|
79
|
+
startIndex: 0,
|
|
80
|
+
stopIndex: 9,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// Should now load indices 3-9 (excluding already pending 3-4)
|
|
84
|
+
expect(loadMoreRows).toHaveBeenLastCalledWith(5, 9)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('should respect minimumBatchSize', () => {
|
|
88
|
+
const loadMoreRows = vi.fn(() => Promise.resolve())
|
|
89
|
+
|
|
90
|
+
const props: InfiniteLoaderProps = {
|
|
91
|
+
isRowLoaded: (index) => index < 4,
|
|
92
|
+
loadMoreRows,
|
|
93
|
+
rowCount: 10,
|
|
94
|
+
minimumBatchSize: 4,
|
|
95
|
+
threshold: 0, // Disable threshold for this test
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const { onRowsRendered } = useInfiniteLoader(props)
|
|
99
|
+
|
|
100
|
+
onRowsRendered({
|
|
101
|
+
startIndex: 0,
|
|
102
|
+
stopIndex: 4,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// Should load at least minimumBatchSize (4) rows
|
|
106
|
+
expect(loadMoreRows).toHaveBeenCalledWith(4, 7)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('should respect threshold for pre-fetching', () => {
|
|
110
|
+
const loadMoreRows = vi.fn(() => Promise.resolve())
|
|
111
|
+
|
|
112
|
+
const props: InfiniteLoaderProps = {
|
|
113
|
+
isRowLoaded: (index) => index < 5,
|
|
114
|
+
loadMoreRows,
|
|
115
|
+
rowCount: 100,
|
|
116
|
+
threshold: 10,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const { onRowsRendered } = useInfiniteLoader(props)
|
|
120
|
+
|
|
121
|
+
// User is viewing rows 10-20
|
|
122
|
+
onRowsRendered({
|
|
123
|
+
startIndex: 10,
|
|
124
|
+
stopIndex: 20,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// With threshold 10, should check indices 0-30
|
|
128
|
+
// Rows 5-30 should be loaded
|
|
129
|
+
expect(loadMoreRows).toHaveBeenCalledWith(5, 30)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('should track pending rows', () => {
|
|
133
|
+
const loadMoreRows = vi.fn(() => Promise.resolve())
|
|
134
|
+
|
|
135
|
+
const props: InfiniteLoaderProps = {
|
|
136
|
+
isRowLoaded: (index) => index <= 2,
|
|
137
|
+
loadMoreRows,
|
|
138
|
+
rowCount: 10,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const { onRowsRendered, pendingRowsCache } = useInfiniteLoader(props)
|
|
142
|
+
|
|
143
|
+
expect(pendingRowsCache.size).toBe(0)
|
|
144
|
+
|
|
145
|
+
onRowsRendered({
|
|
146
|
+
startIndex: 0,
|
|
147
|
+
stopIndex: 5,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
// Rows 3, 4, 5 should be marked as pending
|
|
151
|
+
expect(pendingRowsCache.has(3)).toBe(true)
|
|
152
|
+
expect(pendingRowsCache.has(4)).toBe(true)
|
|
153
|
+
expect(pendingRowsCache.has(5)).toBe(true)
|
|
154
|
+
expect(pendingRowsCache.has(2)).toBe(false)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('should not load rows that are already pending', () => {
|
|
158
|
+
const loadMoreRows = vi.fn(() => Promise.resolve())
|
|
159
|
+
|
|
160
|
+
const props: InfiniteLoaderProps = {
|
|
161
|
+
isRowLoaded: (index) => index <= 2,
|
|
162
|
+
loadMoreRows,
|
|
163
|
+
rowCount: 10,
|
|
164
|
+
threshold: 0, // Disable threshold for this test
|
|
165
|
+
minimumBatchSize: 0, // Disable minimum batch size for this test
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const { onRowsRendered } = useInfiniteLoader(props)
|
|
169
|
+
|
|
170
|
+
// First call loads rows 3-5
|
|
171
|
+
onRowsRendered({
|
|
172
|
+
startIndex: 0,
|
|
173
|
+
stopIndex: 5,
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
expect(loadMoreRows).toHaveBeenCalledTimes(1)
|
|
177
|
+
expect(loadMoreRows).toHaveBeenLastCalledWith(3, 5)
|
|
178
|
+
|
|
179
|
+
// Second call with same range shouldn't load again
|
|
180
|
+
onRowsRendered({
|
|
181
|
+
startIndex: 0,
|
|
182
|
+
stopIndex: 5,
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// Should still be called only once
|
|
186
|
+
expect(loadMoreRows).toHaveBeenCalledTimes(1)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('should clean up pending cache when rows are loaded', () => {
|
|
190
|
+
const loadMoreRows = vi.fn(() => Promise.resolve())
|
|
191
|
+
const loadedRows = new Set<number>([0, 1, 2])
|
|
192
|
+
|
|
193
|
+
const props: InfiniteLoaderProps = {
|
|
194
|
+
isRowLoaded: (index) => loadedRows.has(index),
|
|
195
|
+
loadMoreRows,
|
|
196
|
+
rowCount: 10,
|
|
197
|
+
threshold: 0,
|
|
198
|
+
minimumBatchSize: 0,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const { onRowsRendered, pendingRowsCache } = useInfiniteLoader(props)
|
|
202
|
+
|
|
203
|
+
// First call loads rows 3-5
|
|
204
|
+
onRowsRendered({
|
|
205
|
+
startIndex: 0,
|
|
206
|
+
stopIndex: 5,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
expect(pendingRowsCache.size).toBe(3) // Rows 3, 4, 5 are pending
|
|
210
|
+
expect(pendingRowsCache.has(3)).toBe(true)
|
|
211
|
+
expect(pendingRowsCache.has(4)).toBe(true)
|
|
212
|
+
expect(pendingRowsCache.has(5)).toBe(true)
|
|
213
|
+
|
|
214
|
+
// Simulate rows 3 and 4 being loaded
|
|
215
|
+
loadedRows.add(3)
|
|
216
|
+
loadedRows.add(4)
|
|
217
|
+
|
|
218
|
+
// Next call should clean up loaded rows from pending cache
|
|
219
|
+
onRowsRendered({
|
|
220
|
+
startIndex: 3,
|
|
221
|
+
stopIndex: 7,
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// Rows 3 and 4 should be removed from pending cache
|
|
225
|
+
expect(pendingRowsCache.has(3)).toBe(false)
|
|
226
|
+
expect(pendingRowsCache.has(4)).toBe(false)
|
|
227
|
+
// Row 5 should still be pending
|
|
228
|
+
expect(pendingRowsCache.has(5)).toBe(true)
|
|
229
|
+
// Rows 6 and 7 should now be pending
|
|
230
|
+
expect(pendingRowsCache.has(6)).toBe(true)
|
|
231
|
+
expect(pendingRowsCache.has(7)).toBe(true)
|
|
232
|
+
|
|
233
|
+
// Total should be 3 (rows 5, 6, 7)
|
|
234
|
+
expect(pendingRowsCache.size).toBe(3)
|
|
235
|
+
})
|
|
236
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { computed, reactive, toValue, type MaybeRefOrGetter } from 'vue'
|
|
2
|
+
import { scanForUnloadedIndices } from './scanForUnloadedIndices'
|
|
3
|
+
import type { Indices, OnRowsRendered, InfiniteLoaderProps } from './types'
|
|
4
|
+
|
|
5
|
+
export function useInfiniteLoader(
|
|
6
|
+
props: MaybeRefOrGetter<InfiniteLoaderProps>,
|
|
7
|
+
) {
|
|
8
|
+
// Create a reactive Set to track pending rows
|
|
9
|
+
const pendingRowsCache = reactive(new Set<number>())
|
|
10
|
+
|
|
11
|
+
// Computed values that react to props changes
|
|
12
|
+
const isRowLoaded = computed(
|
|
13
|
+
() => toValue(props).isRowLoaded,
|
|
14
|
+
)
|
|
15
|
+
const loadMoreRows = computed(
|
|
16
|
+
() => toValue(props).loadMoreRows,
|
|
17
|
+
)
|
|
18
|
+
const minimumBatchSize = computed(
|
|
19
|
+
() => toValue(props).minimumBatchSize ?? 10,
|
|
20
|
+
)
|
|
21
|
+
const rowCount = computed(
|
|
22
|
+
() => toValue(props).rowCount,
|
|
23
|
+
)
|
|
24
|
+
const threshold = computed(
|
|
25
|
+
() => toValue(props).threshold ?? 15,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
// Check if a row is loaded or pending
|
|
29
|
+
const isRowLoadedOrPending = (index: number): boolean => {
|
|
30
|
+
if (isRowLoaded.value(index)) {
|
|
31
|
+
// Clean up: remove from pending cache if it's now loaded
|
|
32
|
+
if (pendingRowsCache.has(index)) {
|
|
33
|
+
pendingRowsCache.delete(index)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return pendingRowsCache.has(index)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Main callback to be passed to VList's onRowsRendered
|
|
43
|
+
const onRowsRendered: OnRowsRendered = ({ startIndex, stopIndex }: Indices) => {
|
|
44
|
+
// Clean up: remove loaded rows from pending cache
|
|
45
|
+
// This prevents unbounded memory growth in long-running apps
|
|
46
|
+
if (pendingRowsCache.size > 0) {
|
|
47
|
+
const loadedIndices: number[] = []
|
|
48
|
+
|
|
49
|
+
pendingRowsCache.forEach((index) => {
|
|
50
|
+
if (isRowLoaded.value(index)) {
|
|
51
|
+
loadedIndices.push(index)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
loadedIndices.forEach((index) => pendingRowsCache.delete(index))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const unloadedIndices = scanForUnloadedIndices({
|
|
59
|
+
isRowLoaded: isRowLoadedOrPending,
|
|
60
|
+
minimumBatchSize: minimumBatchSize.value,
|
|
61
|
+
rowCount: rowCount.value,
|
|
62
|
+
startIndex: Math.max(0, startIndex - threshold.value),
|
|
63
|
+
stopIndex: Math.min(rowCount.value - 1, stopIndex + threshold.value),
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
for (let index = 0; index < unloadedIndices.length; index++) {
|
|
67
|
+
const { startIndex: unloadedStartIndex, stopIndex: unloadedStopIndex } =
|
|
68
|
+
unloadedIndices[index]
|
|
69
|
+
|
|
70
|
+
// Mark all indices in this range as pending
|
|
71
|
+
for (
|
|
72
|
+
let idx = unloadedStartIndex;
|
|
73
|
+
idx <= unloadedStopIndex;
|
|
74
|
+
idx++
|
|
75
|
+
) {
|
|
76
|
+
pendingRowsCache.add(idx)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Fire the load request (fire-and-forget pattern)
|
|
80
|
+
loadMoreRows.value(unloadedStartIndex, unloadedStopIndex)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
onRowsRendered,
|
|
86
|
+
pendingRowsCache,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { assert } from '../utils/assert'
|
|
2
|
+
import type { Bounds, CachedBounds, SizeFunction } from './types'
|
|
3
|
+
|
|
4
|
+
export function createCachedBounds<Props extends object>({
|
|
5
|
+
itemCount,
|
|
6
|
+
itemProps,
|
|
7
|
+
itemSize,
|
|
8
|
+
}: {
|
|
9
|
+
itemCount: number;
|
|
10
|
+
itemProps: Props;
|
|
11
|
+
itemSize: number | SizeFunction<Props>;
|
|
12
|
+
}): CachedBounds {
|
|
13
|
+
const cache = new Map<number, Bounds>()
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
get(index: number) {
|
|
17
|
+
assert(index < itemCount, `Invalid index ${index}`)
|
|
18
|
+
|
|
19
|
+
while (cache.size - 1 < index) {
|
|
20
|
+
const currentIndex = cache.size
|
|
21
|
+
|
|
22
|
+
let size: number
|
|
23
|
+
|
|
24
|
+
switch (typeof itemSize) {
|
|
25
|
+
case 'function': {
|
|
26
|
+
size = itemSize(currentIndex, itemProps)
|
|
27
|
+
break
|
|
28
|
+
}
|
|
29
|
+
case 'number': {
|
|
30
|
+
size = itemSize
|
|
31
|
+
break
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (currentIndex === 0) {
|
|
36
|
+
cache.set(currentIndex, {
|
|
37
|
+
size,
|
|
38
|
+
scrollOffset: 0,
|
|
39
|
+
})
|
|
40
|
+
} else {
|
|
41
|
+
const previousRowBounds = cache.get(currentIndex - 1)
|
|
42
|
+
|
|
43
|
+
assert(
|
|
44
|
+
previousRowBounds !== undefined,
|
|
45
|
+
`Unexpected bounds cache miss for index ${index}`,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
cache.set(currentIndex, {
|
|
49
|
+
scrollOffset:
|
|
50
|
+
previousRowBounds.scrollOffset + previousRowBounds.size,
|
|
51
|
+
size,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const bounds = cache.get(index)
|
|
57
|
+
|
|
58
|
+
assert(
|
|
59
|
+
bounds !== undefined,
|
|
60
|
+
`Unexpected bounds cache miss for index ${index}`,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return bounds
|
|
64
|
+
},
|
|
65
|
+
set(index: number, bounds: Bounds) {
|
|
66
|
+
cache.set(index, bounds)
|
|
67
|
+
},
|
|
68
|
+
get size() {
|
|
69
|
+
return cache.size
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CachedBounds, SizeFunction } from './types'
|
|
2
|
+
import { assert } from '../utils/assert'
|
|
3
|
+
|
|
4
|
+
export function getEstimatedSize<Props extends object>({
|
|
5
|
+
cachedBounds,
|
|
6
|
+
itemCount,
|
|
7
|
+
itemSize,
|
|
8
|
+
}: {
|
|
9
|
+
cachedBounds: CachedBounds;
|
|
10
|
+
itemCount: number;
|
|
11
|
+
itemSize: number | SizeFunction<Props>;
|
|
12
|
+
}) {
|
|
13
|
+
if (itemCount === 0) {
|
|
14
|
+
return 0
|
|
15
|
+
} else if (typeof itemSize === 'number') {
|
|
16
|
+
return itemCount * itemSize
|
|
17
|
+
} else {
|
|
18
|
+
const bounds = cachedBounds.get(
|
|
19
|
+
cachedBounds.size === 0 ? 0 : cachedBounds.size - 1,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
assert(bounds !== undefined, 'Unexpected bounds cache miss')
|
|
23
|
+
|
|
24
|
+
const averageItemSize =
|
|
25
|
+
(bounds.scrollOffset + bounds.size) / cachedBounds.size
|
|
26
|
+
|
|
27
|
+
return itemCount * averageItemSize
|
|
28
|
+
}
|
|
29
|
+
}
|