@wyxos/vibe 1.2.14 → 1.2.16
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/package.json +1 -1
- package/src/App.vue +2 -1
- package/src/Masonry.vue +75 -166
- package/src/masonryUtils.js +59 -0
- package/src/useMasonryScroll.js +86 -0
- package/src/useMasonryTransitions.js +45 -0
package/package.json
CHANGED
package/src/App.vue
CHANGED
|
@@ -43,8 +43,9 @@ const getPage = async (page) => {
|
|
|
43
43
|
💾 <a href="https://github.com/wyxos/vibe" target="_blank" class="underline hover:text-black">Source on GitHub</a>
|
|
44
44
|
</p>
|
|
45
45
|
|
|
46
|
-
<div v-if="masonry">
|
|
46
|
+
<div v-if="masonry" class="flex gap-4">
|
|
47
47
|
<p>Loading: <span class="bg-blue-500 text-white p-2 rounded">{{ masonry.isLoading }}</span></p>
|
|
48
|
+
<p>Showing: <span class="bg-blue-500 text-white p-2 rounded">{{ items.length }}</span></p>
|
|
48
49
|
</div>
|
|
49
50
|
</header>
|
|
50
51
|
<masonry class="bg-blue-500 " v-model:items="items" :get-next-page="getPage" :load-at-page="1" ref="masonry">
|
package/src/Masonry.vue
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
import {computed, nextTick, onMounted, onUnmounted, ref} from "vue";
|
|
3
3
|
import calculateLayout from "./calculateLayout.js";
|
|
4
4
|
import { debounce } from 'lodash-es'
|
|
5
|
+
import {
|
|
6
|
+
getColumnCount,
|
|
7
|
+
calculateContainerHeight,
|
|
8
|
+
getItemAttributes,
|
|
9
|
+
calculateColumnHeights
|
|
10
|
+
} from './masonryUtils.js'
|
|
11
|
+
import { useMasonryTransitions } from './useMasonryTransitions.js'
|
|
12
|
+
import { useMasonryScroll } from './useMasonryScroll.js'
|
|
5
13
|
|
|
6
14
|
const props = defineProps({
|
|
7
15
|
getNextPage: {
|
|
@@ -74,13 +82,21 @@ const isLoading = ref(false)
|
|
|
74
82
|
const containerHeight = ref(0)
|
|
75
83
|
|
|
76
84
|
const columnHeights = computed(() => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
return calculateColumnHeights(masonry.value, columns.value)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Setup composables
|
|
89
|
+
const { onEnter, onBeforeEnter, onBeforeLeave, onLeave } = useMasonryTransitions(masonry)
|
|
90
|
+
|
|
91
|
+
const { handleScroll } = useMasonryScroll({
|
|
92
|
+
container,
|
|
93
|
+
masonry,
|
|
94
|
+
columns,
|
|
95
|
+
containerHeight,
|
|
96
|
+
isLoading,
|
|
97
|
+
maxItems: props.maxItems,
|
|
98
|
+
refreshLayout,
|
|
99
|
+
loadNext
|
|
84
100
|
})
|
|
85
101
|
|
|
86
102
|
defineExpose({
|
|
@@ -88,97 +104,12 @@ defineExpose({
|
|
|
88
104
|
refreshLayout,
|
|
89
105
|
containerHeight,
|
|
90
106
|
onRemove,
|
|
91
|
-
loadNext
|
|
107
|
+
loadNext,
|
|
108
|
+
loadPage
|
|
92
109
|
})
|
|
93
110
|
|
|
94
|
-
async function onScroll() {
|
|
95
|
-
const {scrollTop, clientHeight} = container.value
|
|
96
|
-
const visibleBottom = scrollTop + clientHeight
|
|
97
|
-
|
|
98
|
-
const whitespaceVisible = columnHeights.value.some(height => height + 300 < visibleBottom - 1)
|
|
99
|
-
|
|
100
|
-
const reachedContainerBottom = scrollTop + clientHeight >= containerHeight.value - 1
|
|
101
|
-
|
|
102
|
-
if ((whitespaceVisible || reachedContainerBottom) && !isLoading.value) {
|
|
103
|
-
isLoading.value = true
|
|
104
|
-
|
|
105
|
-
if (masonry.value.length > props.maxItems) {
|
|
106
|
-
// get first item - only proceed if it exists
|
|
107
|
-
const firstItem = masonry.value[0]
|
|
108
|
-
|
|
109
|
-
if (!firstItem) {
|
|
110
|
-
// Skip removal logic if there are no items
|
|
111
|
-
await loadNext()
|
|
112
|
-
await nextTick()
|
|
113
|
-
isLoading.value = false
|
|
114
|
-
return
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// get page number
|
|
118
|
-
const page = firstItem.page
|
|
119
|
-
|
|
120
|
-
// find all item with this page
|
|
121
|
-
const removedItems = masonry.value.filter(i => i.page !== page)
|
|
122
|
-
|
|
123
|
-
// Only proceed with removal if there are actually items to remove
|
|
124
|
-
if (removedItems.length === masonry.value.length) {
|
|
125
|
-
// All items belong to the same page, skip removal logic
|
|
126
|
-
await loadNext()
|
|
127
|
-
await nextTick()
|
|
128
|
-
isLoading.value = false
|
|
129
|
-
return
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
refreshLayout(removedItems)
|
|
133
|
-
|
|
134
|
-
await nextTick()
|
|
135
|
-
|
|
136
|
-
const lowestColumnIndex = columnHeights.value.indexOf(Math.min(...columnHeights.value))
|
|
137
|
-
|
|
138
|
-
// find the last item in that column
|
|
139
|
-
const lastItemInColumn = masonry.value.filter((_, index) => index % columns.value === lowestColumnIndex).pop()
|
|
140
|
-
|
|
141
|
-
// Only proceed with scroll adjustment if we have a valid item
|
|
142
|
-
if (lastItemInColumn) {
|
|
143
|
-
const lastItemInColumnTop = lastItemInColumn.top + lastItemInColumn.columnHeight
|
|
144
|
-
const lastItemInColumnBottom = lastItemInColumnTop + lastItemInColumn.columnHeight
|
|
145
|
-
const containerTop = container.value.scrollTop
|
|
146
|
-
const containerBottom = containerTop + container.value.clientHeight
|
|
147
|
-
const itemInView = lastItemInColumnTop >= containerTop && lastItemInColumnBottom <= containerBottom
|
|
148
|
-
if (!itemInView) {
|
|
149
|
-
container.value.scrollTo({
|
|
150
|
-
top: lastItemInColumnTop - 10,
|
|
151
|
-
behavior: 'smooth'
|
|
152
|
-
})
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
await loadNext()
|
|
158
|
-
|
|
159
|
-
await nextTick()
|
|
160
|
-
|
|
161
|
-
isLoading.value = false
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function getColumnCount() {
|
|
166
|
-
const width = window.innerWidth
|
|
167
|
-
|
|
168
|
-
const sizes = layout.value.sizes
|
|
169
|
-
|
|
170
|
-
if (width >= 1536 && sizes['2xl']) return sizes['2xl']
|
|
171
|
-
if (width >= 1280 && sizes.xl) return sizes.xl
|
|
172
|
-
if (width >= 1024 && sizes.lg) return sizes.lg
|
|
173
|
-
if (width >= 768 && sizes.md) return sizes.md
|
|
174
|
-
if (width >= 640 && sizes.sm) return sizes.sm
|
|
175
|
-
return sizes.base
|
|
176
|
-
}
|
|
177
|
-
|
|
178
111
|
function calculateHeight(content) {
|
|
179
|
-
containerHeight.value = content
|
|
180
|
-
return Math.max(acc, item.top + item.columnHeight)
|
|
181
|
-
}, 0)
|
|
112
|
+
containerHeight.value = calculateContainerHeight(content)
|
|
182
113
|
}
|
|
183
114
|
|
|
184
115
|
function refreshLayout(items) {
|
|
@@ -197,17 +128,38 @@ async function getContent(page) {
|
|
|
197
128
|
return response
|
|
198
129
|
}
|
|
199
130
|
|
|
200
|
-
async function
|
|
201
|
-
|
|
202
|
-
|
|
131
|
+
async function loadPage(page) {
|
|
132
|
+
if (isLoading.value) return // Prevent concurrent loading
|
|
133
|
+
|
|
134
|
+
isLoading.value = true
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const response = await getContent(page)
|
|
138
|
+
paginationHistory.value.push(response.nextPage)
|
|
139
|
+
return response
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error('Error loading page:', error)
|
|
142
|
+
throw error
|
|
143
|
+
} finally {
|
|
144
|
+
isLoading.value = false
|
|
145
|
+
}
|
|
203
146
|
}
|
|
204
147
|
|
|
205
|
-
|
|
206
|
-
return
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
148
|
+
async function loadNext() {
|
|
149
|
+
if (isLoading.value) return // Prevent concurrent loading
|
|
150
|
+
|
|
151
|
+
isLoading.value = true
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const currentPage = paginationHistory.value[paginationHistory.value.length - 1]
|
|
155
|
+
const response = await getContent(currentPage)
|
|
156
|
+
paginationHistory.value.push(response.nextPage)
|
|
157
|
+
return response
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.error('Error loading next page:', error)
|
|
160
|
+
throw error
|
|
161
|
+
} finally {
|
|
162
|
+
isLoading.value = false
|
|
211
163
|
}
|
|
212
164
|
}
|
|
213
165
|
|
|
@@ -215,81 +167,38 @@ function onRemove(item) {
|
|
|
215
167
|
refreshLayout(masonry.value.filter(i => i.id !== item.id))
|
|
216
168
|
}
|
|
217
169
|
|
|
218
|
-
function onEnter(el, done) {
|
|
219
|
-
// set top to data-top
|
|
220
|
-
const top = el.dataset.top
|
|
221
|
-
requestAnimationFrame(() => {
|
|
222
|
-
el.style.top = `${top}px`
|
|
223
|
-
done()
|
|
224
|
-
})
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function onBeforeEnter(el) {
|
|
228
|
-
// set top to last item + 500
|
|
229
|
-
const lastItem = masonry.value[masonry.value.length - 1]
|
|
230
|
-
if (lastItem) {
|
|
231
|
-
const lastTop = lastItem.top + lastItem.columnHeight + 10
|
|
232
|
-
el.style.top = `${lastTop}px`
|
|
233
|
-
} else {
|
|
234
|
-
el.style.top = '0px'
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function onBeforeLeave(el) {
|
|
239
|
-
// Ensure it's at its current position before animating
|
|
240
|
-
el.style.transition = 'none'
|
|
241
|
-
el.style.top = `${el.offsetTop}px`
|
|
242
|
-
void el.offsetWidth // force reflow to flush style
|
|
243
|
-
el.style.transition = '' // allow transition to apply again
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function onLeave(el, done) {
|
|
247
|
-
el.style.top = '-600px'
|
|
248
|
-
el.style.opacity = '0'
|
|
249
|
-
el.addEventListener('transitionend', done)
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function itemAttributes(item) {
|
|
253
|
-
return {
|
|
254
|
-
style: getItemStyle(item),
|
|
255
|
-
'data-top': item.top,
|
|
256
|
-
'data-id': `${item.page}-${item.id}`,
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
170
|
function onResize() {
|
|
261
|
-
columns.value = getColumnCount()
|
|
171
|
+
columns.value = getColumnCount(layout.value)
|
|
262
172
|
refreshLayout(masonry.value)
|
|
263
173
|
}
|
|
264
174
|
|
|
265
175
|
onMounted(async () => {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
columns.value = getColumnCount()
|
|
176
|
+
try {
|
|
177
|
+
columns.value = getColumnCount(layout.value)
|
|
269
178
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
179
|
+
// For cursor-based pagination, loadAtPage can be null for the first request
|
|
180
|
+
const initialPage = props.loadAtPage
|
|
181
|
+
paginationHistory.value = [initialPage]
|
|
273
182
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
183
|
+
// Skip initial load if skipInitialLoad prop is true
|
|
184
|
+
if (!props.skipInitialLoad) {
|
|
185
|
+
await loadPage(paginationHistory.value[0]) // loadPage manages its own loading state
|
|
186
|
+
} else {
|
|
187
|
+
await nextTick()
|
|
188
|
+
// Just refresh the layout with any existing items
|
|
189
|
+
refreshLayout(masonry.value)
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error('Error during component initialization:', error)
|
|
282
193
|
}
|
|
283
194
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
container.value?.addEventListener('scroll', debounce(onScroll, 200));
|
|
195
|
+
container.value?.addEventListener('scroll', debounce(handleScroll, 200));
|
|
287
196
|
|
|
288
197
|
window.addEventListener('resize', debounce(onResize, 200));
|
|
289
198
|
})
|
|
290
199
|
|
|
291
200
|
onUnmounted(() => {
|
|
292
|
-
container.value?.removeEventListener('scroll', debounce(
|
|
201
|
+
container.value?.removeEventListener('scroll', debounce(handleScroll, 200));
|
|
293
202
|
|
|
294
203
|
window.removeEventListener('resize', debounce(onResize, 200));
|
|
295
204
|
})
|
|
@@ -303,7 +212,7 @@ onUnmounted(() => {
|
|
|
303
212
|
@before-leave="onBeforeLeave">
|
|
304
213
|
<div v-for="item in masonry" :key="`${item.page}-${item.id}`"
|
|
305
214
|
class="absolute transition-[top,left,opacity] duration-500 ease-in-out"
|
|
306
|
-
v-bind="
|
|
215
|
+
v-bind="getItemAttributes(item)">
|
|
307
216
|
<slot name="item" v-bind="{item, onRemove}">
|
|
308
217
|
<img :src="item.src" class="w-full"/>
|
|
309
218
|
<button class="absolute bottom-0 right-0 bg-red-500 text-white p-2 rounded cursor-pointer"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get responsive column count based on window width and layout sizes
|
|
3
|
+
*/
|
|
4
|
+
export function getColumnCount(layout) {
|
|
5
|
+
const width = window.innerWidth
|
|
6
|
+
const sizes = layout.sizes
|
|
7
|
+
|
|
8
|
+
if (width >= 1536 && sizes['2xl']) return sizes['2xl']
|
|
9
|
+
if (width >= 1280 && sizes.xl) return sizes.xl
|
|
10
|
+
if (width >= 1024 && sizes.lg) return sizes.lg
|
|
11
|
+
if (width >= 768 && sizes.md) return sizes.md
|
|
12
|
+
if (width >= 640 && sizes.sm) return sizes.sm
|
|
13
|
+
return sizes.base
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Calculate container height based on item positions
|
|
18
|
+
*/
|
|
19
|
+
export function calculateContainerHeight(items) {
|
|
20
|
+
return items.reduce((acc, item) => {
|
|
21
|
+
return Math.max(acc, item.top + item.columnHeight)
|
|
22
|
+
}, 0)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get style object for masonry item positioning
|
|
27
|
+
*/
|
|
28
|
+
export function getItemStyle(item) {
|
|
29
|
+
return {
|
|
30
|
+
top: `${item.top}px`,
|
|
31
|
+
left: `${item.left}px`,
|
|
32
|
+
width: `${item.columnWidth}px`,
|
|
33
|
+
height: `${item.columnHeight}px`
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get item attributes for rendering
|
|
39
|
+
*/
|
|
40
|
+
export function getItemAttributes(item) {
|
|
41
|
+
return {
|
|
42
|
+
style: getItemStyle(item),
|
|
43
|
+
'data-top': item.top,
|
|
44
|
+
'data-id': `${item.page}-${item.id}`,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Calculate column heights for masonry layout
|
|
50
|
+
*/
|
|
51
|
+
export function calculateColumnHeights(items, columnCount) {
|
|
52
|
+
const heights = new Array(columnCount).fill(0)
|
|
53
|
+
for (let i = 0; i < items.length; i++) {
|
|
54
|
+
const item = items[i]
|
|
55
|
+
const col = i % columnCount
|
|
56
|
+
heights[col] = Math.max(heights[col], item.top + item.columnHeight)
|
|
57
|
+
}
|
|
58
|
+
return heights
|
|
59
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { nextTick } from 'vue'
|
|
2
|
+
import { calculateColumnHeights } from './masonryUtils.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Composable for handling masonry scroll behavior and item cleanup
|
|
6
|
+
*/
|
|
7
|
+
export function useMasonryScroll({
|
|
8
|
+
container,
|
|
9
|
+
masonry,
|
|
10
|
+
columns,
|
|
11
|
+
containerHeight,
|
|
12
|
+
isLoading,
|
|
13
|
+
maxItems,
|
|
14
|
+
refreshLayout,
|
|
15
|
+
loadNext
|
|
16
|
+
}) {
|
|
17
|
+
|
|
18
|
+
async function handleScroll() {
|
|
19
|
+
const { scrollTop, clientHeight } = container.value
|
|
20
|
+
const visibleBottom = scrollTop + clientHeight
|
|
21
|
+
|
|
22
|
+
const columnHeights = calculateColumnHeights(masonry.value, columns.value)
|
|
23
|
+
const whitespaceVisible = columnHeights.some(height => height + 300 < visibleBottom - 1)
|
|
24
|
+
const reachedContainerBottom = scrollTop + clientHeight >= containerHeight.value - 1
|
|
25
|
+
|
|
26
|
+
if ((whitespaceVisible || reachedContainerBottom) && !isLoading.value) {
|
|
27
|
+
try {
|
|
28
|
+
// Handle cleanup when too many items
|
|
29
|
+
if (masonry.value.length > maxItems) {
|
|
30
|
+
await handleItemCleanup(columnHeights)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await loadNext() // loadNext manages its own loading state
|
|
34
|
+
await nextTick()
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('Error in scroll handler:', error)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function handleItemCleanup(columnHeights) {
|
|
42
|
+
const firstItem = masonry.value[0]
|
|
43
|
+
|
|
44
|
+
if (!firstItem) {
|
|
45
|
+
// Don't set isLoading here - let main handleScroll manage it
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const page = firstItem.page
|
|
50
|
+
const removedItems = masonry.value.filter(i => i.page !== page)
|
|
51
|
+
|
|
52
|
+
if (removedItems.length === masonry.value.length) {
|
|
53
|
+
// Don't set isLoading here - let main handleScroll manage it
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
refreshLayout(removedItems)
|
|
58
|
+
await nextTick()
|
|
59
|
+
|
|
60
|
+
await adjustScrollPosition(columnHeights)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function adjustScrollPosition(columnHeights) {
|
|
64
|
+
const lowestColumnIndex = columnHeights.indexOf(Math.min(...columnHeights))
|
|
65
|
+
const lastItemInColumn = masonry.value.filter((_, index) => index % columns.value === lowestColumnIndex).pop()
|
|
66
|
+
|
|
67
|
+
if (lastItemInColumn) {
|
|
68
|
+
const lastItemInColumnTop = lastItemInColumn.top + lastItemInColumn.columnHeight
|
|
69
|
+
const lastItemInColumnBottom = lastItemInColumnTop + lastItemInColumn.columnHeight
|
|
70
|
+
const containerTop = container.value.scrollTop
|
|
71
|
+
const containerBottom = containerTop + container.value.clientHeight
|
|
72
|
+
const itemInView = lastItemInColumnTop >= containerTop && lastItemInColumnBottom <= containerBottom
|
|
73
|
+
|
|
74
|
+
if (!itemInView) {
|
|
75
|
+
container.value.scrollTo({
|
|
76
|
+
top: lastItemInColumnTop - 10,
|
|
77
|
+
behavior: 'smooth'
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
handleScroll
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable for handling masonry item transitions
|
|
3
|
+
*/
|
|
4
|
+
export function useMasonryTransitions(masonry) {
|
|
5
|
+
function onEnter(el, done) {
|
|
6
|
+
// Set top to data-top
|
|
7
|
+
const top = el.dataset.top
|
|
8
|
+
requestAnimationFrame(() => {
|
|
9
|
+
el.style.top = `${top}px`
|
|
10
|
+
done()
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function onBeforeEnter(el) {
|
|
15
|
+
// Set top to last item + offset
|
|
16
|
+
const lastItem = masonry.value[masonry.value.length - 1]
|
|
17
|
+
if (lastItem) {
|
|
18
|
+
const lastTop = lastItem.top + lastItem.columnHeight + 10
|
|
19
|
+
el.style.top = `${lastTop}px`
|
|
20
|
+
} else {
|
|
21
|
+
el.style.top = '0px'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function onBeforeLeave(el) {
|
|
26
|
+
// Ensure it's at its current position before animating
|
|
27
|
+
el.style.transition = 'none'
|
|
28
|
+
el.style.top = `${el.offsetTop}px`
|
|
29
|
+
void el.offsetWidth // force reflow to flush style
|
|
30
|
+
el.style.transition = '' // allow transition to apply again
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function onLeave(el, done) {
|
|
34
|
+
el.style.top = '-600px'
|
|
35
|
+
el.style.opacity = '0'
|
|
36
|
+
el.addEventListener('transitionend', done)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
onEnter,
|
|
41
|
+
onBeforeEnter,
|
|
42
|
+
onBeforeLeave,
|
|
43
|
+
onLeave
|
|
44
|
+
}
|
|
45
|
+
}
|