@wyxos/vibe 1.4.1 → 1.5.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.
- package/lib/index.js +524 -433
- package/lib/vibe.css +1 -1
- package/package.json +3 -2
- package/src/App.vue +3 -1
- package/src/Masonry.vue +219 -32
- package/src/calculateLayout.ts +168 -151
- package/src/masonryUtils.ts +18 -5
- package/src/useMasonryScroll.ts +17 -105
- package/src/useMasonryTransitions.ts +14 -7
package/src/calculateLayout.ts
CHANGED
|
@@ -1,177 +1,194 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {LayoutOptions, MasonryItem, ProcessedMasonryItem} from './types'
|
|
2
|
+
|
|
3
|
+
let __cachedScrollbarWidth: number | null = null
|
|
2
4
|
|
|
3
5
|
function getScrollbarWidth(): number {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
6
|
+
if (__cachedScrollbarWidth != null) return __cachedScrollbarWidth
|
|
7
|
+
const div = document.createElement('div')
|
|
8
|
+
div.style.visibility = 'hidden'
|
|
9
|
+
div.style.overflow = 'scroll'
|
|
10
|
+
;(div.style as any).msOverflowStyle = 'scrollbar'
|
|
11
|
+
div.style.width = '100px'
|
|
12
|
+
div.style.height = '100px'
|
|
13
|
+
document.body.appendChild(div)
|
|
14
|
+
|
|
15
|
+
const inner = document.createElement('div')
|
|
16
|
+
inner.style.width = '100%'
|
|
17
|
+
div.appendChild(inner)
|
|
18
|
+
|
|
19
|
+
const scrollbarWidth = div.offsetWidth - inner.offsetWidth
|
|
20
|
+
document.body.removeChild(div)
|
|
21
|
+
__cachedScrollbarWidth = scrollbarWidth
|
|
22
|
+
return scrollbarWidth
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
export default function calculateLayout(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
items: MasonryItem[],
|
|
27
|
+
container: HTMLElement,
|
|
28
|
+
columnCount: number,
|
|
29
|
+
options: LayoutOptions = {}
|
|
26
30
|
): ProcessedMasonryItem[] {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
31
|
+
const {
|
|
32
|
+
gutterX = 0,
|
|
33
|
+
gutterY = 0,
|
|
34
|
+
header = 0,
|
|
35
|
+
footer = 0,
|
|
36
|
+
paddingLeft = 0,
|
|
37
|
+
paddingRight = 0,
|
|
38
|
+
sizes = {
|
|
39
|
+
base: 1,
|
|
40
|
+
sm: 2,
|
|
41
|
+
md: 3,
|
|
42
|
+
lg: 4,
|
|
43
|
+
xl: 5,
|
|
44
|
+
'2xl': 6
|
|
45
|
+
},
|
|
46
|
+
placement = 'masonry'
|
|
47
|
+
} = options
|
|
48
|
+
|
|
49
|
+
let cssPaddingLeft = 0
|
|
50
|
+
let cssPaddingRight = 0
|
|
51
|
+
try {
|
|
52
|
+
if (container && container.nodeType === 1 && typeof window !== 'undefined' && window.getComputedStyle) {
|
|
53
|
+
const styles = window.getComputedStyle(container)
|
|
54
|
+
cssPaddingLeft = parseFloat(styles.paddingLeft) || 0
|
|
55
|
+
cssPaddingRight = parseFloat(styles.paddingRight) || 0
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// noop
|
|
52
59
|
}
|
|
53
|
-
} catch {
|
|
54
|
-
// noop
|
|
55
|
-
}
|
|
56
60
|
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
const effectivePaddingLeft = (paddingLeft || 0) + cssPaddingLeft
|
|
62
|
+
const effectivePaddingRight = (paddingRight || 0) + cssPaddingRight
|
|
59
63
|
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
const measuredScrollbarWidth = container.offsetWidth - container.clientWidth
|
|
65
|
+
const scrollbarWidth = measuredScrollbarWidth > 0 ? measuredScrollbarWidth + 2 : getScrollbarWidth() + 2
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
const usableWidth = container.offsetWidth - scrollbarWidth - effectivePaddingLeft - effectivePaddingRight
|
|
68
|
+
const totalGutterX = gutterX * (columnCount - 1)
|
|
69
|
+
const columnWidth = Math.floor((usableWidth - totalGutterX) / columnCount)
|
|
66
70
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
const baseHeights = items.map((item) => {
|
|
72
|
+
const originalWidth = item.width
|
|
73
|
+
const originalHeight = item.height
|
|
74
|
+
const imageHeight = Math.round((columnWidth * originalHeight) / originalWidth)
|
|
75
|
+
return imageHeight + footer + header
|
|
76
|
+
})
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
if (placement === 'sequential-balanced') {
|
|
79
|
+
const n = baseHeights.length
|
|
80
|
+
if (n === 0) return []
|
|
77
81
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
const addWithGutter = (currentSum: number, itemsInGroup: number, nextHeight: number) => {
|
|
83
|
+
return currentSum + (itemsInGroup > 0 ? gutterY : 0) + nextHeight
|
|
84
|
+
}
|
|
81
85
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
86
|
+
let low = Math.max(...baseHeights)
|
|
87
|
+
let high = baseHeights.reduce((sum, h) => sum + h, 0) + gutterY * Math.max(0, n - 1)
|
|
88
|
+
|
|
89
|
+
const canPartition = (cap: number) => {
|
|
90
|
+
let groups = 1
|
|
91
|
+
let sum = 0
|
|
92
|
+
let count = 0
|
|
93
|
+
for (let i = 0; i < n; i++) {
|
|
94
|
+
const h = baseHeights[i]
|
|
95
|
+
const next = addWithGutter(sum, count, h)
|
|
96
|
+
if (next <= cap) {
|
|
97
|
+
sum = next
|
|
98
|
+
count++
|
|
99
|
+
} else {
|
|
100
|
+
groups++
|
|
101
|
+
sum = h
|
|
102
|
+
count = 1
|
|
103
|
+
if (h > cap) return false
|
|
104
|
+
if (groups > columnCount) return false
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return groups <= columnCount
|
|
101
108
|
}
|
|
102
|
-
}
|
|
103
|
-
return groups <= columnCount
|
|
104
|
-
}
|
|
105
109
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
110
|
+
while (low < high) {
|
|
111
|
+
const mid = Math.floor((low + high) / 2)
|
|
112
|
+
if (canPartition(mid)) high = mid
|
|
113
|
+
else low = mid + 1
|
|
114
|
+
}
|
|
115
|
+
const cap = high
|
|
116
|
+
|
|
117
|
+
const starts = new Array<number>(columnCount).fill(0)
|
|
118
|
+
let groupIndex = columnCount - 1
|
|
119
|
+
let sum = 0
|
|
120
|
+
let count = 0
|
|
121
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
122
|
+
const h = baseHeights[i]
|
|
123
|
+
const needAtLeast = i < groupIndex
|
|
124
|
+
const canFit = addWithGutter(sum, count, h) <= cap
|
|
125
|
+
if (!canFit || needAtLeast) {
|
|
126
|
+
starts[groupIndex] = i + 1
|
|
127
|
+
groupIndex--
|
|
128
|
+
sum = h
|
|
129
|
+
count = 1
|
|
130
|
+
} else {
|
|
131
|
+
sum = addWithGutter(sum, count, h)
|
|
132
|
+
count++
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
starts[0] = 0
|
|
136
|
+
|
|
137
|
+
const processedItems: ProcessedMasonryItem[] = []
|
|
138
|
+
const tops = new Array<number>(columnCount).fill(0)
|
|
139
|
+
for (let col = 0; col < columnCount; col++) {
|
|
140
|
+
const start = starts[col]
|
|
141
|
+
const end = col + 1 < columnCount ? starts[col + 1] : n
|
|
142
|
+
const left = col * (columnWidth + gutterX)
|
|
143
|
+
for (let i = start; i < end; i++) {
|
|
144
|
+
const item = items[i]
|
|
145
|
+
const newItem: ProcessedMasonryItem = {
|
|
146
|
+
...(item as any),
|
|
147
|
+
columnWidth,
|
|
148
|
+
imageHeight: 0,
|
|
149
|
+
columnHeight: 0,
|
|
150
|
+
left: 0,
|
|
151
|
+
top: 0
|
|
152
|
+
}
|
|
153
|
+
newItem.imageHeight = baseHeights[i] - (footer + header)
|
|
154
|
+
newItem.columnHeight = baseHeights[i]
|
|
155
|
+
newItem.left = left
|
|
156
|
+
newItem.top = tops[col]
|
|
157
|
+
tops[col] += newItem.columnHeight + (i + 1 < end ? gutterY : 0)
|
|
158
|
+
processedItems.push(newItem)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return processedItems
|
|
130
162
|
}
|
|
131
|
-
starts[0] = 0
|
|
132
163
|
|
|
164
|
+
const columnHeights = new Array<number>(columnCount).fill(0)
|
|
133
165
|
const processedItems: ProcessedMasonryItem[] = []
|
|
134
|
-
const tops = new Array<number>(columnCount).fill(0)
|
|
135
|
-
for (let col = 0; col < columnCount; col++) {
|
|
136
|
-
const start = starts[col]
|
|
137
|
-
const end = col + 1 < columnCount ? starts[col + 1] : n
|
|
138
|
-
const left = col * (columnWidth + gutterX)
|
|
139
|
-
for (let i = start; i < end; i++) {
|
|
140
|
-
const item = items[i]
|
|
141
|
-
const newItem: ProcessedMasonryItem = { ...(item as any), columnWidth, imageHeight: 0, columnHeight: 0, left: 0, top: 0 }
|
|
142
|
-
const imageHeight = baseHeights[i] - (footer + header)
|
|
143
|
-
newItem.imageHeight = imageHeight
|
|
144
|
-
newItem.columnHeight = baseHeights[i]
|
|
145
|
-
newItem.left = left
|
|
146
|
-
newItem.top = tops[col]
|
|
147
|
-
tops[col] += newItem.columnHeight + (i + 1 < end ? gutterY : 0)
|
|
148
|
-
processedItems.push(newItem)
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return processedItems
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const columnHeights = new Array<number>(columnCount).fill(0)
|
|
155
|
-
const processedItems: ProcessedMasonryItem[] = []
|
|
156
166
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
167
|
+
for (let index = 0; index < items.length; index++) {
|
|
168
|
+
const item = items[index]
|
|
169
|
+
const newItem: ProcessedMasonryItem = {
|
|
170
|
+
...(item as any),
|
|
171
|
+
columnWidth: 0,
|
|
172
|
+
imageHeight: 0,
|
|
173
|
+
columnHeight: 0,
|
|
174
|
+
left: 0,
|
|
175
|
+
top: 0
|
|
176
|
+
}
|
|
160
177
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
178
|
+
const col = columnHeights.indexOf(Math.min(...columnHeights))
|
|
179
|
+
const originalWidth = item.width
|
|
180
|
+
const originalHeight = item.height
|
|
164
181
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
182
|
+
newItem.columnWidth = columnWidth
|
|
183
|
+
newItem.left = col * (columnWidth + gutterX)
|
|
184
|
+
newItem.imageHeight = Math.round((columnWidth * originalHeight) / originalWidth)
|
|
185
|
+
newItem.columnHeight = newItem.imageHeight + footer + header
|
|
186
|
+
newItem.top = columnHeights[col]
|
|
170
187
|
|
|
171
|
-
|
|
188
|
+
columnHeights[col] += newItem.columnHeight + gutterY
|
|
172
189
|
|
|
173
|
-
|
|
174
|
-
|
|
190
|
+
processedItems.push(newItem)
|
|
191
|
+
}
|
|
175
192
|
|
|
176
|
-
|
|
193
|
+
return processedItems
|
|
177
194
|
}
|
package/src/masonryUtils.ts
CHANGED
|
@@ -55,11 +55,24 @@ export function getItemAttributes(item: ProcessedMasonryItem, index: number = 0)
|
|
|
55
55
|
* Calculate column heights for masonry layout
|
|
56
56
|
*/
|
|
57
57
|
export function calculateColumnHeights(items: ProcessedMasonryItem[], columnCount: number): number[] {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const col = i % columnCount
|
|
62
|
-
heights[col] = Math.max(heights[col], item.top + item.columnHeight)
|
|
58
|
+
// Derive columns by actual left positions to reflect shortest-column placement
|
|
59
|
+
if (!items.length || columnCount <= 0) {
|
|
60
|
+
return new Array<number>(Math.max(1, columnCount)).fill(0)
|
|
63
61
|
}
|
|
62
|
+
// Unique lefts (sorted) represent the columns in visual order
|
|
63
|
+
const uniqueLefts = Array.from(new Set(items.map(i => i.left))).sort((a, b) => a - b)
|
|
64
|
+
const limitedLefts = uniqueLefts.slice(0, columnCount)
|
|
65
|
+
const leftIndexMap = new Map<number, number>()
|
|
66
|
+
for (let idx = 0; idx < limitedLefts.length; idx++) leftIndexMap.set(limitedLefts[idx], idx)
|
|
67
|
+
|
|
68
|
+
const heights = new Array<number>(limitedLefts.length).fill(0)
|
|
69
|
+
for (const it of items) {
|
|
70
|
+
const col = leftIndexMap.get(it.left)
|
|
71
|
+
if (col != null) {
|
|
72
|
+
heights[col] = Math.max(heights[col], it.top + it.columnHeight)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Pad if some columns haven't been populated yet (e.g., initial render)
|
|
76
|
+
while (heights.length < columnCount) heights.push(0)
|
|
64
77
|
return heights
|
|
65
78
|
}
|
package/src/useMasonryScroll.ts
CHANGED
|
@@ -11,135 +11,47 @@ export function useMasonryScroll({
|
|
|
11
11
|
columns,
|
|
12
12
|
containerHeight,
|
|
13
13
|
isLoading,
|
|
14
|
-
maxItems,
|
|
15
14
|
pageSize,
|
|
16
15
|
refreshLayout,
|
|
17
16
|
setItemsRaw,
|
|
18
17
|
loadNext,
|
|
19
|
-
|
|
18
|
+
loadThresholdPx
|
|
20
19
|
}: {
|
|
21
20
|
container: Ref<HTMLElement | null>
|
|
22
21
|
masonry: Ref<ProcessedMasonryItem[]>
|
|
23
22
|
columns: Ref<number>
|
|
24
23
|
containerHeight: Ref<number>
|
|
25
24
|
isLoading: Ref<boolean>
|
|
26
|
-
maxItems: number
|
|
27
25
|
pageSize: number
|
|
28
26
|
refreshLayout: (items: ProcessedMasonryItem[]) => void
|
|
29
27
|
setItemsRaw: (items: ProcessedMasonryItem[]) => void
|
|
30
28
|
loadNext: () => Promise<any>
|
|
31
|
-
|
|
29
|
+
loadThresholdPx?: number
|
|
32
30
|
}) {
|
|
33
31
|
let cleanupInProgress = false
|
|
34
32
|
let lastScrollTop = 0
|
|
35
33
|
|
|
36
|
-
async function handleScroll() {
|
|
34
|
+
async function handleScroll(precomputedHeights?: number[]) {
|
|
37
35
|
if (!container.value) return
|
|
38
36
|
|
|
39
|
-
const
|
|
40
|
-
const
|
|
37
|
+
const columnHeights = precomputedHeights ?? calculateColumnHeights(masonry.value, columns.value)
|
|
38
|
+
const tallest = columnHeights.length ? Math.max(...columnHeights) : 0
|
|
39
|
+
const scrollerBottom = container.value.scrollTop + container.value.clientHeight
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
lastScrollTop = scrollTop
|
|
41
|
+
const isScrollingDown = container.value.scrollTop > lastScrollTop + 1 // tolerate tiny jitter
|
|
42
|
+
lastScrollTop = container.value.scrollTop
|
|
45
43
|
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
const threshold = typeof loadThresholdPx === 'number' ? loadThresholdPx : 200
|
|
45
|
+
const triggerPoint = threshold >= 0
|
|
46
|
+
? Math.max(0, tallest - threshold)
|
|
47
|
+
: Math.max(0, tallest + threshold)
|
|
48
|
+
const nearBottom = scrollerBottom >= triggerPoint
|
|
50
49
|
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
await loadNext()
|
|
58
|
-
await nextTick()
|
|
59
|
-
} catch (error) {
|
|
60
|
-
console.error('Error in scroll handler:', error)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async function handleItemCleanup(columnHeightsBefore: number[]) {
|
|
66
|
-
if (!masonry.value.length) return
|
|
67
|
-
if (masonry.value.length <= pageSize) return
|
|
68
|
-
|
|
69
|
-
const pageGroups: Record<string | number, ProcessedMasonryItem[]> = masonry.value.reduce((acc, item) => {
|
|
70
|
-
const key = (item as any).page
|
|
71
|
-
if (!acc[key]) acc[key] = []
|
|
72
|
-
acc[key].push(item)
|
|
73
|
-
return acc
|
|
74
|
-
}, {} as Record<string | number, ProcessedMasonryItem[]>)
|
|
75
|
-
|
|
76
|
-
const pages = Object.keys(pageGroups).sort((a, b) => parseInt(a) - parseInt(b))
|
|
77
|
-
if (pages.length === 0) return
|
|
78
|
-
|
|
79
|
-
let totalRemovedItems = 0
|
|
80
|
-
const pagesToRemove: string[] = []
|
|
81
|
-
|
|
82
|
-
for (const page of pages) {
|
|
83
|
-
pagesToRemove.push(page)
|
|
84
|
-
totalRemovedItems += pageGroups[page].length
|
|
85
|
-
if (totalRemovedItems >= pageSize) break
|
|
50
|
+
if (nearBottom && isScrollingDown && !isLoading.value) {
|
|
51
|
+
await loadNext()
|
|
52
|
+
await nextTick()
|
|
53
|
+
return
|
|
86
54
|
}
|
|
87
|
-
|
|
88
|
-
const remainingItems = masonry.value.filter(item => !pagesToRemove.includes(String((item as any).page)))
|
|
89
|
-
if (remainingItems.length === masonry.value.length) return
|
|
90
|
-
|
|
91
|
-
cleanupInProgress = true
|
|
92
|
-
|
|
93
|
-
setItemsRaw(remainingItems)
|
|
94
|
-
await nextTick()
|
|
95
|
-
// Allow leave to start, then FLIP survivors concurrently (single RAF)
|
|
96
|
-
await new Promise<void>(r => requestAnimationFrame(() => r()))
|
|
97
|
-
|
|
98
|
-
refreshLayout(remainingItems)
|
|
99
|
-
await nextTick()
|
|
100
|
-
|
|
101
|
-
await maintainAnchorPosition()
|
|
102
|
-
|
|
103
|
-
cleanupInProgress = false
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function msLeaveEstimate() {
|
|
107
|
-
const base = typeof leaveEstimateMs === 'number' && leaveEstimateMs > 0 ? leaveEstimateMs : 250
|
|
108
|
-
return base + 50
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function waitFor(ms: number) {
|
|
112
|
-
return new Promise(resolve => setTimeout(resolve, ms))
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async function maintainAnchorPosition() {
|
|
116
|
-
if (!container.value) return
|
|
117
|
-
|
|
118
|
-
const { scrollTop, clientHeight } = container.value
|
|
119
|
-
const pivotY = scrollTop + clientHeight * 0.4
|
|
120
|
-
|
|
121
|
-
const heights = calculateColumnHeights(masonry.value, columns.value)
|
|
122
|
-
const anchorColumnIndex = heights.indexOf(Math.max(...heights))
|
|
123
|
-
|
|
124
|
-
const itemsInAnchor = masonry.value.filter((_, index) => index % columns.value === anchorColumnIndex)
|
|
125
|
-
if (itemsInAnchor.length === 0) return
|
|
126
|
-
|
|
127
|
-
let pivotItem = itemsInAnchor[0]
|
|
128
|
-
for (const it of itemsInAnchor) {
|
|
129
|
-
if (it.top <= pivotY && it.top >= pivotItem.top) {
|
|
130
|
-
pivotItem = it
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const desiredTop = Math.max(0, pivotItem.top - clientHeight * 0.4)
|
|
135
|
-
|
|
136
|
-
if (Math.abs(desiredTop - scrollTop) > 4) {
|
|
137
|
-
container.value.scrollTo({ top: desiredTop, behavior: 'auto' })
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async function adjustScrollPosition() {
|
|
142
|
-
await maintainAnchorPosition()
|
|
143
55
|
}
|
|
144
56
|
|
|
145
57
|
return {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Composable for handling masonry item transitions (typed)
|
|
3
3
|
*/
|
|
4
|
-
export function useMasonryTransitions(masonry: any) {
|
|
4
|
+
export function useMasonryTransitions(masonry: any, opts?: { leaveDurationMs?: number }) {
|
|
5
5
|
function onEnter(el: HTMLElement, done: () => void) {
|
|
6
6
|
const left = parseInt(el.dataset.left || '0', 10)
|
|
7
7
|
const top = parseInt(el.dataset.top || '0', 10)
|
|
@@ -41,18 +41,25 @@ export function useMasonryTransitions(masonry: any) {
|
|
|
41
41
|
el.style.opacity = '1'
|
|
42
42
|
el.style.transform = `translate3d(${left}px, ${top}px, 0) scale(1)`
|
|
43
43
|
el.style.removeProperty('--masonry-opacity-delay')
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
// Avoid forced reflow: re-enable transition on the next frame
|
|
45
|
+
requestAnimationFrame(() => {
|
|
46
|
+
el.style.transition = ''
|
|
47
|
+
})
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
function onLeave(el: HTMLElement, done: () => void) {
|
|
49
51
|
const left = parseInt(el.dataset.left || '0', 10)
|
|
50
52
|
const top = parseInt(el.dataset.top || '0', 10)
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
// Prefer explicit option, fallback to CSS variable for safety
|
|
55
|
+
const fromOpts = typeof opts?.leaveDurationMs === 'number' ? opts!.leaveDurationMs : NaN
|
|
56
|
+
let leaveMs = Number.isFinite(fromOpts) && fromOpts > 0 ? fromOpts : NaN
|
|
57
|
+
if (!Number.isFinite(leaveMs)) {
|
|
58
|
+
const cs = getComputedStyle(el)
|
|
59
|
+
const varVal = cs.getPropertyValue('--masonry-leave-duration') || ''
|
|
60
|
+
const parsed = parseFloat(varVal)
|
|
61
|
+
leaveMs = Number.isFinite(parsed) && parsed > 0 ? parsed : 200
|
|
62
|
+
}
|
|
56
63
|
|
|
57
64
|
const prevDuration = el.style.transitionDuration
|
|
58
65
|
|