@wyxos/vibe 1.6.27 → 1.6.29
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 +1063 -1023
- package/lib/vibe.css +1 -1
- package/package.json +1 -1
- package/src/Masonry.vue +1030 -1008
- package/src/components/MasonryItem.vue +499 -501
- package/src/createMasonryTransitions.ts +18 -27
- package/src/types.ts +101 -38
- package/src/useMasonryItems.ts +231 -234
- package/src/useMasonryLayout.ts +164 -164
- package/src/useMasonryPagination.ts +116 -42
- package/src/useMasonryVirtualization.ts +1 -1
- package/src/views/Home.vue +2 -2
package/src/useMasonryLayout.ts
CHANGED
|
@@ -1,164 +1,164 @@
|
|
|
1
|
-
import { ref, nextTick, type Ref, type ComputedRef } from 'vue'
|
|
2
|
-
import calculateLayout from './calculateLayout'
|
|
3
|
-
import { getColumnCount, calculateContainerHeight } from './masonryUtils'
|
|
4
|
-
|
|
5
|
-
export interface UseMasonryLayoutOptions {
|
|
6
|
-
masonry: Ref<any[]>
|
|
7
|
-
useSwipeMode: ComputedRef<boolean>
|
|
8
|
-
container: Ref<HTMLElement | null>
|
|
9
|
-
columns: Ref<number>
|
|
10
|
-
containerWidth: Ref<number>
|
|
11
|
-
masonryContentHeight: Ref<number>
|
|
12
|
-
layout: ComputedRef<any>
|
|
13
|
-
fixedDimensions: Ref<{ width?: number; height?: number } | null>
|
|
14
|
-
checkItemDimensions: (items: any[], context: string) => void
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function useMasonryLayout(options: UseMasonryLayoutOptions) {
|
|
18
|
-
const {
|
|
19
|
-
masonry,
|
|
20
|
-
useSwipeMode,
|
|
21
|
-
container,
|
|
22
|
-
columns,
|
|
23
|
-
containerWidth,
|
|
24
|
-
masonryContentHeight,
|
|
25
|
-
layout,
|
|
26
|
-
fixedDimensions,
|
|
27
|
-
checkItemDimensions
|
|
28
|
-
} = options
|
|
29
|
-
|
|
30
|
-
// Cache previous layout state for incremental updates
|
|
31
|
-
let previousLayoutItems: any[] = []
|
|
32
|
-
|
|
33
|
-
function calculateHeight(content: any[]) {
|
|
34
|
-
const newHeight = calculateContainerHeight(content as any)
|
|
35
|
-
let floor = 0
|
|
36
|
-
if (container.value) {
|
|
37
|
-
const { scrollTop, clientHeight } = container.value
|
|
38
|
-
floor = scrollTop + clientHeight + 100
|
|
39
|
-
}
|
|
40
|
-
masonryContentHeight.value = Math.max(newHeight, floor)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function refreshLayout(items: any[]) {
|
|
44
|
-
if (useSwipeMode.value) {
|
|
45
|
-
// In swipe mode, no layout calculation needed - items are stacked vertically
|
|
46
|
-
masonry.value = items as any
|
|
47
|
-
return
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Always update masonry value, even if container isn't ready
|
|
51
|
-
// This ensures items are added in tests and when container isn't available yet
|
|
52
|
-
masonry.value = items as any
|
|
53
|
-
|
|
54
|
-
if (!container.value) return
|
|
55
|
-
// Developer diagnostics: warn when dimensions are invalid
|
|
56
|
-
checkItemDimensions(items as any[], 'refreshLayout')
|
|
57
|
-
|
|
58
|
-
// Optimization: For large arrays, check if we can do incremental update
|
|
59
|
-
// Only works if items were removed from the end (common case)
|
|
60
|
-
const canUseIncremental = items.length > 1000 &&
|
|
61
|
-
previousLayoutItems.length > items.length &&
|
|
62
|
-
previousLayoutItems.length - items.length < 100 // Only small removals
|
|
63
|
-
|
|
64
|
-
if (canUseIncremental) {
|
|
65
|
-
// Check if items were removed from the end (most common case)
|
|
66
|
-
let removedFromEnd = true
|
|
67
|
-
for (let i = 0; i < items.length; i++) {
|
|
68
|
-
if (items[i]?.id !== previousLayoutItems[i]?.id) {
|
|
69
|
-
removedFromEnd = false
|
|
70
|
-
break
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (removedFromEnd) {
|
|
75
|
-
// Items removed from end - we can reuse previous positions for remaining items
|
|
76
|
-
// Just update indices and recalculate height
|
|
77
|
-
const itemsWithIndex = items.map((item, index) => ({
|
|
78
|
-
...previousLayoutItems[index],
|
|
79
|
-
originalIndex: index
|
|
80
|
-
}))
|
|
81
|
-
|
|
82
|
-
// Recalculate height only
|
|
83
|
-
calculateHeight(itemsWithIndex as any)
|
|
84
|
-
masonry.value = itemsWithIndex
|
|
85
|
-
previousLayoutItems = itemsWithIndex
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Full recalculation (fallback for all other cases)
|
|
91
|
-
// Update original index to reflect current position in array
|
|
92
|
-
// This ensures indices are correct after items are removed
|
|
93
|
-
const itemsWithIndex = items.map((item, index) => ({
|
|
94
|
-
...item,
|
|
95
|
-
originalIndex: index
|
|
96
|
-
}))
|
|
97
|
-
|
|
98
|
-
// When fixed dimensions are set, ensure container uses the fixed width for layout
|
|
99
|
-
// This prevents gaps when the container's actual width differs from the fixed width
|
|
100
|
-
const containerEl = container.value as HTMLElement
|
|
101
|
-
if (fixedDimensions.value && fixedDimensions.value.width !== undefined) {
|
|
102
|
-
// Temporarily set width to match fixed dimensions for accurate layout calculation
|
|
103
|
-
const originalWidth = containerEl.style.width
|
|
104
|
-
const originalBoxSizing = containerEl.style.boxSizing
|
|
105
|
-
containerEl.style.boxSizing = 'border-box'
|
|
106
|
-
containerEl.style.width = `${fixedDimensions.value.width}px`
|
|
107
|
-
// Force reflow
|
|
108
|
-
containerEl.offsetWidth
|
|
109
|
-
|
|
110
|
-
const content = calculateLayout(itemsWithIndex as any, containerEl, columns.value, layout.value as any)
|
|
111
|
-
|
|
112
|
-
// Restore original width
|
|
113
|
-
containerEl.style.width = originalWidth
|
|
114
|
-
containerEl.style.boxSizing = originalBoxSizing
|
|
115
|
-
|
|
116
|
-
calculateHeight(content as any)
|
|
117
|
-
masonry.value = content
|
|
118
|
-
// Cache for next incremental update
|
|
119
|
-
previousLayoutItems = content
|
|
120
|
-
} else {
|
|
121
|
-
const content = calculateLayout(itemsWithIndex as any, containerEl, columns.value, layout.value as any)
|
|
122
|
-
calculateHeight(content as any)
|
|
123
|
-
masonry.value = content
|
|
124
|
-
// Cache for next incremental update
|
|
125
|
-
previousLayoutItems = content
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function setFixedDimensions(
|
|
130
|
-
dimensions: { width?: number; height?: number } | null,
|
|
131
|
-
updateScrollProgress?: () => void
|
|
132
|
-
) {
|
|
133
|
-
fixedDimensions.value = dimensions
|
|
134
|
-
if (dimensions) {
|
|
135
|
-
if (dimensions.width !== undefined) containerWidth.value = dimensions.width
|
|
136
|
-
// Force layout refresh when dimensions change
|
|
137
|
-
if (!useSwipeMode.value && container.value && masonry.value.length > 0) {
|
|
138
|
-
// Use nextTick to ensure DOM has updated
|
|
139
|
-
nextTick(() => {
|
|
140
|
-
columns.value = getColumnCount(layout.value as any, containerWidth.value)
|
|
141
|
-
refreshLayout(masonry.value as any)
|
|
142
|
-
if (updateScrollProgress) {
|
|
143
|
-
updateScrollProgress()
|
|
144
|
-
}
|
|
145
|
-
})
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
// When clearing fixed dimensions, restore from wrapper
|
|
149
|
-
// Note: wrapper is not available in this composable, so this needs to be handled by caller
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function onResize() {
|
|
153
|
-
columns.value = getColumnCount(layout.value as any, containerWidth.value)
|
|
154
|
-
refreshLayout(masonry.value as any)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return {
|
|
158
|
-
refreshLayout,
|
|
159
|
-
setFixedDimensions,
|
|
160
|
-
onResize,
|
|
161
|
-
calculateHeight
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
1
|
+
import { ref, nextTick, type Ref, type ComputedRef } from 'vue'
|
|
2
|
+
import calculateLayout from './calculateLayout'
|
|
3
|
+
import { getColumnCount, calculateContainerHeight } from './masonryUtils'
|
|
4
|
+
|
|
5
|
+
export interface UseMasonryLayoutOptions {
|
|
6
|
+
masonry: Ref<any[]>
|
|
7
|
+
useSwipeMode: ComputedRef<boolean>
|
|
8
|
+
container: Ref<HTMLElement | null>
|
|
9
|
+
columns: Ref<number>
|
|
10
|
+
containerWidth: Ref<number>
|
|
11
|
+
masonryContentHeight: Ref<number>
|
|
12
|
+
layout: ComputedRef<any>
|
|
13
|
+
fixedDimensions: Ref<{ width?: number; height?: number } | null>
|
|
14
|
+
checkItemDimensions: (items: any[], context: string) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useMasonryLayout(options: UseMasonryLayoutOptions) {
|
|
18
|
+
const {
|
|
19
|
+
masonry,
|
|
20
|
+
useSwipeMode,
|
|
21
|
+
container,
|
|
22
|
+
columns,
|
|
23
|
+
containerWidth,
|
|
24
|
+
masonryContentHeight,
|
|
25
|
+
layout,
|
|
26
|
+
fixedDimensions,
|
|
27
|
+
checkItemDimensions
|
|
28
|
+
} = options
|
|
29
|
+
|
|
30
|
+
// Cache previous layout state for incremental updates
|
|
31
|
+
let previousLayoutItems: any[] = []
|
|
32
|
+
|
|
33
|
+
function calculateHeight(content: any[]) {
|
|
34
|
+
const newHeight = calculateContainerHeight(content as any)
|
|
35
|
+
let floor = 0
|
|
36
|
+
if (container.value) {
|
|
37
|
+
const { scrollTop, clientHeight } = container.value
|
|
38
|
+
floor = scrollTop + clientHeight + 100
|
|
39
|
+
}
|
|
40
|
+
masonryContentHeight.value = Math.max(newHeight, floor)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function refreshLayout(items: any[]) {
|
|
44
|
+
if (useSwipeMode.value) {
|
|
45
|
+
// In swipe mode, no layout calculation needed - items are stacked vertically
|
|
46
|
+
masonry.value = items as any
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Always update masonry value, even if container isn't ready
|
|
51
|
+
// This ensures items are added in tests and when container isn't available yet
|
|
52
|
+
masonry.value = items as any
|
|
53
|
+
|
|
54
|
+
if (!container.value) return
|
|
55
|
+
// Developer diagnostics: warn when dimensions are invalid
|
|
56
|
+
checkItemDimensions(items as any[], 'refreshLayout')
|
|
57
|
+
|
|
58
|
+
// Optimization: For large arrays, check if we can do incremental update
|
|
59
|
+
// Only works if items were removed from the end (common case)
|
|
60
|
+
const canUseIncremental = items.length > 1000 &&
|
|
61
|
+
previousLayoutItems.length > items.length &&
|
|
62
|
+
previousLayoutItems.length - items.length < 100 // Only small removals
|
|
63
|
+
|
|
64
|
+
if (canUseIncremental) {
|
|
65
|
+
// Check if items were removed from the end (most common case)
|
|
66
|
+
let removedFromEnd = true
|
|
67
|
+
for (let i = 0; i < items.length; i++) {
|
|
68
|
+
if (items[i]?.id !== previousLayoutItems[i]?.id) {
|
|
69
|
+
removedFromEnd = false
|
|
70
|
+
break
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (removedFromEnd) {
|
|
75
|
+
// Items removed from end - we can reuse previous positions for remaining items
|
|
76
|
+
// Just update indices and recalculate height
|
|
77
|
+
const itemsWithIndex = items.map((item, index) => ({
|
|
78
|
+
...previousLayoutItems[index],
|
|
79
|
+
originalIndex: index
|
|
80
|
+
}))
|
|
81
|
+
|
|
82
|
+
// Recalculate height only
|
|
83
|
+
calculateHeight(itemsWithIndex as any)
|
|
84
|
+
masonry.value = itemsWithIndex
|
|
85
|
+
previousLayoutItems = itemsWithIndex
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Full recalculation (fallback for all other cases)
|
|
91
|
+
// Update original index to reflect current position in array
|
|
92
|
+
// This ensures indices are correct after items are removed
|
|
93
|
+
const itemsWithIndex = items.map((item, index) => ({
|
|
94
|
+
...item,
|
|
95
|
+
originalIndex: index
|
|
96
|
+
}))
|
|
97
|
+
|
|
98
|
+
// When fixed dimensions are set, ensure container uses the fixed width for layout
|
|
99
|
+
// This prevents gaps when the container's actual width differs from the fixed width
|
|
100
|
+
const containerEl = container.value as HTMLElement
|
|
101
|
+
if (fixedDimensions.value && fixedDimensions.value.width !== undefined) {
|
|
102
|
+
// Temporarily set width to match fixed dimensions for accurate layout calculation
|
|
103
|
+
const originalWidth = containerEl.style.width
|
|
104
|
+
const originalBoxSizing = containerEl.style.boxSizing
|
|
105
|
+
containerEl.style.boxSizing = 'border-box'
|
|
106
|
+
containerEl.style.width = `${fixedDimensions.value.width}px`
|
|
107
|
+
// Force reflow
|
|
108
|
+
containerEl.offsetWidth
|
|
109
|
+
|
|
110
|
+
const content = calculateLayout(itemsWithIndex as any, containerEl, columns.value, layout.value as any)
|
|
111
|
+
|
|
112
|
+
// Restore original width
|
|
113
|
+
containerEl.style.width = originalWidth
|
|
114
|
+
containerEl.style.boxSizing = originalBoxSizing
|
|
115
|
+
|
|
116
|
+
calculateHeight(content as any)
|
|
117
|
+
masonry.value = content
|
|
118
|
+
// Cache for next incremental update
|
|
119
|
+
previousLayoutItems = content
|
|
120
|
+
} else {
|
|
121
|
+
const content = calculateLayout(itemsWithIndex as any, containerEl, columns.value, layout.value as any)
|
|
122
|
+
calculateHeight(content as any)
|
|
123
|
+
masonry.value = content
|
|
124
|
+
// Cache for next incremental update
|
|
125
|
+
previousLayoutItems = content
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function setFixedDimensions(
|
|
130
|
+
dimensions: { width?: number; height?: number } | null,
|
|
131
|
+
updateScrollProgress?: () => void
|
|
132
|
+
) {
|
|
133
|
+
fixedDimensions.value = dimensions
|
|
134
|
+
if (dimensions) {
|
|
135
|
+
if (dimensions.width !== undefined) containerWidth.value = dimensions.width
|
|
136
|
+
// Force layout refresh when dimensions change
|
|
137
|
+
if (!useSwipeMode.value && container.value && masonry.value.length > 0) {
|
|
138
|
+
// Use nextTick to ensure DOM has updated
|
|
139
|
+
nextTick(() => {
|
|
140
|
+
columns.value = getColumnCount(layout.value as any, containerWidth.value)
|
|
141
|
+
refreshLayout(masonry.value as any)
|
|
142
|
+
if (updateScrollProgress) {
|
|
143
|
+
updateScrollProgress()
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// When clearing fixed dimensions, restore from wrapper
|
|
149
|
+
// Note: wrapper is not available in this composable, so this needs to be handled by caller
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function onResize() {
|
|
153
|
+
columns.value = getColumnCount(layout.value as any, containerWidth.value)
|
|
154
|
+
refreshLayout(masonry.value as any)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
refreshLayout,
|
|
159
|
+
setFixedDimensions,
|
|
160
|
+
onResize,
|
|
161
|
+
calculateHeight
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ref, type Ref } from 'vue'
|
|
1
|
+
import { ref, nextTick, type Ref } from 'vue'
|
|
2
2
|
import { normalizeError } from './utils/errorHandler'
|
|
3
3
|
|
|
4
4
|
export interface UseMasonryPaginationOptions {
|
|
@@ -14,7 +14,7 @@ export interface UseMasonryPaginationOptions {
|
|
|
14
14
|
retryMaxAttempts: number
|
|
15
15
|
retryInitialDelayMs: number
|
|
16
16
|
retryBackoffStepMs: number
|
|
17
|
-
mode: string
|
|
17
|
+
mode: string | Ref<string>
|
|
18
18
|
backfillDelayMs: number
|
|
19
19
|
backfillMaxCalls: number
|
|
20
20
|
pageSize: number
|
|
@@ -23,10 +23,10 @@ export interface UseMasonryPaginationOptions {
|
|
|
23
23
|
(event: 'retry:start', payload: { attempt: number; max: number; totalMs: number }): void
|
|
24
24
|
(event: 'retry:tick', payload: { attempt: number; remainingMs: number; totalMs: number }): void
|
|
25
25
|
(event: 'retry:stop', payload: { attempt: number; success: boolean }): void
|
|
26
|
-
(event: 'backfill:start', payload: { target: number; fetched: number; calls: number }): void
|
|
27
|
-
(event: 'backfill:tick', payload: { fetched: number; target: number; calls: number; remainingMs: number; totalMs: number }): void
|
|
28
|
-
(event: 'backfill:stop', payload: { fetched: number; calls: number; cancelled?: boolean }): void
|
|
29
|
-
(event: 'loading:stop', payload: { fetched: number }): 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
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -51,6 +51,9 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
|
|
|
51
51
|
emits
|
|
52
52
|
} = options
|
|
53
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
|
+
|
|
54
57
|
const cancelRequested = ref(false)
|
|
55
58
|
let backfillActive = false
|
|
56
59
|
|
|
@@ -135,9 +138,18 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
|
|
|
135
138
|
|
|
136
139
|
async function getContent(page: number) {
|
|
137
140
|
try {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
141
153
|
} catch (error) {
|
|
142
154
|
// Error is handled by callers (loadPage, loadNext, etc.) which set loadError
|
|
143
155
|
throw error
|
|
@@ -145,7 +157,7 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
|
|
|
145
157
|
}
|
|
146
158
|
|
|
147
159
|
async function maybeBackfillToTarget(baselineCount: number, force = false) {
|
|
148
|
-
if (!force &&
|
|
160
|
+
if (!force && modeRef.value !== 'backfill') return
|
|
149
161
|
if (backfillActive) return
|
|
150
162
|
if (cancelRequested.value) return
|
|
151
163
|
// Don't backfill if we've reached the end
|
|
@@ -170,7 +182,15 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
|
|
|
170
182
|
}
|
|
171
183
|
try {
|
|
172
184
|
let calls = 0
|
|
173
|
-
|
|
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
|
+
})
|
|
174
194
|
|
|
175
195
|
while (
|
|
176
196
|
masonry.value.length < targetCount &&
|
|
@@ -180,20 +200,24 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
|
|
|
180
200
|
!hasReachedEnd.value &&
|
|
181
201
|
backfillActive
|
|
182
202
|
) {
|
|
203
|
+
const tickCurrentPage = currentPage.value
|
|
204
|
+
const tickNextPage = paginationHistory.value[paginationHistory.value.length - 1]
|
|
183
205
|
await waitWithProgress(backfillDelayMs, (remaining, total) => {
|
|
184
206
|
emits('backfill:tick', {
|
|
185
207
|
fetched: masonry.value.length,
|
|
186
208
|
target: targetCount,
|
|
187
209
|
calls,
|
|
188
210
|
remainingMs: remaining,
|
|
189
|
-
totalMs: total
|
|
211
|
+
totalMs: total,
|
|
212
|
+
currentPage: tickCurrentPage,
|
|
213
|
+
nextPage: tickNextPage
|
|
190
214
|
})
|
|
191
215
|
})
|
|
192
216
|
|
|
193
217
|
if (cancelRequested.value || !backfillActive) break
|
|
194
218
|
|
|
195
|
-
const
|
|
196
|
-
if (
|
|
219
|
+
const currentPageToLoad = paginationHistory.value[paginationHistory.value.length - 1]
|
|
220
|
+
if (currentPageToLoad == null) {
|
|
197
221
|
hasReachedEnd.value = true
|
|
198
222
|
break
|
|
199
223
|
}
|
|
@@ -201,13 +225,14 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
|
|
|
201
225
|
// Don't toggle isLoading here - keep it true throughout backfill
|
|
202
226
|
// Check cancellation before starting getContent to avoid unnecessary requests
|
|
203
227
|
if (cancelRequested.value || !backfillActive) break
|
|
204
|
-
const
|
|
228
|
+
const pageData = await getContent(currentPageToLoad)
|
|
205
229
|
if (cancelRequested.value || !backfillActive) break
|
|
206
230
|
// Clear error on successful load
|
|
207
231
|
loadError.value = null
|
|
208
|
-
|
|
232
|
+
currentPage.value = currentPageToLoad
|
|
233
|
+
paginationHistory.value.push(pageData.nextPage)
|
|
209
234
|
// Update hasReachedEnd if nextPage is null
|
|
210
|
-
if (
|
|
235
|
+
if (pageData.nextPage == null) {
|
|
211
236
|
hasReachedEnd.value = true
|
|
212
237
|
}
|
|
213
238
|
} catch (error) {
|
|
@@ -219,12 +244,25 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
|
|
|
219
244
|
calls++
|
|
220
245
|
}
|
|
221
246
|
|
|
222
|
-
|
|
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
|
+
})
|
|
223
255
|
} finally {
|
|
224
256
|
backfillActive = false
|
|
225
257
|
// Only set loading to false when backfill completes or is cancelled
|
|
226
258
|
isLoading.value = false
|
|
227
|
-
|
|
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
|
+
})
|
|
228
266
|
}
|
|
229
267
|
}
|
|
230
268
|
|
|
@@ -242,24 +280,31 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
|
|
|
242
280
|
try {
|
|
243
281
|
const baseline = masonry.value.length
|
|
244
282
|
if (cancelRequested.value) return
|
|
245
|
-
const
|
|
283
|
+
const pageData = await getContent(page)
|
|
246
284
|
if (cancelRequested.value) return
|
|
247
285
|
// Clear error on successful load
|
|
248
286
|
loadError.value = null
|
|
249
287
|
currentPage.value = page // Track the current page
|
|
250
|
-
paginationHistory.value.push(
|
|
288
|
+
paginationHistory.value.push(pageData.nextPage)
|
|
251
289
|
// Update hasReachedEnd if nextPage is null
|
|
252
|
-
if (
|
|
290
|
+
if (pageData.nextPage == null) {
|
|
253
291
|
hasReachedEnd.value = true
|
|
254
292
|
}
|
|
255
293
|
await maybeBackfillToTarget(baseline)
|
|
256
|
-
return
|
|
294
|
+
return pageData
|
|
257
295
|
} catch (error) {
|
|
258
296
|
// Set load error - error is handled and exposed to UI via loadError
|
|
259
297
|
loadError.value = normalizeError(error)
|
|
260
298
|
throw error
|
|
261
299
|
} finally {
|
|
262
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
|
+
})
|
|
263
308
|
}
|
|
264
309
|
}
|
|
265
310
|
|
|
@@ -280,18 +325,18 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
|
|
|
280
325
|
if (cancelRequested.value) return
|
|
281
326
|
|
|
282
327
|
// Refresh mode: check if current page needs refreshing before loading next
|
|
283
|
-
if (
|
|
328
|
+
if (modeRef.value === 'refresh' && currentPage.value != null) {
|
|
284
329
|
const currentPageItemCount = countItemsForPage(currentPage.value)
|
|
285
330
|
|
|
286
331
|
// If current page has fewer items than pageSize, refresh it first
|
|
287
332
|
if (currentPageItemCount < pageSize) {
|
|
288
|
-
const
|
|
333
|
+
const pageData = await fetchWithRetry(() => getPage(currentPage.value, context?.value))
|
|
289
334
|
if (cancelRequested.value) return
|
|
290
335
|
|
|
291
336
|
// Get only new items that don't already exist
|
|
292
337
|
// We need to check against the current masonry state at this moment
|
|
293
338
|
const currentMasonrySnapshot = [...masonry.value]
|
|
294
|
-
const newItems =
|
|
339
|
+
const newItems = pageData.items.filter((item: any) => {
|
|
295
340
|
if (!item || item.id == null || item.page == null) return false
|
|
296
341
|
return !currentMasonrySnapshot.some((existing: any) => {
|
|
297
342
|
return existing && existing.id === item.id && existing.page === item.page
|
|
@@ -301,9 +346,14 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
|
|
|
301
346
|
// Append only new items to masonry (same pattern as getContent)
|
|
302
347
|
if (newItems.length > 0) {
|
|
303
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()
|
|
304
356
|
refreshLayout(updatedItems)
|
|
305
|
-
// Wait a tick for masonry to update
|
|
306
|
-
await new Promise(resolve => setTimeout(resolve, 0))
|
|
307
357
|
}
|
|
308
358
|
|
|
309
359
|
// Clear error on successful load
|
|
@@ -350,8 +400,8 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
|
|
|
350
400
|
await maybeBackfillToTarget(baseline)
|
|
351
401
|
return nextResponse
|
|
352
402
|
} else {
|
|
353
|
-
// Still not enough items, but we refreshed - return the refresh
|
|
354
|
-
return
|
|
403
|
+
// Still not enough items, but we refreshed - return the refresh pageData
|
|
404
|
+
return pageData
|
|
355
405
|
}
|
|
356
406
|
}
|
|
357
407
|
}
|
|
@@ -363,25 +413,31 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
|
|
|
363
413
|
hasReachedEnd.value = true
|
|
364
414
|
return
|
|
365
415
|
}
|
|
366
|
-
const
|
|
416
|
+
const pageData = await getContent(nextPageToLoad)
|
|
367
417
|
if (cancelRequested.value) return
|
|
368
418
|
// Clear error on successful load
|
|
369
419
|
loadError.value = null
|
|
370
420
|
currentPage.value = nextPageToLoad // Track the current page
|
|
371
|
-
paginationHistory.value.push(
|
|
421
|
+
paginationHistory.value.push(pageData.nextPage)
|
|
372
422
|
// Update hasReachedEnd if nextPage is null
|
|
373
|
-
if (
|
|
423
|
+
if (pageData.nextPage == null) {
|
|
374
424
|
hasReachedEnd.value = true
|
|
375
425
|
}
|
|
376
426
|
await maybeBackfillToTarget(baseline)
|
|
377
|
-
return
|
|
427
|
+
return pageData
|
|
378
428
|
} catch (error) {
|
|
379
429
|
// Set load error - error is handled and exposed to UI via loadError
|
|
380
430
|
loadError.value = normalizeError(error)
|
|
381
431
|
throw error
|
|
382
432
|
} finally {
|
|
383
433
|
isLoading.value = false
|
|
384
|
-
|
|
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
|
+
})
|
|
385
441
|
}
|
|
386
442
|
}
|
|
387
443
|
|
|
@@ -411,16 +467,16 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
|
|
|
411
467
|
paginationHistory.value = [pageToRefresh]
|
|
412
468
|
|
|
413
469
|
// Reload the current page
|
|
414
|
-
const
|
|
470
|
+
const pageData = await getContent(pageToRefresh)
|
|
415
471
|
if (cancelRequested.value) return
|
|
416
472
|
|
|
417
473
|
// Clear error on successful load
|
|
418
474
|
loadError.value = null
|
|
419
475
|
// Update pagination state
|
|
420
476
|
currentPage.value = pageToRefresh
|
|
421
|
-
paginationHistory.value.push(
|
|
477
|
+
paginationHistory.value.push(pageData.nextPage)
|
|
422
478
|
// Update hasReachedEnd if nextPage is null
|
|
423
|
-
if (
|
|
479
|
+
if (pageData.nextPage == null) {
|
|
424
480
|
hasReachedEnd.value = true
|
|
425
481
|
}
|
|
426
482
|
|
|
@@ -428,14 +484,20 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
|
|
|
428
484
|
const baseline = masonry.value.length
|
|
429
485
|
await maybeBackfillToTarget(baseline)
|
|
430
486
|
|
|
431
|
-
return
|
|
487
|
+
return pageData
|
|
432
488
|
} catch (error) {
|
|
433
489
|
// Set load error - error is handled and exposed to UI via loadError
|
|
434
490
|
loadError.value = normalizeError(error)
|
|
435
491
|
throw error
|
|
436
492
|
} finally {
|
|
437
493
|
isLoading.value = false
|
|
438
|
-
|
|
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
|
+
})
|
|
439
501
|
}
|
|
440
502
|
}
|
|
441
503
|
|
|
@@ -446,11 +508,23 @@ export function useMasonryPagination(options: UseMasonryPaginationOptions) {
|
|
|
446
508
|
// Set backfillActive to false to immediately stop backfilling
|
|
447
509
|
// The backfill loop checks this flag and will exit on the next iteration
|
|
448
510
|
backfillActive = false
|
|
511
|
+
const cancelCurrentPage = currentPage.value
|
|
512
|
+
const cancelNextPage = paginationHistory.value[paginationHistory.value.length - 1]
|
|
449
513
|
// If backfill was active, emit stop event immediately
|
|
450
514
|
if (wasBackfilling) {
|
|
451
|
-
emits('backfill:stop', {
|
|
515
|
+
emits('backfill:stop', {
|
|
516
|
+
fetched: masonry.value.length,
|
|
517
|
+
calls: 0,
|
|
518
|
+
cancelled: true,
|
|
519
|
+
currentPage: cancelCurrentPage,
|
|
520
|
+
nextPage: cancelNextPage
|
|
521
|
+
})
|
|
452
522
|
}
|
|
453
|
-
emits('loading:stop', {
|
|
523
|
+
emits('loading:stop', {
|
|
524
|
+
fetched: masonry.value.length,
|
|
525
|
+
currentPage: cancelCurrentPage,
|
|
526
|
+
nextPage: cancelNextPage
|
|
527
|
+
})
|
|
454
528
|
}
|
|
455
529
|
|
|
456
530
|
return {
|