Refactor TreeNodeComponent
This commit is contained in:
@@ -63,52 +63,15 @@ function nextVisibleElementInTree(
|
|||||||
node: q.TreeNode<TopicViewModel>,
|
node: q.TreeNode<TopicViewModel>,
|
||||||
direction: 'next' | 'previous'
|
direction: 'next' | 'previous'
|
||||||
): q.TreeNode<TopicViewModel> | undefined {
|
): q.TreeNode<TopicViewModel> | undefined {
|
||||||
const startNode = (node.sourceEdge && node.sourceEdge.source) || tree
|
if (direction === 'next') {
|
||||||
const nodes = flattenNeighbors(settings, node, startNode)
|
return findNextNodeDownward(settings, node)
|
||||||
const idx = nodes.findIndex(n => n.path() === node.path())
|
} else {
|
||||||
const indexDirection = direction === 'next' ? 1 : -1
|
return findNextNodeUpward(settings, node)
|
||||||
return nodes[idx + indexDirection]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Used to select partial relevant trees, to prevent the whole tree from being flattened */
|
|
||||||
function flattenNeighbors(
|
|
||||||
settings: SettingsState,
|
|
||||||
selected: q.TreeNode<TopicViewModel>,
|
|
||||||
treeNode: q.TreeNode<TopicViewModel>
|
|
||||||
): Array<q.TreeNode<TopicViewModel>> {
|
|
||||||
let candidates: Array<q.TreeNode<TopicViewModel>> = []
|
|
||||||
const nextNode = findNextNodeDownward(settings, selected)
|
|
||||||
|
|
||||||
const neighborsOfSelected = sortedNodes(settings, treeNode)
|
|
||||||
const nodeIdx = neighborsOfSelected.findIndex(n => n.path() === selected.path())
|
|
||||||
const previousNeighbor = neighborsOfSelected[nodeIdx - 1]
|
|
||||||
const parentNode = selected.sourceEdge && selected.sourceEdge.source
|
|
||||||
|
|
||||||
if (previousNeighbor) {
|
|
||||||
candidates = candidates
|
|
||||||
.concat(flattenVisibleTree(settings, previousNeighbor))
|
|
||||||
.concat(flattenVisibleTree(settings, selected))
|
|
||||||
} else if (parentNode) {
|
|
||||||
candidates = candidates.concat(flattenVisibleTree(settings, parentNode))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextNode ? candidates.concat([nextNode]) : candidates
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Not very efficient but easy to implement, complexity should not be an issue here */
|
/** Not very efficient but easy to implement, complexity should not be an issue here */
|
||||||
function flattenVisibleTree(
|
function findNextNodeUpward(
|
||||||
settings: SettingsState,
|
|
||||||
treeNode: q.TreeNode<TopicViewModel>
|
|
||||||
): Array<q.TreeNode<TopicViewModel>> {
|
|
||||||
return [treeNode].concat(
|
|
||||||
sortedNodes(settings, treeNode)
|
|
||||||
.filter(isTreeNodeVisible)
|
|
||||||
.map(node => flattenVisibleTree(settings, node))
|
|
||||||
.reduce((a, b) => a.concat(b), [])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function findNextNodeDownward(
|
|
||||||
settings: SettingsState,
|
settings: SettingsState,
|
||||||
treeNode: q.TreeNode<TopicViewModel>
|
treeNode: q.TreeNode<TopicViewModel>
|
||||||
): q.TreeNode<TopicViewModel> | undefined {
|
): q.TreeNode<TopicViewModel> | undefined {
|
||||||
@@ -117,13 +80,55 @@ function findNextNodeDownward(
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentNodes = sortedNodes(settings, parent)
|
const neighborNodes = sortedNodes(settings, parent)
|
||||||
const nodeIdx = parentNodes.findIndex(n => n.path() === treeNode.path())
|
const nodeIdx = neighborNodes.findIndex(n => n.path() === treeNode.path())
|
||||||
|
if (nodeIdx === 0) {
|
||||||
const nextNode = parentNodes[nodeIdx + 1]
|
return parent
|
||||||
if (nextNode) {
|
}
|
||||||
return nextNode
|
const upwardNeighbor = neighborNodes[nodeIdx - 1]
|
||||||
|
if (upwardNeighbor) {
|
||||||
|
return lastVisibleChild(settings, upwardNeighbor)
|
||||||
} else {
|
} else {
|
||||||
return findNextNodeDownward(settings, parent)
|
return findNextNodeUpward(settings, parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function lastVisibleChild(settings: SettingsState, treeNode: q.TreeNode<TopicViewModel>): q.TreeNode<TopicViewModel> {
|
||||||
|
const nodes = sortedNodes(settings, treeNode).filter(isTreeNodeVisible)
|
||||||
|
if (nodes.length === 0) {
|
||||||
|
return treeNode
|
||||||
|
}
|
||||||
|
return lastVisibleChild(settings, nodes[nodes.length - 1])
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNextNodeDownward(
|
||||||
|
settings: SettingsState,
|
||||||
|
treeNode: q.TreeNode<TopicViewModel>
|
||||||
|
): q.TreeNode<TopicViewModel> | undefined {
|
||||||
|
const children = sortedNodes(settings, treeNode).filter(isTreeNodeVisible)
|
||||||
|
const firstChild = children[0]
|
||||||
|
if (firstChild) {
|
||||||
|
return firstChild
|
||||||
|
}
|
||||||
|
|
||||||
|
return findNextNodeDownwardNeighbor(settings, treeNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNextNodeDownwardNeighbor(
|
||||||
|
settings: SettingsState,
|
||||||
|
treeNode: q.TreeNode<TopicViewModel>
|
||||||
|
): q.TreeNode<TopicViewModel> | undefined {
|
||||||
|
const parent = treeNode.sourceEdge && treeNode.sourceEdge.source
|
||||||
|
if (!parent) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const neighborNodes = sortedNodes(settings, parent).filter(isTreeNodeVisible)
|
||||||
|
const nodeIdx = neighborNodes.findIndex(n => n.path() === treeNode.path())
|
||||||
|
const downwardNeighbor = neighborNodes[nodeIdx + 1]
|
||||||
|
if (downwardNeighbor) {
|
||||||
|
return downwardNeighbor
|
||||||
|
} else {
|
||||||
|
return findNextNodeDownwardNeighbor(settings, parent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ function useIsAllowedToAutoExpandState(props: Props): boolean {
|
|||||||
const [isAllowedToAutoExpand, setAllowAutoExpand] = useState(false)
|
const [isAllowedToAutoExpand, setAllowAutoExpand] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newIsAllowedToAutoExpand = !(treeNode.edgeCount() > settings.get('autoExpandLimit'))
|
const newIsAllowedToAutoExpand = isRoot || treeNode.edgeCount() <= settings.get('autoExpandLimit')
|
||||||
if (!isRoot && newIsAllowedToAutoExpand !== isAllowedToAutoExpand) {
|
if (newIsAllowedToAutoExpand !== isAllowedToAutoExpand) {
|
||||||
setAllowAutoExpand(newIsAllowedToAutoExpand)
|
setAllowAutoExpand(newIsAllowedToAutoExpand)
|
||||||
}
|
}
|
||||||
}, [treeNode.edgeCount(), settings.get('autoExpandLimit')])
|
}, [treeNode.edgeCount(), settings.get('autoExpandLimit')])
|
||||||
@@ -77,45 +77,56 @@ function useIsAllowedToAutoExpandState(props: Props): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function TreeNodeComponent(props: Props) {
|
function TreeNodeComponent(props: Props) {
|
||||||
const { classes, className, settings, theme, treeNode, lastUpdate } = props
|
const { classes, className, settings, theme, treeNode, lastUpdate, name } = props
|
||||||
|
|
||||||
const [animationDirty, setAnimationDirty] = useState(false)
|
const [showUpdateAnimation, setShowUpdateAnimation] = useState(false)
|
||||||
const [collapsed, setCollapsed] = useState<boolean | undefined>(props.collapsed)
|
const [collapsedOverride, setCollapsedOverride] = useState<boolean | undefined>(undefined)
|
||||||
const [cssAnimationWasSetAt, setCssAnimationWasSetAt] = useState(0)
|
|
||||||
const [willUpdateTime, setWillUpdateTime] = useState(performance.now())
|
|
||||||
const [isHovering, setIsHovering] = useState(false)
|
const [isHovering, setIsHovering] = useState(false)
|
||||||
const [selected, setSelected] = useState(false)
|
const [selected, setSelected] = useState(false)
|
||||||
const nodeRef = useRef<HTMLDivElement>()
|
const nodeRef = useRef<HTMLDivElement>()
|
||||||
const isAllowedToAutoExpand = useIsAllowedToAutoExpandState(props)
|
const isAllowedToAutoExpand = useIsAllowedToAutoExpandState(props)
|
||||||
useViewModelSubscriptions(treeNode, nodeRef, setSelected, setCollapsed)
|
useViewModelSubscriptions(treeNode, nodeRef, setSelected, setCollapsedOverride)
|
||||||
|
useAnimationToIndicateTopicUpdate(lastUpdate, setShowUpdateAnimation, showUpdateAnimation)
|
||||||
|
|
||||||
|
const isCollapsed =
|
||||||
|
Boolean(collapsedOverride) === collapsedOverride ? Boolean(collapsedOverride) : !isAllowedToAutoExpand
|
||||||
|
|
||||||
const setHover = debounce((hover: boolean) => {
|
const setHover = debounce((hover: boolean) => {
|
||||||
setIsHovering(hover)
|
setIsHovering(hover)
|
||||||
}, 45)
|
}, 45)
|
||||||
|
|
||||||
const toggle = useCallback(() => {
|
const toggle = useCallback(() => {
|
||||||
setCollapsed(!collapsed)
|
setCollapsedOverride(!isCollapsed)
|
||||||
}, [collapsed])
|
}, [isCollapsed])
|
||||||
|
|
||||||
const didSelectTopic = useCallback(
|
const didSelectTopic = useCallback(
|
||||||
(event?: React.MouseEvent) => {
|
(event?: React.MouseEvent) => {
|
||||||
console.log('Did select', treeNode.path())
|
|
||||||
console.log(event)
|
|
||||||
event && event.stopPropagation()
|
event && event.stopPropagation()
|
||||||
props.selectTopicAction(treeNode)
|
props.selectTopicAction(treeNode)
|
||||||
},
|
},
|
||||||
[treeNode]
|
[treeNode]
|
||||||
)
|
)
|
||||||
|
|
||||||
const didClickTitle = (event: React.MouseEvent) => {
|
const didClickTitle = React.useCallback(
|
||||||
|
(event: React.MouseEvent) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
didSelectTopic()
|
didSelectTopic()
|
||||||
toggle()
|
toggle()
|
||||||
}
|
},
|
||||||
|
[toggle, didSelectTopic]
|
||||||
|
)
|
||||||
|
|
||||||
|
const toggleCollapsed = useCallback(
|
||||||
|
(event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
toggle()
|
||||||
|
},
|
||||||
|
[toggle]
|
||||||
|
)
|
||||||
|
|
||||||
const didObtainFocus = useCallback(() => {
|
const didObtainFocus = useCallback(() => {
|
||||||
didSelectTopic()
|
didSelectTopic()
|
||||||
}, [])
|
}, [didSelectTopic])
|
||||||
|
|
||||||
const mouseOver = (event: React.MouseEvent) => {
|
const mouseOver = (event: React.MouseEvent) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
@@ -130,23 +141,13 @@ function TreeNodeComponent(props: Props) {
|
|||||||
setHover(false)
|
setHover(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleCollapsed = useCallback(
|
|
||||||
(event: React.MouseEvent) => {
|
|
||||||
event.stopPropagation()
|
|
||||||
toggle()
|
|
||||||
},
|
|
||||||
[toggle]
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
treeNode.viewModel && treeNode.viewModel.setExpanded(!collapsed, false)
|
treeNode.viewModel && treeNode.viewModel.setExpanded(!isCollapsed, false)
|
||||||
}, [collapsed])
|
}, [isCollapsed])
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const shouldBeRenderedCollapsed = Boolean(collapsed) === collapsed ? Boolean(collapsed) : !isAllowedToAutoExpand
|
|
||||||
|
|
||||||
function renderNodes() {
|
function renderNodes() {
|
||||||
if (shouldBeRenderedCollapsed) {
|
if (isCollapsed) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,14 +161,13 @@ function TreeNodeComponent(props: Props) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldStartAnimation = settings.get('highlightTopicUpdates')
|
const shouldStartAnimation = settings.get('highlightTopicUpdates') && showUpdateAnimation
|
||||||
const animationName = theme.palette.type === 'light' ? 'updateLight' : 'updateDark'
|
const animationName = 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` }
|
||||||
: {}
|
: {}
|
||||||
|
|
||||||
const highlightClass = selected ? classes.selected : isHovering ? classes.hover : ''
|
const highlightClass = selected ? classes.selected : isHovering ? classes.hover : ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
@@ -176,15 +176,15 @@ function TreeNodeComponent(props: Props) {
|
|||||||
style={animation}
|
style={animation}
|
||||||
onMouseOver={mouseOver}
|
onMouseOver={mouseOver}
|
||||||
onMouseOut={mouseOut}
|
onMouseOut={mouseOut}
|
||||||
onClick={didClickTitle}
|
|
||||||
onFocus={didObtainFocus}
|
onFocus={didObtainFocus}
|
||||||
|
onClick={didClickTitle}
|
||||||
ref={nodeRef as any}
|
ref={nodeRef as any}
|
||||||
tabIndex={1000}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<TreeNodeTitle
|
<TreeNodeTitle
|
||||||
toggleCollapsed={toggleCollapsed}
|
toggleCollapsed={toggleCollapsed}
|
||||||
didSelectNode={didSelectTopic}
|
didSelectNode={didSelectTopic}
|
||||||
collapsed={shouldBeRenderedCollapsed}
|
collapsed={isCollapsed}
|
||||||
treeNode={treeNode}
|
treeNode={treeNode}
|
||||||
name={name}
|
name={name}
|
||||||
/>
|
/>
|
||||||
@@ -192,7 +192,26 @@ function TreeNodeComponent(props: Props) {
|
|||||||
{renderNodes()}
|
{renderNodes()}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}, [lastUpdate, treeNode, collapsed, selected, isAllowedToAutoExpand, isHovering])
|
}, [lastUpdate, treeNode, name, isCollapsed, selected, showUpdateAnimation, isHovering])
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles, { withTheme: true })(TreeNodeComponent)
|
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])
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function useViewModelSubscriptions(
|
|||||||
return function cleanup() {
|
return function cleanup() {
|
||||||
removeSubscriber()
|
removeSubscriber()
|
||||||
}
|
}
|
||||||
})
|
}, [treeNode])
|
||||||
|
|
||||||
function addSubscriber() {
|
function addSubscriber() {
|
||||||
treeNode.viewModel = new TopicViewModel()
|
treeNode.viewModel = new TopicViewModel()
|
||||||
|
|||||||
Reference in New Issue
Block a user