Ben Lin
2024-06-18 9dfa701454d6a94690bad39dbb0e38f2a0b31489
src/components/SimpleMenu/src/components/SubMenuItem.vue
@@ -10,7 +10,7 @@
        />
      </div>
      <CollapseTransition>
        <ul :class="prefixCls" v-show="opened">
        <ul :class="prefixCls" v-show="state.opened">
          <slot></slot>
        </ul>
      </CollapseTransition>
@@ -20,9 +20,10 @@
      placement="right"
      :overlayClassName="`${prefixCls}-menu-popover`"
      v-else
      :visible="getIsOpend"
      @visible-change="handleVisibleChange"
      :open="getIsOpend"
      @on-open-change="handleVisibleChange"
      :overlayStyle="getOverlayStyle"
      :overlayInnerStyle="{ padding: 0 }"
      :align="{ offset: [0, 0] }"
    >
      <div :class="getSubClass" v-bind="getEvents(false)">
@@ -44,7 +45,7 @@
        />
      </div>
      <!-- eslint-disable-next-line -->
      <template #content v-show="opened">
      <template #content v-show="state.opened">
        <div v-bind="getEvents(true)">
          <ul :class="[prefixCls, `${prefixCls}-${getTheme}`, `${prefixCls}-popup`]">
            <slot></slot>
@@ -55,281 +56,253 @@
  </li>
