DrawioViewer.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import React, {
  2. ReactNode, useCallback, useEffect, useMemo, useRef, useState,
  3. } from 'react';
  4. import { debounce } from 'throttle-debounce';
  5. import type { IGraphViewerGlobal } from '..';
  6. import { generateMxgraphData } from '../utils/embed';
  7. import { isGraphViewerGlobal } 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: IGraphViewerGlobal;
  12. }
  13. export type DrawioViewerProps = {
  14. diagramIndex: number,
  15. bol: number,
  16. eol: number,
  17. children?: ReactNode,
  18. onRenderingStart?: () => void,
  19. onRenderingUpdated?: (mxfile: string | null) => void,
  20. }
  21. export type DrawioEditByViewerProps = {
  22. bol: number,
  23. eol: number,
  24. drawioMxFile: string,
  25. }
  26. export const DrawioViewer = React.memo((props: DrawioViewerProps): JSX.Element => {
  27. const {
  28. diagramIndex, bol, eol, children,
  29. onRenderingStart, onRenderingUpdated,
  30. } = props;
  31. const drawioContainerRef = useRef<HTMLDivElement>(null);
  32. const [error, setError] = useState<Error>();
  33. const renderDrawio = useCallback(() => {
  34. if (drawioContainerRef.current == null) {
  35. return;
  36. }
  37. if (!('GraphViewer' in window && isGraphViewerGlobal(GraphViewer))) {
  38. // Do nothing if loading has not been terminated.
  39. // Alternatively, GraphViewer.processElements() will be called in Script.onLoad.
  40. // see DrawioViewerScript.tsx
  41. return;
  42. }
  43. const mxgraphs = drawioContainerRef.current.getElementsByClassName('mxgraph');
  44. if (mxgraphs.length > 0) {
  45. // This component should have only one '.mxgraph' element
  46. const div = mxgraphs[0];
  47. if (div != null) {
  48. div.innerHTML = '';
  49. // render diagram with createViewerForElement
  50. try {
  51. GraphViewer.createViewerForElement(div);
  52. }
  53. catch (err) {
  54. setError(err);
  55. }
  56. }
  57. }
  58. }, []);
  59. const renderDrawioWithDebounce = useMemo(() => debounce(200, renderDrawio), [renderDrawio]);
  60. const mxgraphHtml = useMemo(() => {
  61. setError(undefined);
  62. if (children == null) {
  63. return '';
  64. }
  65. const code = children instanceof Array
  66. ? children.map(e => e?.toString()).join('')
  67. : children.toString();
  68. let mxgraphData;
  69. try {
  70. mxgraphData = generateMxgraphData(code);
  71. }
  72. catch (err) {
  73. setError(err);
  74. }
  75. return `<div class="mxgraph" data-mxgraph="${mxgraphData}"></div>`;
  76. }, [children]);
  77. useEffect(() => {
  78. if (mxgraphHtml.length > 0) {
  79. onRenderingStart?.();
  80. renderDrawioWithDebounce();
  81. }
  82. }, [mxgraphHtml, onRenderingStart, renderDrawioWithDebounce]);
  83. useEffect(() => {
  84. if (error != null) {
  85. onRenderingUpdated?.(null);
  86. }
  87. }, [error, onRenderingUpdated]);
  88. // **************** detect data-mxgraph has rendered ****************
  89. useEffect(() => {
  90. const container = drawioContainerRef.current;
  91. if (container == null) return;
  92. const observerCallback = (mutationRecords:MutationRecord[]) => {
  93. mutationRecords.forEach((record:MutationRecord) => {
  94. const target = record.target as HTMLElement;
  95. const mxgraphData = target.dataset.mxgraph;
  96. if (mxgraphData != null) {
  97. const mxgraph = JSON.parse(mxgraphData);
  98. onRenderingUpdated?.(mxgraph.xml);
  99. }
  100. });
  101. };
  102. const observer = new MutationObserver(observerCallback);
  103. observer.observe(container, { childList: true, subtree: true });
  104. return () => {
  105. observer.disconnect();
  106. };
  107. }, [onRenderingUpdated]);
  108. // ******************************* end *******************************
  109. return (
  110. <div
  111. key={`drawio-viewer-${diagramIndex}`}
  112. ref={drawioContainerRef}
  113. className={`drawio-viewer ${styles['drawio-viewer']} p-2`}
  114. data-begin-line-number-of-markdown={bol}
  115. data-end-line-number-of-markdown={eol}
  116. >
  117. {/* show error */}
  118. { error != null && (
  119. <span className="text-muted"><i className="icon-fw icon-exclamation"></i>
  120. {error.name && <strong>{error.name}: </strong>}
  121. {error.message}
  122. </span>
  123. ) }
  124. { error == null && (
  125. // eslint-disable-next-line react/no-danger
  126. <div dangerouslySetInnerHTML={{ __html: mxgraphHtml }} />
  127. ) }
  128. </div>
  129. );
  130. });
  131. DrawioViewer.displayName = 'DrawioViewer';