pull/9/head
tangjinzhou 7 years ago
parent dfaec7f076
commit 09500d37eb

@ -0,0 +1,10 @@
export default {
methods: {
setState (state, callback) {
Object.assign(this.$date, state)
this.$nextTick(() => {
callback()
})
},
},
}

@ -0,0 +1,3 @@
// do not modify this file
import Animate from './src/Animate'
export default Animate

@ -0,0 +1,333 @@
import React from 'react'
import PropTypes from 'prop-types'
import {
toArrayChildren,
mergeChildren,
findShownChildInChildrenByKey,
findChildInChildrenByKey,
isSameChildren,
} from './ChildrenUtils'
import AnimateChild from './AnimateChild'
const defaultKey = `rc_animate_${Date.now()}`
import animUtil from './util'
function getChildrenFromProps (props) {
const children = props.children
if (React.isValidElement(children)) {
if (!children.key) {
return React.cloneElement(children, {
key: defaultKey,
})
}
}
return children
}
function noop () {
}
export default class Animate extends React.Component {
static propTypes = {
component: PropTypes.any,
componentProps: PropTypes.object,
animation: PropTypes.object,
transitionName: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
]),
transitionEnter: PropTypes.bool,
transitionAppear: PropTypes.bool,
exclusive: PropTypes.bool,
transitionLeave: PropTypes.bool,
onEnd: PropTypes.func,
onEnter: PropTypes.func,
onLeave: PropTypes.func,
onAppear: PropTypes.func,
showProp: PropTypes.string,
}
static defaultProps = {
animation: {},
component: 'span',
componentProps: {},
transitionEnter: true,
transitionLeave: true,
transitionAppear: false,
onEnd: noop,
onEnter: noop,
onLeave: noop,
onAppear: noop,
}
constructor (props) {
super(props)
this.currentlyAnimatingKeys = {}
this.keysToEnter = []
this.keysToLeave = []
this.state = {
children: toArrayChildren(getChildrenFromProps(this.props)),
}
this.childrenRefs = {}
}
componentDidMount () {
const showProp = this.props.showProp
let children = this.state.children
if (showProp) {
children = children.filter((child) => {
return !!child.props[showProp]
})
}
children.forEach((child) => {
if (child) {
this.performAppear(child.key)
}
})
}
componentWillReceiveProps (nextProps) {
this.nextProps = nextProps
const nextChildren = toArrayChildren(getChildrenFromProps(nextProps))
const props = this.props
// exclusive needs immediate response
if (props.exclusive) {
Object.keys(this.currentlyAnimatingKeys).forEach((key) => {
this.stop(key)
})
}
const showProp = props.showProp
const currentlyAnimatingKeys = this.currentlyAnimatingKeys
// last props children if exclusive
const currentChildren = props.exclusive
? toArrayChildren(getChildrenFromProps(props))
: this.state.children
// in case destroy in showProp mode
let newChildren = []
if (showProp) {
currentChildren.forEach((currentChild) => {
const nextChild = currentChild && findChildInChildrenByKey(nextChildren, currentChild.key)
let newChild
if ((!nextChild || !nextChild.props[showProp]) && currentChild.props[showProp]) {
newChild = React.cloneElement(nextChild || currentChild, {
[showProp]: true,
})
} else {
newChild = nextChild
}
if (newChild) {
newChildren.push(newChild)
}
})
nextChildren.forEach((nextChild) => {
if (!nextChild || !findChildInChildrenByKey(currentChildren, nextChild.key)) {
newChildren.push(nextChild)
}
})
} else {
newChildren = mergeChildren(
currentChildren,
nextChildren
)
}
// need render to avoid update
this.setState({
children: newChildren,
})
nextChildren.forEach((child) => {
const key = child && child.key
if (child && currentlyAnimatingKeys[key]) {
return
}
const hasPrev = child && findChildInChildrenByKey(currentChildren, key)
if (showProp) {
const showInNext = child.props[showProp]
if (hasPrev) {
const showInNow = findShownChildInChildrenByKey(currentChildren, key, showProp)
if (!showInNow && showInNext) {
this.keysToEnter.push(key)
}
} else if (showInNext) {
this.keysToEnter.push(key)
}
} else if (!hasPrev) {
this.keysToEnter.push(key)
}
})
currentChildren.forEach((child) => {
const key = child && child.key
if (child && currentlyAnimatingKeys[key]) {
return
}
const hasNext = child && findChildInChildrenByKey(nextChildren, key)
if (showProp) {
const showInNow = child.props[showProp]
if (hasNext) {
const showInNext = findShownChildInChildrenByKey(nextChildren, key, showProp)
if (!showInNext && showInNow) {
this.keysToLeave.push(key)
}
} else if (showInNow) {
this.keysToLeave.push(key)
}
} else if (!hasNext) {
this.keysToLeave.push(key)
}
})
}
componentDidUpdate () {
const keysToEnter = this.keysToEnter
this.keysToEnter = []
keysToEnter.forEach(this.performEnter)
const keysToLeave = this.keysToLeave
this.keysToLeave = []
keysToLeave.forEach(this.performLeave)
}
performEnter = (key) => {
// may already remove by exclusive
if (this.childrenRefs[key]) {
this.currentlyAnimatingKeys[key] = true
this.childrenRefs[key].componentWillEnter(
this.handleDoneAdding.bind(this, key, 'enter')
)
}
}
performAppear = (key) => {
if (this.childrenRefs[key]) {
this.currentlyAnimatingKeys[key] = true
this.childrenRefs[key].componentWillAppear(
this.handleDoneAdding.bind(this, key, 'appear')
)
}
}
handleDoneAdding = (key, type) => {
const props = this.props
delete this.currentlyAnimatingKeys[key]
// if update on exclusive mode, skip check
if (props.exclusive && props !== this.nextProps) {
return
}
const currentChildren = toArrayChildren(getChildrenFromProps(props))
if (!this.isValidChildByKey(currentChildren, key)) {
// exclusive will not need this
this.performLeave(key)
} else {
if (type === 'appear') {
if (animUtil.allowAppearCallback(props)) {
props.onAppear(key)
props.onEnd(key, true)
}
} else {
if (animUtil.allowEnterCallback(props)) {
props.onEnter(key)
props.onEnd(key, true)
}
}
}
}
performLeave = (key) => {
// may already remove by exclusive
if (this.childrenRefs[key]) {
this.currentlyAnimatingKeys[key] = true
this.childrenRefs[key].componentWillLeave(this.handleDoneLeaving.bind(this, key))
}
}
handleDoneLeaving = (key) => {
const props = this.props
delete this.currentlyAnimatingKeys[key]
// if update on exclusive mode, skip check
if (props.exclusive && props !== this.nextProps) {
return
}
const currentChildren = toArrayChildren(getChildrenFromProps(props))
// in case state change is too fast
if (this.isValidChildByKey(currentChildren, key)) {
this.performEnter(key)
} else {
const end = () => {
if (animUtil.allowLeaveCallback(props)) {
props.onLeave(key)
props.onEnd(key, false)
}
}
if (!isSameChildren(this.state.children,
currentChildren, props.showProp)) {
this.setState({
children: currentChildren,
}, end)
} else {
end()
}
}
}
isValidChildByKey (currentChildren, key) {
const showProp = this.props.showProp
if (showProp) {
return findShownChildInChildrenByKey(currentChildren, key, showProp)
}
return findChildInChildrenByKey(currentChildren, key)
}
stop (key) {
delete this.currentlyAnimatingKeys[key]
const component = this.childrenRefs[key]
if (component) {
component.stop()
}
}
render () {
const props = this.props
this.nextProps = props
const stateChildren = this.state.children
let children = null
if (stateChildren) {
children = stateChildren.map((child) => {
if (child === null || child === undefined) {
return child
}
if (!child.key) {
throw new Error('must set key for <rc-animate> children')
}
return (
<AnimateChild
key={child.key}
ref={node => this.childrenRefs[child.key] = node}
animation={props.animation}
transitionName={props.transitionName}
transitionEnter={props.transitionEnter}
transitionAppear={props.transitionAppear}
transitionLeave={props.transitionLeave}
>
{child}
</AnimateChild>
)
})
}
const Component = props.component
if (Component) {
let passedProps = props
if (typeof Component === 'string') {
passedProps = {
className: props.className,
style: props.style,
...props.componentProps,
}
}
return <Component {...passedProps}>{children}</Component>
}
return children[0] || null
}
}

