import omit from 'omit.js'; import PropTypes from '../_util/vue-types'; import Trigger from '../vc-trigger'; import KeyCode from '../_util/KeyCode'; import { connect } from '../_util/store'; import SubPopupMenu from './SubPopupMenu'; import placements from './placements'; import BaseMixin from '../_util/BaseMixin'; import { getComponentFromProp, filterEmpty } from '../_util/props-util'; import { requestAnimationTimeout, cancelAnimationTimeout } from '../_util/requestAnimationTimeout'; import { noop, loopMenuItemRecursively, getMenuIdFromSubMenuEventKey } from './util'; import getTransitionProps from '../_util/getTransitionProps'; let guid = 0; const popupPlacementMap = { horizontal: 'bottomLeft', vertical: 'rightTop', 'vertical-left': 'rightTop', 'vertical-right': 'leftTop', }; const updateDefaultActiveFirst = (store, eventKey, defaultActiveFirst) => { const menuId = getMenuIdFromSubMenuEventKey(eventKey); const state = store.getState(); store.setState({ defaultActiveFirst: { ...state.defaultActiveFirst, [menuId]: defaultActiveFirst, }, }); }; const SubMenu = { name: 'SubMenu', props: { parentMenu: PropTypes.object, title: PropTypes.any, selectedKeys: PropTypes.array.def([]), openKeys: PropTypes.array.def([]), openChange: PropTypes.func.def(noop), rootPrefixCls: PropTypes.string, eventKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), multiple: PropTypes.bool, active: PropTypes.bool, // TODO: remove isRootMenu: PropTypes.bool.def(false), index: PropTypes.number, triggerSubMenuAction: PropTypes.string, popupClassName: PropTypes.string, getPopupContainer: PropTypes.func, forceSubMenuRender: PropTypes.bool, openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), disabled: PropTypes.bool, 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, isOpen: PropTypes.bool, store: PropTypes.object, 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, }, mixins: [BaseMixin], isSubMenu: true, data() { const props = this.$props; const store = props.store; const eventKey = props.eventKey; const defaultActiveFirst = store.getState().defaultActiveFirst; let value = false; if (defaultActiveFirst) { value = defaultActiveFirst[eventKey]; } updateDefaultActiveFirst(store, eventKey, value); return { // defaultActiveFirst: false, }; }, mounted() { this.$nextTick(() => { this.handleUpdated(); }); }, updated() { this.$nextTick(() => { this.handleUpdated(); }); }, beforeDestroy() { 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: { handleUpdated() { const { mode, parentMenu, manualRef } = this.$props; // invoke customized ref to expose component to mixin if (manualRef) { manualRef(this); } if (mode !== 'horizontal' || !parentMenu.isRootMenu || !this.isOpen) { return; } this.minWidthTimeout = requestAnimationTimeout(() => this.adjustWidth(), 0); }, onKeyDown(e) { const keyCode = e.keyCode; const menu = this.menuInstance; const { store, isOpen } = this.$props; if (keyCode === KeyCode.ENTER) { this.onTitleClick(e); updateDefaultActiveFirst(store, this.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(store, this.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); } }, onPopupVisibleChange(visible) { this.triggerOpenChange(visible, visible ? 'mouseenter' : 'mouseleave'); }, onMouseEnter(e) { const { eventKey: key, store } = this.$props; updateDefaultActiveFirst(store, key, false); this.__emit('mouseenter', { key, domEvent: e, }); }, onMouseLeave(e) { const { eventKey, parentMenu } = this; parentMenu.subMenuInstance = this; // parentMenu.subMenuLeaveFn = () => { // // trigger mouseleave // this.__emit('mouseleave', { // key: eventKey, // domEvent: e, // }) // } this.__emit('mouseleave', { key: eventKey, domEvent: e, }); // prevent popup menu and submenu gap // parentMenu.subMenuLeaveTimer = setTimeout(parentMenu.subMenuLeaveFn, 100) }, onTitleMouseEnter(domEvent) { const { eventKey: key } = this.$props; // this.clearSubMenuTitleLeaveTimer() this.__emit('itemHover', { key, hover: true, }); this.__emit('titleMouseenter', { key, domEvent, }); }, onTitleMouseLeave(e) { const { eventKey, parentMenu } = this; parentMenu.subMenuInstance = this; this.__emit('itemHover', { key: eventKey, hover: false, }); this.__emit('titleMouseleave', { key: eventKey, domEvent: e, }); }, onTitleClick(e) { const { triggerSubMenuAction, eventKey, isOpen, store } = this.$props; this.__emit('titleClick', { key: eventKey, domEvent: e, }); if (triggerSubMenuAction === 'hover') { return; } this.triggerOpenChange(!isOpen, 'click'); updateDefaultActiveFirst(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 // this.__emit('openChange', { // key, // item: this, // trigger: type, // open, // }) // }, triggerOpenChange(open, type) { const key = this.$props.eventKey; const openChange = () => { this.__emit('openChange', { key, item: this, trigger: type, open, }); }; if (type === 'mouseenter') { // make sure mouseenter happen after other menu item's mouseleave this.mouseenterTimeout = requestAnimationTimeout(() => { openChange(); }, 0); } else { openChange(); } }, isChildrenSelected() { const ret = { find: false }; loopMenuItemRecursively(this.$slots.default, this.$props.selectedKeys, ret); return ret.find; }, // isOpen () { // return this.$props.openKeys.indexOf(this.$props.eventKey) !== -1 // }, adjustWidth() { /* istanbul ignore if */ if (!this.$refs.subMenuTitle || !this.menuInstance) { return; } const popupMenu = this.menuInstance.$el; if (popupMenu.offsetWidth >= this.$refs.subMenuTitle.offsetWidth) { return; } /* istanbul ignore next */ popupMenu.style.minWidth = `${this.$refs.subMenuTitle.offsetWidth}px`; }, renderChildren(children) { const props = this.$props; const { select, deselect, openChange } = this.$listeners; const subPopupMenuProps = { props: { mode: props.mode === 'horizontal' ? 'vertical' : props.mode, visible: props.isOpen, level: props.level + 1, inlineIndent: props.inlineIndent, focusable: false, selectedKeys: props.selectedKeys, eventKey: `${props.eventKey}-menu-`, openKeys: props.openKeys, openTransitionName: props.openTransitionName, openAnimation: props.openAnimation, subMenuOpenDelay: props.subMenuOpenDelay, parentMenu: this, subMenuCloseDelay: props.subMenuCloseDelay, forceSubMenuRender: props.forceSubMenuRender, triggerSubMenuAction: props.triggerSubMenuAction, builtinPlacements: props.builtinPlacements, defaultActiveFirst: props.store.getState().defaultActiveFirst[ getMenuIdFromSubMenuEventKey(props.eventKey) ], multiple: props.multiple, prefixCls: props.rootPrefixCls, manualRef: this.saveMenuInstance, itemIcon: getComponentFromProp(this, 'itemIcon'), expandIcon: getComponentFromProp(this, 'expandIcon'), children, }, on: { click: this.onSubMenuClick, select, deselect, openChange, }, id: this._menuId, }; const baseProps = subPopupMenuProps.props; const haveRendered = this.haveRendered; this.haveRendered = true; this.haveOpened = this.haveOpened || baseProps.visible || baseProps.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 || !baseProps.visible || !baseProps.mode === 'inline'; subPopupMenuProps.class = ` ${baseProps.prefixCls}-sub`; let animProps = { appear: transitionAppear, css: false }; let transitionProps = { props: animProps, on: {}, }; if (baseProps.openTransitionName) { transitionProps = getTransitionProps(baseProps.openTransitionName, { appear: transitionAppear, }); } else if (typeof baseProps.openAnimation === 'object') { animProps = { ...animProps, ...(baseProps.openAnimation.props || {}) }; if (!transitionAppear) { animProps.appear = false; } } else if (typeof baseProps.openAnimation === 'string') { transitionProps = getTransitionProps(baseProps.openAnimation, { appear: transitionAppear }); } if (typeof baseProps.openAnimation === 'object' && baseProps.openAnimation.on) { transitionProps.on = baseProps.openAnimation.on; } return (