import { computed, defineComponent, getCurrentInstance, inject, onBeforeUnmount, onMounted, provide, reactive, } from 'vue'; import omit from 'omit.js'; import PropTypes from '../_util/vue-types'; import Trigger from '../vc-trigger'; import KeyCode from '../_util/KeyCode'; import SubPopupMenu from './SubPopupMenu'; import placements from './placements'; import BaseMixin from '../_util/BaseMixin'; import { getComponent, splitAttrs, findDOMNode, getSlot } from '../_util/props-util'; import { requestAnimationTimeout, cancelAnimationTimeout } from '../_util/requestAnimationTimeout'; import { noop, getMenuIdFromSubMenuEventKey, loopMenuItemRecursively } from './util'; import { getTransitionProps, Transition } from '../_util/transition'; import InjectExtraProps from './InjectExtraProps'; let guid = 0; const popupPlacementMap = { horizontal: 'bottomLeft', vertical: 'rightTop', 'vertical-left': 'rightTop', 'vertical-right': 'leftTop', }; const updateDefaultActiveFirst = (store, eventKey, defaultActiveFirst) => { const menuId = getMenuIdFromSubMenuEventKey(eventKey); store.defaultActiveFirst[menuId] = defaultActiveFirst; }; let indexGuid = 0; const SubMenu = defineComponent({ name: 'SubMenu', mixins: [BaseMixin], inheritAttrs: false, isSubMenu: true, props: { title: PropTypes.any, openKeys: PropTypes.array.def([]), openChange: PropTypes.func.def(noop), rootPrefixCls: PropTypes.string, eventKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), multiple: PropTypes.looseBool, isRootMenu: PropTypes.looseBool.def(false), index: PropTypes.number, triggerSubMenuAction: PropTypes.string, popupClassName: PropTypes.string, getPopupContainer: PropTypes.func, forceSubMenuRender: PropTypes.looseBool.def(false), openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), disabled: PropTypes.looseBool, subMenuOpenDelay: PropTypes.number.def(0.1), subMenuCloseDelay: PropTypes.number.def(0.1), level: PropTypes.number.def(1), inlineIndent: PropTypes.number.def(24), openTransitionName: PropTypes.string, popupOffset: PropTypes.array, mode: PropTypes.oneOf([ 'horizontal', 'vertical', 'vertical-left', 'vertical-right', 'inline', ]).def('vertical'), manualRef: PropTypes.func.def(noop), builtinPlacements: PropTypes.object.def(() => ({})), itemIcon: PropTypes.any, expandIcon: PropTypes.any, subMenuKey: PropTypes.string, theme: PropTypes.string, parentUniKeys: PropTypes.array.def(() => []), parentUniKey: PropTypes.string, }, isSubMenu: true, setup(props) { const uniKey = `sub_menu_${++indexGuid}`; const store = inject('menuStore', () => ({})); onMounted(() => { store.addChildrenInfo( uniKey, computed(() => ({ parentUniKeys: props.parentUniKeys, parentUniKey: props.parentUniKey, eventKey: props.eventKey, disabled: props.disabled, })), ); }); onBeforeUnmount(() => { store.removeChildrenInfo(uniKey); }); const isChildrenSelected = computed(() => { return store.selectedParentUniKeys.indexOf(uniKey) !== -1; }); const ins = getCurrentInstance(); const getEl = () => { return ins.vnode.el; }; provide( 'parentMenu', reactive({ isRootMenu: computed(() => props.isRootMenu), getPopupContainer: props.getPopupContainer, getEl, }), ); return { parentMenu: inject('parentMenu', undefined), store, isChildrenSelected, childrenUniKeys: [...props.parentUniKeys, uniKey], uniKey, isOpen: computed(() => store.openKeys.indexOf(props.eventKey) > -1), active: computed(() => store.activeKey[props.subMenuKey] === props.eventKey), }; }, data() { const props = this.$props; const store = this.store; const eventKey = props.eventKey; const defaultActiveFirst = store.defaultActiveFirst; let value = false; if (defaultActiveFirst) { value = defaultActiveFirst[eventKey]; } updateDefaultActiveFirst(store, eventKey, value); this.internalMenuId = undefined; this.haveRendered = undefined; this.haveOpened = undefined; this.subMenuTitle = undefined; return {}; }, mounted() { this.$nextTick(() => { this.handleUpdated(); }); }, updated() { this.$nextTick(() => { this.handleUpdated(); }); }, beforeUnmount() { const { eventKey } = this; this.__emit('destroy', eventKey); /* istanbul ignore if */ if (this.minWidthTimeout) { cancelAnimationTimeout(this.minWidthTimeout); this.minWidthTimeout = null; } /* istanbul ignore if */ if (this.mouseenterTimeout) { cancelAnimationTimeout(this.mouseenterTimeout); this.mouseenterTimeout = null; } }, methods: { isChildrenSelected2() { if (this.haveOpened) return this.isChildrenSelected; const ret = { find: false }; loopMenuItemRecursively(getSlot(this), this.store.selectedKeys, ret); return ret.find; }, handleUpdated() { const { mode, manualRef } = this.$props; // invoke customized ref to expose component to mixin if (manualRef) { manualRef(this); } if (mode !== 'horizontal' || !this.parentMenu.isRootMenu || !this.isOpen) { return; } this.minWidthTimeout = requestAnimationTimeout(() => this.adjustWidth(), 0); }, onKeyDown(e) { const keyCode = e.keyCode; const menu = this.menuInstance; const { isOpen } = this; if (keyCode === KeyCode.ENTER) { this.onTitleClick(e); updateDefaultActiveFirst(this.store, this.$props.eventKey, true); return true; } if (keyCode === KeyCode.RIGHT) { if (isOpen) { menu.onKeyDown(e); } else { this.triggerOpenChange(true); // need to update current menu's defaultActiveFirst value updateDefaultActiveFirst(this.store, this.$props.eventKey, true); } return true; } if (keyCode === KeyCode.LEFT) { let handled; if (isOpen) { handled = menu.onKeyDown(e); } else { return undefined; } if (!handled) { this.triggerOpenChange(false); handled = true; } return handled; } if (isOpen && (keyCode === KeyCode.UP || keyCode === KeyCode.DOWN)) { return menu.onKeyDown(e); } return undefined; }, onPopupVisibleChange(visible) { this.triggerOpenChange(visible, visible ? 'mouseenter' : 'mouseleave'); }, onMouseEnter(e) { const { eventKey: key } = this.$props; updateDefaultActiveFirst(this.store, key, false); this.__emit('mouseenter', { key, domEvent: e, }); }, onMouseLeave(e) { const { eventKey } = this.$props; this.__emit('mouseleave', { key: eventKey, domEvent: e, }); }, onTitleMouseEnter(domEvent) { const { eventKey: key } = this.$props; this.__emit('itemHover', { key, hover: true, }); this.__emit('titleMouseenter', { key, domEvent, }); }, onTitleMouseLeave(e) { const { eventKey } = this.$props; this.__emit('itemHover', { key: eventKey, hover: false, }); this.__emit('titleMouseleave', { key: eventKey, domEvent: e, }); }, onTitleClick(e) { const { triggerSubMenuAction, eventKey } = this.$props; this.__emit('titleClick', { key: eventKey, domEvent: e, }); if (triggerSubMenuAction === 'hover') { return; } this.triggerOpenChange(!this.isOpen, 'click'); updateDefaultActiveFirst(this.store, eventKey, false); }, onSubMenuClick(info) { this.__emit('click', this.addKeyPath(info)); }, getPrefixCls() { return `${this.$props.rootPrefixCls}-submenu`; }, getActiveClassName() { return `${this.getPrefixCls()}-active`; }, getDisabledClassName() { return `${this.getPrefixCls()}-disabled`; }, getSelectedClassName() { return `${this.getPrefixCls()}-selected`; }, getOpenClassName() { return `${this.$props.rootPrefixCls}-submenu-open`; }, saveMenuInstance(c) { // children menu instance this.menuInstance = c; }, addKeyPath(info) { return { ...info, keyPath: (info.keyPath || []).concat(this.$props.eventKey), }; }, triggerOpenChange(open, type) { const key = this.$props.eventKey; const openChange = () => { this.__emit('openChange', { key, item: this.$props, trigger: type, open, }); }; if (type === 'mouseenter') { // make sure mouseenter happen after other menu item's mouseleave this.mouseenterTimeout = requestAnimationTimeout(() => { openChange(); }, 0); } else { openChange(); } }, adjustWidth() { /* istanbul ignore if */ if (!this.subMenuTitle || !this.menuInstance) { return; } const popupMenu = findDOMNode(this.menuInstance); if (popupMenu.offsetWidth >= this.subMenuTitle.offsetWidth) { return; } /* istanbul ignore next */ popupMenu.style.minWidth = `${this.subMenuTitle.offsetWidth}px`; }, saveSubMenuTitle(subMenuTitle) { this.subMenuTitle = subMenuTitle; }, renderChildren() { const props = { ...this.$props, ...this.$attrs }; const subPopupMenuProps = { mode: props.mode === 'horizontal' ? 'vertical' : props.mode, visible: this.isOpen, level: props.level + 1, inlineIndent: props.inlineIndent, focusable: false, eventKey: `${props.eventKey}-menu-`, openKeys: props.openKeys, openTransitionName: props.openTransitionName, openAnimation: props.openAnimation, subMenuOpenDelay: props.subMenuOpenDelay, subMenuCloseDelay: props.subMenuCloseDelay, forceSubMenuRender: props.forceSubMenuRender, triggerSubMenuAction: props.triggerSubMenuAction, builtinPlacements: props.builtinPlacements, multiple: props.multiple, prefixCls: props.rootPrefixCls, manualRef: this.saveMenuInstance, itemIcon: getComponent(this, 'itemIcon'), expandIcon: getComponent(this, 'expandIcon'), onClick: this.onSubMenuClick, onSelect: props.onSelect || noop, onDeselect: props.onDeselect || noop, onOpenChange: props.onOpenChange || noop, id: this.internalMenuId, parentUniKeys: this.childrenUniKeys, parentUniKey: this.uniKey, }; const haveRendered = this.haveRendered; this.haveRendered = true; this.haveOpened = this.haveOpened || subPopupMenuProps.visible || subPopupMenuProps.forceSubMenuRender; // never rendered not planning to, don't render if (!this.haveOpened) { return
; } // don't show transition on first rendering (no animation for opened menu) // show appear transition if it's not visible (not sure why) // show appear transition if it's not inline mode const transitionAppear = haveRendered || !subPopupMenuProps.visible || !subPopupMenuProps.mode === 'inline'; subPopupMenuProps.class = ` ${subPopupMenuProps.prefixCls}-sub`; let transitionProps = { appear: transitionAppear, css: false }; if (subPopupMenuProps.openTransitionName) { transitionProps = getTransitionProps(subPopupMenuProps.openTransitionName, { appear: transitionAppear, }); } else if (typeof subPopupMenuProps.openAnimation === 'object') { transitionProps = { ...transitionProps, ...(subPopupMenuProps.openAnimation || {}) }; if (!transitionAppear) { transitionProps.appear = false; } } else if (typeof subPopupMenuProps.openAnimation === 'string') { transitionProps = getTransitionProps(subPopupMenuProps.openAnimation, { appear: transitionAppear, }); } return ( ); }, }, render() { const props = { ...this.$props, ...this.$attrs }; const { onEvents } = splitAttrs(props); const isOpen = this.isOpen; const prefixCls = this.getPrefixCls(); const isInlineMode = props.mode === 'inline'; const children = this.renderChildren(); const className = { [prefixCls]: true, [`${prefixCls}-${props.mode}`]: true, [props.class]: !!props.class, [this.getOpenClassName()]: isOpen, [this.getActiveClassName()]: this.active || (isOpen && !isInlineMode), [this.getDisabledClassName()]: props.disabled, [this.getSelectedClassName()]: this.isChildrenSelected || this.isChildrenSelected2(), }; if (!this.internalMenuId) { if (props.eventKey) { this.internalMenuId = `${props.eventKey}$Menu`; } else { this.internalMenuId = `$__$${++guid}$Menu`; } } let mouseEvents = {}; let titleClickEvents = {}; let titleMouseEvents = {}; if (!props.disabled) { mouseEvents = { onMouseleave: this.onMouseLeave, onMouseenter: this.onMouseEnter, }; // only works in title, not outer li titleClickEvents = { onClick: this.onTitleClick, }; titleMouseEvents = { onMouseenter: this.onTitleMouseEnter, onMouseleave: this.onTitleMouseLeave, }; } const style = {}; if (isInlineMode) { style.paddingLeft = `${props.inlineIndent * props.level}px`; } let ariaOwns = {}; // only set aria-owns when menu is open // otherwise it would be an invalid aria-owns value // since corresponding node cannot be found if (isOpen) { ariaOwns = { 'aria-owns': this.internalMenuId, }; } const titleProps = { 'aria-expanded': isOpen, ...ariaOwns, 'aria-haspopup': 'true', title: typeof props.title === 'string' ? props.title : undefined, ...titleMouseEvents, ...titleClickEvents, style, class: `${prefixCls}-title`, ref: this.saveSubMenuTitle, }; // expand custom icon should NOT be displayed in menu with horizontal mode. let icon = null; if (props.mode !== 'horizontal') { icon = getComponent(this, 'expandIcon', props); } const title = (
{getComponent(this, 'title')} {icon || }
); const getPopupContainer = this.parentMenu.isRootMenu ? this.parentMenu.getPopupContainer : triggerNode => triggerNode.parentNode; const popupPlacement = popupPlacementMap[props.mode]; const popupAlign = props.popupOffset ? { offset: props.popupOffset } : {}; let popupClassName = props.mode === 'inline' ? '' : props.popupClassName || ''; popupClassName = `${prefixCls}-popup ${popupClassName}`; const liProps = { ...omit(onEvents, ['onClick']), ...mouseEvents, class: className, style: props.style, }; return (
  • {isInlineMode && title} {isInlineMode && children} {!isInlineMode && ( {title} )}
  • ); }, }); export default InjectExtraProps(SubMenu);