@ -0,0 +1,86 @@
import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
import cssAnimate, { isCssAnimationSupported } from 'css-animation'
import animUtil from './util'
const transitionMap = {
enter: 'transitionEnter',
appear: 'transitionAppear',
leave: 'transitionLeave',
}
export default class AnimateChild extends React.Component {
static propTypes = {
children: PropTypes.any,
}
componentWillUnmount () {
this.stop()
}
componentWillEnter (done) {
if (animUtil.isEnterSupported(this.props)) {
this.transition('enter', done)
} else {
done()
}
}
componentWillAppear (done) {
if (animUtil.isAppearSupported(this.props)) {
this.transition('appear', done)
} else {
done()
}
}
componentWillLeave (done) {
if (animUtil.isLeaveSupported(this.props)) {
this.transition('leave', done)
} else {
// always sync, do not interupt with react component life cycle
// update hidden -> animate hidden ->
// didUpdate -> animate leave -> unmount (if animate is none)
done()
}
}
transition (animationType, finishCallback) {
const node = ReactDOM.findDOMNode(this)
const props = this.props
const transitionName = props.transitionName
const nameIsObj = typeof transitionName === 'object'
this.stop()
const end = () => {
this.stopper = null
finishCallback()
}
if ((isCssAnimationSupported || !props.animation[animationType]) &&
transitionName && props[transitionMap[animationType]]) {
const name = nameIsObj ? transitionName[animationType] : `${transitionName}-${animationType}`
let activeName = `${name}-active`
if (nameIsObj && transitionName[`${animationType}Active`]) {
activeName = transitionName[`${animationType}Active`]
}
this.stopper = cssAnimate(node, {
name,
active: activeName,
}, end)
} else {
this.stopper = props.animation[animationType](node, end)
}
}
stop () {
const stopper = this.stopper
if (stopper) {
this.stopper = null
stopper.stop()
}
}
render () {
return this.props.children
}
}

@ -0,0 +1,101 @@
import React from 'react'
export function toArrayChildren (children) {
const ret = []
React.Children.forEach(children, (child) => {
ret.push(child)
})
return ret
}
export function findChildInChildrenByKey (children, key) {
let ret = null
if (children) {
children.forEach((child) => {
if (ret) {
return
}
if (child && child.key === key) {
ret = child
}
})
}
return ret
}
export function findShownChildInChildrenByKey (children, key, showProp) {
let ret = null
if (children) {
children.forEach((child) => {
if (child && child.key === key && child.props[showProp]) {
if (ret) {
throw new Error('two child with same key for <rc-animate> children')
}
ret = child
}
})
}
return ret
}
export function findHiddenChildInChildrenByKey (children, key, showProp) {
let found = 0
if (children) {
children.forEach((child) => {
if (found) {
return
}
found = child && child.key === key && !child.props[showProp]
})
}
return found
}
export function isSameChildren (c1, c2, showProp) {
let same = c1.length === c2.length
if (same) {
c1.forEach((child, index) => {
const child2 = c2[index]
if (child && child2) {
if ((child && !child2) || (!child && child2)) {
same = false
} else if (child.key !== child2.key) {
same = false
} else if (showProp && child.props[showProp] !== child2.props[showProp]) {
same = false
}
}
})
}
return same
}
export function mergeChildren (prev, next) {
let ret = []
// For each key of `next`, the list of keys to insert before that key in
// the combined list
const nextChildrenPending = {}
let pendingChildren = []
prev.forEach((child) => {
if (child && findChildInChildrenByKey(next, child.key)) {
if (pendingChildren.length) {
nextChildrenPending[child.key] = pendingChildren
pendingChildren = []
}
} else {
pendingChildren.push(child)
}
})
next.forEach((child) => {
if (child && nextChildrenPending.hasOwnProperty(child.key)) {
ret = ret.concat(nextChildrenPending[child.key])
}
ret.push(child)
})
ret = ret.concat(pendingChildren)
return ret
}

