material-ui
Version:
Material Design UI components built with React
641 lines (542 loc) • 19.5 kB
JavaScript
'use strict';
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
var React = require('react');
var ReactDOM = require('react-dom');
var CssEvent = require('../utils/css-event');
var KeyLine = require('../utils/key-line');
var KeyCode = require('../utils/key-code');
var StylePropable = require('../mixins/style-propable');
var Transitions = require('../styles/transitions');
var ClickAwayable = require('../mixins/click-awayable');
var Paper = require('../paper');
var MenuItem = require('./menu-item');
var LinkMenuItem = require('./link-menu-item');
var SubheaderMenuItem = require('./subheader-menu-item');
var DefaultRawTheme = require('../styles/raw-themes/light-raw-theme');
var ThemeManager = require('../styles/theme-manager');
/***********************
* Nested Menu Component
***********************/
var NestedMenuItem = React.createClass({
displayName: 'NestedMenuItem',
mixins: [ClickAwayable, StylePropable],
contextTypes: {
muiTheme: React.PropTypes.object
},
propTypes: {
index: React.PropTypes.number.isRequired,
text: React.PropTypes.string,
menuItems: React.PropTypes.array.isRequired,
zDepth: React.PropTypes.number,
disabled: React.PropTypes.bool,
active: React.PropTypes.bool,
onItemTap: React.PropTypes.func,
menuItemStyle: React.PropTypes.object
},
getDefaultProps: function getDefaultProps() {
return {
disabled: false
};
},
//for passing default theme context to children
childContextTypes: {
muiTheme: React.PropTypes.object
},
getChildContext: function getChildContext() {
return {
muiTheme: this.state.muiTheme
};
},
getInitialState: function getInitialState() {
return {
muiTheme: this.context.muiTheme ? this.context.muiTheme : ThemeManager.getMuiTheme(DefaultRawTheme),
open: false,
activeIndex: 0
};
},
componentClickAway: function componentClickAway() {
this._closeNestedMenu();
},
componentDidMount: function componentDidMount() {
this._positionNestedMenu();
ReactDOM.findDOMNode(this).focus();
},
componentDidUpdate: function componentDidUpdate() {
this._positionNestedMenu();
},
getSpacing: function getSpacing() {
return this.state.muiTheme.rawTheme.spacing;
},
getStyles: function getStyles() {
var 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: function getTheme() {
return this.state.muiTheme.menuItem;
},
render: function render() {
var styles = this.getStyles();
styles = this.prepareStyles(styles.root, this.props.active && !this.props.disabled && styles.rootWhenHovered, {
position: 'relative'
}, this.props.style);
var iconCustomArrowDropRight = {
marginRight: this.getSpacing().desktopGutterMini * -1,
color: this.state.muiTheme.dropDownMenu.accentColor
};
var _props = this.props;
var index = _props.index;
var menuItemStyle = _props.menuItemStyle;
var other = _objectWithoutProperties(_props, ['index', 'menuItemStyle']);
return React.createElement(
'div',
{
ref: 'root',
style: styles,
onMouseEnter: this._openNestedMenu,
onMouseLeave: this._closeNestedMenu,
onMouseOver: this._handleMouseOver,
onMouseOut: this._handleMouseOut },
React.createElement(
MenuItem,
{
index: index,
style: menuItemStyle,
disabled: this.props.disabled,
iconRightStyle: iconCustomArrowDropRight,
iconRightClassName: 'muidocs-icon-custom-arrow-drop-right',
onTouchTap: this._onParentItemTap },
this.props.text
),
React.createElement(Menu, _extends({}, 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 }))
);
},
toggleNestedMenu: function toggleNestedMenu() {
if (!this.props.disabled) this.setState({ open: !this.state.open });
},
isOpen: function isOpen() {
return this.state.open;
},
_positionNestedMenu: function _positionNestedMenu() {
var el = ReactDOM.findDOMNode(this);
var nestedMenu = ReactDOM.findDOMNode(this.refs.nestedMenu);
nestedMenu.style.left = el.offsetWidth + 'px';
},
_openNestedMenu: function _openNestedMenu() {
if (!this.props.disabled) this.setState({ open: true });
},
_closeNestedMenu: function _closeNestedMenu() {
this.setState({ open: false });
ReactDOM.findDOMNode(this).focus();
},
_onParentItemTap: function _onParentItemTap() {
this.toggleNestedMenu();
},
_onMenuItemTap: function _onMenuItemTap(e, index, menuItem) {
if (this.props.onItemTap) this.props.onItemTap(e, index, menuItem);
this._closeNestedMenu();
},
_handleMouseOver: function _handleMouseOver(e) {
if (!this.props.disabled && this.props.onMouseOver) this.props.onMouseOver(e, this.props.index);
},
_handleMouseOut: function _handleMouseOut(e) {
if (!this.props.disabled && this.props.onMouseOut) this.props.onMouseOut(e, this.props.index);
}
});
/****************
* Menu Component
****************/
var Menu = React.createClass({
displayName: 'Menu',
mixins: [StylePropable],
contextTypes: {
muiTheme: React.PropTypes.object
},
propTypes: {
autoWidth: React.PropTypes.bool,
onItemTap: React.PropTypes.func,
onToggle: React.PropTypes.func,
onRequestClose: React.PropTypes.func,
menuItems: React.PropTypes.array.isRequired,
selectedIndex: React.PropTypes.number,
hideable: React.PropTypes.bool,
visible: React.PropTypes.bool,
zDepth: React.PropTypes.number,
menuItemStyle: React.PropTypes.object,
menuItemStyleSubheader: React.PropTypes.object,
menuItemStyleLink: React.PropTypes.object,
menuItemClassName: React.PropTypes.string,
menuItemClassNameSubheader: React.PropTypes.string,
menuItemClassNameLink: React.PropTypes.string
},
//for passing default theme context to children
childContextTypes: {
muiTheme: React.PropTypes.object
},
getChildContext: function getChildContext() {
return {
muiTheme: this.state.muiTheme
};
},
getInitialState: function getInitialState() {
return {
muiTheme: this.context.muiTheme ? this.context.muiTheme : ThemeManager.getMuiTheme(DefaultRawTheme),
nestedMenuShown: false,
activeIndex: 0
};
},
getDefaultProps: function getDefaultProps() {
return {
autoWidth: true,
hideable: false,
visible: true,
zDepth: 1,
onRequestClose: function onRequestClose() {}
};
},
componentDidMount: function componentDidMount() {
var el = ReactDOM.findDOMNode(this);
//Set the menu width
this._setKeyWidth(el);
//Show or Hide the menu according to visibility
this._renderVisibility();
},
componentDidUpdate: function componentDidUpdate(prevProps) {
if (this.props.visible !== prevProps.visible || this.props.menuItems.length !== prevProps.menuItems.length) {
this._renderVisibility();
}
},
//to update theme inside state whenever a new theme is passed down
//from the parent / owner using context
componentWillReceiveProps: function componentWillReceiveProps(nextProps, nextContext) {
var newMuiTheme = nextContext.muiTheme ? nextContext.muiTheme : this.state.muiTheme;
this.setState({ muiTheme: newMuiTheme });
//Set the menu width
this._setKeyWidth(ReactDOM.findDOMNode(this));
},
getTheme: function getTheme() {
return this.state.muiTheme.menu;
},
getSpacing: function getSpacing() {
return this.state.muiTheme.rawTheme.spacing;
},
getStyles: function getStyles() {
var 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;
},
render: function render() {
var styles = this.getStyles();
return React.createElement(
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()
);
},
_getChildren: function _getChildren() {
var menuItem = undefined,
itemComponent = undefined,
isDisabled = undefined;
var styles = this.getStyles();
this._children = [];
//This array is used to keep track of all nested menu refs
this._nestedChildren = [];
for (var i = 0; i < this.props.menuItems.length; i++) {
menuItem = this.props.menuItems[i];
isDisabled = menuItem.disabled === undefined ? false : menuItem.disabled;
var _menuItem = menuItem;
var icon = _menuItem.icon;
var data = _menuItem.data;
var attribute = _menuItem.attribute;
var number = _menuItem.number;
var toggle = _menuItem.toggle;
var onTouchTap = _menuItem.onTouchTap;
var other = _objectWithoutProperties(_menuItem, ['icon', 'data', 'attribute', 'number', 'toggle', 'onTouchTap']);
switch (menuItem.type) {
case MenuItem.Types.LINK:
itemComponent = React.createElement(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 = React.createElement(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:
var _props2 = this.props,
ref = _props2.ref,
key = _props2.key,
index = _props2.index,
zDepth = _props2.zDepth,
other = _objectWithoutProperties(_props2, ['ref', 'key', 'index', 'zDepth']);
itemComponent = React.createElement(NestedMenuItem, _extends({}, 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 = React.createElement(
MenuItem,
_extends({}, 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
);
}
this._children.push(itemComponent);
}
return this._children;
},
_setKeyWidth: function _setKeyWidth(el) {
//Update the menu width
var menuWidth = '100%';
if (this.props.autoWidth) {
el.style.width = 'auto';
menuWidth = KeyLine.getIncrementalDim(el.offsetWidth) + 'px';
}
el.style.width = menuWidth;
},
_renderVisibility: function _renderVisibility() {
if (this.props.hideable) {
if (this.props.visible) this._expandHideableMenu();else this._collapseHideableMenu();
}
},
_expandHideableMenu: function _expandHideableMenu() {
var _this = this;
var el = ReactDOM.findDOMNode(this);
var container = ReactDOM.findDOMNode(this.refs.paperContainer);
var padding = this.getSpacing().desktopGutterMini;
var height = this._getHiddenMenuHeight(el, padding);
//Add transition
if (!el.style.transition) {
el.style.transition = Transitions.easeOut();
}
this._nextAnimationFrame(function () {
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, function () {
//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: function _getHiddenMenuHeight(el, padding) {
//Add padding to the offset height, because it is not yet set in the style.
var 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: function _collapseHideableMenu() {
var el = ReactDOM.findDOMNode(this);
var container = ReactDOM.findDOMNode(this.refs.paperContainer);
var 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';
var end = function end() {
el.style.transition = null;
};
if (originalOpacity === '') end();else CssEvent.onTransitionEnd(el, end);
});
},
_nextAnimationFrame: function _nextAnimationFrame(func) {
if (window.requestAnimationFrame) {
return window.requestAnimationFrame(func);
}
return setTimeout(func, 16);
},
_onNestedItemTap: function _onNestedItemTap(e, index, menuItem) {
if (this.props.onItemTap) this.props.onItemTap(e, index, menuItem);
},
_onItemTap: function _onItemTap(e, index) {
if (this.props.onItemTap) this.props.onItemTap(e, index, this.props.menuItems[index]);
},
_onItemToggle: function _onItemToggle(e, index, toggled) {
if (this.props.onItemToggle) this.props.onItemToggle(e, index, this.props.menuItems[index], toggled);
},
_onItemActivated: function _onItemActivated(e, index) {
this.setState({ activeIndex: index });
},
_onItemDeactivated: function _onItemDeactivated(e, index) {
if (this.state.activeKey === index) this.setState({ activeIndex: 0 });
},
_onKeyDown: function _onKeyDown(e) {
if (!(this.state.open || this.props.visible)) return;
var 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: function _activatePreviousItem() {
var active = this.state.activeIndex || 0;
active = Math.max(active - 1, 0);
this.setState({ activeIndex: active });
},
_activateNextItem: function _activateNextItem() {
var active = this.state.activeIndex || 0;
active = Math.min(active + 1, this._children.length - 1);
this.setState({ activeIndex: active });
},
_triggerSelection: function _triggerSelection(e) {
var index = this.state.activeIndex || 0;
this._onItemTap(e, index);
},
_close: function _close() {
this.props.onRequestClose();
},
_tryToggleNested: function _tryToggleNested(index) {
var item = this.refs[index];
if (item && item.toggleNestedMenu) item.toggleNestedMenu();
}
});
module.exports = Menu;