<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>
|