d2-ui
Version:
618 lines (522 loc) • 16.4 kB
JSX
import React from 'react';
import ReactDOM from 'react-dom';
import WindowListenable from './mixins/window-listenable';
import KeyCode from './utils/key-code';
import Transitions from './styles/transitions';
import StylePropable from './mixins/style-propable';
import FlatButton from './flat-button';
import Overlay from './overlay';
import RenderToLayer from './render-to-layer';
import Paper from './paper';
import getMuiTheme from './styles/getMuiTheme';
import warning from 'warning';
import deprecated from './utils/deprecatedPropType';
import ReactTransitionGroup from 'react-addons-transition-group';
const TransitionItem = React.createClass({
propTypes: {
children: React.PropTypes.node,
style: React.PropTypes.object,
},
contextTypes: {
muiTheme: React.PropTypes.object,
},
//for passing default theme context to children
childContextTypes: {
muiTheme: React.PropTypes.object,
},
mixins: [StylePropable],
getInitialState() {
return {
style: {},
muiTheme: this.context.muiTheme || getMuiTheme(),
};
},
getChildContext() {
return {
muiTheme: this.state.muiTheme,
};
},
//to update theme inside state whenever a new theme is passed down
//from the parent / owner using context
componentWillReceiveProps(nextProps, nextContext) {
const newMuiTheme = nextContext.muiTheme ? nextContext.muiTheme : this.state.muiTheme;
this.setState({muiTheme: newMuiTheme});
},
componentWillEnter(callback) {
this.componentWillAppear(callback);
},
componentWillAppear(callback) {
const spacing = this.state.muiTheme.rawTheme.spacing;
this.setState({
style: {
opacity: 1,
transform: 'translate3d(0, ' + spacing.desktopKeylineIncrement + 'px, 0)',
},
});
setTimeout(callback, 450); // matches transition duration
},
componentWillLeave(callback) {
this.setState({
style: {
opacity: 0,
transform: 'translate3d(0, 0, 0)',
},
});
setTimeout(() => {
if (this.isMounted()) callback();
}, 450); // matches transition duration
},
render() {
const {
style,
children,
...other,
} = this.props;
return (
<div {...other} style={this.prepareStyles(this.state.style, style)}>
{children}
</div>
);
},
});
const DialogInline = React.createClass({
propTypes: {
actionFocus: React.PropTypes.string,
actions: React.PropTypes.node,
actionsContainerClassName: React.PropTypes.string,
actionsContainerStyle: React.PropTypes.object,
autoDetectWindowHeight: React.PropTypes.bool,
autoScrollBodyContent: React.PropTypes.bool,
bodyClassName: React.PropTypes.string,
bodyStyle: React.PropTypes.object,
children: React.PropTypes.node,
className: React.PropTypes.string,
contentClassName: React.PropTypes.string,
contentStyle: React.PropTypes.object,
modal: React.PropTypes.bool,
onRequestClose: React.PropTypes.func,
open: React.PropTypes.bool.isRequired,
overlayClassName: React.PropTypes.string,
overlayStyle: React.PropTypes.object,
repositionOnUpdate: React.PropTypes.bool,
style: React.PropTypes.object,
title: React.PropTypes.node,
titleClassName: React.PropTypes.string,
titleStyle: React.PropTypes.object,
width: React.PropTypes.any,
},
contextTypes: {
muiTheme: React.PropTypes.object,
},
//for passing default theme context to children
childContextTypes: {
muiTheme: React.PropTypes.object,
},
mixins: [WindowListenable, StylePropable],
getInitialState() {
return {
muiTheme: this.context.muiTheme || getMuiTheme(),
};
},
getChildContext() {
return {
muiTheme: this.state.muiTheme,
};
},
componentDidMount() {
this._positionDialog();
},
componentWillReceiveProps(nextProps, nextContext) {
const newMuiTheme = nextContext.muiTheme ? nextContext.muiTheme : this.state.muiTheme;
this.setState({muiTheme: newMuiTheme});
},
componentDidUpdate() {
this._positionDialog();
},
windowListeners: {
keyup: '_handleWindowKeyUp',
resize: '_handleResize',
},
getStyles() {
const {
autoScrollBodyContent,
open,
width,
} = this.props;
const muiTheme = this.state.muiTheme;
const rawTheme = muiTheme.rawTheme;
const spacing = rawTheme.spacing;
const gutter = spacing.desktopGutter;
return {
root: {
position: 'fixed',
boxSizing: 'border-box',
WebkitTapHighlightColor: 'rgba(0,0,0,0)',
zIndex: muiTheme.zIndex.dialog,
top: 0,
left: open ? 0 : -10000,
width: '100%',
height: '100%',
transition: open
? Transitions.easeOut('0ms', 'left', '0ms')
: Transitions.easeOut('0ms', 'left', '450ms'),
},
content: {
boxSizing: 'border-box',
WebkitTapHighlightColor: 'rgba(0,0,0,0)',
transition: Transitions.easeOut(),
position: 'relative',
width: width || '75%',
maxWidth: spacing.desktopKeylineIncrement * 12,
margin: '0 auto',
zIndex: muiTheme.zIndex.dialog,
},
body: {
padding: spacing.desktopGutter,
overflowY: autoScrollBodyContent ? 'auto' : 'hidden',
overflowX: 'hidden',
},
actionsContainer: {
boxSizing: 'border-box',
WebkitTapHighlightColor: 'rgba(0,0,0,0)',
padding: 8,
marginBottom: 8,
width: '100%',
textAlign: 'right',
},
paper: {
background: rawTheme.palette.canvasColor,
},
overlay: {
zIndex: muiTheme.zIndex.dialogOverlay,
},
title: {
margin: 0,
padding: `${gutter}px ${gutter}px 0 ${gutter}px`,
color: rawTheme.palette.textColor,
fontSize: 24,
lineHeight: '32px',
fontWeight: 400,
},
};
},
_getAction(actionJSON) {
warning(false, `using actionsJSON is deprecated on Dialog, please provide an array of
buttons, or any other components instead. For more information please refer to documentations.`);
const props = {
secondary: true,
onClick: actionJSON.onClick,
onTouchTap: () => {
if (actionJSON.onTouchTap) {
actionJSON.onTouchTap.call(undefined);
}
if (!(actionJSON.onClick || actionJSON.onTouchTap)) {
this._requestClose(true);
}
},
label: actionJSON.text,
style: {
marginRight: 8,
},
};
if (actionJSON.ref) {
props.ref = actionJSON.ref;
props.keyboardFocused = actionJSON.ref === this.props.actionFocus;
}
if (actionJSON.id) {
props.id = actionJSON.id;
}
return (
<FlatButton {...props} />
);
},
_getActionObjects(actions) {
const actionObjects = [];
// ------- Replace this selction with:
//
// React.Children.forEach(actions, action => {
// if (React.isValidElement(action)) {
// actionObjects.push(action);
// }
// });
//
// Also the return element will not need a call to React.Children.toArray
//
// for the 0.15.0 release
if (actions) {
if (React.isValidElement(actions)) {
actionObjects.push(actions);
} else {
actions.forEach(action => {
if (action) {
if (!React.isValidElement(action)) {
action = this._getAction(action);
}
actionObjects.push(action);
}
});
}
}
// ------- End of section
return actionObjects;
},
_getActionsContainer(actions, styles, className) {
const actionObjects = this._getActionObjects(actions);
return actionObjects.length > 0 && (
<div className={className} style={this.prepareStyles(styles)}>
{React.Children.toArray(actionObjects)}
</div>
);
},
_positionDialog() {
const {
actions,
autoDetectWindowHeight,
autoScrollBodyContent,
bodyStyle,
open,
repositionOnUpdate,
title,
} = this.props;
if (!open) {
return;
}
const clientHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
const container = ReactDOM.findDOMNode(this);
const dialogWindow = ReactDOM.findDOMNode(this.refs.dialogWindow);
const dialogContent = ReactDOM.findDOMNode(this.refs.dialogContent);
const minPaddingTop = 16;
//Reset the height in case the window was resized.
dialogWindow.style.height = '';
dialogContent.style.height = '';
const dialogWindowHeight = dialogWindow.offsetHeight;
let paddingTop = ((clientHeight - dialogWindowHeight) / 2) - 64;
if (paddingTop < minPaddingTop) paddingTop = minPaddingTop;
//Vertically center the dialog window, but make sure it doesn't
//transition to that position.
if (repositionOnUpdate || !container.style.paddingTop) {
container.style.paddingTop = paddingTop + 'px';
}
// Force a height if the dialog is taller than clientHeight
if (autoDetectWindowHeight || autoScrollBodyContent) {
const styles = this.getStyles();
styles.body = this.mergeStyles(styles.body, bodyStyle);
let maxDialogContentHeight = clientHeight - 2 * (styles.body.padding + 64);
if (title) maxDialogContentHeight -= dialogContent.previousSibling.offsetHeight;
const hasActions = this._getActionObjects(actions).length > 0;
if (hasActions) maxDialogContentHeight -= dialogContent.nextSibling.offsetHeight;
dialogContent.style.maxHeight = maxDialogContentHeight + 'px';
}
},
_requestClose(buttonClicked) {
if (!buttonClicked && this.props.modal) {
return;
}
if (this.props.onRequestClose) {
this.props.onRequestClose(!!buttonClicked);
}
},
_handleOverlayTouchTap() {
this._requestClose(false);
},
_handleWindowKeyUp(event) {
if (event.keyCode === KeyCode.ESC) {
this._requestClose(false);
}
},
_handleResize() {
if (this.props.open) {
this._positionDialog();
}
},
render() {
const {
actions,
actionsContainerClassName,
actionsContainerStyle,
bodyClassName,
bodyStyle,
children,
className,
contentClassName,
contentStyle,
overlayClassName,
overlayStyle,
open,
titleClassName,
titleStyle,
title,
style,
} = this.props;
const styles = this.getStyles();
styles.root = this.mergeStyles(styles.root, style);
styles.content = this.mergeStyles(styles.content, contentStyle);
styles.body = this.mergeStyles(styles.body, bodyStyle);
styles.actionsContainer = this.mergeStyles(styles.actionsContainer, actionsContainerStyle);
styles.overlay = this.mergeStyles(styles.overlay, overlayStyle);
styles.title = this.mergeStyles(styles.title, titleStyle);
const actionsContainer = this._getActionsContainer(actions, styles.actionsContainer, actionsContainerClassName);
const titleElement = typeof title === 'string'
? <h3 className={titleClassName} style={this.prepareStyles(styles.title)}>{title}</h3>
: title;
return (
<div className={className} style={this.prepareStyles(styles.root)}>
<ReactTransitionGroup
component="div" ref="dialogWindow"
transitionAppear={true} transitionAppearTimeout={450}
transitionEnter={true} transitionEnterTimeout={450}
>
{open &&
<TransitionItem
className={contentClassName}
style={styles.content}
>
<Paper
style={styles.paper}
zDepth={4}
>
{titleElement}
<div
ref="dialogContent"
className={bodyClassName}
style={this.prepareStyles(styles.body)}
>
{children}
</div>
{actionsContainer}
</Paper>
</TransitionItem>
}
</ReactTransitionGroup>
<Overlay
show={open}
className={overlayClassName}
style={styles.overlay}
onTouchTap={this._handleOverlayTouchTap}
/>
</div>
);
},
});
const Dialog = React.createClass({
propTypes: {
/**
* The `ref` of the action to focus on when the `Dialog` is displayed.
*/
actionFocus: deprecated(React.PropTypes.string,
'Instead, use a custom `actions` property.'),
/**
* This prop can be either a JSON object containing the actions to render (This is **DEPRECATED**),
* a react elements, or an array of react elements.
*/
actions: React.PropTypes.node,
/**
* The `className` to add to the actions container's root element.
*/
actionsContainerClassName: React.PropTypes.string,
/**
* Overrides the inline-styles of the actions container's root element.
*/
actionsContainerStyle: React.PropTypes.object,
/**
* If set to true, the height of the `Dialog` will be auto detected. A max height
* will be enforced so that the content does not extend beyond the viewport.
*/
autoDetectWindowHeight: React.PropTypes.bool,
/**
* If set to true, the body content of the `Dialog` will be scrollable.
*/
autoScrollBodyContent: React.PropTypes.bool,
/**
* The `className` to add to the content's root element under the title.
*/
bodyClassName: React.PropTypes.string,
/**
* Overrides the inline-styles of the content's root element under the title.
*/
bodyStyle: React.PropTypes.object,
/**
* The contents of the `Dialog`.
*/
children: React.PropTypes.node,
/**
* The css class name of the root element.
*/
className: React.PropTypes.string,
/**
* The `className` to add to the content container.
*/
contentClassName: React.PropTypes.string,
/**
* Overrides the inline-styles of the content container.
*/
contentStyle: React.PropTypes.object,
/**
* Force the user to use one of the actions in the `Dialog`.
* Clicking outside the `Dialog` will not trigger the `onRequestClose`.
*/
modal: React.PropTypes.bool,
/**
* Fired when the `Dialog` is requested to be closed by a click outside the `Dialog` or on the buttons.
*
* @param {bool} buttonClicked Determines whether a button click triggered this request.
*/
onRequestClose: React.PropTypes.func,
/**
* Controls whether the Dialog is opened or not.
*/
open: React.PropTypes.bool.isRequired,
/**
* The `className` to add to the `Overlay` component that is rendered behind the `Dialog`.
*/
overlayClassName: React.PropTypes.string,
/**
* Overrides the inline-styles of the `Overlay` component that is rendered behind the `Dialog`.
*/
overlayStyle: React.PropTypes.object,
/**
* Determines whether the `Dialog` should be repositioned when it's contents are updated.
*/
repositionOnUpdate: React.PropTypes.bool,
/**
* Override the inline-styles of the root element.
*/
style: React.PropTypes.object,
/**
* The title to display on the `Dialog`. Could be number, string, element or an array containing these types.
*/
title: React.PropTypes.node,
/**
* The `className` to add to the title's root container element.
*/
titleClassName: React.PropTypes.string,
/**
* Overrides the inline-styles of the title's root container element.
*/
titleStyle: React.PropTypes.object,
/**
* Changes the width of the `Dialog`.
*/
width: deprecated(React.PropTypes.any,
'Use the contentStyle.'),
},
getDefaultProps() {
return {
autoDetectWindowHeight: true,
autoScrollBodyContent: false,
modal: false,
repositionOnUpdate: true,
};
},
renderLayer() {
return (
<DialogInline {...this.props} />
);
},
render() {
return (
<RenderToLayer render={this.renderLayer} open={true} useLayerForClickAway={false} />
);
},
});
export default Dialog;