11import type { ReactElement , MouseEvent } from "react" ;
2- import { memo , useCallback } from "react" ;
2+ import { memo , useCallback , useLayoutEffect , useRef } from "react" ;
33import type { Node , NodeProps } from "@xyflow/react" ;
44import { Handle , Position } from "@xyflow/react" ;
55import cc from "classcat" ;
66import type { TreeNode } from "../tree-description" ;
77import { NodeIcon } from "./NodeIcon" ;
88import "./QueryNode.css" ;
99import { useGraphRenderingStore } from "./store" ;
10+ import { animationStartTime , graphAnimationProgress } from "./animation-timing" ;
1011
1112export type QueryGraphNode = Node < TreeNode , "querynode" > ;
1213
1314function QueryNode ( { data, id} : NodeProps < QueryGraphNode > ) {
1415 const expanded = useGraphRenderingStore ( ( s ) => s . expandedNodes [ id ] ) ;
1516 const toggleNode = useGraphRenderingStore ( ( s ) => s . toggleExpandedNode ) ;
17+ const finishNodeAnimation = useGraphRenderingStore ( ( s ) => s . finishNodeAnimation ) ;
18+ const sizeAnimation = useGraphRenderingStore ( ( s ) => s . nodeSizeAnimations . get ( id ) ) ;
1619 const subtreeExpanded = useGraphRenderingStore ( ( s ) => s . expandedSubtrees [ id ] ) ;
1720 const toggleSubtree = useGraphRenderingStore ( ( s ) => s . toggleExpandedSubtree ) ;
1821
1922 const hasProperties = data . properties ?. size ;
2023 const hasSubtree = data . collapsedChildren && data . collapsedChildren . length > 0 ;
24+ const graphNodeRef = useRef < HTMLDivElement > ( null ) ;
25+ const bodyWrapperRef = useRef < HTMLDivElement > ( null ) ;
26+
27+ const measureTargetDimensions = useCallback ( ( targetExpanded : boolean ) => {
28+ const graphNode = graphNodeRef . current ;
29+ const flowNode = graphNode ?. closest < HTMLElement > ( ".react-flow__node" ) ;
30+ if ( graphNode === null || flowNode === null || flowNode === undefined ) {
31+ return { node : { width : 50 , height : 50 } , body : { width : 0 , height : 0 } } ;
32+ }
33+
34+ const clone = flowNode . cloneNode ( true ) as HTMLElement ;
35+ const clonedGraphNode = clone . querySelector < HTMLElement > ( ".qg-graph-node" ) ;
36+ const clonedBodyWrapper = clone . querySelector < HTMLElement > ( ".qg-graph-node-body-wrapper" ) ;
37+ clone . style . position = "fixed" ;
38+ clone . style . transform = "none" ;
39+ clone . style . visibility = "hidden" ;
40+ clone . style . pointerEvents = "none" ;
41+ clonedGraphNode ?. classList . toggle ( "qg-expanded" , targetExpanded ) ;
42+ clonedGraphNode ?. classList . remove ( "qg-animating" ) ;
43+ clonedBodyWrapper ?. style . removeProperty ( "width" ) ;
44+ clonedBodyWrapper ?. style . removeProperty ( "height" ) ;
45+ flowNode . parentElement ?. append ( clone ) ;
46+ const measurements = {
47+ node : { width : clone . offsetWidth , height : clone . offsetHeight } ,
48+ body : { width : clonedBodyWrapper ?. offsetWidth ?? 0 , height : clonedBodyWrapper ?. offsetHeight ?? 0 } ,
49+ } ;
50+ clone . remove ( ) ;
51+ return measurements ;
52+ } , [ ] ) ;
53+
54+ useLayoutEffect ( ( ) => {
55+ const element = bodyWrapperRef . current ;
56+ if ( element === null ) return ;
57+ if ( sizeAnimation === undefined ) {
58+ element . style . removeProperty ( "width" ) ;
59+ element . style . removeProperty ( "height" ) ;
60+ return ;
61+ }
62+
63+ let animationFrame : number | undefined ;
64+ const step = ( now : number ) => {
65+ const progress = graphAnimationProgress ( sizeAnimation . startedAt , now ) ;
66+ const width = sizeAnimation . from . width + ( sizeAnimation . to . width - sizeAnimation . from . width ) * progress ;
67+ const height = sizeAnimation . from . height + ( sizeAnimation . to . height - sizeAnimation . from . height ) * progress ;
68+ element . style . width = `${ width } px` ;
69+ element . style . height = `${ height } px` ;
70+ if ( progress < 1 ) animationFrame = requestAnimationFrame ( step ) ;
71+ else finishNodeAnimation ( id ) ;
72+ } ;
73+ step ( sizeAnimation . startedAt ) ;
74+ return ( ) => {
75+ if ( animationFrame !== undefined ) cancelAnimationFrame ( animationFrame ) ;
76+ } ;
77+ } , [ finishNodeAnimation , id , sizeAnimation ] ) ;
2178
2279 const onClick = useCallback (
2380 ( e : MouseEvent ) => {
2481 if ( e . shiftKey ) {
25- if ( hasSubtree ) toggleSubtree ( id ) ;
82+ if ( hasSubtree ) toggleSubtree ( id , animationStartTime ( ) ) ;
2683 } else {
27- if ( hasProperties ) toggleNode ( id ) ;
84+ if ( hasProperties ) {
85+ const target = measureTargetDimensions ( ! expanded ) ;
86+ const bodyWrapper = bodyWrapperRef . current ;
87+ const startedAt = animationStartTime ( ) ;
88+ const sizeAnimation =
89+ startedAt === undefined || bodyWrapper === null
90+ ? undefined
91+ : {
92+ from : { width : bodyWrapper . offsetWidth , height : bodyWrapper . offsetHeight } ,
93+ to : target . body ,
94+ startedAt,
95+ } ;
96+ toggleNode ( id , target . node , sizeAnimation ) ;
97+ }
2898 }
2999 e . stopPropagation ( ) ;
30100 } ,
31- [ toggleNode , toggleSubtree , hasProperties , hasSubtree , id ] ,
101+ [ toggleNode , toggleSubtree , hasProperties , hasSubtree , expanded , id , measureTargetDimensions ] ,
32102 ) ;
33103 const onSubtreeHandleClick = useCallback (
34104 ( e : MouseEvent ) => {
35- if ( hasSubtree ) toggleSubtree ( id ) ;
105+ if ( hasSubtree ) toggleSubtree ( id , animationStartTime ( ) ) ;
36106 e . stopPropagation ( ) ;
37107 } ,
38108 [ toggleSubtree , hasSubtree , id ] ,
39109 ) ;
40-
41110 const children = [ ] as ReactElement [ ] ;
42111 for ( const [ key , value ] of ( data . properties || [ ] ) . entries ( ) ) {
43112 children . push (
@@ -51,6 +120,7 @@ function QueryNode({data, id}: NodeProps<QueryGraphNode>) {
51120 "qg-graph-node" ,
52121 {
53122 "qg-expanded" : expanded ,
123+ "qg-animating" : sizeAnimation !== undefined ,
54124 "qg-collapsed" : hasProperties && ! expanded ,
55125 "qg-no-props" : ! hasProperties ,
56126 } ,
@@ -75,15 +145,15 @@ function QueryNode({data, id}: NodeProps<QueryGraphNode>) {
75145 return (
76146 < >
77147 < Handle type = "target" position = { Position . Top } />
78- < div className = { nodeClassName } onClick = { onClick } >
148+ < div ref = { graphNodeRef } className = { nodeClassName } onClick = { onClick } >
79149 < div className = "qg-graph-node-head" >
80150 { colorBar ( data . barsAbove , "above" ) }
81151 < NodeIcon icon = { data . icon } iconColor = { data . iconColor } />
82152 < div className = "qg-graph-node-label" style = { { background : data . nodeColor } } >
83153 { data . name }
84154 </ div >
85155 </ div >
86- < div className = "qg-graph-node-body-wrapper nowheel" >
156+ < div ref = { bodyWrapperRef } className = "qg-graph-node-body-wrapper nowheel" >
87157 < div className = "qg-graph-node-body" > { children } </ div >
88158 </ div >
89159 { colorBar ( data . barsBelow , "below" ) }
0 commit comments