@@ -17,6 +17,8 @@ export default class PgNode extends HTMLElement {
1717 @Prop ( ) y : number = 0 ;
1818 @Prop ( ) width : number = 12 ;
1919 @Prop ( ) height : number = 3 ;
20+ // The node type's declared width; nodes cannot be resized below it.
21+ @Prop ( ) minWidth : number = 6 ;
2022 @Prop ( ) itemId : number = 0 ;
2123 @Prop ( ) label : string = '' ;
2224 @Prop ( ) fields : any = [ ] ;
@@ -42,42 +44,15 @@ export default class PgNode extends HTMLElement {
4244 } ,
4345 create : ( $item : any , item ) => {
4446 this . height += $item . height ;
47+ this . #fieldHeights. set ( item . itemKey , $item . height ) ;
4548 $item . addEventListener ( 'input' , ( e : any ) => {
46- this . dispatchEvent ( new CustomEvent ( 'input' , {
47- detail : {
48- type : 'arg' ,
49- id : this . itemId ,
50- key : item . itemKey ,
51- value : e . detail . value ,
52- }
53- } ) ) ;
49+ this . #dispatchArg( 'input' , item . itemKey , e . detail . value ) ;
5450 } ) ;
5551 $item . addEventListener ( 'change' , ( e : any ) => {
56- this . dispatchEvent ( new CustomEvent ( 'change' , {
57- detail : {
58- type : 'arg' ,
59- id : this . itemId ,
60- key : item . itemKey ,
61- value : e . detail . value ,
62- }
63- } ) ) ;
64- // todo: cache height by key and only trigger when height changes
65- // as this is currently bad for performance.
66- if ( Array . isArray ( e . detail . value ) ) {
67- const $outputs = this . $outputs . children ;
68- this . outputs . forEach ( ( output , outputIndex ) => {
69- const $output = $outputs [ outputIndex ] ;
70- const top = this . $node . getBoundingClientRect ( ) . top ;
71- this . dispatchEvent ( new CustomEvent ( 'registernodeoutput' , {
72- detail : {
73- node : this . itemId ,
74- key : output . key ,
75- label : output . label ,
76- offset : $output . getBoundingClientRect ( ) . top - top + 9 ,
77- } ,
78- } ) ) ;
79- } ) ;
80- }
52+ // Editors can grow or shrink on change (e.g. TextArray rows);
53+ // reflow first so change listeners see the final geometry.
54+ this . #syncFieldHeight( item . itemKey , $item ) ;
55+ this . #dispatchArg( 'change' , item . itemKey , e . detail . value ) ;
8156 } ) ;
8257 } ,
8358 } ) ;
@@ -92,23 +67,71 @@ export default class PgNode extends HTMLElement {
9267 this . height += $item . height ;
9368 } ,
9469 connect : ( $item : any , item ) => {
70+ // Measured mid-setup, before render() applies the host styles, so
71+ // the adjust constant differs from the post-layout one below.
9572 const top = this . $node . getBoundingClientRect ( ) . top ;
96- this . dispatchEvent ( new CustomEvent ( 'registernodeoutput' , {
97- detail : {
98- node : this . itemId ,
99- key : item . key ,
100- label : item . label ,
101- offset : $item . getBoundingClientRect ( ) . top - top + 31 ,
102- }
103- } ) ) ;
73+ this . #registerOutputPin( item . key , item . label , $item . getBoundingClientRect ( ) . top - top + 31 ) ;
10474 } ,
10575 } ) ;
76+ this . #intrinsicHeight = this . height ;
10677 if ( requestedHeight > this . height ) {
10778 this . height = requestedHeight ;
10879 }
10980 this . $node . addEventListener ( 'pointerover' , this . #handlePointerOver. bind ( this ) ) ;
11081 }
11182
83+ // Editor heights are cached by field key so a change event only triggers a
84+ // reflow (node height + output pin offsets) when a height actually changed.
85+ #fieldHeights = new Map < string , number > ( ) ;
86+ #intrinsicHeight = 2 ;
87+ #syncFieldHeight( key : string , $item : any ) {
88+ const previous = this . #fieldHeights. get ( key ) ?? 0 ;
89+ if ( $item . height === previous ) return ;
90+ this . #fieldHeights. set ( key , $item . height ) ;
91+ // Auto-sized nodes track their content; manually resized nodes only grow
92+ // when the content no longer fits.
93+ const wasAuto = this . height === this . #intrinsicHeight;
94+ this . #intrinsicHeight += $item . height - previous ;
95+ this . height = wasAuto ? this . #intrinsicHeight : Math . max ( this . height , this . #intrinsicHeight) ;
96+ this . #registerOutputPins( ) ;
97+ }
98+
99+ #dispatchArg( type : 'input' | 'change' , key : string , value : any ) {
100+ this . dispatchEvent ( new CustomEvent ( type , {
101+ detail : {
102+ type : 'arg' ,
103+ id : this . itemId ,
104+ key,
105+ value,
106+ }
107+ } ) ) ;
108+ }
109+
110+ #registerOutputPin( key : string , label : string , offset : number ) {
111+ this . dispatchEvent ( new CustomEvent ( 'registernodeoutput' , {
112+ detail : {
113+ node : this . itemId ,
114+ key,
115+ label,
116+ offset,
117+ }
118+ } ) ) ;
119+ }
120+
121+ // Re-measures every rendered output row. 'then' outputs render no row
122+ // (their pin stays on the header line), so the DOM index advances only
123+ // for outputs that produced an element.
124+ #registerOutputPins( ) {
125+ const top = this . $node . getBoundingClientRect ( ) . top ;
126+ let domIndex = 0 ;
127+ this . outputs . forEach ( ( output : any ) => {
128+ if ( output . key === 'then' ) return ;
129+ const $output = this . $outputs . children [ domIndex ++ ] ;
130+ if ( ! $output ) return ;
131+ this . #registerOutputPin( output . key , output . label , $output . getBoundingClientRect ( ) . top - top + 9 ) ;
132+ } ) ;
133+ }
134+
112135 render ( changes : any ) {
113136 if ( changes . outputs ) {
114137 this . outputs . forEach ( ( { key, label } : any ) => {
@@ -230,7 +253,7 @@ export default class PgNode extends HTMLElement {
230253 }
231254
232255 getMinWidth ( ) {
233- return this . width || 6 ;
256+ return this . minWidth ;
234257 }
235258
236259 focus ( ) {
0 commit comments