@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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/dist/Filler.cjs +1 -0
  3. package/dist/Filler.d.ts +33 -0
  4. package/dist/Filler.js +57 -0
  5. package/dist/Item.cjs +1 -0
  6. package/dist/Item.d.ts +18 -0
  7. package/dist/Item.js +27 -0
  8. package/dist/List.cjs +1 -0
  9. package/dist/List.d.ts +108 -0
  10. package/dist/List.js +276 -0
  11. package/dist/ScrollBar.cjs +1 -0
  12. package/dist/ScrollBar.d.ts +116 -0
  13. package/dist/ScrollBar.js +193 -0
  14. package/dist/hooks/useDiffItem.cjs +1 -0
  15. package/dist/hooks/useDiffItem.d.ts +2 -0
  16. package/dist/hooks/useDiffItem.js +21 -0
  17. package/dist/hooks/useFrameWheel.cjs +1 -0
  18. package/dist/hooks/useFrameWheel.d.ts +11 -0
  19. package/dist/hooks/useFrameWheel.js +52 -0
  20. package/dist/hooks/useGetSize.cjs +1 -0
  21. package/dist/hooks/useGetSize.d.ts +7 -0
  22. package/dist/hooks/useGetSize.js +24 -0
  23. package/dist/hooks/useHeights.cjs +1 -0
  24. package/dist/hooks/useHeights.d.ts +9 -0
  25. package/dist/hooks/useHeights.js +43 -0
  26. package/dist/hooks/useMobileTouchMove.cjs +1 -0
  27. package/dist/hooks/useMobileTouchMove.d.ts +2 -0
  28. package/dist/hooks/useMobileTouchMove.js +44 -0
  29. package/dist/hooks/useOriginScroll.cjs +1 -0
  30. package/dist/hooks/useOriginScroll.d.ts +2 -0
  31. package/dist/hooks/useOriginScroll.js +17 -0
  32. package/dist/hooks/useScrollDrag.cjs +1 -0
  33. package/dist/hooks/useScrollDrag.d.ts +3 -0
  34. package/dist/hooks/useScrollDrag.js +51 -0
  35. package/dist/index.cjs +1 -0
  36. package/dist/index.d.ts +3 -0
  37. package/dist/index.js +4 -0
  38. package/dist/interface.cjs +1 -0
  39. package/dist/interface.d.ts +27 -0
  40. package/dist/interface.js +1 -0
  41. package/dist/utils/CacheMap.cjs +1 -0
  42. package/dist/utils/CacheMap.d.ts +16 -0
  43. package/dist/utils/CacheMap.js +29 -0
  44. package/dist/utils/isFirefox.cjs +1 -0
  45. package/dist/utils/isFirefox.d.ts +2 -0
  46. package/dist/utils/isFirefox.js +4 -0
  47. package/dist/utils/scrollbarUtil.cjs +1 -0
  48. package/dist/utils/scrollbarUtil.d.ts +1 -0
  49. package/dist/utils/scrollbarUtil.js +7 -0
  50. package/docs/basic.vue +175 -0
  51. package/docs/height.vue +48 -0
  52. package/docs/nest.vue +60 -0
  53. package/docs/no-virtual.vue +127 -0
  54. package/docs/switch.vue +101 -0
  55. package/docs/virtual-list.stories.vue +31 -0
  56. package/package.json +38 -0
  57. package/src/Filler.tsx +72 -0
  58. package/src/Item.tsx +34 -0
  59. package/src/List.tsx +577 -0
  60. package/src/ScrollBar.tsx +298 -0
  61. package/src/__tests__/List.test.ts +59 -0
  62. package/src/hooks/useDiffItem.ts +27 -0
  63. package/src/hooks/useFrameWheel.ts +141 -0
  64. package/src/hooks/useGetSize.ts +44 -0
  65. package/src/hooks/useHeights.ts +106 -0
  66. package/src/hooks/useMobileTouchMove.ts +131 -0
  67. package/src/hooks/useOriginScroll.ts +47 -0
  68. package/src/hooks/useScrollDrag.ts +123 -0
  69. package/src/index.ts +5 -0
  70. package/src/interface.ts +32 -0
  71. package/src/utils/CacheMap.ts +42 -0
  72. package/src/utils/isFirefox.ts +3 -0
  73. package/src/utils/scrollbarUtil.ts +10 -0
  74. package/vite.config.ts +18 -0
  75. 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
@@ -0,0 +1,5 @@
1
+ import List from './List'
2
+
3
+ export type { ListProps, ListRef, ScrollConfig, ScrollInfo } from './List'
4
+
5
+ export default List
@@ -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,3 @@
1
+ const isFF = typeof navigator === 'object' && /Firefox/i.test(navigator.userAgent)
2
+
3
+ export default isFF
@@ -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
+ })
@@ -0,0 +1,11 @@
1
+ import { defineProject, mergeConfig } from 'vitest/config'
2
+ import configShared from '../../vitest.config.ts'
3
+
4
+ export default mergeConfig(
5
+ configShared,
6
+ defineProject({
7
+ test: {
8
+ environment: 'jsdom',
9
+ },
10
+ }),
11
+ )