@ -0,0 +1,21 @@
const util = {
isAppearSupported(props) {
return props.transitionName && props.transitionAppear || props.animation.appear;
},
isEnterSupported(props) {
return props.transitionName && props.transitionEnter || props.animation.enter;
},
isLeaveSupported(props) {
return props.transitionName && props.transitionLeave || props.animation.leave;
},
allowAppearCallback(props) {
return props.transitionAppear || props.animation.appear;
},
allowEnterCallback(props) {
return props.transitionEnter || props.animation.enter;
},
allowLeaveCallback(props) {
return props.transitionLeave || props.animation.leave;
},
};
export default util;

@ -0,0 +1,38 @@
<script>
import omit from 'omit.js'
export default {
name: 'DOMWrap',
props: {
visible: {
type: Boolean,
default: false,
},
tag: {
type: String,
default: 'div',
},
hiddenClassName: String,
},
computed: {
class () {
const { visible, hiddenClassName } = this.$props
return {
[hiddenClassName]: !visible,
}
},
},
render () {
const otherProps = omit(this.$props, [
'tag',
'hiddenClassName',
'visible',
])
const Tag = this.$props.tag
const tagProps = {
attr: { ...otherProps, ...this.$attrs },
}
return <Tag {...tagProps} class={this.class} />
},
}
</script>

@ -0,0 +1,16 @@
<script>
export default {
name: 'Divider',
props: {
disabled: {
type: Boolean,
default: true,
},
rootPrefixCls: String,
},
render () {
const { rootPrefixCls } = this.$props
return <li class={`${rootPrefixCls}-item-divider`}/>
},
}
</script>

@ -0,0 +1,221 @@
// import React from 'react';
import PropTypes from 'prop-types'
import createReactClass from 'create-react-class'
import MenuMixin from './MenuMixin'
import { noop } from './util'
const Menu = createReactClass({
displayName: 'Menu',
propTypes: {
defaultSelectedKeys: PropTypes.arrayOf(PropTypes.string),
selectedKeys: PropTypes.arrayOf(PropTypes.string),
defaultOpenKeys: PropTypes.arrayOf(PropTypes.string),
openKeys: PropTypes.arrayOf(PropTypes.string),
mode: PropTypes.oneOf(['horizontal', 'vertical', 'vertical-left', 'vertical-right', 'inline']),
getPopupContainer: PropTypes.func,
onClick: PropTypes.func,
onSelect: PropTypes.func,
onDeselect: PropTypes.func,
onDestroy: PropTypes.func,
openTransitionName: PropTypes.string,
openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
subMenuOpenDelay: PropTypes.number,
subMenuCloseDelay: PropTypes.number,
forceSubMenuRender: PropTypes.bool,
triggerSubMenuAction: PropTypes.string,
level: PropTypes.number,
selectable: PropTypes.bool,
multiple: PropTypes.bool,
children: PropTypes.any,
},
mixins: [MenuMixin],
isRootMenu: true,
getDefaultProps () {
return {
selectable: true,
onClick: noop,
onSelect: noop,
onOpenChange: noop,
onDeselect: noop,
defaultSelectedKeys: [],
defaultOpenKeys: [],
subMenuOpenDelay: 0,
subMenuCloseDelay: 0.1,
triggerSubMenuAction: 'hover',
}
},
getInitialState () {
const props = this.props
let selectedKeys = props.defaultSelectedKeys
let openKeys = props.defaultOpenKeys
if ('selectedKeys' in props) {
selectedKeys = props.selectedKeys || []
}
if ('openKeys' in props) {
openKeys = props.openKeys || []
}
return {
selectedKeys,
openKeys,
}
},
componentWillReceiveProps (nextProps) {
const props = {}
if ('selectedKeys' in nextProps) {
props.selectedKeys = nextProps.selectedKeys || []
}
if ('openKeys' in nextProps) {
props.openKeys = nextProps.openKeys || []
}
this.setState(props)
},
onDestroy (key) {
const state = this.state
const props = this.props
const selectedKeys = state.selectedKeys
const openKeys = state.openKeys
let index = selectedKeys.indexOf(key)
if (!('selectedKeys' in props) && index !== -1) {
selectedKeys.splice(index, 1)
}
index = openKeys.indexOf(key)
if (!('openKeys' in props) && index !== -1) {
openKeys.splice(index, 1)
}
},
onSelect (selectInfo) {
const props = this.props
if (props.selectable) {
// root menu
let selectedKeys = this.state.selectedKeys
const selectedKey = selectInfo.key
if (props.multiple) {
selectedKeys = selectedKeys.concat([selectedKey])
} else {
selectedKeys = [selectedKey]
}
if (!('selectedKeys' in props)) {
this.setState({
selectedKeys,
})
}
props.onSelect({
...selectInfo,
selectedKeys,
})
}
},
onClick (e) {
this.props.onClick(e)
},
onOpenChange (e_) {
const props = this.props
const openKeys = this.state.openKeys.concat()
let changed = false
const processSingle = (e) => {
let oneChanged = false
if (e.open) {
oneChanged = openKeys.indexOf(e.key) === -1
if (oneChanged) {
openKeys.push(e.key)
}
} else {
const index = openKeys.indexOf(e.key)
oneChanged = index !== -1
if (oneChanged) {
openKeys.splice(index, 1)
}
}
changed = changed || oneChanged
}
if (Array.isArray(e_)) {
// batch change call
e_.forEach(processSingle)
} else {
processSingle(e_)
}
if (changed) {
if (!('openKeys' in this.props)) {
this.setState({ openKeys })
}
props.onOpenChange(openKeys)
}
},
onDeselect (selectInfo) {
const props = this.props
if (props.selectable) {
const selectedKeys = this.state.selectedKeys.concat()
const selectedKey = selectInfo.key
const index = selectedKeys.indexOf(selectedKey)
if (index !== -1) {
selectedKeys.splice(index, 1)
}
if (!('selectedKeys' in props)) {
this.setState({
selectedKeys,
})
}
props.onDeselect({
...selectInfo,
selectedKeys,
})
}
},
getOpenTransitionName () {
const props = this.props
let transitionName = props.openTransitionName
const animationName = props.openAnimation
if (!transitionName && typeof animationName === 'string') {
transitionName = `${props.prefixCls}-open-${animationName}`
}
return transitionName
},
isInlineMode () {
return this.props.mode === 'inline'
},
lastOpenSubMenu () {
let lastOpen = []
const { openKeys } = this.state
if (openKeys.length) {
lastOpen = this.getFlatInstanceArray().filter((c) => {
return c && openKeys.indexOf(c.props.eventKey) !== -1
})
}
return lastOpen[0]
},
renderMenuItem (c, i, subIndex) {
if (!c) {
return null
}
const state = this.state
const extraProps = {
openKeys: state.openKeys,
selectedKeys: state.selectedKeys,
triggerSubMenuAction: this.props.triggerSubMenuAction,
}
return this.renderCommonMenuItem(c, i, subIndex, extraProps)
},
render () {
const props = { ...this.props }
props.className += ` ${props.prefixCls}-root`
return this.renderRoot(props)
},
})
export default Menu

