d2-ui
Version:
657 lines (559 loc) • 17.3 kB
JSX
import React from 'react';
import ReactDOM from 'react-dom';
import CssEvent from '../utils/css-event';
import KeyLine from '../utils/key-line';
import KeyCode from '../utils/key-code';
import StylePropable from '../mixins/style-propable';
import Transitions from '../styles/transitions';
import ClickAwayable from '../mixins/click-awayable';
import Paper from '../paper';
import MenuItem from './menu-item';
import LinkMenuItem from './link-menu-item';
import SubheaderMenuItem from './subheader-menu-item';
import getMuiTheme from '../styles/getMuiTheme';
import warning from 'warning';
/*eslint-disable */
/***********************
* Nested Menu Component
***********************/
const NestedMenuItem = React.createClass({
propTypes: {
active: React.PropTypes.bool,
disabled: React.PropTypes.bool,
index: React.PropTypes.number.isRequired,
menuItemStyle: React.PropTypes.object,
menuItems: React.PropTypes.array.isRequired,
onItemTap: React.PropTypes.func,
onMouseOut: React.PropTypes.func,
onMouseOver: React.PropTypes.func,
style: React.PropTypes.object,
text: React.PropTypes.string,
zDepth: React.PropTypes.number,
},
contextTypes: {
muiTheme: React.PropTypes.object,
},
//for passing default theme context to children
childContextTypes: {
muiTheme: React.PropTypes.object,
},
mixins: [ClickAwayable, StylePropable],
getDefaultProps() {
return {
disabled: false,
};
},
getInitialState() {
return {
muiTheme: this.context.muiTheme || getMuiTheme(),
open: false,
activeIndex: 0,
};
},
getChildContext() {
return {
muiTheme: this.state.muiTheme,
};
},
componentDidMount() {
this._positionNestedMenu();
ReactDOM.findDOMNode(this).focus();
},
componentDidUpdate() {
this._positionNestedMenu();
},
componentClickAway() {
this._closeNestedMenu();
},
getSpacing() {
return this.state.muiTheme.rawTheme.spacing;
},
getStyles() {
let styles = {
root: {
userSelect: 'none',
cursor: 'pointer',
lineHeight: this.getTheme().height + 'px',
color: this.state.muiTheme.rawTheme.palette.textColor,
},
icon: {
float: 'left',
lineHeight: this.getTheme().height + 'px',
marginRight: this.getSpacing().desktopGutter,
},
toggle: {
marginTop: ((this.getTheme().height - this.state.muiTheme.radioButton.size) / 2),
float: 'right',
width: 42,
},
rootWhenHovered: {
backgroundColor: this.getTheme().hoverColor,
},
rootWhenSelected: {
color: this.getTheme().selectedTextColor,
},
rootWhenDisabled: {
cursor: 'default',
color: this.state.muiTheme.rawTheme.palette.disabledColor,
},
};
return styles;
},
getTheme() {
return this.state.muiTheme.menuItem;
},
toggleNestedMenu() {
if (!this.props.disabled) this.setState({open: !this.state.open});
},
isOpen() {
return this.state.open;
},
_positionNestedMenu() {
let el = ReactDOM.findDOMNode(this);
let nestedMenu = ReactDOM.findDOMNode(this.refs.nestedMenu);
nestedMenu.style.left = el.offsetWidth + 'px';
},
_openNestedMenu() {
if (!this.props.disabled) this.setState({open: true});
},
_closeNestedMenu() {
this.setState({open: false});
ReactDOM.findDOMNode(this).focus();
},
_onParentItemTap() {
this.toggleNestedMenu();
},
_onMenuItemTap(e, index, menuItem) {
if (this.props.onItemTap) this.props.onItemTap(e, index, menuItem);
this._closeNestedMenu();
},
_handleMouseOver(e) {
if (!this.props.disabled && this.props.onMouseOver) this.props.onMouseOver(e, this.props.index);
},
_handleMouseOut(e) {
if (!this.props.disabled && this.props.onMouseOut) this.props.onMouseOut(e, this.props.index);
},
render() {
let styles = this.getStyles();
styles = this.mergeStyles(styles.root,
(this.props.active && !this.props.disabled) && styles.rootWhenHovered, {
position: 'relative',
}, this.props.style);
let iconCustomArrowDropRight = {
marginRight: this.getSpacing().desktopGutterMini * -1,
color: this.state.muiTheme.dropDownMenu.accentColor,
};
let {
index,
menuItemStyle,
...other,
} = this.props;
return (
<div
ref="root"
style={this.prepareStyles(styles)}
onMouseEnter={this._openNestedMenu}
onMouseLeave={this._closeNestedMenu}
onMouseOver={this._handleMouseOver}
onMouseOut={this._handleMouseOut}>
<MenuItem
index={index}
style={menuItemStyle}
disabled={this.props.disabled}
iconRightStyle={iconCustomArrowDropRight}
iconRightClassName="muidocs-icon-custom-arrow-drop-right"
onTouchTap={this._onParentItemTap}>
{this.props.text}
</MenuItem>
<Menu {...other}
ref="nestedMenu"
menuItems={this.props.menuItems}
menuItemStyle={menuItemStyle}
onItemTap={this._onMenuItemTap}
hideable={true}
visible={this.state.open}
onRequestClose={this._closeNestedMenu}
zDepth={this.props.zDepth + 1} />
</div>
);
},
});
/****************
* Menu Component
****************/
const Menu = React.createClass({
propTypes: {
autoWidth: React.PropTypes.bool,
hideable: React.PropTypes.bool,
menuItemClassName: React.PropTypes.string,
menuItemClassNameLink: React.PropTypes.string,
menuItemClassNameSubheader: React.PropTypes.string,
menuItemStyle: React.PropTypes.object,
menuItemStyleLink: React.PropTypes.object,
menuItemStyleSubheader: React.PropTypes.object,
menuItems: React.PropTypes.array.isRequired,
onItemTap: React.PropTypes.func,
onItemToggle: React.PropTypes.func,
onRequestClose: React.PropTypes.func,
onToggle: React.PropTypes.func,
selectedIndex: React.PropTypes.number,
style: React.PropTypes.object,
visible: React.PropTypes.bool,
zDepth: React.PropTypes.number,
},
contextTypes: {
muiTheme: React.PropTypes.object,
},
//for passing default theme context to children
childContextTypes: {
muiTheme: React.PropTypes.object,
},
mixins: [StylePropable],
getDefaultProps() {
return {
autoWidth: true,
hideable: false,
visible: true,
zDepth: 1,
onRequestClose: () => {},
};
},
getInitialState() {
warning(false, 'This menu component is deprecated use menus/menu instead.');
return {
muiTheme: this.context.muiTheme || getMuiTheme(),
nestedMenuShown: false,
activeIndex: 0,
};
},
getChildContext() {
return {
muiTheme: this.state.muiTheme,
};
},
componentDidMount() {
let el = ReactDOM.findDOMNode(this);
//Set the menu width
this._setKeyWidth(el);
//Show or Hide the menu according to visibility
this._renderVisibility();
},
//to update theme inside state whenever a new theme is passed down
//from the parent / owner using context
componentWillReceiveProps(nextProps, nextContext) {
let newMuiTheme = nextContext.muiTheme ? nextContext.muiTheme : this.state.muiTheme;
this.setState({muiTheme: newMuiTheme});
//Set the menu width
this._setKeyWidth(ReactDOM.findDOMNode(this));
},
componentDidUpdate(prevProps) {
if (this.props.visible !== prevProps.visible || this.props.menuItems.length !== prevProps.menuItems.length) {
this._renderVisibility();
}
},
getTheme() {
return this.state.muiTheme.menu;
},
getSpacing() {
return this.state.muiTheme.rawTheme.spacing;
},
getStyles() {
let styles = {
root: {
backgroundColor: this.getTheme().containerBackgroundColor,
paddingTop: this.getSpacing().desktopGutterMini,
paddingBottom: this.getSpacing().desktopGutterMini,
transition: Transitions.easeOut(null, 'height'),
outline: 'none !important',
},
subheader: {
paddingLeft: this.state.muiTheme.menuSubheader.padding,
paddingRight: this.state.muiTheme.menuSubheader.padding,
},
hideable: {
overflow: 'hidden',
position: 'absolute',
top: 0,
zIndex: 1,
},
item: {
height: 34,
},
};
return styles;
},
_getChildren() {
let menuItem;
let itemComponent;
let isDisabled;
let styles = this.getStyles();
this._children = [];
//This array is used to keep track of all nested menu refs
this._nestedChildren = [];
for (let i = 0; i < this.props.menuItems.length; i++) {
menuItem = this.props.menuItems[i];
isDisabled = (menuItem.disabled === undefined) ? false : menuItem.disabled;
let {
icon,
data,
attribute,
number,
toggle,
onTouchTap,
...other,
} = menuItem;
switch (menuItem.type) {
case MenuItem.Types.LINK:
itemComponent = (
<LinkMenuItem
key={i}
index={i}
active={this.state.activeIndex === i}
text={menuItem.text}
disabled={isDisabled}
className={this.props.menuItemClassNameLink}
style={this.props.menuItemStyleLink}
payload={menuItem.payload}
target={menuItem.target}/>
);
break;
case MenuItem.Types.SUBHEADER:
itemComponent = (
<SubheaderMenuItem
key={i}
index={i}
className={this.props.menuItemClassNameSubheader}
style={this.mergeStyles(styles.subheader, this.props.menuItemStyleSubheader)}
firstChild={i === 0}
text={menuItem.text} />
);
break;
case MenuItem.Types.NESTED:
let {
zDepth,
...other,
} = this.props;
itemComponent = (
<NestedMenuItem
{...other}
ref={i}
key={i}
index={i}
nested={true}
active={this.state.activeIndex === i}
text={menuItem.text}
disabled={isDisabled}
menuItems={menuItem.items}
menuItemStyle={this.props.menuItemStyle}
zDepth={this.props.zDepth}
onMouseEnter={this._onItemActivated}
onMouseLeave={this._onItemDeactivated}
onItemTap={this._onNestedItemTap} />
);
this._nestedChildren.push(i);
break;
default:
itemComponent = (
<MenuItem
{...other}
selected={this.props.selectedIndex === i}
key={i}
index={i}
active={this.state.activeIndex === i}
icon={menuItem.icon}
data={menuItem.data}
className={this.props.menuItemClassName}
style={this.props.menuItemStyle}
attribute={menuItem.attribute}
number={menuItem.number}
toggle={menuItem.toggle}
onToggle={this.props.onToggle}
disabled={isDisabled}
onTouchTap={this._onItemTap}
onMouseEnter={this._onItemActivated}
onMouseLeave={this._onItemDeactivated}
>
{menuItem.text}
</MenuItem>
);
}
this._children.push(itemComponent);
}
return this._children;
},
_setKeyWidth(el) {
//Update the menu width
let menuWidth = '100%';
if (this.props.autoWidth) {
el.style.width = 'auto';
menuWidth = KeyLine.getIncrementalDim(el.offsetWidth) + 'px';
}
el.style.width = menuWidth;
},
_renderVisibility() {
if (this.props.hideable) {
if (this.props.visible) this._expandHideableMenu();
else this._collapseHideableMenu();
}
},
_expandHideableMenu() {
let el = ReactDOM.findDOMNode(this);
let container = ReactDOM.findDOMNode(this.refs.paperContainer);
let padding = this.getSpacing().desktopGutterMini;
let height = this._getHiddenMenuHeight(el, padding);
//Add transition
if (!el.style.transition) {
el.style.transition = Transitions.easeOut();
}
this._nextAnimationFrame(() => {
container.style.overflow = 'hidden';
// Yeild to the DOM, then apply height and padding. This makes the transition smoother.
el.style.paddingTop = padding + 'px';
el.style.paddingBottom = padding + 'px';
el.style.height = height + 'px';
el.style.opacity = 1;
//Set the overflow to visible after the animation is done so
//that other nested menus can be shown
CssEvent.onTransitionEnd(el, () => {
//Make sure the menu is open before setting the overflow.
//This is to accout for fast clicks
if (this.props.visible) container.style.overflow = 'visible';
el.style.transition = null;
el.focus();
});
});
},
_getHiddenMenuHeight(el, padding) {
//Add padding to the offset height, because it is not yet set in the style.
let height = padding * 2;
//Hide the element and allow the browser to automatically resize it.
el.style.visibility = 'hidden';
el.style.height = 'auto';
//Determine the height of the menu.
height += el.offsetHeight;
//Unhide the menu with the height set back to zero.
el.style.height = '0px';
el.style.visibility = 'visible';
return height;
},
_collapseHideableMenu() {
let el = ReactDOM.findDOMNode(this);
let container = ReactDOM.findDOMNode(this.refs.paperContainer);
let originalOpacity = el.style.opacity;
//Add transition
if (!el.style.transition && originalOpacity !== '') {
el.style.transition = Transitions.easeOut();
}
this._nextAnimationFrame(function() {
//Set the overflow to hidden so that animation works properly
container.style.overflow = 'hidden';
//Close the menu
el.style.opacity = 0;
el.style.height = '0px';
el.style.paddingTop = '0px';
el.style.paddingBottom = '0px';
let end = () => {
el.style.transition = null;
};
if (originalOpacity === '') end();
else CssEvent.onTransitionEnd(el, end);
});
},
_nextAnimationFrame(func) {
if (window.requestAnimationFrame) {
return window.requestAnimationFrame(func);
}
return setTimeout(func, 16);
},
_onNestedItemTap(e, index, menuItem) {
if (this.props.onItemTap) this.props.onItemTap(e, index, menuItem);
},
_onItemTap(e, index) {
if (this.props.onItemTap) this.props.onItemTap(e, index, this.props.menuItems[index]);
},
_onItemToggle(e, index, toggled) {
if (this.props.onItemToggle) this.props.onItemToggle(e, index, this.props.menuItems[index], toggled);
},
_onItemActivated(e, index) {
this.setState({activeIndex: index});
},
_onItemDeactivated(e, index) {
if (this.state.activeKey === index)
this.setState({activeIndex: 0});
},
_onKeyDown(e) {
if (!(this.state.open || this.props.visible))
return;
let nested = this._children[this.state.activeIndex];
if (nested && nested.props.nested && this.refs[this.state.activeIndex].isOpen())
return;
switch (e.which) {
case KeyCode.UP:
this._activatePreviousItem();
break;
case KeyCode.DOWN:
this._activateNextItem();
break;
case KeyCode.RIGHT:
this._tryToggleNested(this.state.activeIndex);
break;
case KeyCode.LEFT:
this._close();
break;
case KeyCode.ESC:
this._close();
break;
case KeyCode.TAB:
this._close();
return; // so the tab key can propagate
case KeyCode.ENTER:
case KeyCode.SPACE:
e.stopPropagation(); // needs called before the close
this._triggerSelection(e);
break;
default:
return; //important
}
e.preventDefault();
e.stopPropagation();
},
_activatePreviousItem() {
let active = this.state.activeIndex || 0;
active = Math.max(active - 1, 0);
this.setState({activeIndex: active});
},
_activateNextItem() {
let active = this.state.activeIndex || 0;
active = Math.min(active + 1, this._children.length - 1);
this.setState({activeIndex: active});
},
_triggerSelection(e) {
let index = this.state.activeIndex || 0;
this._onItemTap(e, index);
},
_close() {
this.props.onRequestClose();
},
_tryToggleNested(index) {
let item = this.refs[index];
if (item && item.toggleNestedMenu)
item.toggleNestedMenu();
},
render() {
let styles = this.getStyles();
return (
<Paper
ref="paperContainer"
tabIndex="0"
onKeyDown={this._onKeyDown}
zDepth={this.props.zDepth}
style={this.mergeStyles(
styles.root,
this.props.hideable && styles.hideable,
this.props.style)}>
{this._getChildren()}
</Paper>
);
},
});
export default Menu;