ant-design-vue/components/vc-menu/SubMenu.jsx

507 lines
14 KiB
React
Raw Normal View History

2018-03-19 02:16:27 +00:00
2018-02-06 09:22:36 +00:00
import PropTypes from '../_util/vue-types'
import Trigger from '../trigger'
import KeyCode from '../_util/KeyCode'
2017-12-08 06:31:53 +00:00
import SubPopupMenu from './SubPopupMenu'
import placements from './placements'
2018-01-03 10:30:12 +00:00
import { loopMenuItemRecusively, noop } from './util'
2018-02-06 09:22:36 +00:00
import BaseMixin from '../_util/BaseMixin'
import { getComponentFromProp } from '../_util/props-util'
2018-03-01 11:09:45 +00:00
import { requestAnimationTimeout, cancelAnimationTimeout } from '../_util/requestAnimationTimeout'
2017-12-08 06:31:53 +00:00
let guid = 0
const popupPlacementMap = {
horizontal: 'bottomLeft',
vertical: 'rightTop',
'vertical-left': 'rightTop',
'vertical-right': 'leftTop',
}
2018-01-03 10:30:12 +00:00
export default {
2017-12-29 10:45:11 +00:00
name: 'SubMenu',
props: {
2018-01-02 11:05:02 +00:00
mode: PropTypes.oneOf(['horizontal', 'vertical', 'vertical-left', 'vertical-right', 'inline']).def('vertical'),
2018-01-17 08:12:53 +00:00
title: PropTypes.any,
2018-01-03 10:30:12 +00:00
selectedKeys: PropTypes.array.def([]),
openKeys: PropTypes.array.def([]),
openChange: PropTypes.func.def(noop),
2017-12-08 06:31:53 +00:00
rootPrefixCls: PropTypes.string,
2018-02-22 04:07:37 +00:00
eventKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
2017-12-08 06:31:53 +00:00
multiple: PropTypes.bool,
active: PropTypes.bool, // TODO: remove
2018-01-03 10:30:12 +00:00
isRootMenu: PropTypes.bool,
2018-01-09 06:21:15 +00:00
index: PropTypes.number,
2017-12-08 06:31:53 +00:00
triggerSubMenuAction: PropTypes.string,
2018-01-02 11:05:02 +00:00
popupClassName: PropTypes.string,
2018-01-03 10:30:12 +00:00
getPopupContainer: PropTypes.func,
test: PropTypes.any,
2018-01-04 11:06:54 +00:00
forceSubMenuRender: PropTypes.bool,
openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
disabled: PropTypes.bool,
2018-01-29 11:05:36 +00:00
subMenuOpenDelay: PropTypes.number.def(0.1),
2018-01-08 10:31:04 +00:00
subMenuCloseDelay: PropTypes.number.def(0.1),
level: PropTypes.number.def(1),
inlineIndent: PropTypes.number.def(24),
2018-01-17 08:12:53 +00:00
openTransitionName: PropTypes.string,
2018-01-02 11:05:02 +00:00
},
2018-01-08 10:31:04 +00:00
inject: {
parentMenuContext: { default: undefined },
},
2018-01-12 08:10:41 +00:00
mixins: [BaseMixin],
2018-01-04 11:06:54 +00:00
isSubMenu: true,
2018-01-02 11:05:02 +00:00
data () {
2017-12-08 06:31:53 +00:00
return {
defaultActiveFirst: false,
}
},
2018-01-02 11:05:02 +00:00
mounted () {
2018-02-02 11:09:38 +00:00
this.$nextTick(() => {
this.handleUpdated()
})
2017-12-08 06:31:53 +00:00
},
2018-01-02 11:05:02 +00:00
updated () {
2018-02-02 11:09:38 +00:00
this.$nextTick(() => {
this.handleUpdated()
})
2017-12-08 06:31:53 +00:00
},
2018-01-02 11:05:02 +00:00
beforeDestroy () {
2018-01-17 08:12:53 +00:00
const { eventKey } = this
2018-01-12 08:10:41 +00:00
this.__emit('destroy', eventKey)
2018-01-17 08:12:53 +00:00
// if (parentMenuContext.subMenuInstance === this) {
// this.clearSubMenuTimers()
// }
if (this.minWidthTimeout) {
2018-03-01 11:09:45 +00:00
cancelAnimationTimeout(this.minWidthTimeout)
2018-03-01 11:13:08 +00:00
this.minWidthTimeout = null
2018-01-17 08:12:53 +00:00
}
if (this.mouseenterTimeout) {
2018-03-01 11:09:45 +00:00
cancelAnimationTimeout(this.mouseenterTimeout)
2018-03-01 11:13:08 +00:00
this.mouseenterTimeout = null
2018-01-08 10:31:04 +00:00
}
2017-12-08 06:31:53 +00:00
},
2018-01-02 11:05:02 +00:00
methods: {
handleUpdated () {
2018-01-03 10:30:12 +00:00
const { mode, isRootMenu } = this.$props
if (mode !== 'horizontal' || !isRootMenu || !this.isOpen()) {
2018-01-02 11:05:02 +00:00
return
}
2018-01-17 08:12:53 +00:00
const self = this
2018-03-01 11:09:45 +00:00
this.minWidthTimeout = requestAnimationTimeout(() => {
2018-01-17 08:12:53 +00:00
if (!self.$refs.subMenuTitle || !self.$refs.menuInstance) {
2018-01-02 11:05:02 +00:00
return
}
2018-01-17 08:12:53 +00:00
const popupMenu = self.$refs.menuInstance.$el
if (popupMenu.offsetWidth >= self.$refs.subMenuTitle.offsetWidth) {
2018-01-02 11:05:02 +00:00
return
}
2018-01-17 08:12:53 +00:00
popupMenu.style.minWidth = `${self.$refs.subMenuTitle.offsetWidth}px`
2018-01-02 11:05:02 +00:00
}, 0)
},
onKeyDown (e) {
const keyCode = e.keyCode
2018-01-09 06:21:15 +00:00
const menu = this.$refs.menuInstance
2018-01-02 11:05:02 +00:00
const isOpen = this.isOpen()
if (keyCode === KeyCode.ENTER) {
this.onTitleClick(e)
2017-12-08 06:31:53 +00:00
this.setState({
defaultActiveFirst: true,
})
2018-01-02 11:05:02 +00:00
return true
2017-12-08 06:31:53 +00:00
}
2018-01-02 11:05:02 +00:00
if (keyCode === KeyCode.RIGHT) {
if (isOpen) {
menu.onKeyDown(e)
} else {
this.triggerOpenChange(true)
this.setState({
defaultActiveFirst: true,
})
}
return true
2017-12-08 06:31:53 +00:00
}
2018-01-02 11:05:02 +00:00
if (keyCode === KeyCode.LEFT) {
let handled
if (isOpen) {
handled = menu.onKeyDown(e)
} else {
return undefined
}
if (!handled) {
this.triggerOpenChange(false)
handled = true
}
return handled
2017-12-08 06:31:53 +00:00
}
2018-01-02 11:05:02 +00:00
if (isOpen && (keyCode === KeyCode.UP || keyCode === KeyCode.DOWN)) {
return menu.onKeyDown(e)
}
},
2017-12-08 06:31:53 +00:00
2018-01-02 11:05:02 +00:00
onPopupVisibleChange (visible) {
this.triggerOpenChange(visible, visible ? 'mouseenter' : 'mouseleave')
},
2017-12-08 06:31:53 +00:00
2018-01-02 11:05:02 +00:00
onMouseEnter (e) {
const { eventKey: key } = this.$props
2018-01-17 08:12:53 +00:00
// this.clearSubMenuLeaveTimer()
2018-01-02 11:05:02 +00:00
this.setState({
defaultActiveFirst: false,
})
2018-01-12 08:10:41 +00:00
this.__emit('mouseenter', {
2018-01-02 11:05:02 +00:00
key,
2017-12-08 06:31:53 +00:00
domEvent: e,
})
2018-01-02 11:05:02 +00:00
},
onMouseLeave (e) {
const {
eventKey,
2018-01-08 10:31:04 +00:00
parentMenuContext,
} = this
parentMenuContext.subMenuInstance = this
2018-01-17 08:12:53 +00:00
// parentMenuContext.subMenuLeaveFn = () => {
// // trigger mouseleave
// this.__emit('mouseleave', {
// key: eventKey,
// domEvent: e,
// })
// }
this.__emit('mouseleave', {
key: eventKey,
domEvent: e,
})
2018-01-02 11:05:02 +00:00
// prevent popup menu and submenu gap
2018-01-17 08:12:53 +00:00
// parentMenuContext.subMenuLeaveTimer = setTimeout(parentMenuContext.subMenuLeaveFn, 100)
2018-01-02 11:05:02 +00:00
},
2017-12-08 06:31:53 +00:00
2018-01-02 11:05:02 +00:00
onTitleMouseEnter (domEvent) {
const { eventKey: key } = this.$props
2018-01-17 08:12:53 +00:00
// this.clearSubMenuTitleLeaveTimer()
2018-01-12 08:10:41 +00:00
this.__emit('itemHover', {
2018-01-02 11:05:02 +00:00
key,
hover: true,
2017-12-08 06:31:53 +00:00
})
2018-01-12 08:10:41 +00:00
this.__emit('titleMouseenter', {
2018-01-02 11:05:02 +00:00
key,
domEvent,
2017-12-08 06:31:53 +00:00
})
2018-01-02 11:05:02 +00:00
},
onTitleMouseLeave (e) {
2018-01-08 10:31:04 +00:00
const { eventKey, parentMenuContext } = this
parentMenuContext.subMenuInstance = this
2018-01-17 08:12:53 +00:00
this.__emit('itemHover', {
key: eventKey,
hover: false,
})
this.__emit('titleMouseleave', {
key: eventKey,
domEvent: e,
})
// parentMenuContext.subMenuTitleLeaveFn = () => {
// this.__emit('itemHover', {
// key: eventKey,
// hover: false,
// })
// this.__emit('titleMouseleave', {
// key: eventKey,
// domEvent: e,
// })
// }
// parentMenuContext.subMenuTitleLeaveTimer = setTimeout(parentMenuContext.subMenuTitleLeaveFn, 100)
2018-01-02 11:05:02 +00:00
},
2017-12-08 06:31:53 +00:00
2018-01-02 11:05:02 +00:00
onTitleClick (e) {
const { triggerSubMenuAction, eventKey } = this.$props
2018-01-19 10:01:43 +00:00
this.$emit('titleClick', {
2018-01-02 11:05:02 +00:00
key: eventKey,
domEvent: e,
})
if (triggerSubMenuAction === 'hover') {
return
}
this.triggerOpenChange(!this.isOpen(), 'click')
this.setState({
defaultActiveFirst: false,
})
},
2017-12-08 06:31:53 +00:00
2018-01-02 11:05:02 +00:00
onSubMenuClick (info) {
2018-01-12 08:10:41 +00:00
this.__emit('click', this.addKeyPath(info))
2018-01-02 11:05:02 +00:00
},
2017-12-08 06:31:53 +00:00
2018-01-02 11:05:02 +00:00
getPrefixCls () {
return `${this.$props.rootPrefixCls}-submenu`
},
2017-12-08 06:31:53 +00:00
2018-01-02 11:05:02 +00:00
getActiveClassName () {
return `${this.getPrefixCls()}-active`
},
2017-12-08 06:31:53 +00:00
2018-01-02 11:05:02 +00:00
getDisabledClassName () {
return `${this.getPrefixCls()}-disabled`
},
2017-12-08 06:31:53 +00:00
2018-01-02 11:05:02 +00:00
getSelectedClassName () {
return `${this.getPrefixCls()}-selected`
},
2017-12-08 06:31:53 +00:00
2018-01-02 11:05:02 +00:00
getOpenClassName () {
return `${this.$props.rootPrefixCls}-submenu-open`
},
2017-12-08 06:31:53 +00:00
2018-01-02 11:05:02 +00:00
addKeyPath (info) {
return {
...info,
keyPath: (info.keyPath || []).concat(this.$props.eventKey),
}
},
2018-01-17 08:12:53 +00:00
// triggerOpenChange (open, type) {
// const key = this.$props.eventKey
// this.__emit('openChange', {
// key,
// item: this,
// trigger: type,
// open,
// })
// },
2018-01-02 11:05:02 +00:00
triggerOpenChange (open, type) {
const key = this.$props.eventKey
2018-01-17 08:12:53 +00:00
const openChange = () => {
this.__emit('openChange', {
key,
item: this,
trigger: type,
open,
})
2018-01-02 11:05:02 +00:00
}
2018-01-17 08:12:53 +00:00
if (type === 'mouseenter') {
// make sure mouseenter happen after other menu item's mouseleave
2018-03-01 11:09:45 +00:00
this.mouseenterTimeout = requestAnimationTimeout(() => {
2018-01-17 08:12:53 +00:00
openChange()
}, 0)
} else {
openChange()
2018-01-02 11:05:02 +00:00
}
},
2018-01-17 08:12:53 +00:00
// clearSubMenuTimers () {
// this.clearSubMenuLeaveTimer()
// this.clearSubMenuTitleLeaveTimer()
// },
// clearSubMenuTitleLeaveTimer () {
// const parentMenuContext = this.parentMenuContext
// if (parentMenuContext.subMenuTitleLeaveTimer) {
// clearTimeout(parentMenuContext.subMenuTitleLeaveTimer)
// parentMenuContext.subMenuTitleLeaveTimer = null
// parentMenuContext.subMenuTitleLeaveFn = null
// }
// },
// clearSubMenuLeaveTimer () {
// const parentMenuContext = this.parentMenuContext
// if (parentMenuContext.subMenuLeaveTimer) {
// clearTimeout(parentMenuContext.subMenuLeaveTimer)
// parentMenuContext.subMenuLeaveTimer = null
// parentMenuContext.subMenuLeaveFn = null
// }
// },
2018-01-02 11:05:02 +00:00
isChildrenSelected () {
const ret = { find: false }
loopMenuItemRecusively(this.$slots.default, this.$props.selectedKeys, ret)
return ret.find
},
isOpen () {
return this.$props.openKeys.indexOf(this.$props.eventKey) !== -1
},
2018-01-08 10:31:04 +00:00
renderChildren (children, vShow) {
2018-01-02 11:05:02 +00:00
const props = this.$props
2018-01-08 10:31:04 +00:00
const isOpen = this.isOpen()
2018-01-17 08:12:53 +00:00
const { select, deselect, openChange } = this.$listeners
2018-01-02 11:05:02 +00:00
const subPopupMenuProps = {
props: {
mode: props.mode === 'horizontal' ? 'vertical' : props.mode,
2018-01-08 10:31:04 +00:00
visible: isOpen,
2018-01-02 11:05:02 +00:00
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,
subMenuCloseDelay: props.subMenuCloseDelay,
forceSubMenuRender: props.forceSubMenuRender,
triggerSubMenuAction: props.triggerSubMenuAction,
defaultActiveFirst: this.$data.defaultActiveFirst,
multiple: props.multiple,
prefixCls: props.rootPrefixCls,
2018-01-17 08:12:53 +00:00
// clearSubMenuTimers: this.clearSubMenuTimers,
2018-01-02 11:05:02 +00:00
},
on: {
click: this.onSubMenuClick,
2018-01-17 08:12:53 +00:00
select, deselect, openChange,
2018-01-02 11:05:02 +00:00
},
id: this._menuId,
ref: 'menuInstance',
}
2018-01-08 10:31:04 +00:00
return vShow
? <SubPopupMenu v-show={isOpen} {...subPopupMenuProps}>{children}</SubPopupMenu>
: <SubPopupMenu {...subPopupMenuProps}>{children}</SubPopupMenu>
2018-01-02 11:05:02 +00:00
},
2017-12-08 06:31:53 +00:00
},
2018-01-02 11:05:02 +00:00
render (h) {
2017-12-29 10:45:11 +00:00
const props = this.$props
2018-01-19 10:01:43 +00:00
const { rootPrefixCls, parentMenuContext } = this
2017-12-08 06:31:53 +00:00
const isOpen = this.isOpen()
const prefixCls = this.getPrefixCls()
const isInlineMode = props.mode === 'inline'
2017-12-29 10:45:11 +00:00
const className = {
2018-01-02 11:05:02 +00:00
[prefixCls]: true,
2017-12-29 10:45:11 +00:00
[`${prefixCls}-${props.mode}`]: true,
2017-12-08 06:31:53 +00:00
[this.getOpenClassName()]: isOpen,
[this.getActiveClassName()]: props.active || (isOpen && !isInlineMode),
[this.getDisabledClassName()]: props.disabled,
[this.getSelectedClassName()]: this.isChildrenSelected(),
2017-12-29 10:45:11 +00:00
}
2017-12-08 06:31:53 +00:00
if (!this._menuId) {
if (props.eventKey) {
this._menuId = `${props.eventKey}$Menu`
} else {
this._menuId = `$__$${++guid}$Menu`
}
}
let mouseEvents = {}
let titleClickEvents = {}
let titleMouseEvents = {}
if (!props.disabled) {
mouseEvents = {
2018-01-02 11:05:02 +00:00
mouseleave: this.onMouseLeave,
mouseenter: this.onMouseEnter,
2017-12-08 06:31:53 +00:00
}
// only works in title, not outer li
titleClickEvents = {
2018-01-02 11:05:02 +00:00
click: this.onTitleClick,
2017-12-08 06:31:53 +00:00
}
titleMouseEvents = {
2018-01-02 11:05:02 +00:00
mouseenter: this.onTitleMouseEnter,
mouseleave: this.onTitleMouseLeave,
2017-12-08 06:31:53 +00:00
}
}
const style = {}
if (isInlineMode) {
2018-01-02 11:05:02 +00:00
style.paddingLeft = `${props.inlineIndent * props.level}px`
}
const titleProps = {
attrs: {
'aria-expanded': isOpen,
'aria-owns': this._menuId,
'aria-haspopup': 'true',
title: typeof props.title === 'string' ? props.title : undefined,
},
on: {
...titleMouseEvents,
...titleClickEvents,
},
style,
class: `${prefixCls}-title`,
ref: 'subMenuTitle',
2017-12-08 06:31:53 +00:00
}
const title = (
<div
2018-01-02 11:05:02 +00:00
{...titleProps}
2017-12-08 06:31:53 +00:00
>
2018-01-18 02:43:39 +00:00
{getComponentFromProp(this, 'title')}
2017-12-29 10:45:11 +00:00
<i class={`${prefixCls}-arrow`} />
2017-12-08 06:31:53 +00:00
</div>
)
2018-01-08 10:31:04 +00:00
// const children = this.renderChildren(this.$slots.default)
2017-12-08 06:31:53 +00:00
2018-01-03 10:30:12 +00:00
const getPopupContainer = this.isRootMenu
? this.getPopupContainer : triggerNode => triggerNode.parentNode
2017-12-08 06:31:53 +00:00
const popupPlacement = popupPlacementMap[props.mode]
const popupClassName = props.mode === 'inline' ? '' : props.popupClassName
2018-01-02 11:05:02 +00:00
const liProps = {
on: { ...mouseEvents },
class: className,
}
2018-01-04 11:06:54 +00:00
const { forceSubMenuRender, mode, openTransitionName, openAnimation } = this.$props
const haveRendered = this.haveRendered
this.haveRendered = true
this.haveOpened = this.haveOpened || isOpen || forceSubMenuRender
const transitionAppear = !(!haveRendered && isOpen && mode === 'inline')
let animProps = { appear: true }
if (openTransitionName) {
animProps.name = openTransitionName
} else if (typeof openAnimation === 'object') {
2018-01-08 10:31:04 +00:00
animProps = { ...animProps, ...openAnimation.props || {}}
2018-01-04 11:06:54 +00:00
if (!transitionAppear) {
animProps.appear = false
}
} else if (typeof openAnimation === 'string') {
animProps.name = openAnimation
}
const transitionProps = {
props: animProps,
}
2018-01-08 10:31:04 +00:00
if (typeof openAnimation === 'object' && openAnimation.on) {
transitionProps.on = { ...openAnimation.on }
}
2018-01-30 08:52:56 +00:00
const children = this.renderChildren(this.$slots.default, isInlineMode)
2017-12-08 06:31:53 +00:00
return (
2018-01-02 11:05:02 +00:00
<li {...liProps}>
2017-12-08 06:31:53 +00:00
{isInlineMode && title}
2018-01-04 11:06:54 +00:00
{isInlineMode && (
<transition {...transitionProps}>
{children}
</transition>
)}
2017-12-08 06:31:53 +00:00
{!isInlineMode && (
<Trigger
prefixCls={prefixCls}
2018-01-19 10:01:43 +00:00
popupClassName={`${prefixCls}-popup ${rootPrefixCls}-${parentMenuContext.theme} ${popupClassName || ''}`}
2018-01-03 10:30:12 +00:00
getPopupContainer={getPopupContainer}
2017-12-08 06:31:53 +00:00
builtinPlacements={placements}
popupPlacement={popupPlacement}
popupVisible={isOpen}
action={props.disabled ? [] : [props.triggerSubMenuAction]}
mouseEnterDelay={props.subMenuOpenDelay}
mouseLeaveDelay={props.subMenuCloseDelay}
onPopupVisibleChange={this.onPopupVisibleChange}
forceRender={props.forceSubMenuRender}
2018-01-04 11:06:54 +00:00
// popupTransitionName='rc-menu-open-slide-up'
2018-01-17 08:12:53 +00:00
popupAnimation={transitionProps}
2017-12-08 06:31:53 +00:00
>
2018-01-02 11:05:02 +00:00
<template slot='popup'>
2018-01-04 11:06:54 +00:00
{this.haveOpened ? children : null}
2018-01-02 11:05:02 +00:00
</template>
2017-12-08 06:31:53 +00:00
{title}
</Trigger>
)}
</li>
)
},
2017-12-29 10:45:11 +00:00
}
2018-03-19 02:16:27 +00:00