@ -0,0 +1,165 @@
import React from 'react'
import PropTypes from 'prop-types'
import createReactClass from 'create-react-class'
import KeyCode from 'rc-util/lib/KeyCode'
import classNames from 'classnames'
import { noop } from './util'
/* eslint react/no-is-mounted:0 */
const MenuItem = createReactClass({
displayName: 'MenuItem',
propTypes: {
rootPrefixCls: PropTypes.string,
eventKey: PropTypes.string,
active: PropTypes.bool,
children: PropTypes.any,
selectedKeys: PropTypes.array,
disabled: PropTypes.bool,
title: PropTypes.string,
onItemHover: PropTypes.func,
onSelect: PropTypes.func,
onClick: PropTypes.func,
onDeselect: PropTypes.func,
parentMenu: PropTypes.object,
onDestroy: PropTypes.func,
onMouseEnter: PropTypes.func,
onMouseLeave: PropTypes.func,
},
getDefaultProps () {
return {
onSelect: noop,
onMouseEnter: noop,
onMouseLeave: noop,
}
},
componentWillUnmount () {
const props = this.props
if (props.onDestroy) {
props.onDestroy(props.eventKey)
}
},
onKeyDown (e) {
const keyCode = e.keyCode
if (keyCode === KeyCode.ENTER) {
this.onClick(e)
return true
}
},
onMouseLeave (e) {
const { eventKey, onItemHover, onMouseLeave } = this.props
onItemHover({
key: eventKey,
hover: false,
})
onMouseLeave({
key: eventKey,
domEvent: e,
})
},
onMouseEnter (e) {
const { eventKey, parentMenu, onItemHover, onMouseEnter } = this.props
if (parentMenu.subMenuInstance) {
parentMenu.subMenuInstance.clearSubMenuTimers()
}
onItemHover({
key: eventKey,
hover: true,
})
onMouseEnter({
key: eventKey,
domEvent: e,
})
},
onClick (e) {
const { eventKey, multiple, onClick, onSelect, onDeselect } = this.props
const selected = this.isSelected()
const info = {
key: eventKey,
keyPath: [eventKey],
item: this,
domEvent: e,
}
onClick(info)
if (multiple) {
if (selected) {
onDeselect(info)
} else {
onSelect(info)
}
} else if (!selected) {
onSelect(info)
}
},
getPrefixCls () {
return `${this.props.rootPrefixCls}-item`
},
getActiveClassName () {
return `${this.getPrefixCls()}-active`
},
getSelectedClassName () {
return `${this.getPrefixCls()}-selected`
},
getDisabledClassName () {
return `${this.getPrefixCls()}-disabled`
},
isSelected () {
return this.props.selectedKeys.indexOf(this.props.eventKey) !== -1
},
render () {
const props = this.props
const selected = this.isSelected()
const className = classNames(this.getPrefixCls(), props.className, {
[this.getActiveClassName()]: !props.disabled && props.active,
[this.getSelectedClassName()]: selected,
[this.getDisabledClassName()]: props.disabled,
})
const attrs = {
...props.attribute,
title: props.title,
className,
role: 'menuitem',
'aria-selected': selected,
'aria-disabled': props.disabled,
}
let mouseEvent = {}
if (!props.disabled) {
mouseEvent = {
onClick: this.onClick,
onMouseLeave: this.onMouseLeave,
onMouseEnter: this.onMouseEnter,
}
}
const style = {
...props.style,
}
if (props.mode === 'inline') {
style.paddingLeft = props.inlineIndent * props.level
}
return (
<li
{...attrs}
{...mouseEvent}
style={style}
>
{props.children}
</li>
)
},
})
MenuItem.isMenuItem = 1
export default MenuItem

