import { type JSX, memo, type ReactNode, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { GROWI_IS_CONTENT_RENDERING_ATTR } from '@growi/core/dist/consts'; import { debounce } from 'throttle-debounce'; import type { IGraphViewerGlobal } from '..'; import { generateMxgraphData } from '../utils/embed'; import { isGraphViewerGlobal } from '../utils/global'; import styles from './DrawioViewer.module.scss'; declare global { var GraphViewer: IGraphViewerGlobal; } export type DrawioViewerProps = { isDarkMode: 'true' | 'false'; diagramIndex: number; bol: number; eol: number; children?: ReactNode; onRenderingStart?: () => void; onRenderingUpdated?: (mxfile: string | null) => void; }; export type DrawioEditByViewerProps = { bol: number; eol: number; drawioMxFile: string; }; export const DrawioViewer = memo((props: DrawioViewerProps): JSX.Element => { const { isDarkMode, diagramIndex, bol, eol, children, onRenderingStart, onRenderingUpdated, } = props; const drawioContainerRef = useRef(null); const [error, setError] = useState(); const renderDrawio = useCallback(() => { if (drawioContainerRef.current == null) { return; } if (!('GraphViewer' in window && isGraphViewerGlobal(GraphViewer))) { // Do nothing if loading has not been terminated. // Alternatively, GraphViewer.processElements() will be called in Script.onLoad. // see DrawioViewerScript.tsx return; } const mxgraphs = drawioContainerRef.current.getElementsByClassName( 'mxgraph', ) as HTMLCollectionOf; if (mxgraphs.length > 0) { // This component should have only one '.mxgraph' element const div = mxgraphs[0]; if (div != null) { div.innerHTML = ''; div.style.width = ''; div.style.height = ''; // render diagram with createViewerForElement try { GraphViewer.useResizeSensor = false; GraphViewer.prototype.checkVisibleState = false; GraphViewer.prototype.lightboxZIndex = 1055; // set $zindex-modal GraphViewer.prototype.toolbarZIndex = 1055; // set $zindex-modal GraphViewer.createViewerForElement(div); } catch (err) { setError(err); } } } }, []); const renderDrawioWithDebounce = useMemo( () => debounce(200, renderDrawio), [renderDrawio], ); const mxgraphHtml = useMemo(() => { setError(undefined); if (children == null) { return ''; } const code = Array.isArray(children) ? children .filter((elem) => typeof elem === 'string') // omit non-string elements (e.g. br element generated by line-breaks option) .join('') : children.toString(); let mxgraphData: string | undefined; try { mxgraphData = generateMxgraphData(code, isDarkMode === 'true'); } catch (err) { setError(err); } return `
`; }, [children, isDarkMode]); useEffect(() => { if (mxgraphHtml.length > 0) { onRenderingStart?.(); renderDrawioWithDebounce(); } }, [mxgraphHtml, onRenderingStart, renderDrawioWithDebounce]); useEffect(() => { if (error != null) { onRenderingUpdated?.(null); // finish rendering to allow auto-scroll system to detect the upcoming layout shift drawioContainerRef.current?.setAttribute( GROWI_IS_CONTENT_RENDERING_ATTR, 'false', ); } }, [error, onRenderingUpdated]); // **************** detect data-mxgraph has rendered **************** useEffect(() => { const container = drawioContainerRef.current; if (container == null) return; const observerCallback = (mutationRecords: MutationRecord[]) => { for (const record of mutationRecords) { const target = record.target as HTMLElement; const mxgraphData = target.dataset.mxgraph; if (mxgraphData != null) { onRenderingUpdated?.(JSON.parse(mxgraphData).xml); drawioContainerRef.current?.setAttribute( GROWI_IS_CONTENT_RENDERING_ATTR, 'false', ); } } }; const observer = new MutationObserver(observerCallback); observer.observe(container, { childList: true, subtree: true }); return () => { observer.disconnect(); }; }, [onRenderingUpdated]); // ******************************* end ******************************* // ******************* detect container is resized ******************* useEffect(() => { if (drawioContainerRef.current == null) { return; } const observer = new ResizeObserver((entries) => { for (const _entry of entries) { onRenderingStart?.(); // Signal re-rendering in progress so the auto-scroll system can detect the upcoming layout shift drawioContainerRef.current?.setAttribute( GROWI_IS_CONTENT_RENDERING_ATTR, 'true', ); renderDrawioWithDebounce(); } }); observer.observe(drawioContainerRef.current); return () => { observer.disconnect(); }; }, [onRenderingStart, renderDrawioWithDebounce]); // ******************************* end ******************************* return (
{/* show error */} {error != null && ( error {error.name && {error.name}: } {error.message} )} {error == null && ( // biome-ignore lint/security/noDangerouslySetInnerHtml: ignore
)}
); }); DrawioViewer.displayName = 'DrawioViewer';