mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-21 13:23:07 +01:00
* Adds asset changes for i18n * Adds asset changes for i18n * Adds vite configs * Adds auto import and cleans up props * Initital localzation * Fixes dockerfile * Fixes tests * Updates fixutres * Updates default lang
159 lines
3.5 KiB
Vue
159 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" :class="hasMore ? 'has-more' : ''" @click="scrollToBottom()" v-show="paused">
|
|
<mdi-light-chevron-double-down />
|
|
</button>
|
|
</transition>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
withDefaults(
|
|
defineProps<{
|
|
scrollable: boolean;
|
|
}>(),
|
|
{
|
|
scrollable: false,
|
|
}
|
|
);
|
|
|
|
const paused = ref(false);
|
|
const hasMore = ref(false);
|
|
const loading = ref(false);
|
|
const scrollObserver = ref<HTMLElement>();
|
|
const scrollableContent = ref<HTMLElement>();
|
|
|
|
const mutationObserver = new MutationObserver((e) => {
|
|
if (!paused.value) {
|
|
scrollToBottom();
|
|
} else {
|
|
const record = e[e.length - 1];
|
|
if (record.target.children[record.target.children.length - 1] == record.addedNodes[record.addedNodes.length - 1]) {
|
|
hasMore.value = true;
|
|
}
|
|
}
|
|
});
|
|
|
|
const intersectionObserver = new IntersectionObserver((entries) => (paused.value = 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.value = false;
|
|
}
|
|
|
|
function setLoading(value: boolean) {
|
|
loading.value = 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(--secondary-color);
|
|
transition: background-color 1s ease-out;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
|
border: none !important;
|
|
color: #222;
|
|
|
|
&.has-more {
|
|
background-color: var(--primary-color);
|
|
animation-name: bounce;
|
|
animation-duration: 1000ms;
|
|
animation-fill-mode: both;
|
|
color: #fff;
|
|
}
|
|
}
|
|
}
|
|
|
|
@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>
|