material-ui
Version:
Material Design UI components built with React
382 lines (328 loc) • 10.8 kB
JSX
const isBrowser = require('./utils/is-browser');
let Modernizr = isBrowser ? require('./utils/modernizr.custom') : undefined;
const React = require('react');
const ReactDOM = require('react-dom');
const KeyCode = require('./utils/key-code');
const StylePropable = require('./mixins/style-propable');
const AutoPrefix = require('./styles/auto-prefix');
const Transitions = require('./styles/transitions');
const WindowListenable = require('./mixins/window-listenable');
const Overlay = require('./overlay');
const Paper = require('./paper');
const Menu = require('./menu/menu');
const DefaultRawTheme = require('./styles/raw-themes/light-raw-theme');
const ThemeManager = require('./styles/theme-manager');
let openNavEventHandler = null;
const LeftNav = React.createClass({
mixins: [StylePropable, WindowListenable],
contextTypes: {
muiTheme: React.PropTypes.object,
},
//for passing default theme context to children
childContextTypes: {
muiTheme: React.PropTypes.object,
},
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() {
return {
disableSwipeToOpen: false,
docked: true,
};
},
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 (nextProps, nextContext) {
let newMuiTheme = nextContext.muiTheme ? nextContext.muiTheme : this.state.muiTheme;
this.setState({muiTheme: newMuiTheme});
},
componentDidMount() {
this._updateMenuHeight();
this._enableSwipeHandling();
},
componentDidUpdate() {
this._updateMenuHeight();
this._enableSwipeHandling();
},
componentWillUnmount() {
this._disableSwipeHandling();
},
toggle() {
this.setState({ open: !this.state.open });
return this;
},
close() {
this.setState({ open: false });
if (this.props.onNavClose) this.props.onNavClose();
return this;
},
open() {
this.setState({ open: true });
if (this.props.onNavOpen) this.props.onNavOpen();
return this;
},
getThemePalette() {
return this.state.muiTheme.rawTheme.palette;
},
getTheme() {
return this.state.muiTheme.leftNav;
},
getStyles() {
let x = this._getTranslateMultiplier() * (this.state.open ? 0 : this._getMaxTranslateX());
let 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() {
let selectedIndex = this.props.selectedIndex;
let overlay;
let styles = this.getStyles();
if (!this.props.docked) {
overlay = (
<Overlay
ref="overlay"
show={this.state.open || !!this.state.swiping}
transitionEnabled={!this.state.swiping}
onTouchTap={this._onOverlayTouchTap} />
);
}
return (
<div className={this.props.className}>
{overlay}
<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}
<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} />
</Paper>
</div>
);
},
_updateMenuHeight() {
if (this.props.header) {
let container = ReactDOM.findDOMNode(this.refs.clickAwayableElement);
let menu = ReactDOM.findDOMNode(this.refs.menuItems);
let menuHeight = container.clientHeight - menu.offsetTop;
menu.style.height = menuHeight + 'px';
}
},
_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() {
this.close();
},
_onWindowKeyUp(e) {
if (e.keyCode === KeyCode.ESC &&
!this.props.docked &&
this.state.open) {
this.close();
}
},
_onWindowResize() {
this._updateMenuHeight();
},
_getMaxTranslateX() {
return this.getTheme().width + 10;
},
_getTranslateMultiplier() {
return this.props.openRight ? 1 : -1;
},
_enableSwipeHandling() {
if (!this.props.docked) {
document.body.addEventListener('touchstart', this._onBodyTouchStart);
if (!openNavEventHandler) {
openNavEventHandler = this._onBodyTouchStart;
}
} else {
this._disableSwipeHandling();
}
},
_disableSwipeHandling() {
document.body.removeEventListener('touchstart', this._onBodyTouchStart);
if (openNavEventHandler === this._onBodyTouchStart) {
openNavEventHandler = null;
}
},
_onBodyTouchStart(e) {
if (!this.state.open &&
(openNavEventHandler !== this._onBodyTouchStart ||
this.props.disableSwipeToOpen)
) {
return;
}
let touchStartX = e.touches[0].pageX;
let 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(translateX) {
let 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(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(e) {
let currentX = e.touches[0].pageX;
let currentY = e.touches[0].pageY;
if (this.state.swiping) {
e.preventDefault();
this._setPosition(this._getTranslateX(currentX));
}
else if (this._maybeSwiping) {
let dXAbs = Math.abs(currentX - this._touchStartX);
let 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.
let 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(e) {
if (this.state.swiping) {
let currentX = e.changedTouches[0].pageX;
let translateRatio = this._getTranslateX(currentX) / this._getMaxTranslateX();
this._maybeSwiping = false;
let 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;