2024-05-30 00:42:53 +02:00
|
|
|
<script setup lang="ts">
|
|
|
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
2021-10-01 17:55:59 +02:00
|
|
|
|
2024-05-30 00:42:53 +02:00
|
|
|
const props = withDefaults(
|
|
|
|
defineProps<{
|
|
|
|
unrender?: boolean
|
|
|
|
minHeight?: number
|
|
|
|
renderDelay?: number
|
|
|
|
unrenderDelay?: number
|
|
|
|
rootMargin?: string
|
|
|
|
}>(),
|
|
|
|
{
|
|
|
|
unrender: true,
|
|
|
|
minHeight: 0,
|
|
|
|
renderDelay: 100,
|
|
|
|
unrenderDelay: 2000,
|
|
|
|
rootMargin: '300px',
|
2021-10-01 17:55:59 +02:00
|
|
|
},
|
2024-05-30 00:42:53 +02:00
|
|
|
)
|
2021-10-01 17:55:59 +02:00
|
|
|
|
2024-05-30 00:42:53 +02:00
|
|
|
defineSlots<{
|
|
|
|
default: any
|
|
|
|
}>()
|
2021-10-01 17:55:59 +02:00
|
|
|
|
2024-05-30 00:42:53 +02:00
|
|
|
const observer = ref<IntersectionObserver | null>(null)
|
|
|
|
const render = ref(false)
|
|
|
|
const fixedMinHeight = ref(props.minHeight)
|
|
|
|
const rootElem = ref<HTMLDivElement | null>(null)
|
2021-11-04 17:12:09 +01:00
|
|
|
|
2024-05-30 00:42:53 +02:00
|
|
|
onMounted(() => {
|
|
|
|
let unrenderTimer: NodeJS.Timeout
|
|
|
|
let renderTimer: NodeJS.Timeout
|
2021-11-04 17:12:09 +01:00
|
|
|
|
2024-05-30 00:42:53 +02:00
|
|
|
observer.value = new IntersectionObserver(
|
|
|
|
(entries) => {
|
|
|
|
let intersecting = entries[0].isIntersecting
|
|
|
|
|
|
|
|
// Fix for weird bug when typing fast in app search or on slow client.
|
|
|
|
// Intersection is triggered but even if the element is indeed in the viewport,
|
|
|
|
// isIntersecting is `false`, so we have to manually check this…
|
|
|
|
// FIXME Would be great to find out why this is happening
|
|
|
|
if (!intersecting && rootElem.value!.offsetTop < window.innerHeight) {
|
|
|
|
intersecting = true
|
|
|
|
}
|
2021-11-04 17:12:09 +01:00
|
|
|
|
2024-05-30 00:42:53 +02:00
|
|
|
if (intersecting) {
|
|
|
|
clearTimeout(unrenderTimer)
|
|
|
|
// Show the component after a delay (to avoid rendering while scrolling fast)
|
|
|
|
renderTimer = setTimeout(
|
|
|
|
() => {
|
|
|
|
render.value = true
|
|
|
|
},
|
|
|
|
props.unrender ? props.renderDelay : 0,
|
|
|
|
)
|
2021-10-01 17:55:59 +02:00
|
|
|
|
2024-05-30 00:42:53 +02:00
|
|
|
if (!props.unrender) {
|
|
|
|
// Stop listening to intersections after first appearance if unrendering is not activated
|
|
|
|
this.observer.disconnect()
|
2021-10-01 17:55:59 +02:00
|
|
|
}
|
2024-05-30 00:42:53 +02:00
|
|
|
} else if (props.unrender) {
|
|
|
|
clearTimeout(renderTimer)
|
|
|
|
// Hide the component after a delay if it's no longer in the viewport
|
|
|
|
unrenderTimer = setTimeout(() => {
|
|
|
|
fixedMinHeight.value = rootElem.value!.clientHeight
|
|
|
|
render.value = false
|
|
|
|
}, props.unrenderDelay)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{ rootMargin: props.rootMargin },
|
|
|
|
)
|
2021-10-01 17:55:59 +02:00
|
|
|
|
2024-05-30 00:42:53 +02:00
|
|
|
observer.value.observe(rootElem.value!)
|
|
|
|
})
|
2021-10-01 17:55:59 +02:00
|
|
|
|
2024-05-30 00:42:53 +02:00
|
|
|
onBeforeUnmount(() => {
|
|
|
|
observer.value!.disconnect()
|
|
|
|
})
|
2021-10-01 17:55:59 +02:00
|
|
|
</script>
|
2024-05-30 00:42:53 +02:00
|
|
|
|
|
|
|
<template>
|
|
|
|
<div
|
|
|
|
ref="rootElem"
|
|
|
|
class="lazy-renderer"
|
|
|
|
:style="`min-height: ${fixedMinHeight}px`"
|
|
|
|
>
|
|
|
|
<slot v-if="render" />
|
|
|
|
</div>
|
|
|
|
</template>
|