@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.
@@ -1,132 +0,0 @@
1
- import type { Ref } from 'vue'
2
- import { onUnmounted, ref, watch } from 'vue'
3
-
4
- const SMOOTH_PTG = 14 / 15
5
-
6
- export default function useMobileTouchMove(
7
- inVirtual: Ref<boolean>,
8
- listRef: Ref<HTMLDivElement | null | undefined>,
9
- callback: (
10
- isHorizontal: boolean,
11
- offset: number,
12
- smoothOffset: boolean,
13
- e?: TouchEvent,
14
- ) => boolean,
15
- ) {
16
- const touchedRef = ref(false)
17
- const touchXRef = ref(0)
18
- const touchYRef = ref(0)
19
-
20
- let elementRef: HTMLElement | null = null
21
- let touchStartElement: HTMLDivElement | null = null
22
-
23
- // Smooth scroll
24
- let intervalId: ReturnType<typeof setInterval> | null = null
25
-
26
- let cleanUpEvents: () => void
27
-
28
- const onTouchMove = (e: TouchEvent) => {
29
- if (touchedRef.value) {
30
- const currentX = Math.ceil(e.touches[0].pageX)
31
- const currentY = Math.ceil(e.touches[0].pageY)
32
- let offsetX = touchXRef.value - currentX
33
- let offsetY = touchYRef.value - currentY
34
- const isHorizontal = Math.abs(offsetX) > Math.abs(offsetY)
35
- if (isHorizontal) {
36
- touchXRef.value = currentX
37
- }
38
- else {
39
- touchYRef.value = currentY
40
- }
41
-
42
- const scrollHandled = callback(isHorizontal, isHorizontal ? offsetX : offsetY, false, e)
43
- if (scrollHandled) {
44
- e.preventDefault()
45
- }
46
-
47
- // Smooth interval
48
- if (intervalId)
49
- clearInterval(intervalId)
50
-
51
- if (scrollHandled) {
52
- intervalId = setInterval(() => {
53
- if (isHorizontal) {
54
- offsetX *= SMOOTH_PTG
55
- }
56
- else {
57
- offsetY *= SMOOTH_PTG
58
- }
59
- const offset = Math.floor(isHorizontal ? offsetX : offsetY)
60
- if (!callback(isHorizontal, offset, true) || Math.abs(offset) <= 0.1) {
61
- if (intervalId)
62
- clearInterval(intervalId)
63
- }
64
- }, 16)
65
- }
66
- }
67
- }
68
-
69
- const onTouchEnd = () => {
70
- touchedRef.value = false
71
-
72
- cleanUpEvents()
73
- }
74
-
75
- const onTouchStart = (e: TouchEvent) => {
76
- cleanUpEvents()
77
-
78
- if (e.touches.length === 1 && !touchedRef.value) {
79
- touchedRef.value = true
80
- touchXRef.value = Math.ceil(e.touches[0].pageX)
81
- touchYRef.value = Math.ceil(e.touches[0].pageY)
82
-
83
- elementRef = e.target as HTMLElement
84
- elementRef.addEventListener('touchmove', onTouchMove, { passive: false })
85
- elementRef.addEventListener('touchend', onTouchEnd, { passive: true } as any)
86
- }
87
- }
88
-
89
- cleanUpEvents = () => {
90
- if (elementRef) {
91
- elementRef.removeEventListener('touchmove', onTouchMove)
92
- elementRef.removeEventListener('touchend', onTouchEnd)
93
- elementRef = null
94
- }
95
- }
96
-
97
- const removeTouchStartListener = () => {
98
- if (touchStartElement) {
99
- touchStartElement.removeEventListener('touchstart', onTouchStart)
100
- touchStartElement = null
101
- }
102
- }
103
-
104
- const teardown = () => {
105
- removeTouchStartListener()
106
- cleanUpEvents()
107
- if (intervalId) {
108
- clearInterval(intervalId)
109
- intervalId = null
110
- }
111
- }
112
-
113
- onUnmounted(teardown)
114
-
115
- watch(
116
- [inVirtual, listRef],
117
- ([enabled, ele], _prev, onCleanup) => {
118
- if (enabled && ele) {
119
- touchStartElement = ele
120
- ele.addEventListener('touchstart', onTouchStart, { passive: true } as any)
121
-
122
- onCleanup(() => {
123
- teardown()
124
- })
125
- }
126
- else {
127
- teardown()
128
- }
129
- },
130
- { immediate: true },
131
- )
132
- }
@@ -1,48 +0,0 @@
1
- import type { Ref } from 'vue'
2
- import { ref } from 'vue'
3
-
4
- export default function useOriginScroll(
5
- isScrollAtTop: Ref<boolean>,
6
- isScrollAtBottom: Ref<boolean>,
7
- isScrollAtLeft: Ref<boolean>,
8
- isScrollAtRight: Ref<boolean>,
9
- ) {
10
- // Do lock for a wheel when scrolling
11
- const lockRef = ref(false)
12
- let lockTimeout: ReturnType<typeof setTimeout> | null = null
13
-
14
- function lockScroll() {
15
- if (lockTimeout)
16
- clearTimeout(lockTimeout)
17
-
18
- lockRef.value = true
19
-
20
- lockTimeout = setTimeout(() => {
21
- lockRef.value = false
22
- }, 50)
23
- }
24
-
25
- return (isHorizontal: boolean, delta: number, smoothOffset = false) => {
26
- const originScroll = isHorizontal
27
- // Pass origin wheel when on the left
28
- ? (delta < 0 && isScrollAtLeft.value)
29
- // Pass origin wheel when on the right
30
- || (delta > 0 && isScrollAtRight.value)
31
- // Pass origin wheel when on the top
32
- : (delta < 0 && isScrollAtTop.value)
33
- // Pass origin wheel when on the bottom
34
- || (delta > 0 && isScrollAtBottom.value)
35
-
36
- if (smoothOffset && originScroll) {
37
- // No need lock anymore when it's smooth offset from touchMove interval
38
- if (lockTimeout)
39
- clearTimeout(lockTimeout)
40
- lockRef.value = false
41
- }
42
- else if (!originScroll || lockRef.value) {
43
- lockScroll()
44
- }
45
-
46
- return !lockRef.value && originScroll
47
- }
48
- }
@@ -1,124 +0,0 @@
1
- import type { Ref } from 'vue'
2
- import { onUnmounted, watch } from 'vue'
3
-
4
- function smoothScrollOffset(offset: number) {
5
- return Math.floor(offset ** 0.5)
6
- }
7
-
8
- export function getPageXY(
9
- e: MouseEvent | TouchEvent,
10
- horizontal: boolean,
11
- ): number {
12
- const obj = 'touches' in e ? e.touches[0] : e
13
- return obj[horizontal ? 'pageX' : 'pageY'] - window[horizontal ? 'scrollX' : 'scrollY']
14
- }
15
-
16
- export default function useScrollDrag(
17
- inVirtual: Ref<boolean>,
18
- componentRef: Ref<HTMLElement | null | undefined>,
19
- onScrollOffset: (offset: number) => void,
20
- ) {
21
- let cachedElement: HTMLElement | null = null
22
- let cachedDocument: Document | null = null
23
- let mouseDownLock = false
24
- let rafId: number | null = null
25
- let offset = 0
26
-
27
- const stopScroll = () => {
28
- if (rafId !== null) {
29
- cancelAnimationFrame(rafId)
30
- rafId = null
31
- }
32
- }
33
-
34
- const continueScroll = () => {
35
- stopScroll()
36
-
37
- rafId = requestAnimationFrame(() => {
38
- onScrollOffset(offset)
39
- continueScroll()
40
- })
41
- }
42
-
43
- const clearDragState = () => {
44
- mouseDownLock = false
45
- stopScroll()
46
- }
47
-
48
- const onMouseDown = (e: MouseEvent) => {
49
- // Skip if element set draggable
50
- if ((e.target as HTMLElement).draggable || e.button !== 0) {
51
- return
52
- }
53
- // Skip if nest List has handled this event
54
- const event = e as MouseEvent & {
55
- _virtualHandled?: boolean
56
- }
57
- if (!event._virtualHandled) {
58
- event._virtualHandled = true
59
- mouseDownLock = true
60
- }
61
- }
62
-
63
- const onMouseMove = (e: MouseEvent) => {
64
- if (mouseDownLock && cachedElement) {
65
- const mouseY = getPageXY(e, false)
66
- const { top, bottom } = cachedElement.getBoundingClientRect()
67
-
68
- if (mouseY <= top) {
69
- const diff = top - mouseY
70
- offset = -smoothScrollOffset(diff)
71
- continueScroll()
72
- }
73
- else if (mouseY >= bottom) {
74
- const diff = mouseY - bottom
75
- offset = smoothScrollOffset(diff)
76
- continueScroll()
77
- }
78
- else {
79
- stopScroll()
80
- }
81
- }
82
- }
83
-
84
- const teardown = () => {
85
- if (cachedElement) {
86
- cachedElement.removeEventListener('mousedown', onMouseDown)
87
- cachedElement = null
88
- }
89
-
90
- if (cachedDocument) {
91
- cachedDocument.removeEventListener('mouseup', clearDragState)
92
- cachedDocument.removeEventListener('mousemove', onMouseMove)
93
- cachedDocument.removeEventListener('dragend', clearDragState)
94
- cachedDocument = null
95
- }
96
-
97
- clearDragState()
98
- }
99
-
100
- onUnmounted(teardown)
101
-
102
- watch(
103
- [inVirtual, componentRef],
104
- ([enabled, ele], _prev, onCleanup) => {
105
- if (enabled && ele) {
106
- cachedElement = ele
107
- cachedDocument = ele.ownerDocument
108
-
109
- cachedElement.addEventListener('mousedown', onMouseDown)
110
- cachedDocument.addEventListener('mouseup', clearDragState)
111
- cachedDocument.addEventListener('mousemove', onMouseMove)
112
- cachedDocument.addEventListener('dragend', clearDragState)
113
-
114
- onCleanup(() => {
115
- teardown()
116
- })
117
- }
118
- else {
119
- teardown()
120
- }
121
- },
122
- { immediate: true },
123
- )
124
- }
@@ -1,184 +0,0 @@
1
- import type { Key } from '@v-c/util/dist/type'
2
- import type { Ref } from 'vue'
3
- import type { GetKey } from '../interface.ts'
4
- import type CacheMap from '../utils/CacheMap.ts'
5
- import { warning } from '@v-c/util'
6
- import raf from '@v-c/util/dist/raf'
7
- import { shallowRef, watch } from 'vue'
8
-
9
- const MAX_TIMES = 10
10
-
11
- export type ScrollAlign = 'top' | 'bottom' | 'auto'
12
-
13
- export interface ScrollPos {
14
- left?: number
15
- top?: number
16
- }
17
-
18
- export type ScrollTarget = {
19
- index: number
20
- align?: ScrollAlign
21
- offset?: number
22
- } | {
23
- key: Key
24
- align?: ScrollAlign
25
- offset?: number
26
- }
27
-
28
- export default function useScrollTo(
29
- containerRef: Ref<HTMLDivElement>,
30
- data: Ref<any[]>,
31
- heights: CacheMap,
32
- itemHeight: Ref<number>,
33
- getKey: GetKey<any>,
34
- collectHeight: () => void,
35
- syncScrollTop: (newTop: number) => void,
36
- triggerFlash: () => void,
37
- ): (arg: number | ScrollTarget) => void {
38
- const scrollRef = shallowRef<number>()
39
-
40
- const syncState = shallowRef<{
41
- times: number
42
- index: number
43
- offset: number
44
- originAlign: ScrollAlign
45
- targetAlign?: 'top' | 'bottom'
46
- lastTop?: number
47
- } | null>(null)
48
-
49
- // ========================== Sync Scroll ==========================
50
- watch(
51
- [syncState, containerRef],
52
- () => {
53
- if (syncState.value && syncState.value.times < MAX_TIMES) {
54
- // Never reach
55
- if (!containerRef.value) {
56
- syncState.value = { ...syncState.value }
57
- return
58
- }
59
- collectHeight()
60
- const { targetAlign, originAlign, index, offset } = syncState.value
61
-
62
- const height = containerRef.value.clientHeight
63
- let needCollectHeight = false
64
- let newTargetAlign: 'top' | 'bottom' | null = targetAlign ?? null
65
- let targetTop: number | null = null
66
- // Go to next frame if height not exist
67
- if (height) {
68
- const mergedAlign = targetAlign || originAlign
69
- // Get top & bottom
70
- let stackTop = 0
71
- let itemTop = 0
72
- let itemBottom = 0
73
-
74
- const maxLen = Math.min(data.value.length - 1, index)
75
-
76
- for (let i = 0; i <= maxLen; i += 1) {
77
- const key = getKey(data.value[i])
78
- itemTop = stackTop
79
- const cacheHeight = heights.get(key)
80
- itemBottom = itemTop + (cacheHeight === undefined ? itemHeight.value : cacheHeight)
81
-
82
- stackTop = itemBottom
83
- }
84
-
85
- // Check if need sync height (visible range has item not record height)
86
- let leftHeight = mergedAlign === 'top' ? offset : height - offset
87
- for (let i = maxLen; i >= 0; i -= 1) {
88
- const key = getKey(data.value[i])
89
- const cacheHeight = heights.get(key)
90
-
91
- if (cacheHeight === undefined) {
92
- needCollectHeight = true
93
- break
94
- }
95
-
96
- leftHeight -= cacheHeight
97
- if (leftHeight <= 0) {
98
- break
99
- }
100
- }
101
-
102
- // Scroll to
103
- switch (mergedAlign) {
104
- case 'top':
105
- targetTop = itemTop - offset
106
- break
107
- case 'bottom':
108
- targetTop = itemBottom - height + offset
109
- break
110
-
111
- default: {
112
- const { scrollTop } = containerRef.value
113
- const scrollBottom = scrollTop + height
114
- if (itemTop < scrollTop) {
115
- newTargetAlign = 'top'
116
- }
117
- else if (itemBottom > scrollBottom) {
118
- newTargetAlign = 'bottom'
119
- }
120
- }
121
- }
122
- if (targetTop !== null) {
123
- syncScrollTop(targetTop)
124
- }
125
-
126
- // One more time for sync
127
- if (targetTop !== syncState.value.lastTop) {
128
- needCollectHeight = true
129
- }
130
- }
131
-
132
- if (needCollectHeight) {
133
- syncState.value = {
134
- ...syncState.value,
135
- times: syncState.value.times + 1,
136
- targetAlign: newTargetAlign as any,
137
- lastTop: targetTop as any,
138
- }
139
- }
140
- }
141
- else if (process.env.NODE_ENV !== 'production' && syncState.value?.times === MAX_TIMES) {
142
- warning(
143
- false,
144
- 'Seems `scrollTo` with `rc-virtual-list` reach the max limitation. Please fire issue for us. Thanks.',
145
- )
146
- }
147
- },
148
- {
149
- immediate: true,
150
- flush: 'post',
151
- },
152
- )
153
-
154
- // =========================== Scroll To ===========================
155
- return (arg) => {
156
- if (arg === null || arg === undefined) {
157
- triggerFlash()
158
- return
159
- }
160
- // Normalize target
161
- raf.cancel(scrollRef.value!)
162
- if (typeof arg === 'number') {
163
- syncScrollTop(arg)
164
- }
165
- else if (arg && typeof arg === 'object') {
166
- let index: number
167
- const { align } = arg
168
- if ('index' in arg) {
169
- ({ index } = arg)
170
- }
171
- else {
172
- index = data.value.findIndex(item => getKey(item) === arg.key)
173
- }
174
- const { offset = 0 } = arg
175
-
176
- syncState.value = {
177
- times: 0,
178
- index,
179
- offset,
180
- originAlign: align!,
181
- }
182
- }
183
- }
184
- }
package/src/index.ts DELETED
@@ -1,5 +0,0 @@
1
- import List from './List'
2
-
3
- export type { ListProps, ListRef, ScrollConfig, ScrollInfo, ScrollTo } from './List'
4
-
5
- export default List
package/src/interface.ts DELETED
@@ -1,32 +0,0 @@
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
- }
@@ -1,42 +0,0 @@
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
@@ -1,3 +0,0 @@
1
- const isFF = typeof navigator === 'object' && /Firefox/i.test(navigator.userAgent)
2
-
3
- export default isFF
@@ -1,10 +0,0 @@
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/tsconfig.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "extends": ["../../tsconfig.base.json"],
3
- "compilerOptions": {
4
-
5
- },
6
- "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.jsx"]
7
- }
package/vite.config.ts DELETED
@@ -1,18 +0,0 @@
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
- })
package/vitest.config.ts DELETED
@@ -1,11 +0,0 @@
1
- import { defineProject, mergeConfig } from 'vitest/config'
2
- import configShared from '../../vitest.config'
3
-
4
- export default mergeConfig(
5
- configShared,
6
- defineProject({
7
- test: {
8
- environment: 'jsdom',
9
- },
10
- }),
11
- )