11import {
22 BaseBoxShapeUtil ,
33 Editor ,
4+ Geometry2d ,
45 HTMLContainer ,
6+ Rectangle2d ,
57 SvgExportContext ,
68 TLBaseShape ,
9+ TLUnknownShape ,
710 useIsEditing ,
811} from "@tldraw/tldraw" ;
912import { useBoxShadow } from "../use-box-shadow.hook" ;
1013
1114import mermaid from "mermaid" ;
12- import { useEffect , useRef } from "react" ;
15+ import { useEffect , useRef , useState } from "react" ;
1316import { mermaidConfig } from "./mermaid.config" ;
17+ import { SourceStyleProp } from "../style-props" ;
18+ import { T } from "@tldraw/validate" ;
1419
1520mermaid . initialize ( mermaidConfig ) ;
1621
@@ -24,7 +29,9 @@ export type MermaidShape = TLBaseShape<
2429> ;
2530
2631export class MermaidShapeUtil extends BaseBoxShapeUtil < MermaidShape > {
27- static override type = "mermaid" as const ;
32+ static override type = "mermaid" as const satisfies string ;
33+
34+ svgNode : SVGElement | null = null ;
2835
2936 getDefaultProps ( ) : MermaidShape [ "props" ] {
3037 return {
@@ -34,60 +41,113 @@ export class MermaidShapeUtil extends BaseBoxShapeUtil<MermaidShape> {
3441 } ;
3542 }
3643
44+ static override props = {
45+ source : SourceStyleProp ,
46+ w : T . number ,
47+ h : T . number ,
48+ } ;
49+
3750 override canEdit = ( ) => true ;
38- override isAspectRatioLocked = ( _shape : MermaidShape ) => false ;
39- override canResize = ( _shape : MermaidShape ) => false ;
40- override canBind = ( _shape : MermaidShape ) => false ;
51+ override isAspectRatioLocked = ( _shape : TLUnknownShape ) => false ;
52+ override canResize = ( _shape : TLUnknownShape ) => false ;
53+ override canBind = ( _shape : TLUnknownShape ) => true ;
4154 override canUnmount = ( ) => true ;
55+ override canSnap = ( _shape : TLUnknownShape ) => true ;
56+
57+ override getGeometry ( shape : MermaidShape ) : Geometry2d {
58+ return new Rectangle2d ( {
59+ width : shape . props . w ,
60+ height : shape . props . h ,
61+ isFilled : true ,
62+ } ) ;
63+ }
64+
4265 override toSvg (
4366 _shape : MermaidShape ,
4467 _ctx : SvgExportContext ,
4568 ) : SVGElement | Promise < SVGElement > {
46- const g = document . createElementNS ( "http://www.w3.org/2000/svg" , "g" ) ;
47- return g ;
69+ if ( ! this . svgNode )
70+ return document . createElementNS ( "http://www.w3.org/2000/svg" , "g" ) ;
71+
72+ return this . svgNode . cloneNode ( true ) as SVGElement ;
4873 }
4974
5075 constructor ( editor : Editor ) {
5176 super ( editor ) ;
5277 }
5378
5479 override component ( shape : MermaidShape ) {
80+ const renderOnce = useRef ( false ) ;
5581 const diagramRef = useRef < HTMLDivElement > ( null ) ;
5682 const boxShadow = useBoxShadow ( this . editor , shape ) ;
5783 const isEditing = useIsEditing ( shape . id ) ;
5884 const mermaidDivId = `mermaid-${ shape . id . replace ( ":" , "-" ) } ` ;
85+ const [ svg , setSvg ] = useState < null | string > ( null ) ;
5986
6087 const { source } = shape . props ;
6188
89+ // Render mermaid diagram
6290 useEffect ( ( ) => {
6391 ( async ( ) => {
6492 if ( isEditing || ! diagramRef . current ) return ;
65- console . debug ( "rendering mermaid" , source ) ;
66- await mermaid . run ( {
67- nodes : [ diagramRef . current ] ,
68- } ) ;
69- const svg = diagramRef . current . querySelector ( "svg" ) ;
70- if ( ! svg ) return ;
71- this . editor . updateShape ( {
72- id : shape . id ,
73- type : "mermaid" ,
74- props : {
75- w : diagramRef . current . offsetWidth ,
76- h : diagramRef . current . offsetHeight ,
77- } ,
78- } ) ;
93+
94+ // This is a hack to get arround https://github.com/mermaid-js/mermaid/issues/2651
95+ if ( ! renderOnce . current ) {
96+ renderOnce . current = true ;
97+ await mermaid . render ( mermaidDivId , source ) ;
98+ await new Promise ( ( resolve ) => setTimeout ( resolve , 1 ) ) ;
99+ }
100+
101+ const { svg : renderedSvg } = await mermaid . render ( mermaidDivId , source ) ;
102+
103+ setSvg ( renderedSvg ) ;
79104 } ) ( ) ;
80- } , [ source , isEditing , shape . id ] ) ;
105+ } , [ source , isEditing , shape . id , setSvg , mermaidDivId ] ) ;
106+
107+ // Resize bounding box to fit diagram & update svg node ref
108+ useEffect ( ( ) => {
109+ if ( ! diagramRef . current ) return ;
110+
111+ const current = diagramRef . current ;
112+
113+ const onResize = ( ) => {
114+ if (
115+ current . offsetWidth !== shape . props . w ||
116+ current . offsetHeight !== shape . props . h
117+ ) {
118+ this . editor . updateShape ( {
119+ id : shape . id ,
120+ type : shape . type ,
121+ props : {
122+ w : current . offsetWidth ,
123+ h : current . offsetHeight ,
124+ } ,
125+ } ) ;
126+ }
127+
128+ const svgNode = diagramRef . current ?. querySelector ( "svg" ) ;
129+ if ( svgNode ) this . svgNode = svgNode ;
130+ } ;
131+
132+ const observer = new ResizeObserver ( onResize ) ;
133+ observer . observe ( current ) ;
134+ return ( ) => {
135+ observer . unobserve ( current ) ;
136+ observer . disconnect ( ) ;
137+ } ;
138+ } , [ diagramRef , shape . props . w , shape . props . h , shape . id , shape . type ] ) ;
81139
82140 return (
83141 < HTMLContainer
84142 className = "tl-mermaid-container"
85143 id = { shape . id }
86144 style = { { boxShadow } }
87145 >
88- < div ref = { diagramRef } className = "mermaid " id = { mermaidDivId } >
89- { source }
90- </ div >
146+ < div
147+ ref = { diagramRef }
148+ className = "mermaid"
149+ dangerouslySetInnerHTML = { svg ? { __html : svg } : undefined }
150+ > </ div >
91151 </ HTMLContainer >
92152 ) ;
93153 }
0 commit comments