123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271 |
- <script lang="ts" setup>
- import type { HoverCardContentProps } from '@vben-core/shadcn-ui';
- import type {
- MenuItemRegistered,
- MenuProvider,
- SubMenuProps,
- } from '../interface';
- import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
- import { useNamespace } from '@vben-core/hooks';
- 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<ReturnType<typeof setTimeout> | null>(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 side =
- mode.value === 'horizontal' && isFirstLevel.value ? 'bottom' : 'right';
- return {
- side,
- sideOffset: isFirstLevel.value ? 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 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="[
- 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="icon"
- :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="icon"
- :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>
|