@v-c/virtual-list 0.0.1 → 1.0.0

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 (66) hide show
  1. package/bump.config.ts +6 -0
  2. package/dist/Filler.cjs +51 -1
  3. package/dist/Filler.js +47 -56
  4. package/dist/Item.cjs +26 -1
  5. package/dist/Item.js +23 -26
  6. package/dist/List.cjs +408 -1
  7. package/dist/List.d.ts +45 -9
  8. package/dist/List.js +403 -274
  9. package/dist/ScrollBar.cjs +259 -1
  10. package/dist/ScrollBar.d.ts +3 -97
  11. package/dist/ScrollBar.js +254 -191
  12. package/dist/_virtual/rolldown_runtime.cjs +21 -0
  13. package/dist/hooks/useDiffItem.cjs +19 -1
  14. package/dist/hooks/useDiffItem.js +16 -20
  15. package/dist/hooks/useFrameWheel.cjs +63 -1
  16. package/dist/hooks/useFrameWheel.js +60 -51
  17. package/dist/hooks/useGetSize.cjs +29 -1
  18. package/dist/hooks/useGetSize.d.ts +2 -2
  19. package/dist/hooks/useGetSize.js +27 -23
  20. package/dist/hooks/useHeights.cjs +66 -1
  21. package/dist/hooks/useHeights.d.ts +1 -1
  22. package/dist/hooks/useHeights.js +62 -41
  23. package/dist/hooks/useMobileTouchMove.cjs +82 -1
  24. package/dist/hooks/useMobileTouchMove.js +79 -43
  25. package/dist/hooks/useOriginScroll.cjs +23 -1
  26. package/dist/hooks/useOriginScroll.js +20 -16
  27. package/dist/hooks/useScrollDrag.cjs +83 -1
  28. package/dist/hooks/useScrollDrag.js +77 -48
  29. package/dist/hooks/useScrollTo.cjs +97 -0
  30. package/dist/hooks/useScrollTo.d.ts +19 -0
  31. package/dist/hooks/useScrollTo.js +94 -0
  32. package/dist/index.cjs +4 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.js +3 -4
  35. package/dist/interface.cjs +0 -1
  36. package/dist/interface.d.ts +1 -1
  37. package/dist/interface.js +0 -1
  38. package/dist/utils/CacheMap.cjs +25 -1
  39. package/dist/utils/CacheMap.d.ts +1 -1
  40. package/dist/utils/CacheMap.js +23 -28
  41. package/dist/utils/isFirefox.cjs +4 -1
  42. package/dist/utils/isFirefox.js +2 -4
  43. package/dist/utils/scrollbarUtil.cjs +8 -1
  44. package/dist/utils/scrollbarUtil.js +7 -6
  45. package/docs/animate.less +31 -0
  46. package/docs/animate.vue +159 -0
  47. package/docs/basic.vue +2 -1
  48. package/docs/nest.vue +1 -1
  49. package/docs/switch.vue +2 -1
  50. package/docs/virtual-list.stories.vue +4 -0
  51. package/package.json +16 -14
  52. package/src/Filler.tsx +2 -1
  53. package/src/Item.tsx +2 -1
  54. package/src/List.tsx +189 -124
  55. package/src/ScrollBar.tsx +33 -44
  56. package/src/hooks/useDiffItem.ts +3 -2
  57. package/src/hooks/useFrameWheel.ts +2 -1
  58. package/src/hooks/useGetSize.ts +5 -4
  59. package/src/hooks/useHeights.ts +7 -6
  60. package/src/hooks/useMobileTouchMove.ts +2 -1
  61. package/src/hooks/useOriginScroll.ts +2 -1
  62. package/src/hooks/useScrollDrag.ts +2 -1
  63. package/src/hooks/useScrollTo.tsx +184 -0
  64. package/src/index.ts +1 -1
  65. package/tsconfig.json +7 -0
  66. package/vitest.config.ts +1 -1
package/src/ScrollBar.tsx CHANGED
@@ -1,4 +1,8 @@
1
- import { computed, type CSSProperties, defineComponent, onMounted, onUnmounted, type PropType, ref, shallowRef, watch } from 'vue'
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'
2
6
 
