react-cm-ui
Version:
React UI for Healthy Church
400 lines (357 loc) • 11.7 kB
JSX
import {
debounce,
find,
forEach,
isArray,
isFunction,
map,
} from 'lodash';
import ClassNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import ReactDOM from 'react-dom';
import CardDeprecatedHeader from './cardDeprecatedHeader';
import domUtils from '../../utils/domUtils';
import Icon from '../../dataDisplay/icon';
import withStyles from '../../styles/withStyles';
const propTypes = {
active: PropTypes.bool,
attached: PropTypes.bool,
children: PropTypes.node,
classes: PropTypes.shape({
root: PropTypes.string,
}),
className: PropTypes.string,
collapsable: PropTypes.bool,
compact: PropTypes.bool,
header: PropTypes.bool,
id: PropTypes.string,
nest: PropTypes.bool,
onClick: PropTypes.func,
style: PropTypes.shape({}),
title: PropTypes.string,
};
const defaultProps = {
active: false,
attached: false,
children: null,
classes: null,
className: null,
collapsable: false,
compact: false,
header: false,
id: null,
nest: false,
onClick: null,
style: null,
title: null,
};
const styles = (theme) => ({
root: {
backgroundColor: theme.palette.background.primary,
borderRadius: theme.shape.borderRadius,
boxShadow: `inset 0 0 0 1px ${theme.palette.border.secondary}`,
margin: 'calc(2rem - .14285em) 0 1rem',
padding: 22,
position: 'relative',
transition: 'background-color 150ms ease-out',
'&.card-active': {
backgroundColor: theme.palette.grey[800],
color: theme.palette.text.contrastText,
cursor: 'default',
},
'&.card-attached': {
borderRadius: 0,
'& .card-header-attached': {
borderRadius: 0,
},
},
'&.card-clickable': {
cursor: 'pointer',
'&:hover': {
backgroundColor: theme.palette.background.secondary,
},
'&:active': {
backgroundColor: theme.palette.background.primary,
},
},
'& .card-header': {
marginBottom: 16,
'&.card-header-attached': {
alignItems: 'center',
borderRadius: '3px 3px 0 0',
display: 'flex',
height: 44,
margin: '-22px -22px 16px',
padding: '0 22px',
},
'&.card-header-color': {
'&-blue, &-green, &-pink': {
color: theme.palette.text.contrastText,
},
'&-blue': {
background: 'linear-gradient(to left, #54cc86 -66%, #1c93c4 100%)',
},
'&-green': {
background: 'linear-gradient(to left, #8bbd72 -28%, #329594 94%)',
},
'&-pink': {
background: 'linear-gradient(to left, #f99e49 -11%, #c33580 62%)',
},
},
},
'&.card-attached .card-header-attached': {
borderRadius: 0,
},
'&.card-compact .card-header-attached': {
margin: '-11px -11px 16px',
padding: '0 11px',
},
'& .card-collapse-button': {
position: 'absolute',
right: 22,
top: 26,
zIndex: 1,
},
'& .card-content': {
transition: 'height 166ms linear, opacity 166ms linear',
'& > div >': {
'& *:first-child': {
marginTop: 0,
},
'& *:last-child': {
marginBottom: 0,
},
},
},
'&.card-collapsable.card-is-collapsing .card-content': {
overflow: 'hidden',
},
'&.card-is-collapsed': {
paddingBottom: 6,
'& .card-content': {
opacity: 0,
},
},
'&.card-compact': {
padding: 11,
'&.card-is-collapsed': {
paddingBottom: 0,
},
'& .card-collapse-button': {
right: 11,
top: 15,
},
},
'&.card-nest': {
border: 0,
boxShadow: '-1px 1px 2px 0 rgba(0, 0, 0, .1), 1px 1px 2px 0 rgba(0, 0, 0, .1)',
},
'& .card-content >': {
'& :first-child': {
marginTop: 0,
},
'& :last-child': {
marginBottom: 0,
},
},
'&:first-child': {
marginTop: 0,
},
'&:last-child': {
marginBottom: 0,
},
},
});
class CardDeprecated extends React.Component {
constructor(props) {
super(props);
this.state = {
contentHeight: null,
isCollapsed: false,
isCollapsing: false,
};
this.getContentHeight = this.getContentHeight.bind(this);
this.onClick = this.onClick.bind(this);
this.onCollapseClick = this.onCollapseClick.bind(this);
this.renderCollapsableButton = this.renderCollapsableButton.bind(this);
this.removeIsCollapsing = this.removeIsCollapsing.bind(this);
this.setContentHeight = this.setContentHeight.bind(this);
this.setContentHeightDebounce = debounce(() => this.setContentHeight(), 50);
if (props.collapsable) {
this.prevContentHeight = 0;
this.observer = new MutationObserver((mutations) => {
forEach(mutations, (m) => {
this.setContentHeight();
});
});
}
}
componentDidMount() {
const { collapsable } = this.props;
if (collapsable) {
const outerContent = ReactDOM.findDOMNode(this.outerContent);
const innerContent = ReactDOM.findDOMNode(this.innerContent);
window.addEventListener('resize', this.setContentHeightDebounce);
outerContent.addEventListener(domUtils.cssTransitionType(outerContent), this.removeIsCollapsing);
this.observer.observe(innerContent, {
childList: true,
characterData: true,
subtree: true,
});
this.setContentHeight();
}
}
componentWillUnmount() {
const { collapsable } = this.props;
if (collapsable) {
const outerContent = ReactDOM.findDOMNode(this.outerContent);
window.removeEventListener('resize', this.setContentHeightDebounce);
outerContent.removeEventListener(domUtils.cssTransitionType(outerContent), this.removeIsCollapsing);
this.observer.disconnect();
}
}
onClick() {
const { onClick } = this.props;
const isTextHighlighted = window.getSelection().toString();
if (!isTextHighlighted && isFunction(onClick)) {
onClick();
}
}
onCollapseClick() {
const { isCollapsed } = this.state;
this.setState({
contentHeight: !isCollapsed ? 0 : this.getContentHeight(),
isCollapsed: !isCollapsed,
isCollapsing: true,
});
}
getContentHeight() {
const innerHeight = ReactDOM.findDOMNode(this.innerContent).offsetHeight;
if (innerHeight <= 0) {
return this.prevContentHeight;
}
this.prevContentHeight = innerHeight;
return innerHeight;
}
setContentHeight() {
const { isCollapsed } = this.state;
if (!isCollapsed) {
this.setState({ contentHeight: this.getContentHeight() });
}
}
removeIsCollapsing() {
this.setState({ isCollapsing: false });
}
renderCollapsableButton() {
const { collapsable } = this.props;
const { isCollapsed } = this.state;
const rootClasses = ClassNames('card-collapse-button');
if (!collapsable) {
return null;
}
return (
<div className={rootClasses}>
<Icon
compact
onClick={this.onCollapseClick}
rotate={!isCollapsed ? 135 : 0}
type="plus"
/>
</div>
);
}
render() {
const {
active,
attached,
children,
classes,
className,
collapsable,
compact,
header,
id,
onClick,
nest,
style,
title,
} = this.props;
const { contentHeight, isCollapsed, isCollapsing } = this.state;
const rootClasses = ClassNames(
'ui',
'card',
classes.root,
className,
{
'card-active': active,
'card-attached': attached,
'card-clickable': onClick,
'card-collapsable': collapsable,
'card-is-collapsed': isCollapsed,
'card-is-collapsing': isCollapsing,
'card-compact': compact,
'card-nest': nest,
},
);
const convertChildren = isArray(children) ? children : [children];
const customHeaderObj = find(convertChildren, (child) => child && isFunction(child.type) && child.type.name === 'CardDeprecatedHeader');
let renderHeader;
if (header && customHeaderObj) {
renderHeader = (
<CardDeprecatedHeader
// eslint-disable-next-line react/jsx-props-no-spreading
{...customHeaderObj.props}
title={title}
/>
);
}
if (title && !header) {
renderHeader = (
<CardDeprecatedHeader title={title} />
);
}
// eslint-disable-next-line consistent-return
const renderContent = map(convertChildren, (child, index) => {
const isCardDeprecatedHeader = child && isFunction(child.type) && child.type.name === 'CardDeprecatedHeader';
if (!isCardDeprecatedHeader) {
return (
<div key={index}>
{child}
</div>
);
}
});
return (
// eslint-disable-next-line max-len
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
<div
className={rootClasses}
id={id}
onClick={this.onClick}
style={style}
>
{this.renderCollapsableButton()}
{renderHeader}
<div
className="card-content"
ref={(outerContent) => {
this.outerContent = outerContent;
}}
style={{ height: contentHeight }}
>
<div ref={(innerContent) => {
this.innerContent = innerContent;
}}
>
{renderContent}
</div>
</div>
</div>
);
}
}
CardDeprecated.Header = CardDeprecatedHeader;
CardDeprecated.propTypes = propTypes;
CardDeprecated.defaultProps = defaultProps;
export default withStyles(styles)(CardDeprecated);