mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-21 21:33:18 +01:00
165 lines
3.5 KiB
Vue
165 lines
3.5 KiB
Vue
<template>
|
|
<section :class="{ 'is-full-height-scrollable': scrollable }">
|
|
<header v-if="$slots.header">
|
|
<slot name="header"></slot>
|
|
</header>
|
|
<main :data-scrolling="scrollable ? true : undefined">
|
|
<div class="is-scrollbar-progress is-hidden-mobile">
|
|
<scroll-progress v-show="paused" :indeterminate="loading" :auto-hide="!loading"></scroll-progress>
|
|
</div>
|
|
<div ref="scrollableContent">
|
|
<slot :setLoading="setLoading"></slot>
|
|
</div>
|
|
|
|
<div ref="scrollObserver" class="is-scroll-observer"></div>
|
|
</main>
|
|
|
|
<div class="is-scrollbar-notification">
|
|
<transition name="fade">
|
|
<button
|
|
class="button has-boxshadow"
|
|
:class="hasMore ? 'has-more' : ''"
|
|
@click="scrollToBottom()"
|
|
v-show="paused"
|
|
>
|
|
<mdi:light-chevron-double-down />
|
|
</button>
|
|
</transition>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
const { scrollable = false } = defineProps<{ scrollable?: boolean }>();
|
|
|
|
let paused = $ref(false);
|
|
let hasMore = $ref(false);
|
|
let loading = $ref(false);
|
|
const scrollObserver = ref<HTMLElement>();
|
|
const scrollableContent = ref<HTMLElement>();
|
|
|
|
provide("scrollingPaused", $$(paused));
|
|
|
|
const mutationObserver = new MutationObserver((e) => {
|
|
if (!paused) {
|
|
scrollToBottom();
|
|
} else {
|
|
const record = e[e.length - 1];
|
|
const children = (record.target as HTMLElement).children;
|
|
if (children[children.length - 1] == record.addedNodes[record.addedNodes.length - 1]) {
|
|
hasMore = true;
|
|
}
|
|
}
|
|
});
|
|
|
|
const intersectionObserver = new IntersectionObserver((entries) => (paused = entries[0].intersectionRatio == 0), {
|
|
threshold: [0, 1],
|
|
rootMargin: "80px 0px",
|
|
});
|
|
|
|
onMounted(() => {
|
|
mutationObserver.observe(scrollableContent.value!, { childList: true, subtree: true });
|
|
intersectionObserver.observe(scrollObserver.value!);
|
|
});
|
|
|
|
function scrollToBottom(behavior: "auto" | "smooth" = "auto") {
|
|
scrollObserver.value?.scrollIntoView({ behavior });
|
|
hasMore = false;
|
|
}
|
|
|
|
function setLoading(value: boolean) {
|
|
loading = value;
|
|
}
|
|
</script>
|
|
<style scoped lang="scss">
|
|
section {
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
header {
|
|
position: sticky;
|
|
top: 0;
|
|
background: var(--body-background-color);
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
z-index: 1;
|
|
}
|
|
|
|
&.is-full-height-scrollable {
|
|
height: 100vh;
|
|
min-height: 0;
|
|
}
|
|
|
|
main {
|
|
flex: 1;
|
|
overflow: auto;
|
|
scroll-snap-type: y proximity;
|
|
}
|
|
|
|
.is-scrollbar-progress {
|
|
text-align: right;
|
|
margin-right: 110px;
|
|
|
|
.scroll-progress {
|
|
position: fixed;
|
|
top: 60px;
|
|
z-index: 2;
|
|
}
|
|
}
|
|
|
|
.is-scroll-observer {
|
|
height: 1px;
|
|
}
|
|
|
|
.is-scrollbar-notification {
|
|
text-align: right;
|
|
margin-right: 65px;
|
|
|
|
button {
|
|
position: fixed;
|
|
bottom: 30px;
|
|
background-color: var(--primary-color);
|
|
transition: background-color 0.24s ease-out;
|
|
border: none !important;
|
|
color: #eee;
|
|
|
|
&.has-more {
|
|
background-color: var(--secondary-color);
|
|
animation-name: bounce;
|
|
animation-duration: 1000ms;
|
|
animation-fill-mode: both;
|
|
|
|
color: #222;
|
|
}
|
|
}
|
|
}
|
|
|
|
@keyframes bounce {
|
|
0%,
|
|
20%,
|
|
50%,
|
|
80%,
|
|
100% {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
40% {
|
|
transform: translateY(-30px);
|
|
}
|
|
|
|
60% {
|
|
transform: translateY(-15px);
|
|
}
|
|
}
|
|
|
|
.fade-enter-active,
|
|
.fade-leave-active {
|
|
transition: opacity 0.15s ease-in;
|
|
}
|
|
|
|
.fade-enter,
|
|
.fade-leave-to {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
</style>
|