zs
2025-05-05 6ecd5b23904d2e862dd572ab7feb1a85824fc9ad
HIAWms/web/src/components/vue3-context-menu/ContextMenuItem.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,380 @@
<template>
  <div v-if="!hidden" class="mx-context-menu-item-wrapper" ref="menuItemRef" data-type="ContextMenuItem">
    <!--Custom render-->
    <VNodeRender v-if="globalHasSlot('itemRender')" :vnode="() => globalRenderSlot('itemRender', getItemDataForChildren())" />
    <VNodeRender v-else-if="customRender" :vnode="customRender" :data="getItemDataForChildren()" />
    <!--Default item-->
    <div
      v-else
      :class="[
        'mx-context-menu-item',
        (disabled ? 'disabled' : ''),
        (keyBoardFocusMenu ? 'keyboard-focus' : ''),
        (customClass ? (' ' + customClass) : ''),
        (showSubMenu ? 'open' : ''),
      ]"
      @click="onClick"
      @mouseenter="onMouseEnter"
    >
      <slot>
        <div class="mx-item-row">
          <div :class="[
            'mx-icon-placeholder',
            preserveIconWidth ? 'preserve-width': '',
          ]">
            <slot name="icon">
              <VNodeRender v-if="globalHasSlot('itemIconRender')" :vnode="() => globalRenderSlot('itemIconRender', getItemDataForChildren())" />
              <svg v-else-if="typeof svgIcon === 'string' && svgIcon" class="icon svg" v-bind="svgProps">
                <use :xlink:href="svgIcon"></use>
              </svg>
              <VNodeRender v-else-if="(typeof icon !== 'string')" :vnode="icon" :data="icon" />
              <i v-else-if="typeof icon === 'string' && icon !== ''" :class="icon + ' icon '+ iconFontClass + ' ' + globalIconFontClass"></i>
            </slot>
            <slot v-if="checked" name="check">
              <VNodeRender v-if="globalHasSlot('itemCheckRender')" :vnode="() => globalRenderSlot('itemCheckRender', getItemDataForChildren())" />
              <ContextMenuIconCheck />
            </slot>
          </div>
          <slot name="label">
            <VNodeRender v-if="globalHasSlot('itemLabelRender')" :vnode="() => globalRenderSlot('itemLabelRender', getItemDataForChildren())" />
            <span class="label" v-else-if="typeof label === 'string'">{{ label }}</span>
            <VNodeRender v-else :vnode="label" :data="label" />
          </slot>
        </div>
        <div class="mx-item-row">
          <slot v-if="shortcut" name="shortcut">
            <VNodeRender v-if="globalHasSlot('itemShortcutRender')" :vnode="() => globalRenderSlot('itemShortcutRender', getItemDataForChildren())" />
            <span class="mx-shortcut">{{ shortcut }}</span>
          </slot>
          <slot v-if="showRightArrow" name="rightArrow">
            <VNodeRender v-if="globalHasSlot('itemRightArrowRender')" :vnode="() => globalRenderSlot('itemRightArrowRender', getItemDataForChildren())" />
            <ContextMenuIconRight />
          </slot>
        </div>
      </slot>
    </div>
    <!--Sub menu render-->
    <Transition v-if="globalMenuTransitionProps" v-bind="globalMenuTransitionProps">
      <slot v-if="showSubMenu" name="submenu"></slot>
    </Transition>
    <slot v-else-if="showSubMenu" name="submenu"></slot>
  </div>
</template>
<script setup lang="ts">
import { inject, nextTick, onBeforeUnmount, onMounted, type PropType, ref, type SVGAttributes, toRefs, type TransitionProps } from 'vue'
import type { MenuItemContext, SubMenuParentContext } from './ContextSubMenu.vue'
import type { GlobalHasSlot, GlobalRenderSlot } from './ContextMenu.vue'
import type { MenuItem } from './ContextMenuDefine'
import { VNodeRender } from './ContextMenuUtils'
import ContextMenuIconCheck from './ContextMenuIconCheck.vue'
import ContextMenuIconRight from './ContextMenuIconRight.vue'
/**
 * Menu Item
 */
