219 lines
6.5 KiB
Vue
219 lines
6.5 KiB
Vue
<script setup>
|
||
import { TransitionGroup } from 'vue'
|
||
import { layoutConfig } from '@layouts'
|
||
import {
|
||
TransitionExpand,
|
||
VerticalNavLink,
|
||
} from '@layouts/components'
|
||
import { canViewNavMenuGroup } from '@layouts/plugins/casl'
|
||
import { useLayoutConfigStore } from '@layouts/stores/config'
|
||
import { injectionKeyIsVerticalNavHovered } from '@layouts/symbols'
|
||
import {
|
||
getDynamicI18nProps,
|
||
isNavGroupActive,
|
||
openGroups,
|
||
} from '@layouts/utils'
|
||
|
||
const props = defineProps({
|
||
item: {
|
||
type: null,
|
||
required: true,
|
||
},
|
||
})
|
||
|
||
defineOptions({
|
||
name: 'VerticalNavGroup',
|
||
})
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const configStore = useLayoutConfigStore()
|
||
const hideTitleAndBadge = configStore.isVerticalNavMini()
|
||
|
||
/*ℹ️ 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))
|
||
|
||
// isGroupOpen.value = value ? false : isGroupActive.value
|
||
|
||
// })
|
||
const isGroupActive = ref(false)
|
||
const isGroupOpen = ref(false)
|
||
|
||
const isAnyChildOpen = children => {
|
||
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 => {
|
||
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
|
||
isGroupOpen.value = isActive && !configStore.isVerticalNavMini(isVerticalNavHovered).value
|
||
isGroupActive.value = isActive
|
||
}, { immediate: true })
|
||
watch(isGroupOpen, val => {
|
||
|
||
// 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)
|
||
} 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.at(-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(configStore.isVerticalNavMini(isVerticalNavHovered), val => {
|
||
isGroupOpen.value = val ? false : isGroupActive.value
|
||
})
|
||
|
||
// isGroupOpen.value = value ? false : isGroupActive.value
|
||
|
||
// })
|
||
const isMounted = useMounted()
|
||
</script>
|
||
|
||
<template>
|
||
<li
|
||
v-if="canViewNavMenuGroup(item)"
|
||
class="nav-group"
|
||
:class="[
|
||
{
|
||
active: isGroupActive,
|
||
open: isGroupOpen,
|
||
disabled: item.disable,
|
||
},
|
||
]"
|
||
>
|
||
<div
|
||
class="nav-group-label"
|
||
@click="isGroupOpen = !isGroupOpen"
|
||
>
|
||
<Component
|
||
:is="layoutConfig.app.iconRenderer || 'div'"
|
||
v-bind="item.icon || layoutConfig.verticalNav.defaultNavItemIconProps"
|
||
class="nav-item-icon"
|
||
/>
|
||
<!--
|
||
ℹ️ isMounted is workaround of nuxt's hydration issue:
|
||
https://github.com/vuejs/core/issues/6715
|
||
-->
|
||
<Component
|
||
:is="isMounted ? TransitionGroup : 'div'"
|
||
name="transition-slide-x"
|
||
v-bind="!isMounted ? { class: 'd-flex align-center flex-grow-1' } : undefined"
|
||
>
|
||
<!-- 👉 Title -->
|
||
<Component
|
||
:is=" layoutConfig.app.i18n.enable ? 'i18n-t' : 'span'"
|
||
v-bind="getDynamicI18nProps(item.title, 'span')"
|
||
v-show="!hideTitleAndBadge"
|
||
key="title"
|
||
class="nav-item-title"
|
||
>
|
||
{{ item.title }}
|
||
</Component>
|
||
|
||
<!-- 👉 Badge -->
|
||
<Component
|
||
:is="layoutConfig.app.i18n.enable ? 'i18n-t' : 'span'"
|
||
v-bind="getDynamicI18nProps(item.badgeContent, 'span')"
|
||
v-show="!hideTitleAndBadge"
|
||
v-if="item.badgeContent"
|
||
key="badge"
|
||
class="nav-item-badge"
|
||
:class="item.badgeClass"
|
||
>
|
||
{{ item.badgeContent }}
|
||
</Component>
|
||
<Component
|
||
:is="layoutConfig.app.iconRenderer || 'div'"
|
||
v-show="!hideTitleAndBadge"
|
||
v-bind="layoutConfig.icons.chevronRight"
|
||
key="arrow"
|
||
class="nav-group-arrow"
|
||
/>
|
||
</Component>
|
||
</div>
|
||
<TransitionExpand>
|
||
<ul
|
||
v-show="isGroupOpen"
|
||
class="nav-group-children"
|
||
>
|
||
<Component
|
||
:is="'children' in child ? 'VerticalNavGroup' : VerticalNavLink"
|
||
v-for="child in item.children"
|
||
: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>
|