<script lang="ts" setup>
|
import type { HoverCardContentProps } from '@vben-core/shadcn-ui';
|
|
import type { MenuItemRegistered, MenuProvider, SubMenuProps } from '../types';
|
|
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
|
|
import { useNamespace } from '@vben-core/composables';
|
import { VbenHoverCard } from '@vben-core/shadcn-ui';
|
|
import {
|
createSubMenuContext,
|
useMenu,
|
useMenuContext,
|
useMenuStyle,
|
useSubMenuContext,
|
} from '../hooks';
|
import CollapseTransition from './collapse-transition.vue';
|
import SubMenuContent from './sub-menu-content.vue';
|
|
interface Props extends SubMenuProps {
|
isSubMenuMore?: boolean;
|
}
|
|
defineOptions({ name: 'SubMenu' });
|
|
const props = withDefaults(defineProps<Props>(), {
|
disabled: false,
|
isSubMenuMore: false,
|
});
|
|
const { parentMenu, parentPaths } = useMenu();
|
const { b, is } = useNamespace('sub-menu');
|
const nsMenu = useNamespace('menu');
|
const rootMenu = useMenuContext();
|
const subMenu = useSubMenuContext();
|
const subMenuStyle = useMenuStyle(subMenu);
|
|
const mouseInChild = ref(false);
|
|
const items = ref<MenuProvider['items']>({});
|
const subMenus = ref<MenuProvider['subMenus']>({});
|
const timer = ref<null | ReturnType<typeof setTimeout>>(null);
|
|
createSubMenuContext({
|
addSubMenu,
|
handleMouseleave,
|
level: (subMenu?.level ?? 0) + 1,
|
mouseInChild,
|
removeSubMenu,
|
});
|
|
const opened = computed(() => {
|
return rootMenu?.openedMenus.includes(props.path);
|
});
|
const isTopLevelMenuSubmenu = computed(
|
() => parentMenu.value?.type.name === 'Menu',
|
);
|
const mode = computed(() => rootMenu?.props.mode ?? 'vertical');
|
const rounded = computed(() => rootMenu?.props.rounded);
|
const currentLevel = computed(() => subMenu?.level ?? 0);
|
const isFirstLevel = computed(() => {
|
return currentLevel.value === 1;
|
});
|
|
const contentProps = computed((): HoverCardContentProps => {
|
const isHorizontal = mode.value === 'horizontal';
|
const side = isHorizontal && isFirstLevel.value ? 'bottom' : 'right';
|
return {
|
collisionPadding: { top: 20 },
|
side,
|
sideOffset: isHorizontal ? 5 : 10,
|
};
|
});
|
|
const active = computed(() => {
|
let isActive = false;
|
|
Object.values(items.value).forEach((item) => {
|
if (item.active) {
|
isActive = true;
|
}
|
});
|
|
Object.values(subMenus.value).forEach((subItem) => {
|
if (subItem.active) {
|
isActive = true;
|
}
|
});
|
return isActive;
|
});
|
|
function addSubMenu(subMenu: MenuItemRegistered) {
|
subMenus.value[subMenu.path] = subMenu;
|
}
|
|
function removeSubMenu(subMenu: MenuItemRegistered) {
|
Reflect.deleteProperty(subMenus.value, subMenu.path);
|
}
|
|
/**
|
* 点击submenu展开/关闭
|
*/
|
function handleClick() {
|
const mode = rootMenu?.props.mode;
|
if (
|
// 当前菜单禁用时,不展开
|
props.disabled ||
|
(rootMenu?.props.collapse && mode === 'vertical') ||
|
// 水平模式下不展开
|
mode === 'horizontal'
|
) {
|
return;
|
}
|
|
rootMenu?.handleSubMenuClick({
|
active: active.value,
|
parentPaths: parentPaths.value,
|
path: props.path,
|
});
|
}
|
|
function handleMouseenter(event: FocusEvent | MouseEvent, showTimeout = 300) {
|
if (event.type === 'focus') {
|
return;
|
}
|
|
if (
|
(!rootMenu?.props.collapse && rootMenu?.props.mode === 'vertical') ||
|
props.disabled
|
) {
|
if (subMenu) {
|
subMenu.mouseInChild.value = true;
|
}
|
return;
|
}
|
if (subMenu) {
|
subMenu.mouseInChild.value = true;
|
}
|
|
timer.value && window.clearTimeout(timer.value);
|
timer.value = setTimeout(() => {
|
rootMenu?.openMenu(props.path, parentPaths.value);
|
}, showTimeout);
|
parentMenu.value?.vnode.el?.dispatchEvent(new MouseEvent('mouseenter'));
|
}
|
|
function handleMouseleave(deepDispatch = false) {
|
if (
|
!rootMenu?.props.collapse &&
|
rootMenu?.props.mode === 'vertical' &&
|
subMenu
|
) {
|
subMenu.mouseInChild.value = false;
|
return;
|
}
|
|
timer.value && window.clearTimeout(timer.value);
|
|
if (subMenu) {
|
subMenu.mouseInChild.value = false;
|
}
|
timer.value = setTimeout(() => {
|
!mouseInChild.value && rootMenu?.closeMenu(props.path, parentPaths.value);
|
}, 300);
|
|
if (deepDispatch) {
|
subMenu?.handleMouseleave?.(true);
|
}
|
}
|
|
const menuIcon = computed(() =>
|
active.value ? props.activeIcon || props.icon : props.icon,
|
);
|
|
const item = reactive({
|
active,
|
parentPaths,
|
path: props.path,
|
});
|
|
onMounted(() => {
|
subMenu?.addSubMenu?.(item);
|
rootMenu?.addSubMenu?.(item);
|
});
|
|
onBeforeUnmount(() => {
|
subMenu?.removeSubMenu?.(item);
|
rootMenu?.removeSubMenu?.(item);
|
});
|
</script>
|
<template>
|
<li
|
:class="[
|
b(),
|
is('opened', opened),
|
is('active', active),
|
is('disabled', disabled),
|
]"
|
@focus="handleMouseenter"
|
@mouseenter="handleMouseenter"
|
@mouseleave="() => handleMouseleave()"
|
>
|
<template v-if="rootMenu.isMenuPopup">
|
<VbenHoverCard
|
:content-class="[
|
rootMenu.theme,
|
nsMenu.e('popup-container'),
|
is(rootMenu.theme, true),
|
opened ? '' : 'hidden',
|
]"
|
:content-props="contentProps"
|
:open="true"
|
:open-delay="0"
|
>
|
<template #trigger>
|
<SubMenuContent
|
:class="is('active', active)"
|
:icon="menuIcon"
|
:is-menu-more="isSubMenuMore"
|
:is-top-level-menu-submenu="isTopLevelMenuSubmenu"
|
:level="currentLevel"
|
:path="path"
|
@click.stop="handleClick"
|
>
|
<template #title>
|
<slot name="title"></slot>
|
</template>
|
</SubMenuContent>
|
</template>
|
<div
|
:class="[nsMenu.is(mode, true), nsMenu.e('popup')]"
|
@focus="(e) => handleMouseenter(e, 100)"
|
@mouseenter="(e) => handleMouseenter(e, 100)"
|
@mouseleave="() => handleMouseleave(true)"
|
>
|
<ul
|
:class="[nsMenu.b(), is('rounded', rounded)]"
|
:style="subMenuStyle"
|
>
|
<slot></slot>
|
</ul>
|
</div>
|
</VbenHoverCard>
|
</template>
|
|
<template v-else>
|
<SubMenuContent
|
:class="is('active', active)"
|
:icon="menuIcon"
|
:is-menu-more="isSubMenuMore"
|
:is-top-level-menu-submenu="isTopLevelMenuSubmenu"
|
:level="currentLevel"
|
:path="path"
|
@click.stop="handleClick"
|
>
|
<slot name="content"></slot>
|
<template #title>
|
<slot name="title"></slot>
|
</template>
|
</SubMenuContent>
|
<CollapseTransition>
|
<ul
|
v-show="opened"
|
:class="[nsMenu.b(), is('rounded', rounded)]"
|
:style="subMenuStyle"
|
>
|
<slot></slot>
|
</ul>
|
</CollapseTransition>
|
</template>
|
</li>
|
</template>
|