3
7
  export interface ScrollBarProps {
4
8
  prefixCls: string
@@ -28,23 +32,8 @@ function getPageXY(
28
32
  return obj[horizontal ? 'pageX' : 'pageY'] - window[horizontal ? 'scrollX' : 'scrollY']
29
33
  }
30
34
 
31
- export default defineComponent({
35
+ export default defineComponent<ScrollBarProps>({
32
36
  name: 'ScrollBar',
33
- props: {
34
- prefixCls: { type: String, required: true },
35
- scrollOffset: { type: Number, required: true },
36
- scrollRange: { type: Number, required: true },
37
- rtl: { type: Boolean, default: false },
38
- onScroll: { type: Function as PropType<(scrollOffset: number, horizontal?: boolean) => void>, required: true },
39
- onStartMove: { type: Function as PropType<() => void>, required: true },
40
- onStopMove: { type: Function as PropType<() => void>, required: true },
41
- horizontal: { type: Boolean, default: false },
42
- style: Object as PropType<CSSProperties>,
43
- thumbStyle: Object as PropType<CSSProperties>,
44
- spinSize: { type: Number, required: true },
45
- containerSize: { type: Number, required: true },
46
- showScrollBar: { type: [Boolean, String] as PropType<boolean | 'optional'> },
47
- },
48
37
  setup(props, { expose }) {
49
38
  const dragging = ref(false)
50
39
  const pageXY = ref<number | null>(null)
@@ -52,13 +41,11 @@ export default defineComponent({
52
41
 
53
42
  const isLTR = computed(() => !props.rtl)
54
43
 
55
- // Refs
44
+ // ========================= Refs =========================
56
45
  const scrollbarRef = shallowRef<HTMLDivElement>()
57
46
  const thumbRef = shallowRef<HTMLDivElement>()
58
47
 
59
- // Visible
60
- // When showScrollBar is 'optional', start as visible (true)
61
- // When showScrollBar is true/false, use that value
48
+ // ======================= Visible ========================
62
49
  const visible = ref(props.showScrollBar === 'optional' ? true : props.showScrollBar)
63
50
  let visibleTimeout: ReturnType<typeof setTimeout> | null = null
64
51
 
@@ -74,11 +61,11 @@ export default defineComponent({
74
61
  }, 3000)
75
62
  }
76
63
 
77
- // Range
64
+ // ======================== Range =========================
78
65
  const enableScrollRange = computed(() => props.scrollRange - props.containerSize || 0)
79
66
  const enableOffsetRange = computed(() => props.containerSize - props.spinSize || 0)
80
67
 
81
- // Top position
68
+ // ========================= Top ==========================
82
69
  const top = computed(() => {
83
70
  if (props.scrollOffset === 0 || enableScrollRange.value === 0) {
84
71
  return 0
@@ -87,7 +74,7 @@ export default defineComponent({
87
74
  return ptg * enableOffsetRange.value
88
75
  })
89
76
 
90
- // State ref for event handlers
77
+ // ======================== Thumb =========================
91
78
  const stateRef = shallowRef({
92
79
  top: top.value,
93
80
  dragging: dragging.value,
@@ -114,12 +101,16 @@ export default defineComponent({
114
101
  pageXY.value = getPageXY(e, props.horizontal || false)
115
102
  startTop.value = stateRef.value.top
116
103
 
117
- props.onStartMove()
104
+ props?.onStartMove?.()
118
105
  e.stopPropagation()
119
106
  e.preventDefault()
120
107
  }
121
108
 
122
- // Effect: Add passive:false event listeners
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
123
114
  onMounted(() => {
124
115
  const onScrollbarTouchStart = (e: TouchEvent) => {
125
116
  e.preventDefault()
@@ -140,7 +131,7 @@ export default defineComponent({
140
131
  })
141
132
 
142
133
  // Effect: Handle dragging
143
- watch(dragging, (isDragging) => {
134
+ watch(dragging, (isDragging, _O, onCleanup) => {
144
135
  if (isDragging) {
145
136
  let moveRafId: number | null = null
146
137
 
@@ -150,9 +141,7 @@ export default defineComponent({
150
141
  pageY: statePageY,
151
142
  startTop: stateStartTop,
152
143
  } = stateRef.value
153
-
154
- if (moveRafId)
155
- cancelAnimationFrame(moveRafId)
144
+ raf.cancel(moveRafId!)
156
145
 
157
146
  const rect = scrollbarRef.value!.getBoundingClientRect()
158
147
  const scale = props.containerSize / (props.horizontal ? rect.width : rect.height)
@@ -177,8 +166,8 @@ export default defineComponent({
177
166
  newScrollTop = Math.max(newScrollTop, 0)
178
167
  newScrollTop = Math.min(newScrollTop, tmpEnableScrollRange)
179
168
 
180
- moveRafId = requestAnimationFrame(() => {
181
- props.onScroll(newScrollTop, props.horizontal)
169
+ moveRafId = raf(() => {
170
+ props?.onScroll?.(newScrollTop, props.horizontal)
182
171
  })
183
172
  }
184
173
  }
@@ -193,26 +182,25 @@ export default defineComponent({
193
182
  window.addEventListener('mouseup', onMouseUp, { passive: true } as any)
194
183
  window.addEventListener('touchend', onMouseUp, { passive: true } as any)
195
184
 
196
- onUnmounted(() => {
185
+ onCleanup(() => {
197
186
  window.removeEventListener('mousemove', onMouseMove)
198
187
  window.removeEventListener('touchmove', onMouseMove)
199
188
  window.removeEventListener('mouseup', onMouseUp)
200
189
  window.removeEventListener('touchend', onMouseUp)
201
190
 
202
- if (moveRafId)
203
- cancelAnimationFrame(moveRafId)
191
+ raf.cancel(moveRafId!)
204
192
  })
205
193
  }
206
194
  })
207
195
 
208
196
  // Effect: Delay hidden on scroll offset change
209
- watch(() => props.scrollOffset, () => {
197
+ watch(() => props.scrollOffset, (_n, _o, onCleanup) => {
210
198
  delayHidden()
211
- })
212
-
213
- onUnmounted(() => {
214
- if (visibleTimeout)
215
- clearTimeout(visibleTimeout)
199
+ onCleanup(() => {
200
+ if (visibleTimeout) {
201
+ clearTimeout(visibleTimeout)
202
+ }
203
+ })
216
204
  })
217
205
 
218
206
  // Imperative handle
@@ -221,7 +209,8 @@ export default defineComponent({
221
209
  })
222
210
 
223
211
  return () => {
224
- const scrollbarPrefixCls = `${props.prefixCls}-scrollbar`
212
+ const { prefixCls, horizontal } = props
213
+ const scrollbarPrefixCls = `${prefixCls}-scrollbar`
225
214
 
226
215
  const containerStyle: CSSProperties = {
227
216
  position: 'absolute',
@@ -271,8 +260,8 @@ export default defineComponent({
271
260
  class={[
272
261
  scrollbarPrefixCls,
273
262
  {
274
- [`${scrollbarPrefixCls}-horizontal`]: props.horizontal,
275
- [`${scrollbarPrefixCls}-vertical`]: !props.horizontal,
263
+ [`${scrollbarPrefixCls}-horizontal`]: horizontal,
264
+ [`${scrollbarPrefixCls}-vertical`]: !horizontal,
276
265
  [`${scrollbarPrefixCls}-visible`]: visible.value,
277
266
  },
278
267
  ]}
@@ -1,4 +1,5 @@
1
- import { ref, type Ref, watch } from 'vue'
1
+ import type { Ref } from 'vue'
2
+ import { ref, watch } from 'vue'
2
3
 
3
4
  export default function useDiffItem<T>(data: Ref<T[]>, getKey: (item: T) => any): Ref<T | undefined> {
4
5
  const prevDataRef = ref<T[]>([])
@@ -13,7 +14,7 @@ export default function useDiffItem<T>(data: Ref<T[]>, getKey: (item: T) => any)
13
14
  // Find added item
14
15
  const addedItem = newData.find((item) => {
15
16
  const key = getKey(item)
16
- return !prevData.some(prevItem => getKey(prevItem) === key)
17
+ return !prevData.some(prevItem => getKey(prevItem as any) === key)
17
18
  })
18
19
 
19
20
  diffItem.value = addedItem
@@ -1,4 +1,5 @@
1
- import { onUnmounted, ref, type Ref } from 'vue'
1
+ import type { Ref } from 'vue'
2
+ import { onUnmounted, ref } from 'vue'
2
3
  import isFF from '../utils/isFirefox'
3
4
  import useOriginScroll from './useOriginScroll'
4
5
 
@@ -1,12 +1,13 @@
1
+ import type { ComputedRef, Ref } from 'vue'
1
2
  import type { GetKey } from '../interface'
2
3
  import type CacheMap from '../utils/CacheMap'
3
- import { computed, type ComputedRef, type Ref } from 'vue'
4
+ import { computed } from 'vue'
4
5
 
5
6
  export function useGetSize<T>(
6
7
  mergedData: Ref<T[]>,
7
8
  getKey: GetKey<T>,
8
9
  heights: CacheMap,
9
- itemHeight: number,
10
+ itemHeight: Ref<number>,
10
11
  ): ComputedRef<(startKey: any, endKey?: any) => { top: number, bottom: number }> {
11
12
  return computed(() => {
12
13
  return (startKey: any, endKey?: any) => {
@@ -25,14 +26,14 @@ export function useGetSize<T>(
25
26
  for (let i = 0; i < topIndex; i += 1) {
26
27
  const key = getKey(mergedData.value[i])
27
28
  const cacheHeight = heights.get(key)
28
- top += cacheHeight === undefined ? itemHeight : cacheHeight
29
+ top += cacheHeight === undefined ? itemHeight.value : cacheHeight
29
30
  }
30
31
 
31
32
  let bottom = 0
32
33
  for (let i = mergedData.value.length - 1; i > bottomIndex; i -= 1) {
33
34
  const key = getKey(mergedData.value[i])
34
35
  const cacheHeight = heights.get(key)
35
- bottom += cacheHeight === undefined ? itemHeight : cacheHeight
36
+ bottom += cacheHeight === undefined ? itemHeight.value : cacheHeight
36
37
  }
37
38
 
38
39
  return {
@@ -1,6 +1,7 @@
1
1
  import type { Key } from '@v-c/util/dist/type'
2
+ import type { Ref } from 'vue'
2
3
  import type { GetKey } from '../interface'
3
- import { onUnmounted, ref, type Ref } from 'vue'
4
+ import { onUnmounted, reactive, ref } from 'vue'
4
5
  import CacheMap from '../utils/CacheMap'
5
6
 
6
7
  function parseNumber(value: string) {
@@ -17,10 +18,10 @@ export default function useHeights<T>(
17
18
  collectHeight: (sync?: boolean) => void,
18
19
  cacheMap: CacheMap,
19
20
  updatedMark: Ref<number>,
20
- ] {
21
+ ] {
21
22
  const updatedMark = ref(0)
22
23
  const instanceRef = ref(new Map<Key, HTMLElement>())
23
- const heightsRef = ref(new CacheMap())
24
+ const heightsRef = reactive(new CacheMap())
24
25
 
25
26
  const promiseIdRef = ref<number>(0)
26
27
 
@@ -43,8 +44,8 @@ export default function useHeights<T>(
43
44
  const marginBottomNum = parseNumber(marginBottom)
44
45
  const totalHeight = offsetHeight + marginTopNum + marginBottomNum
45
46
 
46
- if (heightsRef.value.get(key) !== totalHeight) {
47
- heightsRef.value.set(key, totalHeight)
47
+ if (heightsRef.get(key) !== totalHeight) {
48
+ heightsRef.set(key, totalHeight)
48
49
  changed = true
49
50
  }
50
51
  }
@@ -102,5 +103,5 @@ export default function useHeights<T>(
102
103
  cancelRaf()
103
104
  })
104
105
 
105
- return [setInstanceRef, collectHeight, heightsRef.value, updatedMark]
106
+ return [setInstanceRef, collectHeight, heightsRef, updatedMark]
106
107
  }
@@ -1,4 +1,5 @@
1
- import { onUnmounted, ref, type Ref, watch } from 'vue'
1
+ import type { Ref } from 'vue'
2
+ import { onUnmounted, ref, watch } from 'vue'
2
3
 
3
4
  const SMOOTH_PTG = 14 / 15
4
5
 
@@ -1,4 +1,5 @@
1
- import { ref, type Ref } from 'vue'
1
+ import type { Ref } from 'vue'
2
+ import { ref } from 'vue'
2
3
 
3
4
  export default function useOriginScroll(
4
5
  isScrollAtTop: Ref<boolean>,
@@ -1,4 +1,5 @@
1
- import { onUnmounted, type Ref, watch } from 'vue'
1
+ import type { Ref } from 'vue'
2
+ import { onUnmounted, watch } from 'vue'
2
3
 
3
4
  function smoothScrollOffset(offset: number) {
4
5
  return Math.floor(offset ** 0.5)
@@ -0,0 +1,184 @@
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 CHANGED
@@ -1,5 +1,5 @@
1
1
  import List from './List'
2
2
 
3
- export type { ListProps, ListRef, ScrollConfig, ScrollInfo } from './List'
3
+ export type { ListProps, ListRef, ScrollConfig, ScrollInfo, ScrollTo } from './List'
4
4
 
5
5
  export default List
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": ["../../tsconfig.base.json"],
3
+ "compilerOptions": {
4
+
5
+ },
6
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.jsx"]
7
+ }
package/vitest.config.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { defineProject, mergeConfig } from 'vitest/config'
2
- import configShared from '../../vitest.config.ts'
2
+ import configShared from '../../vitest.config'
3
3
 
4
4
  export default mergeConfig(
5
5
  configShared,