1
- // src/components/DocusaurusMermaidFileFallback.js
2
- import React , { useEffect , useState , useRef } from 'react' ;
1
+ import React , { useEffect , useState , useRef , useCallback } from 'react' ;
3
2
import { useColorMode } from '@docusaurus/theme-common' ;
4
3
import Icon from './Icon'
5
4
@@ -13,9 +12,11 @@ export default function DocusaurusMermaidFileFallback({
13
12
const [ chartContent , setChartContent ] = useState ( '' ) ;
14
13
const [ isLoading , setIsLoading ] = useState ( true ) ;
15
14
const [ renderFailed , setRenderFailed ] = useState ( false ) ;
15
+ const [ shouldRender , setShouldRender ] = useState ( false ) ;
16
16
const { colorMode } = useColorMode ( ) ;
17
17
const imgRef = useRef ( null ) ;
18
18
const mermaidContainerRef = useRef ( null ) ;
19
+ const hasRendered = useRef ( false ) ;
19
20
20
21
// Determine which image to display based on the current theme
21
22
const imageToShow = colorMode === 'dark' && fallbackImageDark
@@ -37,10 +38,31 @@ export default function DocusaurusMermaidFileFallback({
37
38
) ;
38
39
} ;
39
40
41
+ // Clean and prepare mermaid content
42
+ const cleanMermaidContent = useCallback ( ( ) => {
43
+ if ( mermaidContainerRef . current ) {
44
+ const mermaidDiv = mermaidContainerRef . current . querySelector ( '.mermaid' ) ;
45
+ if ( mermaidDiv ) {
46
+ // Remove any existing SVG and reset
47
+ mermaidDiv . innerHTML = chartContent ;
48
+ mermaidDiv . removeAttribute ( 'data-processed' ) ;
49
+ // Remove any mermaid-generated attributes
50
+ const attributes = [ ...mermaidDiv . attributes ] ;
51
+ attributes . forEach ( attr => {
52
+ if ( attr . name . startsWith ( 'data-mermaid' ) ) {
53
+ mermaidDiv . removeAttribute ( attr . name ) ;
54
+ }
55
+ } ) ;
56
+ }
57
+ }
58
+ } , [ chartContent ] ) ;
59
+
40
60
// Load chart content from file
41
61
useEffect ( ( ) => {
42
62
if ( chartFile ) {
43
63
setIsLoading ( true ) ;
64
+ setRenderFailed ( false ) ;
65
+ hasRendered . current = false ;
44
66
45
67
fetch ( chartFile )
46
68
. then ( response => {
@@ -53,6 +75,8 @@ export default function DocusaurusMermaidFileFallback({
53
75
console . log ( 'Mermaid file loaded successfully:' , chartFile ) ;
54
76
setChartContent ( content . trim ( ) ) ;
55
77
setIsLoading ( false ) ;
78
+ // Trigger initial render attempt
79
+ setShouldRender ( true ) ;
56
80
} )
57
81
. catch ( error => {
58
82
console . error ( 'Error loading Mermaid chart file:' , error ) ;
@@ -61,58 +85,155 @@ export default function DocusaurusMermaidFileFallback({
61
85
} ) ;
62
86
}
63
87
} , [ chartFile ] ) ;
64
-
65
- // Force Mermaid to use the current theme when it renders
66
- useEffect ( ( ) => {
67
- // When content is loaded or theme changes, we'll need to re-render Mermaid
68
- if ( ! isLoading && chartContent && mermaidContainerRef . current ) {
69
- const renderMermaidWithTheme = async ( ) => {
70
- try {
71
- // Dynamically import mermaid to ensure it's loaded in the browser
72
- const mermaid = ( await import ( 'mermaid' ) ) . default ;
73
-
74
- // Initialize mermaid with the current theme
75
- mermaid . initialize ( {
76
- startOnLoad : false ,
77
- theme : colorMode === 'dark' ? 'dark' : 'default' ,
78
- securityLevel : 'loose' ,
79
- } ) ;
80
-
81
- // Clear previous content
82
- const mermaidDiv = mermaidContainerRef . current . querySelector ( '.mermaid' ) ;
83
- if ( mermaidDiv ) {
84
- mermaidDiv . innerHTML = chartContent ;
85
-
86
- // Run mermaid to render the diagram
87
- await mermaid . run ( {
88
- nodes : [ mermaidDiv ] ,
89
- } ) ;
90
- }
91
- } catch ( error ) {
92
- console . error ( 'Mermaid rendering failed:' , error ) ;
93
- setRenderFailed ( true ) ;
94
- }
95
- } ;
88
+
89
+ // Check if component is in active tab
90
+ const checkIfVisible = useCallback ( ( ) => {
91
+ if ( ! mermaidContainerRef . current ) return false ;
92
+
93
+ // Check if the container is actually visible (not in hidden tab)
94
+ const rect = mermaidContainerRef . current . getBoundingClientRect ( ) ;
95
+ const isInViewport = rect . width > 0 && rect . height > 0 ;
96
+
97
+ // Also check if parent tab is active
98
+ const tabPanel = mermaidContainerRef . current . closest ( '[role="tabpanel"]' ) ;
99
+ const isTabActive = tabPanel ? ! tabPanel . hasAttribute ( 'hidden' ) : true ;
100
+
101
+ return isInViewport && isTabActive ;
102
+ } , [ ] ) ;
103
+
104
+ // Render Mermaid diagram
105
+ const renderMermaidDiagram = useCallback ( async ( ) => {
106
+ if ( ! chartContent || ! mermaidContainerRef . current ) return ;
107
+
108
+ // Don't render if not visible or already rendered
109
+ if ( ! checkIfVisible ( ) ) {
110
+ return ;
111
+ }
112
+
113
+ try {
114
+ console . log ( 'Attempting to render Mermaid diagram...' ) ;
96
115
97
- renderMermaidWithTheme ( ) ;
116
+ // Clean previous content
117
+ cleanMermaidContent ( ) ;
118
+
119
+ // Dynamically import mermaid
120
+ const mermaid = ( await import ( 'mermaid' ) ) . default ;
98
121
99
- // Also set a timeout to check if rendering was successful
122
+ // Initialize mermaid with current theme
123
+ mermaid . initialize ( {
124
+ startOnLoad : false ,
125
+ theme : colorMode === 'dark' ? 'dark' : 'default' ,
126
+ securityLevel : 'loose' ,
127
+ } ) ;
128
+
129
+ const mermaidDiv = mermaidContainerRef . current . querySelector ( '.mermaid' ) ;
130
+ if ( mermaidDiv && chartContent ) {
131
+ // Set content
132
+ mermaidDiv . innerHTML = chartContent ;
133
+
134
+ // Generate unique ID
135
+ const uniqueId = `mermaid-${ chartFile . replace ( / [ ^ a - z A - Z 0 - 9 ] / g, '' ) } -${ Date . now ( ) } ` ;
136
+ mermaidDiv . id = uniqueId ;
137
+
138
+ // Render the diagram
139
+ await mermaid . run ( {
140
+ nodes : [ mermaidDiv ] ,
141
+ } ) ;
142
+
143
+ hasRendered . current = true ;
144
+ console . log ( 'Mermaid diagram rendered successfully' ) ;
145
+ }
146
+ } catch ( error ) {
147
+ console . error ( 'Mermaid rendering failed:' , error ) ;
148
+ setRenderFailed ( true ) ;
149
+ }
150
+ } , [ chartContent , colorMode , cleanMermaidContent , checkIfVisible , chartFile ] ) ;
151
+
152
+ // Listen for tab changes and visibility
153
+ useEffect ( ( ) => {
154
+ if ( ! shouldRender || isLoading || renderFailed ) return ;
155
+
156
+ // Initial render attempt
157
+ const initialRender = ( ) => {
158
+ if ( checkIfVisible ( ) ) {
159
+ renderMermaidDiagram ( ) ;
160
+ }
161
+ } ;
162
+
163
+ // Small delay for initial render
164
+ const initialTimer = setTimeout ( initialRender , 100 ) ;
165
+
166
+ // Listen for tab changes
167
+ const handleTabChange = ( ) => {
168
+ setTimeout ( ( ) => {
169
+ if ( checkIfVisible ( ) && ! hasRendered . current ) {
170
+ hasRendered . current = false ; // Reset for re-render
171
+ renderMermaidDiagram ( ) ;
172
+ }
173
+ } , 50 ) ;
174
+ } ;
175
+
176
+ // Listen for clicks on tabs (Docusaurus tab behavior)
177
+ const tabButtons = document . querySelectorAll ( '[role="tab"]' ) ;
178
+ tabButtons . forEach ( button => {
179
+ button . addEventListener ( 'click' , handleTabChange ) ;
180
+ } ) ;
181
+
182
+ // Also listen for visibility changes
183
+ const observer = new MutationObserver ( ( mutations ) => {
184
+ mutations . forEach ( ( mutation ) => {
185
+ if ( mutation . type === 'attributes' && mutation . attributeName === 'hidden' ) {
186
+ handleTabChange ( ) ;
187
+ }
188
+ } ) ;
189
+ } ) ;
190
+
191
+ const tabPanel = mermaidContainerRef . current ?. closest ( '[role="tabpanel"]' ) ;
192
+ if ( tabPanel ) {
193
+ observer . observe ( tabPanel , { attributes : true } ) ;
194
+ }
195
+
196
+ return ( ) => {
197
+ clearTimeout ( initialTimer ) ;
198
+ tabButtons . forEach ( button => {
199
+ button . removeEventListener ( 'click' , handleTabChange ) ;
200
+ } ) ;
201
+ observer . disconnect ( ) ;
202
+ } ;
203
+ } , [ shouldRender , isLoading , renderFailed , renderMermaidDiagram , checkIfVisible ] ) ;
204
+
205
+ // Re-render when theme changes
206
+ useEffect ( ( ) => {
207
+ if ( ! isLoading && chartContent && shouldRender && ! renderFailed ) {
208
+ hasRendered . current = false ;
209
+ setTimeout ( ( ) => {
210
+ if ( checkIfVisible ( ) ) {
211
+ renderMermaidDiagram ( ) ;
212
+ }
213
+ } , 100 ) ;
214
+ }
215
+ } , [ colorMode , isLoading , chartContent , shouldRender , renderFailed , checkIfVisible , renderMermaidDiagram ] ) ;
216
+
217
+ // Check for rendering success/failure
218
+ useEffect ( ( ) => {
219
+ if ( ! isLoading && chartContent && shouldRender && ! renderFailed ) {
100
220
const timer = setTimeout ( ( ) => {
101
- if ( mermaidContainerRef . current ) {
221
+ if ( mermaidContainerRef . current && checkIfVisible ( ) ) {
102
222
const mermaidDiv = mermaidContainerRef . current . querySelector ( '.mermaid' ) ;
103
- if (
223
+ if ( mermaidDiv && (
104
224
! mermaidDiv . querySelector ( 'svg' ) ||
105
225
mermaidDiv . textContent . includes ( 'Syntax error' ) ||
106
226
mermaidDiv . querySelector ( '.error-icon' )
107
- ) {
227
+ ) ) {
228
+ console . warn ( 'Mermaid rendering appears to have failed, switching to fallback' ) ;
108
229
setRenderFailed ( true ) ;
109
230
}
110
231
}
111
- } , 2000 ) ;
232
+ } , 3000 ) ;
112
233
113
234
return ( ) => clearTimeout ( timer ) ;
114
235
}
115
- } , [ chartContent , isLoading , colorMode ] ) ;
236
+ } , [ isLoading , chartContent , shouldRender , renderFailed , checkIfVisible ] ) ;
116
237
117
238
// Initialize zoom on the fallback image
118
239
useEffect ( ( ) => {
0 commit comments