DrawioViewer.tsx 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import React, {
  2. ReactNode, useCallback, useEffect, useMemo, useRef, useState,
  3. } from 'react';
  4. import { debounce } from 'throttle-debounce';
  5. import type { IGraphViewer } from '..';
  6. import { generateMxgraphData } from '../utils/embed';
  7. import { isGraphViewer } from '../utils/global';
  8. import styles from './DrawioViewer.module.scss';
  9. declare global {
  10. // eslint-disable-next-line vars-on-top, no-var
  11. var GraphViewer: IGraphViewer;
  12. }
  13. export type DrawioViewerProps = {
  14. diagramIndex: number,
  15. bol?: number,
  16. eol?: number,
  17. children?: ReactNode,
  18. onRenderingUpdated?: (hasError: boolean) => void,
  19. }
  20. export const DrawioViewer = React.memo((props: DrawioViewerProps): JSX.Element => {
  21. const {
  22. diagramIndex, bol, eol, children,
  23. } = props;
  24. const drawioContainerRef = useRef<HTMLDivElement>(null);
  25. const [error, setError] = useState<Error>();
  26. const renderDrawio = useCallback(() => {
  27. if (drawioContainerRef.current == null) {
  28. return;
  29. }
  30. if (!isGraphViewer(GraphViewer)) {
  31. // Do nothing if loading has not been terminated.
  32. // Alternatively, GraphViewer.processElements() will be called in Script.onLoad.
  33. // see DrawioViewerScript.tsx
  34. return;
  35. }
  36. const mxgraphs = drawioContainerRef.current.getElementsByClassName('mxgraph');
  37. if (mxgraphs.length > 0) {
  38. // This component should have only one '.mxgraph' element
  39. const div = mxgraphs[0];
  40. if (div != null) {
  41. div.innerHTML = '';
  42. try {
  43. GraphViewer.createViewerForElement(div);
  44. }
  45. catch (err) {
  46. setError(err);
  47. }
  48. }
  49. }
  50. }, [drawioContainerRef]);
  51. const renderDrawioWithDebounce = useMemo(() => debounce(200, renderDrawio), [renderDrawio]);
  52. const mxgraphHtml = useMemo(() => {
  53. setError(undefined);
  54. if (children == null) {
  55. return '';
  56. }
  57. const code = children instanceof Array
  58. ? children.map(e => e?.toString()).join('')
  59. : children.toString();
  60. let mxgraphData;
  61. try {
  62. mxgraphData = generateMxgraphData(code);
  63. }
  64. catch (err) {
  65. setError(err);
  66. }
  67. return `<div class="mxgraph" data-mxgraph="${mxgraphData}"></div>`;
  68. }, [children]);
  69. useEffect(() => {
  70. if (mxgraphHtml.length > 0) {
  71. renderDrawioWithDebounce();
  72. }
  73. }, [mxgraphHtml, renderDrawioWithDebounce]);
  74. return (
  75. <div
  76. key={`drawio-viewer-${diagramIndex}`}
  77. ref={drawioContainerRef}
  78. className={`drawio-viewer ${styles['drawio-viewer']} p-2`}
  79. data-begin-line-number-of-markdown={bol}
  80. data-end-line-number-of-markdown={eol}
  81. >
  82. {/* show error */}
  83. { error != null && (
  84. <span className="text-muted"><i className="icon-fw icon-exclamation"></i>
  85. {error.name && <strong>{error.name}: </strong>}
  86. {error.message}
  87. </span>
  88. ) }
  89. { error == null && (
  90. // eslint-disable-next-line react/no-danger
  91. <div dangerouslySetInnerHTML={{ __html: mxgraphHtml }} />
  92. ) }
  93. </div>
  94. );
  95. });
  96. DrawioViewer.displayName = 'DrawioViewer';