220 lines
6.8 KiB
Vue
220 lines
6.8 KiB
Vue
<script lang="ts" setup>
|
|
// types
|
|
|
|
type Props = {
|
|
isOpen: boolean;
|
|
items: {
|
|
title: string;
|
|
subitems: {
|
|
title: string;
|
|
subitems: {
|
|
title: string;
|
|
link: string;
|
|
}[];
|
|
}[];
|
|
}[];
|
|
};
|
|
|
|
// props
|
|
|
|
const props = defineProps<Props>();
|
|
const { items, isOpen } = toRefs(props);
|
|
|
|
// emits
|
|
|
|
const emit = defineEmits(["update:isOpen"]);
|
|
|
|
// states
|
|
|
|
const selectedItem = ref(0);
|
|
const selectTabTimer = ref<NodeJS.Timeout | null>(null);
|
|
|
|
const isScrollLocked = useScrollLock(window);
|
|
|
|
const navbarEl = ref<HTMLElement | null>(null);
|
|
const { height: navbarHeight } = useElementSize(navbarEl);
|
|
|
|
const { $gsap: gsap, hooks } = useNuxtApp();
|
|
let gsapTimeline: gsap.core.Timeline;
|
|
|
|
const prevNavbarStyle = ref({
|
|
itemFilter: "",
|
|
background: "",
|
|
});
|
|
|
|
// computed
|
|
|
|
const selectedItemSubItems = computed(() => {
|
|
if (items.value.length > 0) {
|
|
return items.value[selectedItem.value].subitems;
|
|
}
|
|
|
|
return [];
|
|
});
|
|
|
|
// methods
|
|
|
|
const menuTabMouseEnter = (index: number) => {
|
|
if (selectTabTimer.value) clearTimeout(selectTabTimer.value);
|
|
selectTabTimer.value = setTimeout(() => {
|
|
selectedItem.value = index;
|
|
}, 150);
|
|
};
|
|
|
|
const menuTabMouseLeave = () => {
|
|
if (selectTabTimer.value) clearTimeout(selectTabTimer.value);
|
|
selectTabTimer.value = null;
|
|
};
|
|
|
|
const updatePrevStyle = () => {
|
|
const headerNavbarEl = document.querySelector<HTMLDivElement>("#header-navbar")!;
|
|
const headerNavbarItemEl = document.querySelector<HTMLDivElement>(".header-navbar-item")!;
|
|
|
|
prevNavbarStyle.value.background = getComputedStyle(headerNavbarEl).backgroundColor;
|
|
prevNavbarStyle.value.itemFilter = getComputedStyle(headerNavbarItemEl).filter;
|
|
};
|
|
|
|
const closeAfterNavigate = () => {
|
|
hooks.hookOnce("page:finish", () => {
|
|
emit("update:isOpen", false);
|
|
});
|
|
};
|
|
|
|
// watches
|
|
|
|
watch(
|
|
isOpen,
|
|
(newValue) => {
|
|
isScrollLocked.value = newValue;
|
|
|
|
if (!newValue) {
|
|
selectedItem.value = 0;
|
|
}
|
|
},
|
|
{
|
|
immediate: true,
|
|
}
|
|
);
|
|
|
|
watch(isOpen, async (newValue) => {
|
|
if (newValue) {
|
|
updatePrevStyle();
|
|
|
|
gsapTimeline
|
|
.to(".header-navbar-item", {
|
|
filter: "invert(0%)",
|
|
})
|
|
.to(
|
|
"#header-navbar",
|
|
{
|
|
background: "white",
|
|
},
|
|
"="
|
|
);
|
|
} else {
|
|
gsapTimeline
|
|
.fromTo(
|
|
".header-navbar-item",
|
|
{
|
|
filter: "invert(0%)",
|
|
},
|
|
{
|
|
filter: prevNavbarStyle.value.itemFilter,
|
|
}
|
|
)
|
|
.fromTo(
|
|
"#header-navbar",
|
|
{
|
|
background: "white",
|
|
},
|
|
{
|
|
background: prevNavbarStyle.value.background,
|
|
},
|
|
"="
|
|
);
|
|
}
|
|
});
|
|
|
|
// lifecycle
|
|
|
|
onMounted(() => {
|
|
gsapTimeline = gsap.timeline();
|
|
|
|
navbarEl.value = document.querySelector<HTMLDivElement>("#header-navbar");
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<Transition
|
|
name="fade"
|
|
mode="out-in"
|
|
>
|
|
<div
|
|
class="max-md:hidden fixed right-0 top-0 w-full z-1100 h-svh"
|
|
v-if="isOpen"
|
|
:style="{ marginTop: `${navbarHeight}px` }"
|
|
>
|
|
<div
|
|
class="absolute w-full h-full bg-black/50 flex justify-center"
|
|
@mouseenter="$emit('update:isOpen', false)"
|
|
/>
|
|
|
|
<div class="container relative z-10">
|
|
<div class="flex h-200 bg-white rounded-b-2xl border-t border-slate-200">
|
|
<div class="bg-slate-100 p-4 flex flex-col overflow-y-auto mask-b-from-90% pb-8">
|
|
<button
|
|
v-for="(item, index) in items"
|
|
:key="item.title"
|
|
class="text-black transition-all p-4 rounded-xl cursor-default text-start text-sm max-lg:text-xs"
|
|
:class="index === selectedItem ? 'bg-blue-100 text-blue-500' : 'bg-slate-100'"
|
|
:id="`mega-menu-tab-${index}`"
|
|
@mouseenter="menuTabMouseEnter(index)"
|
|
@click="selectedItem = index"
|
|
@mouseleave="menuTabMouseLeave"
|
|
>
|
|
{{ item.title }}
|
|
</button>
|
|
</div>
|
|
<div class="overflow-y-auto w-full mask-b-from-90% pb-8">
|
|
<Transition
|
|
name="fade"
|
|
mode="out-in"
|
|
:duration="150"
|
|
>
|
|
<div
|
|
:key="selectedItem"
|
|
class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 p-4 gap-2 text-back w-full h-fit"
|
|
:style="{
|
|
// gridTemplateRows: 'repeat(5, auto)',
|
|
// justifyContent: 'center',
|
|
}"
|
|
>
|
|
<template
|
|
v-for="mainItem in selectedItemSubItems"
|
|
:key="mainItem.title"
|
|
>
|
|
|
|
<span class="text-primary px-4 py-2 flex items-center gap-2 text-sm max-lg:text-xs font-semibold">
|
|
<span class="w-2 h-[3px] rounded-full bg-blue-400"> </span>
|
|
{{ mainItem.title }}
|
|
</span>
|
|
<NuxtLink
|
|
v-for="item in mainItem.subitems"
|
|
:to="{ name: 'products-slug', params: { slug: ['category', item.link] } }"
|
|
@click="closeAfterNavigate"
|
|
class="px-4 py-2 whitespace-nowrap h-fit text-slate-500/85 flex items-center justify-between hover:text-blue-500 hover:-translate-x-1.5 transition-all text-sm max-lg:text-xs"
|
|
>
|
|
<span class="truncate w-[90%]">
|
|
{{ item.title }}
|
|
</span>
|
|
</NuxtLink>
|
|
</template>
|
|
</div>
|
|
</Transition>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</template>
|