material-ui
Version:
Material Design UI components built with React
362 lines (309 loc) • 11.3 kB
JavaScript
'use strict';
var isBrowser = require('./utils/is-browser');
var Modernizr = isBrowser ? require('./utils/modernizr.custom') : undefined;
var React = require('react');
var ReactDOM = require('react-dom');
var KeyCode = require('./utils/key-code');
var StylePropable = require('./mixins/style-propable');
var AutoPrefix = require('./styles/auto-prefix');
var Transitions = require('./styles/transitions');
var WindowListenable = require('./mixins/window-listenable');
var Overlay = require('./overlay');
var Paper = require('./paper');
var Menu = require('./menu/menu');
var DefaultRawTheme = require('./styles/raw-themes/light-raw-theme');
var ThemeManager = require('./styles/theme-manager');
var openNavEventHandler = null;
var LeftNav = React.createClass({
displayName: 'LeftNav',
mixins: [StylePropable, WindowListenable],
contextTypes: {
muiTheme: React.PropTypes.object
},
//for passing default theme context to children
childContextTypes: {
muiTheme: React.PropTypes.object
},
getChildContext: function getChildContext() {
return {
muiTheme: this.state.muiTheme
};
},
propTypes: {
className: React.PropTypes.string,
disableSwipeToOpen: React.PropTypes.bool,
docked: React.PropTypes.bool,
header: React.PropTypes.element,
menuItems: React.PropTypes.array.isRequired,
onChange: React.PropTypes.func,
onNavOpen: React.PropTypes.func,
onNavClose: React.PropTypes.func,
openRight: React.PropTypes.bool,
selectedIndex: React.PropTypes.number,
menuItemClassName: React.PropTypes.string,
menuItemClassNameSubheader: React.PropTypes.string,
menuItemClassNameLink: React.PropTypes.string
},
windowListeners: {
'keyup': '_onWindowKeyUp',
'resize': '_onWindowResize'
},
getDefaultProps: function getDefaultProps() {
return {
disableSwipeToOpen: false,
docked: true
};
},
getInitialState: function getInitialState() {
this._maybeSwiping = false;
this._touchStartX = null;
this._touchStartY = null;
this._swipeStartX = null;
return {
open: this.props.docked,
swiping: null,
muiTheme: this.context.muiTheme ? this.context.muiTheme : ThemeManager.getMuiTheme(DefaultRawTheme)
};
},
//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 });
},
componentDidMount: function componentDidMount() {
this._updateMenuHeight();
this._enableSwipeHandling();
},
componentDidUpdate: function componentDidUpdate() {
this._updateMenuHeight();
this._enableSwipeHandling();
},
componentWillUnmount: function componentWillUnmount() {
this._disableSwipeHandling();
},
toggle: function toggle() {
this.setState({ open: !this.state.open });
return this;
},
close: function close() {
this.setState({ open: false });
if (this.props.onNavClose) this.props.onNavClose();
return this;
},
open: function open() {
this.setState({ open: true });
if (this.props.onNavOpen) this.props.onNavOpen();
return this;
},
getThemePalette: function getThemePalette() {
return this.state.muiTheme.rawTheme.palette;
},
getTheme: function getTheme() {
return this.state.muiTheme.leftNav;
},
getStyles: function getStyles() {
var x = this._getTranslateMultiplier() * (this.state.open ? 0 : this._getMaxTranslateX());
var styles = {
root: {
height: '100%',
width: this.getTheme().width,
position: 'fixed',
zIndex: 10,
left: isBrowser && Modernizr.csstransforms3d ? 0 : x,
top: 0,
transform: 'translate3d(' + x + 'px, 0, 0)',
transition: !this.state.swiping && Transitions.easeOut(),
backgroundColor: this.getTheme().color,
overflow: 'hidden'
},
menu: {
overflowY: 'auto',
overflowX: 'hidden',
height: '100%',
borderRadius: '0'
},
menuItem: {
height: this.state.muiTheme.rawTheme.spacing.desktopLeftNavMenuItemHeight,
lineHeight: this.state.muiTheme.rawTheme.spacing.desktopLeftNavMenuItemHeight + 'px'
},
rootWhenOpenRight: {
left: 'auto',
right: 0
}
};
styles.menuItemLink = this.mergeStyles(styles.menuItem, {
display: 'block',
textDecoration: 'none',
color: this.getThemePalette().textColor
});
styles.menuItemSubheader = this.mergeStyles(styles.menuItem, {
overflow: 'hidden'
});
return styles;
},
render: function render() {
var selectedIndex = this.props.selectedIndex;
var overlay = undefined;
var styles = this.getStyles();
if (!this.props.docked) {
overlay = React.createElement(Overlay, {
ref: 'overlay',
show: this.state.open || !!this.state.swiping,
transitionEnabled: !this.state.swiping,
onTouchTap: this._onOverlayTouchTap });
}
return React.createElement(
'div',
{ className: this.props.className },
overlay,
React.createElement(
Paper,
{
ref: 'clickAwayableElement',
zDepth: 2,
rounded: false,
transitionEnabled: !this.state.swiping,
style: this.mergeStyles(styles.root, this.props.openRight && styles.rootWhenOpenRight, this.props.style) },
this.props.header,
React.createElement(Menu, {
ref: 'menuItems',
style: this.mergeStyles(styles.menu),
zDepth: 0,
menuItems: this.props.menuItems,
menuItemStyle: this.mergeStyles(styles.menuItem),
menuItemStyleLink: this.mergeStyles(styles.menuItemLink),
menuItemStyleSubheader: this.mergeStyles(styles.menuItemSubheader),
menuItemClassName: this.props.menuItemClassName,
menuItemClassNameSubheader: this.props.menuItemClassNameSubheader,
menuItemClassNameLink: this.props.menuItemClassNameLink,
selectedIndex: selectedIndex,
onItemTap: this._onMenuItemClick })
)
);
},
_updateMenuHeight: function _updateMenuHeight() {
if (this.props.header) {
var container = ReactDOM.findDOMNode(this.refs.clickAwayableElement);
var menu = ReactDOM.findDOMNode(this.refs.menuItems);
var menuHeight = container.clientHeight - menu.offsetTop;
menu.style.height = menuHeight + 'px';
}
},
_onMenuItemClick: function _onMenuItemClick(e, key, payload) {
if (this.props.onChange && this.props.selectedIndex !== key) {
this.props.onChange(e, key, payload);
}
if (!this.props.docked) this.close();
},
_onOverlayTouchTap: function _onOverlayTouchTap() {
this.close();
},
_onWindowKeyUp: function _onWindowKeyUp(e) {
if (e.keyCode === KeyCode.ESC && !this.props.docked && this.state.open) {
this.close();
}
},
_onWindowResize: function _onWindowResize() {
this._updateMenuHeight();
},
_getMaxTranslateX: function _getMaxTranslateX() {
return this.getTheme().width + 10;
},
_getTranslateMultiplier: function _getTranslateMultiplier() {
return this.props.openRight ? 1 : -1;
},
_enableSwipeHandling: function _enableSwipeHandling() {
if (!this.props.docked) {
document.body.addEventListener('touchstart', this._onBodyTouchStart);
if (!openNavEventHandler) {
openNavEventHandler = this._onBodyTouchStart;
}
} else {
this._disableSwipeHandling();
}
},
_disableSwipeHandling: function _disableSwipeHandling() {
document.body.removeEventListener('touchstart', this._onBodyTouchStart);
if (openNavEventHandler === this._onBodyTouchStart) {
openNavEventHandler = null;
}
},
_onBodyTouchStart: function _onBodyTouchStart(e) {
if (!this.state.open && (openNavEventHandler !== this._onBodyTouchStart || this.props.disableSwipeToOpen)) {
return;
}
var touchStartX = e.touches[0].pageX;
var touchStartY = e.touches[0].pageY;
this._maybeSwiping = true;
this._touchStartX = touchStartX;
this._touchStartY = touchStartY;
document.body.addEventListener('touchmove', this._onBodyTouchMove);
document.body.addEventListener('touchend', this._onBodyTouchEnd);
document.body.addEventListener('touchcancel', this._onBodyTouchEnd);
},
_setPosition: function _setPosition(translateX) {
var leftNav = ReactDOM.findDOMNode(this.refs.clickAwayableElement);
leftNav.style[AutoPrefix.single('transform')] = 'translate3d(' + this._getTranslateMultiplier() * translateX + 'px, 0, 0)';
this.refs.overlay.setOpacity(1 - translateX / this._getMaxTranslateX());
},
_getTranslateX: function _getTranslateX(currentX) {
return Math.min(Math.max(this.state.swiping === 'closing' ? this._getTranslateMultiplier() * (currentX - this._swipeStartX) : this._getMaxTranslateX() - this._getTranslateMultiplier() * (this._swipeStartX - currentX), 0), this._getMaxTranslateX());
},
_onBodyTouchMove: function _onBodyTouchMove(e) {
var currentX = e.touches[0].pageX;
var currentY = e.touches[0].pageY;
if (this.state.swiping) {
e.preventDefault();
this._setPosition(this._getTranslateX(currentX));
} else if (this._maybeSwiping) {
var dXAbs = Math.abs(currentX - this._touchStartX);
var dYAbs = Math.abs(currentY - this._touchStartY);
// If the user has moved his thumb ten pixels in either direction,
// we can safely make an assumption about whether he was intending
// to swipe or scroll.
var threshold = 10;
if (dXAbs > threshold && dYAbs <= threshold) {
this._swipeStartX = currentX;
this.setState({
swiping: this.state.open ? 'closing' : 'opening'
});
this._setPosition(this._getTranslateX(currentX));
} else if (dXAbs <= threshold && dYAbs > threshold) {
this._onBodyTouchEnd();
}
}
},
_onBodyTouchEnd: function _onBodyTouchEnd(e) {
if (this.state.swiping) {
var currentX = e.changedTouches[0].pageX;
var translateRatio = this._getTranslateX(currentX) / this._getMaxTranslateX();
this._maybeSwiping = false;
var swiping = this.state.swiping;
this.setState({
swiping: null
});
// We have to open or close after setting swiping to null,
// because only then CSS transition is enabled.
if (translateRatio > 0.5) {
if (swiping === 'opening') {
this._setPosition(this._getMaxTranslateX());
} else {
this.close();
}
} else {
if (swiping === 'opening') {
this.open();
} else {
this._setPosition(0);
}
}
} else {
this._maybeSwiping = false;
}
document.body.removeEventListener('touchmove', this._onBodyTouchMove);
document.body.removeEventListener('touchend', this._onBodyTouchEnd);
document.body.removeEventListener('touchcancel', this._onBodyTouchEnd);
}
});
module.exports = LeftNav;