zs
2025-04-30 3fbfefcf6bdbf35ac36d6e9b01aa75e2cff77ab6
HIAWms/web/src/components/vue3-context-menu/ContextSubMenuWrapper.vue
对比新文件
@@ -0,0 +1,326 @@
<template>
  <div class="mx-menu-ghost-host">
    <Transition
      v-if="options.menuTransitionProps"
      appear
      v-bind="options.menuTransitionProps"
      @after-leave="emit('closeAnimFinished')"
    >
      <ContextSubMenuConstructor
        v-if="show"
        class="mx-menu-host"
        :items="options.items"
        :adjustPosition="options.adjustPosition"
        :maxWidth="options.maxWidth || MenuConstOptions.defaultMaxWidth"
        :minWidth="options.minWidth || MenuConstOptions.defaultMinWidth"
        :direction="(options.direction || MenuConstOptions.defaultDirection as MenuPopDirection)"
      >
        <slot />
      </ContextSubMenuConstructor>
    </Transition>
    <ContextSubMenuConstructor
      v-else-if="show"
      class="mx-menu-host"
      :items="options.items"
      :adjustPosition="options.adjustPosition"
      :maxWidth="options.maxWidth || MenuConstOptions.defaultMaxWidth"
      :minWidth="options.minWidth || MenuConstOptions.defaultMinWidth"
      :direction="(options.direction || MenuConstOptions.defaultDirection as MenuPopDirection)"
    >
      <slot />
    </ContextSubMenuConstructor>
  </div>
