@indielayer/ui 1.16.0 → 1.18.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 +2 -2
- package/docs/assets/css/tailwind.css +6 -0
- package/docs/components/common/CodePreview.vue +14 -9
- package/docs/components/common/DocsFeatures.vue +41 -0
- package/docs/components/common/DocsHero.vue +216 -0
- package/docs/components/common/DocumentPage.vue +99 -112
- package/docs/components/common/ExampleBlocks.vue +157 -0
- package/docs/components/menu/DocsMenu.vue +3 -0
- package/docs/components/toolbar/Toolbar.vue +11 -2
- package/docs/components/toolbar/ToolbarColorToggle.vue +4 -4
- package/docs/components/toolbar/ToolbarSearch.vue +59 -62
- package/docs/composables/useDocMeta.ts +47 -0
- package/docs/icons.ts +28 -0
- package/docs/layouts/default.vue +1 -3
- package/docs/layouts/simple.vue +3 -1
- package/docs/main.ts +5 -0
- package/docs/pages/colors.vue +56 -47
- 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/select/size.vue +1 -1
- package/docs/pages/component/select/usage.vue +14 -7
- 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/pages/error.vue +5 -3
- package/docs/pages/icons.vue +64 -54
- package/docs/pages/index.vue +93 -82
- package/docs/pages/typography.vue +38 -28
- package/docs/router/index.ts +31 -3
- package/docs/search/components.json +1 -1
- package/docs/search/index.json +1 -0
- package/lib/components/container/theme/Container.base.theme.js +1 -1
- package/lib/components/divider/theme/Divider.base.theme.js +1 -1
- package/lib/components/input/Input.vue.js +23 -24
- package/lib/components/select/Select.vue.d.ts +16 -27
- package/lib/components/select/Select.vue.js +452 -345
- package/lib/components/table/Table.vue.js +1 -1
- 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 +159 -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 +68 -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 +8 -3
- package/src/components/container/theme/Container.base.theme.ts +1 -1
- package/src/components/divider/theme/Divider.base.theme.ts +1 -1
- package/src/components/input/Input.vue +1 -2
- package/src/components/select/Select.vue +97 -20
- package/src/components/table/Table.vue +1 -1
- 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 +164 -0
- package/src/virtual/components/virtualList/VirtualList.vue +227 -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 +197 -0
- package/src/virtual/components/virtualList/useDynamicRowHeight.ts +149 -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 +21 -0
- package/src/virtual/utils/shallowCompare.ts +29 -0
- package/volar.d.ts +3 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { ref, nextTick } from 'vue'
|
|
3
|
+
import { useDynamicRowHeight, DATA_ATTRIBUTE_LIST_INDEX } from './useDynamicRowHeight'
|
|
4
|
+
import type { DynamicRowHeight } from './types'
|
|
5
|
+
import { mockResizeObserver, setElementSize } from '../../test-utils/mockResizeObserver'
|
|
6
|
+
|
|
7
|
+
describe('useDynamicRowHeight', () => {
|
|
8
|
+
let unmock: (() => void) | undefined
|
|
9
|
+
let instances: DynamicRowHeight[] = []
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
unmock = mockResizeObserver()
|
|
13
|
+
instances = []
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
instances.forEach((instance) => instance.cleanup())
|
|
18
|
+
if (unmock) {
|
|
19
|
+
unmock()
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
function createDynamicRowHeight(
|
|
24
|
+
...args: Parameters<typeof useDynamicRowHeight>
|
|
25
|
+
): DynamicRowHeight {
|
|
26
|
+
const instance = useDynamicRowHeight(...args)
|
|
27
|
+
|
|
28
|
+
instances.push(instance)
|
|
29
|
+
|
|
30
|
+
return instance
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('getAverageRowHeight', () => {
|
|
34
|
+
test('returns an initial estimate based on the defaultRowHeight', () => {
|
|
35
|
+
const dynamicRowHeight = createDynamicRowHeight({
|
|
36
|
+
defaultRowHeight: 100,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
expect(dynamicRowHeight.getAverageRowHeight()).toBe(100)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('returns an estimate based on measured rows', () => {
|
|
43
|
+
const dynamicRowHeight = createDynamicRowHeight({
|
|
44
|
+
defaultRowHeight: 100,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
dynamicRowHeight.setRowHeight(0, 10)
|
|
48
|
+
dynamicRowHeight.setRowHeight(1, 20)
|
|
49
|
+
expect(dynamicRowHeight.getAverageRowHeight()).toBe(15)
|
|
50
|
+
|
|
51
|
+
dynamicRowHeight.setRowHeight(2, 30)
|
|
52
|
+
expect(dynamicRowHeight.getAverageRowHeight()).toBe(20)
|
|
53
|
+
|
|
54
|
+
dynamicRowHeight.setRowHeight(2, 15)
|
|
55
|
+
expect(dynamicRowHeight.getAverageRowHeight()).toBe(15)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('resets when key changes', async () => {
|
|
59
|
+
const key = ref('a')
|
|
60
|
+
const dynamicRowHeight = createDynamicRowHeight({
|
|
61
|
+
defaultRowHeight: 100,
|
|
62
|
+
key,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
dynamicRowHeight.setRowHeight(0, 10)
|
|
66
|
+
expect(dynamicRowHeight.getAverageRowHeight()).toBe(10)
|
|
67
|
+
|
|
68
|
+
// Key hasn't changed
|
|
69
|
+
await nextTick()
|
|
70
|
+
expect(dynamicRowHeight.getAverageRowHeight()).toBe(10)
|
|
71
|
+
|
|
72
|
+
// Change key
|
|
73
|
+
key.value = 'b'
|
|
74
|
+
await nextTick()
|
|
75
|
+
expect(dynamicRowHeight.getAverageRowHeight()).toBe(100)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('getRowHeight', () => {
|
|
80
|
+
test('returns estimated height for a row that has not yet been measured', () => {
|
|
81
|
+
const dynamicRowHeight = createDynamicRowHeight({
|
|
82
|
+
defaultRowHeight: 100,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
expect(dynamicRowHeight.getRowHeight(0)).toBe(100)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('returns the most recently measured size', () => {
|
|
89
|
+
const dynamicRowHeight = createDynamicRowHeight({
|
|
90
|
+
defaultRowHeight: 100,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
dynamicRowHeight.setRowHeight(0, 15)
|
|
94
|
+
dynamicRowHeight.setRowHeight(1, 20)
|
|
95
|
+
dynamicRowHeight.setRowHeight(3, 25)
|
|
96
|
+
expect(dynamicRowHeight.getRowHeight(0)).toBe(15)
|
|
97
|
+
expect(dynamicRowHeight.getRowHeight(1)).toBe(20)
|
|
98
|
+
expect(dynamicRowHeight.getRowHeight(2)).toBe(100)
|
|
99
|
+
expect(dynamicRowHeight.getRowHeight(3)).toBe(25)
|
|
100
|
+
|
|
101
|
+
dynamicRowHeight.setRowHeight(1, 25)
|
|
102
|
+
expect(dynamicRowHeight.getRowHeight(1)).toBe(25)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('resets when key changes', async () => {
|
|
106
|
+
const key = ref('a')
|
|
107
|
+
const dynamicRowHeight = createDynamicRowHeight({
|
|
108
|
+
defaultRowHeight: 100,
|
|
109
|
+
key,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
dynamicRowHeight.setRowHeight(0, 10)
|
|
113
|
+
expect(dynamicRowHeight.getRowHeight(0)).toBe(10)
|
|
114
|
+
|
|
115
|
+
// Key hasn't changed
|
|
116
|
+
await nextTick()
|
|
117
|
+
expect(dynamicRowHeight.getRowHeight(0)).toBe(10)
|
|
118
|
+
|
|
119
|
+
// Change key
|
|
120
|
+
key.value = 'b'
|
|
121
|
+
await nextTick()
|
|
122
|
+
expect(dynamicRowHeight.getRowHeight(0)).toBe(100)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe('observeRowElements', () => {
|
|
127
|
+
function createRowElement(index: number) {
|
|
128
|
+
const element = document.createElement('div')
|
|
129
|
+
|
|
130
|
+
element.setAttribute(DATA_ATTRIBUTE_LIST_INDEX, '' + index)
|
|
131
|
+
|
|
132
|
+
return element
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
test('should update cache when an observed element is resized', async () => {
|
|
136
|
+
const dynamicRowHeight = createDynamicRowHeight({
|
|
137
|
+
defaultRowHeight: 100,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const elementA = createRowElement(0)
|
|
141
|
+
const elementB = createRowElement(1)
|
|
142
|
+
|
|
143
|
+
dynamicRowHeight.observeRowElements([elementA, elementB])
|
|
144
|
+
await nextTick()
|
|
145
|
+
expect(dynamicRowHeight.getRowHeight(0)).toBe(100)
|
|
146
|
+
expect(dynamicRowHeight.getRowHeight(1)).toBe(100)
|
|
147
|
+
|
|
148
|
+
setElementSize({
|
|
149
|
+
element: elementB,
|
|
150
|
+
width: 100,
|
|
151
|
+
height: 20,
|
|
152
|
+
})
|
|
153
|
+
await nextTick()
|
|
154
|
+
expect(dynamicRowHeight.getRowHeight(0)).toBe(100)
|
|
155
|
+
expect(dynamicRowHeight.getRowHeight(1)).toBe(20)
|
|
156
|
+
|
|
157
|
+
setElementSize({
|
|
158
|
+
element: elementA,
|
|
159
|
+
width: 100,
|
|
160
|
+
height: 15,
|
|
161
|
+
})
|
|
162
|
+
await nextTick()
|
|
163
|
+
expect(dynamicRowHeight.getRowHeight(0)).toBe(15)
|
|
164
|
+
expect(dynamicRowHeight.getRowHeight(1)).toBe(20)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test('should unobserve an element when requested', async () => {
|
|
168
|
+
const dynamicRowHeight = createDynamicRowHeight({
|
|
169
|
+
defaultRowHeight: 100,
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const element = createRowElement(0)
|
|
173
|
+
|
|
174
|
+
const unobserve = dynamicRowHeight.observeRowElements([element])
|
|
175
|
+
|
|
176
|
+
setElementSize({
|
|
177
|
+
element,
|
|
178
|
+
width: 100,
|
|
179
|
+
height: 10,
|
|
180
|
+
})
|
|
181
|
+
await nextTick()
|
|
182
|
+
expect(dynamicRowHeight.getRowHeight(0)).toBe(10)
|
|
183
|
+
|
|
184
|
+
unobserve()
|
|
185
|
+
|
|
186
|
+
setElementSize({
|
|
187
|
+
element,
|
|
188
|
+
width: 100,
|
|
189
|
+
height: 20,
|
|
190
|
+
})
|
|
191
|
+
await nextTick()
|
|
192
|
+
expect(dynamicRowHeight.getRowHeight(0)).toBe(10)
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// setRowHeight is tested indirectly by "getAverageRowHeight" and "getRowHeight" blocks above
|
|
197
|
+
})
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { getCurrentInstance, onBeforeUnmount, ref, watch, type Ref } from 'vue'
|
|
2
|
+
import { assert } from '../../utils/assert'
|
|
3
|
+
import type { DynamicRowHeight } from './types'
|
|
4
|
+
|
|
5
|
+
export const DATA_ATTRIBUTE_LIST_INDEX = 'data-virtual-index'
|
|
6
|
+
|
|
7
|
+
export function useDynamicRowHeight({
|
|
8
|
+
defaultRowHeight,
|
|
9
|
+
key,
|
|
10
|
+
}: {
|
|
11
|
+
defaultRowHeight: number;
|
|
12
|
+
key?: Ref<string | number | undefined> | string | number;
|
|
13
|
+
}): DynamicRowHeight {
|
|
14
|
+
const map = ref<Map<number, number>>(new Map())
|
|
15
|
+
// Revision counter to trigger reactivity when map changes
|
|
16
|
+
const revision = ref(0)
|
|
17
|
+
|
|
18
|
+
// Handle reactive key changes
|
|
19
|
+
if (key !== undefined && typeof key === 'object' && 'value' in key) {
|
|
20
|
+
watch(key as Ref<string | number | undefined>, () => {
|
|
21
|
+
map.value = new Map()
|
|
22
|
+
revision.value++
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const getAverageRowHeight = () => {
|
|
27
|
+
// Access revision to track dependency
|
|
28
|
+
revision.value
|
|
29
|
+
|
|
30
|
+
let totalHeight = 0
|
|
31
|
+
|
|
32
|
+
map.value.forEach((height) => {
|
|
33
|
+
totalHeight += height
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
if (totalHeight === 0) {
|
|
37
|
+
return defaultRowHeight
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return totalHeight / map.value.size
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const getRowHeight = (index: number) => {
|
|
44
|
+
// Access revision to track dependency
|
|
45
|
+
revision.value
|
|
46
|
+
|
|
47
|
+
const measuredHeight = map.value.get(index)
|
|
48
|
+
|
|
49
|
+
if (measuredHeight !== undefined) {
|
|
50
|
+
return measuredHeight
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Don't cache default height here - let ResizeObserver do the measuring
|
|
54
|
+
// This prevents stale default heights from blocking updates
|
|
55
|
+
return defaultRowHeight
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const setRowHeight = (index: number, size: number) => {
|
|
59
|
+
if (map.value.get(index) === size) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Create a new Map to trigger reactivity
|
|
64
|
+
const newMap = new Map(map.value)
|
|
65
|
+
|
|
66
|
+
newMap.set(index, size)
|
|
67
|
+
map.value = newMap
|
|
68
|
+
revision.value++
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const resizeObserverCallback = (entries: ResizeObserverEntry[]) => {
|
|
72
|
+
if (entries.length === 0) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Batch updates to avoid triggering multiple recalculations
|
|
77
|
+
let hasChanges = false
|
|
78
|
+
const updates: Array<{ index: number; height: number; }> = []
|
|
79
|
+
|
|
80
|
+
entries.forEach((entry) => {
|
|
81
|
+
const { borderBoxSize, target } = entry
|
|
82
|
+
|
|
83
|
+
const attribute = target.getAttribute(DATA_ATTRIBUTE_LIST_INDEX)
|
|
84
|
+
|
|
85
|
+
assert(
|
|
86
|
+
attribute !== null,
|
|
87
|
+
`Invalid ${DATA_ATTRIBUTE_LIST_INDEX} attribute value`,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
const index = parseInt(attribute)
|
|
91
|
+
|
|
92
|
+
const { blockSize: height } = borderBoxSize[0]
|
|
93
|
+
|
|
94
|
+
if (!height) {
|
|
95
|
+
// Ignore heights that have not yet been measured (e.g. <img> elements that have not yet loaded)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check if height actually changed before updating
|
|
100
|
+
const currentHeight = map.value.get(index)
|
|
101
|
+
|
|
102
|
+
if (currentHeight !== height) {
|
|
103
|
+
updates.push({ index, height })
|
|
104
|
+
hasChanges = true
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// Apply all updates at once
|
|
109
|
+
if (hasChanges) {
|
|
110
|
+
const newMap = new Map(map.value)
|
|
111
|
+
|
|
112
|
+
updates.forEach(({ index, height }) => {
|
|
113
|
+
newMap.set(index, height)
|
|
114
|
+
})
|
|
115
|
+
map.value = newMap
|
|
116
|
+
revision.value++
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const resizeObserver = new ResizeObserver(resizeObserverCallback)
|
|
121
|
+
|
|
122
|
+
function cleanup() {
|
|
123
|
+
resizeObserver.disconnect()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (getCurrentInstance()) {
|
|
127
|
+
onBeforeUnmount(cleanup)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const observeRowElements = (elements: Element[] | NodeListOf<Element>) => {
|
|
131
|
+
const elementsArray = Array.isArray(elements)
|
|
132
|
+
? elements
|
|
133
|
+
: Array.from(elements)
|
|
134
|
+
|
|
135
|
+
elementsArray.forEach((element) => resizeObserver.observe(element))
|
|
136
|
+
|
|
137
|
+
return () => {
|
|
138
|
+
elementsArray.forEach((element) => resizeObserver.unobserve(element))
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
getAverageRowHeight,
|
|
144
|
+
getRowHeight,
|
|
145
|
+
setRowHeight,
|
|
146
|
+
observeRowElements,
|
|
147
|
+
cleanup,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -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
|
+
};
|