const props = defineProps({
  /**
   * Is this menu disabled?
   */
  disabled: {
    type: Boolean,
    default: false
  },
  /**
   * Is this menu hidden?
   */
  hidden: {
    type: Boolean,
    default: false
  },
  customRender: {
    type: Function,
    default: null
  },
  /**
   * Custom css class for submenu
   */
  customClass: {
    type: String,
    default: ''
  },
  clickHandler: {
    type: Function as PropType<(e: MouseEvent|KeyboardEvent) => void>,
    default: null
  },
  /**
   * Menu label
   */
  label: {
    type: [String, Object, Function],
    default: ''
  },
  /**
   * Menu icon (for icon class)
   */
  icon: {
    type: [String, Object, Function],
    default: ''
  },
  /**
   * Custom icon library font class name.
   *
   * Only for css font icon, If you use the svg icon, you do not need to use this.
   */
  iconFontClass: {
    type: String,
    default: 'iconfont'
  },
  /**
   * Is this menu item checked?
   *
   * The check mark are displayed on the left side of the icon, so it is not recommended to display the icon at the same time.
   */
  checked: {
    type: Boolean,
    default: false
  },
  /**
   * Shortcut key text display on the right.
   *
   * The shortcut keys here are only for display. You need to handle the key events by yourself.
   */
  shortcut: {
    type: String,
    default: ''
  },
  /**
   * Display icons use svg symbol (`<use xlink:href="#icon-symbol-name">`) ï¼Œ only valid when icon attribute is empty.
   */
  svgIcon: {
    type: String,
    default: ''
  },
  /**
   * The user-defined attribute of the svg tag, which is valid when using `svgIcon`.
   */
  svgProps: {
    type: Object as PropType<SVGAttributes>,
    default: null
  },
  /**
   * Should a fixed-width icon area be reserved for menu items without icon. (this item)
   *
   * Default is true .
   *
   * The width of icon area can be override with css var `--mx-menu-placeholder-width`.
   */
  preserveIconWidth: {
    type: Boolean,
    default: true,
  },
  /**
   * Show right arrow on this menu?
   */
  showRightArrow: {
    type: Boolean,
    default: false
  },
  hasChildren: {
    type: Boolean,
    default: false
  },
  /**
   * Should close menu when Click this menu item ?
   */
  clickClose: {
    type: Boolean,
    default: true
  },
  /**
   * When there are subitems in this item, is it allowed to trigger its own click event? Default is false
   */
  clickableWhenHasChildren: {
    type: Boolean,
    default: false
  },
  rawMenuItem: {
    type: Object as PropType<MenuItem>,
    default: undefined
  },
});
const emit = defineEmits([
  'click',
  'subMenuOpen',
  'subMenuClose',
])
const {
  clickHandler, clickClose, clickableWhenHasChildren, disabled, hidden,
  label, icon, iconFontClass,
  showRightArrow, shortcut,
  hasChildren,
} = toRefs(props);
const showSubMenu = ref(false);
const keyBoardFocusMenu = ref(false);
const menuItemRef = ref<HTMLElement>();
const globalHasSlot = inject('globalHasSlot') as GlobalHasSlot;
const globalRenderSlot = inject('globalRenderSlot') as GlobalRenderSlot;
const globalTheme = inject('globalTheme') as string;
const globalIconFontClass = inject('globalIconFontClass') as string;
const globalMenuTransitionProps = inject('globalMenuTransitionProps') as TransitionProps;
const globalClickCloseClassName = inject('globalClickCloseClassName') as string;
const globalIgnoreClickClassName = inject('globalIgnoreClickClassName') as string;
const globalCloseMenu = inject('globalCloseMenu') as (fromItem: MenuItem|undefined) => void;
const menuContext = inject('menuContext') as SubMenuParentContext;
//Instance Contet for keyboadr control
const menuItemInstance : MenuItemContext = {
  showSubMenu: () => {
    if (showSubMenu.value) {
      //Mark current item
      menuContext.markActiveMenuItem(menuItemInstance, true);
      return true;
    } else if (hasChildren.value) {
      onMouseEnter();
      return true;
    }
    return false;
  },
  isDisabledOrHidden: () => disabled.value || hidden.value,
  getElement: () => menuItemRef.value,
  focus: () => keyBoardFocusMenu.value = true,
  blur: () => keyBoardFocusMenu.value = false,
  click: onClick,
}
onMounted(() => {
  if (menuContext.isMenuItemDataCollectedFlag()) {
    //当前菜单条目是在整体加载完成后才显示的,此时菜单顺序已经无法知道,
    //所以这里需要在父级元素中查找得出当前菜单的位置。
    //
    //The current menu item is displayed after the overall loading is completed.
    //At this time, the menu order cannot be known, so here we need to
    //find the position of the current menu in the parent element.
    nextTick(() => {
      let index = 0;
      const parentEl = menuContext.getElement();
      if (parentEl) {
        let indexCounting = 0;
        for (let i = 0; i < parentEl.children.length; i++) {
          const el = parentEl.children[i];
          if (el.getAttribute('data-type') === 'ContextMenuItem') {
            if (el === menuItemRef.value) {
              index = indexCounting;
              break;
            }
            indexCounting++;
          }
        }
      }
      //Insert to pos
      menuContext.addChildMenuItem(menuItemInstance, index);
    });
  } else
    menuContext.addChildMenuItem(menuItemInstance);
});
onBeforeUnmount(() => {
  menuContext.removeChildMenuItem(menuItemInstance);
});
//Click handler
function onClick(e: MouseEvent|KeyboardEvent) {
  //Ignore clicking when disabled
  if (disabled.value)
    return;
  //Ignore clicking when click on some special elements
  if (e) {
    const currentTarget = e.target as HTMLElement;
    if (currentTarget.classList.contains('mx-context-no-clickable'))
      return;
    if (globalIgnoreClickClassName && currentTarget.classList.contains(globalIgnoreClickClassName))
      return;
    if (globalClickCloseClassName && currentTarget.classList.contains(globalClickCloseClassName)) {
      e.stopPropagation();
      globalCloseMenu(props.rawMenuItem);
      return;
    }
  }
  //Has submenu?
  if (hasChildren.value) {
    if (clickableWhenHasChildren.value) {
      if (typeof clickHandler.value === 'function')
        clickHandler.value(e);
      emit('click', e);
    }
    else if (!showSubMenu.value)
      onMouseEnter();
  } else {
    //Call hander from options
    if (typeof clickHandler.value === 'function')
      clickHandler.value(e);
    emit('click', e);
    if (clickClose.value) {
      //emit close
      globalCloseMenu(props.rawMenuItem);
    }
  }
}
//MouseEnter handler: show item submenu
function onMouseEnter(e?: MouseEvent) {
  //Clear keyBoard focus style
  keyBoardFocusMenu.value = false;
  //等待一个延时,以防止用户过快移动鼠标导致菜单隐藏
  //Wait for a delay to prevent the menu from being hidden due to the user moving the mouse too fast
  if (!menuContext.checkCloseOtherSubMenuTimeOut())
    menuContext.closeOtherSubMenu();
  if (!disabled.value) {
    //Mark current item
    menuContext.markActiveMenuItem(menuItemInstance);
    if (hasChildren.value) {
      if (!e)
        menuContext.markThisOpenedByKeyBoard();
      //Open sub menu
      menuContext.addOpenedSubMenu(() => {
        keyBoardFocusMenu.value = false;
        showSubMenu.value = false;
        emit('subMenuClose');
      });
      showSubMenu.value = true;
      emit('subMenuOpen');
    }
  }
}
//Data for custom render
function getItemDataForChildren() {
  return {
    disabled: disabled.value,
    label: label.value,
    icon: icon.value,
    iconFontClass: iconFontClass.value,
    showRightArrow: showRightArrow.value,
    clickClose: clickClose.value,
    clickableWhenHasChildren: clickableWhenHasChildren.value,
    shortcut: shortcut.value,
    theme: globalTheme,
    isOpen: showSubMenu,
    hasChildren: hasChildren,
    onClick,
    onMouseEnter,
    closeMenu: globalCloseMenu,
  }
}
defineExpose({
  showSubMenu,
  keyBoardFocusMenu,
});
</script>
<style>
</style>