@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
package/src/router/index.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { createRouter, createWebHashHistory } from 'vue-router'
|
|
2
|
-
|
|
3
|
-
const router = createRouter({
|
|
4
|
-
history: createWebHashHistory(), // Hash mode works perfectly with GitHub Pages
|
|
5
|
-
routes: [
|
|
6
|
-
{
|
|
7
|
-
path: '/',
|
|
8
|
-
name: 'home',
|
|
9
|
-
component: () => import('../views/Home.vue')
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
path: '/examples',
|
|
13
|
-
name: 'examples',
|
|
14
|
-
component: () => import('../views/Examples.vue')
|
|
15
|
-
}
|
|
16
|
-
]
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
export default router
|
|
20
|
-
|
package/src/style.css
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
@import "tailwindcss";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
:root {
|
|
5
|
-
font-family: 'Inter', sans-serif;
|
|
6
|
-
-webkit-font-smoothing: antialiased;
|
|
7
|
-
-moz-osx-font-smoothing: grayscale;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
body {
|
|
11
|
-
background-color: #f8fafc;
|
|
12
|
-
color: #0f172a;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/* Custom Scrollbar */
|
|
16
|
-
::-webkit-scrollbar {
|
|
17
|
-
width: 8px;
|
|
18
|
-
height: 8px;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
::-webkit-scrollbar-track {
|
|
22
|
-
background: transparent;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
::-webkit-scrollbar-thumb {
|
|
26
|
-
background: #93c5fd;
|
|
27
|
-
border-radius: 4px;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
::-webkit-scrollbar-thumb:hover {
|
|
31
|
-
background: #60a5fa;
|
|
32
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
export type MasonryItem = {
|
|
2
|
-
id: string
|
|
3
|
-
width: number
|
|
4
|
-
height: number
|
|
5
|
-
page: number
|
|
6
|
-
index: number
|
|
7
|
-
src: string
|
|
8
|
-
// allow extra fields
|
|
9
|
-
[key: string]: any
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export type ProcessedMasonryItem = MasonryItem & {
|
|
13
|
-
columnWidth: number
|
|
14
|
-
imageHeight: number
|
|
15
|
-
columnHeight: number
|
|
16
|
-
left: number
|
|
17
|
-
top: number
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export type LayoutOptions = {
|
|
21
|
-
gutterX?: number
|
|
22
|
-
gutterY?: number
|
|
23
|
-
header?: number
|
|
24
|
-
footer?: number
|
|
25
|
-
paddingLeft?: number
|
|
26
|
-
paddingRight?: number
|
|
27
|
-
sizes?: {
|
|
28
|
-
base: number
|
|
29
|
-
sm?: number
|
|
30
|
-
md?: number
|
|
31
|
-
lg?: number
|
|
32
|
-
xl?: number
|
|
33
|
-
'2xl'?: number
|
|
34
|
-
}
|
|
35
|
-
placement?: 'masonry' | 'sequential-balanced'
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export type GetPageResult = { items: MasonryItem[]; nextPage: number | null }
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Type for the Masonry component instance (what's exposed via defineExpose)
|
|
42
|
-
* Use this type when accessing the component via template refs
|
|
43
|
-
*/
|
|
44
|
-
export interface MasonryInstance {
|
|
45
|
-
// Cancels any ongoing load operations (page loads, backfills, etc.)
|
|
46
|
-
cancelLoad: () => void
|
|
47
|
-
// Opaque caller context passed through to getPage(page, context)
|
|
48
|
-
context: any
|
|
49
|
-
// Container height (wrapper element) in pixels
|
|
50
|
-
containerHeight: number
|
|
51
|
-
// Container width (wrapper element) in pixels
|
|
52
|
-
containerWidth: number
|
|
53
|
-
// Current Tailwind breakpoint name (base, sm, md, lg, xl, 2xl) based on containerWidth
|
|
54
|
-
currentBreakpoint: string
|
|
55
|
-
// Current page number or cursor being displayed
|
|
56
|
-
currentPage: number | string | null
|
|
57
|
-
// Completely destroys the component, clearing all state and resetting to initial state
|
|
58
|
-
destroy: () => void
|
|
59
|
-
// Boolean indicating if the end of the list has been reached (no more pages to load)
|
|
60
|
-
hasReachedEnd: boolean
|
|
61
|
-
// Initializes the component with items, page, and next page cursor. Use this for manual init mode.
|
|
62
|
-
initialize: (items: MasonryItem[], page: number | string, next: number | string | null) => Promise<void> | void
|
|
63
|
-
// Boolean indicating if the component has been initialized (first content has loaded)
|
|
64
|
-
isInitialized: boolean
|
|
65
|
-
// Boolean indicating if a page load or backfill operation is currently in progress
|
|
66
|
-
isLoading: boolean
|
|
67
|
-
// Error object if the last load operation failed, null otherwise
|
|
68
|
-
loadError: Error | null
|
|
69
|
-
// Loads the next page of items asynchronously
|
|
70
|
-
loadNext: () => Promise<void>
|
|
71
|
-
// Loads a specific page number or cursor asynchronously
|
|
72
|
-
loadPage: (page: number | string) => Promise<void>
|
|
73
|
-
// Array tracking pagination history (pages/cursors that have been loaded)
|
|
74
|
-
paginationHistory: Array<number | string | null>
|
|
75
|
-
// Refreshes the current page by clearing items and reloading from the current page
|
|
76
|
-
refreshCurrentPage: () => Promise<void>
|
|
77
|
-
// Recalculates the layout positions for all items. Call this after manually modifying items.
|
|
78
|
-
refreshLayout: (items: MasonryItem[]) => void
|
|
79
|
-
// Removes a single item from the masonry
|
|
80
|
-
remove: (item: MasonryItem) => void
|
|
81
|
-
// Removes all items from the masonry
|
|
82
|
-
removeAll: () => void
|
|
83
|
-
// Removes multiple items from the masonry in a single operation
|
|
84
|
-
removeMany: (items: MasonryItem[]) => Promise<void> | void
|
|
85
|
-
// Resets the component to initial state (clears items, resets pagination, scrolls to top)
|
|
86
|
-
reset: () => void
|
|
87
|
-
// Restores a single item at its original index (useful for undo operations)
|
|
88
|
-
restore: (item: MasonryItem, index: number) => Promise<void> | void
|
|
89
|
-
// Restores multiple items at their original indices (useful for undo operations)
|
|
90
|
-
restoreMany: (items: MasonryItem[], indices: number[]) => Promise<void> | void
|
|
91
|
-
// Scrolls the container to a specific position
|
|
92
|
-
scrollTo: (position: number) => void
|
|
93
|
-
// Scrolls the container to the top
|
|
94
|
-
scrollToTop: () => void
|
|
95
|
-
// Sets the opaque caller context (alternative to v-model:context)
|
|
96
|
-
setContext: (val: any) => void
|
|
97
|
-
// Sets fixed dimensions for the container, overriding ResizeObserver. Pass null to restore automatic sizing.
|
|
98
|
-
setFixedDimensions: (dimensions: { width: number; height: number } | null) => void
|
|
99
|
-
// Computed property returning the total number of items currently in the masonry
|
|
100
|
-
totalItems: number
|
|
101
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { ref, type Ref } from 'vue'
|
|
2
|
-
|
|
3
|
-
export interface UseMasonryDimensionsOptions {
|
|
4
|
-
masonry: Ref<any[]>
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function useMasonryDimensions(options: UseMasonryDimensionsOptions) {
|
|
8
|
-
const { masonry } = options
|
|
9
|
-
|
|
10
|
-
// Track items with invalid dimensions to avoid duplicate warnings
|
|
11
|
-
const invalidDimensionIds = ref<Set<number | string>>(new Set())
|
|
12
|
-
|
|
13
|
-
function isPositiveNumber(value: any): boolean {
|
|
14
|
-
return typeof value === 'number' && value > 0 && Number.isFinite(value)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function checkItemDimensions(items: any[], context: string) {
|
|
18
|
-
try {
|
|
19
|
-
if (!Array.isArray(items) || items.length === 0) return
|
|
20
|
-
const missing = items.filter((item) => !isPositiveNumber(item?.width) || !isPositiveNumber(item?.height))
|
|
21
|
-
if (missing.length === 0) return
|
|
22
|
-
|
|
23
|
-
const newIds: Array<number | string> = []
|
|
24
|
-
for (const item of missing) {
|
|
25
|
-
const id = (item?.id as number | string | undefined) ?? `idx:${masonry.value.indexOf(item)}`
|
|
26
|
-
if (!invalidDimensionIds.value.has(id)) {
|
|
27
|
-
invalidDimensionIds.value.add(id)
|
|
28
|
-
newIds.push(id)
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
if (newIds.length > 0) {
|
|
32
|
-
const sample = newIds.slice(0, 10)
|
|
33
|
-
// eslint-disable-next-line no-console
|
|
34
|
-
console.warn(
|
|
35
|
-
'[Masonry] Items missing width/height detected:',
|
|
36
|
-
{
|
|
37
|
-
context,
|
|
38
|
-
count: newIds.length,
|
|
39
|
-
sampleIds: sample,
|
|
40
|
-
hint: 'Ensure each item has positive width and height. Consider providing fallbacks (e.g., 512x512) at the data layer.'
|
|
41
|
-
}
|
|
42
|
-
)
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
// best-effort diagnostics only
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function reset() {
|
|
50
|
-
invalidDimensionIds.value.clear()
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
checkItemDimensions,
|
|
55
|
-
invalidDimensionIds,
|
|
56
|
-
reset
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
package/src/useMasonryItems.ts
DELETED
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
import { nextTick, type Ref, type ComputedRef } from 'vue'
|
|
2
|
-
|
|
3
|
-
export interface UseMasonryItemsOptions {
|
|
4
|
-
masonry: Ref<any[]>
|
|
5
|
-
useSwipeMode: ComputedRef<boolean>
|
|
6
|
-
refreshLayout: (items: any[]) => void
|
|
7
|
-
refreshCurrentPage: () => Promise<any>
|
|
8
|
-
loadNext: () => Promise<any>
|
|
9
|
-
maybeBackfillToTarget: (baseline: number, force?: boolean) => Promise<void>
|
|
10
|
-
paginationHistory: Ref<any[]>
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function useMasonryItems(options: UseMasonryItemsOptions) {
|
|
14
|
-
const {
|
|
15
|
-
masonry,
|
|
16
|
-
useSwipeMode,
|
|
17
|
-
refreshLayout,
|
|
18
|
-
refreshCurrentPage,
|
|
19
|
-
loadNext,
|
|
20
|
-
maybeBackfillToTarget,
|
|
21
|
-
paginationHistory
|
|
22
|
-
} = options
|
|
23
|
-
|
|
24
|
-
// Batch remove operations to prevent visual glitches from rapid successive calls
|
|
25
|
-
let pendingRemoves = new Set<any>()
|
|
26
|
-
let removeTimeoutId: ReturnType<typeof setTimeout> | null = null
|
|
27
|
-
let isProcessingRemoves = false
|
|
28
|
-
|
|
29
|
-
async function processPendingRemoves() {
|
|
30
|
-
if (pendingRemoves.size === 0 || isProcessingRemoves) return
|
|
31
|
-
|
|
32
|
-
isProcessingRemoves = true
|
|
33
|
-
const itemsToRemove = Array.from(pendingRemoves)
|
|
34
|
-
pendingRemoves.clear()
|
|
35
|
-
removeTimeoutId = null
|
|
36
|
-
|
|
37
|
-
// Use removeManyInternal for batched removal (bypass batching to avoid recursion)
|
|
38
|
-
await removeManyInternal(itemsToRemove)
|
|
39
|
-
isProcessingRemoves = false
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function remove(item: any) {
|
|
43
|
-
// Add to pending removes
|
|
44
|
-
pendingRemoves.add(item)
|
|
45
|
-
|
|
46
|
-
// Clear existing timeout
|
|
47
|
-
if (removeTimeoutId) {
|
|
48
|
-
clearTimeout(removeTimeoutId)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Batch removes within a short time window (16ms = ~1 frame at 60fps)
|
|
52
|
-
removeTimeoutId = setTimeout(() => {
|
|
53
|
-
processPendingRemoves()
|
|
54
|
-
}, 16)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function removeManyInternal(items: any[]) {
|
|
58
|
-
if (!items || items.length === 0) return
|
|
59
|
-
const ids = new Set(items.map(i => i.id))
|
|
60
|
-
const next = masonry.value.filter(i => !ids.has(i.id))
|
|
61
|
-
masonry.value = next
|
|
62
|
-
await nextTick()
|
|
63
|
-
|
|
64
|
-
// If all items were removed, load next page
|
|
65
|
-
if (next.length === 0 && paginationHistory.value.length > 0) {
|
|
66
|
-
try {
|
|
67
|
-
await loadNext()
|
|
68
|
-
// Force backfill from 0 to ensure viewport is filled
|
|
69
|
-
await maybeBackfillToTarget(0, true)
|
|
70
|
-
} catch { }
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Commit DOM updates without forcing sync reflow
|
|
75
|
-
await nextTick()
|
|
76
|
-
// Start FLIP on next tick
|
|
77
|
-
await nextTick()
|
|
78
|
-
refreshLayout(next)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function removeMany(items: any[]) {
|
|
82
|
-
if (!items || items.length === 0) return
|
|
83
|
-
|
|
84
|
-
// Add all items to pending removes for batching
|
|
85
|
-
items.forEach(item => pendingRemoves.add(item))
|
|
86
|
-
|
|
87
|
-
// Clear existing timeout
|
|
88
|
-
if (removeTimeoutId) {
|
|
89
|
-
clearTimeout(removeTimeoutId)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Batch removes within a short time window (16ms = ~1 frame at 60fps)
|
|
93
|
-
removeTimeoutId = setTimeout(() => {
|
|
94
|
-
processPendingRemoves()
|
|
95
|
-
}, 16)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Restore a single item at its original index.
|
|
100
|
-
* This is useful for undo operations where an item needs to be restored to its exact position.
|
|
101
|
-
* Handles all index calculation and layout recalculation internally.
|
|
102
|
-
* @param item - Item to restore
|
|
103
|
-
* @param index - Original index of the item
|
|
104
|
-
*/
|
|
105
|
-
async function restore(item: any, index: number) {
|
|
106
|
-
if (!item) return
|
|
107
|
-
|
|
108
|
-
const current = masonry.value
|
|
109
|
-
const existingIndex = current.findIndex(i => i.id === item.id)
|
|
110
|
-
if (existingIndex !== -1) return // Item already exists
|
|
111
|
-
|
|
112
|
-
// Insert at the original index (clamped to valid range)
|
|
113
|
-
const newItems = [...current]
|
|
114
|
-
const targetIndex = Math.min(index, newItems.length)
|
|
115
|
-
newItems.splice(targetIndex, 0, item)
|
|
116
|
-
|
|
117
|
-
// Update the masonry array
|
|
118
|
-
masonry.value = newItems
|
|
119
|
-
await nextTick()
|
|
120
|
-
|
|
121
|
-
// Trigger layout recalculation (same pattern as remove)
|
|
122
|
-
if (!useSwipeMode.value) {
|
|
123
|
-
// Commit DOM updates without forcing sync reflow
|
|
124
|
-
await nextTick()
|
|
125
|
-
// Start FLIP on next tick
|
|
126
|
-
await nextTick()
|
|
127
|
-
refreshLayout(newItems)
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Restore multiple items at their original indices.
|
|
133
|
-
* This is useful for undo operations where items need to be restored to their exact positions.
|
|
134
|
-
* Handles all index calculation and layout recalculation internally.
|
|
135
|
-
* @param items - Array of items to restore
|
|
136
|
-
* @param indices - Array of original indices for each item (must match items array length)
|
|
137
|
-
*/
|
|
138
|
-
async function restoreMany(items: any[], indices: number[]) {
|
|
139
|
-
if (!items || items.length === 0) return
|
|
140
|
-
if (!indices || indices.length !== items.length) {
|
|
141
|
-
console.warn('[Masonry] restoreMany: items and indices arrays must have the same length')
|
|
142
|
-
return
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const current = masonry.value
|
|
146
|
-
const existingIds = new Set(current.map(i => i.id))
|
|
147
|
-
|
|
148
|
-
// Filter out items that already exist and pair with their indices
|
|
149
|
-
const itemsToRestore: Array<{ item: any; index: number }> = []
|
|
150
|
-
for (let i = 0; i < items.length; i++) {
|
|
151
|
-
if (!existingIds.has(items[i]?.id)) {
|
|
152
|
-
itemsToRestore.push({ item: items[i], index: indices[i] })
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (itemsToRestore.length === 0) return
|
|
157
|
-
|
|
158
|
-
// Build the final array by merging current items and restored items
|
|
159
|
-
// Strategy: Build position by position - for each position, decide if it should be
|
|
160
|
-
// a restored item (at its original index) or a current item (accounting for shifts)
|
|
161
|
-
|
|
162
|
-
// Create a map of restored items by their original index for O(1) lookup
|
|
163
|
-
const restoredByIndex = new Map<number, any>()
|
|
164
|
-
for (const { item, index } of itemsToRestore) {
|
|
165
|
-
restoredByIndex.set(index, item)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Find the maximum position we need to consider
|
|
169
|
-
const maxRestoredIndex = itemsToRestore.length > 0
|
|
170
|
-
? Math.max(...itemsToRestore.map(({ index }) => index))
|
|
171
|
-
: -1
|
|
172
|
-
const maxPosition = Math.max(current.length - 1, maxRestoredIndex)
|
|
173
|
-
|
|
174
|
-
// Build the final array position by position
|
|
175
|
-
// Key insight: Current array items are in "shifted" positions (missing the removed items).
|
|
176
|
-
// When we restore items at their original positions, current items naturally shift back.
|
|
177
|
-
// We can build the final array by iterating positions and using items sequentially.
|
|
178
|
-
const newItems: any[] = []
|
|
179
|
-
let currentArrayIndex = 0 // Track which current item we should use next
|
|
180
|
-
|
|
181
|
-
// Iterate through all positions up to the maximum we need
|
|
182
|
-
for (let position = 0; position <= maxPosition; position++) {
|
|
183
|
-
// If there's a restored item that belongs at this position, use it
|
|
184
|
-
if (restoredByIndex.has(position)) {
|
|
185
|
-
newItems.push(restoredByIndex.get(position)!)
|
|
186
|
-
} else {
|
|
187
|
-
// Otherwise, this position should be filled by the next current item
|
|
188
|
-
// Since current array is missing restored items, items are shifted left.
|
|
189
|
-
// By using them sequentially, they naturally end up in the correct positions.
|
|
190
|
-
if (currentArrayIndex < current.length) {
|
|
191
|
-
newItems.push(current[currentArrayIndex])
|
|
192
|
-
currentArrayIndex++
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Add any remaining current items that come after the last restored position
|
|
198
|
-
// (These are items that were originally after maxRestoredIndex)
|
|
199
|
-
while (currentArrayIndex < current.length) {
|
|
200
|
-
newItems.push(current[currentArrayIndex])
|
|
201
|
-
currentArrayIndex++
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Update the masonry array
|
|
205
|
-
masonry.value = newItems
|
|
206
|
-
await nextTick()
|
|
207
|
-
|
|
208
|
-
// Trigger layout recalculation (same pattern as removeMany)
|
|
209
|
-
if (!useSwipeMode.value) {
|
|
210
|
-
// Commit DOM updates without forcing sync reflow
|
|
211
|
-
await nextTick()
|
|
212
|
-
// Start FLIP on next tick
|
|
213
|
-
await nextTick()
|
|
214
|
-
refreshLayout(newItems)
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async function removeAll() {
|
|
219
|
-
// Clear all items
|
|
220
|
-
masonry.value = []
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return {
|
|
224
|
-
remove,
|
|
225
|
-
removeMany,
|
|
226
|
-
restore,
|
|
227
|
-
restoreMany,
|
|
228
|
-
removeAll
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
package/src/useMasonryLayout.ts
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
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
|
-
|