</template>
<script setup lang="ts">
  import {
    h,
    onBeforeUnmount,
    onMounted,
    type PropType,
    provide,
    ref,
    renderSlot,
    toRefs,
    type VNode,
    watch,
    Transition,
    useSlots,
    type Ref,
  } from "vue";
  import type {
    MenuItem,
    MenuOptions,
    MenuPopDirection,
  } from "./ContextMenuDefine";
  import { MenuConstOptions } from "./ContextMenuDefine";
  import {
    addOpenedContextMenu,
    removeOpenedContextMenu,
  } from "./ContextMenuMutex";
  import ContextSubMenuConstructor, {
    type SubMenuContext,
    type SubMenuParentContext,
  } from "./ContextSubMenu.vue";
  /**
   * Context menu component
   */
  export type GlobalHasSlot = (name: string) => boolean;
  export type GlobalRenderSlot = (
    name: string,
    params: Record<string, unknown>
  ) => VNode;
  const props = defineProps({
    /**
     * Menu options
     */
    options: {
      type: Object as PropType<MenuOptions>,
      default: null,
    },
    /**
     * Show menu?
     */
    show: {
      type: Object as PropType<Ref<boolean>>,
      default: null,
    },
    /**
     * Current container, For calculation only
     */
    container: {
      type: Object as PropType<HTMLElement>,
      default: null,
    },
    /**
     * Make sure is user set the custom container.
     */
    isFullScreenContainer: {
      type: Boolean,
      default: true,
    },
  });
  const emit = defineEmits(["close", "closeAnimFinished"]);
  const slots = useSlots();
  const { options, show, container } = toRefs(props);
  onMounted(() => {
    if (show.value) openMenu();
  });
  onBeforeUnmount(() => {
    removeBodyEvents();
  });
  watch(show, (v: boolean) => {
    if (v) {
      openMenu();
    } else {
      removeBodyEvents();
    }
  });
  const instance = {
    closeMenu,
    isClosed,
  };
  let closed = false;
  function openMenu() {
    installBodyEvents();
    addOpenedContextMenu(instance);
  }
  function closeMenu(fromItem?: MenuItem | undefined) {
    closed = true;
    emit("close", fromItem);
    if (!options.value.menuTransitionProps) emit("closeAnimFinished");
    removeOpenedContextMenu(instance);
  }
  function isClosed() {
    return closed;
  }
  function installBodyEvents() {
    setTimeout(() => {
      document.addEventListener("click", onBodyClick, true);
      document.addEventListener("contextmenu", onBodyClick, true);
      document.addEventListener("scroll", onBodyScroll, true);
      if (!props.isFullScreenContainer && container.value)
        container.value.addEventListener("scroll", onBodyScroll, true);
      if (options.value.keyboardControl !== false)
        document.addEventListener("keydown", onMenuKeyDown);
    }, 50);
  }
  function removeBodyEvents() {
    document.removeEventListener("contextmenu", onBodyClick, true);
    document.removeEventListener("click", onBodyClick, true);
    document.removeEventListener("scroll", onBodyScroll, true);
    if (!props.isFullScreenContainer && container.value)
      container.value.removeEventListener("scroll", onBodyScroll, true);
    if (options.value.keyboardControl !== false)
      document.removeEventListener("keydown", onMenuKeyDown);
  }
  //For keyboard event, remember which submenu is active
  const currentOpenedMenu = ref<SubMenuContext | null>();
  provide(
    "globalSetCurrentSubMenu",
    (menu: SubMenuContext | null) => (currentOpenedMenu.value = menu)
  );
  function onMenuKeyDown(e: KeyboardEvent) {
    let handled = true;
    //Handle keyboard event
    switch (e.key) {
      case "Escape": {
        if (currentOpenedMenu.value?.isTopLevel() === false) {
          currentOpenedMenu.value?.closeCurrentSubMenu();
        } else {
          closeMenu();
        }
        break;
      }
      case "ArrowDown":
        currentOpenedMenu.value?.moveCurrentItemDown();
        break;
      case "ArrowUp":
        currentOpenedMenu.value?.moveCurrentItemUp();
        break;
      case "Home":
        currentOpenedMenu.value?.moveCurrentItemFirst();
        break;
      case "End":
        currentOpenedMenu.value?.moveCurrentItemLast();
        break;
      case "ArrowLeft": {
        if (!currentOpenedMenu.value?.closeSelfAndActiveParent())
          options.value.onKeyFocusMoveLeft?.();
        break;
      }
      case "ArrowRight":
        if (!currentOpenedMenu.value?.openCurrentItemSubMenu())
          options.value.onKeyFocusMoveRight?.();
        break;
      case "Enter":
        currentOpenedMenu.value?.triggerCurrentItemClick(e);
        break;
      default:
        handled = false;
        break;
    }
    if (handled && currentOpenedMenu.value) {
      e.stopPropagation();
      e.preventDefault();
    }
  }
  function onBodyScroll() {
    //close when docunment scroll
    if (options.value.closeWhenScroll !== false) closeMenu();
  }
  function onBodyClick(e: MouseEvent) {
    checkTargetAndClose(e.target as HTMLElement);
  }
  function checkTargetAndClose(target: HTMLElement) {
    //Loop target , Check whether the currently clicked element belongs to the current menu.
    // If yes, it will not be closed
    while (target) {
      if (target.classList && target.classList.contains("mx-menu-host")) return;
      target = target.parentNode as HTMLElement;
    }
    if (options.value.clickCloseOnOutside !== false) {
      //Close menu
      removeBodyEvents();
      closeMenu();
    }
  }
  //provide globalOptions for child use
  provide("globalOptions", options.value);
  provide("globalCloseMenu", closeMenu);
  provide("globalTheme", options.value?.theme || "light");
  provide("globalIsFullScreenContainer", props.isFullScreenContainer);
  provide("globalClickCloseClassName", options.value?.clickCloseClassName);
  provide("globalIgnoreClickClassName", options.value?.ignoreClickClassName);
  provide("globalIconFontClass", options.value?.iconFontClass || "iconfont");
  provide("globalMenuTransitionProps", options.value?.menuTransitionProps);
  //check slot exists
  provide("globalHasSlot", (name: string) => {
    return slots[name] !== undefined;
  });
  //render slot
  provide(
    "globalRenderSlot",
    (name: string, params: Record<string, unknown>) => {
      return renderSlot(
        slots,
        name,
        { ...params },
        () => [h("span", "Render slot failed")],
        false
      );
    }
  );
  //provide menuContext for child use
  provide("menuContext", {
    zIndex: options.value.zIndex || MenuConstOptions.defaultZindex,
    container: container.value as unknown as HTMLElement,
    adjustPadding: { x: 0, y: 0 },
    getParentAbsY: () => options.value.x,
    getParentAbsX: () => options.value.y,
    getParentX: () => 0,
    getParentY: () => 0,
    getParentWidth: () => 0,
    getParentHeight: () => 0,
    getPositon: () => [options.value.x, options.value.y],
    closeOtherSubMenuWithTimeOut: () => {
      /* Do nothing */
    },
    checkCloseOtherSubMenuTimeOut: () => false,
    addOpenedSubMenu: () => {
      /* Do nothing */
    },
    closeOtherSubMenu: () => {
      /* Do nothing */
    },
    getParentContext: () => null,
    getSubMenuInstanceContext: () => null,
    getElement: () => null,
    addChildMenuItem: () => {
      /* Do nothing */
    },
    removeChildMenuItem: () => {
      /* Do nothing */
    },
    markActiveMenuItem: () => {
      /* Do nothing */
    },
    markThisOpenedByKeyBoard: () => {
      /* Do nothing */
    },
    isOpenedByKeyBoardFlag: () => false,
    isMenuItemDataCollectedFlag: () => false,
  } as SubMenuParentContext);
  //Expose instance function
  defineExpose(instance);
</script>
<style>
  .mx-menu-ghost-host {
    position: absolute;
    left: 0;
    bottom: 0;
    right: 0;
    top: 0;
    overflow: hidden;
    pointer-events: none;
  }
  .mx-menu-ghost-host.information_full_screen {
    position: fixed;
  }
</style>