feat(component): add side menu component
This component can track position of element within scrollable element.
This commit is contained in:
parent
aa80106d09
commit
7533ea8268
1 changed files with 119 additions and 0 deletions
119
src/components/SideMenu.vue
Normal file
119
src/components/SideMenu.vue
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<script lang="ts" setup>
|
||||
import { onUnmounted } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
type Menu = {
|
||||
element?: HTMLElement;
|
||||
anchor: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
menu: Menu[];
|
||||
|
||||
scrollElement?: Element | HTMLElement | string;
|
||||
|
||||
foreground?: string;
|
||||
background?: string;
|
||||
|
||||
active?: {
|
||||
foreground?: string;
|
||||
background?: string;
|
||||
};
|
||||
}>();
|
||||
|
||||
const scrollElement = ref<Element | HTMLElement>();
|
||||
|
||||
function handleClick(menu: Menu) {
|
||||
const element = document.getElementById(menu.anchor);
|
||||
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const activeMenu = ref(props.menu.at(0)?.anchor || '');
|
||||
|
||||
function onScroll() {
|
||||
let current = '';
|
||||
|
||||
if (!scrollElement.value) return;
|
||||
|
||||
const container = scrollElement.value;
|
||||
|
||||
props.menu.forEach((v) => {
|
||||
if (!v.element) v.element = document.getElementById(v.anchor) || undefined;
|
||||
if (!v.element) return;
|
||||
|
||||
if (
|
||||
container.scrollTop + container.clientHeight / 2 >
|
||||
v.element.offsetTop
|
||||
) {
|
||||
current = v.anchor;
|
||||
}
|
||||
});
|
||||
|
||||
activeMenu.value = current;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof props.scrollElement === 'string' || !props.scrollElement) {
|
||||
const element = document.querySelector(props.scrollElement || 'body');
|
||||
if (element) scrollElement.value = element;
|
||||
} else {
|
||||
scrollElement.value = props.scrollElement;
|
||||
}
|
||||
|
||||
scrollElement.value?.addEventListener('scroll', onScroll);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
scrollElement.value?.removeEventListener('scroll', onScroll);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="side-menu__container">
|
||||
<span
|
||||
:style="{
|
||||
'--side-menu__fg': activeMenu !== v.anchor ? foreground : undefined,
|
||||
'--side-menu__bg': activeMenu !== v.anchor ? background : undefined,
|
||||
'--side-menu__fg-active': active?.foreground,
|
||||
'--side-menu__bg-active': active?.background,
|
||||
}"
|
||||
class="side-menu__item"
|
||||
:class="{ 'side-menu__active': activeMenu === v.anchor }"
|
||||
v-for="v in menu"
|
||||
@click="handleClick(v)"
|
||||
>
|
||||
{{ v.name }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.side-menu__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-1);
|
||||
padding: var(--size-2);
|
||||
|
||||
& > .side-menu__item {
|
||||
transition: 0.3s background ease-in-out;
|
||||
color: var(--side-menu__fg, inherit);
|
||||
background: var(--side-menu__bg, inherit);
|
||||
padding: var(--size-2) var(--size-4);
|
||||
border-radius: var(--radius-2);
|
||||
cursor: pointer;
|
||||
|
||||
&.side-menu__active {
|
||||
--side-menu__fg: var(--side-menu__fg-active, inherit);
|
||||
--side-menu__bg: var(--side-menu__bg-active, inherit);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue