222
schangxiang@126.com
2025-04-30 9bec4dcae002f36aa23231da11cb03a156b40110
PipeLineLems/web/src/components/vue3-context-menu/ContextMenuItem.vue
@@ -1,61 +1,133 @@
<template>
  <div v-if="!hidden" class="mx-context-menu-item-wrapper" ref="menuItemRef" data-type="ContextMenuItem">
  <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()" />
    <VNodeRender
      v-if="globalHasSlot('itemRender')"
      :vnode="() => globalRenderSlot('itemRender', getItemDataForChildren())"
    />
    <VNodeRender
      v-else-if="customRender"
      :vnode="customRender"
      :data="getItemDataForChildren()"
    />
    <!--Default item-->
    <div
    <div
      v-else
      :class="[
        'mx-context-menu-item',
        (disabled ? 'disabled' : ''),
        (keyBoardFocusMenu ? 'keyboard-focus' : ''),
        (customClass ? (' ' + customClass) : ''),
        (showSubMenu ? 'open' : ''),
        disabled ? 'disabled' : '',
        keyBoardFocusMenu ? 'keyboard-focus' : '',
        customClass ? ' ' + customClass : '',
        showSubMenu ? 'open' : '',
      ]"
      @click="onClick"
      @touchstart="onTouchStart"
      @mouseenter="onMouseEnter"
    >
      <slot>
        <div class="mx-item-row">
          <div :class="[
            'mx-icon-placeholder',
            preserveIconWidth ? 'preserve-width': '',
          ]">
          <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">
              <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>
              <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())" />
              <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-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())" />
            <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())" />
            <VNodeRender
              v-if="globalHasSlot('itemRightArrowRender')"
              :vnode="
                () =>
                  globalRenderSlot(
                    'itemRightArrowRender',
                    getItemDataForChildren()
                  )
              "
            />
            <ContextMenuIconRight />
          </slot>
        </div>
      </slot>
    </div>
    <!--Sub menu render-->
    <Transition v-if="globalMenuTransitionProps" v-bind="globalMenuTransitionProps">
    <Transition
      v-if="globalMenuTransitionProps"
      v-bind="globalMenuTransitionProps"
    >
      <slot v-if="showSubMenu" name="submenu"></slot>
    </Transition>
    <slot v-else-if="showSubMenu" name="submenu"></slot>
@@ -63,8 +135,21 @@
</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 {
  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'
@@ -77,94 +162,94 @@
const props = defineProps({
  /**
   * Is this menu disabled?
   * Is this menu disabled?
   */
  disabled: {
    type: Boolean,
    default: false
    default: false,
  },
  /**
   * Is this menu hidden?
   * Is this menu hidden?
   */
  hidden: {
    type: Boolean,
    default: false
    default: false,
  },
  customRender: {
    type: Function,
    default: null
    default: null,
  },
  /**
   * Custom css class for submenu
   */
  customClass: {
    type: String,
    default: ''
    default: '',
  },
  clickHandler: {
    type: Function as PropType<(e: MouseEvent|KeyboardEvent) => void>,
    default: null
    type: Function as PropType<(e: MouseEvent | KeyboardEvent) => void>,
    default: null,
  },
  /**
   * Menu label
   */
  label: {
    type: [String, Object, Function],
    default: ''
    default: '',
  },
  /**
   * Menu icon (for icon class)
   */
  icon: {
    type: [String, Object, Function],
    default: ''
    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'
    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
    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: ''
    default: '',
  },
  /**
   * Display icons use svg symbol (`<use xlink:href="#icon-symbol-name">`) , only valid when icon attribute is empty.
   */
  svgIcon: {
    type: String,
    default: ''
    default: '',
  },
  /**
   * The user-defined attribute of the svg tag, which is valid when using `svgIcon`.
   */
  svgProps: {
    type: Object as PropType<SVGAttributes>,
    default: null
    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: {
@@ -176,76 +261,85 @@
   */
  showRightArrow: {
    type: Boolean,
    default: false
    default: false,
  },
  hasChildren: {
    type: Boolean,
    default: false
    default: false,
  },
  /**
   * Should close menu when Click this menu item ?
   */
  clickClose: {
    type: Boolean,
    default: true
    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
    default: false,
  },
  rawMenuItem: {
    type: Object as PropType<MenuItem>,
    default: undefined
    default: undefined,
  },
});
const emit = defineEmits([
  'click',
  'subMenuOpen',
  'subMenuClose',
])
})
const emit = defineEmits(['click', 'subMenuOpen', 'subMenuClose'])
const {
  clickHandler, clickClose, clickableWhenHasChildren, disabled, hidden,
  label, icon, iconFontClass,
  showRightArrow, shortcut,
const {
  clickHandler,
  clickClose,
  clickableWhenHasChildren,
  disabled,
  hidden,
  label,
  icon,
  iconFontClass,
  showRightArrow,
  shortcut,
  hasChildren,
} = toRefs(props);
const showSubMenu = ref(false);
const keyBoardFocusMenu = ref(false);
} = toRefs(props)
const showSubMenu = ref(false)
const keyBoardFocusMenu = ref(false)
const menuItemRef = ref<HTMLElement>();
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 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;
const menuContext = inject('menuContext') as SubMenuParentContext
//Instance Contet for keyboadr control
const menuItemInstance : MenuItemContext = {
const menuItemInstance: MenuItemContext = {
  showSubMenu: () => {
    if (showSubMenu.value) {
      //Mark current item
      menuContext.markActiveMenuItem(menuItemInstance, true);
      return true;
      menuContext.markActiveMenuItem(menuItemInstance, true)
      return true
    } else if (hasChildren.value) {
      onMouseEnter();
      return true;
      onMouseEnter()
      return true
    }
    return false;
    return false
  },
  isDisabledOrHidden: () => disabled.value || hidden.value,
  getElement: () => menuItemRef.value,
  focus: () => keyBoardFocusMenu.value = true,
  blur: () => keyBoardFocusMenu.value = false,
  focus: () => (keyBoardFocusMenu.value = true),
  blur: () => (keyBoardFocusMenu.value = false),
  click: onClick,
}
@@ -254,102 +348,104 @@
    //当前菜单条目是在整体加载完成后才显示的,此时菜单顺序已经无法知道,
    //所以这里需要在父级元素中查找得出当前菜单的位置。
    //
    //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
    //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();
      let index = 0
      const parentEl = menuContext.getElement()
      if (parentEl) {
        let indexCounting = 0;
        let indexCounting = 0
        for (let i = 0; i < parentEl.children.length; i++) {
          const el = parentEl.children[i];
          const el = parentEl.children[i]
          if (el.getAttribute('data-type') === 'ContextMenuItem') {
            if (el === menuItemRef.value) {
              index = indexCounting;
              break;
              index = indexCounting
              break
            }
            indexCounting++;
            indexCounting++
          }
        }
      }
      //Insert to pos
      menuContext.addChildMenuItem(menuItemInstance, index);
    });
  } else
    menuContext.addChildMenuItem(menuItemInstance);
});
      menuContext.addChildMenuItem(menuItemInstance, index)
    })
  } else menuContext.addChildMenuItem(menuItemInstance)
})
onBeforeUnmount(() => {
  menuContext.removeChildMenuItem(menuItemInstance);
});
  menuContext.removeChildMenuItem(menuItemInstance)
})
function onTouchStart(e: TouchEvent) {
  e?.stopPropagation()
}
//Click handler
function onClick(e: MouseEvent|KeyboardEvent) {
function onClick(e: MouseEvent | KeyboardEvent) {
  //Ignore clicking when disabled
  if (disabled.value)
    return;
  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;
    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();
      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 (typeof clickHandler.value === 'function') clickHandler.value(e)
    emit('click', e)
    if (clickClose.value) {
      //emit close
      globalCloseMenu(props.rawMenuItem);
      globalCloseMenu(props.rawMenuItem)
    }
  }
}
//MouseEnter handler: show item submenu
function onMouseEnter(e?: MouseEvent) {
  //Clear keyBoard focus style
  keyBoardFocusMenu.value = false;
  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();
    menuContext.closeOtherSubMenu()
  if (!disabled.value) {
    //Mark current item
    menuContext.markActiveMenuItem(menuItemInstance);
    menuContext.markActiveMenuItem(menuItemInstance)
    if (hasChildren.value) {
      if (!e)
        menuContext.markThisOpenedByKeyBoard();
      if (!e) menuContext.markThisOpenedByKeyBoard()
      //Open sub menu
      menuContext.addOpenedSubMenu(() => {
        keyBoardFocusMenu.value = false;
        showSubMenu.value = false;
        emit('subMenuClose');
      });
      showSubMenu.value = true;
      emit('subMenuOpen');
        keyBoardFocusMenu.value = false
        showSubMenu.value = false
        emit('subMenuClose')
      })
      showSubMenu.value = true
      emit('subMenuOpen')
    }
  }
}
//Data for custom render
function getItemDataForChildren() {
  return {
@@ -373,8 +469,7 @@
defineExpose({
  showSubMenu,
  keyBoardFocusMenu,
});
})
</script>
<style>
</style>
<style></style>