import * as React from 'react' import * as q from '../../../../backend/src/Model' import { Theme, withStyles } from '@material-ui/core/styles' import LabelImportant from '@material-ui/icons/LabelImportant' import TreeNodeSubnodes from './TreeNodeSubnodes' import TreeNodeTitle from './TreeNodeTitle' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import { isElementInViewport } from '../helper/isElementInViewport' import { treeActions } from '../../actions' import { AppState } from '../../reducers' declare var performance: any const styles = (theme: Theme) => { return { collapsedSubnodes: { color: theme.palette.text.secondary, }, displayBlock: { display: 'block', }, node: { display: 'block', marginLeft: '10px', }, // hover: { // '&:hover': { // backgroundColor: 'rgba(80, 80, 80, 0.35)', // }, // }, topicSelect: { float: 'right' as 'right', opacity: 0, cursor: 'pointer', marginTop: '-1px', }, } } interface Props { actions: typeof treeActions lastUpdate: number animateChages: boolean isRoot?: boolean treeNode: q.TreeNode name?: string | undefined collapsed?: boolean | undefined performanceCallback?: ((ms: number) => void) | undefined autoExpandLimit: number classes: any className?: string } interface State { collapsedOverride: boolean | undefined } class TreeNode extends React.Component { private dirtySubnodes: boolean = true private dirtyEdges: boolean = true private dirtyMessage: boolean = true private animationDirty: boolean = false private cssAnimationWasSetAt?: number private willUpdateTime: number = performance.now() private titleRef?: React.RefObject = React.createRef() private nodeRef?: React.RefObject = React.createRef() private topicSelectRef?: React.RefObject = React.createRef() private subnodesDidchange = () => { this.dirtySubnodes = true } private messageDidChange = () => { this.dirtyMessage = true } private edgesDidChange = () => { this.dirtyMessage = true } constructor(props: Props) { super(props) this.state = { collapsedOverride: props.collapsed, } } public componentDidMount() { const { treeNode } = this.props this.addSubscriber(treeNode) } private addSubscriber(treeNode: q.TreeNode) { treeNode.onMerge.subscribe(this.subnodesDidchange) treeNode.onEdgesChange.subscribe(this.edgesDidChange) treeNode.onMessage.subscribe(this.messageDidChange) } private removeSubscriber(treeNode: q.TreeNode) { treeNode.onMerge.unsubscribe(this.subnodesDidchange) treeNode.onEdgesChange.unsubscribe(this.edgesDidChange) treeNode.onMessage.unsubscribe(this.messageDidChange) } public componentWillReceiveProps(nextProps: Props) { if (nextProps.treeNode !== this.props.treeNode) { this.removeSubscriber(this.props.treeNode) this.addSubscriber(nextProps.treeNode) } } public componentWillUnmount() { const { treeNode } = this.props this.removeSubscriber(treeNode) this.topicSelectRef = undefined this.titleRef = undefined this.nodeRef = undefined } private stateHasChanged(newState: State) { return this.state.collapsedOverride !== newState.collapsedOverride } private propsHasChanged(newProps: Props) { return this.props.autoExpandLimit !== newProps.autoExpandLimit } public shouldComponentUpdate(nextProps: Props, nextState: State) { const shouldRenderToRemoveCssAnimation = this.cssAnimationWasSetAt !== undefined return this.stateHasChanged(nextState) || this.propsHasChanged(nextProps) || this.dirtyEdges || this.dirtyMessage || this.dirtySubnodes || this.animationDirty || shouldRenderToRemoveCssAnimation } public componentDidUpdate() { if (this.props.performanceCallback) { const renderTime = performance.now() - this.willUpdateTime this.props.performanceCallback(renderTime) } } public componentWillUpdate() { if (this.props.performanceCallback) { this.willUpdateTime = performance.now() } } private toggle() { this.setState({ collapsedOverride: !this.collapsed() }) } private collapsed() { if (this.state.collapsedOverride !== undefined) { return this.state.collapsedOverride } return this.props.treeNode.edgeCount() > this.props.autoExpandLimit } public render() { const { classes } = this.props const isDirty = this.dirtyEdges || this.dirtyMessage || this.dirtySubnodes this.dirtyEdges = this.dirtyMessage = this.dirtySubnodes = false const shouldStartAnimation = (isDirty && !this.animationDirty) && !this.props.isRoot const animation = shouldStartAnimation ? { willChange: 'auto', translateZ: 0, animation: 'example 0.5s' } : {} this.animationDirty = shouldStartAnimation return (
{this.renderNodes()}
) } private mouseOver = (event: React.MouseEvent) => { event.stopPropagation() if (this.nodeRef && this.nodeRef.current) { this.nodeRef.current.style.backgroundColor = 'rgba(100, 100, 100, 0.55)' } if (this.topicSelectRef && this.topicSelectRef.current) { this.topicSelectRef.current.style.opacity = '1' } } private mouseOut = (event: React.MouseEvent) => { event.stopPropagation() if (this.nodeRef && this.nodeRef.current) { this.nodeRef.current.style.backgroundColor = 'inherit' } if (this.topicSelectRef && this.topicSelectRef.current) { this.topicSelectRef.current.style.opacity = '0' } } private didSelectNode = (event: React.MouseEvent) => { event.stopPropagation() if (this.topicSelectRef && this.topicSelectRef.current) { this.topicSelectRef.current.style.opacity = '1' } this.props.actions.selectTopic(this.props.treeNode) } private didClickNode = (event: React.MouseEvent) => { event.stopPropagation() this.toggle() this.props.actions.selectTopic(this.props.treeNode) } private renderNodes() { return ( ) } } const mapDispatchToProps = (dispatch: any) => { return { actions: bindActionCreators(treeActions, dispatch), } } const mapStateToProps = (state: AppState) => { return { autoExpandLimit: state.settings.autoExpandLimit, } } export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(TreeNode))