@ -0,0 +1,48 @@
import React from 'react'
import PropTypes from 'prop-types'
import createReactClass from 'create-react-class'
const MenuItemGroup = createReactClass({
displayName: 'MenuItemGroup',
propTypes: {
renderMenuItem: PropTypes.func,
index: PropTypes.number,
className: PropTypes.string,
rootPrefixCls: PropTypes.string,
},
getDefaultProps () {
// To fix keyboard UX.
return { disabled: true }
},
renderInnerMenuItem (item, subIndex) {
const { renderMenuItem, index } = this.props
return renderMenuItem(item, index, subIndex)
},
render () {
const props = this.props
const { className = '', rootPrefixCls } = props
const titleClassName = `${rootPrefixCls}-item-group-title`
const listClassName = `${rootPrefixCls}-item-group-list`
return (
<li className={`${className} ${rootPrefixCls}-item-group`}>
<div
className={titleClassName}
title={typeof props.title === 'string' ? props.title : undefined}
>
{props.title}
</div>
<ul className={listClassName}>
{React.Children.map(props.children, this.renderInnerMenuItem)}
</ul>
</li>
)
},
})
MenuItemGroup.isMenuItemGroup = true
export default MenuItemGroup

@ -0,0 +1,298 @@
import React from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import KeyCode from 'rc-util/lib/KeyCode'
import createChainedFunction from 'rc-util/lib/createChainedFunction'
import classNames from 'classnames'
import scrollIntoView from 'dom-scroll-into-view'
import { getKeyFromChildrenIndex, loopMenuItem } from './util'
import DOMWrap from './DOMWrap'
function allDisabled (arr) {
if (!arr.length) {
return true
}
return arr.every(c => !!c.props.disabled)
}
function getActiveKey (props, originalActiveKey) {
let activeKey = originalActiveKey
const { children, eventKey } = props
if (activeKey) {
let found
loopMenuItem(children, (c, i) => {
if (c && !c.props.disabled && activeKey === getKeyFromChildrenIndex(c, eventKey, i)) {
found = true
}
})
if (found) {
return activeKey
}
}
activeKey = null
if (props.defaultActiveFirst) {
loopMenuItem(children, (c, i) => {
if (!activeKey && c && !c.props.disabled) {
activeKey = getKeyFromChildrenIndex(c, eventKey, i)
}
})
return activeKey
}
return activeKey
}
function saveRef (index, subIndex, c) {
if (c) {
if (subIndex !== undefined) {
this.instanceArray[index] = this.instanceArray[index] || []
this.instanceArray[index][subIndex] = c
} else {
this.instanceArray[index] = c
}
}
}
const MenuMixin = {
propTypes: {
focusable: PropTypes.bool,
multiple: PropTypes.bool,
style: PropTypes.object,
defaultActiveFirst: PropTypes.bool,
visible: PropTypes.bool,
activeKey: PropTypes.string,
selectedKeys: PropTypes.arrayOf(PropTypes.string),
defaultSelectedKeys: PropTypes.arrayOf(PropTypes.string),
defaultOpenKeys: PropTypes.arrayOf(PropTypes.string),
openKeys: PropTypes.arrayOf(PropTypes.string),
children: PropTypes.any,
},
getDefaultProps () {
return {
prefixCls: 'rc-menu',
className: '',
mode: 'vertical',
level: 1,
inlineIndent: 24,
visible: true,
focusable: true,
style: {},
}
},
getInitialState () {
const props = this.props
return {
activeKey: getActiveKey(props, props.activeKey),
}
},
componentWillReceiveProps (nextProps) {
let props
if ('activeKey' in nextProps) {
props = {
activeKey: getActiveKey(nextProps, nextProps.activeKey),
}
} else {
const originalActiveKey = this.state.activeKey
const activeKey = getActiveKey(nextProps, originalActiveKey)
// fix: this.setState(), parent.render(),
if (activeKey !== originalActiveKey) {
props = {
activeKey,
}
}
}
if (props) {
this.setState(props)
}
},
shouldComponentUpdate (nextProps) {
return this.props.visible || nextProps.visible
},
componentWillMount () {
this.instanceArray = []
},
// all keyboard events callbacks run from here at first
onKeyDown (e, callback) {
const keyCode = e.keyCode
let handled
this.getFlatInstanceArray().forEach((obj) => {
if (obj && obj.props.active && obj.onKeyDown) {
handled = obj.onKeyDown(e)
}
})
if (handled) {
return 1
}
let activeItem = null
if (keyCode === KeyCode.UP || keyCode === KeyCode.DOWN) {
activeItem = this.step(keyCode === KeyCode.UP ? -1 : 1)
}
if (activeItem) {
e.preventDefault()
this.setState({
activeKey: activeItem.props.eventKey,
}, () => {
scrollIntoView(ReactDOM.findDOMNode(activeItem), ReactDOM.findDOMNode(this), {
onlyScrollIfNeeded: true,
})
// https://github.com/react-component/menu/commit/9899a9672f6f028ec3cdf773f1ecea5badd2d33e
if (typeof callback === 'function') {
callback(activeItem)
}
})
return 1
} else if (activeItem === undefined) {
e.preventDefault()
this.setState({
activeKey: null,
})
return 1
}
},
onItemHover (e) {
const { key, hover } = e
this.setState({
activeKey: hover ? key : null,
})
},
getFlatInstanceArray () {
let instanceArray = this.instanceArray
const hasInnerArray = instanceArray.some((a) => {
return Array.isArray(a)
})
if (hasInnerArray) {
instanceArray = []
this.instanceArray.forEach((a) => {
if (Array.isArray(a)) {
instanceArray.push.apply(instanceArray, a)
} else {
instanceArray.push(a)
}
})
this.instanceArray = instanceArray
}
return instanceArray
},
renderCommonMenuItem (child, i, subIndex, extraProps) {
const state = this.state
const props = this.props
const key = getKeyFromChildrenIndex(child, props.eventKey, i)
const childProps = child.props
const isActive = key === state.activeKey
const newChildProps = {
mode: props.mode,
level: props.level,
inlineIndent: props.inlineIndent,
renderMenuItem: this.renderMenuItem,
rootPrefixCls: props.prefixCls,
index: i,
parentMenu: this,
ref: childProps.disabled ? undefined
: createChainedFunction(child.ref, saveRef.bind(this, i, subIndex)),
eventKey: key,
active: !childProps.disabled && isActive,
multiple: props.multiple,
onClick: this.onClick,
onItemHover: this.onItemHover,
openTransitionName: this.getOpenTransitionName(),
openAnimation: props.openAnimation,
subMenuOpenDelay: props.subMenuOpenDelay,
subMenuCloseDelay: props.subMenuCloseDelay,
forceSubMenuRender: props.forceSubMenuRender,
onOpenChange: this.onOpenChange,
onDeselect: this.onDeselect,
onDestroy: this.onDestroy,
onSelect: this.onSelect,
...extraProps,
}
if (props.mode === 'inline') {
newChildProps.triggerSubMenuAction = 'click'
}
return React.cloneElement(child, newChildProps)
},
renderRoot (props) {
this.instanceArray = []
const className = classNames(
props.prefixCls,
props.className,
`${props.prefixCls}-${props.mode}`,
)
const domProps = {
className,
role: 'menu',
'aria-activedescendant': '',
}
if (props.id) {
domProps.id = props.id
}
if (props.focusable) {
domProps.tabIndex = '0'
domProps.onKeyDown = this.onKeyDown
}
return (
// ESLint is not smart enough to know that the type of `children` was checked.
/* eslint-disable */
<DOMWrap
style={props.style}
tag="ul"
hiddenClassName={`${props.prefixCls}-hidden`}
visible={props.visible}
{...domProps}
>
{React.Children.map(props.children, this.renderMenuItem)}
</DOMWrap>
/*eslint -enable */
)
},
step (direction) {
let children = this.getFlatInstanceArray()
const activeKey = this.state.activeKey
const len = children.length
if (!len) {
return null
}
if (direction < 0) {
children = children.concat().reverse()
}
// find current activeIndex
let activeIndex = -1
children.every((c, ci) => {
if (c && c.props.eventKey === activeKey) {
activeIndex = ci
return false
}
return true
})
if (!this.props.defaultActiveFirst && activeIndex !== -1) {
if (allDisabled(children.slice(activeIndex, len - 1))) {
return undefined
}
}
const start = (activeIndex + 1) % len
let i = start
for (; ;) {
const child = children[i]
if (!child || child.props.disabled) {
i = (i + 1 + len) % len
// complete a loop
if (i === start) {
return null
}
} else {
return child
}
}
},
}
export default MenuMixin

