@v-c/virtual-list 0.0.1
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/LICENSE +21 -0
- package/dist/Filler.cjs +1 -0
- package/dist/Filler.d.ts +33 -0
- package/dist/Filler.js +57 -0
- package/dist/Item.cjs +1 -0
- package/dist/Item.d.ts +18 -0
- package/dist/Item.js +27 -0
- package/dist/List.cjs +1 -0
- package/dist/List.d.ts +108 -0
- package/dist/List.js +276 -0
- package/dist/ScrollBar.cjs +1 -0
- package/dist/ScrollBar.d.ts +116 -0
- package/dist/ScrollBar.js +193 -0
- package/dist/hooks/useDiffItem.cjs +1 -0
- package/dist/hooks/useDiffItem.d.ts +2 -0
- package/dist/hooks/useDiffItem.js +21 -0
- package/dist/hooks/useFrameWheel.cjs +1 -0
- package/dist/hooks/useFrameWheel.d.ts +11 -0
- package/dist/hooks/useFrameWheel.js +52 -0
- package/dist/hooks/useGetSize.cjs +1 -0
- package/dist/hooks/useGetSize.d.ts +7 -0
- package/dist/hooks/useGetSize.js +24 -0
- package/dist/hooks/useHeights.cjs +1 -0
- package/dist/hooks/useHeights.d.ts +9 -0
- package/dist/hooks/useHeights.js +43 -0
- package/dist/hooks/useMobileTouchMove.cjs +1 -0
- package/dist/hooks/useMobileTouchMove.d.ts +2 -0
- package/dist/hooks/useMobileTouchMove.js +44 -0
- package/dist/hooks/useOriginScroll.cjs +1 -0
- package/dist/hooks/useOriginScroll.d.ts +2 -0
- package/dist/hooks/useOriginScroll.js +17 -0
- package/dist/hooks/useScrollDrag.cjs +1 -0
- package/dist/hooks/useScrollDrag.d.ts +3 -0
- package/dist/hooks/useScrollDrag.js +51 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/interface.cjs +1 -0
- package/dist/interface.d.ts +27 -0
- package/dist/interface.js +1 -0
- package/dist/utils/CacheMap.cjs +1 -0
- package/dist/utils/CacheMap.d.ts +16 -0
- package/dist/utils/CacheMap.js +29 -0
- package/dist/utils/isFirefox.cjs +1 -0
- package/dist/utils/isFirefox.d.ts +2 -0
- package/dist/utils/isFirefox.js +4 -0
- package/dist/utils/scrollbarUtil.cjs +1 -0
- package/dist/utils/scrollbarUtil.d.ts +1 -0
- package/dist/utils/scrollbarUtil.js +7 -0
- package/docs/basic.vue +175 -0
- package/docs/height.vue +48 -0
- package/docs/nest.vue +60 -0
- package/docs/no-virtual.vue +127 -0
- package/docs/switch.vue +101 -0
- package/docs/virtual-list.stories.vue +31 -0
- package/package.json +38 -0
- package/src/Filler.tsx +72 -0
- package/src/Item.tsx +34 -0
- package/src/List.tsx +577 -0
- package/src/ScrollBar.tsx +298 -0
- package/src/__tests__/List.test.ts +59 -0
- package/src/hooks/useDiffItem.ts +27 -0
- package/src/hooks/useFrameWheel.ts +141 -0
- package/src/hooks/useGetSize.ts +44 -0
- package/src/hooks/useHeights.ts +106 -0
- package/src/hooks/useMobileTouchMove.ts +131 -0
- package/src/hooks/useOriginScroll.ts +47 -0
- package/src/hooks/useScrollDrag.ts +123 -0
- package/src/index.ts +5 -0
- package/src/interface.ts +32 -0
- package/src/utils/CacheMap.ts +42 -0
- package/src/utils/isFirefox.ts +3 -0
- package/src/utils/scrollbarUtil.ts +10 -0
- package/vite.config.ts +18 -0
- package/vitest.config.ts +11 -0
package/src/Filler.tsx
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import ResizeObserver from '@v-c/resize-observer'
|
|
2
|
+
import { type CSSProperties, defineComponent, type PropType, type VNode } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface InnerProps {
|
|
5
|
+
role?: string
|
|
6
|
+
id?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default defineComponent({
|
|
10
|
+
name: 'Filler',
|
|
11
|
+
props: {
|
|
12
|
+
prefixCls: String,
|
|
13
|
+
/** Virtual filler height. Should be `count * itemMinHeight` */
|
|
14
|
+
height: Number,
|
|
15
|
+
/** Set offset of visible items. Should be the top of start item position */
|
|
16
|
+
offsetY: Number,
|
|
17
|
+
offsetX: Number,
|
|
18
|
+
scrollWidth: Number,
|
|
19
|
+
onInnerResize: Function as PropType<() => void>,
|
|
20
|
+
innerProps: Object as PropType<InnerProps>,
|
|
21
|
+
rtl: Boolean,
|
|
22
|
+
extra: Object as PropType<VNode>,
|
|
23
|
+
},
|
|
24
|
+
setup(props, { slots }) {
|
|
25
|
+
return () => {
|
|
26
|
+
let outerStyle: CSSProperties = {}
|
|
27
|
+
let innerStyle: CSSProperties = {
|
|
28
|
+
display: 'flex',
|
|
29
|
+
flexDirection: 'column',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (props.offsetY !== undefined) {
|
|
33
|
+
outerStyle = {
|
|
34
|
+
height: `${props.height}px`,
|
|
35
|
+
position: 'relative',
|
|
36
|
+
overflow: 'hidden',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
innerStyle = {
|
|
40
|
+
...innerStyle,
|
|
41
|
+
transform: `translateY(${props.offsetY}px)`,
|
|
42
|
+
[props.rtl ? 'marginRight' : 'marginLeft']: `-${props.offsetX || 0}px`,
|
|
43
|
+
position: 'absolute',
|
|
44
|
+
left: 0,
|
|
45
|
+
right: 0,
|
|
46
|
+
top: 0,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div style={outerStyle}>
|
|
52
|
+
<ResizeObserver
|
|
53
|
+
onResize={({ offsetHeight }) => {
|
|
54
|
+
if (offsetHeight && props.onInnerResize) {
|
|
55
|
+
props.onInnerResize()
|
|
56
|
+
}
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
<div
|
|
60
|
+
style={innerStyle}
|
|
61
|
+
class={props.prefixCls ? `${props.prefixCls}-holder-inner` : undefined}
|
|
62
|
+
{...props.innerProps}
|
|
63
|
+
>
|
|
64
|
+
{slots.default?.()}
|
|
65
|
+
{props.extra}
|
|
66
|
+
</div>
|
|
67
|
+
</ResizeObserver>
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
})
|
package/src/Item.tsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { filterEmpty } from '@v-c/util/dist/props-util'
|
|
2
|
+
import { cloneVNode, defineComponent, type PropType, shallowRef } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface ItemProps {
|
|
5
|
+
setRef: (element: HTMLElement | null) => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default defineComponent({
|
|
9
|
+
name: 'Item',
|
|
10
|
+
props: {
|
|
11
|
+
setRef: {
|
|
12
|
+
type: Function as PropType<(element: HTMLElement | null) => void>,
|
|
13
|
+
required: true,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
setup(props, { slots }) {
|
|
17
|
+
// Store the ref callback to avoid recreating it on each render
|
|
18
|
+
const currentElement = shallowRef<HTMLElement | null>(null)
|
|
19
|
+
const refFunc = (node: any) => {
|
|
20
|
+
if (currentElement.value !== node) {
|
|
21
|
+
currentElement.value = node
|
|
22
|
+
props.setRef(node)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return () => {
|
|
27
|
+
const child = filterEmpty(slots.default?.() ?? [])[0]
|
|
28
|
+
if (!child)
|
|
29
|
+
return null
|
|
30
|
+
|
|
31
|
+
return cloneVNode(child, { ref: refFunc })
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
})
|
package/src/List.tsx
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import type { Key } from '@v-c/util/dist/type'
|
|
2
|
+
|
|
3
|
+
import type { ExtraRenderInfo } from './interface'
|
|
4
|
+
import ResizeObserver from '@v-c/resize-observer'
|
|
5
|
+
import {
|
|
6
|
+
computed,
|
|
7
|
+
type CSSProperties,
|
|
8
|
+
defineComponent,
|
|
9
|
+
type PropType,
|
|
10
|
+
ref,
|
|
11
|
+
shallowRef,
|
|
12
|
+
type VNode,
|
|
13
|
+
watch,
|
|
14
|
+
} from 'vue'
|
|
15
|
+
import Filler, { type InnerProps } from './Filler'
|
|
16
|
+
import useDiffItem from './hooks/useDiffItem'
|
|
17
|
+
import useFrameWheel from './hooks/useFrameWheel'
|
|
18
|
+
import { useGetSize } from './hooks/useGetSize'
|
|
19
|
+
import useHeights from './hooks/useHeights'
|
|
20
|
+
import useMobileTouchMove from './hooks/useMobileTouchMove'
|
|
21
|
+
import useScrollDrag from './hooks/useScrollDrag'
|
|
22
|
+
import Item from './Item'
|
|
23
|
+
import ScrollBar, { type ScrollBarRef } from './ScrollBar'
|
|
24
|
+
import { getSpinSize } from './utils/scrollbarUtil'
|
|
25
|
+
|
|
26
|
+
const EMPTY_DATA: any[] = []
|
|
27
|
+
|
|
28
|
+
const ScrollStyle: CSSProperties = {
|
|
29
|
+
overflowY: 'auto',
|
|
30
|
+
overflowAnchor: 'none',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ScrollInfo {
|
|
34
|
+
x: number
|
|
35
|
+
y: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ListRef {
|
|
39
|
+
nativeElement?: HTMLDivElement
|
|
40
|
+
scrollTo: (arg?: number | ScrollConfig) => void
|
|
41
|
+
getScrollInfo: () => ScrollInfo
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ScrollPos {
|
|
45
|
+
left?: number
|
|
46
|
+
top?: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ScrollTarget {
|
|
50
|
+
index?: number
|
|
51
|
+
key?: Key
|
|
52
|
+
align?: 'top' | 'bottom' | 'auto'
|
|
53
|
+
offset?: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type ScrollConfig = ScrollTarget | ScrollPos
|
|
57
|
+
|
|
58
|
+
export interface ListProps<T = any> {
|
|
59
|
+
prefixCls?: string
|
|
60
|
+
data?: T[]
|
|
61
|
+
height?: number
|
|
62
|
+
itemHeight?: number
|
|
63
|
+
fullHeight?: boolean
|
|
64
|
+
itemKey: Key | ((item: T) => Key)
|
|
65
|
+
component?: string
|
|
66
|
+
virtual?: boolean
|
|
67
|
+
onScroll?: (e: Event) => void
|
|
68
|
+
onVirtualScroll?: (info: ScrollInfo) => void
|
|
69
|
+
onVisibleChange?: (visibleList: T[], fullList: T[]) => void
|
|
70
|
+
innerProps?: InnerProps
|
|
71
|
+
extraRender?: (info: ExtraRenderInfo) => VNode
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default defineComponent({
|
|
75
|
+
name: 'VirtualList',
|
|
76
|
+
props: {
|
|
77
|
+
prefixCls: { type: String, default: 'vc-virtual-list' },
|
|
78
|
+
data: { type: Array as PropType<any[]> },
|
|
79
|
+
height: Number,
|
|
80
|
+
itemHeight: Number,
|
|
81
|
+
fullHeight: { type: Boolean, default: true },
|
|
82
|
+
itemKey: { type: [String, Number, Function] as PropType<Key | ((item: any) => Key)>, required: true },
|
|
83
|
+
component: { type: String, default: 'div' },
|
|
84
|
+
virtual: { type: Boolean, default: true },
|
|
85
|
+
onScroll: Function as PropType<(e: Event) => void>,
|
|
86
|
+
onVirtualScroll: Function as PropType<(info: ScrollInfo) => void>,
|
|
87
|
+
onVisibleChange: Function as PropType<(visibleList: any[], fullList: any[]) => void>,
|
|
88
|
+
innerProps: Object as PropType<InnerProps>,
|
|
89
|
+
extraRender: Function as PropType<(info: ExtraRenderInfo) => VNode>,
|
|
90
|
+
},
|
|
91
|
+
setup(props, { expose, attrs, slots }) {
|
|
92
|
+
// =============================== Item Key ===============================
|
|
93
|
+
const getKey = (item: any): Key => {
|
|
94
|
+
if (typeof props.itemKey === 'function') {
|
|
95
|
+
return props.itemKey(item)
|
|
96
|
+
}
|
|
97
|
+
return item?.[props.itemKey as string]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ================================ Height ================================
|
|
101
|
+
const [setInstanceRef, collectHeight, heights, heightUpdatedMark] = useHeights(
|
|
102
|
+
getKey,
|
|
103
|
+
undefined,
|
|
104
|
+
undefined,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
// ================================= MISC =================================
|
|
108
|
+
const mergedData = computed(() => props.data || EMPTY_DATA)
|
|
109
|
+
|
|
110
|
+
const useVirtual = computed(
|
|
111
|
+
() => !!(props.virtual !== false && props.height && props.itemHeight),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
const containerHeight = computed(() =>
|
|
115
|
+
Object.values(heights.maps).reduce((total: number, curr: number) => total + curr, 0),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const inVirtual = computed(() => {
|
|
119
|
+
const data = mergedData.value
|
|
120
|
+
return (
|
|
121
|
+
useVirtual.value
|
|
122
|
+
&& data
|
|
123
|
+
&& Math.max(props.itemHeight! * data.length, containerHeight.value) > props.height!
|
|
124
|
+
)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const componentRef = ref<HTMLDivElement>()
|
|
128
|
+
const fillerInnerRef = ref<HTMLDivElement>()
|
|
129
|
+
const containerRef = ref<HTMLDivElement>()
|
|
130
|
+
const verticalScrollBarRef = shallowRef<ScrollBarRef>()
|
|
131
|
+
|
|
132
|
+
const offsetTop = ref(0)
|
|
133
|
+
const offsetLeft = ref(0)
|
|
134
|
+
const scrollMoving = ref(false)
|
|
135
|
+
|
|
136
|
+
// ScrollBar related
|
|
137
|
+
const verticalScrollBarSpinSize = ref(0)
|
|
138
|
+
const scrollWidth = ref(0)
|
|
139
|
+
|
|
140
|
+
// ================================ Scroll ================================
|
|
141
|
+
function syncScrollTop(newTop: number | ((prev: number) => number)) {
|
|
142
|
+
let value: number
|
|
143
|
+
if (typeof newTop === 'function') {
|
|
144
|
+
value = newTop(offsetTop.value)
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
value = newTop
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const maxScrollHeight = scrollHeight!.value! - props.height!
|
|
151
|
+
const alignedTop = Math.max(0, Math.min(value, maxScrollHeight || 0))
|
|
152
|
+
|
|
153
|
+
if (componentRef.value) {
|
|
154
|
+
componentRef.value.scrollTop = alignedTop
|
|
155
|
+
}
|
|
156
|
+
offsetTop.value = alignedTop
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ================================ Range ================================
|
|
160
|
+
useDiffItem(mergedData, getKey)
|
|
161
|
+
|
|
162
|
+
// ========================== Visible Calculation =========================
|
|
163
|
+
const scrollHeight = ref(0)
|
|
164
|
+
const start = ref(0)
|
|
165
|
+
const end = ref(0)
|
|
166
|
+
const fillerOffset = ref<number | undefined>(undefined)
|
|
167
|
+
|
|
168
|
+
watch(
|
|
169
|
+
[
|
|
170
|
+
() => inVirtual.value,
|
|
171
|
+
() => useVirtual.value,
|
|
172
|
+
() => offsetTop.value,
|
|
173
|
+
() => mergedData.value,
|
|
174
|
+
() => heightUpdatedMark.value,
|
|
175
|
+
() => props.height,
|
|
176
|
+
],
|
|
177
|
+
() => {
|
|
178
|
+
if (!useVirtual.value) {
|
|
179
|
+
scrollHeight.value = 0
|
|
180
|
+
start.value = 0
|
|
181
|
+
end.value = mergedData.value.length - 1
|
|
182
|
+
fillerOffset.value = undefined
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!inVirtual.value) {
|
|
187
|
+
scrollHeight.value = fillerInnerRef.value?.offsetHeight || 0
|
|
188
|
+
start.value = 0
|
|
189
|
+
end.value = mergedData.value.length - 1
|
|
190
|
+
fillerOffset.value = undefined
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let itemTop = 0
|
|
195
|
+
let startIndex: number | undefined
|
|
196
|
+
let startOffset: number | undefined
|
|
197
|
+
let endIndex: number | undefined
|
|
198
|
+
|
|
199
|
+
const dataLen = mergedData.value.length
|
|
200
|
+
const data = mergedData.value
|
|
201
|
+
|
|
202
|
+
for (let i = 0; i < dataLen; i += 1) {
|
|
203
|
+
const item = data[i]
|
|
204
|
+
const key = getKey(item)
|
|
205
|
+
|
|
206
|
+
const cacheHeight = heights.get(key)
|
|
207
|
+
const currentItemBottom = itemTop + (cacheHeight === undefined ? props.itemHeight! : cacheHeight)
|
|
208
|
+
|
|
209
|
+
if (currentItemBottom >= offsetTop.value && startIndex === undefined) {
|
|
210
|
+
startIndex = i
|
|
211
|
+
startOffset = itemTop
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (currentItemBottom > offsetTop.value + props.height! && endIndex === undefined) {
|
|
215
|
+
endIndex = i
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
itemTop = currentItemBottom
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (startIndex === undefined) {
|
|
222
|
+
startIndex = 0
|
|
223
|
+
startOffset = 0
|
|
224
|
+
endIndex = Math.ceil(props.height! / props.itemHeight!)
|
|
225
|
+
}
|
|
226
|
+
if (endIndex === undefined) {
|
|
227
|
+
endIndex = mergedData.value.length - 1
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
endIndex = Math.min(endIndex + 1, mergedData.value.length - 1)
|
|
231
|
+
|
|
232
|
+
scrollHeight.value = itemTop
|
|
233
|
+
start.value = startIndex
|
|
234
|
+
end.value = endIndex
|
|
235
|
+
fillerOffset.value = startOffset
|
|
236
|
+
},
|
|
237
|
+
{ immediate: true },
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
// Sync scroll top when height changes
|
|
241
|
+
watch(
|
|
242
|
+
() => scrollHeight.value,
|
|
243
|
+
() => {
|
|
244
|
+
const changedRecord = heights.getRecord()
|
|
245
|
+
if (changedRecord.size === 1) {
|
|
246
|
+
const recordKey = Array.from(changedRecord.keys())[0]
|
|
247
|
+
const prevCacheHeight = changedRecord.get(recordKey)
|
|
248
|
+
|
|
249
|
+
const startItem = mergedData.value[start.value]
|
|
250
|
+
if (startItem && prevCacheHeight === undefined) {
|
|
251
|
+
const startIndexKey = getKey(startItem)
|
|
252
|
+
if (startIndexKey === recordKey) {
|
|
253
|
+
const realStartHeight = heights.get(recordKey)
|
|
254
|
+
const diffHeight = realStartHeight - props.itemHeight!
|
|
255
|
+
syncScrollTop(ori => ori + diffHeight)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
heights.resetRecord()
|
|
261
|
+
},
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
// ================================= Size =================================
|
|
265
|
+
const size = ref({ width: 0, height: props.height || 0 })
|
|
266
|
+
|
|
267
|
+
const onHolderResize = (sizeInfo: { offsetWidth: number, offsetHeight: number }) => {
|
|
268
|
+
size.value = {
|
|
269
|
+
width: sizeInfo.offsetWidth,
|
|
270
|
+
height: sizeInfo.offsetHeight,
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// =============================== Scroll ===============================
|
|
275
|
+
const getVirtualScrollInfo = () => ({
|
|
276
|
+
x: offsetLeft.value,
|
|
277
|
+
y: offsetTop.value,
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
const lastVirtualScrollInfo = ref(getVirtualScrollInfo())
|
|
281
|
+
|
|
282
|
+
const triggerScroll = (params?: { x?: number, y?: number }) => {
|
|
283
|
+
if (props.onVirtualScroll) {
|
|
284
|
+
const nextInfo = { ...getVirtualScrollInfo(), ...params }
|
|
285
|
+
|
|
286
|
+
if (
|
|
287
|
+
lastVirtualScrollInfo.value.x !== nextInfo.x
|
|
288
|
+
|| lastVirtualScrollInfo.value.y !== nextInfo.y
|
|
289
|
+
) {
|
|
290
|
+
props.onVirtualScroll(nextInfo)
|
|
291
|
+
lastVirtualScrollInfo.value = nextInfo
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ========================== Scroll Position ===========================
|
|
297
|
+
const isScrollAtTop = computed(() => offsetTop.value === 0)
|
|
298
|
+
const isScrollAtBottom = computed(() => offsetTop.value + props.height! >= scrollHeight.value)
|
|
299
|
+
const isScrollAtLeft = computed(() => offsetLeft.value === 0)
|
|
300
|
+
const isScrollAtRight = computed(() => offsetLeft.value + size.value.width >= scrollWidth.value)
|
|
301
|
+
|
|
302
|
+
// ========================== Wheel & Touch =========================
|
|
303
|
+
const delayHideScrollBar = () => {
|
|
304
|
+
verticalScrollBarRef.value?.delayHidden()
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const [onWheel] = useFrameWheel(
|
|
308
|
+
inVirtual,
|
|
309
|
+
isScrollAtTop,
|
|
310
|
+
isScrollAtBottom,
|
|
311
|
+
isScrollAtLeft,
|
|
312
|
+
isScrollAtRight,
|
|
313
|
+
false, // horizontalScroll
|
|
314
|
+
(offsetY, isHorizontal) => {
|
|
315
|
+
if (isHorizontal) {
|
|
316
|
+
// Not implemented yet
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
syncScrollTop(top => top + offsetY)
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
useMobileTouchMove(
|
|
325
|
+
inVirtual,
|
|
326
|
+
componentRef,
|
|
327
|
+
(isHorizontal, offset, _smoothOffset, _e) => {
|
|
328
|
+
if (isHorizontal) {
|
|
329
|
+
// Not implemented yet
|
|
330
|
+
return false
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
syncScrollTop(top => top + offset)
|
|
334
|
+
return true
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
useScrollDrag(
|
|
340
|
+
inVirtual,
|
|
341
|
+
componentRef,
|
|
342
|
+
(offset) => {
|
|
343
|
+
syncScrollTop(top => top + offset)
|
|
344
|
+
},
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
// ========================== ScrollBar =========================
|
|
348
|
+
const onScrollBar = (newScrollOffset: number, horizontal?: boolean) => {
|
|
349
|
+
const newOffset = newScrollOffset
|
|
350
|
+
if (horizontal) {
|
|
351
|
+
// Not implemented yet
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
syncScrollTop(newOffset)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const onScrollbarStartMove = () => {
|
|
359
|
+
scrollMoving.value = true
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const onScrollbarStopMove = () => {
|
|
363
|
+
scrollMoving.value = false
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Calculate ScrollBar spin size
|
|
367
|
+
watch(
|
|
368
|
+
[() => props.height, () => scrollHeight.value, () => inVirtual.value, () => size.value.height],
|
|
369
|
+
() => {
|
|
370
|
+
if (inVirtual.value && props.height && scrollHeight.value) {
|
|
371
|
+
// First parameter is container size, second is scroll range
|
|
372
|
+
verticalScrollBarSpinSize.value = getSpinSize(size.value.height, scrollHeight.value)
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
{ immediate: true },
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
function onFallbackScroll(e: Event) {
|
|
379
|
+
const target = e.currentTarget as HTMLDivElement
|
|
380
|
+
const newScrollTop = target.scrollTop
|
|
381
|
+
if (newScrollTop !== offsetTop.value) {
|
|
382
|
+
syncScrollTop(newScrollTop)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
props.onScroll?.(e)
|
|
386
|
+
triggerScroll()
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ================================= Ref ==================================
|
|
390
|
+
const scrollTo = (config?: number | ScrollConfig) => {
|
|
391
|
+
if (config === null || config === undefined) {
|
|
392
|
+
return
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (typeof config === 'number') {
|
|
396
|
+
syncScrollTop(config)
|
|
397
|
+
}
|
|
398
|
+
else if (config && typeof config === 'object') {
|
|
399
|
+
let scrollTop: number | undefined
|
|
400
|
+
|
|
401
|
+
if ('left' in config) {
|
|
402
|
+
offsetLeft.value = config.left || 0
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if ('top' in config) {
|
|
406
|
+
scrollTop = config.top
|
|
407
|
+
}
|
|
408
|
+
else if ('index' in config) {
|
|
409
|
+
const index = config.index || 0
|
|
410
|
+
const item = mergedData.value[index]
|
|
411
|
+
if (item) {
|
|
412
|
+
let itemTop = 0
|
|
413
|
+
for (let i = 0; i < index; i += 1) {
|
|
414
|
+
const key = getKey(mergedData.value[i])
|
|
415
|
+
const cacheHeight = heights.get(key)
|
|
416
|
+
itemTop += cacheHeight === undefined ? props.itemHeight! : cacheHeight
|
|
417
|
+
}
|
|
418
|
+
scrollTop = itemTop
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
else if ('key' in config) {
|
|
422
|
+
const index = mergedData.value.findIndex(item => getKey(item) === config.key)
|
|
423
|
+
if (index >= 0) {
|
|
424
|
+
let itemTop = 0
|
|
425
|
+
for (let i = 0; i < index; i += 1) {
|
|
426
|
+
const key = getKey(mergedData.value[i])
|
|
427
|
+
const cacheHeight = heights.get(key)
|
|
428
|
+
itemTop += cacheHeight === undefined ? props.itemHeight! : cacheHeight
|
|
429
|
+
}
|
|
430
|
+
scrollTop = itemTop
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (scrollTop !== undefined) {
|
|
435
|
+
syncScrollTop(scrollTop)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
expose({
|
|
441
|
+
nativeElement: containerRef,
|
|
442
|
+
getScrollInfo: getVirtualScrollInfo,
|
|
443
|
+
scrollTo,
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
// ================================ Effect ================================
|
|
447
|
+
watch(
|
|
448
|
+
[() => start.value, () => end.value, () => mergedData.value],
|
|
449
|
+
() => {
|
|
450
|
+
if (props.onVisibleChange) {
|
|
451
|
+
const renderList = mergedData.value.slice(start.value, end.value + 1)
|
|
452
|
+
props.onVisibleChange(renderList, mergedData.value)
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
// ================================ Render ================================
|
|
458
|
+
const renderChildren = () => {
|
|
459
|
+
const children: VNode[] = []
|
|
460
|
+
const data = mergedData.value
|
|
461
|
+
const defaultSlot = slots.default
|
|
462
|
+
|
|
463
|
+
if (!defaultSlot) {
|
|
464
|
+
return children
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
for (let i = start.value; i <= end.value; i += 1) {
|
|
468
|
+
const item = data[i]
|
|
469
|
+
const key = getKey(item)
|
|
470
|
+
// Call the slot function with item, index, and props
|
|
471
|
+
const nodes = defaultSlot({
|
|
472
|
+
item,
|
|
473
|
+
index: i,
|
|
474
|
+
style: {},
|
|
475
|
+
offsetX: offsetLeft.value,
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
// Wrap each node in Item component
|
|
479
|
+
const node = Array.isArray(nodes) ? nodes[0] : nodes
|
|
480
|
+
if (node) {
|
|
481
|
+
children.push(
|
|
482
|
+
<Item key={key} setRef={ele => setInstanceRef(item, ele)}>
|
|
483
|
+
{node}
|
|
484
|
+
</Item>,
|
|
485
|
+
)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return children
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const getSize = useGetSize(mergedData, getKey, heights, props.itemHeight!)
|
|
493
|
+
|
|
494
|
+
return () => {
|
|
495
|
+
const componentStyle: CSSProperties = {}
|
|
496
|
+
if (props.height) {
|
|
497
|
+
componentStyle[props.fullHeight ? 'height' : 'maxHeight'] = `${props.height}px`
|
|
498
|
+
Object.assign(componentStyle, ScrollStyle)
|
|
499
|
+
|
|
500
|
+
// Use custom ScrollBar when virtual scrolling is enabled
|
|
501
|
+
if (useVirtual.value) {
|
|
502
|
+
componentStyle.overflowY = 'hidden'
|
|
503
|
+
|
|
504
|
+
if (scrollWidth.value) {
|
|
505
|
+
componentStyle.overflowX = 'hidden'
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (scrollMoving.value) {
|
|
509
|
+
componentStyle.pointerEvents = 'none'
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const extraContent = props.extraRender?.({
|
|
515
|
+
start: start.value,
|
|
516
|
+
end: end.value,
|
|
517
|
+
virtual: inVirtual.value,
|
|
518
|
+
offsetX: offsetLeft.value,
|
|
519
|
+
offsetY: fillerOffset.value || 0,
|
|
520
|
+
rtl: false,
|
|
521
|
+
getSize: getSize.value,
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
const Component = props.component as any
|
|
525
|
+
|
|
526
|
+
return (
|
|
527
|
+
<div
|
|
528
|
+
ref={containerRef}
|
|
529
|
+
style={{ position: 'relative', ...(attrs.style as CSSProperties) }}
|
|
530
|
+
class={[props.prefixCls, attrs.class]}
|
|
531
|
+
>
|
|
532
|
+
<ResizeObserver onResize={onHolderResize}>
|
|
533
|
+
<Component
|
|
534
|
+
class={`${props.prefixCls}-holder`}
|
|
535
|
+
style={componentStyle}
|
|
536
|
+
ref={componentRef}
|
|
537
|
+
onScroll={onFallbackScroll}
|
|
538
|
+
onWheel={onWheel}
|
|
539
|
+
onMouseenter={delayHideScrollBar}
|
|
540
|
+
>
|
|
541
|
+
<Filler
|
|
542
|
+
prefixCls={props.prefixCls}
|
|
543
|
+
height={scrollHeight.value}
|
|
544
|
+
offsetX={offsetLeft.value}
|
|
545
|
+
offsetY={fillerOffset.value}
|
|
546
|
+
onInnerResize={collectHeight}
|
|
547
|
+
ref={fillerInnerRef}
|
|
548
|
+
innerProps={props.innerProps}
|
|
549
|
+
rtl={false}
|
|
550
|
+
extra={extraContent}
|
|
551
|
+
>
|
|
552
|
+
{renderChildren()}
|
|
553
|
+
</Filler>
|
|
554
|
+
</Component>
|
|
555
|
+
</ResizeObserver>
|
|
556
|
+
|
|
557
|
+
{/* ScrollBar */}
|
|
558
|
+
{inVirtual.value && scrollHeight.value > (props.height || 0) && (
|
|
559
|
+
<ScrollBar
|
|
560
|
+
ref={verticalScrollBarRef}
|
|
561
|
+
prefixCls={props.prefixCls}
|
|
562
|
+
scrollOffset={offsetTop.value}
|
|
563
|
+
scrollRange={scrollHeight.value}
|
|
564
|
+
rtl={false}
|
|
565
|
+
onScroll={onScrollBar}
|
|
566
|
+
onStartMove={onScrollbarStartMove}
|
|
567
|
+
onStopMove={onScrollbarStopMove}
|
|
568
|
+
spinSize={verticalScrollBarSpinSize.value}
|
|
569
|
+
containerSize={size.value.height}
|
|
570
|
+
showScrollBar="optional"
|
|
571
|
+
/>
|
|
572
|
+
)}
|
|
573
|
+
</div>
|
|
574
|
+
)
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
})
|