@v-c/virtual-list 1.0.0 → 1.0.2
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/dist/List.cjs +18 -9
- package/dist/List.js +19 -10
- package/dist/hooks/useHeights.cjs +1 -1
- package/dist/hooks/useHeights.js +2 -2
- package/package.json +6 -2
- package/bump.config.ts +0 -6
- package/docs/animate.less +0 -31
- package/docs/animate.vue +0 -159
- package/docs/basic.vue +0 -176
- package/docs/height.vue +0 -48
- package/docs/nest.vue +0 -60
- package/docs/no-virtual.vue +0 -127
- package/docs/switch.vue +0 -102
- package/docs/virtual-list.stories.vue +0 -35
- package/src/Filler.tsx +0 -73
- package/src/Item.tsx +0 -35
- package/src/List.tsx +0 -642
- package/src/ScrollBar.tsx +0 -287
- package/src/__tests__/List.test.ts +0 -59
- package/src/hooks/useDiffItem.ts +0 -28
- package/src/hooks/useFrameWheel.ts +0 -142
- package/src/hooks/useGetSize.ts +0 -45
- package/src/hooks/useHeights.ts +0 -107
- package/src/hooks/useMobileTouchMove.ts +0 -132
- package/src/hooks/useOriginScroll.ts +0 -48
- package/src/hooks/useScrollDrag.ts +0 -124
- package/src/hooks/useScrollTo.tsx +0 -184
- package/src/index.ts +0 -5
- package/src/interface.ts +0 -32
- package/src/utils/CacheMap.ts +0 -42
- package/src/utils/isFirefox.ts +0 -3
- package/src/utils/scrollbarUtil.ts +0 -10
- package/tsconfig.json +0 -7
- package/vite.config.ts +0 -18
- package/vitest.config.ts +0 -11
package/src/ScrollBar.tsx
DELETED
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
import type { CSSProperties } from 'vue'
|
|
2
|
-
import raf from '@v-c/util/dist/raf'
|
|
3
|
-
import { computed, defineComponent, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
|
|
4
|
-
|
|
5
|
-
export type ScrollBarDirectionType = 'ltr' | 'rtl'
|
|
6
|
-
|
|
7
|
-
export interface ScrollBarProps {
|
|
8
|
-
prefixCls: string
|
|
9
|
-
scrollOffset: number
|
|
10
|
-
scrollRange: number
|
|
11
|
-
rtl: boolean
|
|
12
|
-
onScroll: (scrollOffset: number, horizontal?: boolean) => void
|
|
13
|
-
onStartMove: () => void
|
|
14
|
-
onStopMove: () => void
|
|
15
|
-
horizontal?: boolean
|
|
16
|
-
style?: CSSProperties
|
|
17
|
-
thumbStyle?: CSSProperties
|
|
18
|
-
spinSize: number
|
|
19
|
-
containerSize: number
|
|
20
|
-
showScrollBar?: boolean | 'optional'
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface ScrollBarRef {
|
|
24
|
-
delayHidden: () => void
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function getPageXY(
|
|
28
|
-
e: MouseEvent | TouchEvent,
|
|
29
|
-
horizontal: boolean,
|
|
30
|
-
): number {
|
|
31
|
-
const obj = 'touches' in e ? e.touches[0] : e
|
|
32
|
-
return obj[horizontal ? 'pageX' : 'pageY'] - window[horizontal ? 'scrollX' : 'scrollY']
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export default defineComponent<ScrollBarProps>({
|
|
36
|
-
name: 'ScrollBar',
|
|
37
|
-
setup(props, { expose }) {
|
|
38
|
-
const dragging = ref(false)
|
|
39
|
-
const pageXY = ref<number | null>(null)
|
|
40
|
-
const startTop = ref<number | null>(null)
|
|
41
|
-
|
|
42
|
-
const isLTR = computed(() => !props.rtl)
|
|
43
|
-
|
|
44
|
-
// ========================= Refs =========================
|
|
45
|
-
const scrollbarRef = shallowRef<HTMLDivElement>()
|
|
46
|
-
const thumbRef = shallowRef<HTMLDivElement>()
|
|
47
|
-
|
|
48
|
-
// ======================= Visible ========================
|
|
49
|
-
const visible = ref(props.showScrollBar === 'optional' ? true : props.showScrollBar)
|
|
50
|
-
let visibleTimeout: ReturnType<typeof setTimeout> | null = null
|
|
51
|
-
|
|
52
|
-
const delayHidden = () => {
|
|
53
|
-
// Don't auto-hide if showScrollBar is explicitly true or false
|
|
54
|
-
if (props.showScrollBar === true || props.showScrollBar === false)
|
|
55
|
-
return
|
|
56
|
-
if (visibleTimeout)
|
|
57
|
-
clearTimeout(visibleTimeout)
|
|
58
|
-
visible.value = true
|
|
59
|
-
visibleTimeout = setTimeout(() => {
|
|
60
|
-
visible.value = false
|
|
61
|
-
}, 3000)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ======================== Range =========================
|
|
65
|
-
const enableScrollRange = computed(() => props.scrollRange - props.containerSize || 0)
|
|
66
|
-
const enableOffsetRange = computed(() => props.containerSize - props.spinSize || 0)
|
|
67
|
-
|
|
68
|
-
// ========================= Top ==========================
|
|
69
|
-
const top = computed(() => {
|
|
70
|
-
if (props.scrollOffset === 0 || enableScrollRange.value === 0) {
|
|
71
|
-
return 0
|
|
72
|
-
}
|
|
73
|
-
const ptg = props.scrollOffset / enableScrollRange.value
|
|
74
|
-
return ptg * enableOffsetRange.value
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
// ======================== Thumb =========================
|
|
78
|
-
const stateRef = shallowRef({
|
|
79
|
-
top: top.value,
|
|
80
|
-
dragging: dragging.value,
|
|
81
|
-
pageY: pageXY.value,
|
|
82
|
-
startTop: startTop.value,
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
watch([top, dragging, pageXY, startTop], () => {
|
|
86
|
-
stateRef.value = {
|
|
87
|
-
top: top.value,
|
|
88
|
-
dragging: dragging.value,
|
|
89
|
-
pageY: pageXY.value,
|
|
90
|
-
startTop: startTop.value,
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
const onContainerMouseDown = (e: MouseEvent) => {
|
|
95
|
-
e.stopPropagation()
|
|
96
|
-
e.preventDefault()
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const onThumbMouseDown = (e: MouseEvent | TouchEvent) => {
|
|
100
|
-
dragging.value = true
|
|
101
|
-
pageXY.value = getPageXY(e, props.horizontal || false)
|
|
102
|
-
startTop.value = stateRef.value.top
|
|
103
|
-
|
|
104
|
-
props?.onStartMove?.()
|
|
105
|
-
e.stopPropagation()
|
|
106
|
-
e.preventDefault()
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ======================== Effect ========================
|
|
110
|
-
|
|
111
|
-
// React make event as passive, but we need to preventDefault
|
|
112
|
-
// Add event on dom directly instead.
|
|
113
|
-
// ref: https://github.com/facebook/react/issues/9809
|
|
114
|
-
onMounted(() => {
|
|
115
|
-
const onScrollbarTouchStart = (e: TouchEvent) => {
|
|
116
|
-
e.preventDefault()
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const scrollbarEle = scrollbarRef.value
|
|
120
|
-
const thumbEle = thumbRef.value
|
|
121
|
-
|
|
122
|
-
if (scrollbarEle && thumbEle) {
|
|
123
|
-
scrollbarEle.addEventListener('touchstart', onScrollbarTouchStart, { passive: false })
|
|
124
|
-
thumbEle.addEventListener('touchstart', onThumbMouseDown as any, { passive: false })
|
|
125
|
-
|
|
126
|
-
onUnmounted(() => {
|
|
127
|
-
scrollbarEle.removeEventListener('touchstart', onScrollbarTouchStart)
|
|
128
|
-
thumbEle.removeEventListener('touchstart', onThumbMouseDown as any)
|
|
129
|
-
})
|
|
130
|
-
}
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
// Effect: Handle dragging
|
|
134
|
-
watch(dragging, (isDragging, _O, onCleanup) => {
|
|
135
|
-
if (isDragging) {
|
|
136
|
-
let moveRafId: number | null = null
|
|
137
|
-
|
|
138
|
-
const onMouseMove = (e: MouseEvent | TouchEvent) => {
|
|
139
|
-
const {
|
|
140
|
-
dragging: stateDragging,
|
|
141
|
-
pageY: statePageY,
|
|
142
|
-
startTop: stateStartTop,
|
|
143
|
-
} = stateRef.value
|
|
144
|
-
raf.cancel(moveRafId!)
|
|
145
|
-
|
|
146
|
-
const rect = scrollbarRef.value!.getBoundingClientRect()
|
|
147
|
-
const scale = props.containerSize / (props.horizontal ? rect.width : rect.height)
|
|
148
|
-
|
|
149
|
-
if (stateDragging) {
|
|
150
|
-
const offset = (getPageXY(e, props.horizontal || false) - (statePageY || 0)) * scale
|
|
151
|
-
let newTop = stateStartTop || 0
|
|
152
|
-
|
|
153
|
-
if (!isLTR.value && props.horizontal) {
|
|
154
|
-
newTop -= offset
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
newTop += offset
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const tmpEnableScrollRange = enableScrollRange.value
|
|
161
|
-
const tmpEnableOffsetRange = enableOffsetRange.value
|
|
162
|
-
|
|
163
|
-
const ptg: number = tmpEnableOffsetRange ? newTop / tmpEnableOffsetRange : 0
|
|
164
|
-
|
|
165
|
-
let newScrollTop = Math.ceil(ptg * tmpEnableScrollRange)
|
|
166
|
-
newScrollTop = Math.max(newScrollTop, 0)
|
|
167
|
-
newScrollTop = Math.min(newScrollTop, tmpEnableScrollRange)
|
|
168
|
-
|
|
169
|
-
moveRafId = raf(() => {
|
|
170
|
-
props?.onScroll?.(newScrollTop, props.horizontal)
|
|
171
|
-
})
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const onMouseUp = () => {
|
|
176
|
-
dragging.value = false
|
|
177
|
-
props.onStopMove()
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
window.addEventListener('mousemove', onMouseMove, { passive: true } as any)
|
|
181
|
-
window.addEventListener('touchmove', onMouseMove, { passive: true } as any)
|
|
182
|
-
window.addEventListener('mouseup', onMouseUp, { passive: true } as any)
|
|
183
|
-
window.addEventListener('touchend', onMouseUp, { passive: true } as any)
|
|
184
|
-
|
|
185
|
-
onCleanup(() => {
|
|
186
|
-
window.removeEventListener('mousemove', onMouseMove)
|
|
187
|
-
window.removeEventListener('touchmove', onMouseMove)
|
|
188
|
-
window.removeEventListener('mouseup', onMouseUp)
|
|
189
|
-
window.removeEventListener('touchend', onMouseUp)
|
|
190
|
-
|
|
191
|
-
raf.cancel(moveRafId!)
|
|
192
|
-
})
|
|
193
|
-
}
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
// Effect: Delay hidden on scroll offset change
|
|
197
|
-
watch(() => props.scrollOffset, (_n, _o, onCleanup) => {
|
|
198
|
-
delayHidden()
|
|
199
|
-
onCleanup(() => {
|
|
200
|
-
if (visibleTimeout) {
|
|
201
|
-
clearTimeout(visibleTimeout)
|
|
202
|
-
}
|
|
203
|
-
})
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
// Imperative handle
|
|
207
|
-
expose({
|
|
208
|
-
delayHidden,
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
return () => {
|
|
212
|
-
const { prefixCls, horizontal } = props
|
|
213
|
-
const scrollbarPrefixCls = `${prefixCls}-scrollbar`
|
|
214
|
-
|
|
215
|
-
const containerStyle: CSSProperties = {
|
|
216
|
-
position: 'absolute',
|
|
217
|
-
visibility: visible.value ? undefined : 'hidden',
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const thumbStyle: CSSProperties = {
|
|
221
|
-
position: 'absolute',
|
|
222
|
-
borderRadius: '99px',
|
|
223
|
-
background: 'var(--vc-virtual-list-scrollbar-bg, rgba(0, 0, 0, 0.5))',
|
|
224
|
-
cursor: 'pointer',
|
|
225
|
-
userSelect: 'none',
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (props.horizontal) {
|
|
229
|
-
Object.assign(containerStyle, {
|
|
230
|
-
height: '8px',
|
|
231
|
-
left: 0,
|
|
232
|
-
right: 0,
|
|
233
|
-
bottom: 0,
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
Object.assign(thumbStyle, {
|
|
237
|
-
height: '100%',
|
|
238
|
-
width: `${props.spinSize}px`,
|
|
239
|
-
[isLTR.value ? 'left' : 'right']: `${top.value}px`,
|
|
240
|
-
})
|
|
241
|
-
}
|
|
242
|
-
else {
|
|
243
|
-
Object.assign(containerStyle, {
|
|
244
|
-
width: '8px',
|
|
245
|
-
top: 0,
|
|
246
|
-
bottom: 0,
|
|
247
|
-
[isLTR.value ? 'right' : 'left']: 0,
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
Object.assign(thumbStyle, {
|
|
251
|
-
width: '100%',
|
|
252
|
-
height: `${props.spinSize}px`,
|
|
253
|
-
top: `${top.value}px`,
|
|
254
|
-
})
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return (
|
|
258
|
-
<div
|
|
259
|
-
ref={scrollbarRef}
|
|
260
|
-
class={[
|
|
261
|
-
scrollbarPrefixCls,
|
|
262
|
-
{
|
|
263
|
-
[`${scrollbarPrefixCls}-horizontal`]: horizontal,
|
|
264
|
-
[`${scrollbarPrefixCls}-vertical`]: !horizontal,
|
|
265
|
-
[`${scrollbarPrefixCls}-visible`]: visible.value,
|
|
266
|
-
},
|
|
267
|
-
]}
|
|
268
|
-
style={{ ...containerStyle, ...props.style }}
|
|
269
|
-
onMousedown={onContainerMouseDown}
|
|
270
|
-
onMousemove={delayHidden}
|
|
271
|
-
>
|
|
272
|
-
<div
|
|
273
|
-
ref={thumbRef}
|
|
274
|
-
class={[
|
|
275
|
-
`${scrollbarPrefixCls}-thumb`,
|
|
276
|
-
{
|
|
277
|
-
[`${scrollbarPrefixCls}-thumb-moving`]: dragging.value,
|
|
278
|
-
},
|
|
279
|
-
]}
|
|
280
|
-
style={{ ...thumbStyle, ...props.thumbStyle }}
|
|
281
|
-
onMousedown={onThumbMouseDown}
|
|
282
|
-
/>
|
|
283
|
-
</div>
|
|
284
|
-
)
|
|
285
|
-
}
|
|
286
|
-
},
|
|
287
|
-
})
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { mount } from '@vue/test-utils'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import { h } from 'vue'
|
|
4
|
-
import VirtualList from '../List'
|
|
5
|
-
|
|
6
|
-
describe('virtualList', () => {
|
|
7
|
-
it('should render basic list', () => {
|
|
8
|
-
const data = Array.from({ length: 100 }, (_, i) => ({ id: i, text: `Item ${i}` }))
|
|
9
|
-
|
|
10
|
-
const wrapper = mount(VirtualList, {
|
|
11
|
-
props: {
|
|
12
|
-
data,
|
|
13
|
-
height: 200,
|
|
14
|
-
itemHeight: 20,
|
|
15
|
-
itemKey: 'id',
|
|
16
|
-
},
|
|
17
|
-
slots: {
|
|
18
|
-
default: ({ item }: any) => h('div', `${item.text}`),
|
|
19
|
-
},
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
expect(wrapper.exists()).toBe(true)
|
|
23
|
-
expect(wrapper.find('.vc-virtual-list').exists()).toBe(true)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('should handle empty data', () => {
|
|
27
|
-
const wrapper = mount(VirtualList, {
|
|
28
|
-
props: {
|
|
29
|
-
data: [],
|
|
30
|
-
height: 200,
|
|
31
|
-
itemHeight: 20,
|
|
32
|
-
itemKey: 'id',
|
|
33
|
-
},
|
|
34
|
-
slots: {
|
|
35
|
-
default: ({ item }: any) => h('div', `${item.text}`),
|
|
36
|
-
},
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
expect(wrapper.exists()).toBe(true)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('should work with function itemKey', () => {
|
|
43
|
-
const data = Array.from({ length: 100 }, (_, i) => ({ id: i, text: `Item ${i}` }))
|
|
44
|
-
|
|
45
|
-
const wrapper = mount(VirtualList, {
|
|
46
|
-
props: {
|
|
47
|
-
data,
|
|
48
|
-
height: 200,
|
|
49
|
-
itemHeight: 20,
|
|
50
|
-
itemKey: (item: any) => item.id,
|
|
51
|
-
},
|
|
52
|
-
slots: {
|
|
53
|
-
default: ({ item }: any) => h('div', `${item.text}`),
|
|
54
|
-
},
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
expect(wrapper.exists()).toBe(true)
|
|
58
|
-
})
|
|
59
|
-
})
|
package/src/hooks/useDiffItem.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { Ref } from 'vue'
|
|
2
|
-
import { ref, watch } from 'vue'
|
|
3
|
-
|
|
4
|
-
export default function useDiffItem<T>(data: Ref<T[]>, getKey: (item: T) => any): Ref<T | undefined> {
|
|
5
|
-
const prevDataRef = ref<T[]>([])
|
|
6
|
-
const diffItem = ref<T>()
|
|
7
|
-
|
|
8
|
-
watch(
|
|
9
|
-
data,
|
|
10
|
-
(newData) => {
|
|
11
|
-
const prevData = prevDataRef.value
|
|
12
|
-
|
|
13
|
-
if (newData !== prevData) {
|
|
14
|
-
// Find added item
|
|
15
|
-
const addedItem = newData.find((item) => {
|
|
16
|
-
const key = getKey(item)
|
|
17
|
-
return !prevData.some(prevItem => getKey(prevItem as any) === key)
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
diffItem.value = addedItem
|
|
21
|
-
prevDataRef.value = newData
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
{ immediate: true },
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
return diffItem
|
|
28
|
-
}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import type { Ref } from 'vue'
|
|
2
|
-
import { onUnmounted, ref } from 'vue'
|
|
3
|
-
import isFF from '../utils/isFirefox'
|
|
4
|
-
import useOriginScroll from './useOriginScroll'
|
|
5
|
-
|
|
6
|
-
interface FireFoxDOMMouseScrollEvent {
|
|
7
|
-
detail: number
|
|
8
|
-
preventDefault: VoidFunction
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export default function useFrameWheel(
|
|
12
|
-
inVirtual: Ref<boolean>,
|
|
13
|
-
isScrollAtTop: Ref<boolean>,
|
|
14
|
-
isScrollAtBottom: Ref<boolean>,
|
|
15
|
-
isScrollAtLeft: Ref<boolean>,
|
|
16
|
-
isScrollAtRight: Ref<boolean>,
|
|
17
|
-
horizontalScroll: boolean,
|
|
18
|
-
/**
|
|
19
|
-
* Return `true` when you need to prevent default event
|
|
20
|
-
*/
|
|
21
|
-
onWheelDelta: (offset: number, horizontal: boolean) => void,
|
|
22
|
-
): [(e: WheelEvent) => void, (e: FireFoxDOMMouseScrollEvent) => void] {
|
|
23
|
-
const offsetRef = ref(0)
|
|
24
|
-
let nextFrame: number | null = null
|
|
25
|
-
|
|
26
|
-
// Firefox patch
|
|
27
|
-
const wheelValueRef = ref<number | null>(null)
|
|
28
|
-
const isMouseScrollRef = ref<boolean>(false)
|
|
29
|
-
|
|
30
|
-
// Scroll status sync
|
|
31
|
-
const originScroll = useOriginScroll(
|
|
32
|
-
isScrollAtTop,
|
|
33
|
-
isScrollAtBottom,
|
|
34
|
-
isScrollAtLeft,
|
|
35
|
-
isScrollAtRight,
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
function onWheelY(e: WheelEvent, deltaY: number) {
|
|
39
|
-
if (nextFrame)
|
|
40
|
-
cancelAnimationFrame(nextFrame)
|
|
41
|
-
|
|
42
|
-
// Do nothing when scroll at the edge, Skip check when is in scroll
|
|
43
|
-
if (originScroll(false, deltaY))
|
|
44
|
-
return
|
|
45
|
-
|
|
46
|
-
// Skip if nest List has handled this event
|
|
47
|
-
const event = e as WheelEvent & {
|
|
48
|
-
_virtualHandled?: boolean
|
|
49
|
-
}
|
|
50
|
-
if (!event._virtualHandled) {
|
|
51
|
-
event._virtualHandled = true
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
return
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
offsetRef.value += deltaY
|
|
58
|
-
wheelValueRef.value = deltaY
|
|
59
|
-
|
|
60
|
-
// Proxy of scroll events
|
|
61
|
-
if (!isFF) {
|
|
62
|
-
event.preventDefault()
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
nextFrame = requestAnimationFrame(() => {
|
|
66
|
-
// Patch a multiple for Firefox to fix wheel number too small
|
|
67
|
-
const patchMultiple = isMouseScrollRef.value ? 10 : 1
|
|
68
|
-
onWheelDelta(offsetRef.value * patchMultiple, false)
|
|
69
|
-
offsetRef.value = 0
|
|
70
|
-
})
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function onWheelX(event: WheelEvent, deltaX: number) {
|
|
74
|
-
onWheelDelta(deltaX, true)
|
|
75
|
-
|
|
76
|
-
if (!isFF) {
|
|
77
|
-
event.preventDefault()
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Check for which direction does wheel do. `sx` means `shift + wheel`
|
|
82
|
-
const wheelDirectionRef = ref<'x' | 'y' | 'sx' | null>(null)
|
|
83
|
-
let wheelDirectionClean: number | null = null
|
|
84
|
-
|
|
85
|
-
function onWheel(event: WheelEvent) {
|
|
86
|
-
if (!inVirtual.value)
|
|
87
|
-
return
|
|
88
|
-
|
|
89
|
-
// Wait for 2 frame to clean direction
|
|
90
|
-
if (wheelDirectionClean)
|
|
91
|
-
cancelAnimationFrame(wheelDirectionClean)
|
|
92
|
-
wheelDirectionClean = requestAnimationFrame(() => {
|
|
93
|
-
wheelDirectionRef.value = null
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
const { deltaX, deltaY, shiftKey } = event
|
|
97
|
-
|
|
98
|
-
let mergedDeltaX = deltaX
|
|
99
|
-
let mergedDeltaY = deltaY
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
wheelDirectionRef.value === 'sx'
|
|
103
|
-
|| (!wheelDirectionRef.value && (shiftKey || false) && deltaY && !deltaX)
|
|
104
|
-
) {
|
|
105
|
-
mergedDeltaX = deltaY
|
|
106
|
-
mergedDeltaY = 0
|
|
107
|
-
|
|
108
|
-
wheelDirectionRef.value = 'sx'
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const absX = Math.abs(mergedDeltaX)
|
|
112
|
-
const absY = Math.abs(mergedDeltaY)
|
|
113
|
-
|
|
114
|
-
if (wheelDirectionRef.value === null) {
|
|
115
|
-
wheelDirectionRef.value = horizontalScroll && absX > absY ? 'x' : 'y'
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (wheelDirectionRef.value === 'y') {
|
|
119
|
-
onWheelY(event, mergedDeltaY)
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
onWheelX(event, mergedDeltaX)
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// A patch for firefox
|
|
127
|
-
function onFireFoxScroll(event: FireFoxDOMMouseScrollEvent) {
|
|
128
|
-
if (!inVirtual.value)
|
|
129
|
-
return
|
|
130
|
-
|
|
131
|
-
isMouseScrollRef.value = event.detail === wheelValueRef.value
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
onUnmounted(() => {
|
|
135
|
-
if (nextFrame)
|
|
136
|
-
cancelAnimationFrame(nextFrame)
|
|
137
|
-
if (wheelDirectionClean)
|
|
138
|
-
cancelAnimationFrame(wheelDirectionClean)
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
return [onWheel, onFireFoxScroll]
|
|
142
|
-
}
|
package/src/hooks/useGetSize.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { ComputedRef, Ref } from 'vue'
|
|
2
|
-
import type { GetKey } from '../interface'
|
|
3
|
-
import type CacheMap from '../utils/CacheMap'
|
|
4
|
-
import { computed } from 'vue'
|
|
5
|
-
|
|
6
|
-
export function useGetSize<T>(
|
|
7
|
-
mergedData: Ref<T[]>,
|
|
8
|
-
getKey: GetKey<T>,
|
|
9
|
-
heights: CacheMap,
|
|
10
|
-
itemHeight: Ref<number>,
|
|
11
|
-
): ComputedRef<(startKey: any, endKey?: any) => { top: number, bottom: number }> {
|
|
12
|
-
return computed(() => {
|
|
13
|
-
return (startKey: any, endKey?: any) => {
|
|
14
|
-
let topIndex = 0
|
|
15
|
-
let bottomIndex = mergedData.value.length - 1
|
|
16
|
-
|
|
17
|
-
if (startKey !== undefined && startKey !== null) {
|
|
18
|
-
topIndex = mergedData.value.findIndex(item => getKey(item) === startKey)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (endKey !== undefined && endKey !== null) {
|
|
22
|
-
bottomIndex = mergedData.value.findIndex(item => getKey(item) === endKey)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
let top = 0
|
|
26
|
-
for (let i = 0; i < topIndex; i += 1) {
|
|
27
|
-
const key = getKey(mergedData.value[i])
|
|
28
|
-
const cacheHeight = heights.get(key)
|
|
29
|
-
top += cacheHeight === undefined ? itemHeight.value : cacheHeight
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
let bottom = 0
|
|
33
|
-
for (let i = mergedData.value.length - 1; i > bottomIndex; i -= 1) {
|
|
34
|
-
const key = getKey(mergedData.value[i])
|
|
35
|
-
const cacheHeight = heights.get(key)
|
|
36
|
-
bottom += cacheHeight === undefined ? itemHeight.value : cacheHeight
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
top,
|
|
41
|
-
bottom,
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
})
|
|
45
|
-
}
|
package/src/hooks/useHeights.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import type { Key } from '@v-c/util/dist/type'
|
|
2
|
-
import type { Ref } from 'vue'
|
|
3
|
-
import type { GetKey } from '../interface'
|
|
4
|
-
import { onUnmounted, reactive, ref } from 'vue'
|
|
5
|
-
import CacheMap from '../utils/CacheMap'
|
|
6
|
-
|
|
7
|
-
function parseNumber(value: string) {
|
|
8
|
-
const num = parseFloat(value)
|
|
9
|
-
return isNaN(num) ? 0 : num
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export default function useHeights<T>(
|
|
13
|
-
getKey: GetKey<T>,
|
|
14
|
-
onItemAdd?: (item: T) => void,
|
|
15
|
-
onItemRemove?: (item: T) => void,
|
|
16
|
-
): [
|
|
17
|
-
setInstanceRef: (item: T, instance: HTMLElement | null) => void,
|
|
18
|
-
collectHeight: (sync?: boolean) => void,
|
|
19
|
-
cacheMap: CacheMap,
|
|
20
|
-
updatedMark: Ref<number>,
|
|
21
|
-
] {
|
|
22
|
-
const updatedMark = ref(0)
|
|
23
|
-
const instanceRef = ref(new Map<Key, HTMLElement>())
|
|
24
|
-
const heightsRef = reactive(new CacheMap())
|
|
25
|
-
|
|
26
|
-
const promiseIdRef = ref<number>(0)
|
|
27
|
-
|
|
28
|
-
function cancelRaf() {
|
|
29
|
-
promiseIdRef.value += 1
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function collectHeight(sync = false) {
|
|
33
|
-
cancelRaf()
|
|
34
|
-
|
|
35
|
-
const doCollect = () => {
|
|
36
|
-
let changed = false
|
|
37
|
-
|
|
38
|
-
instanceRef.value.forEach((element, key) => {
|
|
39
|
-
if (element && element.offsetParent) {
|
|
40
|
-
const { offsetHeight } = element
|
|
41
|
-
const { marginTop, marginBottom } = getComputedStyle(element)
|
|
42
|
-
|
|
43
|
-
const marginTopNum = parseNumber(marginTop)
|
|
44
|
-
const marginBottomNum = parseNumber(marginBottom)
|
|
45
|
-
const totalHeight = offsetHeight + marginTopNum + marginBottomNum
|
|
46
|
-
|
|
47
|
-
if (heightsRef.get(key) !== totalHeight) {
|
|
48
|
-
heightsRef.set(key, totalHeight)
|
|
49
|
-
changed = true
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
// Always trigger update mark to tell parent that should re-calculate heights when resized
|
|
55
|
-
if (changed) {
|
|
56
|
-
updatedMark.value += 1
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (sync) {
|
|
61
|
-
doCollect()
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
promiseIdRef.value += 1
|
|
65
|
-
const id = promiseIdRef.value
|
|
66
|
-
Promise.resolve().then(() => {
|
|
67
|
-
if (id === promiseIdRef.value) {
|
|
68
|
-
doCollect()
|
|
69
|
-
}
|
|
70
|
-
})
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function setInstanceRef(item: T, instance: HTMLElement | null) {
|
|
75
|
-
const key = getKey(item)
|
|
76
|
-
const origin = instanceRef.value.get(key)
|
|
77
|
-
|
|
78
|
-
// Only update if the instance actually changed
|
|
79
|
-
if (origin === instance) {
|
|
80
|
-
return
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (instance) {
|
|
84
|
-
instanceRef.value.set(key, instance)
|
|
85
|
-
collectHeight()
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
instanceRef.value.delete(key)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Instance changed
|
|
92
|
-
if (!origin !== !instance) {
|
|
93
|
-
if (instance) {
|
|
94
|
-
onItemAdd?.(item)
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
onItemRemove?.(item)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
onUnmounted(() => {
|
|
103
|
-
cancelRaf()
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
return [setInstanceRef, collectHeight, heightsRef, updatedMark]
|
|
107
|
-
}
|