import React, { ReactNode, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; 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 { // eslint-disable-next-line vars-on-top, no-var var GraphViewer: IGraphViewerGlobal; } export type DrawioViewerProps = { 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 = React.memo((props: DrawioViewerProps): JSX.Element => { const { 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'); if (mxgraphs.length > 0) { // This component should have only one '.mxgraph' element const div = mxgraphs[0]; if (div != null) { div.innerHTML = ''; // render diagram with createViewerForElement try { 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 = children instanceof Array ? children .filter(elem => (typeof elem === 'string')) // omit non-string elements (e.g. br element generated by line-breaks option) .join('') : children.toString(); let mxgraphData; try { mxgraphData = generateMxgraphData(code); } catch (err) { setError(err); } return `
`; }, [children]); useEffect(() => { if (mxgraphHtml.length > 0) { onRenderingStart?.(); renderDrawioWithDebounce(); } }, [mxgraphHtml, onRenderingStart, renderDrawioWithDebounce]); useEffect(() => { if (error != null) { onRenderingUpdated?.(null); } }, [error, onRenderingUpdated]); // **************** detect data-mxgraph has rendered **************** useEffect(() => { const container = drawioContainerRef.current; if (container == null) return; const observerCallback = (mutationRecords:MutationRecord[]) => { mutationRecords.forEach((record:MutationRecord) => { const target = record.target as HTMLElement; const mxgraphData = target.dataset.mxgraph; if (mxgraphData != null) { const mxgraph = JSON.parse(mxgraphData); onRenderingUpdated?.(mxgraph.xml); } }); }; const observer = new MutationObserver(observerCallback); observer.observe(container, { childList: true, subtree: true }); return () => { observer.disconnect(); }; }, [onRenderingUpdated]); // ******************************* end ******************************* return (
{/* show error */} { error != null && ( {error.name && {error.name}: } {error.message} ) } { error == null && ( // eslint-disable-next-line react/no-danger
) }
); }); DrawioViewer.displayName = 'DrawioViewer';