@wyxos/vibe 1.6.25 → 1.6.27
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 +287 -254
- package/lib/index.js +1201 -1115
- package/lib/vibe.css +1 -1
- package/package.json +1 -1
- package/src/Masonry.vue +159 -188
- package/src/components/MasonryItem.vue +501 -434
- package/src/components/examples/BasicExample.vue +2 -1
- package/src/components/examples/CustomItemExample.vue +2 -1
- package/src/components/examples/HeaderFooterExample.vue +79 -78
- package/src/components/examples/ManualInitExample.vue +78 -0
- package/src/components/examples/SwipeModeExample.vue +2 -1
- package/src/{useMasonryTransitions.ts → createMasonryTransitions.ts} +6 -6
- package/src/useMasonryItems.ts +234 -218
- package/src/useMasonryLayout.ts +4 -0
- package/src/useMasonryPagination.ts +465 -342
- package/src/views/Examples.vue +80 -32
- package/src/views/Home.vue +321 -321
package/lib/vibe.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.masonry-container[data-v-
|
|
1
|
+
.masonry-container[data-v-ce75570c]{overflow-anchor:none}.masonry-item[data-v-ce75570c]{will-change:transform,opacity;contain:layout paint;transition:transform var(--masonry-duration, .45s) var(--masonry-ease, cubic-bezier(.22, .61, .36, 1)),opacity var(--masonry-leave-duration, .16s) ease-out var(--masonry-opacity-delay, 0ms);backface-visibility:hidden}.masonry-move[data-v-ce75570c]{transition:transform var(--masonry-duration, .45s) var(--masonry-ease, cubic-bezier(.22, .61, .36, 1))}@media (prefers-reduced-motion: reduce){.masonry-container:not(.force-motion) .masonry-item[data-v-ce75570c],.masonry-container:not(.force-motion) .masonry-move[data-v-ce75570c]{transition-duration:1ms!important}}
|
package/package.json
CHANGED
package/src/Masonry.vue
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
getItemAttributes,
|
|
10
10
|
calculateColumnHeights
|
|
11
11
|
} from './masonryUtils'
|
|
12
|
-
import {
|
|
12
|
+
import { createMasonryTransitions } from './createMasonryTransitions'
|
|
13
13
|
import { useMasonryScroll } from './useMasonryScroll'
|
|
14
14
|
import { useSwipeMode as useSwipeModeComposable } from './useSwipeMode'
|
|
15
15
|
import { useMasonryPagination } from './useMasonryPagination'
|
|
@@ -21,7 +21,7 @@ import MasonryItem from './components/MasonryItem.vue'
|
|
|
21
21
|
import { normalizeError } from './utils/errorHandler'
|
|
22
22
|
|
|
23
23
|
const props = defineProps({
|
|
24
|
-
|
|
24
|
+
getPage: {
|
|
25
25
|
type: Function,
|
|
26
26
|
default: () => { }
|
|
27
27
|
},
|
|
@@ -33,6 +33,12 @@ const props = defineProps({
|
|
|
33
33
|
type: Array,
|
|
34
34
|
default: () => []
|
|
35
35
|
},
|
|
36
|
+
// Opaque caller-owned context passed through to getPage(page, context).
|
|
37
|
+
// Useful for including filters, service selection, tabId, etc.
|
|
38
|
+
context: {
|
|
39
|
+
type: Object,
|
|
40
|
+
default: null
|
|
41
|
+
},
|
|
36
42
|
layout: {
|
|
37
43
|
type: Object
|
|
38
44
|
},
|
|
@@ -41,27 +47,20 @@ const props = defineProps({
|
|
|
41
47
|
default: 'page', // or 'cursor'
|
|
42
48
|
validator: (v: string) => ['page', 'cursor'].includes(v)
|
|
43
49
|
},
|
|
44
|
-
|
|
45
|
-
type:
|
|
46
|
-
default:
|
|
47
|
-
|
|
48
|
-
// Initial pagination state when skipInitialLoad is true and items are provided
|
|
49
|
-
initialPage: {
|
|
50
|
-
type: [Number, String],
|
|
51
|
-
default: null
|
|
52
|
-
},
|
|
53
|
-
initialNextPage: {
|
|
54
|
-
type: [Number, String],
|
|
55
|
-
default: null
|
|
50
|
+
init: {
|
|
51
|
+
type: String,
|
|
52
|
+
default: 'manual',
|
|
53
|
+
validator: (v: string) => ['auto', 'manual'].includes(v)
|
|
56
54
|
},
|
|
57
55
|
pageSize: {
|
|
58
56
|
type: Number,
|
|
59
57
|
default: 40
|
|
60
58
|
},
|
|
61
59
|
// Backfill configuration
|
|
62
|
-
|
|
63
|
-
type:
|
|
64
|
-
default:
|
|
60
|
+
mode: {
|
|
61
|
+
type: String,
|
|
62
|
+
default: 'backfill',
|
|
63
|
+
validator: (value: string) => ['backfill', 'none', 'refresh'].includes(value)
|
|
65
64
|
},
|
|
66
65
|
backfillDelayMs: {
|
|
67
66
|
type: Number,
|
|
@@ -110,10 +109,6 @@ const props = defineProps({
|
|
|
110
109
|
type: Number,
|
|
111
110
|
default: 200
|
|
112
111
|
},
|
|
113
|
-
autoRefreshOnEmpty: {
|
|
114
|
-
type: Boolean,
|
|
115
|
-
default: false
|
|
116
|
-
},
|
|
117
112
|
// Layout mode: 'auto' (detect from screen size), 'masonry', or 'swipe'
|
|
118
113
|
layoutMode: {
|
|
119
114
|
type: String,
|
|
@@ -181,19 +176,22 @@ const useSwipeMode = computed(() => {
|
|
|
181
176
|
|
|
182
177
|
const emits = defineEmits([
|
|
183
178
|
'update:items',
|
|
179
|
+
'loading:start',
|
|
184
180
|
'backfill:start',
|
|
185
181
|
'backfill:tick',
|
|
186
182
|
'backfill:stop',
|
|
187
183
|
'retry:start',
|
|
188
184
|
'retry:tick',
|
|
189
185
|
'retry:stop',
|
|
186
|
+
'loading:stop',
|
|
190
187
|
'remove-all:complete',
|
|
191
188
|
// Re-emit item-level preload events from the default MasonryItem
|
|
192
189
|
'item:preload:success',
|
|
193
190
|
'item:preload:error',
|
|
194
191
|
// Mouse events from MasonryItem content
|
|
195
192
|
'item:mouse-enter',
|
|
196
|
-
'item:mouse-leave'
|
|
193
|
+
'item:mouse-leave',
|
|
194
|
+
'update:context'
|
|
197
195
|
])
|
|
198
196
|
|
|
199
197
|
const masonry = computed<any>({
|
|
@@ -201,6 +199,20 @@ const masonry = computed<any>({
|
|
|
201
199
|
set: (val) => emits('update:items', val)
|
|
202
200
|
})
|
|
203
201
|
|
|
202
|
+
const context = computed<any>({
|
|
203
|
+
get: () => props.context,
|
|
204
|
+
set: (val) => emits('update:context', val)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
function setContext(val: any) {
|
|
208
|
+
context.value = val
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const masonryLength = computed((): number => {
|
|
212
|
+
const items = masonry.value as any[]
|
|
213
|
+
return items?.length ?? 0
|
|
214
|
+
})
|
|
215
|
+
|
|
204
216
|
const columns = ref<number>(7)
|
|
205
217
|
const container = ref<HTMLElement | null>(null)
|
|
206
218
|
const paginationHistory = ref<any[]>([])
|
|
@@ -209,6 +221,10 @@ const isLoading = ref<boolean>(false)
|
|
|
209
221
|
const masonryContentHeight = ref<number>(0)
|
|
210
222
|
const hasReachedEnd = ref<boolean>(false) // Track when we've reached the last page
|
|
211
223
|
const loadError = ref<Error | null>(null) // Track load errors
|
|
224
|
+
// Track when first content has loaded
|
|
225
|
+
// For 'manual' init, show masonry immediately since we're about to load
|
|
226
|
+
// For 'auto' init, wait for items to be provided or loaded
|
|
227
|
+
const isInitialized = ref<boolean>(false)
|
|
212
228
|
|
|
213
229
|
// Current breakpoint
|
|
214
230
|
const currentBreakpoint = computed(() => getBreakpointName(containerWidth.value))
|
|
@@ -251,8 +267,8 @@ const virtualization = useMasonryVirtualization({
|
|
|
251
267
|
// Extract virtualization state and functions
|
|
252
268
|
const { viewportTop, viewportHeight, virtualizing, scrollProgress, visibleMasonry, updateScrollProgress, updateViewport: updateViewportVirtualization, reset: resetVirtualization } = virtualization
|
|
253
269
|
|
|
254
|
-
// Initialize transitions
|
|
255
|
-
const { onEnter, onBeforeEnter, onBeforeLeave, onLeave } =
|
|
270
|
+
// Initialize transitions factory with virtualization support
|
|
271
|
+
const { onEnter, onBeforeEnter, onBeforeLeave, onLeave } = createMasonryTransitions(
|
|
256
272
|
{ container, masonry: masonry as any },
|
|
257
273
|
{ leaveDurationMs: props.leaveDurationMs, virtualizing }
|
|
258
274
|
)
|
|
@@ -265,7 +281,8 @@ const leave = onLeave
|
|
|
265
281
|
|
|
266
282
|
// Initialize pagination composable
|
|
267
283
|
const pagination = useMasonryPagination({
|
|
268
|
-
|
|
284
|
+
getPage: props.getPage as (page: any, ctx?: any) => Promise<{ items: any[]; nextPage: any }>,
|
|
285
|
+
context,
|
|
269
286
|
masonry: masonry as any,
|
|
270
287
|
isLoading,
|
|
271
288
|
hasReachedEnd,
|
|
@@ -276,11 +293,10 @@ const pagination = useMasonryPagination({
|
|
|
276
293
|
retryMaxAttempts: props.retryMaxAttempts,
|
|
277
294
|
retryInitialDelayMs: props.retryInitialDelayMs,
|
|
278
295
|
retryBackoffStepMs: props.retryBackoffStepMs,
|
|
279
|
-
|
|
296
|
+
mode: props.mode,
|
|
280
297
|
backfillDelayMs: props.backfillDelayMs,
|
|
281
298
|
backfillMaxCalls: props.backfillMaxCalls,
|
|
282
299
|
pageSize: props.pageSize,
|
|
283
|
-
autoRefreshOnEmpty: props.autoRefreshOnEmpty,
|
|
284
300
|
emits
|
|
285
301
|
})
|
|
286
302
|
|
|
@@ -324,12 +340,11 @@ const items = useMasonryItems({
|
|
|
324
340
|
refreshCurrentPage,
|
|
325
341
|
loadNext,
|
|
326
342
|
maybeBackfillToTarget,
|
|
327
|
-
autoRefreshOnEmpty: props.autoRefreshOnEmpty,
|
|
328
343
|
paginationHistory
|
|
329
344
|
})
|
|
330
345
|
|
|
331
346
|
// Extract item management functions
|
|
332
|
-
const { remove, removeMany, restore, restoreMany, removeAll
|
|
347
|
+
const { remove, removeMany, restore, restoreMany, removeAll } = items
|
|
333
348
|
|
|
334
349
|
// setFixedDimensions is now in useMasonryLayout composable
|
|
335
350
|
// Wrapper function to maintain API compatibility and handle wrapper restoration
|
|
@@ -343,39 +358,62 @@ function setFixedDimensions(dimensions: { width?: number; height?: number } | nu
|
|
|
343
358
|
}
|
|
344
359
|
|
|
345
360
|
defineExpose({
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
//
|
|
349
|
-
|
|
361
|
+
// Cancels any ongoing load operations (page loads, backfills, etc.)
|
|
362
|
+
cancelLoad,
|
|
363
|
+
// Opaque caller context passed through to getPage(page, context). Useful for including filters, service selection, tabId, etc.
|
|
364
|
+
context,
|
|
365
|
+
// Container height (wrapper element) in pixels
|
|
350
366
|
containerHeight,
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
-
// Current
|
|
367
|
+
// Container width (wrapper element) in pixels
|
|
368
|
+
containerWidth,
|
|
369
|
+
// Current Tailwind breakpoint name (base, sm, md, lg, xl, 2xl) based on containerWidth
|
|
370
|
+
currentBreakpoint,
|
|
371
|
+
// Current page number or cursor being displayed
|
|
354
372
|
currentPage,
|
|
355
|
-
//
|
|
373
|
+
// Completely destroys the component, clearing all state and resetting to initial state
|
|
374
|
+
destroy,
|
|
375
|
+
// Boolean indicating if the end of the list has been reached (no more pages to load)
|
|
356
376
|
hasReachedEnd,
|
|
357
|
-
//
|
|
377
|
+
// Initializes the component with items, page, and next page cursor. Use this for manual init mode.
|
|
378
|
+
initialize,
|
|
379
|
+
// Boolean indicating if the component has been initialized (first content has loaded)
|
|
380
|
+
isInitialized,
|
|
381
|
+
// Boolean indicating if a page load or backfill operation is currently in progress
|
|
382
|
+
isLoading,
|
|
383
|
+
// Error object if the last load operation failed, null otherwise
|
|
358
384
|
loadError,
|
|
359
|
-
//
|
|
360
|
-
setFixedDimensions,
|
|
361
|
-
remove,
|
|
362
|
-
removeMany,
|
|
363
|
-
removeAll: removeAllItems,
|
|
364
|
-
restore,
|
|
365
|
-
restoreMany,
|
|
385
|
+
// Loads the next page of items asynchronously
|
|
366
386
|
loadNext,
|
|
387
|
+
// Loads a specific page number or cursor asynchronously
|
|
367
388
|
loadPage,
|
|
389
|
+
// Array tracking pagination history (pages/cursors that have been loaded)
|
|
390
|
+
paginationHistory,
|
|
391
|
+
// Refreshes the current page by clearing items and reloading from the current page
|
|
368
392
|
refreshCurrentPage,
|
|
393
|
+
// Recalculates the layout positions for all items. Call this after manually modifying items.
|
|
394
|
+
refreshLayout,
|
|
395
|
+
// Removes a single item from the masonry
|
|
396
|
+
remove,
|
|
397
|
+
// Removes all items from the masonry
|
|
398
|
+
removeAll,
|
|
399
|
+
// Removes multiple items from the masonry in a single operation
|
|
400
|
+
removeMany,
|
|
401
|
+
// Resets the component to initial state (clears items, resets pagination, scrolls to top)
|
|
369
402
|
reset,
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
scrollToTop,
|
|
403
|
+
// Restores a single item at its original index (useful for undo operations)
|
|
404
|
+
restore,
|
|
405
|
+
// Restores multiple items at their original indices (useful for undo operations)
|
|
406
|
+
restoreMany,
|
|
407
|
+
// Scrolls the container to a specific position
|
|
376
408
|
scrollTo,
|
|
377
|
-
|
|
378
|
-
|
|
409
|
+
// Scrolls the container to the top
|
|
410
|
+
scrollToTop,
|
|
411
|
+
// Sets the opaque caller context (alternative to v-model:context)
|
|
412
|
+
setContext,
|
|
413
|
+
// Sets fixed dimensions for the container, overriding ResizeObserver. Pass null to restore automatic sizing.
|
|
414
|
+
setFixedDimensions,
|
|
415
|
+
// Computed property returning the total number of items currently in the masonry
|
|
416
|
+
totalItems: computed(() => (masonry.value as any[]).length)
|
|
379
417
|
})
|
|
380
418
|
|
|
381
419
|
// Layout functions are now in useMasonryLayout composable
|
|
@@ -401,6 +439,18 @@ const goToNextItem = swipeMode.goToNextItem
|
|
|
401
439
|
const goToPreviousItem = swipeMode.goToPreviousItem
|
|
402
440
|
const snapToCurrentItem = swipeMode.snapToCurrentItem
|
|
403
441
|
|
|
442
|
+
// Helper functions for swipe mode percentage calculations
|
|
443
|
+
function getSwipeItemTop(index: string | number): string {
|
|
444
|
+
const length = masonryLength.value
|
|
445
|
+
const numIndex = typeof index === 'string' ? parseInt(index, 10) : index
|
|
446
|
+
return length > 0 ? `${numIndex * (100 / length)}%` : '0%'
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function getSwipeItemHeight(): string {
|
|
450
|
+
const length = masonryLength.value
|
|
451
|
+
return length > 0 ? `${100 / length}%` : '0%'
|
|
452
|
+
}
|
|
453
|
+
|
|
404
454
|
// refreshCurrentPage is now in useMasonryPagination composable
|
|
405
455
|
|
|
406
456
|
// Item management functions (remove, removeMany, restore, restoreMany, removeAll) are now in useMasonryItems composable
|
|
@@ -430,7 +480,7 @@ function scrollTo(options: { top?: number; left?: number; behavior?: ScrollBehav
|
|
|
430
480
|
}
|
|
431
481
|
}
|
|
432
482
|
|
|
433
|
-
// removeAll is now in useMasonryItems composable
|
|
483
|
+
// removeAll is now in useMasonryItems composable
|
|
434
484
|
|
|
435
485
|
// onResize is now in useMasonryLayout composable (onResizeLayout)
|
|
436
486
|
function onResize() {
|
|
@@ -461,12 +511,10 @@ function reset() {
|
|
|
461
511
|
paginationHistory.value = [props.loadAtPage]
|
|
462
512
|
hasReachedEnd.value = false // Reset end flag
|
|
463
513
|
loadError.value = null // Reset error flag
|
|
514
|
+
isInitialized.value = false // Reset initialization flag
|
|
464
515
|
|
|
465
516
|
// Reset virtualization state
|
|
466
517
|
resetVirtualization()
|
|
467
|
-
|
|
468
|
-
// Reset auto-initialization flag so watcher can work again if needed
|
|
469
|
-
hasInitializedWithItems = false
|
|
470
518
|
}
|
|
471
519
|
|
|
472
520
|
function destroy() {
|
|
@@ -481,6 +529,7 @@ function destroy() {
|
|
|
481
529
|
hasReachedEnd.value = false
|
|
482
530
|
loadError.value = null
|
|
483
531
|
isLoading.value = false
|
|
532
|
+
isInitialized.value = false
|
|
484
533
|
|
|
485
534
|
// Reset swipe mode state
|
|
486
535
|
currentSwipeIndex.value = 0
|
|
@@ -519,24 +568,32 @@ function handleWindowResize() {
|
|
|
519
568
|
// Note: containerWidth is updated by ResizeObserver
|
|
520
569
|
}
|
|
521
570
|
|
|
522
|
-
function
|
|
571
|
+
function initialize(items: any[], page: any, next: any) {
|
|
523
572
|
currentPage.value = page // Track the initial current page
|
|
524
573
|
paginationHistory.value = [page]
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
574
|
+
if (next !== null && next !== undefined) {
|
|
575
|
+
paginationHistory.value.push(next)
|
|
576
|
+
}
|
|
577
|
+
// Only treat explicit null as end-of-list. Undefined means "unknown".
|
|
578
|
+
hasReachedEnd.value = next === null
|
|
528
579
|
// Diagnostics: check incoming initial items
|
|
529
|
-
checkItemDimensions(items as any[], '
|
|
580
|
+
checkItemDimensions(items as any[], 'initialize')
|
|
581
|
+
|
|
582
|
+
// If masonry is empty, replace items; otherwise add them
|
|
583
|
+
const currentItems = masonry.value as any[]
|
|
584
|
+
const newItems = currentItems.length === 0 ? items : [...currentItems, ...items]
|
|
585
|
+
|
|
586
|
+
// Set items first (this updates the v-model)
|
|
587
|
+
masonry.value = newItems
|
|
530
588
|
|
|
531
589
|
if (useSwipeMode.value) {
|
|
532
|
-
// In swipe mode, just
|
|
533
|
-
masonry.value = [...(masonry.value as any[]), ...items]
|
|
590
|
+
// In swipe mode, just set items without layout calculation
|
|
534
591
|
// Reset swipe index if we're at the start
|
|
535
592
|
if (currentSwipeIndex.value === 0 && masonry.value.length > 0) {
|
|
536
593
|
swipeOffset.value = 0
|
|
537
594
|
}
|
|
538
595
|
} else {
|
|
539
|
-
refreshLayout(
|
|
596
|
+
refreshLayout(newItems)
|
|
540
597
|
|
|
541
598
|
// Update viewport state from container's scroll position
|
|
542
599
|
// Critical after refresh when browser may restore scroll position
|
|
@@ -555,89 +612,14 @@ function init(items: any[], page: any, next: any) {
|
|
|
555
612
|
}
|
|
556
613
|
})
|
|
557
614
|
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* Restore items when skipInitialLoad is true.
|
|
562
|
-
* This method should be called instead of directly assigning to v-model:items
|
|
563
|
-
* when restoring items from saved state.
|
|
564
|
-
* @param items - Items to restore
|
|
565
|
-
* @param page - Current page number/cursor
|
|
566
|
-
* @param next - Next page cursor (or null if at end)
|
|
567
|
-
*/
|
|
568
|
-
async function restoreItems(items: any[], page: any, next: any) {
|
|
569
|
-
// If skipInitialLoad is false, fall back to init behavior
|
|
570
|
-
if (!props.skipInitialLoad) {
|
|
571
|
-
init(items, page, next)
|
|
572
|
-
return
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// When skipInitialLoad is true, we need to restore items without triggering initial load
|
|
576
|
-
currentPage.value = page
|
|
577
|
-
paginationHistory.value = [page]
|
|
578
|
-
if (next !== null && next !== undefined) {
|
|
579
|
-
paginationHistory.value.push(next)
|
|
580
|
-
}
|
|
581
|
-
// Only set hasReachedEnd to true if next is explicitly null (end of list)
|
|
582
|
-
// undefined means "unknown" - don't assume end of list
|
|
583
|
-
hasReachedEnd.value = next === null
|
|
584
|
-
loadError.value = null
|
|
585
|
-
|
|
586
|
-
// Diagnostics: check incoming items
|
|
587
|
-
checkItemDimensions(items as any[], 'restoreItems')
|
|
588
|
-
|
|
589
|
-
// Set items directly (v-model will sync) and refresh layout
|
|
590
|
-
// Follow the same pattern as init() and getContent()
|
|
591
|
-
if (useSwipeMode.value) {
|
|
592
|
-
// In swipe mode, just set items without layout calculation
|
|
593
|
-
masonry.value = items
|
|
594
|
-
// Reset swipe index if we're at the start
|
|
595
|
-
if (currentSwipeIndex.value === 0 && masonry.value.length > 0) {
|
|
596
|
-
swipeOffset.value = 0
|
|
597
|
-
}
|
|
598
|
-
} else {
|
|
599
|
-
// In masonry mode, refresh layout with the restored items
|
|
600
|
-
refreshLayout(items)
|
|
601
|
-
|
|
602
|
-
// Update viewport state from container's scroll position
|
|
603
|
-
if (container.value) {
|
|
604
|
-
viewportTop.value = container.value.scrollTop
|
|
605
|
-
viewportHeight.value = container.value.clientHeight || window.innerHeight
|
|
606
|
-
}
|
|
607
615
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
viewportTop.value = container.value.scrollTop
|
|
612
|
-
viewportHeight.value = container.value.clientHeight || window.innerHeight
|
|
613
|
-
updateScrollProgress()
|
|
614
|
-
|
|
615
|
-
// Check if user is already at the bottom after restoration
|
|
616
|
-
// If so, trigger loading to restore scroll-to-bottom functionality
|
|
617
|
-
// Wait for layout to be fully calculated before checking
|
|
618
|
-
await nextTick()
|
|
619
|
-
const columnHeights = calculateColumnHeights(masonry.value as any, columns.value)
|
|
620
|
-
const tallest = columnHeights.length ? Math.max(...columnHeights) : 0
|
|
621
|
-
const scrollerBottom = container.value.scrollTop + container.value.clientHeight
|
|
622
|
-
const threshold = typeof props.loadThresholdPx === 'number' ? props.loadThresholdPx : 200
|
|
623
|
-
const triggerPoint = threshold >= 0
|
|
624
|
-
? Math.max(0, tallest - threshold)
|
|
625
|
-
: Math.max(0, tallest + threshold)
|
|
626
|
-
const nearBottom = scrollerBottom >= triggerPoint
|
|
627
|
-
|
|
628
|
-
// If user is at bottom and there's a next page, trigger loading
|
|
629
|
-
// This restores scroll-to-bottom functionality after tab restoration
|
|
630
|
-
if (nearBottom && !hasReachedEnd.value && !isLoading.value && paginationHistory.value.length > 0) {
|
|
631
|
-
const nextPage = paginationHistory.value[paginationHistory.value.length - 1]
|
|
632
|
-
if (nextPage != null) {
|
|
633
|
-
// Use handleScroll with forceCheck=true to bypass isScrollingDown check
|
|
634
|
-
await handleScroll(columnHeights, true)
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
616
|
+
// Mark as initialized when items are provided
|
|
617
|
+
if (items && items.length > 0) {
|
|
618
|
+
isInitialized.value = true
|
|
638
619
|
}
|
|
639
620
|
}
|
|
640
621
|
|
|
622
|
+
|
|
641
623
|
// Watch for layout changes and update columns + refresh layout dynamically
|
|
642
624
|
watch(
|
|
643
625
|
layout,
|
|
@@ -676,31 +658,14 @@ watch(container, (el) => {
|
|
|
676
658
|
}
|
|
677
659
|
}, { immediate: true })
|
|
678
660
|
|
|
679
|
-
// Watch for items when
|
|
680
|
-
// This handles cases where items are provided after mount or updated externally
|
|
681
|
-
let hasInitializedWithItems = false
|
|
661
|
+
// Watch for when items are first loaded (for init='manual' when items are loaded via initialize)
|
|
682
662
|
watch(
|
|
683
|
-
() =>
|
|
684
|
-
(
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (
|
|
690
|
-
skipInitialLoad &&
|
|
691
|
-
items &&
|
|
692
|
-
items.length > 0 &&
|
|
693
|
-
!hasInitializedWithItems
|
|
694
|
-
) {
|
|
695
|
-
hasInitializedWithItems = true
|
|
696
|
-
const page = initialPage !== null && initialPage !== undefined
|
|
697
|
-
? initialPage
|
|
698
|
-
: (props.loadAtPage as any)
|
|
699
|
-
const next = initialNextPage !== undefined
|
|
700
|
-
? initialNextPage
|
|
701
|
-
: undefined // undefined means "unknown", null means "end of list"
|
|
702
|
-
|
|
703
|
-
restoreItems(items as any[], page, next)
|
|
663
|
+
() => masonry.value.length,
|
|
664
|
+
(newLength, oldLength) => {
|
|
665
|
+
// For manual mode, mark as initialized when items first appear
|
|
666
|
+
// This handles the case where items are loaded via initialize after mount
|
|
667
|
+
if (props.init === 'manual' && !isInitialized.value && newLength > 0 && oldLength === 0) {
|
|
668
|
+
isInitialized.value = true
|
|
704
669
|
}
|
|
705
670
|
},
|
|
706
671
|
{ immediate: false }
|
|
@@ -852,23 +817,21 @@ onMounted(async () => {
|
|
|
852
817
|
const initialPage = props.loadAtPage as any
|
|
853
818
|
paginationHistory.value = [initialPage]
|
|
854
819
|
|
|
855
|
-
if (
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
//
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
await restoreItems(props.items as any[], page, next)
|
|
869
|
-
// Mark as initialized to prevent watcher from running again
|
|
870
|
-
hasInitializedWithItems = true
|
|
820
|
+
if (props.init === 'auto') {
|
|
821
|
+
// Auto mode: automatically call loadPage on mount
|
|
822
|
+
// Set initialized BEFORE loading so the masonry container renders
|
|
823
|
+
// This allows refreshLayout to access the container element for measurements
|
|
824
|
+
isInitialized.value = true
|
|
825
|
+
await nextTick() // Ensure container is rendered before loading
|
|
826
|
+
|
|
827
|
+
try {
|
|
828
|
+
await loadPage(initialPage)
|
|
829
|
+
} catch (error) {
|
|
830
|
+
// Error is already handled by loadPage via loadError
|
|
831
|
+
// Continue - component is already initialized
|
|
832
|
+
}
|
|
871
833
|
}
|
|
834
|
+
// Manual mode: do nothing, user will manually call restore()
|
|
872
835
|
|
|
873
836
|
if (!useSwipeMode.value) {
|
|
874
837
|
updateScrollProgress()
|
|
@@ -886,6 +849,7 @@ onMounted(async () => {
|
|
|
886
849
|
loadError.value = normalizeError(error)
|
|
887
850
|
}
|
|
888
851
|
isLoading.value = false
|
|
852
|
+
// isInitialized is already set to true before loadPage for 'auto' mode
|
|
889
853
|
}
|
|
890
854
|
|
|
891
855
|
// Scroll listener is handled by watcher now for consistency
|
|
@@ -918,19 +882,26 @@ onUnmounted(() => {
|
|
|
918
882
|
|
|
919
883
|
<template>
|
|
920
884
|
<div ref="wrapper" class="w-full h-full flex flex-col relative">
|
|
885
|
+
<!-- Loading message while waiting for initial content -->
|
|
886
|
+
<div v-if="!isInitialized" class="w-full h-full flex items-center justify-center">
|
|
887
|
+
<slot name="loading-message">
|
|
888
|
+
<p class="text-gray-500 dark:text-gray-400">Waiting for content to load...</p>
|
|
889
|
+
</slot>
|
|
890
|
+
</div>
|
|
891
|
+
|
|
921
892
|
<!-- Swipe Feed Mode (Mobile/Tablet) -->
|
|
922
|
-
<div v-if="useSwipeMode" class="overflow-hidden w-full flex-1 swipe-container touch-none select-none"
|
|
893
|
+
<div v-else-if="useSwipeMode" class="overflow-hidden w-full flex-1 swipe-container touch-none select-none"
|
|
923
894
|
:class="{ 'force-motion': props.forceMotion, 'cursor-grab': !isDragging, 'cursor-grabbing': isDragging }"
|
|
924
895
|
ref="swipeContainer" style="height: 100%; max-height: 100%; position: relative;">
|
|
925
896
|
<div class="relative w-full" :style="{
|
|
926
897
|
transform: `translateY(${swipeOffset}px)`,
|
|
927
898
|
transition: isDragging ? 'none' : `transform ${transitionDurationMs}ms ${transitionEasing}`,
|
|
928
|
-
height: `${
|
|
899
|
+
height: `${masonryLength * 100}%`
|
|
929
900
|
}">
|
|
930
901
|
<div v-for="(item, index) in masonry" :key="`${item.page}-${item.id}`" class="absolute top-0 left-0 w-full"
|
|
931
902
|
:style="{
|
|
932
|
-
top:
|
|
933
|
-
height:
|
|
903
|
+
top: getSwipeItemTop(index),
|
|
904
|
+
height: getSwipeItemHeight()
|
|
934
905
|
}">
|
|
935
906
|
<div class="w-full h-full flex items-center justify-center p-4">
|
|
936
907
|
<div class="w-full h-full max-w-full max-h-full relative">
|