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