@wyxos/vibe 1.2.1
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/LICENSE +21 -0
- package/README.md +124 -0
- package/index.js +9 -0
- package/package.json +39 -0
- package/src/App.vue +47 -0
- package/src/Masonry.vue +262 -0
- package/src/archive/App.vue +98 -0
- package/src/archive/InfiniteMansonry.spec.js +11 -0
- package/src/archive/InfiniteMasonry.vue +218 -0
- package/src/assets/vue.svg +1 -0
- package/src/calculateLayout.js +55 -0
- package/src/calculateLayout.test.js +48 -0
- package/src/main.js +5 -0
- package/src/pages.json +32502 -0
- package/src/style.css +2 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Wyxos
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# 🔷 VIBE — Vue Infinite Block Engine
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@wyxos/vibe)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://wyxos.github.io/vibe/)
|
|
6
|
+
|
|
7
|
+
A responsive, dynamic, infinite-scroll masonry layout engine for Vue 3.
|
|
8
|
+
Built for performance, flexibility, and pixel-perfect layout control.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## ✅ Features
|
|
13
|
+
|
|
14
|
+
- Responsive masonry layout that adapts to screen size
|
|
15
|
+
- Automatically loads more items as you scroll
|
|
16
|
+
- Supports removing and reflowing items with animation
|
|
17
|
+
- Keeps scroll position stable after layout updates
|
|
18
|
+
- Fully customizable item rendering
|
|
19
|
+
- Optimized for large datasets
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 📦 Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @wyxos/vibe
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 🚀 Usage
|
|
32
|
+
|
|
33
|
+
```vue
|
|
34
|
+
<script setup>
|
|
35
|
+
import Vibe from '@wyxos/vibe'
|
|
36
|
+
import { ref } from 'vue'
|
|
37
|
+
import fixture from './pages.json'
|
|
38
|
+
|
|
39
|
+
const items = ref([])
|
|
40
|
+
|
|
41
|
+
const getNextPage = async (page) => {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
resolve({
|
|
45
|
+
items: fixture[page - 1].items,
|
|
46
|
+
nextPage: page + 1
|
|
47
|
+
})
|
|
48
|
+
}, 1000)
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<Vibe v-model:items="items" :get-next-page="getNextPage">
|
|
55
|
+
<template #item="{ item, onRemove }">
|
|
56
|
+
<img :src="item.src" class="w-full" />
|
|
57
|
+
<button
|
|
58
|
+
class="absolute bottom-0 right-0 bg-red-500 text-white p-2 rounded cursor-pointer"
|
|
59
|
+
@click="onRemove(item)"
|
|
60
|
+
>
|
|
61
|
+
<i class="fas fa-trash"></i>
|
|
62
|
+
</button>
|
|
63
|
+
</template>
|
|
64
|
+
</Vibe>
|
|
65
|
+
</template>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## ⚙️ Props
|
|
71
|
+
|
|
72
|
+
| Prop | Type | Required | Description |
|
|
73
|
+
|--------------|----------|----------|-----------------------------------------------------------------------------|
|
|
74
|
+
| `items` | `Array` | ✅ | Two-way bound item array (each item must include `width`, `height`, `id`) |
|
|
75
|
+
| `getNextPage`| `Function(page: Number)` | ✅ | Async function to load the next page — returns `{ items, nextPage }` |
|
|
76
|
+
| `loadAtPage` | `Number` | ❌ | Starting page number (default: `1`) |
|
|
77
|
+
| `sizes` | `Object` | ❌ | Mobile-first column config (default: Tailwind-style breakpoints) |
|
|
78
|
+
| `gutterX` | `Number` | ❌ | Horizontal gutter between items (default: `10`) |
|
|
79
|
+
| `gutterY` | `Number` | ❌ | Vertical gutter between items (default: `10`) |
|
|
80
|
+
|
|
81
|
+
### `sizes` example:
|
|
82
|
+
```js
|
|
83
|
+
{
|
|
84
|
+
base: 1,
|
|
85
|
+
sm: 2,
|
|
86
|
+
md: 3,
|
|
87
|
+
lg: 4,
|
|
88
|
+
xl: 5,
|
|
89
|
+
'2xl': 6
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 💡 Slots
|
|
96
|
+
|
|
97
|
+
| Slot Name | Props | Description |
|
|
98
|
+
|-----------|--------------------------------|-----------------------------------|
|
|
99
|
+
| `item` | `{ item, onRemove }` | Custom rendering for each block |
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 🧪 Run Locally
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
git clone https://github.com/wyxos/vibe
|
|
107
|
+
cd vibe
|
|
108
|
+
npm install
|
|
109
|
+
npm run dev
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Visit [`http://localhost:5173`](http://localhost:5173)
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 🌐 Live Demo
|
|
117
|
+
|
|
118
|
+
👉 [View Demo on GitHub Pages](https://wyxos.github.io/vibe/)
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## 📄 License
|
|
123
|
+
|
|
124
|
+
MIT © [@wyxos](https://github.com/wyxos)
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wyxos/vibe",
|
|
3
|
+
"version": "1.2.1",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"module": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"src",
|
|
9
|
+
"index.js"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"dev": "vite",
|
|
13
|
+
"build": "vite build && node write-cname.js",
|
|
14
|
+
"preview": "vite preview",
|
|
15
|
+
"watch": "vite build --watch",
|
|
16
|
+
"release": "node release.mjs",
|
|
17
|
+
"test": "vitest"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"lodash": "^4.17.21",
|
|
21
|
+
"vue": "^3.0.0"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"vue": "^3.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@tailwindcss/vite": "^4.0.15",
|
|
28
|
+
"@vitejs/plugin-vue": "^5.2.1",
|
|
29
|
+
"@vue/test-utils": "^2.4.6",
|
|
30
|
+
"chalk": "^5.3.0",
|
|
31
|
+
"inquirer": "^10.1.8",
|
|
32
|
+
"jsdom": "^26.0.0",
|
|
33
|
+
"simple-git": "^3.27.0",
|
|
34
|
+
"tailwindcss": "^4.0.15",
|
|
35
|
+
"uuid": "^11.1.0",
|
|
36
|
+
"vite": "^6.2.0",
|
|
37
|
+
"vitest": "^3.0.9"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/App.vue
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import Masonry from "./Masonry.vue";
|
|
3
|
+
import {ref} from "vue";
|
|
4
|
+
import fixture from "./pages.json";
|
|
5
|
+
|
|
6
|
+
const items = ref([])
|
|
7
|
+
|
|
8
|
+
const getPage = async (index) => {
|
|
9
|
+
console.log('index', index)
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
setTimeout(() => {
|
|
12
|
+
let output = {
|
|
13
|
+
items: fixture[index - 1].items,
|
|
14
|
+
nextPage: index + 1
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
console.log('output', output)
|
|
18
|
+
resolve(output)
|
|
19
|
+
}, 1000)
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
<template>
|
|
24
|
+
<main class="flex flex-col items-center p-4 bg-slate-100 h-screen overflow-hidden">
|
|
25
|
+
<header class="sticky top-0 z-10 bg-slate-100 w-full p-4 flex flex-col items-center gap-4">
|
|
26
|
+
<h1 class="text-2xl font-semibold mb-4">VIBE</h1>
|
|
27
|
+
<p>Vue Infinite Block Engine</p>
|
|
28
|
+
|
|
29
|
+
<p class="text-sm text-gray-500 text-center mb-4">
|
|
30
|
+
🚀 Built by <a href="https://wyxos.com" target="_blank" class="underline hover:text-black">wyxos.com</a> •
|
|
31
|
+
💾 <a href="https://github.com/wyxos/vibe" target="_blank" class="underline hover:text-black">Source on GitHub</a>
|
|
32
|
+
</p>
|
|
33
|
+
</header>
|
|
34
|
+
<masonry v-model:items="items" :get-next-page="getPage">
|
|
35
|
+
<template #item="{item, onRemove}">
|
|
36
|
+
<img :src="item.src" class="w-full"/>
|
|
37
|
+
<button class="absolute bottom-0 right-0 bg-red-500 text-white p-2 rounded cursor-pointer" @click="onRemove(item)">
|
|
38
|
+
<i class="fas fa-trash"></i>
|
|
39
|
+
</button>
|
|
40
|
+
</template>
|
|
41
|
+
</masonry>
|
|
42
|
+
</main>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
package/src/Masonry.vue
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import {computed, nextTick, onMounted, onUnmounted, ref} from "vue";
|
|
3
|
+
import calculateLayout from "./calculateLayout.js";
|
|
4
|
+
import {debounce} from 'lodash'
|
|
5
|
+
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
getNextPage: {
|
|
8
|
+
type: Function,
|
|
9
|
+
default: () => {
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
loadAtPage: {
|
|
13
|
+
type: Number,
|
|
14
|
+
default: 1
|
|
15
|
+
},
|
|
16
|
+
items: {
|
|
17
|
+
type: Array,
|
|
18
|
+
default: () => []
|
|
19
|
+
},
|
|
20
|
+
sizes: {
|
|
21
|
+
type: Object,
|
|
22
|
+
default: () => ({
|
|
23
|
+
base: 1, // mobile-first default
|
|
24
|
+
sm: 2, // ≥ 640px
|
|
25
|
+
md: 3, // ≥ 768px
|
|
26
|
+
lg: 4, // ≥ 1024px
|
|
27
|
+
xl: 5, // ≥ 1280px
|
|
28
|
+
'2xl': 6 // ≥ 1536px
|
|
29
|
+
})
|
|
30
|
+
},
|
|
31
|
+
gutterX: {
|
|
32
|
+
type: Number,
|
|
33
|
+
default: 10
|
|
34
|
+
},
|
|
35
|
+
gutterY: {
|
|
36
|
+
type: Number,
|
|
37
|
+
default: 10
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const emits = defineEmits(['update:items'])
|
|
42
|
+
|
|
43
|
+
const masonry = computed({
|
|
44
|
+
get: () => props.items,
|
|
45
|
+
set: (val) => emits('update:items', val)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
const columns = ref(7)
|
|
50
|
+
|
|
51
|
+
const container = ref(null)
|
|
52
|
+
|
|
53
|
+
const currentPage = ref(null)
|
|
54
|
+
|
|
55
|
+
const nextPage = ref(null)
|
|
56
|
+
|
|
57
|
+
const isLoading = ref(false)
|
|
58
|
+
|
|
59
|
+
const containerHeight = ref(0)
|
|
60
|
+
|
|
61
|
+
const columnHeights = computed(() => {
|
|
62
|
+
const heights = new Array(columns.value).fill(0)
|
|
63
|
+
for (let i = 0; i < masonry.value.length; i++) {
|
|
64
|
+
const item = masonry.value[i]
|
|
65
|
+
const col = i % columns.value
|
|
66
|
+
heights[col] = Math.max(heights[col], item.top + item.columnHeight)
|
|
67
|
+
}
|
|
68
|
+
return heights
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
async function onScroll() {
|
|
72
|
+
const {scrollTop, clientHeight} = container.value
|
|
73
|
+
const visibleBottom = scrollTop + clientHeight
|
|
74
|
+
|
|
75
|
+
const whitespaceVisible = columnHeights.value.some(height => height + 300 < visibleBottom - 1)
|
|
76
|
+
|
|
77
|
+
if (whitespaceVisible && !isLoading.value) {
|
|
78
|
+
isLoading.value = true
|
|
79
|
+
|
|
80
|
+
if (currentPage.value > 3) {
|
|
81
|
+
// get first item
|
|
82
|
+
const firstItem = masonry.value[0]
|
|
83
|
+
|
|
84
|
+
// get page number
|
|
85
|
+
const page = firstItem.page
|
|
86
|
+
|
|
87
|
+
// find all item with this page
|
|
88
|
+
const removedItems = masonry.value.filter(i => i.page !== page)
|
|
89
|
+
|
|
90
|
+
refreshLayout(removedItems)
|
|
91
|
+
|
|
92
|
+
await nextTick()
|
|
93
|
+
|
|
94
|
+
const lowestColumnIndex = columnHeights.value.indexOf(Math.min(...columnHeights.value))
|
|
95
|
+
|
|
96
|
+
// find the last item in that column
|
|
97
|
+
const lastItemInColumn = masonry.value.filter((_, index) => index % columns.value === lowestColumnIndex).pop()
|
|
98
|
+
const lastItemInColumnTop = lastItemInColumn.top
|
|
99
|
+
const lastItemInColumnBottom = lastItemInColumnTop + lastItemInColumn.columnHeight
|
|
100
|
+
const containerTop = container.value.scrollTop
|
|
101
|
+
const containerBottom = containerTop + container.value.clientHeight
|
|
102
|
+
const itemInView = lastItemInColumnTop >= containerTop && lastItemInColumnBottom <= containerBottom
|
|
103
|
+
if (!itemInView) {
|
|
104
|
+
container.value.scrollTo({
|
|
105
|
+
top: lastItemInColumnTop - 10,
|
|
106
|
+
behavior: 'smooth'
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await loadNext()
|
|
112
|
+
|
|
113
|
+
await nextTick()
|
|
114
|
+
|
|
115
|
+
isLoading.value = false
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getColumnCount() {
|
|
120
|
+
const width = window.innerWidth
|
|
121
|
+
|
|
122
|
+
if (width >= 1536 && props.sizes['2xl']) return props.sizes['2xl']
|
|
123
|
+
if (width >= 1280 && props.sizes.xl) return props.sizes.xl
|
|
124
|
+
if (width >= 1024 && props.sizes.lg) return props.sizes.lg
|
|
125
|
+
if (width >= 768 && props.sizes.md) return props.sizes.md
|
|
126
|
+
if (width >= 640 && props.sizes.sm) return props.sizes.sm
|
|
127
|
+
return props.sizes.base
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function calculateHeight(layout) {
|
|
131
|
+
containerHeight.value = layout.reduce((acc, item) => {
|
|
132
|
+
return Math.max(acc, item.top + item.columnHeight)
|
|
133
|
+
}, 0)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function refreshLayout(items){
|
|
137
|
+
const layout = calculateLayout(items, container.value, columns.value, props.gutterX, props.gutterY);
|
|
138
|
+
|
|
139
|
+
calculateHeight(layout)
|
|
140
|
+
|
|
141
|
+
masonry.value = layout
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function getContent(page) {
|
|
145
|
+
const response = await props.getNextPage(page)
|
|
146
|
+
|
|
147
|
+
refreshLayout([...masonry.value, ...response.items])
|
|
148
|
+
|
|
149
|
+
return response
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function loadNext() {
|
|
153
|
+
const response = await getContent(nextPage.value)
|
|
154
|
+
currentPage.value = nextPage.value
|
|
155
|
+
nextPage.value = response.nextPage
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const getItemStyle = (item) => {
|
|
159
|
+
return {
|
|
160
|
+
top: `${item.top}px`,
|
|
161
|
+
left: `${item.left}px`,
|
|
162
|
+
width: `${item.columnWidth}px`,
|
|
163
|
+
height: `${item.columnHeight}px`
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function onRemove(item) {
|
|
168
|
+
refreshLayout(masonry.value.filter(i => i.id !== item.id))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function onEnter(el, done) {
|
|
172
|
+
// set top to data-top
|
|
173
|
+
const top = el.dataset.top
|
|
174
|
+
requestAnimationFrame(() => {
|
|
175
|
+
el.style.top = `${top}px`
|
|
176
|
+
done()
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function onBeforeEnter(el) {
|
|
181
|
+
// set top to last item + 500
|
|
182
|
+
const lastItem = masonry.value[masonry.value.length - 1]
|
|
183
|
+
if (lastItem) {
|
|
184
|
+
const lastTop = lastItem.top + lastItem.columnHeight + 10
|
|
185
|
+
el.style.top = `${lastTop}px`
|
|
186
|
+
} else {
|
|
187
|
+
el.style.top = '0px'
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function onBeforeLeave(el) {
|
|
192
|
+
// Ensure it's at its current position before animating
|
|
193
|
+
el.style.transition = 'none'
|
|
194
|
+
el.style.top = `${el.offsetTop}px`
|
|
195
|
+
void el.offsetWidth // force reflow to flush style
|
|
196
|
+
el.style.transition = '' // allow transition to apply again
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function onLeave(el, done) {
|
|
200
|
+
el.style.top = '-600px'
|
|
201
|
+
el.style.opacity = '0'
|
|
202
|
+
el.addEventListener('transitionend', done)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function itemAttributes(item) {
|
|
206
|
+
return {
|
|
207
|
+
style: getItemStyle(item),
|
|
208
|
+
'data-top': item.top,
|
|
209
|
+
'data-id': `${item.page}-${item.id}`,
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function onResize() {
|
|
214
|
+
columns.value = getColumnCount()
|
|
215
|
+
refreshLayout(masonry.value)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
onMounted(async () => {
|
|
219
|
+
isLoading.value = true
|
|
220
|
+
|
|
221
|
+
columns.value = getColumnCount()
|
|
222
|
+
|
|
223
|
+
currentPage.value = props.loadAtPage
|
|
224
|
+
|
|
225
|
+
const response = await getContent(currentPage.value)
|
|
226
|
+
|
|
227
|
+
nextPage.value = response.nextPage
|
|
228
|
+
|
|
229
|
+
isLoading.value = false
|
|
230
|
+
|
|
231
|
+
container.value?.addEventListener('scroll', debounce(onScroll, 200));
|
|
232
|
+
|
|
233
|
+
window.addEventListener('resize', debounce(onResize, 200));
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
onUnmounted(() => {
|
|
237
|
+
container.value?.removeEventListener('scroll', debounce(onScroll, 200));
|
|
238
|
+
|
|
239
|
+
window.removeEventListener('resize', debounce(onResize, 200));
|
|
240
|
+
})
|
|
241
|
+
</script>
|
|
242
|
+
|
|
243
|
+
<template>
|
|
244
|
+
<div class="overflow-auto bg-blue-500 w-full flex-1" ref="container">
|
|
245
|
+
<div class="relative" :style="{height: `${containerHeight}px`}">
|
|
246
|
+
<transition-group :css="false" @enter="onEnter" @before-enter="onBeforeEnter"
|
|
247
|
+
@leave="onLeave"
|
|
248
|
+
@before-leave="onBeforeLeave">
|
|
249
|
+
<div v-for="item in masonry" :key="`${item.page}-${item.id}`"
|
|
250
|
+
class="bg-slate-200 absolute transition-[top,left,opacity] duration-500 ease-in-out"
|
|
251
|
+
v-bind="itemAttributes(item)">
|
|
252
|
+
<slot name="item" v-bind="{item, onRemove}">
|
|
253
|
+
<img :src="item.src" class="w-full"/>
|
|
254
|
+
<button class="absolute bottom-0 right-0 bg-red-500 text-white p-2 rounded cursor-pointer" @click="onRemove(item)">
|
|
255
|
+
<i class="fas fa-trash"></i>
|
|
256
|
+
</button>
|
|
257
|
+
</slot>
|
|
258
|
+
</div>
|
|
259
|
+
</transition-group>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
</template>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<!--<script setup>-->
|
|
2
|
+
<!--import InfiniteMasonry from "./components/InfiniteMasonry.vue";-->
|
|
3
|
+
<!--import {nextTick, onMounted, ref} from "vue";-->
|
|
4
|
+
<!--import pages from './pages.json'-->
|
|
5
|
+
|
|
6
|
+
<!--const scrollDetails = ref({-->
|
|
7
|
+
<!-- position: 0,-->
|
|
8
|
+
<!-- direction: 'down',-->
|
|
9
|
+
<!-- isEnd: false,-->
|
|
10
|
+
<!-- isStart: true,-->
|
|
11
|
+
<!-- hasShortColumn: false,-->
|
|
12
|
+
<!--});-->
|
|
13
|
+
|
|
14
|
+
<!--const items = ref([]);-->
|
|
15
|
+
|
|
16
|
+
<!--const count = ref(30);-->
|
|
17
|
+
|
|
18
|
+
<!--const scroller = ref();-->
|
|
19
|
+
|
|
20
|
+
<!--const pageIndex = ref(0);-->
|
|
21
|
+
|
|
22
|
+
<!--const isLoading = ref(false);-->
|
|
23
|
+
|
|
24
|
+
<!--const updateItems = (action) => {-->
|
|
25
|
+
<!-- setTimeout(async () => {-->
|
|
26
|
+
<!-- if (action === 'add') {-->
|
|
27
|
+
<!-- if(items.value.length > 3){-->
|
|
28
|
+
<!-- // remove the first page from items-->
|
|
29
|
+
<!-- items.value.splice(0, 1);-->
|
|
30
|
+
<!-- }-->
|
|
31
|
+
|
|
32
|
+
<!-- items.value.push(pages[pageIndex.value]);-->
|
|
33
|
+
|
|
34
|
+
<!-- pageIndex.value = pageIndex.value + 1;-->
|
|
35
|
+
|
|
36
|
+
<!-- console.log(pageIndex.value)-->
|
|
37
|
+
|
|
38
|
+
<!-- await nextTick()-->
|
|
39
|
+
|
|
40
|
+
<!-- isLoading.value = false;-->
|
|
41
|
+
<!-- } else {-->
|
|
42
|
+
<!-- items.value.splice(-count.value);-->
|
|
43
|
+
|
|
44
|
+
<!-- isLoading.value = false;-->
|
|
45
|
+
<!-- }-->
|
|
46
|
+
<!-- }, 1000)-->
|
|
47
|
+
<!--}-->
|
|
48
|
+
|
|
49
|
+
<!--const onScroll = (attributes) => {-->
|
|
50
|
+
<!-- scrollDetails.value = attributes-->
|
|
51
|
+
|
|
52
|
+
<!-- if (autoLoad.value && attributes.hasShortColumn && !isLoading.value) {-->
|
|
53
|
+
<!-- isLoading.value = true;-->
|
|
54
|
+
|
|
55
|
+
<!-- updateItems('add');-->
|
|
56
|
+
<!-- }-->
|
|
57
|
+
<!--}-->
|
|
58
|
+
|
|
59
|
+
<!--const autoLoad = ref(true);-->
|
|
60
|
+
|
|
61
|
+
<!--onMounted(async () => {-->
|
|
62
|
+
<!-- setTimeout(() => {-->
|
|
63
|
+
<!-- items.value = [pages[pageIndex.value]]-->
|
|
64
|
+
|
|
65
|
+
<!-- pageIndex.value = pageIndex.value + 1;-->
|
|
66
|
+
<!-- }, 2000)-->
|
|
67
|
+
<!--})-->
|
|
68
|
+
<!--</script>-->
|
|
69
|
+
|
|
70
|
+
<!--<template>-->
|
|
71
|
+
<!-- <main class="flex flex-col items-center p-4 bg-slate-100 h-screen overflow-hidden">-->
|
|
72
|
+
<!-- <header class="sticky top-0 z-10 bg-slate-100 w-full p-4 flex flex-col items-center gap-4">-->
|
|
73
|
+
<!-- <h1 class="text-2xl font-semibold mb-4">Vue Infinite Masonry</h1>-->
|
|
74
|
+
|
|
75
|
+
<!-- <p class="text-sm text-gray-500 text-center mb-4">-->
|
|
76
|
+
<!-- 🚀 Built by <a href="https://wyxos.com" target="_blank" class="underline hover:text-black">wyxos.com</a> •-->
|
|
77
|
+
<!-- 💾 <a href="https://github.com/wyxos/vue-infinite-masonry" target="_blank" class="underline hover:text-black">Source on GitHub</a>-->
|
|
78
|
+
<!-- </p>-->
|
|
79
|
+
|
|
80
|
+
<!-- <div class="flex flex-col md:flex-row gap-4 items-center">-->
|
|
81
|
+
<!-- <p>Scroll {{ scrollDetails }}</p>-->
|
|
82
|
+
|
|
83
|
+
<!-- <p>Page: {{ pageIndex }}</p>-->
|
|
84
|
+
|
|
85
|
+
<!-- <p>Pages in array {{ items.length }}</p>-->
|
|
86
|
+
|
|
87
|
+
<!-- </div>-->
|
|
88
|
+
<!-- </header>-->
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
<!-- <infinite-masonry-->
|
|
92
|
+
<!-- ref="scroller"-->
|
|
93
|
+
<!-- v-model="items"-->
|
|
94
|
+
<!-- @scroll="onScroll"-->
|
|
95
|
+
<!-- :options="{ gutterY: 50 }"-->
|
|
96
|
+
<!-- />-->
|
|
97
|
+
<!-- </main>-->
|
|
98
|
+
<!--</template>-->
|