@wyxos/vibe 1.6.29 → 2.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.
- package/README.md +29 -287
- package/lib/index.cjs +1 -0
- package/lib/index.js +795 -1791
- package/lib/logo-dark.svg +36 -36
- package/lib/logo-light.svg +29 -29
- package/lib/logo.svg +32 -32
- package/lib/manifest.json +1 -1
- package/package.json +82 -96
- package/LICENSE +0 -21
- package/lib/vibe.css +0 -1
- package/lib/vite.svg +0 -1
- package/src/App.vue +0 -35
- package/src/Masonry.vue +0 -1030
- package/src/archive/App.vue +0 -96
- package/src/archive/InfiniteMansonry.spec.ts +0 -10
- package/src/archive/InfiniteMasonry.vue +0 -218
- package/src/assets/vue.svg +0 -1
- package/src/calculateLayout.ts +0 -194
- package/src/components/CodeTabs.vue +0 -158
- package/src/components/MasonryItem.vue +0 -499
- package/src/components/examples/BasicExample.vue +0 -46
- package/src/components/examples/CustomItemExample.vue +0 -87
- package/src/components/examples/HeaderFooterExample.vue +0 -79
- package/src/components/examples/ManualInitExample.vue +0 -78
- package/src/components/examples/SwipeModeExample.vue +0 -40
- package/src/createMasonryTransitions.ts +0 -176
- package/src/main.ts +0 -6
- package/src/masonryUtils.ts +0 -96
- package/src/pages.json +0 -36402
- package/src/router/index.ts +0 -20
- package/src/style.css +0 -32
- package/src/types.ts +0 -101
- package/src/useMasonryDimensions.ts +0 -59
- package/src/useMasonryItems.ts +0 -231
- package/src/useMasonryLayout.ts +0 -164
- package/src/useMasonryPagination.ts +0 -539
- package/src/useMasonryScroll.ts +0 -61
- package/src/useMasonryVirtualization.ts +0 -140
- package/src/useSwipeMode.ts +0 -233
- package/src/utils/errorHandler.ts +0 -8
- package/src/views/Examples.vue +0 -323
- package/src/views/Home.vue +0 -321
- package/toggle-link.mjs +0 -92
|
@@ -1,539 +0,0 @@
|
|
|
1
|
-
import { ref, nextTick, type Ref } from 'vue'
|
|
2
|
-
import { normalizeError } from './utils/errorHandler'
|
|
3
|
-
|
|
4
|
-
export interface UseMasonryPaginationOptions {
|
|
5
|
-
getPage: (page: any, context?: any) => Promise<{ items: any[]; nextPage: any }>
|
|
6
|
-
context?: Ref<any>
|
|
7
|
-
masonry: Ref<any[]>
|
|
8
|
-
isLoading: Ref<boolean>
|
|
9
|
-
hasReachedEnd: Ref<boolean>
|
|
10
|
-
loadError: Ref<Error | null>
|
|
11
|
-
currentPage: Ref<any>
|
|
12
|
-
paginationHistory: Ref<any[]>
|
|
13
|
-
refreshLayout: (items: any[]) => void
|
|
14
|
-
retryMaxAttempts: number
|
|
15
|
-
retryInitialDelayMs: number
|
|
16
|
-
retryBackoffStepMs: number
|
|
17
|
-
mode: string | Ref<string>
|
|
18
|
-
backfillDelayMs: number
|
|
19
|
-
backfillMaxCalls: number
|
|
20
|
-
pageSize: number
|
|
21
|
-
emits: {
|
|
22
|
-
(event: 'loading:start'): void
|
|
23
|
-
(event: 'retry:start', payload: { attempt: number; max: number; totalMs: number }): void
|
|
24
|
-
(event: 'retry:tick', payload: { attempt: number; remainingMs: number; totalMs: number }): void
|
|
25
|
-
(event: 'retry:stop', payload: { attempt: number; success: boolean }): void
|
|
26
|
-
(event: 'backfill:start', payload: { target: number; fetched: number; calls: number; currentPage: any; nextPage: any }): void
|
|
27
|
-
(event: 'backfill:tick', payload: { fetched: number; target: number; calls: number; remainingMs: number; totalMs: number; currentPage: any; nextPage: any }): void
|
|
28
|
-
(event: 'backfill:stop', payload: { fetched: number; calls: number; cancelled?: boolean; currentPage: any; nextPage: any }): void
|
|
29
|
-
(event: 'loading:stop', payload: { fetched: number; currentPage: any; nextPage: any }): void
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function useMasonryPagination(options: UseMasonryPaginationOptions) {
|
|
34
|
-
const {
|
|
35
|
-
getPage,
|
|
36
|
-
context,
|
|
37
|
-
masonry,
|
|
38
|
-
isLoading,
|
|
39
|
-
hasReachedEnd,
|
|
40
|
-
loadError,
|
|
41
|
-
currentPage,
|
|
42
|
-
paginationHistory,
|
|
43
|
-
refreshLayout,
|
|
44
|
-
retryMaxAttempts,
|
|
45
|
-
retryInitialDelayMs,
|
|
46
|
-
retryBackoffStepMs,
|
|
47
|
-
mode,
|
|
48
|
-
backfillDelayMs,
|
|
49
|
-
backfillMaxCalls,
|
|
50
|
-
pageSize,
|
|
51
|
-
emits
|
|
52
|
-
} = options
|
|
53
|
-
|
|
54
|
-
// Make mode reactive so it updates when the prop changes
|
|
55
|
-
const modeRef = typeof mode === 'string' ? ref(mode) : (mode as Ref<string>)
|
|
56
|
-
|
|
57
|
-
const cancelRequested = ref(false)
|
|
58
|
-
let backfillActive = false
|
|
59
|
-
|
|
60
|
-
// Helper function to count items for a specific page
|
|
61
|
-
function countItemsForPage(page: any): number {
|
|
62
|
-
return masonry.value.filter((item: any) => item.page === page).length
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Helper function to check if an item already exists in masonry
|
|
66
|
-
function itemExists(item: any, itemsArray?: any[]): boolean {
|
|
67
|
-
if (!item || item.id == null || item.page == null) return false
|
|
68
|
-
const itemsToCheck = itemsArray || masonry.value
|
|
69
|
-
return itemsToCheck.some((existing: any) => {
|
|
70
|
-
return existing && existing.id === item.id && existing.page === item.page
|
|
71
|
-
})
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Helper function to get only new items from a response
|
|
75
|
-
function getNewItems(responseItems: any[]): any[] {
|
|
76
|
-
if (!responseItems || responseItems.length === 0) return []
|
|
77
|
-
// Create a snapshot of current masonry items to avoid reactivity issues
|
|
78
|
-
const currentItems = [...masonry.value]
|
|
79
|
-
return responseItems.filter((item: any) => {
|
|
80
|
-
if (!item || item.id == null || item.page == null) return false
|
|
81
|
-
// Check if item exists by comparing id and page
|
|
82
|
-
const exists = currentItems.some((existing: any) => {
|
|
83
|
-
return existing && existing.id === item.id && existing.page === item.page
|
|
84
|
-
})
|
|
85
|
-
return !exists
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function waitWithProgress(totalMs: number, onTick: (remaining: number, total: number) => void) {
|
|
90
|
-
return new Promise<void>((resolve) => {
|
|
91
|
-
const total = Math.max(0, totalMs | 0)
|
|
92
|
-
const start = Date.now()
|
|
93
|
-
onTick(total, total)
|
|
94
|
-
const id = setInterval(() => {
|
|
95
|
-
// Check for cancellation
|
|
96
|
-
if (cancelRequested.value) {
|
|
97
|
-
clearInterval(id)
|
|
98
|
-
resolve()
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
const elapsed = Date.now() - start
|
|
102
|
-
const remaining = Math.max(0, total - elapsed)
|
|
103
|
-
onTick(remaining, total)
|
|
104
|
-
if (remaining <= 0) {
|
|
105
|
-
clearInterval(id)
|
|
106
|
-
resolve()
|
|
107
|
-
}
|
|
108
|
-
}, 100)
|
|
109
|
-
})
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function fetchWithRetry<T = any>(fn: () => Promise<T>): Promise<T> {
|
|
113
|
-
let attempt = 0
|
|
114
|
-
const max = retryMaxAttempts
|
|
115
|
-
let delay = retryInitialDelayMs
|
|
116
|
-
// eslint-disable-next-line no-constant-condition
|
|
117
|
-
while (true) {
|
|
118
|
-
try {
|
|
119
|
-
const res = await fn()
|
|
120
|
-
if (attempt > 0) {
|
|
121
|
-
emits('retry:stop', { attempt, success: true })
|
|
122
|
-
}
|
|
123
|
-
return res
|
|
124
|
-
} catch (err) {
|
|
125
|
-
attempt++
|
|
126
|
-
if (attempt > max) {
|
|
127
|
-
emits('retry:stop', { attempt: attempt - 1, success: false })
|
|
128
|
-
throw err
|
|
129
|
-
}
|
|
130
|
-
emits('retry:start', { attempt, max, totalMs: delay })
|
|
131
|
-
await waitWithProgress(delay, (remaining, total) => {
|
|
132
|
-
emits('retry:tick', { attempt, remainingMs: remaining, totalMs: total })
|
|
133
|
-
})
|
|
134
|
-
delay += retryBackoffStepMs
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async function getContent(page: number) {
|
|
140
|
-
try {
|
|
141
|
-
const pageData = await fetchWithRetry(() => getPage(page, context?.value))
|
|
142
|
-
// Add items to masonry array first (allows Vue transition-group to detect new items)
|
|
143
|
-
const newItems = [...masonry.value, ...pageData.items]
|
|
144
|
-
masonry.value = newItems
|
|
145
|
-
await nextTick()
|
|
146
|
-
|
|
147
|
-
// Commit DOM updates without forcing sync reflow
|
|
148
|
-
await nextTick()
|
|
149
|
-
// Start FLIP on next tick (same pattern as restore/restoreMany)
|
|
150
|
-
await nextTick()
|
|
151
|
-
refreshLayout(newItems)
|
|
152
|
-
return pageData
|
|
153
|
-
} catch (error) {
|
|
154
|
-
// Error is handled by callers (loadPage, loadNext, etc.) which set loadError
|
|
155
|
-
throw error
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function maybeBackfillToTarget(baselineCount: number, force = false) {
|
|
160
|
-
if (!force && modeRef.value !== 'backfill') return
|
|
161
|
-
if (backfillActive) return
|
|
162
|
-
if (cancelRequested.value) return
|
|
163
|
-
// Don't backfill if we've reached the end
|
|
164
|
-
if (hasReachedEnd.value) return
|
|
165
|
-
|
|
166
|
-
const targetCount = (baselineCount || 0) + (pageSize || 0)
|
|
167
|
-
if (!pageSize || pageSize <= 0) return
|
|
168
|
-
|
|
169
|
-
const lastNext = paginationHistory.value[paginationHistory.value.length - 1]
|
|
170
|
-
if (lastNext == null) {
|
|
171
|
-
hasReachedEnd.value = true
|
|
172
|
-
return
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (masonry.value.length >= targetCount) return
|
|
176
|
-
|
|
177
|
-
backfillActive = true
|
|
178
|
-
// Set loading to true at the start of backfill and keep it true throughout
|
|
179
|
-
if (!isLoading.value) {
|
|
180
|
-
isLoading.value = true
|
|
181
|
-
emits('loading:start')
|
|
182
|
-
}
|
|
183
|
-
try {
|
|
184
|
-
let calls = 0
|
|
185
|
-
const initialCurrentPage = currentPage.value
|
|
186
|
-
const initialNextPage = paginationHistory.value[paginationHistory.value.length - 1]
|
|
187
|
-
emits('backfill:start', {
|
|
188
|
-
target: targetCount,
|
|
189
|
-
fetched: masonry.value.length,
|
|
190
|
-
calls,
|
|
191
|
-
currentPage: initialCurrentPage,
|
|
192
|
-
nextPage: initialNextPage
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
while (
|
|
196
|
-
masonry.value.length < targetCount &&
|
|
197
|
-
calls < backfillMaxCalls &&
|
|
198
|
-
paginationHistory.value[paginationHistory.value.length - 1] != null &&
|
|
199
|
-
!cancelRequested.value &&
|
|
200
|
-
!hasReachedEnd.value &&
|
|
201
|
-
backfillActive
|
|
202
|
-
) {
|
|
203
|
-
const tickCurrentPage = currentPage.value
|
|
204
|
-
const tickNextPage = paginationHistory.value[paginationHistory.value.length - 1]
|
|
205
|
-
await waitWithProgress(backfillDelayMs, (remaining, total) => {
|
|
206
|
-
emits('backfill:tick', {
|
|
207
|
-
fetched: masonry.value.length,
|
|
208
|
-
target: targetCount,
|
|
209
|
-
calls,
|
|
210
|
-
remainingMs: remaining,
|
|
211
|
-
totalMs: total,
|
|
212
|
-
currentPage: tickCurrentPage,
|
|
213
|
-
nextPage: tickNextPage
|
|
214
|
-
})
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
if (cancelRequested.value || !backfillActive) break
|
|
218
|
-
|
|
219
|
-
const currentPageToLoad = paginationHistory.value[paginationHistory.value.length - 1]
|
|
220
|
-
if (currentPageToLoad == null) {
|
|
221
|
-
hasReachedEnd.value = true
|
|
222
|
-
break
|
|
223
|
-
}
|
|
224
|
-
try {
|
|
225
|
-
// Don't toggle isLoading here - keep it true throughout backfill
|
|
226
|
-
// Check cancellation before starting getContent to avoid unnecessary requests
|
|
227
|
-
if (cancelRequested.value || !backfillActive) break
|
|
228
|
-
const pageData = await getContent(currentPageToLoad)
|
|
229
|
-
if (cancelRequested.value || !backfillActive) break
|
|
230
|
-
// Clear error on successful load
|
|
231
|
-
loadError.value = null
|
|
232
|
-
currentPage.value = currentPageToLoad
|
|
233
|
-
paginationHistory.value.push(pageData.nextPage)
|
|
234
|
-
// Update hasReachedEnd if nextPage is null
|
|
235
|
-
if (pageData.nextPage == null) {
|
|
236
|
-
hasReachedEnd.value = true
|
|
237
|
-
}
|
|
238
|
-
} catch (error) {
|
|
239
|
-
// Set load error but don't break the backfill loop unless cancelled
|
|
240
|
-
if (cancelRequested.value || !backfillActive) break
|
|
241
|
-
loadError.value = normalizeError(error)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
calls++
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const stopCurrentPage = currentPage.value
|
|
248
|
-
const stopNextPage = paginationHistory.value[paginationHistory.value.length - 1]
|
|
249
|
-
emits('backfill:stop', {
|
|
250
|
-
fetched: masonry.value.length,
|
|
251
|
-
calls,
|
|
252
|
-
currentPage: stopCurrentPage,
|
|
253
|
-
nextPage: stopNextPage
|
|
254
|
-
})
|
|
255
|
-
} finally {
|
|
256
|
-
backfillActive = false
|
|
257
|
-
// Only set loading to false when backfill completes or is cancelled
|
|
258
|
-
isLoading.value = false
|
|
259
|
-
const finalCurrentPage = currentPage.value
|
|
260
|
-
const finalNextPage = paginationHistory.value[paginationHistory.value.length - 1]
|
|
261
|
-
emits('loading:stop', {
|
|
262
|
-
fetched: masonry.value.length,
|
|
263
|
-
currentPage: finalCurrentPage,
|
|
264
|
-
nextPage: finalNextPage
|
|
265
|
-
})
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
async function loadPage(page: number) {
|
|
270
|
-
if (isLoading.value) return
|
|
271
|
-
// Starting a new load should clear any previous cancel request
|
|
272
|
-
cancelRequested.value = false
|
|
273
|
-
if (!isLoading.value) {
|
|
274
|
-
isLoading.value = true
|
|
275
|
-
emits('loading:start')
|
|
276
|
-
}
|
|
277
|
-
// Reset hasReachedEnd and loadError when loading a new page
|
|
278
|
-
hasReachedEnd.value = false
|
|
279
|
-
loadError.value = null
|
|
280
|
-
try {
|
|
281
|
-
const baseline = masonry.value.length
|
|
282
|
-
if (cancelRequested.value) return
|
|
283
|
-
const pageData = await getContent(page)
|
|
284
|
-
if (cancelRequested.value) return
|
|
285
|
-
// Clear error on successful load
|
|
286
|
-
loadError.value = null
|
|
287
|
-
currentPage.value = page // Track the current page
|
|
288
|
-
paginationHistory.value.push(pageData.nextPage)
|
|
289
|
-
// Update hasReachedEnd if nextPage is null
|
|
290
|
-
if (pageData.nextPage == null) {
|
|
291
|
-
hasReachedEnd.value = true
|
|
292
|
-
}
|
|
293
|
-
await maybeBackfillToTarget(baseline)
|
|
294
|
-
return pageData
|
|
295
|
-
} catch (error) {
|
|
296
|
-
// Set load error - error is handled and exposed to UI via loadError
|
|
297
|
-
loadError.value = normalizeError(error)
|
|
298
|
-
throw error
|
|
299
|
-
} finally {
|
|
300
|
-
isLoading.value = false
|
|
301
|
-
const finalCurrentPage = currentPage.value
|
|
302
|
-
const finalNextPage = paginationHistory.value[paginationHistory.value.length - 1]
|
|
303
|
-
emits('loading:stop', {
|
|
304
|
-
fetched: masonry.value.length,
|
|
305
|
-
currentPage: finalCurrentPage,
|
|
306
|
-
nextPage: finalNextPage
|
|
307
|
-
})
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
async function loadNext() {
|
|
312
|
-
if (isLoading.value) return
|
|
313
|
-
// Don't load if we've already reached the end
|
|
314
|
-
if (hasReachedEnd.value) return
|
|
315
|
-
// Starting a new load should clear any previous cancel request
|
|
316
|
-
cancelRequested.value = false
|
|
317
|
-
if (!isLoading.value) {
|
|
318
|
-
isLoading.value = true
|
|
319
|
-
emits('loading:start')
|
|
320
|
-
}
|
|
321
|
-
// Clear error when attempting to load
|
|
322
|
-
loadError.value = null
|
|
323
|
-
try {
|
|
324
|
-
const baseline = masonry.value.length
|
|
325
|
-
if (cancelRequested.value) return
|
|
326
|
-
|
|
327
|
-
// Refresh mode: check if current page needs refreshing before loading next
|
|
328
|
-
if (modeRef.value === 'refresh' && currentPage.value != null) {
|
|
329
|
-
const currentPageItemCount = countItemsForPage(currentPage.value)
|
|
330
|
-
|
|
331
|
-
// If current page has fewer items than pageSize, refresh it first
|
|
332
|
-
if (currentPageItemCount < pageSize) {
|
|
333
|
-
const pageData = await fetchWithRetry(() => getPage(currentPage.value, context?.value))
|
|
334
|
-
if (cancelRequested.value) return
|
|
335
|
-
|
|
336
|
-
// Get only new items that don't already exist
|
|
337
|
-
// We need to check against the current masonry state at this moment
|
|
338
|
-
const currentMasonrySnapshot = [...masonry.value]
|
|
339
|
-
const newItems = pageData.items.filter((item: any) => {
|
|
340
|
-
if (!item || item.id == null || item.page == null) return false
|
|
341
|
-
return !currentMasonrySnapshot.some((existing: any) => {
|
|
342
|
-
return existing && existing.id === item.id && existing.page === item.page
|
|
343
|
-
})
|
|
344
|
-
})
|
|
345
|
-
|
|
346
|
-
// Append only new items to masonry (same pattern as getContent)
|
|
347
|
-
if (newItems.length > 0) {
|
|
348
|
-
const updatedItems = [...masonry.value, ...newItems]
|
|
349
|
-
masonry.value = updatedItems
|
|
350
|
-
await nextTick()
|
|
351
|
-
|
|
352
|
-
// Commit DOM updates without forcing sync reflow
|
|
353
|
-
await nextTick()
|
|
354
|
-
// Start FLIP on next tick (same pattern as restore/restoreMany)
|
|
355
|
-
await nextTick()
|
|
356
|
-
refreshLayout(updatedItems)
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Clear error on successful load
|
|
360
|
-
loadError.value = null
|
|
361
|
-
|
|
362
|
-
// If no new items were found, automatically proceed to next page
|
|
363
|
-
// This means the current page has no more items available
|
|
364
|
-
if (newItems.length === 0) {
|
|
365
|
-
const nextPageToLoad = paginationHistory.value[paginationHistory.value.length - 1]
|
|
366
|
-
if (nextPageToLoad == null) {
|
|
367
|
-
hasReachedEnd.value = true
|
|
368
|
-
return
|
|
369
|
-
}
|
|
370
|
-
const nextResponse = await getContent(nextPageToLoad)
|
|
371
|
-
if (cancelRequested.value) return
|
|
372
|
-
loadError.value = null
|
|
373
|
-
currentPage.value = nextPageToLoad
|
|
374
|
-
paginationHistory.value.push(nextResponse.nextPage)
|
|
375
|
-
if (nextResponse.nextPage == null) {
|
|
376
|
-
hasReachedEnd.value = true
|
|
377
|
-
}
|
|
378
|
-
await maybeBackfillToTarget(baseline)
|
|
379
|
-
return nextResponse
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// If we now have enough items for current page, proceed to next page
|
|
383
|
-
// Re-check count after items have been added
|
|
384
|
-
const updatedCount = countItemsForPage(currentPage.value)
|
|
385
|
-
if (updatedCount >= pageSize) {
|
|
386
|
-
// Current page is now full, proceed with normal next page loading
|
|
387
|
-
const nextPageToLoad = paginationHistory.value[paginationHistory.value.length - 1]
|
|
388
|
-
if (nextPageToLoad == null) {
|
|
389
|
-
hasReachedEnd.value = true
|
|
390
|
-
return
|
|
391
|
-
}
|
|
392
|
-
const nextResponse = await getContent(nextPageToLoad)
|
|
393
|
-
if (cancelRequested.value) return
|
|
394
|
-
loadError.value = null
|
|
395
|
-
currentPage.value = nextPageToLoad
|
|
396
|
-
paginationHistory.value.push(nextResponse.nextPage)
|
|
397
|
-
if (nextResponse.nextPage == null) {
|
|
398
|
-
hasReachedEnd.value = true
|
|
399
|
-
}
|
|
400
|
-
await maybeBackfillToTarget(baseline)
|
|
401
|
-
return nextResponse
|
|
402
|
-
} else {
|
|
403
|
-
// Still not enough items, but we refreshed - return the refresh pageData
|
|
404
|
-
return pageData
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Normal flow: load next page
|
|
410
|
-
const nextPageToLoad = paginationHistory.value[paginationHistory.value.length - 1]
|
|
411
|
-
// Don't load if nextPageToLoad is null
|
|
412
|
-
if (nextPageToLoad == null) {
|
|
413
|
-
hasReachedEnd.value = true
|
|
414
|
-
return
|
|
415
|
-
}
|
|
416
|
-
const pageData = await getContent(nextPageToLoad)
|
|
417
|
-
if (cancelRequested.value) return
|
|
418
|
-
// Clear error on successful load
|
|
419
|
-
loadError.value = null
|
|
420
|
-
currentPage.value = nextPageToLoad // Track the current page
|
|
421
|
-
paginationHistory.value.push(pageData.nextPage)
|
|
422
|
-
// Update hasReachedEnd if nextPage is null
|
|
423
|
-
if (pageData.nextPage == null) {
|
|
424
|
-
hasReachedEnd.value = true
|
|
425
|
-
}
|
|
426
|
-
await maybeBackfillToTarget(baseline)
|
|
427
|
-
return pageData
|
|
428
|
-
} catch (error) {
|
|
429
|
-
// Set load error - error is handled and exposed to UI via loadError
|
|
430
|
-
loadError.value = normalizeError(error)
|
|
431
|
-
throw error
|
|
432
|
-
} finally {
|
|
433
|
-
isLoading.value = false
|
|
434
|
-
const finalCurrentPage = currentPage.value
|
|
435
|
-
const finalNextPage = paginationHistory.value[paginationHistory.value.length - 1]
|
|
436
|
-
emits('loading:stop', {
|
|
437
|
-
fetched: masonry.value.length,
|
|
438
|
-
currentPage: finalCurrentPage,
|
|
439
|
-
nextPage: finalNextPage
|
|
440
|
-
})
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
async function refreshCurrentPage() {
|
|
445
|
-
if (isLoading.value) return
|
|
446
|
-
cancelRequested.value = false
|
|
447
|
-
isLoading.value = true
|
|
448
|
-
emits('loading:start')
|
|
449
|
-
|
|
450
|
-
try {
|
|
451
|
-
// Use the tracked current page
|
|
452
|
-
const pageToRefresh = currentPage.value
|
|
453
|
-
|
|
454
|
-
if (pageToRefresh == null) {
|
|
455
|
-
console.warn('[Masonry] No current page to refresh - currentPage:', currentPage.value, 'paginationHistory:', paginationHistory.value)
|
|
456
|
-
return
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Clear existing items
|
|
460
|
-
masonry.value = []
|
|
461
|
-
// Reset end flag when refreshing
|
|
462
|
-
hasReachedEnd.value = false
|
|
463
|
-
// Reset error flag when refreshing
|
|
464
|
-
loadError.value = null
|
|
465
|
-
|
|
466
|
-
// Reset pagination history to just the current page
|
|
467
|
-
paginationHistory.value = [pageToRefresh]
|
|
468
|
-
|
|
469
|
-
// Reload the current page
|
|
470
|
-
const pageData = await getContent(pageToRefresh)
|
|
471
|
-
if (cancelRequested.value) return
|
|
472
|
-
|
|
473
|
-
// Clear error on successful load
|
|
474
|
-
loadError.value = null
|
|
475
|
-
// Update pagination state
|
|
476
|
-
currentPage.value = pageToRefresh
|
|
477
|
-
paginationHistory.value.push(pageData.nextPage)
|
|
478
|
-
// Update hasReachedEnd if nextPage is null
|
|
479
|
-
if (pageData.nextPage == null) {
|
|
480
|
-
hasReachedEnd.value = true
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Optionally backfill if needed
|
|
484
|
-
const baseline = masonry.value.length
|
|
485
|
-
await maybeBackfillToTarget(baseline)
|
|
486
|
-
|
|
487
|
-
return pageData
|
|
488
|
-
} catch (error) {
|
|
489
|
-
// Set load error - error is handled and exposed to UI via loadError
|
|
490
|
-
loadError.value = normalizeError(error)
|
|
491
|
-
throw error
|
|
492
|
-
} finally {
|
|
493
|
-
isLoading.value = false
|
|
494
|
-
const finalCurrentPage = currentPage.value
|
|
495
|
-
const finalNextPage = paginationHistory.value[paginationHistory.value.length - 1]
|
|
496
|
-
emits('loading:stop', {
|
|
497
|
-
fetched: masonry.value.length,
|
|
498
|
-
currentPage: finalCurrentPage,
|
|
499
|
-
nextPage: finalNextPage
|
|
500
|
-
})
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function cancelLoad() {
|
|
505
|
-
const wasBackfilling = backfillActive
|
|
506
|
-
cancelRequested.value = true
|
|
507
|
-
isLoading.value = false
|
|
508
|
-
// Set backfillActive to false to immediately stop backfilling
|
|
509
|
-
// The backfill loop checks this flag and will exit on the next iteration
|
|
510
|
-
backfillActive = false
|
|
511
|
-
const cancelCurrentPage = currentPage.value
|
|
512
|
-
const cancelNextPage = paginationHistory.value[paginationHistory.value.length - 1]
|
|
513
|
-
// If backfill was active, emit stop event immediately
|
|
514
|
-
if (wasBackfilling) {
|
|
515
|
-
emits('backfill:stop', {
|
|
516
|
-
fetched: masonry.value.length,
|
|
517
|
-
calls: 0,
|
|
518
|
-
cancelled: true,
|
|
519
|
-
currentPage: cancelCurrentPage,
|
|
520
|
-
nextPage: cancelNextPage
|
|
521
|
-
})
|
|
522
|
-
}
|
|
523
|
-
emits('loading:stop', {
|
|
524
|
-
fetched: masonry.value.length,
|
|
525
|
-
currentPage: cancelCurrentPage,
|
|
526
|
-
nextPage: cancelNextPage
|
|
527
|
-
})
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
return {
|
|
531
|
-
loadPage,
|
|
532
|
-
loadNext,
|
|
533
|
-
refreshCurrentPage,
|
|
534
|
-
cancelLoad,
|
|
535
|
-
maybeBackfillToTarget,
|
|
536
|
-
getContent
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
package/src/useMasonryScroll.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { nextTick, type Ref } from 'vue'
|
|
2
|
-
import { calculateColumnHeights } from './masonryUtils'
|
|
3
|
-
import type { ProcessedMasonryItem } from './types'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Composable for handling masonry scroll behavior and item cleanup
|
|
7
|
-
*/
|
|
8
|
-
export function useMasonryScroll({
|
|
9
|
-
container,
|
|
10
|
-
masonry,
|
|
11
|
-
columns,
|
|
12
|
-
containerHeight,
|
|
13
|
-
isLoading,
|
|
14
|
-
pageSize,
|
|
15
|
-
refreshLayout,
|
|
16
|
-
setItemsRaw,
|
|
17
|
-
loadNext,
|
|
18
|
-
loadThresholdPx
|
|
19
|
-
}: {
|
|
20
|
-
container: Ref<HTMLElement | null>
|
|
21
|
-
masonry: Ref<ProcessedMasonryItem[]>
|
|
22
|
-
columns: Ref<number>
|
|
23
|
-
containerHeight: Ref<number>
|
|
24
|
-
isLoading: Ref<boolean>
|
|
25
|
-
pageSize: number
|
|
26
|
-
refreshLayout: (items: ProcessedMasonryItem[]) => void
|
|
27
|
-
setItemsRaw: (items: ProcessedMasonryItem[]) => void
|
|
28
|
-
loadNext: () => Promise<any>
|
|
29
|
-
loadThresholdPx?: number
|
|
30
|
-
}) {
|
|
31
|
-
let cleanupInProgress = false
|
|
32
|
-
let lastScrollTop = 0
|
|
33
|
-
|
|
34
|
-
async function handleScroll(precomputedHeights?: number[], forceCheck = false) {
|
|
35
|
-
if (!container.value) return
|
|
36
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
const isScrollingDown = container.value.scrollTop > lastScrollTop + 1 // tolerate tiny jitter
|
|
42
|
-
lastScrollTop = container.value.scrollTop
|
|
43
|
-
|
|
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
|
|
49
|
-
|
|
50
|
-
// Allow loading if near bottom and either scrolling down OR forceCheck is true (for restoration)
|
|
51
|
-
if (nearBottom && (isScrollingDown || forceCheck) && !isLoading.value) {
|
|
52
|
-
await loadNext()
|
|
53
|
-
await nextTick()
|
|
54
|
-
return
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
handleScroll
|
|
60
|
-
}
|
|
61
|
-
}
|