124 lines
3.7 KiB
Vue
124 lines
3.7 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, onMounted, onUnmounted } from "vue";
|
|
import { config } from "../config";
|
|
import type { Component } from "vue";
|
|
|
|
const props = defineProps<{
|
|
currentPath: string;
|
|
}>();
|
|
|
|
const isVisible = ref(true);
|
|
const isScrolling = ref(false);
|
|
const currentClientPath = ref(props.currentPath);
|
|
|
|
const enabledNavigationItems = config.navigationItems.filter(
|
|
(item) => item.enabled !== false,
|
|
);
|
|
|
|
const activePath = computed(() => currentClientPath.value);
|
|
|
|
const normalizedPath = computed(() => {
|
|
const path = activePath.value;
|
|
return path.endsWith("/") && path.length > 1 ? path.slice(0, -1) : path;
|
|
});
|
|
|
|
const updatePath = () => {
|
|
if (typeof window !== "undefined") {
|
|
currentClientPath.value = window.location.pathname;
|
|
}
|
|
};
|
|
|
|
let lastScrollY = 0;
|
|
let ticking = false;
|
|
let scrollTimer: ReturnType<typeof setTimeout> | undefined;
|
|
|
|
const updateScroll = () => {
|
|
const currentScrollY = window.scrollY;
|
|
|
|
if (currentScrollY < 50) {
|
|
isVisible.value = true;
|
|
} else {
|
|
if (Math.abs(currentScrollY - lastScrollY) > 0) {
|
|
isVisible.value = currentScrollY < lastScrollY;
|
|
}
|
|
}
|
|
|
|
lastScrollY = currentScrollY;
|
|
ticking = false;
|
|
};
|
|
|
|
const onScroll = () => {
|
|
isScrolling.value = true;
|
|
|
|
if (scrollTimer) clearTimeout(scrollTimer);
|
|
scrollTimer = setTimeout(() => {
|
|
isScrolling.value = false;
|
|
}, 200);
|
|
|
|
if (!ticking) {
|
|
window.requestAnimationFrame(updateScroll);
|
|
ticking = true;
|
|
}
|
|
};
|
|
|
|
onMounted(() => {
|
|
updatePath();
|
|
lastScrollY = window.scrollY;
|
|
|
|
document.addEventListener("astro:page-load", updatePath);
|
|
document.addEventListener("astro:after-swap", updatePath);
|
|
window.addEventListener("popstate", updatePath);
|
|
window.addEventListener("scroll", onScroll, { passive: true });
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
document.removeEventListener("astro:page-load", updatePath);
|
|
document.removeEventListener("astro:after-swap", updatePath);
|
|
window.removeEventListener("popstate", updatePath);
|
|
window.removeEventListener("scroll", onScroll);
|
|
if (scrollTimer) clearTimeout(scrollTimer);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
class="fixed bottom-3 sm:bottom-4 left-1/2 transform -translate-x-1/2 z-20 transition-all duration-300"
|
|
:class="[
|
|
isScrolling ? 'opacity-30' : 'opacity-100',
|
|
isVisible ? 'translate-y-0' : 'translate-y-20',
|
|
]"
|
|
>
|
|
<div class="overflow-visible">
|
|
<ul
|
|
class="menu menu-horizontal bg-base-200 rounded-box border border-solid border-primary p-1.5 sm:p-2 flex flex-nowrap whitespace-nowrap"
|
|
>
|
|
<li
|
|
v-for="item in enabledNavigationItems"
|
|
:key="item.id"
|
|
class="mx-0.5 sm:mx-1"
|
|
>
|
|
<a
|
|
:href="item.path"
|
|
class="tooltip tooltip-top min-h-11 min-w-11 inline-flex items-center justify-center"
|
|
:class="{
|
|
'menu-active': item.isActive
|
|
? item.isActive(normalizedPath)
|
|
: normalizedPath === item.path,
|
|
}"
|
|
:aria-label="item.tooltip"
|
|
:data-tip="item.tooltip"
|
|
data-astro-prefetch="hover"
|
|
>
|
|
<component
|
|
:is="item.icon as Component"
|
|
:size="18"
|
|
class="sm:w-5 sm:h-5"
|
|
/>
|
|
<span class="sr-only">{{ item.name }}</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</template>
|