1
1
import {
2
2
BaseBoxShapeUtil ,
3
3
Editor ,
4
+ Geometry2d ,
4
5
HTMLContainer ,
6
+ Rectangle2d ,
5
7
SvgExportContext ,
6
8
TLBaseShape ,
9
+ TLUnknownShape ,
7
10
useIsEditing ,
8
11
} from "@tldraw/tldraw" ;
9
12
import { useBoxShadow } from "../use-box-shadow.hook" ;
10
13
11
14
import mermaid from "mermaid" ;
12
- import { useEffect , useRef } from "react" ;
15
+ import { useEffect , useRef , useState } from "react" ;
13
16
import { mermaidConfig } from "./mermaid.config" ;
17
+ import { SourceStyleProp } from "../style-props" ;
18
+ import { T } from "@tldraw/validate" ;
14
19
15
20
mermaid . initialize ( mermaidConfig ) ;
16
21
@@ -24,7 +29,9 @@ export type MermaidShape = TLBaseShape<
24
29
> ;
25
30
26
31
export 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 ;
28
35
29
36
getDefaultProps ( ) : MermaidShape [ "props" ] {
30
37
return {
@@ -34,60 +41,119 @@ export class MermaidShapeUtil extends BaseBoxShapeUtil<MermaidShape> {
34
41
} ;
35
42
}
36
43
44
+ static override props = {
45
+ source : SourceStyleProp ,
46
+ w : T . number ,
47
+ h : T . number ,
48
+ } ;
49
+
37
50
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 ;
41
54
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
+
42
65
override toSvg (
43
66
_shape : MermaidShape ,
44
67
_ctx : SvgExportContext ,
45
68
) : 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 ;
48
73
}
49
74
50
75
constructor ( editor : Editor ) {
51
76
super ( editor ) ;
52
77
}
53
78
54
79
override component ( shape : MermaidShape ) {
80
+ const renderOnce = useRef ( false ) ;
55
81
const diagramRef = useRef < HTMLDivElement > ( null ) ;
56
82
const boxShadow = useBoxShadow ( this . editor , shape ) ;
57
83
const isEditing = useIsEditing ( shape . id ) ;
58
84
const mermaidDivId = `mermaid-${ shape . id . replace ( ":" , "-" ) } ` ;
85
+ const [ svg , setSvg ] = useState < null | string > ( null ) ;
59
86
60
87
const { source } = shape . props ;
61
88
89
+ // Render mermaid diagram
62
90
useEffect ( ( ) => {
63
91
( async ( ) => {
64
92
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 : renderedSvg2 } = await mermaid . render (
102
+ mermaidDivId ,
103
+ source ,
104
+ ) ;
105
+
106
+ setSvg ( renderedSvg2 ) ;
107
+
108
+ const svgNode = diagramRef . current . querySelector ( "svg" ) ;
109
+
110
+ if ( ! svgNode ) return ;
111
+
112
+ this . svgNode = svgNode ;
79
113
} ) ( ) ;
80
- } , [ source , isEditing , shape . id ] ) ;
114
+ } , [ source , isEditing , shape . id , setSvg , mermaidDivId ] ) ;
115
+
116
+ // Resize bounding box to fit diagram
117
+ useEffect ( ( ) => {
118
+ if ( ! diagramRef . current ) return ;
119
+
120
+ const current = diagramRef . current ;
121
+
122
+ const onResize = ( ) => {
123
+ if (
124
+ current . offsetWidth !== shape . props . w ||
125
+ current . offsetHeight !== shape . props . h
126
+ ) {
127
+ this . editor . updateShape ( {
128
+ id : shape . id ,
129
+ type : shape . type ,
130
+ props : {
131
+ w : current . offsetWidth ,
132
+ h : current . offsetHeight ,
133
+ } ,
134
+ } ) ;
135
+ }
136
+ } ;
137
+
138
+ const observer = new ResizeObserver ( onResize ) ;
139
+ observer . observe ( current ) ;
140
+ return ( ) => {
141
+ observer . unobserve ( current ) ;
142
+ observer . disconnect ( ) ;
143
+ } ;
144
+ } , [ diagramRef , shape . props . w , shape . props . h , shape . id , shape . type ] ) ;
81
145
82
146
return (
83
147
< HTMLContainer
84
148
className = "tl-mermaid-container"
85
149
id = { shape . id }
86
150
style = { { boxShadow } }
87
151
>
88
- < div ref = { diagramRef } className = "mermaid " id = { mermaidDivId } >
89
- { source }
90
- </ div >
152
+ < div
153
+ ref = { diagramRef }
154
+ className = "mermaid"
155
+ dangerouslySetInnerHTML = { svg ? { __html : svg } : undefined }
156
+ > </ div >
91
157
</ HTMLContainer >
92
158
) ;
93
159
}
0 commit comments