UNPKG

react-cm-ui

Version:
400 lines (357 loc) 11.7 kB
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);