507 lines
14 KiB
Vue
507 lines
14 KiB
Vue
|
|
import PropTypes from '../_util/vue-types'
|
|
import Trigger from '../trigger'
|
|
import KeyCode from '../_util/KeyCode'
|
|
import SubPopupMenu from './SubPopupMenu'
|
|
import placements from './placements'
|
|
import { loopMenuItemRecusively, noop } from './util'
|
|
import BaseMixin from '../_util/BaseMixin'
|
|
import { getComponentFromProp } from '../_util/props-util'
|
|
import { requestAnimationTimeout, cancelAnimationTimeout } from '../_util/requestAnimationTimeout'
|
|
|
|
let guid = 0
|
|
|
|
const popupPlacementMap = {
|
|
horizontal: 'bottomLeft',
|
|
vertical: 'rightTop',
|
|
'vertical-left': 'rightTop',
|
|
'vertical-right': 'leftTop',
|
|
}
|
|
|
|
export default {
|
|
name: 'SubMenu',
|
|
props: {
|
|
mode: PropTypes.oneOf(['horizontal', 'vertical', 'vertical-left', 'vertical-right', 'inline']).def('vertical'),
|
|
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,
|
|
index: PropTypes.number,
|
|
triggerSubMenuAction: PropTypes.string,
|
|
popupClassName: PropTypes.string,
|
|
getPopupContainer: PropTypes.func,
|
|
test: PropTypes.any,
|
|
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,
|
|
},
|
|
inject: {
|
|
parentMenuContext: { default: undefined },
|
|
},
|
|
mixins: [BaseMixin],
|
|
isSubMenu: true,
|
|
data () {
|
|
return {
|
|
defaultActiveFirst: false,
|
|
}
|
|
},
|
|
mounted () {
|
|
this.$nextTick(() => {
|
|
this.handleUpdated()
|
|
})
|
|
},
|
|
|
|
updated () {
|
|
this.$nextTick(() => {
|
|
this.handleUpdated()
|
|
})
|
|
},
|
|
|
|
beforeDestroy () {
|
|
const { eventKey } = this
|
|
this.__emit('destroy', eventKey)
|
|
// if (parentMenuContext.subMenuInstance === this) {
|
|
// this.clearSubMenuTimers()
|
|
// }
|
|
if (this.minWidthTimeout) {
|
|
cancelAnimationTimeout(this.minWidthTimeout)
|
|
this.minWidthTimeout = null
|
|
}
|
|
if (this.mouseenterTimeout) {
|
|
cancelAnimationTimeout(this.mouseenterTimeout)
|
|
this.mouseenterTimeout = null
|
|
}
|
|
},
|
|
methods: {
|
|
handleUpdated () {
|
|
const { mode, isRootMenu } = this.$props
|
|
if (mode !== 'horizontal' || !isRootMenu || !this.isOpen()) {
|
|
return
|
|
}
|
|
const self = this
|
|
this.minWidthTimeout = requestAnimationTimeout(() => {
|
|
if (!self.$refs.subMenuTitle || !self.$refs.menuInstance) {
|
|
return
|
|
}
|
|
const popupMenu = self.$refs.menuInstance.$el
|
|
if (popupMenu.offsetWidth >= self.$refs.subMenuTitle.offsetWidth) {
|
|
return
|
|
}
|
|
popupMenu.style.minWidth = `${self.$refs.subMenuTitle.offsetWidth}px`
|
|
}, 0)
|
|
},
|
|
|
|
onKeyDown (e) {
|
|
const keyCode = e.keyCode
|
|
const menu = this.$refs.menuInstance
|
|
const isOpen = this.isOpen()
|
|
|
|
if (keyCode === KeyCode.ENTER) {
|
|
this.onTitleClick(e)
|
|
this.setState({
|
|
defaultActiveFirst: true,
|
|
})
|
|
return true
|
|
}
|
|
|
|
if (keyCode === KeyCode.RIGHT) {
|
|
if (isOpen) {
|
|
menu.onKeyDown(e)
|
|
} else {
|
|
this.triggerOpenChange(true)
|
|
this.setState({
|
|
defaultActiveFirst: 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 } = this.$props
|
|
// this.clearSubMenuLeaveTimer()
|
|
this.setState({
|
|
defaultActiveFirst: false,
|
|
})
|
|
this.__emit('mouseenter', {
|
|
key,
|
|
domEvent: e,
|
|
})
|
|
},
|
|
|
|
onMouseLeave (e) {
|
|
const {
|
|
eventKey,
|
|
parentMenuContext,
|
|
} = this
|
|
parentMenuContext.subMenuInstance = this
|
|
// parentMenuContext.subMenuLeaveFn = () => {
|
|
// // trigger mouseleave
|
|
// this.__emit('mouseleave', {
|
|
// key: eventKey,
|
|
// domEvent: e,
|
|
// })
|
|
// }
|
|
this.__emit('mouseleave', {
|
|
key: eventKey,
|
|
domEvent: e,
|
|
})
|
|
// prevent popup menu and submenu gap
|
|
// parentMenuContext.subMenuLeaveTimer = setTimeout(parentMenuContext.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, parentMenuContext } = this
|
|
parentMenuContext.subMenuInstance = this
|
|
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)
|
|
},
|
|
|
|
onTitleClick (e) {
|
|
const { triggerSubMenuAction, eventKey } = this.$props
|
|
this.$emit('titleClick', {
|
|
key: eventKey,
|
|
domEvent: e,
|
|
})
|
|
if (triggerSubMenuAction === 'hover') {
|
|
return
|
|
}
|
|
this.triggerOpenChange(!this.isOpen(), 'click')
|
|
this.setState({
|
|
defaultActiveFirst: 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`
|
|
},
|
|
|
|
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()
|
|
}
|
|
},
|
|
|
|
// 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
|
|
// }
|
|
// },
|
|
|
|
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
|
|
},
|
|
|
|
renderChildren (children, vShow) {
|
|
const props = this.$props
|
|
const isOpen = this.isOpen()
|
|
const { select, deselect, openChange } = this.$listeners
|
|
const subPopupMenuProps = {
|
|
props: {
|
|
mode: props.mode === 'horizontal' ? 'vertical' : props.mode,
|
|
visible: 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,
|
|
subMenuCloseDelay: props.subMenuCloseDelay,
|
|
forceSubMenuRender: props.forceSubMenuRender,
|
|
triggerSubMenuAction: props.triggerSubMenuAction,
|
|
defaultActiveFirst: this.$data.defaultActiveFirst,
|
|
multiple: props.multiple,
|
|
prefixCls: props.rootPrefixCls,
|
|
// clearSubMenuTimers: this.clearSubMenuTimers,
|
|
},
|
|
on: {
|
|
click: this.onSubMenuClick,
|
|
select, deselect, openChange,
|
|
},
|
|
id: this._menuId,
|
|
ref: 'menuInstance',
|
|
}
|
|
return vShow
|
|
? <SubPopupMenu v-show={isOpen} {...subPopupMenuProps}>{children}</SubPopupMenu>
|
|
: <SubPopupMenu {...subPopupMenuProps}>{children}</SubPopupMenu>
|
|
},
|
|
},
|
|
|
|
render (h) {
|
|
const props = this.$props
|
|
const { rootPrefixCls, parentMenuContext } = this
|
|
const isOpen = this.isOpen()
|
|
const prefixCls = this.getPrefixCls()
|
|
const isInlineMode = props.mode === 'inline'
|
|
const className = {
|
|
[prefixCls]: true,
|
|
[`${prefixCls}-${props.mode}`]: true,
|
|
[this.getOpenClassName()]: isOpen,
|
|
[this.getActiveClassName()]: props.active || (isOpen && !isInlineMode),
|
|
[this.getDisabledClassName()]: props.disabled,
|
|
[this.getSelectedClassName()]: this.isChildrenSelected(),
|
|
}
|
|
|
|
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 = {
|
|
mouseleave: this.onMouseLeave,
|
|
mouseenter: this.onMouseEnter,
|
|
}
|
|
|
|
// only works in title, not outer li
|
|
titleClickEvents = {
|
|
click: this.onTitleClick,
|
|
}
|
|
titleMouseEvents = {
|
|
mouseenter: this.onTitleMouseEnter,
|
|
mouseleave: this.onTitleMouseLeave,
|
|
}
|
|
}
|
|
|
|
const style = {}
|
|
if (isInlineMode) {
|
|
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',
|
|
}
|
|
const title = (
|
|
<div
|
|
{...titleProps}
|
|
>
|
|
{getComponentFromProp(this, 'title')}
|
|
<i class={`${prefixCls}-arrow`} />
|
|
</div>
|
|
)
|
|
// const children = this.renderChildren(this.$slots.default)
|
|
|
|
const getPopupContainer = this.isRootMenu
|
|
? this.parentMenuContext.getPopupContainer : triggerNode => triggerNode.parentNode
|
|
const popupPlacement = popupPlacementMap[props.mode]
|
|
const popupClassName = props.mode === 'inline' ? '' : props.popupClassName
|
|
const liProps = {
|
|
on: { ...mouseEvents },
|
|
class: className,
|
|
}
|
|
|
|
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') {
|
|
animProps = { ...animProps, ...openAnimation.props || {}}
|
|
if (!transitionAppear) {
|
|
animProps.appear = false
|
|
}
|
|
} else if (typeof openAnimation === 'string') {
|
|
animProps.name = openAnimation
|
|
}
|
|
const transitionProps = {
|
|
props: animProps,
|
|
}
|
|
if (typeof openAnimation === 'object' && openAnimation.on) {
|
|
transitionProps.on = { ...openAnimation.on }
|
|
}
|
|
const children = this.renderChildren(this.$slots.default, isInlineMode)
|
|
return (
|
|
<li {...liProps}>
|
|
{isInlineMode && title}
|
|
{isInlineMode && (
|
|
<transition {...transitionProps}>
|
|
{children}
|
|
</transition>
|
|
)}
|
|
{!isInlineMode && (
|
|
<Trigger
|
|
prefixCls={prefixCls}
|
|
popupClassName={`${prefixCls}-popup ${rootPrefixCls}-${parentMenuContext.theme} ${popupClassName || ''}`}
|
|
getPopupContainer={getPopupContainer}
|
|
builtinPlacements={placements}
|
|
popupPlacement={popupPlacement}
|
|
popupVisible={isOpen}
|
|
action={props.disabled ? [] : [props.triggerSubMenuAction]}
|
|
mouseEnterDelay={props.subMenuOpenDelay}
|
|
mouseLeaveDelay={props.subMenuCloseDelay}
|
|
onPopupVisibleChange={this.onPopupVisibleChange}
|
|
forceRender={props.forceSubMenuRender}
|
|
// popupTransitionName='rc-menu-open-slide-up'
|
|
popupAnimation={transitionProps}
|
|
>
|
|
<template slot='popup'>
|
|
{this.haveOpened ? children : null}
|
|
</template>
|
|
{title}
|
|
</Trigger>
|
|
)}
|
|
</li>
|
|
)
|
|
},
|
|
}
|
|
|