cosmos-explorer/src/plugins/vuetify/@layouts/components/VerticalNavGroup.vue
2023-05-20 12:23:53 +08:00

255 lines
8.4 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts" setup>
import { injectionKeyIsVerticalNavHovered, useLayouts } from '@layouts'
import { TransitionExpand, VerticalNavLink } from '@layouts/components'
import { themeConfig as config } from '@themeConfig'
import type { NavGroup } from '@layouts/types'
import { isNavGroupActive, openGroups } from '@layouts/utils'
import { ref } from 'vue'
const props = defineProps<{
item: NavGroup
}>()
defineOptions({
name: 'VerticalNavGroup',
})
const route = useRoute()
const router = useRouter()
const { width: windowWidth } = useWindowSize()
const { isVerticalNavMini, dynamicI18nProps } = useLayouts()
const hideTitleAndBadge = isVerticalNavMini(windowWidth)
/*
We provided default value `ref(false)` because inject will return `T | undefined`
Docs: https://vuejs.org/api/composition-api-dependency-injection.html#inject
*/
const isVerticalNavHovered = inject(injectionKeyIsVerticalNavHovered, ref(false))
/*
We have to add watcher for `isVerticalNavCollapsed` to open & close the group when menu collapse state is changed
We can't rely on watcher for `isVerticalNavHovered` because nav menu can be collapsed via customizer (basically without entering mouse inside nav menu)
Hence, watcher for `isVerticalNavHovered` won't get triggered and there will no change in open state of nav group when menu is collapsed via customizer.
*/
// watch(isVerticalNavCollapsed, value => {
// // If mouse in nav menu `isVerticalNavHovered` watcher will take care of open/close group
// if (isVerticalNavHovered.value)
// return
// isGroupOpen.value = value ? false : isGroupActive.value
// })
const isGroupActive = ref(false)
const isGroupOpen = ref(true)
/**
* Checks if any of children group is open or not.
* This is helpful in preventing closing inactive parent group when inactive child group is opened. (i.e. Do not close "Nav Levels" group if child "Nav Level 2.2" is opened/clicked)
*
* @param {NavGroup['children']} children - Nav group children
* @return {boolean} returns if any of children is open or not.
*/
const isAnyChildOpen = (children: NavGroup['children']): boolean => {
return children.some(child => {
let result = openGroups.value.includes(child.title)
if ('children' in child)
result = isAnyChildOpen(child.children) || result
return result
})
}
const collapseChildren = (children: NavGroup['children']) => {
children.forEach(child => {
if ('children' in child)
collapseChildren(child.children)
openGroups.value = openGroups.value.filter(group => group !== child.title)
})
}
/*
Watch for route changes, more specifically route path. Do note that this won't trigger if route's query is updated.
updates isActive & isOpen based on active state of group.
*/
watch(() => route.path, () => {
const isActive = isNavGroupActive(props.item.children, router)
// Don't open group if vertical nav is collapsed and window size is more than overlay nav breakpoint
if(props.item.badgeContent) {
isGroupOpen.value = isActive && !isVerticalNavMini(windowWidth, isVerticalNavHovered).value
}
isGroupActive.value = isActive
}, { immediate: true })
/*
Watch for isGroupOpen
1. Find group index for adding/removing group from openGroups array
2. update openGroups array for addition/removal of current group
We need `immediate: true` because without it initially opened group is not added in openGroups array
*/
watch(isGroupOpen, (val: boolean) => {
// Find group index for adding/removing group from openGroups array
const grpIndex = openGroups.value.indexOf(props.item.title)
// update openGroups array for addition/removal of current group
// If group is opened => Add it to `openGroups` array
if (val && grpIndex === -1) {
openGroups.value.push(props.item.title)
}
// If group is closed remove itself and its children from the `openGroups`
else if (!val && grpIndex !== -1) {
openGroups.value.splice(grpIndex, 1)
collapseChildren(props.item.children)
}
}, { immediate: true })
/*
Watch for openGroups
It will help in making vertical nav adapting the behavior of accordion.
If we open multiple groups without navigating to any route we must close the inactive or temporarily opened groups.
😵‍💫 Gotchas:
* If we open inactive group then it will auto close that group because we close groups based on active state.
Goal of this watcher is auto close groups which are not active when openGroups array is updated.
So, we have to find a way to do not close recently opened inactive group.
For this we will fetch recently added group in openGroups array and won't perform closing operation if recently added group is current group
*/
watch(openGroups, val => {
// Prevent closing recently opened inactive group.
const lastOpenedGroup = val[val.length - 1]
if (lastOpenedGroup === props.item.title)
return
const isActive = isNavGroupActive(props.item.children, router)
// Goal of this watcher is to close inactive groups. So don't do anything for active groups.
if (isActive)
return
// We won't close group if any of child group is open in current group
if (isAnyChildOpen(props.item.children))
return
// isGroupOpen.value = isActive
isGroupActive.value = isActive
}, { deep: true })
// Previously instead of below watcher we were using two individual watcher for `isVerticalNavHovered`, `isVerticalNavCollapsed` & `isLessThanOverlayNavBreakpoint`
watch(isVerticalNavMini(windowWidth, isVerticalNavHovered), val => {
isGroupOpen.value = val ? false : isGroupActive.value
})
// watch(isVerticalNavHovered, val => {
// // If menu is not collapsed ignore
// if (!(isVerticalNavCollapsed.value && !isLessThanOverlayNavBreakpoint.value(windowWidth.value)))
// return
// isGroupOpen.value = val ? isGroupActive.value : false
// })
/*
Update: We don't need this watcher any more because we have new watch isVerticalNavMini that includes this one
We need this watcher to
- Collapse the group when going to `lgAndUp` if vertical nav is collapsed (else block)
- Expand the group if it's active and screen is `mdAndDown`. Because in this screen vertical nav will be overlay nav
*/
// watch(() => isLessThanOverlayNavBreakpoint.value(windowWidth.value), isLessThanOverlayNavBreakpoint_ => {
// // If window size is more than overlay nav breakpoint => expand group if its active
// if (isLessThanOverlayNavBreakpoint_) { isGroupOpen.value = isGroupActive.value }
// else {
// if (isVerticalNavCollapsed.value)
// isGroupOpen.value = false
// }
// })
</script>
<template>
<li
class="nav-group"
:class="[
{
active: isGroupActive,
open: isGroupOpen,
disabled: item.disable,
},
]"
>
<div
class="nav-group-label"
@click="isGroupOpen = !isGroupOpen"
>
<Component
:is="config.app.iconRenderer || 'div'"
v-bind="item.icon || config.verticalNav.defaultNavItemIconProps"
class="nav-item-icon"
/>
<TransitionGroup name="transition-slide-x">
<!-- 👉 Title -->
<Component
:is=" config.app.enableI18n && item.i18n ? 'i18n-t' : 'span'"
v-bind="dynamicI18nProps(item.title, 'span')"
v-show="!hideTitleAndBadge"
key="title"
class="nav-item-title"
>
{{ item.title }}
</Component>
<!-- 👉 Badge -->
<Component
:is="config.app.enableI18n && false ? 'i18n-t' : 'span'"
v-bind="dynamicI18nProps(item.badgeContent, 'span')"
v-show="!hideTitleAndBadge"
v-if="item.badgeContent"
key="badge"
class="nav-item-badge"
:class="item.badgeClass"
>
{{ item.badgeContent }}
</Component>
<Component
:is="config.app.iconRenderer || 'div'"
v-show="!hideTitleAndBadge"
v-bind="config.icons.chevronRight"
key="arrow"
class="nav-group-arrow"
/>
</TransitionGroup>
</div>
<TransitionExpand>
<ul
v-show="isGroupOpen"
class="nav-group-children"
>
<Component
v-for="child in item.children"
:is="'children' in child ? 'VerticalNavGroup' : VerticalNavLink"
:key="child.title"
:item="child"
/>
</ul>
</TransitionExpand>
</li>
</template>
<style lang="scss">
.layout-vertical-nav {
.nav-group {
&-label {
display: flex;
align-items: center;
cursor: pointer;
}
}
}
</style>