| | |
| | | <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> |
| | |
| | | </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' |
| | |
| | | |
| | | 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: { |
| | |
| | | */ |
| | | 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, |
| | | } |
| | | |
| | |
| | | //当前菜单条目是在整体加载完成后才显示的,此时菜单顺序已经无法知道, |
| | | //所以这里需要在父级元素中查找得出当前菜单的位置。 |
| | | // |
| | | //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 { |
| | |
| | | defineExpose({ |
| | | showSubMenu, |
| | | keyBoardFocusMenu, |
| | | }); |
| | | }) |
| | | </script> |
| | | |
| | | <style> |
| | | </style> |
| | | <style></style> |