Make topics selectable
This commit is contained in:
@@ -4,30 +4,38 @@ import * as q from '../../../../backend/src/Model'
|
||||
import { AppState } from '../../reducers'
|
||||
import TreeNode from './TreeNode'
|
||||
import { connect } from 'react-redux'
|
||||
import { TopicOrder } from '../../reducers/Settings'
|
||||
import { TopicViewModel } from '../../TopicViewModel'
|
||||
|
||||
const MovingAverage = require('moving-average')
|
||||
|
||||
const timeInterval = 10 * 1000
|
||||
const average = MovingAverage(timeInterval)
|
||||
const averagingTimeInterval = 10 * 1000
|
||||
const average = MovingAverage(averagingTimeInterval)
|
||||
|
||||
declare var window: any
|
||||
|
||||
interface Props {
|
||||
autoExpandLimit: number
|
||||
connectionId?: string
|
||||
tree?: q.Tree
|
||||
tree?: q.Tree<TopicViewModel>
|
||||
filter: string
|
||||
host?: string
|
||||
|
||||
topicOrder: TopicOrder
|
||||
autoExpandLimit: number
|
||||
}
|
||||
|
||||
class Tree extends React.Component<Props, {}> {
|
||||
interface State {
|
||||
lastUpdate: number
|
||||
}
|
||||
|
||||
class Tree extends React.PureComponent<Props, State> {
|
||||
private updateTimer?: any
|
||||
private lastUpdate: number = 0
|
||||
private perf: number = 0
|
||||
private renderTime = 0
|
||||
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
this.state = { }
|
||||
this.state = { lastUpdate: 0 }
|
||||
}
|
||||
|
||||
public time(): number {
|
||||
@@ -40,17 +48,17 @@ class Tree extends React.Component<Props, {}> {
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
if (this.props.tree !== nextProps.tree) {
|
||||
if (this.props.tree) {
|
||||
this.props.tree.onMerge.unsubscribe(this.throttledTreeUpdate)
|
||||
this.props.tree.didReceive.unsubscribe(this.throttledTreeUpdate)
|
||||
}
|
||||
if (nextProps.tree) {
|
||||
nextProps.tree.onMerge.subscribe(this.throttledTreeUpdate)
|
||||
nextProps.tree.didReceive.subscribe(this.throttledTreeUpdate)
|
||||
}
|
||||
this.setState(this.state)
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.props.tree && this.props.tree.onMerge.unsubscribe(this.throttledTreeUpdate)
|
||||
this.props.tree && this.props.tree.didReceive.unsubscribe(this.throttledTreeUpdate)
|
||||
}
|
||||
|
||||
public throttledTreeUpdate = () => {
|
||||
@@ -60,14 +68,17 @@ class Tree extends React.Component<Props, {}> {
|
||||
|
||||
const expectedRenderTime = average.forecast()
|
||||
const updateInterval = Math.max(expectedRenderTime * 7, 300)
|
||||
const timeUntilNextUpdate = updateInterval - (performance.now() - this.lastUpdate)
|
||||
const timeUntilNextUpdate = updateInterval - (performance.now() - this.renderTime)
|
||||
|
||||
this.updateTimer = setTimeout(() => {
|
||||
window.requestIdleCallback(() => {
|
||||
this.lastUpdate = performance.now()
|
||||
this.updateTimer && clearTimeout(this.updateTimer)
|
||||
this.updateTimer = undefined
|
||||
this.setState(this.state)
|
||||
this.renderTime = performance.now()
|
||||
this.props.tree && this.props.tree.applyUnmergedChanges()
|
||||
window.requestIdleCallback(() => {
|
||||
this.setState({ lastUpdate: this.renderTime })
|
||||
}, { timeout: 100 })
|
||||
}, { timeout: 500 })
|
||||
}, Math.max(0, timeUntilNextUpdate))
|
||||
}
|
||||
@@ -91,9 +102,11 @@ class Tree extends React.Component<Props, {}> {
|
||||
isRoot={true}
|
||||
treeNode={tree}
|
||||
name={this.props.host}
|
||||
lastUpdate={tree.lastUpdate}
|
||||
collapsed={false}
|
||||
performanceCallback={this.performanceCallback}
|
||||
autoExpandLimit={this.props.autoExpandLimit}
|
||||
topicOrder={this.props.topicOrder}
|
||||
lastUpdate={tree.lastUpdate}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -106,10 +119,11 @@ class Tree extends React.Component<Props, {}> {
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
autoExpandLimit: state.settings.autoExpandLimit,
|
||||
tree: state.tree.tree,
|
||||
filter: state.tree.filter,
|
||||
host: state.connection.host,
|
||||
autoExpandLimit: state.settings.autoExpandLimit,
|
||||
topicOrder: state.settings.topicOrder,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ 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'
|
||||
@@ -11,6 +10,9 @@ import { connect } from 'react-redux'
|
||||
import { isElementInViewport } from '../helper/isElementInViewport'
|
||||
import { treeActions } from '../../actions'
|
||||
import { AppState } from '../../reducers'
|
||||
import { TopicOrder } from '../../reducers/Settings'
|
||||
import { TopicViewModel } from '../../TopicViewModel'
|
||||
const debounce = require('lodash.debounce')
|
||||
|
||||
declare var performance: any
|
||||
|
||||
@@ -26,36 +28,40 @@ const styles = (theme: Theme) => {
|
||||
display: 'block',
|
||||
marginLeft: '10px',
|
||||
},
|
||||
// hover: {
|
||||
// '&:hover': {
|
||||
// backgroundColor: 'rgba(80, 80, 80, 0.35)',
|
||||
// },
|
||||
// },
|
||||
topicSelect: {
|
||||
float: 'right' as 'right',
|
||||
opacity: 0,
|
||||
cursor: 'pointer',
|
||||
marginTop: '-1px',
|
||||
},
|
||||
selected: {
|
||||
backgroundColor: 'rgba(120, 120, 120, 0.55)',
|
||||
},
|
||||
hover: {
|
||||
backgroundColor: 'rgba(80, 80, 80, 0.55)',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
actions: typeof treeActions
|
||||
lastUpdate: number
|
||||
animateChages: boolean
|
||||
isRoot?: boolean
|
||||
treeNode: q.TreeNode
|
||||
treeNode: q.TreeNode<TopicViewModel>
|
||||
name?: string | undefined
|
||||
collapsed?: boolean | undefined
|
||||
performanceCallback?: ((ms: number) => void) | undefined
|
||||
autoExpandLimit: number
|
||||
classes: any
|
||||
className?: string
|
||||
topicOrder: TopicOrder
|
||||
autoExpandLimit: number
|
||||
lastUpdate: number
|
||||
}
|
||||
|
||||
interface State {
|
||||
collapsedOverride: boolean | undefined
|
||||
mouseOver: boolean
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
class TreeNode extends React.Component<Props, State> {
|
||||
@@ -63,13 +69,13 @@ class TreeNode extends React.Component<Props, State> {
|
||||
private dirtyEdges: boolean = true
|
||||
private dirtyMessage: boolean = true
|
||||
private animationDirty: boolean = false
|
||||
private lastRenderTime = 0
|
||||
|
||||
private cssAnimationWasSetAt?: number
|
||||
|
||||
private willUpdateTime: number = performance.now()
|
||||
private titleRef?: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>()
|
||||
private nodeRef?: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>()
|
||||
private topicSelectRef?: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>()
|
||||
|
||||
private subnodesDidchange = () => {
|
||||
this.dirtySubnodes = true
|
||||
@@ -88,6 +94,8 @@ class TreeNode extends React.Component<Props, State> {
|
||||
|
||||
this.state = {
|
||||
collapsedOverride: props.collapsed,
|
||||
mouseOver: false,
|
||||
selected: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,13 +104,21 @@ class TreeNode extends React.Component<Props, State> {
|
||||
this.addSubscriber(treeNode)
|
||||
}
|
||||
|
||||
private addSubscriber(treeNode: q.TreeNode) {
|
||||
private addSubscriber(treeNode: q.TreeNode<TopicViewModel>) {
|
||||
treeNode.viewModel = new TopicViewModel()
|
||||
treeNode.viewModel.change.subscribe(this.viewStateHasChanged)
|
||||
treeNode.onMerge.subscribe(this.subnodesDidchange)
|
||||
treeNode.onEdgesChange.subscribe(this.edgesDidChange)
|
||||
treeNode.onMessage.subscribe(this.messageDidChange)
|
||||
}
|
||||
|
||||
private removeSubscriber(treeNode: q.TreeNode) {
|
||||
private viewStateHasChanged = (msg: void, viewModel: TopicViewModel) => {
|
||||
this.setState({ selected: viewModel.isSelected() })
|
||||
}
|
||||
|
||||
private removeSubscriber(treeNode: q.TreeNode<TopicViewModel>) {
|
||||
treeNode.viewModel && treeNode.viewModel.change.unsubscribe(this.viewStateHasChanged)
|
||||
treeNode.viewModel = undefined
|
||||
treeNode.onMerge.unsubscribe(this.subnodesDidchange)
|
||||
treeNode.onEdgesChange.unsubscribe(this.edgesDidChange)
|
||||
treeNode.onMessage.unsubscribe(this.messageDidChange)
|
||||
@@ -118,13 +134,14 @@ class TreeNode extends React.Component<Props, State> {
|
||||
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
|
||||
|| this.state.mouseOver !== newState.mouseOver
|
||||
|| this.state.selected !== newState.selected
|
||||
}
|
||||
|
||||
private propsHasChanged(newProps: Props) {
|
||||
@@ -135,9 +152,7 @@ class TreeNode extends React.Component<Props, State> {
|
||||
const shouldRenderToRemoveCssAnimation = this.cssAnimationWasSetAt !== undefined
|
||||
return this.stateHasChanged(nextState)
|
||||
|| this.propsHasChanged(nextProps)
|
||||
|| this.dirtyEdges
|
||||
|| this.dirtyMessage
|
||||
|| this.dirtySubnodes
|
||||
|| (this.dirtyEdges || this.dirtyMessage || this.dirtySubnodes)
|
||||
|| this.animationDirty
|
||||
|| shouldRenderToRemoveCssAnimation
|
||||
}
|
||||
@@ -176,10 +191,11 @@ class TreeNode extends React.Component<Props, State> {
|
||||
const animation = shouldStartAnimation ? { willChange: 'auto', translateZ: 0, animation: 'example 0.5s' } : {}
|
||||
this.animationDirty = shouldStartAnimation
|
||||
|
||||
const highlightClass = this.state.selected ? this.props.classes.selected : (this.state.mouseOver ? this.props.classes.hover : '')
|
||||
return (
|
||||
<div
|
||||
key={this.props.treeNode.hash()}
|
||||
className={`${classes.node} ${this.props.className}`}
|
||||
className={`${classes.node} ${this.props.className} ${highlightClass}`}
|
||||
onClick={this.didClickNode}
|
||||
onMouseOver={this.mouseOver}
|
||||
onMouseOut={this.mouseOut}
|
||||
@@ -190,7 +206,7 @@ class TreeNode extends React.Component<Props, State> {
|
||||
collapsed={this.collapsed()}
|
||||
treeNode={this.props.treeNode}
|
||||
name={this.props.name}
|
||||
lastUpdate={this.props.treeNode.lastUpdate}
|
||||
didSelectNode={this.didSelectTopic}
|
||||
/>
|
||||
</span>
|
||||
{this.renderNodes()}
|
||||
@@ -198,31 +214,27 @@ class TreeNode extends React.Component<Props, State> {
|
||||
)
|
||||
}
|
||||
|
||||
private didSelectTopic = () => {
|
||||
this.props.actions.selectTopic(this.props.treeNode)
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
this.setHover(true)
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
this.setHover(false)
|
||||
}
|
||||
|
||||
private setHover = debounce((hover: boolean) => {
|
||||
this.setState({ mouseOver: hover })
|
||||
}, 5)
|
||||
|
||||
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)
|
||||
this.didSelectTopic()
|
||||
}
|
||||
|
||||
private didClickNode = (event: React.MouseEvent) => {
|
||||
@@ -236,8 +248,9 @@ class TreeNode extends React.Component<Props, State> {
|
||||
<TreeNodeSubnodes
|
||||
animateChanges={this.props.animateChages}
|
||||
collapsed={this.collapsed()}
|
||||
autoExpandLimit={this.props.autoExpandLimit}
|
||||
treeNode={this.props.treeNode}
|
||||
autoExpandLimit={this.props.autoExpandLimit}
|
||||
topicOrder={this.props.topicOrder}
|
||||
lastUpdate={this.props.treeNode.lastUpdate}
|
||||
/>
|
||||
)
|
||||
@@ -250,10 +263,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
autoExpandLimit: state.settings.autoExpandLimit,
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(TreeNode))
|
||||
export default withStyles(styles)(connect(null, mapDispatchToProps)(TreeNode))
|
||||
|
||||
@@ -7,17 +7,20 @@ import TreeNode from './TreeNode'
|
||||
import { connect } from 'react-redux'
|
||||
import { TopicOrder } from '../../reducers/Settings'
|
||||
import { Theme, withStyles } from '@material-ui/core'
|
||||
import { TopicViewModel } from '../../TopicViewModel'
|
||||
|
||||
export interface Props {
|
||||
lastUpdate: number
|
||||
topicOrder?: TopicOrder
|
||||
animateChanges: boolean
|
||||
treeNode: q.TreeNode
|
||||
autoExpandLimit: number
|
||||
treeNode: q.TreeNode<TopicViewModel>
|
||||
filter?: string
|
||||
collapsed?: boolean | undefined
|
||||
didSelectNode?: (node: q.TreeNode) => void
|
||||
classes: any
|
||||
|
||||
lastUpdate: number
|
||||
|
||||
topicOrder: TopicOrder
|
||||
selectedTopic?: q.TreeNode<TopicViewModel>
|
||||
autoExpandLimit: number
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -31,7 +34,7 @@ class TreeNodeSubnodes extends React.Component<Props, State> {
|
||||
this.state = { alreadyAdded: 10 }
|
||||
}
|
||||
|
||||
private sortedNodes(): q.TreeNode[] {
|
||||
private sortedNodes(): q.TreeNode<TopicViewModel>[] {
|
||||
const { topicOrder, treeNode } = this.props
|
||||
|
||||
let edges = treeNode.edgeArray
|
||||
@@ -72,15 +75,19 @@ class TreeNodeSubnodes extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
const nodes = this.sortedNodes().slice(0, this.state.alreadyAdded)
|
||||
const listItems = nodes.map(node => (
|
||||
<TreeNode
|
||||
key={`${node.hash()}-${this.props.filter}`}
|
||||
animateChages={this.props.animateChanges}
|
||||
treeNode={node}
|
||||
lastUpdate={node.lastUpdate}
|
||||
className={this.props.classes.listItem}
|
||||
/>
|
||||
))
|
||||
const listItems = nodes.map((node) => {
|
||||
return (
|
||||
<TreeNode
|
||||
key={`${node.hash()}-${this.props.filter}`}
|
||||
animateChages={this.props.animateChanges}
|
||||
treeNode={node}
|
||||
className={this.props.classes.listItem}
|
||||
topicOrder={this.props.topicOrder}
|
||||
autoExpandLimit={this.props.autoExpandLimit}
|
||||
lastUpdate={node.lastUpdate}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<span className={this.props.classes.list}>
|
||||
@@ -90,13 +97,6 @@ class TreeNodeSubnodes extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
topicOrder: state.settings.topicOrder,
|
||||
filter: state.tree.filter,
|
||||
}
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => ({
|
||||
list: {
|
||||
display: 'block' as 'block',
|
||||
@@ -107,4 +107,4 @@ const styles = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default withStyles(styles)(connect(mapStateToProps)(TreeNodeSubnodes))
|
||||
export default withStyles(styles)(TreeNodeSubnodes)
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
import * as React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { treeActions } from '../../actions'
|
||||
import * as q from '../../../../backend/src/Model'
|
||||
import { withStyles, Theme } from '@material-ui/core'
|
||||
import { TopicViewModel } from '../../TopicViewModel'
|
||||
const debounce = require('lodash.debounce')
|
||||
|
||||
export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
|
||||
treeNode: q.TreeNode
|
||||
actions: any
|
||||
treeNode: q.TreeNode<TopicViewModel>
|
||||
name?: string | undefined
|
||||
collapsed?: boolean | undefined
|
||||
lastUpdate: number
|
||||
classes: any
|
||||
didSelectNode: any
|
||||
}
|
||||
|
||||
class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
|
||||
private mouseOver = (event: React.MouseEvent) => {
|
||||
if (this.props.treeNode.message) {
|
||||
this.props.actions.selectTopic(this.props.treeNode)
|
||||
}
|
||||
event.preventDefault()
|
||||
this.selectTopic()
|
||||
}
|
||||
|
||||
private selectTopic = debounce(() => {
|
||||
if (this.props.treeNode.message) {
|
||||
this.props.didSelectNode(this.props.treeNode)
|
||||
}
|
||||
}, 5)
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<span className={this.props.classes.title} onMouseOver={this.mouseOver}>
|
||||
<span className={this.props.classes.title} onMouseOver={this.props.treeNode.message ? this.mouseOver : undefined}>
|
||||
{this.renderExpander()} {this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()}
|
||||
</span>
|
||||
)
|
||||
@@ -59,12 +63,6 @@ class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
actions: bindActionCreators(treeActions, dispatch),
|
||||
}
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => ({
|
||||
value: {
|
||||
whiteSpace: 'nowrap' as 'nowrap',
|
||||
@@ -88,4 +86,4 @@ const styles = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default withStyles(styles)(connect(null, mapDispatchToProps)(TreeNodeTitle))
|
||||
export default withStyles(styles)(TreeNodeTitle)
|
||||
|
||||
Reference in New Issue
Block a user