¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |