This commit is contained in:
Thomas Nordquist
2019-06-26 17:41:04 +02:00
parent e02091f645
commit f9829c2d5c
24 changed files with 204 additions and 152 deletions

View File

@@ -1,7 +1,7 @@
import * as React from 'react'
import ChartPanel from '../ChartPanel'
import ReactSplitPane from 'react-split-pane'
import Tree from '../Tree/Tree'
import Tree from '../Tree'
import { AppState } from '../../reducers'
import { ChartParameters } from '../../reducers/Charts'
import { connect } from 'react-redux'

View File

@@ -41,7 +41,13 @@ function SearchBar(props: {
const tagElementIsNotBlacklisted =
document.activeElement && tagNameBlacklist.indexOf(document.activeElement.tagName) === -1
if ((isCharacter || isAllowedControlCharacter) && !hasFocus && tagElementIsNotBlacklisted && hasConnection) {
if (
(isCharacter || isAllowedControlCharacter) &&
!event.defaultPrevented &&
!hasFocus &&
tagElementIsNotBlacklisted &&
hasConnection
) {
// Focus input field, no preventDefault the event will reach the input element after it has been focussed
inputRef.current && inputRef.current.focus()
}

View File

@@ -1,10 +1,11 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import TreeNode from './TreeNode'
import { SettingsState } from '../../reducers/Settings'
import { sortedNodes } from '../../sortedNodes'
import * as q from '../../../../../backend/src/Model'
import React from 'react'
import TreeNode from '.'
import { SettingsState } from '../../../reducers/Settings'
import { sortedNodes } from '../../../sortedNodes'
import { Theme, withStyles } from '@material-ui/core'
import { TopicViewModel } from '../../model/TopicViewModel'
import { TopicViewModel } from '../../../model/TopicViewModel'
import { treeActions } from '../../../actions'
export interface Props {
treeNode: q.TreeNode<TopicViewModel>
@@ -14,6 +15,7 @@ export interface Props {
selectedTopic?: q.TreeNode<TopicViewModel>
selectTopicAction: (treeNode: q.TreeNode<any>) => void
settings: SettingsState
actions: typeof treeActions
}
interface State {
@@ -60,6 +62,7 @@ class TreeNodeSubnodes extends React.Component<Props, State> {
lastUpdate={node.lastUpdate}
selectTopicAction={this.props.selectTopicAction}
settings={this.props.settings}
actions={this.props.actions}
/>
)
})

View File

@@ -1,8 +1,8 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import * as q from '../../../../../backend/src/Model'
import { withStyles, Theme } from '@material-ui/core'
import { TopicViewModel } from '../../model/TopicViewModel'
import { Base64Message } from '../../../../backend/src/Model/Base64Message'
import { TopicViewModel } from '../../../model/TopicViewModel'
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
treeNode: q.TreeNode<TopicViewModel>

View File

@@ -0,0 +1,22 @@
import React, { useEffect } from 'react'
export function useAnimationToIndicateTopicUpdate(
lastUpdate: number,
selected: boolean,
setShowUpdateAnimation: React.Dispatch<React.SetStateAction<boolean>>,
showUpdateAnimation: boolean
) {
useEffect(() => {
if (Date.now() - lastUpdate < 3000 && !selected) {
setShowUpdateAnimation(true)
}
}, [lastUpdate, selected])
useEffect(() => {
if (showUpdateAnimation) {
const timeout = setTimeout(() => setShowUpdateAnimation(false), 500)
return function cleanup() {
clearTimeout(timeout)
}
}
}, [showUpdateAnimation])
}

View File

@@ -0,0 +1,17 @@
import * as q from '../../../../../../backend/src/Model'
import React, { useCallback } from 'react'
import { KeyCodes } from '../../../../utils/KeyCodes'
import { treeActions } from '../../../../actions'
export function useDeleteKeyCallback(topic: q.TreeNode<any>, actions: typeof treeActions) {
return useCallback(
(event: React.KeyboardEvent) => {
if (event.keyCode === KeyCodes.delete || event.keyCode === KeyCodes.backspace) {
event.stopPropagation()
event.preventDefault()
actions.clearTopic(topic, true, 50)
}
},
[topic]
)
}

View File

@@ -0,0 +1,14 @@
import { Props } from '..'
import { useEffect, useState } from 'react'
export function useIsAllowedToAutoExpandState(props: Props): boolean {
const { settings, treeNode, isRoot } = props
const [isAllowedToAutoExpand, setAllowAutoExpand] = useState(false)
useEffect(() => {
const newIsAllowedToAutoExpand = isRoot || treeNode.edgeCount() <= settings.get('autoExpandLimit')
if (newIsAllowedToAutoExpand !== isAllowedToAutoExpand) {
setAllowAutoExpand(newIsAllowedToAutoExpand)
}
}, [treeNode.edgeCount(), settings.get('autoExpandLimit')])
return isAllowedToAutoExpand
}

View File

@@ -1,6 +1,6 @@
import * as q from '../../../../backend/src/Model'
import * as q from '../../../../../../backend/src/Model'
import React, { useEffect } from 'react'
import { TopicViewModel } from '../../model/TopicViewModel'
import { TopicViewModel } from '../../../../model/TopicViewModel'
export function useViewModelSubscriptions(
treeNode: q.TreeNode<TopicViewModel>,
@@ -11,6 +11,7 @@ export function useViewModelSubscriptions(
const selectionDidChange = () => {
const selected = treeNode.viewModel && treeNode.viewModel.isSelected()
treeNode.viewModel && setSelected(Boolean(selected))
if (selected && nodeRef && nodeRef.current) {
nodeRef.current.focus({ preventScroll: false })
}

View File

@@ -1,14 +1,16 @@
import * as q from '../../../../backend/src/Model'
import * as q from '../../../../../backend/src/Model'
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react'
import TreeNodeSubnodes from './TreeNodeSubnodes'
import TreeNodeTitle from './TreeNodeTitle'
import { SettingsState } from '../../reducers/Settings'
import { SettingsState } from '../../../reducers/Settings'
import { Theme, withStyles } from '@material-ui/core/styles'
import { TopicViewModel } from '../../model/TopicViewModel'
import { useViewModelSubscriptions } from './useViewModelSubscriptions'
const debounce = require('lodash.debounce')
declare var performance: any
import { TopicViewModel } from '../../../model/TopicViewModel'
import { useViewModelSubscriptions } from './effects/useViewModelSubscriptions'
import { treeActions } from '../../../actions'
import { lightBlue, teal, amber, green, deepPurple, blueGrey } from '@material-ui/core/colors'
import { useAnimationToIndicateTopicUpdate } from './effects/useAnimationToIndicateTopicUpdate'
import { useDeleteKeyCallback } from './effects/useDeleteKeyCallback'
import { useIsAllowedToAutoExpandState } from './effects/useIsAllowedToAutoExpandState'
const styles = (theme: Theme) => {
return {
@@ -21,6 +23,9 @@ const styles = (theme: Theme) => {
node: {
display: 'block',
marginLeft: '10px',
'&:hover': {
backgroundColor: theme.palette.type === 'light' ? blueGrey[100] : theme.palette.primary.light,
},
},
topicSelect: {
float: 'right' as 'right',
@@ -32,11 +37,9 @@ const styles = (theme: Theme) => {
marginLeft: theme.spacing(1.5),
},
selected: {
backgroundColor: theme.palette.type === 'dark' ? 'rgba(170, 170, 170, 0.55)' : 'rgba(170, 170, 170, 0.55)',
},
hover: {
backgroundColor: theme.palette.type === 'dark' ? 'rgba(100, 100, 100, 0.55)' : 'rgba(200, 200, 200, 0.55)',
backgroundColor: (theme.palette.type === 'light' ? blueGrey[300] : theme.palette.primary.main) + ' !important',
},
hover: {},
title: {
borderRadius: '4px',
lineHeight: '1em',
@@ -49,7 +52,7 @@ const styles = (theme: Theme) => {
}
}
interface Props {
export interface Props {
isRoot?: boolean
treeNode: q.TreeNode<TopicViewModel>
name?: string | undefined
@@ -57,44 +60,26 @@ interface Props {
classes: any
className?: string
lastUpdate: number
actions: typeof treeActions
selectTopicAction: (treeNode: q.TreeNode<any>) => void
theme: Theme
settings: SettingsState
}
function useIsAllowedToAutoExpandState(props: Props): boolean {
const { settings, treeNode, isRoot } = props
const [isAllowedToAutoExpand, setAllowAutoExpand] = useState(false)
useEffect(() => {
const newIsAllowedToAutoExpand = isRoot || treeNode.edgeCount() <= settings.get('autoExpandLimit')
if (newIsAllowedToAutoExpand !== isAllowedToAutoExpand) {
setAllowAutoExpand(newIsAllowedToAutoExpand)
}
}, [treeNode.edgeCount(), settings.get('autoExpandLimit')])
return isAllowedToAutoExpand
}
function TreeNodeComponent(props: Props) {
const { classes, className, settings, theme, treeNode, lastUpdate, name } = props
const { actions, classes, className, settings, theme, treeNode, lastUpdate, name } = props
const deleteTopicCallback = useDeleteKeyCallback(treeNode, actions)
const [showUpdateAnimation, setShowUpdateAnimation] = useState(false)
const [collapsedOverride, setCollapsedOverride] = useState<boolean | undefined>(undefined)
const [isHovering, setIsHovering] = useState(false)
const [selected, setSelected] = useState(false)
const nodeRef = useRef<HTMLDivElement>()
const isAllowedToAutoExpand = useIsAllowedToAutoExpandState(props)
useViewModelSubscriptions(treeNode, nodeRef, setSelected, setCollapsedOverride)
useAnimationToIndicateTopicUpdate(lastUpdate, setShowUpdateAnimation, showUpdateAnimation)
useAnimationToIndicateTopicUpdate(lastUpdate, selected, setShowUpdateAnimation, showUpdateAnimation)
const isCollapsed =
Boolean(collapsedOverride) === collapsedOverride ? Boolean(collapsedOverride) : !isAllowedToAutoExpand
const setHover = debounce((hover: boolean) => {
setIsHovering(hover)
}, 45)
const toggle = useCallback(() => {
setCollapsedOverride(!isCollapsed)
}, [isCollapsed])
@@ -130,17 +115,11 @@ function TreeNodeComponent(props: Props) {
const mouseOver = (event: React.MouseEvent) => {
event.stopPropagation()
setHover(true)
if (settings.get('selectTopicWithMouseOver') && treeNode && treeNode.message && treeNode.message.value) {
didSelectTopic()
}
}
const mouseOut = (event: React.MouseEvent) => {
event.stopPropagation()
setHover(false)
}
useEffect(() => {
treeNode.viewModel && treeNode.viewModel.setExpanded(!isCollapsed, false)
}, [isCollapsed])
@@ -157,6 +136,7 @@ function TreeNodeComponent(props: Props) {
lastUpdate={treeNode.lastUpdate}
selectTopicAction={props.selectTopicAction}
settings={settings}
actions={props.actions}
/>
)
}
@@ -167,19 +147,19 @@ function TreeNodeComponent(props: Props) {
? { willChange: 'auto', translateZ: 0, animation: `${animationName} 0.5s` }
: {}
const highlightClass = selected ? classes.selected : isHovering ? classes.hover : ''
const highlightClass = selected ? classes.selected : ''
return (
<div>
<div
key={treeNode.hash()}
className={`${classes.node} ${className} ${highlightClass} ${classes.title}`}
style={animation}
onMouseOver={mouseOver}
onMouseOut={mouseOut}
onMouseEnter={mouseOver}
onFocus={didObtainFocus}
onClick={didClickTitle}
ref={nodeRef as any}
tabIndex={-1}
onKeyDown={deleteTopicCallback}
>
<TreeNodeTitle
toggleCollapsed={toggleCollapsed}
@@ -192,26 +172,7 @@ function TreeNodeComponent(props: Props) {
{renderNodes()}
</div>
)
}, [lastUpdate, treeNode, name, isCollapsed, selected, showUpdateAnimation, isHovering])
}, [lastUpdate, treeNode, name, isCollapsed, selected, theme, showUpdateAnimation])
}
export default withStyles(styles, { withTheme: true })(TreeNodeComponent)
function useAnimationToIndicateTopicUpdate(
lastUpdate: number,
setShowUpdateAnimation: React.Dispatch<React.SetStateAction<boolean>>,
showUpdateAnimation: boolean
) {
useEffect(() => {
if (Date.now() - lastUpdate < 3000) {
setShowUpdateAnimation(true)
}
}, [lastUpdate])
useEffect(() => {
if (showUpdateAnimation) {
const timeout = setTimeout(() => setShowUpdateAnimation(false), 500)
return function cleanup() {
clearTimeout(timeout)
}
}
}, [showUpdateAnimation])
}

View File

@@ -1,5 +1,5 @@
import * as q from '../../../../backend/src/Model'
import React from 'react'
import React, { useCallback } from 'react'
import TreeNode from './TreeNode'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
@@ -7,7 +7,6 @@ import { connect } from 'react-redux'
import { SettingsState } from '../../reducers/Settings'
import { TopicViewModel } from '../../model/TopicViewModel'
import { treeActions } from '../../actions'
import { useGlobalKeyEventHandler } from '../../effects/useGlobalKeyEventHandler'
import { KeyCodes } from '../../utils/KeyCodes'
const MovingAverage = require('moving-average')
@@ -30,16 +29,27 @@ interface State {
lastUpdate: number
}
function ArrowKeyHandler(props: {
action: (direction: 'next' | 'previous') => any
leftAction: () => void
rightAction: () => void
}) {
useGlobalKeyEventHandler(KeyCodes.arrow_down, () => props.action('next'), [props.action])
useGlobalKeyEventHandler(KeyCodes.arrow_up, () => props.action('previous'), [props.action])
useGlobalKeyEventHandler(KeyCodes.arrow_right, props.rightAction, [props.action])
useGlobalKeyEventHandler(KeyCodes.arrow_left, props.leftAction, [props.action])
return <div />
function useArrowKeyEventHandler(actions: typeof treeActions) {
return (event: React.KeyboardEvent) => {
switch (event.keyCode) {
case KeyCodes.arrow_down:
actions.moveSelectionUpOrDownwards('next')
event.preventDefault()
break
case KeyCodes.arrow_up:
actions.moveSelectionUpOrDownwards('previous')
event.preventDefault()
break
case KeyCodes.arrow_left:
actions.moveOutward()
event.preventDefault()
break
case KeyCodes.arrow_right:
actions.moveInward()
event.preventDefault()
break
}
}
}
class TreeComponent extends React.PureComponent<Props, State> {
@@ -52,6 +62,7 @@ class TreeComponent extends React.PureComponent<Props, State> {
this.state = { lastUpdate: 0 }
}
private keyEventHandler = useArrowKeyEventHandler(this.props.actions)
private performanceCallback = (ms: number) => {
average.push(Date.now(), ms)
}
@@ -124,16 +135,12 @@ class TreeComponent extends React.PureComponent<Props, State> {
overflowX: 'hidden',
height: '100%',
width: '100%',
outline: '24px black !important',
paddingBottom: '16px', // avoid conflict with chart panel Resizer
}
return (
<div style={style}>
<ArrowKeyHandler
action={this.props.actions.moveSelectionUpOrDownwards}
leftAction={this.props.actions.moveOutward}
rightAction={this.props.actions.moveInward}
/>
<div style={style} tabIndex={0} onKeyDown={this.keyEventHandler}>
<TreeNode
key={tree.hash()}
isRoot={true}
@@ -142,6 +149,7 @@ class TreeComponent extends React.PureComponent<Props, State> {
collapsed={false}
settings={this.props.settings}
lastUpdate={tree.lastUpdate}
actions={this.props.actions}
selectTopicAction={this.props.actions.selectTopic}
/>
</div>