@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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { onUnmounted, ref, type Ref, watch } from 'vue'
|
|
2
|
+
|
|
3
|
+
const SMOOTH_PTG = 14 / 15
|
|
4
|
+
|
|
5
|
+
export default function useMobileTouchMove(
|
|
6
|
+
inVirtual: Ref<boolean>,
|
|
7
|
+
listRef: Ref<HTMLDivElement | null | undefined>,
|
|
8
|
+
callback: (
|
|
9
|
+
isHorizontal: boolean,
|
|
10
|
+
offset: number,
|
|
11
|
+
smoothOffset: boolean,
|
|
12
|
+
e?: TouchEvent,
|
|
13
|
+
) => boolean,
|
|
14
|
+
) {
|
|
15
|
+
const touchedRef = ref(false)
|
|
16
|
+
const touchXRef = ref(0)
|
|
17
|
+
const touchYRef = ref(0)
|
|
18
|
+
|
|
19
|
+
let elementRef: HTMLElement | null = null
|
|
20
|
+
let touchStartElement: HTMLDivElement | null = null
|
|
21
|
+
|
|
22
|
+
// Smooth scroll
|
|
23
|
+
let intervalId: ReturnType<typeof setInterval> | null = null
|
|
24
|
+
|
|
25
|
+
let cleanUpEvents: () => void
|
|
26
|
+
|
|
27
|
+
const onTouchMove = (e: TouchEvent) => {
|
|
28
|
+
if (touchedRef.value) {
|
|
29
|
+
const currentX = Math.ceil(e.touches[0].pageX)
|
|
30
|
+
const currentY = Math.ceil(e.touches[0].pageY)
|
|
31
|
+
let offsetX = touchXRef.value - currentX
|
|
32
|
+
let offsetY = touchYRef.value - currentY
|
|
33
|
+
const isHorizontal = Math.abs(offsetX) > Math.abs(offsetY)
|
|
34
|
+
if (isHorizontal) {
|
|
35
|
+
touchXRef.value = currentX
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
touchYRef.value = currentY
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const scrollHandled = callback(isHorizontal, isHorizontal ? offsetX : offsetY, false, e)
|
|
42
|
+
if (scrollHandled) {
|
|
43
|
+
e.preventDefault()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Smooth interval
|
|
47
|
+
if (intervalId)
|
|
48
|
+
clearInterval(intervalId)
|
|
49
|
+
|
|
50
|
+
if (scrollHandled) {
|
|
51
|
+
intervalId = setInterval(() => {
|
|
52
|
+
if (isHorizontal) {
|
|
53
|
+
offsetX *= SMOOTH_PTG
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
offsetY *= SMOOTH_PTG
|
|
57
|
+
}
|
|
58
|
+
const offset = Math.floor(isHorizontal ? offsetX : offsetY)
|
|
59
|
+
if (!callback(isHorizontal, offset, true) || Math.abs(offset) <= 0.1) {
|
|
60
|
+
if (intervalId)
|
|
61
|
+
clearInterval(intervalId)
|
|
62
|
+
}
|
|
63
|
+
}, 16)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const onTouchEnd = () => {
|
|
69
|
+
touchedRef.value = false
|
|
70
|
+
|
|
71
|
+
cleanUpEvents()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const onTouchStart = (e: TouchEvent) => {
|
|
75
|
+
cleanUpEvents()
|
|
76
|
+
|
|
77
|
+
if (e.touches.length === 1 && !touchedRef.value) {
|
|
78
|
+
touchedRef.value = true
|
|
79
|
+
touchXRef.value = Math.ceil(e.touches[0].pageX)
|
|
80
|
+
touchYRef.value = Math.ceil(e.touches[0].pageY)
|
|
81
|
+
|
|
82
|
+
elementRef = e.target as HTMLElement
|
|
83
|
+
elementRef.addEventListener('touchmove', onTouchMove, { passive: false })
|
|
84
|
+
elementRef.addEventListener('touchend', onTouchEnd, { passive: true } as any)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
cleanUpEvents = () => {
|
|
89
|
+
if (elementRef) {
|
|
90
|
+
elementRef.removeEventListener('touchmove', onTouchMove)
|
|
91
|
+
elementRef.removeEventListener('touchend', onTouchEnd)
|
|
92
|
+
elementRef = null
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const removeTouchStartListener = () => {
|
|
97
|
+
if (touchStartElement) {
|
|
98
|
+
touchStartElement.removeEventListener('touchstart', onTouchStart)
|
|
99
|
+
touchStartElement = null
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const teardown = () => {
|
|
104
|
+
removeTouchStartListener()
|
|
105
|
+
cleanUpEvents()
|
|
106
|
+
if (intervalId) {
|
|
107
|
+
clearInterval(intervalId)
|
|
108
|
+
intervalId = null
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onUnmounted(teardown)
|
|
113
|
+
|
|
114
|
+
watch(
|
|
115
|
+
[inVirtual, listRef],
|
|
116
|
+
([enabled, ele], _prev, onCleanup) => {
|
|
117
|
+
if (enabled && ele) {
|
|
118
|
+
touchStartElement = ele
|
|
119
|
+
ele.addEventListener('touchstart', onTouchStart, { passive: true } as any)
|
|
120
|
+
|
|
121
|
+
onCleanup(() => {
|
|
122
|
+
teardown()
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
teardown()
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{ immediate: true },
|
|
130
|
+
)
|
|
131
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ref, type Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export default function useOriginScroll(
|
|
4
|
+
isScrollAtTop: Ref<boolean>,
|
|
5
|
+
isScrollAtBottom: Ref<boolean>,
|
|
6
|
+
isScrollAtLeft: Ref<boolean>,
|
|
7
|
+
isScrollAtRight: Ref<boolean>,
|
|
8
|
+
) {
|
|
9
|
+
// Do lock for a wheel when scrolling
|
|
10
|
+
const lockRef = ref(false)
|
|
11
|
+
let lockTimeout: ReturnType<typeof setTimeout> | null = null
|
|
12
|
+
|
|
13
|
+
function lockScroll() {
|
|
14
|
+
if (lockTimeout)
|
|
15
|
+
clearTimeout(lockTimeout)
|
|
16
|
+
|
|
17
|
+
lockRef.value = true
|
|
18
|
+
|
|
19
|
+
lockTimeout = setTimeout(() => {
|
|
20
|
+
lockRef.value = false
|
|
21
|
+
}, 50)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (isHorizontal: boolean, delta: number, smoothOffset = false) => {
|
|
25
|
+
const originScroll = isHorizontal
|
|
26
|
+
// Pass origin wheel when on the left
|
|
27
|
+
? (delta < 0 && isScrollAtLeft.value)
|
|
28
|
+
// Pass origin wheel when on the right
|
|
29
|
+
|| (delta > 0 && isScrollAtRight.value)
|
|
30
|
+
// Pass origin wheel when on the top
|
|
31
|
+
: (delta < 0 && isScrollAtTop.value)
|
|
32
|
+
// Pass origin wheel when on the bottom
|
|
33
|
+
|| (delta > 0 && isScrollAtBottom.value)
|
|
34
|
+
|
|
35
|
+
if (smoothOffset && originScroll) {
|
|
36
|
+
// No need lock anymore when it's smooth offset from touchMove interval
|
|
37
|
+
if (lockTimeout)
|
|
38
|
+
clearTimeout(lockTimeout)
|
|
39
|
+
lockRef.value = false
|
|
40
|
+
}
|
|
41
|
+
else if (!originScroll || lockRef.value) {
|
|
42
|
+
lockScroll()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return !lockRef.value && originScroll
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { onUnmounted, type Ref, watch } from 'vue'
|
|
2
|
+
|
|
3
|
+
function smoothScrollOffset(offset: number) {
|
|
4
|
+
return Math.floor(offset ** 0.5)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getPageXY(
|
|
8
|
+
e: MouseEvent | TouchEvent,
|
|
9
|
+
horizontal: boolean,
|
|
10
|
+
): number {
|
|
11
|
+
const obj = 'touches' in e ? e.touches[0] : e
|
|
12
|
+
return obj[horizontal ? 'pageX' : 'pageY'] - window[horizontal ? 'scrollX' : 'scrollY']
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function useScrollDrag(
|
|
16
|
+
inVirtual: Ref<boolean>,
|
|
17
|
+
componentRef: Ref<HTMLElement | null | undefined>,
|
|
18
|
+
onScrollOffset: (offset: number) => void,
|
|
19
|
+
) {
|
|
20
|
+
let cachedElement: HTMLElement | null = null
|
|
21
|
+
let cachedDocument: Document | null = null
|
|
22
|
+
let mouseDownLock = false
|
|
23
|
+
let rafId: number | null = null
|
|
24
|
+
let offset = 0
|
|
25
|
+
|
|
26
|
+
const stopScroll = () => {
|
|
27
|
+
if (rafId !== null) {
|
|
28
|
+
cancelAnimationFrame(rafId)
|
|
29
|
+
rafId = null
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const continueScroll = () => {
|
|
34
|
+
stopScroll()
|
|
35
|
+
|
|
36
|
+
rafId = requestAnimationFrame(() => {
|
|
37
|
+
onScrollOffset(offset)
|
|
38
|
+
continueScroll()
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const clearDragState = () => {
|
|
43
|
+
mouseDownLock = false
|
|
44
|
+
stopScroll()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const onMouseDown = (e: MouseEvent) => {
|
|
48
|
+
// Skip if element set draggable
|
|
49
|
+
if ((e.target as HTMLElement).draggable || e.button !== 0) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
// Skip if nest List has handled this event
|
|
53
|
+
const event = e as MouseEvent & {
|
|
54
|
+
_virtualHandled?: boolean
|
|
55
|
+
}
|
|
56
|
+
if (!event._virtualHandled) {
|
|
57
|
+
event._virtualHandled = true
|
|
58
|
+
mouseDownLock = true
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const onMouseMove = (e: MouseEvent) => {
|
|
63
|
+
if (mouseDownLock && cachedElement) {
|
|
64
|
+
const mouseY = getPageXY(e, false)
|
|
65
|
+
const { top, bottom } = cachedElement.getBoundingClientRect()
|
|
66
|
+
|
|
67
|
+
if (mouseY <= top) {
|
|
68
|
+
const diff = top - mouseY
|
|
69
|
+
offset = -smoothScrollOffset(diff)
|
|
70
|
+
continueScroll()
|
|
71
|
+
}
|
|
72
|
+
else if (mouseY >= bottom) {
|
|
73
|
+
const diff = mouseY - bottom
|
|
74
|
+
offset = smoothScrollOffset(diff)
|
|
75
|
+
continueScroll()
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
stopScroll()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const teardown = () => {
|
|
84
|
+
if (cachedElement) {
|
|
85
|
+
cachedElement.removeEventListener('mousedown', onMouseDown)
|
|
86
|
+
cachedElement = null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (cachedDocument) {
|
|
90
|
+
cachedDocument.removeEventListener('mouseup', clearDragState)
|
|
91
|
+
cachedDocument.removeEventListener('mousemove', onMouseMove)
|
|
92
|
+
cachedDocument.removeEventListener('dragend', clearDragState)
|
|
93
|
+
cachedDocument = null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
clearDragState()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
onUnmounted(teardown)
|
|
100
|
+
|
|
101
|
+
watch(
|
|
102
|
+
[inVirtual, componentRef],
|
|
103
|
+
([enabled, ele], _prev, onCleanup) => {
|
|
104
|
+
if (enabled && ele) {
|
|
105
|
+
cachedElement = ele
|
|
106
|
+
cachedDocument = ele.ownerDocument
|
|
107
|
+
|
|
108
|
+
cachedElement.addEventListener('mousedown', onMouseDown)
|
|
109
|
+
cachedDocument.addEventListener('mouseup', clearDragState)
|
|
110
|
+
cachedDocument.addEventListener('mousemove', onMouseMove)
|
|
111
|
+
cachedDocument.addEventListener('dragend', clearDragState)
|
|
112
|
+
|
|
113
|
+
onCleanup(() => {
|
|
114
|
+
teardown()
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
teardown()
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
{ immediate: true },
|
|
122
|
+
)
|
|
123
|
+
}
|
package/src/index.ts
ADDED
package/src/interface.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Key } from '@v-c/util/dist/type'
|
|
2
|
+
import type { CSSProperties, VNode } from 'vue'
|
|
3
|
+
|
|
4
|
+
export type RenderFunc<T> = (
|
|
5
|
+
item: T,
|
|
6
|
+
index: number,
|
|
7
|
+
props: { style: CSSProperties, offsetX: number },
|
|
8
|
+
) => VNode
|
|
9
|
+
|
|
10
|
+
export interface SharedConfig<T> {
|
|
11
|
+
getKey: (item: T) => Key
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type GetKey<T> = (item: T) => Key
|
|
15
|
+
|
|
16
|
+
export type GetSize = (startKey: Key, endKey?: Key) => { top: number, bottom: number }
|
|
17
|
+
|
|
18
|
+
export interface ExtraRenderInfo {
|
|
19
|
+
/** Virtual list start line */
|
|
20
|
+
start: number
|
|
21
|
+
/** Virtual list end line */
|
|
22
|
+
end: number
|
|
23
|
+
/** Is current in virtual render */
|
|
24
|
+
virtual: boolean
|
|
25
|
+
/** Used for `scrollWidth` tell the horizontal offset */
|
|
26
|
+
offsetX: number
|
|
27
|
+
offsetY: number
|
|
28
|
+
|
|
29
|
+
rtl: boolean
|
|
30
|
+
|
|
31
|
+
getSize: GetSize
|
|
32
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Key } from '@v-c/util/dist/type'
|
|
2
|
+
|
|
3
|
+
// Firefox has low performance of map.
|
|
4
|
+
class CacheMap {
|
|
5
|
+
maps: Record<string, number>
|
|
6
|
+
|
|
7
|
+
// Used for cache key
|
|
8
|
+
// `useMemo` no need to update if `id` not change
|
|
9
|
+
id: number = 0
|
|
10
|
+
|
|
11
|
+
diffRecords = new Map<Key, number>()
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
this.maps = Object.create(null)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
set(key: Key, value: number) {
|
|
18
|
+
// Record prev value
|
|
19
|
+
this.diffRecords.set(key, this.maps[key as string])
|
|
20
|
+
|
|
21
|
+
this.maps[key as string] = value
|
|
22
|
+
this.id += 1
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get(key: Key) {
|
|
26
|
+
return this.maps[key as string]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* CacheMap will record the key changed.
|
|
31
|
+
* To help to know what's update in the next render.
|
|
32
|
+
*/
|
|
33
|
+
resetRecord() {
|
|
34
|
+
this.diffRecords.clear()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getRecord() {
|
|
38
|
+
return this.diffRecords
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default CacheMap
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const MIN_SIZE = 20
|
|
2
|
+
|
|
3
|
+
export function getSpinSize(containerSize = 0, scrollRange = 0) {
|
|
4
|
+
let baseSize = (containerSize / scrollRange) * containerSize
|
|
5
|
+
if (isNaN(baseSize)) {
|
|
6
|
+
baseSize = 0
|
|
7
|
+
}
|
|
8
|
+
baseSize = Math.max(baseSize, MIN_SIZE)
|
|
9
|
+
return Math.floor(baseSize)
|
|
10
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { UserConfig } from 'vite'
|
|
2
|
+
import fg from 'fast-glob'
|
|
3
|
+
import { defineConfig, mergeConfig } from 'vite'
|
|
4
|
+
import { buildCommon } from '../../scripts/build.common'
|
|
5
|
+
|
|
6
|
+
const entry = fg.sync(['src/**/*.ts', 'src/**/*.tsx', '!src/**/*.test.ts', '!src/**/*.test.tsx', '!src/**/tests'])
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
...mergeConfig(buildCommon({
|
|
10
|
+
external: ['vue', /^@v-c\//],
|
|
11
|
+
}), {
|
|
12
|
+
build: {
|
|
13
|
+
lib: {
|
|
14
|
+
entry,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
} as UserConfig),
|
|
18
|
+
})
|