</template>
<script lang="ts">
<script lang="ts" setup>
  import { type TimeoutHandle, type Recordable } from '@vben/types';
  import type { CSSProperties, PropType } from 'vue';
  import type { SubMenuProvider } from './types';
  import {
    defineComponent,
    computed,
    unref,
    getCurrentInstance,
    toRefs,
    reactive,
    provide,
    onBeforeMount,
    inject,
  } from 'vue';
  import { useDesign } from '/@/hooks/web/useDesign';
  import { propTypes } from '/@/utils/propTypes';
  import { useDesign } from '@/hooks/web/useDesign';
  import { propTypes } from '@/utils/propTypes';
  import { useMenuItem } from './useMenu';
  import { useSimpleRootMenuContext } from './useSimpleMenuContext';
  import { CollapseTransition } from '/@/components/Transition';
  import { CollapseTransition } from '@/components/Transition';
  import Icon from '@/components/Icon/Icon.vue';
  import { Popover } from 'ant-design-vue';
  import { isBoolean, isObject } from '/@/utils/is';
  import { mitt } from '/@/utils/mitt';
  import { isBoolean, isObject } from '@/utils/is';
  import { mitt } from '@/utils/mitt';
  defineOptions({ name: 'SubMenu' });
  const props = defineProps({
    name: {
      type: [String, Number] as PropType<string | number>,
      required: true,
    },
    disabled: propTypes.bool,
    collapsedShowTitle: propTypes.bool,
  });
  const DELAY = 200;
  export default defineComponent({
    name: 'SubMenu',
    components: {
      Icon,
      CollapseTransition,
      Popover,
    },
    props: {
      name: {
        type: [String, Number] as PropType<string | number>,
        required: true,
  const instance = getCurrentInstance();
  const state = reactive({
    active: false,
    opened: false,
  });
  const data = reactive({
    timeout: null as TimeoutHandle | null,
    mouseInChild: false,
    isChild: false,
  });
  const { getParentSubMenu, getItemStyle, getParentMenu, getParentList } = useMenuItem(instance);
  const { prefixCls } = useDesign('menu');
  const subMenuEmitter = mitt();
  const { rootMenuEmitter } = useSimpleRootMenuContext();
  const {
    addSubMenu: parentAddSubmenu,
    removeSubMenu: parentRemoveSubmenu,
    removeAll: parentRemoveAll,
    getOpenNames: parentGetOpenNames,
    isRemoveAllPopup,
    sliceIndex,
    level,
    props: rootProps,
    handleMouseleave: parentHandleMouseleave,
  } = inject<SubMenuProvider>(`subMenu:${getParentMenu.value?.uid}`)!;
  const getClass = computed(() => {
    return [
      `${prefixCls}-submenu`,
      {
        [`${prefixCls}-opened`]: state.opened,
        [`${prefixCls}-submenu-disabled`]: props.disabled,
        [`${prefixCls}-submenu-has-parent-submenu`]: unref(getParentSubMenu),
        [`${prefixCls}-child-item-active`]: state.active,
      },
      disabled: propTypes.bool,
      collapsedShowTitle: propTypes.bool,
    },
    setup(props) {
      const instance = getCurrentInstance();
    ];
  });
      const state = reactive({
        active: false,
        opened: false,
  const getAccordion = computed(() => rootProps.accordion);
  const getCollapse = computed(() => rootProps.collapse);
  const getTheme = computed(() => rootProps.theme);
  const getOverlayStyle = computed((): CSSProperties => {
    return {
      minWidth: '200px',
    };
  });
  const getIsOpend = computed(() => {
    const name = props.name;
    if (unref(getCollapse)) {
      return parentGetOpenNames().includes(name);
    }
    return state.opened;
  });
  const getSubClass = computed(() => {
    const isActive = rootProps.activeSubMenuNames.includes(props.name);
    return [
      `${prefixCls}-submenu-title`,
      {
        [`${prefixCls}-submenu-active`]: isActive,
        [`${prefixCls}-submenu-active-border`]: isActive && level === 0,
        [`${prefixCls}-submenu-collapse`]: unref(getCollapse) && level === 0,
      },
    ];
  });
  function getEvents(deep: boolean) {
    if (!unref(getCollapse)) {
      return {};
    }
    return {
      onMouseenter: handleMouseenter,
      onMouseleave: () => handleMouseleave(deep),
    };
  }
  function handleClick() {
    const { disabled } = props;
    if (disabled || unref(getCollapse)) return;
    const opened = state.opened;
    if (unref(getAccordion)) {
      const { uidList } = getParentList();
      rootMenuEmitter.emit('on-update-opened', {
        opend: false,
        parent: instance?.parent,
        uidList: uidList,
      });
      const data = reactive({
        timeout: null as TimeoutHandle | null,
        mouseInChild: false,
        isChild: false,
    } else {
      rootMenuEmitter.emit('open-name-change', {
        name: props.name,
        opened: !opened,
      });
    }
    state.opened = !opened;
  }
      const { getParentSubMenu, getItemStyle, getParentMenu, getParentList } =
        useMenuItem(instance);
  function handleMouseenter() {
    const disabled = props.disabled;
    if (disabled) return;
      const { prefixCls } = useDesign('menu');
    subMenuEmitter.emit('submenu:mouse-enter-child');
      const subMenuEmitter = mitt();
    const index = parentGetOpenNames().findIndex((item) => item === props.name);
      const { rootMenuEmitter } = useSimpleRootMenuContext();
    sliceIndex(index);
      const {
        addSubMenu: parentAddSubmenu,
        removeSubMenu: parentRemoveSubmenu,
        removeAll: parentRemoveAll,
        getOpenNames: parentGetOpenNames,
        isRemoveAllPopup,
        sliceIndex,
        level,
        props: rootProps,
        handleMouseleave: parentHandleMouseleave,
      } = inject<SubMenuProvider>(`subMenu:${getParentMenu.value?.uid}`)!;
    const isRoot = level === 0 && parentGetOpenNames().length === 2;
    if (isRoot) {
      parentRemoveAll();
    }
    data.isChild = parentGetOpenNames().includes(props.name);
    clearTimeout(data.timeout!);
    data.timeout = setTimeout(() => {
      parentAddSubmenu(props.name);
    }, DELAY);
  }
      const getClass = computed(() => {
        return [
          `${prefixCls}-submenu`,
          {
            [`${prefixCls}-item-active`]: state.active,
            [`${prefixCls}-opened`]: state.opened,
            [`${prefixCls}-submenu-disabled`]: props.disabled,
            [`${prefixCls}-submenu-has-parent-submenu`]: unref(getParentSubMenu),
            [`${prefixCls}-child-item-active`]: state.active,
          },
        ];
      });
  function handleMouseleave(deepDispatch = false) {
    const parentName = getParentMenu.value?.props.name;
    if (!parentName) {
      isRemoveAllPopup.value = true;
    }
      const getAccordion = computed(() => rootProps.accordion);
      const getCollapse = computed(() => rootProps.collapse);
      const getTheme = computed(() => rootProps.theme);
    if (parentGetOpenNames().slice(-1)[0] === props.name) {
      data.isChild = false;
    }
      const getOverlayStyle = computed((): CSSProperties => {
        return {
          minWidth: '200px',
        };
      });
      const getIsOpend = computed(() => {
        const name = props.name;
        if (unref(getCollapse)) {
          return parentGetOpenNames().includes(name);
        }
        return state.opened;
      });
      const getSubClass = computed(() => {
        const isActive = rootProps.activeSubMenuNames.includes(props.name);
        return [
          `${prefixCls}-submenu-title`,
          {
            [`${prefixCls}-submenu-active`]: isActive,
            [`${prefixCls}-submenu-active-border`]: isActive && level === 0,
            [`${prefixCls}-submenu-collapse`]: unref(getCollapse) && level === 0,
          },
        ];
      });
      function getEvents(deep: boolean) {
        if (!unref(getCollapse)) {
          return {};
        }
        return {
          onMouseenter: handleMouseenter,
          onMouseleave: () => handleMouseleave(deep),
        };
      }
      function handleClick() {
        const { disabled } = props;
        if (disabled || unref(getCollapse)) return;
        const opened = state.opened;
        if (unref(getAccordion)) {
          const { uidList } = getParentList();
          rootMenuEmitter.emit('on-update-opened', {
            opend: false,
            parent: instance?.parent,
            uidList: uidList,
          });
        } else {
          rootMenuEmitter.emit('open-name-change', {
            name: props.name,
            opened: !opened,
          });
        }
        state.opened = !opened;
      }
      function handleMouseenter() {
        const disabled = props.disabled;
        if (disabled) return;
        subMenuEmitter.emit('submenu:mouse-enter-child');
        const index = parentGetOpenNames().findIndex((item) => item === props.name);
        sliceIndex(index);
        const isRoot = level === 0 && parentGetOpenNames().length === 2;
        if (isRoot) {
    subMenuEmitter.emit('submenu:mouse-leave-child');
    if (data.timeout) {
      clearTimeout(data.timeout!);
      data.timeout = setTimeout(() => {
        if (isRemoveAllPopup.value) {
          parentRemoveAll();
        } else if (!data.mouseInChild) {
          parentRemoveSubmenu(props.name);
        }
        data.isChild = parentGetOpenNames().includes(props.name);
        clearTimeout(data.timeout!);
        data.timeout = setTimeout(() => {
          parentAddSubmenu(props.name);
        }, DELAY);
      }, DELAY);
    }
    if (deepDispatch) {
      if (getParentSubMenu.value) {
        parentHandleMouseleave?.(true);
      }
    }
  }
      function handleMouseleave(deepDispatch = false) {
        const parentName = getParentMenu.value?.props.name;
        if (!parentName) {
          isRemoveAllPopup.value = true;
        }
  onBeforeMount(() => {
    subMenuEmitter.on('submenu:mouse-enter-child', () => {
      data.mouseInChild = true;
      isRemoveAllPopup.value = false;
      clearTimeout(data.timeout!);
    });
    subMenuEmitter.on('submenu:mouse-leave-child', () => {
      if (data.isChild) return;
      data.mouseInChild = false;
      clearTimeout(data.timeout!);
    });
        if (parentGetOpenNames().slice(-1)[0] === props.name) {
          data.isChild = false;
    rootMenuEmitter.on(
      'on-update-opened',
      (data: boolean | (string | number)[] | Recordable<any>) => {
        if (unref(getCollapse)) return;
        if (isBoolean(data)) {
          state.opened = data;
          return;
        }
        subMenuEmitter.emit('submenu:mouse-leave-child');
        if (data.timeout) {
          clearTimeout(data.timeout!);
          data.timeout = setTimeout(() => {
            if (isRemoveAllPopup.value) {
              parentRemoveAll();
            } else if (!data.mouseInChild) {
              parentRemoveSubmenu(props.name);
            }
          }, DELAY);
        }
        if (deepDispatch) {
          if (getParentSubMenu.value) {
            parentHandleMouseleave?.(true);
        if (isObject(data) && rootProps.accordion) {
          const { opend, parent, uidList } = data as Recordable<any>;
          if (parent === instance?.parent) {
            state.opened = opend;
          } else if (!uidList.includes(instance?.uid)) {
            state.opened = false;
          }
          return;
        }
        if (props.name && Array.isArray(data)) {
          state.opened = (data as (string | number)[]).includes(props.name);
        }
      },
    );
    rootMenuEmitter.on('on-update-active-name:submenu', (data: number[]) => {
      if (instance?.uid) {
        state.active = data.includes(instance?.uid);
      }
    });
  });
      onBeforeMount(() => {
        subMenuEmitter.on('submenu:mouse-enter-child', () => {
          data.mouseInChild = true;
          isRemoveAllPopup.value = false;
          clearTimeout(data.timeout!);
        });
        subMenuEmitter.on('submenu:mouse-leave-child', () => {
          if (data.isChild) return;
          data.mouseInChild = false;
          clearTimeout(data.timeout!);
        });
  function handleVisibleChange(visible: boolean) {
    state.opened = visible;
  }
        rootMenuEmitter.on(
          'on-update-opened',
          (data: boolean | (string | number)[] | Recordable<any>) => {
            if (unref(getCollapse)) return;
            if (isBoolean(data)) {
              state.opened = data;
              return;
            }
            if (isObject(data) && rootProps.accordion) {
              const { opend, parent, uidList } = data as Recordable<any>;
              if (parent === instance?.parent) {
                state.opened = opend;
              } else if (!uidList.includes(instance?.uid)) {
                state.opened = false;
              }
              return;
            }
            if (props.name && Array.isArray(data)) {
              state.opened = (data as (string | number)[]).includes(props.name);
            }
          },
        );
        rootMenuEmitter.on('on-update-active-name:submenu', (data: number[]) => {
          if (instance?.uid) {
            state.active = data.includes(instance?.uid);
          }
        });
      });
      function handleVisibleChange(visible: boolean) {
        state.opened = visible;
      }
      // provide
      provide<SubMenuProvider>(`subMenu:${instance?.uid}`, {
        addSubMenu: parentAddSubmenu,
        removeSubMenu: parentRemoveSubmenu,
        getOpenNames: parentGetOpenNames,
        removeAll: parentRemoveAll,
        isRemoveAllPopup,
        sliceIndex,
        level: level + 1,
        handleMouseleave,
        props: rootProps,
      });
      return {
        getClass,
        prefixCls,
        getCollapse,
        getItemStyle,
        handleClick,
        handleVisibleChange,
        getParentSubMenu,
        getOverlayStyle,
        getTheme,
        getIsOpend,
        getEvents,
        getSubClass,
        ...toRefs(state),
        ...toRefs(data),
      };
    },
  // provide
  provide<SubMenuProvider>(`subMenu:${instance?.uid}`, {
    addSubMenu: parentAddSubmenu,
    removeSubMenu: parentRemoveSubmenu,
    getOpenNames: parentGetOpenNames,
    removeAll: parentRemoveAll,
    isRemoveAllPopup,
    sliceIndex,
    level: level + 1,
    handleMouseleave,
    props: rootProps,
  });
</script>