DrawioViewer.tsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  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
  67. .filter(elem => (typeof elem === 'string')) // omit non-string elements (e.g. br element generated by line-breaks option)
  68. .join('')
  69. : children.toString();
  70. let mxgraphData;
  71. try {
  72. mxgraphData = generateMxgraphData(code);
  73. }
  74. catch (err) {
  75. setError(err);
  76. }
  77. return `<div class="mxgraph" data-mxgraph="${mxgraphData}"></div>`;
  78. }, [children]);
  79. useEffect(() => {
  80. if (mxgraphHtml.length > 0) {
  81. onRenderingStart?.();
  82. renderDrawioWithDebounce();
  83. }
  84. }, [mxgraphHtml, onRenderingStart, renderDrawioWithDebounce]);
  85. useEffect(() => {
  86. if (error != null) {
  87. onRenderingUpdated?.(null);
  88. }
  89. }, [error, onRenderingUpdated]);
  90. // **************** detect data-mxgraph has rendered ****************
  91. useEffect(() => {
  92. const container = drawioContainerRef.current;
  93. if (container == null) return;
  94. const observerCallback = (mutationRecords:MutationRecord[]) => {
  95. mutationRecords.forEach((record:MutationRecord) => {
  96. const target = record.target as HTMLElement;
  97. const mxgraphData = target.dataset.mxgraph;
  98. if (mxgraphData != null) {
  99. const mxgraph = JSON.parse(mxgraphData);
  100. onRenderingUpdated?.(mxgraph.xml);
  101. }
  102. });
  103. };
  104. const observer = new MutationObserver(observerCallback);
  105. observer.observe(container, { childList: true, subtree: true });
  106. return () => {
  107. observer.disconnect();
  108. };
  109. }, [onRenderingUpdated]);
  110. // ******************************* end *******************************
  111. return (
  112. <div
  113. key={`drawio-viewer-${diagramIndex}`}
  114. ref={drawioContainerRef}
  115. className={`drawio-viewer ${styles['drawio-viewer']} p-2`}
  116. data-begin-line-number-of-markdown={bol}
  117. data-end-line-number-of-markdown={eol}
  118. >
  119. {/* show error */}
  120. { error != null && (
  121. <span className="text-muted"><i className="icon-fw icon-exclamation"></i>
  122. {error.name && <strong>{error.name}: </strong>}
  123. {error.message}
  124. </span>
  125. ) }
  126. { error == null && (
  127. // eslint-disable-next-line react/no-danger
  128. <div dangerouslySetInnerHTML={{ __html: mxgraphHtml }} />
  129. ) }
  130. </div>
  131. );
  132. });
  133. DrawioViewer.displayName = 'DrawioViewer';