react-cm-ui
Version:
React UI for Healthy Church
736 lines (620 loc) • 24 kB
JSX
import React from 'react';
import _ from 'lodash';
import { Portal } from 'react-portal';
import ClassNames from 'classnames';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import ScrollBar from 'react-custom-scrollbars';
import Button from '../../inputs/button';
import Divider from '../../dataDisplay/divider';
import domUtils from '../../utils/domUtils';
import Header from '../../dataDisplay/header';
import Icon from '../../dataDisplay/icon';
import withStyles from '../../styles/withStyles';
const DEFAULT_CONTAINER_PADDING_TOP = 27;
const colorEnums = [ 'dark-blue', 'grey', 'white' ];
class CloseButton extends React.Component {
constructor() {
super();
this._onCloseClick = this._onCloseClick.bind(this);
}
render() {
const { closeButton, inverse, onClose } = this.props;
if (!onClose) {
return false;
}
let closeButtonJSX;
if (_.isObject(closeButton)) {
closeButtonJSX = React.cloneElement(closeButton, {
className: 'drawer-close-button',
});
} else {
closeButtonJSX = (
<Button
className="drawer-close-button"
color={inverse ? 'transparent' : 'alternate'}
onClick={this._onCloseClick}
icon
style={{ margin: 0 }}
>
<Icon inverse type={_.isString(closeButton) ? closeButton : 'times'} />
</Button>
);
}
return (
<div className="drawer-close-button-container">
{closeButtonJSX}
</div>
);
}
_onCloseClick() {
this.props.onClose();
}
}
CloseButton.propTypes = {
closeButton: PropTypes.oneOfType([
PropTypes.shape({}),
PropTypes.string,
]),
inverse: PropTypes.bool,
onClose: PropTypes.func,
};
class DrawerDeprecatedWing extends React.Component {
render() {
const { children, className, color, width, style } = this.props;
const rootClasses = ClassNames('drawer-wing', className, {
'color-dark-blue': color === 'dark-blue',
'color-grey': color === 'grey',
});
const defaultStyle = {
width: width || null,
};
return (
<div
className={rootClasses}
style={Object.assign({}, defaultStyle, style)}
>
{children}
</div>
);
}
componentDidMount() {
const { onOpenToggle, width } = this.props;
const newWidth = width ? `${width}px` : '100%';
onOpenToggle(newWidth);
}
}
DrawerDeprecatedWing.propTypes = {
className: PropTypes.string,
color: PropTypes.oneOf(colorEnums),
onOpenToggle: PropTypes.func,
style: PropTypes.shape({}),
width: PropTypes.number,
};
class DrawerDeprecatedHeader extends React.Component {
constructor() {
super();
this._drawerContainerInnerPaddingTop = DEFAULT_CONTAINER_PADDING_TOP;
}
render() {
const { children, closeButton, inverse, onClose, title, titleTruncate } = this.props;
const titleClass = ClassNames('title', {
'drawer-title-truncate': titleTruncate,
});
return (
<header className="drawer-header" ref={ref => this._drawerHeaderRef = ref}>
{_.isObject(title) ? (
title
) : title ? (
<Header as="h2" className={titleClass} title={title}>
{title}
</Header>
) : null}
<CloseButton closeButton={closeButton} inverse={inverse} onClose={onClose} />
{children ? (
<div className="drawer-header-children">{children}</div>
) : (
<Divider />
)}
</header>
);
}
componentDidMount() {
this._onHeaderUpdate();
}
_onHeaderUpdate(node) {
const headerEl = ReactDOM.findDOMNode(this);
if (headerEl) {
const paddingTop = headerEl.offsetHeight;
const drawerInnerContainer = headerEl.closest('.drawer-container-inner');
if (drawerInnerContainer && paddingTop !== this._drawerContainerInnerPaddingTop) {
this._drawerContainerInnerPaddingTop = `${paddingTop + DEFAULT_CONTAINER_PADDING_TOP}px`;
drawerInnerContainer.style.paddingTop = this._drawerContainerInnerPaddingTop;
}
}
}
}
DrawerDeprecatedHeader.propTypes = {
closeButton: PropTypes.oneOfType([
PropTypes.shape({}),
PropTypes.string,
]),
inverse: PropTypes.bool,
onClose: PropTypes.func,
title: PropTypes.oneOfType([
PropTypes.shape({}),
PropTypes.string,
]),
titleTruncate: PropTypes.bool,
};
const styles = (theme) => ({
root: {
backfaceVisibility: 'hidden',
height: '100%',
left: 0,
minWidth: 320,
position: 'fixed',
top: 0,
width: '100%',
zIndex: theme.zIndex.drawer,
},
});
class DrawerDeprecated extends React.Component {
constructor(props) {
super(props);
this.state = {
isOpen: props.isOpen,
isScrolled: false,
transformValue: props.position === 'right' ?
'translate(100%, 0)' :
'translate(-100%, 0)',
wing: null,
};
this._onClickOutside = this._onClickOutside.bind(this);
this._onCloseAnimationComplete = this._onCloseAnimationComplete.bind(this);
this._onCloseWingAnimationComplete = this._onCloseWingAnimationComplete.bind(this);
this.onOpen = this.onOpen.bind(this);
this.onOpenAnimationComplete = this.onOpenAnimationComplete.bind(this);
this.onOpenWingToggle = this.onOpenWingToggle.bind(this);
this._onClose = this._onClose.bind(this);
this._onScrollStart = this._onScrollStart.bind(this);
this._onScrollStop = this._onScrollStop.bind(this);
this._drawerContainer = null;
this._useComponentWillUnmount = false;
}
componentDidUpdate(prevProps) {
// Open wing if there wasn't a previous wing open.
if (!!this.props.wing && !!!prevProps.wing) {
this.setState({ wing: this.props.wing });
}
// Open new wing and close old wing.
if (!!this.props.wing && !!prevProps.wing && this.props.wing !== prevProps.wing) {
this.setState({ wing: this.props.wing });
}
// Close wing if there is no other wing to open.
if (!!!this.props.wing && !!prevProps.wing) {
this._onCloseWingToggle(false);
}
if (this.props.isOpen && prevProps.maxWidth !== this.props.maxWidth) {
const { maxWidth } = this.props;
if (_.isNumber(maxWidth)) {
this._drawerContainer.style.maxWidth = `${maxWidth}px`;
} else if (_.isString(maxWidth)) {
this._drawerContainer.style.maxWidth = maxWidth;
} else {
this._drawerContainer.style.maxWidth = '768px';
}
}
if (!prevProps.isOpen && this.props.isOpen) {
this.setState({
isOpen: this.props.isOpen,
}, () => {
this.onOpen();
});
}
if (prevProps.isOpen && !this.props.isOpen) {
this._onBeforeClose();
}
}
componentDidMount() {
const { isOpen } = this.props;
if (isOpen) {
this.setState({
isOpen,
}, () => {
this.onOpen();
});
}
}
componentWillUnmount() {
const { isOpen } = this.state;
if (isOpen && this._useComponentWillUnmount) { // We only want to clean up classes here if the closing animation never happens and the drawer isOpen.
if (document.body.classList.contains('drawer-deprecated-open')) {
document.body.classList.remove('drawer-deprecated-open');
}
if (document.body.classList.contains('drawer-dimmers')) {
document.body.classList.remove('drawer-dimmers');
}
if (document.body.classList.contains('drawer-deprecated-open-layered')) {
document.body.classList.remove('drawer-deprecated-open-layered');
}
if (document.body.classList.contains('drawer-animate-out')) {
document.body.classList.remove('drawer-animate-out');
}
}
}
_onBeforeClose() {
const animationEvent = this._transitionProps(this._drawerContainer);
document.body.classList.add('drawer-animate-out');
if (this._drawerContainer) {
this._drawerContainer.classList.add('drawer-animate-out');
this._drawerContainer.style.transform = this.props.position === 'right' ?
'translate(100%, 0)' :
'translate(-100%, 0)';
this._drawerContainer.addEventListener(animationEvent, this._onCloseAnimationComplete);
}
}
_onClickOutside(event) {
if (this.drawerContainerRef.contains(event.target) || !this.props.onClickOutside) {
return;
}
this._onClose();
}
_onClose() {
const { onClickOutside, onClose } = this.props;
if (onClickOutside) {
document.removeEventListener('click', this._onClickOutside);
}
onClose(...arguments);
}
_onCloseAnimationComplete() {
const { onCloseComplete, position, onClickOutside } = this.props;
const animationEvent = this._transitionProps(this.drawerContainerRef);
const body = document.body;
const numberOfModalDrawers = document.querySelectorAll('.ui.drawer-is_modal').length;
const numberOfModalDeprecatedDrawers = document.querySelectorAll('.ui.drawer.deprecated.drawer-is_modal').length;
if (onClickOutside) {
document.removeEventListener('click', this._onClickOutside);
}
this.drawerContainerRef.removeEventListener(animationEvent, this._onCloseAnimationComplete);
if (numberOfModalDrawers <= 2) {
body.classList.remove('drawer-deprecated-open-layered');
}
if (numberOfModalDrawers <= 1 || numberOfModalDeprecatedDrawers <= 1) {
const scrollPosition = parseInt(body.style.top, 10);
body.classList.remove('drawer-deprecated-open', 'drawer-dimmers');
window.scroll(0, Math.abs(scrollPosition));
body.style.top = null;
}
body.classList.remove('drawer-animate-out');
this.setState({ transformValue: position === 'right' ?
'translate(100%, 0)' :
'translate(-100%, 0)' });
if (_.isFunction(onCloseComplete)) {
onCloseComplete(true);
}
this._useComponentWillUnmount = false;
this.setState({
isOpen: false,
});
}
onOpen() {
const {
isModal,
maxWidth,
onClickOutside,
position,
theme,
} = this.props;
this._useComponentWillUnmount = true;
const body = document.body;
const nodePortal = ReactDOM.findDOMNode(this);
const scrollPosition = window.pageYOffset;
const numberOfModalDrawers = document.querySelectorAll('.ui.drawer-is_modal').length;
this._drawerContainer = nodePortal.querySelector('.drawer-container');
const drawerDimmer = nodePortal.querySelector('.drawer-dimmer-deprecated');
const layeredOffset = 11;
const animationEvent = this._transitionProps(this._drawerContainer);
let newZIndex = theme.zIndex.drawer + 2; // adding 2 accounts for the frist .drawer and .drawer-dimmers- z-indexes
this._drawerContainer.addEventListener(animationEvent, this.onOpenAnimationComplete);
if (onClickOutside) {
document.addEventListener('click', this._onClickOutside);
}
setTimeout(() => {
if (!isModal || numberOfModalDrawers >= 2) {
drawerDimmer.style.display = 'none';
}
if (numberOfModalDrawers >= 2) {
newZIndex = newZIndex + numberOfModalDrawers;
if (isModal) {
domUtils.addClassName(body, 'drawer-deprecated-open-layered');
}
nodePortal.style.zIndex = newZIndex;
this._drawerContainer.style.boxShadow = `${position === 'right' ? '-' : ''}2px 0 7px 0 rgba(0, 0, 0, 0.17)`;
this._drawerContainer.style.zIndex = newZIndex;
} else {
body.style.top = `-${scrollPosition}px`;
if (isModal) {
domUtils.addClassName(body, 'drawer-deprecated-open');
}
this._drawerContainer.style.boxShadow = `${position === 'right' ? '-' : ''}12px 0 19px 0 rgba(0, 0, 0, .22)`;
nodePortal.style.zIndex = newZIndex - 1;
this._drawerContainer.style.zIndex = newZIndex + numberOfModalDrawers;
}
if (!_.isUndefined(maxWidth)) {
this._drawerContainer.style.maxWidth = _.isNumber(maxWidth) ?
`${maxWidth}px` :
_.isString(maxWidth) ?
maxWidth :
'768px';
} else {
this._drawerContainer.style.maxWidth = 768 - (layeredOffset * (numberOfModalDrawers - 1)) + 'px';
}
this._drawerContainer.style.transform = 'translate(0, 0)';
}, 30);
}
onOpenAnimationComplete() {
const {
isModal,
} = this.props;
const animationEvent = this._transitionProps(this.drawerContainerRef);
this.drawerContainerRef.removeEventListener(animationEvent, this.onOpenAnimationComplete);
const { onOpenComplete } = this.props;
if (typeof onOpenComplete === 'function') {
onOpenComplete();
}
if (isModal) {
document.body.classList.add('drawer-dimmers');
}
}
onOpenWingToggle(width) {
const { position } = this.props;
this.setState({ transformValue: position === 'right' ? `translate(-${width}, 0)` : `translate(${width}, 0)` });
}
_onCloseWingToggle() {
const transitionEvent = this._transitionProps(this._drawerContainer);
this._drawerContainer.addEventListener(transitionEvent, this._onCloseWingAnimationComplete);
this.setState({ transformValue: 'translate(0, 0)' });
}
_onCloseWingAnimationComplete() {
const transitionEvent = this._transitionProps(this._drawerContainer);
this._drawerContainer.removeEventListener(transitionEvent, this._onCloseWingAnimationComplete);
this.setState({ wing: null });
}
_onScrollStart() {
const scrollContainerPos = this._drawerContainerInnerRef.parentNode.scrollTop;
this.setState({ isScrolled: scrollContainerPos > 0 ? true : false });
}
_onScrollStop() {
const scrollContainerPos = this._drawerContainerInnerRef.parentNode.scrollTop;
this.setState({ isScrolled: scrollContainerPos > 0 ? true : false });
}
_transitionProps(el) {
let t;
let transitions = {
'transition': 'transitionend',
'oTransition': 'oTransitionEnd',
'MozTransition': 'transitionend',
'WebkitTransition': 'webkitTransitionEnd',
};
for (t in transitions) {
if (el?.style && el.style[t] !== undefined) {
return transitions[t];
}
}
}
render() {
const {
children,
classes,
className,
closeButton,
color,
header,
inverse,
isModal,
scrollBar,
title,
titleTruncate,
position,
} = this.props;
const { isOpen } = this.state;
if (!isOpen) {
return false;
}
const { transformValue, wing } = this.state;
const rootClasses = ClassNames(
'ui',
'drawer',
'deprecated',
classes.root,
className,
{
'drawer-is_modal': isModal,
},
);
const containerInnerClasses = ClassNames('drawer-container', {
'color-dark-blue': color === 'dark-blue',
'drawer-container-inverse': inverse,
'drawer-container-is-scrolled': this.state.isScrolled,
'drawer-container-no-header': header === false,
'left-position': position === 'left',
});
const wingClasses = ClassNames('drawer-wing-container', {
'left-position': position === 'left',
});
let renderContent;
if (_.isArray(children)) {
console.error('Please wrap the Drawer\'s children in an enclosing tag');
return false;
}
renderContent = React.Children.map(children, child => {
if (_.isFunction(child.type)) {
return React.cloneElement(child, {
closeButton: closeButton,
inverse: inverse,
onClose: this._onClose,
title: title,
titleTruncate: titleTruncate,
});
} else {
return React.Children.map(child.props.children, secondaryChild => {
if (_.isFunction(secondaryChild.type) && secondaryChild.type.name === 'DrawerDeprecatedHeader') {
return (
<DrawerDeprecatedHeader
children={secondaryChild.props.children}
closeButton={closeButton}
inverse={inverse}
onClose={this._onClose}
title={title}
titleTruncate={titleTruncate}
/>
);
} else {
return secondaryChild;
}
});
}
});
if (_.isUndefined(header)) {
renderContent.unshift(
<DrawerDeprecatedHeader
closeButton={closeButton}
inverse={inverse}
key={`drawer-header-${_.kebabCase(title)}`}
onClose={this._onClose}
title={title}
titleTruncate={titleTruncate}
/>,
);
}
renderContent = [
<div className="drawer-children" key={`drawer-children-${_.kebabCase(title)}`}>
{renderContent}
</div>,
];
if (header === false) {
renderContent.unshift(
<CloseButton
closeButton={closeButton}
inverse={inverse}
key="drawer-close-button"
onClose={this._onClose}
/>,
);
}
return (
<Portal>
<div className={rootClasses}>
<div
className={containerInnerClasses}
ref={el => this.drawerContainerRef = el}
style={{ transform: transformValue }}
>
{position === 'left' && wing ? (
<div className={wingClasses}>
<div>
{React.cloneElement(wing, { onOpenToggle: this.onOpenWingToggle })}
</div>
</div>
) : null}
{scrollBar === true || _.isUndefined(scrollBar) ? (
<ScrollBar
autoHide
onScrollStart={this._onScrollStart}
onScrollStop={this._onScrollStop}
>
<div
className="drawer-container-inner"
ref={el => this._drawerContainerInnerRef = el}
style={{
paddingBottom: '33px',
paddingLeft: '22px',
paddingRight: '22px',
paddingTop: header === false ? '22px' : null,
}}
>
{renderContent}
</div>
</ScrollBar>
) : (
<div
className="drawer-container-inner"
ref={el => this._drawerContainerInnerRef = el}
style={{
paddingBottom: '33px',
paddingLeft: '22px',
paddingRight: '22px',
paddingTop: header === false ? '22px' : null,
}}
>
{renderContent}
</div>
)}
{position === 'right' && wing ? (
<div className={wingClasses}>
<div>
{React.cloneElement(wing, { onOpenToggle: this.onOpenWingToggle })}
</div>
</div>
) : null}
</div>
<div className="drawer-dimmer-deprecated" />
</div>
</Portal>
);
}
}
DrawerDeprecated.Header = DrawerDeprecatedHeader;
DrawerDeprecated.Wing = DrawerDeprecatedWing;
DrawerDeprecated.propTypes = {
/**
* Override or extend the styles applied to DrawerDeprecated.
*/
classes: PropTypes.shape({
root: PropTypes.string,
}),
className: PropTypes.string,
closeButton: PropTypes.oneOfType([
PropTypes.shape({}),
PropTypes.string,
]),
color: PropTypes.oneOf(colorEnums),
header: PropTypes.bool,
inverse: PropTypes.bool,
isOpen: PropTypes.bool.isRequired,
isModal: PropTypes.bool,
maxWidth: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
]),
onClickOutside: PropTypes.bool,
onClose: PropTypes.func,
onCloseComplete: PropTypes.func,
onOpenComplete: PropTypes.func,
path: PropTypes.string,
position: PropTypes.oneOf([ 'left', 'right' ]),
scrollBar: PropTypes.bool,
style: PropTypes.shape({}),
theme: PropTypes.shape({
zIndex: PropTypes.shape({
drawer: PropTypes.number,
}),
}),
title: PropTypes.oneOfType([
PropTypes.shape({}),
PropTypes.string,
]),
titleTruncate: PropTypes.bool,
wing: PropTypes.shape({}),
};
DrawerDeprecated.defaultProps = {
isModal: true,
isOpen: false,
position: 'right',
theme: null,
};
DrawerDeprecated.contextTypes = {
router: PropTypes.shape({}),
};
export default withStyles(styles, { withTheme: true })(DrawerDeprecated);