@ -0,0 +1,443 @@
import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
import createReactClass from 'create-react-class'
import Trigger from 'rc-trigger'
import KeyCode from 'rc-util/lib/KeyCode'
import classNames from 'classnames'
import SubPopupMenu from './SubPopupMenu'
import placements from './placements'
import { noop, loopMenuItemRecusively } from './util'
let guid = 0
const popupPlacementMap = {
horizontal: 'bottomLeft',
vertical: 'rightTop',
'vertical-left': 'rightTop',
'vertical-right': 'leftTop',
}
const SubMenu = createReactClass({
displayName: 'SubMenu',
propTypes: {
parentMenu: PropTypes.object,
title: PropTypes.node,
children: PropTypes.any,
selectedKeys: PropTypes.array,
openKeys: PropTypes.array,
onClick: PropTypes.func,
onOpenChange: PropTypes.func,
rootPrefixCls: PropTypes.string,
eventKey: PropTypes.string,
multiple: PropTypes.bool,
active: PropTypes.bool, // TODO: remove
onItemHover: PropTypes.func,
onSelect: PropTypes.func,
triggerSubMenuAction: PropTypes.string,
onDeselect: PropTypes.func,
onDestroy: PropTypes.func,
onMouseEnter: PropTypes.func,
onMouseLeave: PropTypes.func,
onTitleMouseEnter: PropTypes.func,
onTitleMouseLeave: PropTypes.func,
onTitleClick: PropTypes.func,
},
isRootMenu: false,
getDefaultProps () {
return {
onMouseEnter: noop,
onMouseLeave: noop,
onTitleMouseEnter: noop,
onTitleMouseLeave: noop,
onTitleClick: noop,
title: '',
}
},
getInitialState () {
this.isSubMenu = 1
return {
defaultActiveFirst: false,
}
},
componentDidMount () {
this.componentDidUpdate()
},
componentDidUpdate () {
const { mode, parentMenu } = this.props
if (mode !== 'horizontal' || !parentMenu.isRootMenu || !this.isOpen()) {
return
}
setTimeout(() => {
if (!this.subMenuTitle || !this.menuInstance) {
return
}
const popupMenu = ReactDOM.findDOMNode(this.menuInstance)
if (popupMenu.offsetWidth >= this.subMenuTitle.offsetWidth) {
return
}
popupMenu.style.minWidth = `${this.subMenuTitle.offsetWidth}px`
}, 0)
},
componentWillUnmount () {
const { onDestroy, eventKey, parentMenu } = this.props
if (onDestroy) {
onDestroy(eventKey)
}
if (parentMenu.subMenuInstance === this) {
this.clearSubMenuTimers()
}
},
onDestroy (key) {
this.props.onDestroy(key)
},
onKeyDown (e) {
const keyCode = e.keyCode
const menu = this.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)
}
},
onOpenChange (e) {
this.props.onOpenChange(e)
},
onPopupVisibleChange (visible) {
this.triggerOpenChange(visible, visible ? 'mouseenter' : 'mouseleave')
},
onMouseEnter (e) {
const { eventKey: key, onMouseEnter } = this.props
this.clearSubMenuLeaveTimer()
this.setState({
defaultActiveFirst: false,
})
onMouseEnter({
key,
domEvent: e,
})
},
onMouseLeave (e) {
const {
parentMenu,
eventKey,
onMouseLeave,
} = this.props
parentMenu.subMenuInstance = this
parentMenu.subMenuLeaveFn = () => {
// trigger mouseleave
onMouseLeave({
key: eventKey,
domEvent: e,
})
}
// prevent popup menu and submenu gap
parentMenu.subMenuLeaveTimer = setTimeout(parentMenu.subMenuLeaveFn, 100)
},
onTitleMouseEnter (domEvent) {
const { eventKey: key, onItemHover, onTitleMouseEnter } = this.props
this.clearSubMenuTitleLeaveTimer()
onItemHover({
key,
hover: true,
})
onTitleMouseEnter({
key,
domEvent,
})
},
onTitleMouseLeave (e) {
const { parentMenu, eventKey, onItemHover, onTitleMouseLeave } = this.props
parentMenu.subMenuInstance = this
parentMenu.subMenuTitleLeaveFn = () => {
onItemHover({
key: eventKey,
hover: false,
})
onTitleMouseLeave({
key: eventKey,
domEvent: e,
})
}
parentMenu.subMenuTitleLeaveTimer = setTimeout(parentMenu.subMenuTitleLeaveFn, 100)
},
onTitleClick (e) {
const { props } = this
props.onTitleClick({
key: props.eventKey,
domEvent: e,
})
if (props.triggerSubMenuAction === 'hover') {
return
}
this.triggerOpenChange(!this.isOpen(), 'click')
this.setState({
defaultActiveFirst: false,
})
},
onSubMenuClick (info) {
this.props.onClick(this.addKeyPath(info))
},
onSelect (info) {
this.props.onSelect(info)
},
onDeselect (info) {
this.props.onDeselect(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) {
this.menuInstance = c
},
addKeyPath (info) {
return {
...info,
keyPath: (info.keyPath || []).concat(this.props.eventKey),
}
},
triggerOpenChange (open, type) {
const key = this.props.eventKey
this.onOpenChange({
key,
item: this,
trigger: type,
open,
})
},
clearSubMenuTimers () {
this.clearSubMenuLeaveTimer()
this.clearSubMenuTitleLeaveTimer()
},
clearSubMenuTitleLeaveTimer () {
const parentMenu = this.props.parentMenu
if (parentMenu.subMenuTitleLeaveTimer) {
clearTimeout(parentMenu.subMenuTitleLeaveTimer)
parentMenu.subMenuTitleLeaveTimer = null
parentMenu.subMenuTitleLeaveFn = null
}
},
clearSubMenuLeaveTimer () {
const parentMenu = this.props.parentMenu
if (parentMenu.subMenuLeaveTimer) {
clearTimeout(parentMenu.subMenuLeaveTimer)
parentMenu.subMenuLeaveTimer = null
parentMenu.subMenuLeaveFn = null
}
},
isChildrenSelected () {
const ret = { find: false }
loopMenuItemRecusively(this.props.children, this.props.selectedKeys, ret)
return ret.find
},
isOpen () {
return this.props.openKeys.indexOf(this.props.eventKey) !== -1
},
renderChildren (children) {
const props = this.props
const baseProps = {
mode: props.mode === 'horizontal' ? 'vertical' : props.mode,
visible: this.isOpen(),
level: props.level + 1,
inlineIndent: props.inlineIndent,
focusable: false,
onClick: this.onSubMenuClick,
onSelect: this.onSelect,
onDeselect: this.onDeselect,
onDestroy: this.onDestroy,
selectedKeys: props.selectedKeys,
eventKey: `${props.eventKey}-menu-`,
openKeys: props.openKeys,
openTransitionName: props.openTransitionName,
openAnimation: props.openAnimation,
onOpenChange: this.onOpenChange,
subMenuOpenDelay: props.subMenuOpenDelay,
subMenuCloseDelay: props.subMenuCloseDelay,
forceSubMenuRender: props.forceSubMenuRender,
triggerSubMenuAction: props.triggerSubMenuAction,
defaultActiveFirst: this.state.defaultActiveFirst,
multiple: props.multiple,
prefixCls: props.rootPrefixCls,
id: this._menuId,
ref: this.saveMenuInstance,
}
return <SubPopupMenu {...baseProps}>{children}</SubPopupMenu>
},
saveSubMenuTitle (subMenuTitle) {
this.subMenuTitle = subMenuTitle
},
render () {
const props = this.props
const isOpen = this.isOpen()
const prefixCls = this.getPrefixCls()
const isInlineMode = props.mode === 'inline'
const className = classNames(prefixCls, `${prefixCls}-${props.mode}`, {
[props.className]: !!props.className,
[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 = {
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
}
const title = (
<div
ref={this.saveSubMenuTitle}
style={style}
className={`${prefixCls}-title`}
{...titleMouseEvents}
{...titleClickEvents}
aria-expanded={isOpen}
aria-owns={this._menuId}
aria-haspopup='true'
title={typeof props.title === 'string' ? props.title : undefined}
>
{props.title}
<i className={`${prefixCls}-arrow`} />
</div>
)
const children = this.renderChildren(props.children)
const getPopupContainer = props.parentMenu.isRootMenu
? props.parentMenu.props.getPopupContainer : triggerNode => triggerNode.parentNode
const popupPlacement = popupPlacementMap[props.mode]
const popupClassName = props.mode === 'inline' ? '' : props.popupClassName
return (
<li {...mouseEvents} className={className} style={props.style}>
{isInlineMode && title}
{isInlineMode && children}
{!isInlineMode && (
<Trigger
prefixCls={prefixCls}
popupClassName={`${prefixCls}-popup ${popupClassName}`}
getPopupContainer={getPopupContainer}
builtinPlacements={placements}
popupPlacement={popupPlacement}
popupVisible={isOpen}
popup={children}
action={props.disabled ? [] : [props.triggerSubMenuAction]}
mouseEnterDelay={props.subMenuOpenDelay}
mouseLeaveDelay={props.subMenuCloseDelay}
onPopupVisibleChange={this.onPopupVisibleChange}
forceRender={props.forceSubMenuRender}
>
{title}
</Trigger>
)}
</li>
)
},
})
SubMenu.isSubMenu = 1
export default SubMenu

@ -0,0 +1,99 @@
import React from 'react'
import PropTypes from 'prop-types'
import createReactClass from 'create-react-class'
import Animate from 'rc-animate'
import MenuMixin from './MenuMixin'
const SubPopupMenu = createReactClass({
displayName: 'SubPopupMenu',
propTypes: {
onSelect: PropTypes.func,
onClick: PropTypes.func,
onDeselect: PropTypes.func,
onOpenChange: PropTypes.func,
onDestroy: PropTypes.func,
openTransitionName: PropTypes.string,
openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
openKeys: PropTypes.arrayOf(PropTypes.string),
visible: PropTypes.bool,
children: PropTypes.any,
},
mixins: [MenuMixin],
onDeselect (selectInfo) {
this.props.onDeselect(selectInfo)
},
onSelect (selectInfo) {
this.props.onSelect(selectInfo)
},
onClick (e) {
this.props.onClick(e)
},
onOpenChange (e) {
this.props.onOpenChange(e)
},
onDestroy (key) {
this.props.onDestroy(key)
},
getOpenTransitionName () {
return this.props.openTransitionName
},
renderMenuItem (c, i, subIndex) {
if (!c) {
return null
}
const props = this.props
const extraProps = {
openKeys: props.openKeys,
selectedKeys: props.selectedKeys,
triggerSubMenuAction: props.triggerSubMenuAction,
}
return this.renderCommonMenuItem(c, i, subIndex, extraProps)
},
render () {
const props = { ...this.props }
const haveRendered = this.haveRendered
this.haveRendered = true
this.haveOpened = this.haveOpened || props.visible || props.forceSubMenuRender
if (!this.haveOpened) {
return null
}
const transitionAppear = !(!haveRendered && props.visible && props.mode === 'inline')
props.className += ` ${props.prefixCls}-sub`
const animProps = {}
if (props.openTransitionName) {
animProps.transitionName = props.openTransitionName
} else if (typeof props.openAnimation === 'object') {
animProps.animation = { ...props.openAnimation }
if (!transitionAppear) {
delete animProps.animation.appear
}
}
return (
<Animate
{...animProps}
showProp='visible'
component=''
transitionAppear={transitionAppear}
>
{this.renderRoot(props)}
</Animate>
)
},
})
export default SubPopupMenu

@ -0,0 +1,9 @@
import Menu from './Menu'
import SubMenu from './SubMenu'
import MenuItem from './MenuItem'
import MenuItemGroup from './MenuItemGroup'
import Divider from './Divider'
export { SubMenu, MenuItem as Item, MenuItem, MenuItemGroup, MenuItemGroup as ItemGroup, Divider }
export default Menu

@ -0,0 +1,29 @@
const autoAdjustOverflow = {
adjustX: 1,
adjustY: 1,
}
export const placements = {
topLeft: {
points: ['bl', 'tl'],
overflow: autoAdjustOverflow,
offset: [0, -7],
},
bottomLeft: {
points: ['tl', 'bl'],
overflow: autoAdjustOverflow,
offset: [0, 7],
},
leftTop: {
points: ['tr', 'tl'],
overflow: autoAdjustOverflow,
offset: [-4, 0],
},
rightTop: {
points: ['tl', 'tr'],
overflow: autoAdjustOverflow,
offset: [4, 0],
},
}
export default placements

@ -0,0 +1,46 @@
import React from 'react'
export function noop () {
}
export function getKeyFromChildrenIndex (child, menuEventKey, index) {
const prefix = menuEventKey || ''
return child.key || `${prefix}item_${index}`
}
export function loopMenuItem (children, cb) {
let index = -1
React.Children.forEach(children, (c) => {
index++
if (c && c.type && c.type.isMenuItemGroup) {
React.Children.forEach(c.props.children, (c2) => {
index++
cb(c2, index)
})
} else {
cb(c, index)
}
})
}
export function loopMenuItemRecusively (children, keys, ret) {
if (!children || ret.find) {
return
}
React.Children.forEach(children, (c) => {
if (ret.find) {
return
}
if (c) {
const construt = c.type
if (!construt || !(construt.isSubMenu || construt.isMenuItem || construt.isMenuItemGroup)) {
return
}
if (keys.indexOf(c.key) !== -1) {
ret.find = true
} else if (c.props.children) {
loopMenuItemRecusively(c.props.children, keys, ret)
}
}
})
}

@ -79,6 +79,7 @@
"eslint-plugin-vue": "^3.13.0",
"lodash.debounce": "^4.0.8",
"omit.js": "^1.0.0",
"vue-types": "^1.0.2",
"warning": "^3.0.0"
}
}

Loading…
Cancel
Save