move to virtual scrolling
This commit is contained in:
parent
49ef3db243
commit
4b79a5af0f
9 changed files with 120 additions and 85 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<lenis root :options="{ duration: 1 }">
|
<lenis root :options="{ duration: 1, infinite: true, syncTouch: true }">
|
||||||
<div :class="classes" :style="styles" id="container">
|
<div :class="classes" :style="styles" id="container">
|
||||||
<suspense>
|
<suspense>
|
||||||
<layout />
|
<layout />
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="infinite-scroll-container">
|
<div class="infinite-scroll-container">
|
||||||
<div class="wrapper" :style="{ height: totalHeight + 'px' }">
|
<transition-group name="fade" tag="div" class="viewport-stage">
|
||||||
<!-- Render visible viewports -->
|
|
||||||
<div
|
<div
|
||||||
v-for="viewport in visibleViewports"
|
v-for="viewport in visibleViewports"
|
||||||
class="viewport"
|
class="viewport"
|
||||||
:style="{
|
:style="{
|
||||||
top: viewport.offsetY + 'px',
|
|
||||||
height: viewport.height + 'px',
|
height: viewport.height + 'px',
|
||||||
|
transform: `translateY(${viewport.offsetY - scrollY}px)`,
|
||||||
}"
|
}"
|
||||||
:key="viewport.index"
|
:key="viewport.index"
|
||||||
>
|
>
|
||||||
|
|
@ -25,11 +24,12 @@
|
||||||
</div>
|
</div>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { watch } from 'vue'
|
||||||
import { useInfiniteScroll } from '@/composables/useInfiniteScroll'
|
import { useInfiniteScroll } from '@/composables/useInfiniteScroll'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -74,11 +74,11 @@ const emit = defineEmits(['viewport-change', 'content-loaded'])
|
||||||
const {
|
const {
|
||||||
currentViewportIndex,
|
currentViewportIndex,
|
||||||
visibleViewports,
|
visibleViewports,
|
||||||
totalHeight,
|
|
||||||
scrollY,
|
scrollY,
|
||||||
scrollToViewport,
|
|
||||||
reset,
|
reset,
|
||||||
retryViewport,
|
retryViewport,
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
hasError,
|
hasError,
|
||||||
errors,
|
errors,
|
||||||
_state,
|
_state,
|
||||||
|
|
@ -93,43 +93,45 @@ const {
|
||||||
initialIndex: props.initialIndex,
|
initialIndex: props.initialIndex,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watch for viewport changes and emit event
|
|
||||||
import { watch } from 'vue'
|
|
||||||
watch(currentViewportIndex, (newIndex, oldIndex) => {
|
watch(currentViewportIndex, (newIndex, oldIndex) => {
|
||||||
emit('viewport-change', { newIndex, oldIndex })
|
emit('viewport-change', { newIndex, oldIndex })
|
||||||
})
|
})
|
||||||
|
|
||||||
// Expose methods to parent component
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
scrollToViewport,
|
|
||||||
reset,
|
reset,
|
||||||
retryViewport,
|
retryViewport,
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
currentViewportIndex,
|
currentViewportIndex,
|
||||||
visibleViewports,
|
visibleViewports,
|
||||||
totalHeight,
|
|
||||||
scrollY,
|
scrollY,
|
||||||
hasError,
|
hasError,
|
||||||
errors,
|
errors,
|
||||||
// Debug access
|
|
||||||
_state,
|
_state,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.infinite-scroll-container {
|
.infinite-scroll-container {
|
||||||
position: relative;
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
.wrapper {
|
.viewport-stage {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100vh;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.viewport {
|
.viewport {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
will-change: transform;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
</template async>
|
</template async>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { computed} from 'vue';
|
||||||
import { useSeoMeta } from '@unhead/vue'
|
import { useSeoMeta } from '@unhead/vue'
|
||||||
import { useStrapiGlobal } from '@/composables/useStrapi'
|
import { useStrapiGlobal } from '@/composables/useStrapi'
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
:bufferSize="2"
|
:bufferSize="2"
|
||||||
:initialIndex="0"
|
:initialIndex="0"
|
||||||
:generateContent="generateMosaicContent"
|
:generateContent="generateMosaicContent"
|
||||||
|
ref="scrollContainer"
|
||||||
>
|
>
|
||||||
<template #viewport="{ viewport, content }">
|
<template #viewport="{ viewport, content }">
|
||||||
<mosaic-viewport :viewport="viewport" :content="content" />
|
<mosaic-viewport :viewport="viewport" :content="content" />
|
||||||
|
|
@ -13,9 +14,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import _shuffle from 'lodash/shuffle'
|
import _shuffle from 'lodash/shuffle'
|
||||||
import { useMosaicLayout } from '@/composables/useMosaicLayout'
|
import { useMosaicLayout } from '@/composables/useMosaicLayout'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
items: Array,
|
items: Array,
|
||||||
|
|
@ -23,8 +25,10 @@ const props = defineProps({
|
||||||
|
|
||||||
const itemIndex = ref(0)
|
const itemIndex = ref(0)
|
||||||
const shuffledItems = ref(_shuffle(props.items))
|
const shuffledItems = ref(_shuffle(props.items))
|
||||||
|
const scrollContainer = ref(null)
|
||||||
|
|
||||||
const { generateLayout, populateCells } = useMosaicLayout()
|
const { generateLayout, populateCells } = useMosaicLayout()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
const generateCellContent = () => {
|
const generateCellContent = () => {
|
||||||
if (itemIndex.value >= shuffledItems.value.length - 1) {
|
if (itemIndex.value >= shuffledItems.value.length - 1) {
|
||||||
|
|
@ -61,6 +65,19 @@ const generateMosaicContent = async (index) => {
|
||||||
cells: populatedCells,
|
cells: populatedCells,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start/stop scroll when page is open
|
||||||
|
watch(
|
||||||
|
() => route.name,
|
||||||
|
() => {
|
||||||
|
if (route.name === 'mosaic-item') {
|
||||||
|
scrollContainer.value?.stop?.()
|
||||||
|
} else {
|
||||||
|
scrollContainer.value?.start?.()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ const props = defineProps({
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
background: var(--theme-bg);
|
||||||
|
|
||||||
&.layout-portrait {
|
&.layout-portrait {
|
||||||
.strapi-media {
|
.strapi-media {
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,8 @@ const props = defineProps({
|
||||||
strong {
|
strong {
|
||||||
@include h6;
|
@include h6;
|
||||||
}
|
}
|
||||||
p {
|
p,
|
||||||
|
a {
|
||||||
margin: desktop-vw(6px) 0;
|
margin: desktop-vw(6px) 0;
|
||||||
@include p-small;
|
@include p-small;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,11 @@ const props = defineProps({
|
||||||
background: var(--grey);
|
background: var(--grey);
|
||||||
border-radius: desktop-vw(2px);
|
border-radius: desktop-vw(2px);
|
||||||
padding: desktop-vw(20px);
|
padding: desktop-vw(20px);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
import { ref, reactive, computed, nextTick } from 'vue'
|
import gsap from 'gsap'
|
||||||
import { useWindowSize } from '@vueuse/core'
|
import { useWindowSize } from '@vueuse/core'
|
||||||
import useLenis from '@/composables/useLenis'
|
import useLenis from '@/composables/useLenis'
|
||||||
|
import { ref, reactive, computed, nextTick, onBeforeUnmount, watch } from 'vue'
|
||||||
|
|
||||||
// Window size for viewport calculations
|
// Window size for viewport calculations
|
||||||
const { height: wHeight } = useWindowSize()
|
const { height: wHeight } = useWindowSize()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bidirectional infinite scroll composable
|
* Bidirectional infinite virtual scroll composable
|
||||||
* Manages virtual viewports with on-demand content generation
|
* Manages virtual viewports with on-demand content generation.
|
||||||
|
* The page stays at 100vh — scroll position is tracked via Lenis
|
||||||
|
* virtual-scroll events and viewports are positioned with CSS transforms.
|
||||||
*
|
*
|
||||||
* @param {Object} options Configuration options
|
* @param {Object} options Configuration options
|
||||||
* @param {Function} options.generateContent Function to generate content for a viewport
|
* @param {Function} options.generateContent Function to generate content for a viewport
|
||||||
|
|
@ -32,8 +35,10 @@ export const useInfiniteScroll = (options = {}) => {
|
||||||
|
|
||||||
// Core state
|
// Core state
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
// Current scroll position
|
// Accumulated virtual scroll position (not the real DOM scroll)
|
||||||
scrollY: 0,
|
scrollY: initialIndex * (viewportHeight || 0),
|
||||||
|
// Active state to stop/start scroll
|
||||||
|
active: true,
|
||||||
// Index of the currently visible viewport
|
// Index of the currently visible viewport
|
||||||
currentViewportIndex: initialIndex,
|
currentViewportIndex: initialIndex,
|
||||||
// Map of loaded viewports { index: { content, element, height, offsetY } }
|
// Map of loaded viewports { index: { content, element, height, offsetY } }
|
||||||
|
|
@ -41,8 +46,6 @@ export const useInfiniteScroll = (options = {}) => {
|
||||||
// Loading states
|
// Loading states
|
||||||
isLoadingUp: false,
|
isLoadingUp: false,
|
||||||
isLoadingDown: false,
|
isLoadingDown: false,
|
||||||
// Total virtual height (for scrollbar)
|
|
||||||
totalHeight: 0,
|
|
||||||
// Error handling
|
// Error handling
|
||||||
errors: new Map(),
|
errors: new Map(),
|
||||||
hasError: false,
|
hasError: false,
|
||||||
|
|
@ -51,7 +54,9 @@ export const useInfiniteScroll = (options = {}) => {
|
||||||
|
|
||||||
// Initialize with starting viewport
|
// Initialize with starting viewport
|
||||||
const initializeViewports = async () => {
|
const initializeViewports = async () => {
|
||||||
// Create initial viewport
|
// Seed scrollY once vpHeight is available
|
||||||
|
state.scrollY = initialIndex * vpHeight.value
|
||||||
|
|
||||||
const initialViewport = {
|
const initialViewport = {
|
||||||
index: initialIndex,
|
index: initialIndex,
|
||||||
content: await generateContent(initialIndex),
|
content: await generateContent(initialIndex),
|
||||||
|
|
@ -61,15 +66,12 @@ export const useInfiniteScroll = (options = {}) => {
|
||||||
|
|
||||||
state.viewports.set(initialIndex, initialViewport)
|
state.viewports.set(initialIndex, initialViewport)
|
||||||
|
|
||||||
// Load buffer viewports above and below
|
|
||||||
await loadViewportsInRange(
|
await loadViewportsInRange(
|
||||||
initialIndex - bufferSize,
|
initialIndex - bufferSize,
|
||||||
initialIndex + bufferSize,
|
initialIndex + bufferSize,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Update positions and total height
|
|
||||||
updateViewportPositions()
|
updateViewportPositions()
|
||||||
updateTotalHeight()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load viewports in a range
|
// Load viewports in a range
|
||||||
|
|
@ -92,7 +94,6 @@ export const useInfiniteScroll = (options = {}) => {
|
||||||
const currentRetryCount = state.retryCount.get(index) || 0
|
const currentRetryCount = state.retryCount.get(index) || 0
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Set loading state based on direction
|
|
||||||
if (index < state.currentViewportIndex) {
|
if (index < state.currentViewportIndex) {
|
||||||
state.isLoadingUp = true
|
state.isLoadingUp = true
|
||||||
} else if (index > state.currentViewportIndex) {
|
} else if (index > state.currentViewportIndex) {
|
||||||
|
|
@ -109,7 +110,6 @@ export const useInfiniteScroll = (options = {}) => {
|
||||||
|
|
||||||
state.viewports.set(index, viewport)
|
state.viewports.set(index, viewport)
|
||||||
|
|
||||||
// Clear any previous errors for this viewport
|
|
||||||
state.errors.delete(index)
|
state.errors.delete(index)
|
||||||
state.retryCount.delete(index)
|
state.retryCount.delete(index)
|
||||||
state.hasError = state.errors.size > 0
|
state.hasError = state.errors.size > 0
|
||||||
|
|
@ -118,7 +118,6 @@ export const useInfiniteScroll = (options = {}) => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to load viewport ${index}:`, error)
|
console.error(`Failed to load viewport ${index}:`, error)
|
||||||
|
|
||||||
// Store error
|
|
||||||
state.errors.set(index, {
|
state.errors.set(index, {
|
||||||
message: error.message || 'Failed to load content',
|
message: error.message || 'Failed to load content',
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
|
|
@ -126,54 +125,26 @@ export const useInfiniteScroll = (options = {}) => {
|
||||||
})
|
})
|
||||||
state.hasError = true
|
state.hasError = true
|
||||||
|
|
||||||
// Retry logic
|
|
||||||
if (currentRetryCount < maxRetries && !isRetry) {
|
if (currentRetryCount < maxRetries && !isRetry) {
|
||||||
state.retryCount.set(index, currentRetryCount + 1)
|
state.retryCount.set(index, currentRetryCount + 1)
|
||||||
console.log(
|
|
||||||
`Retrying viewport ${index} (attempt ${currentRetryCount + 1}/${maxRetries})`,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Exponential backoff
|
|
||||||
const delay = Math.pow(2, currentRetryCount) * 1000
|
const delay = Math.pow(2, currentRetryCount) * 1000
|
||||||
setTimeout(() => {
|
setTimeout(() => loadViewport(index, true), delay)
|
||||||
loadViewport(index, true)
|
|
||||||
}, delay)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
} finally {
|
} finally {
|
||||||
// Clear loading states
|
|
||||||
state.isLoadingUp = false
|
state.isLoadingUp = false
|
||||||
state.isLoadingDown = false
|
state.isLoadingDown = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update viewport positions based on their order
|
// Update viewport positions based on their index
|
||||||
const updateViewportPositions = () => {
|
const updateViewportPositions = () => {
|
||||||
// Each viewport is positioned at index * viewport height
|
|
||||||
for (const [index, viewport] of state.viewports) {
|
for (const [index, viewport] of state.viewports) {
|
||||||
viewport.offsetY = index * vpHeight.value
|
viewport.offsetY = index * vpHeight.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update total virtual height for scrollbar
|
|
||||||
const updateTotalHeight = () => {
|
|
||||||
if (state.viewports.size === 0) {
|
|
||||||
state.totalHeight = wHeight.value * bufferSize // Minimum height
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const indexes = Array.from(state.viewports.keys())
|
|
||||||
// const minIndex = Math.min(...indexes)
|
|
||||||
const maxIndex = Math.max(...indexes)
|
|
||||||
|
|
||||||
// Calculate total height needed to accommodate all viewports
|
|
||||||
// Add extra buffer above and below for infinite scroll
|
|
||||||
const bufferViewports = 10
|
|
||||||
const totalRange = maxIndex + bufferViewports
|
|
||||||
state.totalHeight = totalRange * vpHeight.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove viewports outside buffer range
|
// Remove viewports outside buffer range
|
||||||
const cleanupViewports = () => {
|
const cleanupViewports = () => {
|
||||||
const currentIndex = state.currentViewportIndex
|
const currentIndex = state.currentViewportIndex
|
||||||
|
|
@ -185,37 +156,56 @@ export const useInfiniteScroll = (options = {}) => {
|
||||||
state.viewports.delete(index)
|
state.viewports.delete(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTotalHeight()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate which viewport should be visible based on scroll position
|
// Calculate which viewport should be visible based on virtual scroll position
|
||||||
const calculateVisibleViewport = (scrollY) => {
|
const calculateVisibleViewport = (scrollY) => {
|
||||||
return Math.floor(scrollY / vpHeight.value)
|
return Math.floor(scrollY / vpHeight.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle scroll events
|
// Handle virtual-scroll delta events from Lenis
|
||||||
const handleScroll = async (scrollData) => {
|
const onVirtualScroll = async ({ deltaY }) => {
|
||||||
state.scrollY = scrollData.scroll
|
if (!state.active) return
|
||||||
|
|
||||||
|
deltaY *= 3
|
||||||
|
|
||||||
|
gsap.to(state, {
|
||||||
|
scrollY: state.scrollY + deltaY,
|
||||||
|
duration: 1,
|
||||||
|
ease: 'expo.out',
|
||||||
|
})
|
||||||
|
|
||||||
const newViewportIndex = calculateVisibleViewport(state.scrollY)
|
const newViewportIndex = calculateVisibleViewport(state.scrollY)
|
||||||
|
|
||||||
if (newViewportIndex !== state.currentViewportIndex) {
|
if (newViewportIndex !== state.currentViewportIndex) {
|
||||||
state.currentViewportIndex = newViewportIndex
|
state.currentViewportIndex = newViewportIndex
|
||||||
|
|
||||||
// Load new viewports in range
|
|
||||||
const rangeStart = newViewportIndex - bufferSize
|
const rangeStart = newViewportIndex - bufferSize
|
||||||
const rangeEnd = newViewportIndex + bufferSize
|
const rangeEnd = newViewportIndex + bufferSize
|
||||||
|
|
||||||
await loadViewportsInRange(rangeStart, rangeEnd)
|
await loadViewportsInRange(rangeStart, rangeEnd)
|
||||||
|
|
||||||
// Cleanup old viewports
|
|
||||||
cleanupViewports()
|
cleanupViewports()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup scroll listener with Lenis
|
// Setup Lenis — we use virtual-scroll instead of scroll so the DOM
|
||||||
const lenis = useLenis(handleScroll)
|
// height can stay at 100vh with overflow: hidden.
|
||||||
|
const lenis = useLenis()
|
||||||
|
|
||||||
|
// Attach the virtual-scroll listener once Lenis is ready
|
||||||
|
watch(
|
||||||
|
lenis,
|
||||||
|
(instance) => {
|
||||||
|
if (instance) {
|
||||||
|
instance.on('virtual-scroll', onVirtualScroll)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
lenis.value?.off('virtual-scroll', onVirtualScroll)
|
||||||
|
})
|
||||||
|
|
||||||
// Get viewports to render (current + buffer)
|
// Get viewports to render (current + buffer)
|
||||||
const visibleViewports = computed(() => {
|
const visibleViewports = computed(() => {
|
||||||
|
|
@ -234,14 +224,6 @@ export const useInfiniteScroll = (options = {}) => {
|
||||||
return viewports.sort((a, b) => a.index - b.index)
|
return viewports.sort((a, b) => a.index - b.index)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Scroll to a specific viewport
|
|
||||||
const scrollToViewport = (index) => {
|
|
||||||
if (lenis.value) {
|
|
||||||
const targetY = index * vpHeight.value
|
|
||||||
lenis.value.scrollTo(targetY, { immediate: false })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retry failed viewport
|
// Retry failed viewport
|
||||||
const retryViewport = async (index) => {
|
const retryViewport = async (index) => {
|
||||||
state.errors.delete(index)
|
state.errors.delete(index)
|
||||||
|
|
@ -257,11 +239,22 @@ export const useInfiniteScroll = (options = {}) => {
|
||||||
state.retryCount.clear()
|
state.retryCount.clear()
|
||||||
state.hasError = false
|
state.hasError = false
|
||||||
state.currentViewportIndex = initialIndex
|
state.currentViewportIndex = initialIndex
|
||||||
|
state.scrollY = initialIndex * vpHeight.value
|
||||||
state.isLoadingUp = false
|
state.isLoadingUp = false
|
||||||
state.isLoadingDown = false
|
state.isLoadingDown = false
|
||||||
await initializeViewports()
|
await initializeViewports()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start/stop scrolling
|
||||||
|
const stop = () => {
|
||||||
|
state.active = false
|
||||||
|
|
||||||
|
gsap.killTweensOf(state)
|
||||||
|
}
|
||||||
|
const start = () => {
|
||||||
|
state.active = true
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize on creation
|
// Initialize on creation
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
initializeViewports()
|
initializeViewports()
|
||||||
|
|
@ -274,13 +267,14 @@ export const useInfiniteScroll = (options = {}) => {
|
||||||
visibleViewports,
|
visibleViewports,
|
||||||
isLoadingUp: computed(() => state.isLoadingUp),
|
isLoadingUp: computed(() => state.isLoadingUp),
|
||||||
isLoadingDown: computed(() => state.isLoadingDown),
|
isLoadingDown: computed(() => state.isLoadingDown),
|
||||||
totalHeight: computed(() => state.totalHeight),
|
|
||||||
scrollY: computed(() => state.scrollY),
|
scrollY: computed(() => state.scrollY),
|
||||||
|
vpHeight,
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
scrollToViewport,
|
|
||||||
reset,
|
reset,
|
||||||
retryViewport,
|
retryViewport,
|
||||||
|
stop,
|
||||||
|
start,
|
||||||
|
|
||||||
// Error state
|
// Error state
|
||||||
hasError: computed(() => state.hasError),
|
hasError: computed(() => state.hasError),
|
||||||
|
|
|
||||||
15
src/plugins/gsap.js
Normal file
15
src/plugins/gsap.js
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import gsap from 'gsap'
|
||||||
|
import Tempus from 'tempus'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install: () => {
|
||||||
|
gsap.defaults({ ease: 'none' })
|
||||||
|
|
||||||
|
// merge rafs
|
||||||
|
gsap.ticker.lagSmoothing(0)
|
||||||
|
gsap.ticker.remove(gsap.updateRoot)
|
||||||
|
Tempus?.add((time) => {
|
||||||
|
gsap.updateRoot(time / 1000)
|
||||||
|
}, 0)
|
||||||
|
},
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue