Refactor TreeNodeComponent
This commit is contained in:
@@ -56,13 +56,6 @@ class TreeComponent extends React.PureComponent<Props, State> {
|
|||||||
average.push(Date.now(), ms)
|
average.push(Date.now(), ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
public time(): number {
|
|
||||||
const time = performance.now() - this.perf
|
|
||||||
this.perf = performance.now()
|
|
||||||
|
|
||||||
return time
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentWillReceiveProps(nextProps: Props) {
|
public componentWillReceiveProps(nextProps: Props) {
|
||||||
if (this.props.tree !== nextProps.tree) {
|
if (this.props.tree !== nextProps.tree) {
|
||||||
if (this.props.tree) {
|
if (this.props.tree) {
|
||||||
@@ -110,6 +103,14 @@ class TreeComponent extends React.PureComponent<Props, State> {
|
|||||||
}, Math.max(0, timeUntilNextUpdate))
|
}, Math.max(0, timeUntilNextUpdate))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public componentWillUpdate() {
|
||||||
|
this.perf = performance.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate() {
|
||||||
|
this.performanceCallback(performance.now() - this.perf)
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { tree } = this.props
|
const { tree } = this.props
|
||||||
if (!tree) {
|
if (!tree) {
|
||||||
@@ -139,10 +140,9 @@ class TreeComponent extends React.PureComponent<Props, State> {
|
|||||||
treeNode={tree}
|
treeNode={tree}
|
||||||
name={this.props.host}
|
name={this.props.host}
|
||||||
collapsed={false}
|
collapsed={false}
|
||||||
performanceCallback={this.performanceCallback}
|
|
||||||
settings={this.props.settings}
|
settings={this.props.settings}
|
||||||
lastUpdate={tree.lastUpdate}
|
lastUpdate={tree.lastUpdate}
|
||||||
didSelectTopic={this.props.actions.selectTopic}
|
selectTopicAction={this.props.actions.selectTopic}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import * as q from '../../../../backend/src/Model'
|
import * as q from '../../../../backend/src/Model'
|
||||||
import * as React from 'react'
|
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react'
|
||||||
import TreeNodeSubnodes from './TreeNodeSubnodes'
|
import TreeNodeSubnodes from './TreeNodeSubnodes'
|
||||||
import TreeNodeTitle from './TreeNodeTitle'
|
import TreeNodeTitle from './TreeNodeTitle'
|
||||||
import { SettingsState } from '../../reducers/Settings'
|
import { SettingsState } from '../../reducers/Settings'
|
||||||
import { Theme, withStyles } from '@material-ui/core/styles'
|
import { Theme, withStyles } from '@material-ui/core/styles'
|
||||||
import { TopicViewModel } from '../../model/TopicViewModel'
|
import { TopicViewModel } from '../../model/TopicViewModel'
|
||||||
|
import { useViewModelSubscriptions } from './useViewModelSubscriptions'
|
||||||
const debounce = require('lodash.debounce')
|
const debounce = require('lodash.debounce')
|
||||||
|
|
||||||
declare var performance: any
|
declare var performance: any
|
||||||
@@ -53,216 +54,145 @@ interface Props {
|
|||||||
treeNode: q.TreeNode<TopicViewModel>
|
treeNode: q.TreeNode<TopicViewModel>
|
||||||
name?: string | undefined
|
name?: string | undefined
|
||||||
collapsed?: boolean | undefined
|
collapsed?: boolean | undefined
|
||||||
performanceCallback?: ((ms: number) => void) | undefined
|
|
||||||
classes: any
|
classes: any
|
||||||
className?: string
|
className?: string
|
||||||
lastUpdate: number
|
lastUpdate: number
|
||||||
didSelectTopic: any
|
selectTopicAction: (treeNode: q.TreeNode<any>) => void
|
||||||
theme: Theme
|
theme: Theme
|
||||||
settings: SettingsState
|
settings: SettingsState
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
function useIsAllowedToAutoExpandState(props: Props): boolean {
|
||||||
collapsedOverride: boolean | undefined
|
const { settings, treeNode, isRoot } = props
|
||||||
mouseOver: boolean
|
const [isAllowedToAutoExpand, setAllowAutoExpand] = useState(false)
|
||||||
selected: boolean
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newIsAllowedToAutoExpand = !(treeNode.edgeCount() > settings.get('autoExpandLimit'))
|
||||||
|
if (!isRoot && newIsAllowedToAutoExpand !== isAllowedToAutoExpand) {
|
||||||
|
setAllowAutoExpand(newIsAllowedToAutoExpand)
|
||||||
|
}
|
||||||
|
}, [treeNode.edgeCount(), settings.get('autoExpandLimit')])
|
||||||
|
|
||||||
|
return isAllowedToAutoExpand
|
||||||
}
|
}
|
||||||
|
|
||||||
class TreeNodeComponent extends React.Component<Props, State> {
|
function TreeNodeComponent(props: Props) {
|
||||||
private animationDirty: boolean = false
|
const { classes, className, settings, theme, treeNode, lastUpdate } = props
|
||||||
private cssAnimationWasSetAt?: number
|
|
||||||
private willUpdateTime: number = performance.now()
|
|
||||||
private nodeRef?: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>()
|
|
||||||
|
|
||||||
private setHover = debounce((hover: boolean) => {
|
const [animationDirty, setAnimationDirty] = useState(false)
|
||||||
this.setState({ mouseOver: hover })
|
const [collapsed, setCollapsed] = useState<boolean | undefined>(props.collapsed)
|
||||||
|
const [cssAnimationWasSetAt, setCssAnimationWasSetAt] = useState(0)
|
||||||
|
const [willUpdateTime, setWillUpdateTime] = useState(performance.now())
|
||||||
|
const [isHovering, setIsHovering] = useState(false)
|
||||||
|
const [selected, setSelected] = useState(false)
|
||||||
|
const nodeRef = useRef<HTMLDivElement>()
|
||||||
|
const isAllowedToAutoExpand = useIsAllowedToAutoExpandState(props)
|
||||||
|
useViewModelSubscriptions(treeNode, nodeRef, setSelected, setCollapsed)
|
||||||
|
|
||||||
|
const setHover = debounce((hover: boolean) => {
|
||||||
|
setIsHovering(hover)
|
||||||
}, 45)
|
}, 45)
|
||||||
|
|
||||||
constructor(props: Props) {
|
const toggle = useCallback(() => {
|
||||||
super(props)
|
setCollapsed(!collapsed)
|
||||||
|
}, [collapsed])
|
||||||
|
|
||||||
this.state = {
|
const didSelectTopic = useCallback(
|
||||||
collapsedOverride: props.collapsed,
|
(event?: React.MouseEvent) => {
|
||||||
mouseOver: false,
|
console.log('Did select', treeNode.path())
|
||||||
selected: false,
|
console.log(event)
|
||||||
}
|
event && event.stopPropagation()
|
||||||
}
|
props.selectTopicAction(treeNode)
|
||||||
|
},
|
||||||
|
[treeNode]
|
||||||
|
)
|
||||||
|
|
||||||
private addSubscriber(treeNode: q.TreeNode<TopicViewModel>) {
|
const didClickTitle = (event: React.MouseEvent) => {
|
||||||
treeNode.viewModel = new TopicViewModel()
|
|
||||||
treeNode.viewModel.selectionChange.subscribe(this.selectionDidChange)
|
|
||||||
treeNode.viewModel.expandedChange.subscribe(this.expandedDidChange)
|
|
||||||
}
|
|
||||||
|
|
||||||
private selectionDidChange = () => {
|
|
||||||
const selected = this.props.treeNode.viewModel && this.props.treeNode.viewModel.isSelected()
|
|
||||||
this.props.treeNode.viewModel && this.setState({ selected: Boolean(selected) })
|
|
||||||
if (selected && this.nodeRef && this.nodeRef.current) {
|
|
||||||
this.nodeRef.current.focus({ preventScroll: false })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private expandedDidChange = () => {
|
|
||||||
this.props.treeNode.viewModel && this.setState({ collapsedOverride: !this.props.treeNode.viewModel.isExpanded() })
|
|
||||||
}
|
|
||||||
|
|
||||||
private removeSubscriber(treeNode: q.TreeNode<TopicViewModel>) {
|
|
||||||
if (treeNode.viewModel) {
|
|
||||||
treeNode.viewModel.selectionChange.unsubscribe(this.selectionDidChange)
|
|
||||||
treeNode.viewModel.expandedChange.unsubscribe(this.expandedDidChange)
|
|
||||||
treeNode.viewModel = undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private stateHasChanged(newState: State) {
|
|
||||||
return (
|
|
||||||
this.state.collapsedOverride !== newState.collapsedOverride ||
|
|
||||||
this.state.mouseOver !== newState.mouseOver ||
|
|
||||||
this.state.selected !== newState.selected
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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.settings.get('autoExpandLimit')
|
|
||||||
}
|
|
||||||
|
|
||||||
private didSelectTopic = () => {
|
|
||||||
this.props.didSelectTopic(this.props.treeNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
private didClickTitle = (event: React.MouseEvent) => {
|
|
||||||
event.preventDefault()
|
|
||||||
this.props.didSelectTopic(this.props.treeNode)
|
|
||||||
this.toggle()
|
|
||||||
}
|
|
||||||
|
|
||||||
private mouseOver = (event: React.MouseEvent) => {
|
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
this.setHover(true)
|
didSelectTopic()
|
||||||
if (
|
toggle()
|
||||||
this.props.settings.get('selectTopicWithMouseOver') &&
|
|
||||||
this.props.treeNode &&
|
|
||||||
this.props.treeNode.message &&
|
|
||||||
this.props.treeNode.message.value
|
|
||||||
) {
|
|
||||||
this.props.didSelectTopic(this.props.treeNode)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private mouseOut = (event: React.MouseEvent) => {
|
const didObtainFocus = useCallback(() => {
|
||||||
|
didSelectTopic()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const mouseOver = (event: React.MouseEvent) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
this.setHover(false)
|
setHover(true)
|
||||||
|
if (settings.get('selectTopicWithMouseOver') && treeNode && treeNode.message && treeNode.message.value) {
|
||||||
|
didSelectTopic()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private toggleCollapsed = (event: React.MouseEvent) => {
|
const mouseOut = (event: React.MouseEvent) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
this.toggle()
|
setHover(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderNodes() {
|
const toggleCollapsed = useCallback(
|
||||||
const isCollapsed = this.collapsed()
|
(event: React.MouseEvent) => {
|
||||||
this.props.treeNode.viewModel && this.props.treeNode.viewModel.setExpanded(!isCollapsed, false)
|
event.stopPropagation()
|
||||||
|
toggle()
|
||||||
|
},
|
||||||
|
[toggle]
|
||||||
|
)
|
||||||
|
|
||||||
if (this.collapsed()) {
|
useEffect(() => {
|
||||||
return null
|
treeNode.viewModel && treeNode.viewModel.setExpanded(!collapsed, false)
|
||||||
|
}, [collapsed])
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const shouldBeRenderedCollapsed = Boolean(collapsed) === collapsed ? Boolean(collapsed) : !isAllowedToAutoExpand
|
||||||
|
|
||||||
|
function renderNodes() {
|
||||||
|
if (shouldBeRenderedCollapsed) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TreeNodeSubnodes
|
||||||
|
treeNode={treeNode}
|
||||||
|
lastUpdate={treeNode.lastUpdate}
|
||||||
|
selectTopicAction={props.selectTopicAction}
|
||||||
|
settings={settings}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const shouldStartAnimation = settings.get('highlightTopicUpdates')
|
||||||
<TreeNodeSubnodes
|
const animationName = theme.palette.type === 'light' ? 'updateLight' : 'updateDark'
|
||||||
collapsed={this.collapsed()}
|
|
||||||
treeNode={this.props.treeNode}
|
|
||||||
lastUpdate={this.props.treeNode.lastUpdate}
|
|
||||||
didSelectTopic={this.props.didSelectTopic}
|
|
||||||
settings={this.props.settings}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidMount() {
|
|
||||||
const { treeNode } = this.props
|
|
||||||
this.addSubscriber(treeNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentWillUnmount() {
|
|
||||||
const { treeNode } = this.props
|
|
||||||
this.removeSubscriber(treeNode)
|
|
||||||
this.nodeRef = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
public shouldComponentUpdate(nextProps: Props, nextState: State) {
|
|
||||||
const shouldRenderToRemoveCssAnimation = this.cssAnimationWasSetAt !== undefined
|
|
||||||
return (
|
|
||||||
this.stateHasChanged(nextState) ||
|
|
||||||
this.props.settings !== nextProps.settings ||
|
|
||||||
this.props.lastUpdate !== nextProps.lastUpdate ||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
const { classes } = this.props
|
|
||||||
|
|
||||||
const shouldStartAnimation =
|
|
||||||
!this.animationDirty && !this.props.isRoot && this.props.settings.get('highlightTopicUpdates')
|
|
||||||
const animationName = this.props.theme.palette.type === 'light' ? 'updateLight' : 'updateDark'
|
|
||||||
const animation = shouldStartAnimation
|
const animation = shouldStartAnimation
|
||||||
? { willChange: 'auto', translateZ: 0, animation: `${animationName} 0.5s` }
|
? { willChange: 'auto', translateZ: 0, animation: `${animationName} 0.5s` }
|
||||||
: {}
|
: {}
|
||||||
this.animationDirty = shouldStartAnimation
|
|
||||||
|
|
||||||
const highlightClass = this.state.selected
|
const highlightClass = selected ? classes.selected : isHovering ? classes.hover : ''
|
||||||
? this.props.classes.selected
|
|
||||||
: this.state.mouseOver
|
|
||||||
? this.props.classes.hover
|
|
||||||
: ''
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
key={this.props.treeNode.hash()}
|
key={treeNode.hash()}
|
||||||
className={`${classes.node} ${this.props.className} ${highlightClass} ${classes.title}`}
|
className={`${classes.node} ${className} ${highlightClass} ${classes.title}`}
|
||||||
style={animation}
|
style={animation}
|
||||||
onMouseOver={this.mouseOver}
|
onMouseOver={mouseOver}
|
||||||
onMouseOut={this.mouseOut}
|
onMouseOut={mouseOut}
|
||||||
onClick={this.didClickTitle}
|
onClick={didClickTitle}
|
||||||
onFocus={() => this.props.didSelectTopic(this.props.treeNode)}
|
onFocus={didObtainFocus}
|
||||||
ref={this.nodeRef}
|
ref={nodeRef as any}
|
||||||
tabIndex={1000}
|
tabIndex={1000}
|
||||||
>
|
>
|
||||||
<TreeNodeTitle
|
<TreeNodeTitle
|
||||||
toggleCollapsed={this.toggleCollapsed}
|
toggleCollapsed={toggleCollapsed}
|
||||||
didSelectNode={this.didSelectTopic}
|
didSelectNode={didSelectTopic}
|
||||||
collapsed={this.collapsed()}
|
collapsed={shouldBeRenderedCollapsed}
|
||||||
treeNode={this.props.treeNode}
|
treeNode={treeNode}
|
||||||
name={this.props.name}
|
name={name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{this.renderNodes()}
|
{renderNodes()}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}, [lastUpdate, treeNode, collapsed, selected, isAllowedToAutoExpand, isHovering])
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles, { withTheme: true })(TreeNodeComponent)
|
export default withStyles(styles, { withTheme: true })(TreeNodeComponent)
|
||||||
|
|||||||
@@ -9,11 +9,10 @@ import { TopicViewModel } from '../../model/TopicViewModel'
|
|||||||
export interface Props {
|
export interface Props {
|
||||||
treeNode: q.TreeNode<TopicViewModel>
|
treeNode: q.TreeNode<TopicViewModel>
|
||||||
filter?: string
|
filter?: string
|
||||||
collapsed?: boolean | undefined
|
|
||||||
classes: any
|
classes: any
|
||||||
lastUpdate: number
|
lastUpdate: number
|
||||||
selectedTopic?: q.TreeNode<TopicViewModel>
|
selectedTopic?: q.TreeNode<TopicViewModel>
|
||||||
didSelectTopic: any
|
selectTopicAction: (treeNode: q.TreeNode<any>) => void
|
||||||
settings: SettingsState
|
settings: SettingsState
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +42,7 @@ class TreeNodeSubnodes extends React.Component<Props, State> {
|
|||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const edges = this.props.treeNode.edgeArray
|
const edges = this.props.treeNode.edgeArray
|
||||||
if (edges.length === 0 || this.props.collapsed) {
|
if (edges.length === 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +58,7 @@ class TreeNodeSubnodes extends React.Component<Props, State> {
|
|||||||
treeNode={node}
|
treeNode={node}
|
||||||
className={this.props.classes.listItem}
|
className={this.props.classes.listItem}
|
||||||
lastUpdate={node.lastUpdate}
|
lastUpdate={node.lastUpdate}
|
||||||
didSelectTopic={this.props.didSelectTopic}
|
selectTopicAction={this.props.selectTopicAction}
|
||||||
settings={this.props.settings}
|
settings={this.props.settings}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
43
app/src/components/Tree/useViewModelSubscriptions.tsx
Normal file
43
app/src/components/Tree/useViewModelSubscriptions.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import * as q from '../../../../backend/src/Model'
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import { TopicViewModel } from '../../model/TopicViewModel'
|
||||||
|
|
||||||
|
export function useViewModelSubscriptions(
|
||||||
|
treeNode: q.TreeNode<TopicViewModel>,
|
||||||
|
nodeRef: React.MutableRefObject<HTMLDivElement | undefined>,
|
||||||
|
setSelected: (value: boolean) => void,
|
||||||
|
setCollapsedOverride: (value: boolean) => void
|
||||||
|
) {
|
||||||
|
const selectionDidChange = () => {
|
||||||
|
const selected = treeNode.viewModel && treeNode.viewModel.isSelected()
|
||||||
|
treeNode.viewModel && setSelected(Boolean(selected))
|
||||||
|
if (selected && nodeRef && nodeRef.current) {
|
||||||
|
nodeRef.current.focus({ preventScroll: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandedDidChange = () => {
|
||||||
|
treeNode.viewModel && setCollapsedOverride(!treeNode.viewModel.isExpanded())
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
addSubscriber()
|
||||||
|
return function cleanup() {
|
||||||
|
removeSubscriber()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function addSubscriber() {
|
||||||
|
treeNode.viewModel = new TopicViewModel()
|
||||||
|
treeNode.viewModel.selectionChange.subscribe(selectionDidChange)
|
||||||
|
treeNode.viewModel.expandedChange.subscribe(expandedDidChange)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSubscriber() {
|
||||||
|
if (treeNode.viewModel) {
|
||||||
|
treeNode.viewModel.selectionChange.unsubscribe(selectionDidChange)
|
||||||
|
treeNode.viewModel.expandedChange.unsubscribe(expandedDidChange)
|
||||||
|
treeNode.viewModel = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user