@wyxos/vibe 1.2.11 → 1.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/App.vue +15 -3
- package/src/Masonry.vue +51 -17
- package/src/calculateLayout.js +3 -2
- package/src/calculateLayout.test.js +0 -48
package/package.json
CHANGED
package/src/App.vue
CHANGED
|
@@ -10,9 +10,21 @@ const masonry = ref(null)
|
|
|
10
10
|
const getPage = async (page) => {
|
|
11
11
|
return new Promise((resolve) => {
|
|
12
12
|
setTimeout(() => {
|
|
13
|
+
// Check if the page exists in the fixture
|
|
14
|
+
const pageData = fixture[page - 1];
|
|
15
|
+
|
|
16
|
+
if (!pageData) {
|
|
17
|
+
// Return empty items if page doesn't exist
|
|
18
|
+
resolve({
|
|
19
|
+
items: [],
|
|
20
|
+
nextPage: null // null indicates no more pages
|
|
21
|
+
});
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
let output = {
|
|
14
|
-
items:
|
|
15
|
-
nextPage: page + 1
|
|
26
|
+
items: pageData.items,
|
|
27
|
+
nextPage: page < fixture.length ? page + 1 : null
|
|
16
28
|
};
|
|
17
29
|
|
|
18
30
|
resolve(output)
|
|
@@ -35,7 +47,7 @@ const getPage = async (page) => {
|
|
|
35
47
|
<p>Loading: <span class="bg-blue-500 text-white p-2 rounded">{{ masonry.isLoading }}</span></p>
|
|
36
48
|
</div>
|
|
37
49
|
</header>
|
|
38
|
-
<masonry class="bg-blue-500 " v-model:items="items" :get-next-page="getPage" ref="masonry">
|
|
50
|
+
<masonry class="bg-blue-500 " v-model:items="items" :get-next-page="getPage" :load-at-page="1" ref="masonry">
|
|
39
51
|
<template #item="{item, onRemove}">
|
|
40
52
|
<img :src="item.src" class="w-full"/>
|
|
41
53
|
<button class="absolute bottom-0 right-0 bg-red-500 text-white p-2 rounded cursor-pointer" @click="onRemove(item)">
|
package/src/Masonry.vue
CHANGED
|
@@ -10,8 +10,8 @@ const props = defineProps({
|
|
|
10
10
|
}
|
|
11
11
|
},
|
|
12
12
|
loadAtPage: {
|
|
13
|
-
type: Number,
|
|
14
|
-
default:
|
|
13
|
+
type: [Number, String],
|
|
14
|
+
default: null
|
|
15
15
|
},
|
|
16
16
|
items: {
|
|
17
17
|
type: Array,
|
|
@@ -24,6 +24,10 @@ const props = defineProps({
|
|
|
24
24
|
type: String,
|
|
25
25
|
default: 'page', // or 'cursor'
|
|
26
26
|
validator: v => ['page', 'cursor'].includes(v)
|
|
27
|
+
},
|
|
28
|
+
skipInitialLoad: {
|
|
29
|
+
type: Boolean,
|
|
30
|
+
default: false
|
|
27
31
|
}
|
|
28
32
|
})
|
|
29
33
|
|
|
@@ -80,6 +84,7 @@ defineExpose({
|
|
|
80
84
|
refreshLayout,
|
|
81
85
|
containerHeight,
|
|
82
86
|
onRemove,
|
|
87
|
+
loadNext
|
|
83
88
|
})
|
|
84
89
|
|
|
85
90
|
async function onScroll() {
|
|
@@ -94,14 +99,31 @@ async function onScroll() {
|
|
|
94
99
|
isLoading.value = true
|
|
95
100
|
|
|
96
101
|
if (paginationHistory.value.length > 3) {
|
|
97
|
-
// get first item
|
|
102
|
+
// get first item - only proceed if it exists
|
|
98
103
|
const firstItem = masonry.value[0]
|
|
104
|
+
|
|
105
|
+
if (!firstItem) {
|
|
106
|
+
// Skip removal logic if there are no items
|
|
107
|
+
await loadNext()
|
|
108
|
+
await nextTick()
|
|
109
|
+
isLoading.value = false
|
|
110
|
+
return
|
|
111
|
+
}
|
|
99
112
|
|
|
100
113
|
// get page number
|
|
101
114
|
const page = firstItem.page
|
|
102
115
|
|
|
103
116
|
// find all item with this page
|
|
104
117
|
const removedItems = masonry.value.filter(i => i.page !== page)
|
|
118
|
+
|
|
119
|
+
// Only proceed with removal if there are actually items to remove
|
|
120
|
+
if (removedItems.length === masonry.value.length) {
|
|
121
|
+
// All items belong to the same page, skip removal logic
|
|
122
|
+
await loadNext()
|
|
123
|
+
await nextTick()
|
|
124
|
+
isLoading.value = false
|
|
125
|
+
return
|
|
126
|
+
}
|
|
105
127
|
|
|
106
128
|
refreshLayout(removedItems)
|
|
107
129
|
|
|
@@ -111,16 +133,20 @@ async function onScroll() {
|
|
|
111
133
|
|
|
112
134
|
// find the last item in that column
|
|
113
135
|
const lastItemInColumn = masonry.value.filter((_, index) => index % columns.value === lowestColumnIndex).pop()
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
container.value.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
136
|
+
|
|
137
|
+
// Only proceed with scroll adjustment if we have a valid item
|
|
138
|
+
if (lastItemInColumn) {
|
|
139
|
+
const lastItemInColumnTop = lastItemInColumn.top + lastItemInColumn.columnHeight
|
|
140
|
+
const lastItemInColumnBottom = lastItemInColumnTop + lastItemInColumn.columnHeight
|
|
141
|
+
const containerTop = container.value.scrollTop
|
|
142
|
+
const containerBottom = containerTop + container.value.clientHeight
|
|
143
|
+
const itemInView = lastItemInColumnTop >= containerTop && lastItemInColumnBottom <= containerBottom
|
|
144
|
+
if (!itemInView) {
|
|
145
|
+
container.value.scrollTo({
|
|
146
|
+
top: lastItemInColumnTop - 10,
|
|
147
|
+
behavior: 'smooth'
|
|
148
|
+
})
|
|
149
|
+
}
|
|
124
150
|
}
|
|
125
151
|
}
|
|
126
152
|
|
|
@@ -237,11 +263,19 @@ onMounted(async () => {
|
|
|
237
263
|
|
|
238
264
|
columns.value = getColumnCount()
|
|
239
265
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
266
|
+
// For cursor-based pagination, loadAtPage can be null for the first request
|
|
267
|
+
const initialPage = props.loadAtPage
|
|
268
|
+
paginationHistory.value = [initialPage]
|
|
243
269
|
|
|
244
|
-
|
|
270
|
+
// Skip initial load if skipInitialLoad prop is true
|
|
271
|
+
if (!props.skipInitialLoad) {
|
|
272
|
+
const response = await getContent(paginationHistory.value[0])
|
|
273
|
+
paginationHistory.value.push(response.nextPage)
|
|
274
|
+
} else {
|
|
275
|
+
await nextTick()
|
|
276
|
+
// Just refresh the layout with any existing items
|
|
277
|
+
refreshLayout(masonry.value)
|
|
278
|
+
}
|
|
245
279
|
|
|
246
280
|
isLoading.value = false
|
|
247
281
|
|
package/src/calculateLayout.js
CHANGED
|
@@ -56,11 +56,12 @@ export default function calculateLayout(items, container, columnCount, options =
|
|
|
56
56
|
|
|
57
57
|
const col = index % columnCount;
|
|
58
58
|
const originalWidth = item.width;
|
|
59
|
-
const originalHeight = item.height
|
|
59
|
+
const originalHeight = item.height;
|
|
60
60
|
|
|
61
61
|
newItem.columnWidth = columnWidth;
|
|
62
62
|
newItem.left = col * (columnWidth + gutterX);
|
|
63
|
-
newItem.
|
|
63
|
+
newItem.imageHeight = Math.round((columnWidth * originalHeight) / originalWidth);
|
|
64
|
+
newItem.columnHeight = newItem.imageHeight + footer + header;
|
|
64
65
|
newItem.top = columnHeights[col];
|
|
65
66
|
|
|
66
67
|
columnHeights[col] += newItem.columnHeight + gutterY;
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import calculateLayout from './calculateLayout'
|
|
3
|
-
import fixture from './pages.json'
|
|
4
|
-
|
|
5
|
-
describe('calculateLayout', () => {
|
|
6
|
-
it('should calculate correct layout positions for known inputs', () => {
|
|
7
|
-
const items = fixture[0].items.slice(0, 3) // first 3 items
|
|
8
|
-
const mockContainer = {
|
|
9
|
-
offsetWidth: 1000,
|
|
10
|
-
clientWidth: 980 // scrollbarWidth = 20
|
|
11
|
-
}
|
|
12
|
-
const columnCount = 3
|
|
13
|
-
const gutterX = 10
|
|
14
|
-
const gutterY = 10
|
|
15
|
-
|
|
16
|
-
const scrollbarWidth = mockContainer.offsetWidth - mockContainer.clientWidth
|
|
17
|
-
const usableWidth = mockContainer.offsetWidth - scrollbarWidth
|
|
18
|
-
const totalGutterX = gutterX * (columnCount - 1)
|
|
19
|
-
const expectedColumnWidth = Math.floor((usableWidth - totalGutterX) / columnCount)
|
|
20
|
-
|
|
21
|
-
const result = calculateLayout(items, mockContainer, columnCount, gutterX, gutterY)
|
|
22
|
-
|
|
23
|
-
expect(result.length).toBe(3)
|
|
24
|
-
|
|
25
|
-
result.forEach((item, i) => {
|
|
26
|
-
const original = items[i]
|
|
27
|
-
const expectedColumn = i % columnCount
|
|
28
|
-
const expectedLeft = expectedColumn * (expectedColumnWidth + gutterX)
|
|
29
|
-
const expectedHeight = Math.round((expectedColumnWidth * original.height) / original.width)
|
|
30
|
-
|
|
31
|
-
expect(item.left).toBe(expectedLeft)
|
|
32
|
-
expect(item.columnWidth).toBe(expectedColumnWidth)
|
|
33
|
-
expect(item.columnHeight).toBe(expectedHeight)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
// Test top stacking logic: second item in same column should be placed below with gutter
|
|
37
|
-
const secondBatch = fixture[0].items.slice(0, 6)
|
|
38
|
-
const stacked = calculateLayout(secondBatch, mockContainer, columnCount, gutterX, gutterY)
|
|
39
|
-
for (let col = 0; col < columnCount; col++) {
|
|
40
|
-
const colItems = stacked.filter((_, i) => i % columnCount === col)
|
|
41
|
-
for (let j = 1; j < colItems.length; j++) {
|
|
42
|
-
expect(colItems[j].top).toBe(
|
|
43
|
-
colItems[j - 1].top + colItems[j - 1].columnHeight + gutterY
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
})
|
|
48
|
-
})
|